fix: DNS split-horizon in DDNS mode, service access filter, health check, verbosity persistence
Unit Tests / test (push) Successful in 7m32s

- DNS (critical): add _configured_dns_params() that returns (primary_domain,
  split_horizon_zones) from config_manager so all apply_all_dns_rules() callers
  pass the correct primary zone (e.g. 'pic.ngo') and split-horizon list
  (e.g. ['pic1.pic.ngo']) instead of the FQDN as the primary — fixes
  DNS_PROBE_FINISHED_BAD_CONFIG for all external domains when on VPN

- firewall_manager: add split_horizon_zones param to apply_all_dns_rules()
  and forward it to generate_corefile()

- Peers: filter service_access list to installed services only; peers.py
  derives valid services from config_manager.get_installed_services() with
  the email→mail ID mapping; Peers.jsx fetches from /api/store/installed
  and filters the checkboxes and defaults accordingly

- Health check: fix file_manager→'files' ID mapping so files service health
  is checked when installed (was silently skipped due to 'file' vs 'files')

- Verbosity persistence: move log_levels.json from non-mounted
  /app/api/config/ to CONFIG_DIR (/app/config/) which maps to config/api/
  on the host; both load (managers.py) and save (routes/services.py) updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 13:05:58 -04:00
parent 4ebcb1d077
commit c696ca9ef6
10 changed files with 83 additions and 46 deletions
+25 -12
View File
@@ -1,18 +1,18 @@
import { useState, useEffect } from 'react';
import { Plus, Trash2, Edit, Eye, Shield, Copy, Download, Key, AlertTriangle, CheckCircle, Globe, Lock, Users, Server } from 'lucide-react';
import { peerRegistryAPI, wireguardAPI, cellLinkAPI, getCsrfToken } from '../services/api';
import { peerRegistryAPI, wireguardAPI, cellLinkAPI, storeAPI, getCsrfToken } from '../services/api';
import { useConfig } from '../contexts/ConfigContext';
import QRCode from 'qrcode';
const FULL_TUNNEL_IPS = '0.0.0.0/0, ::/0';
const emptyForm = () => ({
const emptyForm = (availableServiceKeys = ['webdav']) => ({
name: '',
description: '',
public_key: '',
persistent_keepalive: 25,
internet_access: true,
service_access: ['calendar', 'files', 'mail', 'webdav'],
service_access: availableServiceKeys,
peer_access: true,
create_calendar: false,
password: '',
@@ -52,14 +52,20 @@ function Toggle({ checked, onChange, label, description }) {
);
}
const STORE_ID_TO_ACCESS = { email: 'mail', calendar: 'calendar', files: 'files' };
const ALL_SERVICES = [
{ key: 'calendar', label: 'Calendar' },
{ key: 'files', label: 'Files' },
{ key: 'mail', label: 'Webmail' },
{ key: 'webdav', label: 'WebDAV' },
];
function Peers() {
const { domain = 'cell' } = useConfig();
const SERVICES = [
{ key: 'calendar', label: 'Calendar', domain: `calendar.${domain}` },
{ key: 'files', label: 'Files', domain: `files.${domain}` },
{ key: 'mail', label: 'Webmail', domain: `mail.${domain}` },
{ key: 'webdav', label: 'WebDAV', domain: `webdav.${domain}` },
];
const [installedServiceKeys, setInstalledServiceKeys] = useState(['webdav']);
const SERVICES = ALL_SERVICES
.filter(s => installedServiceKeys.includes(s.key))
.map(s => ({ ...s, domain: `${s.key}.${domain}` }));
const [peers, setPeers] = useState([]);
const [connectedCells, setConnectedCells] = useState([]);
@@ -69,7 +75,7 @@ function Peers() {
const [showEditModal, setShowEditModal] = useState(false);
const [showViewModal, setShowViewModal] = useState(false);
const [selectedPeer, setSelectedPeer] = useState(null);
const [formData, setFormData] = useState(emptyForm());
const [formData, setFormData] = useState(emptyForm(installedServiceKeys));
const [showAdvanced, setShowAdvanced] = useState(false);
const [peerConfig, setPeerConfig] = useState('');
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
@@ -81,6 +87,13 @@ function Peers() {
useEffect(() => {
fetchPeers();
cellLinkAPI.listConnections().then(r => setConnectedCells(r.data || [])).catch(() => {});
storeAPI.listInstalled().then(r => {
const installed = r.data?.installed || {};
const keys = ['webdav', ...Object.keys(installed)
.map(id => STORE_ID_TO_ACCESS[id])
.filter(Boolean)];
setInstalledServiceKeys(keys);
}).catch(() => {});
}, []);
const showToast = (msg, type = 'success') => {
@@ -212,7 +225,7 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
? Object.entries(provisioned).filter(([, v]) => v).map(([k]) => k).join(', ')
: '';
setShowAddModal(false);
setFormData(emptyForm());
setFormData(emptyForm(installedServiceKeys));
setErrors({});
fetchPeers();
showToast(
@@ -471,7 +484,7 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
<h1 className="text-2xl font-bold text-gray-900">Peers</h1>
<p className="mt-1 text-gray-600">Manage VPN peer connections and access policies</p>
</div>
<button onClick={() => { setFormData(emptyForm()); setErrors({}); setShowAdvanced(false); setShowAddModal(true); }}
<button onClick={() => { setFormData(emptyForm(installedServiceKeys)); setErrors({}); setShowAdvanced(false); setShowAddModal(true); }}
className="btn btn-primary flex items-center">
<Plus className="h-4 w-4 mr-2" />Add Peer
</button>