Commit Graph

51 Commits

Author SHA1 Message Date
roof 37d023659a fix(ui): parse getPeerStatuses dict response correctly in CellNetwork
The /api/wireguard/peers/statuses endpoint returns {pubkey: {online,...}}
not {peers: [{public_key,...}]}. The status mapping loop was always
producing an empty statusByKey, making every connected cell show Offline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 12:25:12 -04:00
roof 56d677e925 fix: copy button HTTP fallback, reset-admin-password in Docker, scripts volume
- CellNetwork.jsx CopyButton: use execCommand fallback when clipboard API
  is unavailable (HTTP non-localhost context)
- Makefile reset-admin-password: run inside cell-api container via docker exec
  so bcrypt and all deps are available without host installation
- docker-compose.yml: mount ./scripts:/app/scripts:ro in cell-api so the
  reset script is accessible inside the container
- scripts/reset_admin_password.py: auto-detect API module path and data dir
  so the script works in both host (api/ sibling) and container (/app) layouts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 11:34:38 -04:00
roof 562d866a65 feat(cells): Phase 3 tests + Phase 4 UI for cell service-sharing
Phase 3 — tests (50 new, total now 1071):
- test_cell_link_manager: atomicity (WG fail → DNS not called, link not
  persisted), DNS warning non-fatal, inbound_services arg, unknown service
  filtered, update/get permissions, lazy migration of legacy entries
- test_wireguard_manager: subnet overlap rejection (exact, supernet, adjacent
  non-overlapping, different class-A, honours wg0.conf configured network)
- test_firewall_manager: _cell_tag sanitisation, apply_cell_rules emits correct
  ACCEPT/DROP per service + catch-all DROP, clear_cell_rules no-op and exact
  line removal, apply_all_cell_rules iterates with correct args
- test_cells_endpoints: RuntimeError→400, GET /services, GET/PUT permissions
  (200/400/404 paths, service name validation, arg forwarding)

Phase 4 — UI:
- CellNetwork.jsx: replace flat cell list with CellPanel expandable cards;
  add ServiceShareToggle (ARIA switch, saves immediately), InboundServiceBadge
  (read-only), DisconnectConfirmModal (replaces window.confirm); relative
  timestamps; paste validation on blur; WireGuard status merged by public_key
- api.js: add cellLinkAPI.getPermissions, updatePermissions, getServices

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 08:45:32 -04:00
roof f3118ff401 fix(vpn): sync WireGuard server key on startup; fix DNS zone cell_name/SOA; fix peer status UI
- API key store was out of sync with wg0.conf: get_keys() generated a random
  phantom key instead of reading the actual WireGuard server key, so all peer
  configs had the wrong PublicKey and could never handshake. Fixed by writing
  correct raw-bytes key files at deploy time and adding _sync_wg_keys() to API
  startup so the store auto-syncs from wg0.conf on every restart.

- apply_domain() fell back silently when zone file had no $ORIGIN directive;
  now also parses the SOA MNAME as the old-domain fallback.

- apply_cell_name() only replaced the hostname if old_name matched literally
  in the zone file; now auto-detects the actual hostname (non-service A record)
  so a stale zone (mycell vs dev) is corrected on next config apply.

- DNS zone file corrected: SOA pic.ngo. admin.pic.ngo., mycell → dev.

- WireGuard UI: add 30s auto-poll for peer statuses; fix "peers currently
  connected" counter to show online/total instead of total count.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 08:05:45 -04:00
roof 9aaacd11cc fix: CSRF regression — grace period for old sessions, GET check-port/refresh-ip, Peers.jsx native fetch tokens
- check_csrf() now issues a token for sessions that predate CSRF (existing logins) instead of blocking them
- /api/wireguard/check-port and /api/wireguard/refresh-ip accept GET so native fetch calls bypass the token requirement
- WireGuard.jsx: changed three native fetch POST → GET for the above endpoints
- Peers.jsx: add X-CSRF-Token header to three native fetch mutation calls (calendar collection, peer PUT, clear-reinstall)
- api.js: export getCsrfToken() so non-Axios callers can read the current token

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 12:18:02 -04:00
roof a43f9fbf0d fix: full security audit remediation — P0/P1/P2/P3 fixes + 1020 passing tests
P0 — Broken functionality:
- Fix 12+ endpoints with wrong manager method signatures (email/calendar/file/routing)
- Fix email_manager.delete_email_user() missing domain arg
- Fix cell-link DNS forwarding wiped on every peer change (generate_corefile now
  accepts cell_links param; add/remove_cell_dns_forward no longer clobber the file)
- Fix Flask SECRET_KEY regenerating on every restart (persisted to DATA_DIR)
- Fix _next_peer_ip exhaustion returning 500 instead of 409
- Fix ConfigManager Caddyfile path (/app/config-caddy/)
- Fix UI double-add and wrong-key peer bugs in Peers.jsx / WireGuard.jsx
- Remove hardcoded credentials from Dashboard.jsx

P1 — Security:
- CSRF token validation on all POST/PUT/DELETE/PATCH to /api/* (double-submit pattern)
- enforce_auth: 503 only when users file readable but empty; never bypass on IOError
- WireGuard add_cell_peer: validate pubkey, name, endpoint against strict regexes
- DNS add_cell_dns_forward: validate IP and domain; reject injection chars
- DNS zone write: realpath containment + record content validation
- iptables comment /32 suffix prevents substring match deleting wrong peer rules
- is_local_request() trusts only loopback + 172.16.0.0/12 (Docker bridge)
- POST /api/containers: volume allow-list prevents arbitrary host mounts
- file_manager: bcrypt ($2b→$2y) for WebDAV; realpath containment in delete_user
- email/calendar: stop persisting plaintext passwords in user records
- routing_manager: validate IPs, networks, and interface names
- peer_registry: write peers.json at mode 0o600
- vault_manager: Fernet key file at mode 0o600
- CORS: lock down to explicit origin list
- domain/cell_name validation: reject newline, brace, semicolon injection chars

P2 — Architecture:
- Peer add: rollback registry entry if firewall rules fail post-add
- restart_service(): base class now calls _restart_container(); email and calendar
  managers call cell-mail / cell-radicale respectively
- email/calendar managers sync user list (no passwords) to cell_config.json
- Pending-restart flag cleared only after helper subprocess exits with code 0
- docker-compose.yml: add config-caddy volume to API container

P3 — Tests (854 → 1020):
- Fill test_email_endpoints.py, test_calendar_endpoints.py,
  test_network_endpoints.py, test_routing_endpoints.py
- New: test_peer_management_update.py, test_peer_management_edge_cases.py,
  test_input_validation.py, test_enforce_auth_configured.py,
  test_cell_link_dns.py, test_logs_endpoints.py, test_cells_endpoints.py,
  test_is_local_request_per_endpoint.py, test_caddy_routing.py
- E2E conftest: skip WireGuard suite when wg-quick absent
- Update existing tests to match fixed signatures and comment formats

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 11:30:21 -04:00
roof 3690c6d955 fix: correct DNS records, peer dashboard field names, and services API response
- network_manager: api/webui DNS records now point to Caddy (172.20.0.2)
  instead of their container IPs so Caddy can reverse-proxy correctly
- ip_utils: add webui.dev block to generated Caddyfile
- config/caddy/Caddyfile: regenerated with webui.dev block
- config/dns/Corefile: simplify to single forward zone (remove duplicate)
- app.py peer_dashboard: rename peer_name→name, rx_bytes→transfer_rx,
  tx_bytes→transfer_tx to match PeerDashboard.jsx; add service_urls dict
- app.py peer_services: fix DNS (10.0.0.1→real CoreDNS IP), CalDAV URL
  (radicale.dev:5232→calendar.dev), email structure (flat→nested smtp/imap
  objects), rename webdav→files, add WireGuard config text, add username field
- PeerDashboard.jsx: render service icon links from service_urls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 17:11:21 -04:00
roof 31a7951ffd fix: 4 issues — admin password sudo, peer modal, WireGuard fetch creds, port check
1. make reset/show-admin-password: use sudo so data/api/ owned-by-root
   files are writable without explicit sudo prefix

2. Peers.jsx: remove one-time password modal on peer creation — admin
   already knows the password they typed; replace with a success toast
   showing peer name and provisioned accounts

3. WireGuard.jsx + Peers.jsx: add credentials:'include' to every raw
   fetch() call (7 calls across two files, plus fix one hardcoded
   localhost:3000 URL); the port check and peer status calls were
   returning 401 because they didn't send the session cookie

4. test_admin_wireguard.py: update test to match new toast flow (no modal),
   add Scenario 10 test that verifies the port check badge renders on the
   WireGuard page after the credentials fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 03:33:11 -04:00
roof 1e81b3b618 Fix webui port binding: restore public access on 8081
The devops security pass incorrectly bound the webui to 127.0.0.1,
making it unreachable from the network. The webui is the user-facing
interface and must be publicly accessible. Internal-only services
(api :3000, radicale :5232, webdav :8080, rainloop :8888,
filegator :8082) retain their loopback bindings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:10:49 -04:00
roof 8650704316 feat: add authentication and authorization system
Backend:
- AuthManager (api/auth_manager.py): server-side user store with bcrypt
  password hashing, account lockout after 5 failed attempts (15 min),
  and atomic file writes
- AuthRoutes (api/auth_routes.py): Blueprint at /api/auth/* — login,
  logout, me, change-password, admin reset-password, list-users
- app.py: register auth_bp blueprint; add enforce_auth before_request
  hook (401 for unauthenticated, 403 for wrong role; only active when
  auth store has users so pre-auth tests remain green); instantiate
  AuthManager; update POST /api/peers to require password >= 10 chars
  and auto-provision email + calendar + files + auth accounts with full
  rollback on any failure; extend DELETE /api/peers to tear down all
  four service accounts; add /api/peer/dashboard and /api/peer/services
  peer-scoped routes; fix is_local_request to also trust the last
  X-Forwarded-For entry appended by the reverse proxy (Caddy)
- Role-based access: admin for /api/* (except /api/auth/* which is
  public and /api/peer/* which is peer-only)
- setup_cell.py: generate and print initial admin password, store in
  .admin_initial_password with 0600 permissions; cleaned up on first
  admin login

Frontend:
- AuthContext.jsx: React context with login/logout/me state and Axios
  interceptor for automatic 401 redirect
- PrivateRoute.jsx: route guard component
- Login.jsx: login page with error handling and must-change-password
  redirect
- AccountSettings.jsx: change-password form for any authenticated user
- PeerDashboard.jsx: peer-role landing page (IP, service list)
- MyServices.jsx: peer service links page
- App.jsx, Sidebar.jsx: AuthContext integration, logout button,
  PrivateRoute wrappers, peer-role routing
- Peers.jsx, WireGuard.jsx, api.js: auth-aware API calls

Tests: 100 new auth tests all pass (test_auth_manager, test_auth_routes,
test_route_protection, test_peer_provisioning). Fix pre-existing test
failures: update WireGuard test keys to valid 44-char base64 format
(test_wireguard_manager, test_peer_wg_integration), add password field
and service manager mocks to test_api_endpoints peer tests, add auth
helpers to conftest.py. Full suite: 845 passed, 0 failures.

Fixed: .admin_initial_password security cleanup on bootstrap, username
minimum length (3 chars enforced by USERNAME_RE regex)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:00:06 -04:00
roof eb817ffdc5 fix: WireGuard sysctl || true, port check on page load, add peer status tests
Root cause: sysctl -q net.ipv4.conf.all.rp_filter=0 in PostUp exited non-zero
inside the linuxserver/wireguard container (no permission), causing wg-quick to
tear down the wg0 interface — breaking peer status, port check, and internet
access through full tunnel.

- wireguard_manager.py: add || true to both sysctl PostUp/PostDown lines
- docker-compose.yml: add net.ipv4.conf.all.rp_filter=0 to wireguard sysctls
- WireGuard.jsx: kick off port check asynchronously on page load (was refresh-only)
- tests: add TestWireGuardSysctlAndPortCheck — 14 new tests covering sysctl
  content, check_port_open (interface up / down / fallback-to-handshake),
  get_peer_status (online / offline / not-found / no-handshake), and
  get_all_peer_statuses (multi-peer / empty / skips interface line)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 10:31:57 -04:00
roof 4b994a5964 feat: domain validation for NTP servers and mail domain fields
- isValidDomain / isValidDomainOrIp helpers (RFC-compliant label regex)
- network.ntp_servers: each entry validated as hostname or IP; invalid entry shown in error message
- email.domain: validated as a proper domain name; blocks autosave until fixed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 09:39:59 -04:00
roof 15e009bd94 feat: fix export/import, add backup download/upload, restore service checkboxes
- export_config: clean output (no internal _keys), identity exposed as 'identity'
- import_config: handle 'identity' key, merge into existing config (not replace)
- restore_config: accept optional services list for selective restore
- backup_config: include 'identity' in manifest services list
- new GET /api/config/backups/<id>/download → zip file download
- new POST /api/config/backup/upload → zip file upload
- webui: Download + Upload buttons, restore modal with per-service checkboxes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 08:51:40 -04:00
roof 2bd6545f0e fix: silent autosave, pending dedup, domain/cell_name pending, containers access
- Settings: remove Save buttons; autosave is silent (no toast on success, error only)
- Settings: loadAll() resets dirty flags to prevent stale autosave after discard
- app.py: fix domain/ip_range "actually changed" check — full identity is always
  sent on save so these were triggering pending on every keystroke regardless
- app.py: _dedup_changes handles port-change format "service field: old → new"
  (split on ':' not ' changed') so dns_port changed twice shows one entry
- app.py: domain + cell_name changes now go through pending restart banner;
  apply_domain/apply_cell_name write files immediately (reload=False) and set
  pending; Discard restores zone files + Caddyfile to pre-change state
- app.py: _set_pending_restart captures pre-change snapshot BEFORE config writes
  (was snapshotting after, making Discard a no-op)
- app.py: is_local_request reads /proc/net/route to allow the actual Docker
  bridge subnet (172.0.0.0/24) which is not RFC-1918; fixes Containers page 403
- container_manager: get_container_logs raises instead of swallowing exceptions
  so nonexistent container returns 500+error not 200+empty

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 07:16:13 -04:00
roof 4215e03ac6 fix: autosave, cell name overflow, length validation, apply-and-verify tests
Autosave on Apply (was broken):
- App.jsx called useDraftConfig() in the same component that rendered
  DraftConfigProvider — a component cannot consume context it provides.
  Fixed by splitting into AppCore (consumes context, all logic) and App
  (thin shell that wraps AppCore in DraftConfigProvider).  The hook now
  runs inside the provider and hasDirty()/flushAll() work correctly.

Cell name / domain length validation (255-char DNS standard):
- api/app.py: reject cell_name or domain > 255 chars or empty with 400
- api/app.py: reject ip_range without CIDR prefix (bare IPs shift all VIPs)
- webui/src/pages/Settings.jsx: cellNameError + domainError computed values
  block saveIdentity and show inline error; maxLength={255} on inputs
- tests/test_identity_validation.py: 8 unit tests for the new validation

Cell name overflow on all pages:
- Dashboard.jsx: add min-w-0 to flex child div + truncate + title on cell_name
- CellNetwork.jsx: min-w-0 + truncate + title on cell_name, domain, endpoint,
  vpn_subnet in invite cards and connected-cells list

Apply-and-verify integration tests:
- tests/integration/test_apply_propagation.py: TestPendingState (no restarts)
  and TestApplyAndVerify (triggers real container restart + health poll)
  covering the full save → apply → wait → verify propagation lifecycle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 05:29:09 -04:00
roof 768571f2b7 feat: port conflict validation, autosave on Apply, extended integration tests
Port conflict validation:
- api/port_registry.py: detect_conflicts() checks all service sections for shared port values
- api/app.py: returns HTTP 409 on port conflict after existing range validation
- webui/src/pages/Settings.jsx: JS-side detectPortConflicts() with useMemo shows inline
  conflict errors and blocks Save before the request is made; catch blocks surface server
  error messages (including 409) instead of generic fallbacks

Config autosave on Apply:
- webui/src/contexts/DraftConfigContext.jsx: new context; Settings registers flush callbacks
  per section; App calls flushAll() before applyPending() when any section is dirty
- webui/src/App.jsx: wraps tree with DraftConfigProvider, handleApply shows 'saving' banner
  state and awaits flushAll()
- webui/src/pages/Settings.jsx: registers identity + per-service flushers; propagates dirty
  state into context via setDirty; uses refs to avoid stale closures

Extended integration test coverage (114 new tests):
- tests/integration/test_config_api.py: GET/PUT config, export, import, backup lifecycle
- tests/integration/test_network_services.py: DNS records + DHCP reservations CRUD
- tests/integration/test_containers.py: list, restart, logs, stats; recovery polling
- tests/integration/test_negative_scenarios.py: error-path coverage for all endpoints
- tests/test_port_conflicts.py: 20 unit tests for port_registry.detect_conflicts()

Pre-commit hook updated to skip tests/integration/ (live-stack tests require a running
stack and must be run explicitly via `make test-integration`).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 04:45:47 -04:00
roof 55bec04603 Add port and IP validation across all service config forms
UI: validateServiceConfig() checks all port fields (1–65535) and
WireGuard address (IP/CIDR) on every keystroke; Save button is
disabled and saveService() guards against any field errors.
API: update_config() rejects out-of-range port values and invalid
WireGuard address before persisting, returning 400 with a clear
field path (e.g. email.smtp_port).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 00:48:20 -04:00
roof 323729e1ab feat: validate ip_range must be within RFC-1918 on save
API: rejects ip_range outside 10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16
with a 400 error before saving to config.

UI: isRFC1918Cidr() validates on every keystroke; error message shown inline
below the field; Save Identity button disabled while the value is invalid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 00:33:30 -04:00
roof 7a273ad43e fix: consolidate WireGuard port config and propagate port changes to UI
- docker-compose: fix WireGuard port mapping to ${WG_PORT}:${WG_PORT} so
  the daemon ListenPort matches the Docker host-to-container binding
- app.py: sync wireguard.port ↔ identity.wireguard_port in both directions
  so changing either keeps them consistent; identity path now also updates
  wg0.conf via wireguard_manager.update_config
- Settings.jsx: remove duplicate wireguard_port from Cell Identity section
  (port is configurable under WireGuard VPN service config); add
  refreshConfig() after saveService so other pages see new values immediately
- WireGuard.jsx: import useConfig() and use service_configs.wireguard.port
  as the reactive port source for endpoint display and port-open warnings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 13:27:35 -04:00
roof 10878543a9 fix: propagate dynamic IPs/ports to service pages; add apply restart feedback
Service pages (Email, Calendar, Files) now read IPs and ports from the
config API instead of hardcoded 172.20.0.x constants:
- GET /api/config now includes service_ips (dns, vip_mail, vip_calendar,
  vip_files, vip_webdav) computed from ip_range via ip_utils
- Email.jsx: mailIp, dnsIp, imapPort, smtpPort, webmailPort from context
- Calendar.jsx: calendarIp, dnsIp, calendarPort from context
- Files.jsx: filesIp, webdavIp, webdavPort, filegatorPort from context

Apply button now shows restart progress:
- "Restarting containers — please wait…" spinner while polling /health
- "Containers restarted successfully" on success (clears after 4s)
- "Timed out" / error message if health doesn't come back in 45s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 12:41:10 -04:00
roof 16609da529 feat(pending-banner): add Discard button to cancel pending restart without applying
- DELETE /api/config/pending endpoint calls _clear_pending_restart()
- cellAPI.cancelPending() calls the new endpoint
- PendingRestartBanner shows a "Discard" button alongside "Apply Now";
  clicking it drops the pending state without restarting any containers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 12:07:39 -04:00
roof 673fe04164 feat(service-ports): remove hardcoded ports from docker-compose, make all service ports configurable
All host port bindings in docker-compose.yml now use \${VAR:-default} substitution,
driven by the .env file generated by ip_utils.write_env_file(). Changing a port in
Settings triggers a per-container pending-restart banner so only the affected container
is restarted on Apply.

- ip_utils: add PORT_DEFAULTS, PORT_ENV_VAR_NAMES, PORT_TO_CONTAINERS; extend
  write_env_file() to accept optional ports dict and write all port env vars
- docker-compose: convert all hardcoded port bindings to \${VAR:-default} form
- app.py: add _collect_service_ports helper; detect port changes in update_config,
  write updated .env and call _set_pending_restart with specific container list;
  update _set_pending_restart to merge/accumulate pending state with containers list;
  update apply_pending_config to use --no-deps <service> for targeted restarts
- config_manager: add submission_port, webmail_port to email schema; add manager_port
  to files schema
- Settings.jsx: make all email/files ports editable, add submission_port, webmail_port,
  manager_port fields; update stale identity note
- tests: 8 new tests for PORT_DEFAULTS, PORT_ENV_VAR_NAMES, and port override in write_env_file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 11:51:10 -04:00
roof c3b2c8d8e5 feat: pending-restart banner + Apply button for config changes
When ip_range changes, a persistent amber banner appears at the top of
every page showing what changed and a "Apply Now" button.  Clicking it
shows a confirmation modal ("containers will restart briefly"), then
calls POST /api/config/apply which runs docker compose up -d from inside
the API container — no manual make start needed.

Backend:
- _set_pending_restart() / _clear_pending_restart() helpers track state
  in config_manager so it survives page refresh
- GET /api/config/pending returns { needs_restart, changed_at, changes }
- POST /api/config/apply runs docker compose up -d via the mounted
  docker.sock, using the project working_dir label to resolve host paths
- docker-compose.yml mounts docker-compose.yml itself read-only into
  the API container so docker compose can read it from inside

Frontend (App.jsx):
- Polls /api/config/pending every 5 s alongside the health check
- PendingRestartBanner component with confirmation modal
- Optimistically clears banner on Apply click; API and containers
  restart in the background

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 11:29:26 -04:00
roof 848f8cfc7c feat: cell-to-cell (PIC mesh) connection feature
Site-to-site WireGuard tunnels between PIC cells with automatic DNS forwarding.
Each cell generates an invite JSON (public key, endpoint, VPN subnet, DNS IP,
domain); the remote cell imports it to establish a bidirectional tunnel and
CoreDNS forwarding block so each cell's domain resolves across the mesh.

Backend:
- CellLinkManager: invite generation, add/remove connections, live WireGuard
  handshake status; stores links in data/cell_links.json
- WireGuardManager: add_cell_peer() accepts subnet CIDRs (not /32) and an
  optional endpoint for site-to-site peers; _read_iface_field() reads port,
  address, and network directly from wg0.conf at runtime instead of constants
- NetworkManager: add/remove CoreDNS forwarding blocks per remote cell domain
- app.py: /api/cells/* routes; _next_peer_ip() derives VPN range from
  configured address so peer allocation follows any address change

Frontend:
- CellNetwork page: invite panel (JSON + QR), connect form (paste JSON),
  connected cells list (green/red status, disconnect button)
- App.jsx: Cell Network nav entry and route

Tests: 25 new tests across test_wireguard_manager, test_network_manager,
test_cell_link_manager (263 total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 08:34:21 -04:00
roof 3912452fd6 fix: wireguard port/subnet/domain propagate to peer configs and new peer IPs
Backend:
- wireguard_manager: _get_configured_port/address/network() read from wg0.conf
  instead of module-level constants; get_split_tunnel_ips() derives VPN network
  from configured Address; get_server_config() returns configured port, dns_ip,
  split_tunnel_ips, vpn_network
- add_peer() and get_peer_config() use configured port (not hardcoded 51820)
- _next_peer_ip() derives subnet from wireguard_manager._get_configured_address()
  so new peers are allocated IPs from the correct VPN range after address change
- refresh-ip and check-port API endpoints return configured port, not 51820
- PUT /api/config: when wireguard port/address changes, all peers are marked
  config_needs_reinstall so users know to re-download tunnel configs
- get_peer_config endpoint: uses configured split tunnel IPs (not hardcoded)

Frontend:
- Peers.jsx: SERVICES domains use live domain from ConfigContext; generateConfig()
  uses serverConf.dns_ip and serverConf.split_tunnel_ips; vpn_network shown in
  peer-access description; DNS hint uses live domain; server config loaded at
  mount time so it is available without re-fetching on every peer action;
  handleUpdatePeer uses /32 for server-side AllowedIPs (was incorrectly using
  full/split tunnel CIDRs which the backend rejects)
- WireGuard.jsx: generateWireGuardConfig() uses serverConfig.dns_ip,
  split_tunnel_ips from server-config API; split-tunnel description shows
  live IPs

Tests: 9 new tests in TestWireGuardConfigReads verify all config reads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 07:47:19 -04:00
roof 5c89687fab fix: Dashboard and NetworkServices use live domain/cell_name from ConfigContext
- Dashboard: SERVICES array (Cell Home, Calendar, Files, Webmail) now builds
  URLs from useConfig() domain + cell_name instead of hardcoded 'mycell.cell'
- NetworkServices: imports useConfig() and cellAPI; shows current DNS zone and
  DHCP range in page header; fetches service_configs.network to display
  configured DHCP range and NTP servers alongside live operational data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 05:39:29 -04:00
roof 1f3386d43b fix: all service pages use live domain; cell_name/domain propagate to DNS; /api/status reads stored identity
Changes:
- ConfigContext.jsx: React context that loads /api/config once; exposes domain,
  cell_name, refresh() — wraps entire app in App.jsx
- Email/Calendar/Files pages: replace hardcoded 'mail.cell', 'calendar.cell',
  'files.cell', 'webdav.cell' with domain from ConfigContext; hostname updates
  immediately after Settings save (refreshConfig() called on save)
- /api/status: cell_name and domain now read from stored _identity in config_manager,
  not hardcoded 'personal-internet-cell' / 'cell.local'
- network_manager.apply_cell_name(old, new): updates hostname A-record in primary
  zone file and reloads CoreDNS; called from PUT /api/config when cell_name changes
- Old identity captured before save so apply_cell_name gets the correct old value
- Settings EmailForm: smtp/imap ports are read-only with note (docker-compose.yml level)
- Settings FilesForm: port is read-only with note (Caddy proxies on 80 externally)
- Settings CalendarForm: port labeled "Internal port; clients use 80 via Caddy"

Tests added:
- test_apply_cell_name_renames_host_record
- test_apply_cell_name_noop_when_same

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 05:05:51 -04:00
roof 87ff50c378 feat: Settings changes now apply to real service config files and restart containers
Each service manager now has apply_config() that writes to the actual config:
- network: dhcp_range → dnsmasq.conf (reload cell-dhcp), ntp_servers → chrony.conf
  (restart cell-ntp), domain → dnsmasq.conf domain= line
- email: domain → mailserver.env OVERRIDE_HOSTNAME + POSTMASTER_ADDRESS,
  restart cell-mail
- wireguard: port/address/private_key → wg0.conf ListenPort/Address/PrivateKey,
  restart cell-wireguard
- calendar: port → radicale config hosts=, restart cell-radicale

PUT /api/config now calls apply_config() after persisting JSON, and returns
{restarted: [...], warnings: [...]} so Settings UI can show which containers
were restarted. _restart_container() helper added to BaseServiceManager.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 04:27:22 -04:00
roof ae73246878 fix: propagate Settings config changes to service managers and live pages
- PUT /api/config now calls service_manager.update_config() for each service
  so changes write to the service's own config file, not just cell_config.json
- email_manager.get_status() now reads smtp_port/imap_port/domain from its
  config file (defaults: 587/993/cell.local) and includes them in the response
- calendar_manager.get_status() includes configured port (default 5232)
- file_manager.get_status() uses configured port from service config
- Email.jsx reads imap_port/smtp_port from API status instead of hardcoding
- Settings service sections show "port changes require container restart" note

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 03:46:31 -04:00
roof c778ee8eb8 feat: fully editable Settings page with service configs, backup/restore, export/import
- Rewrote Settings.jsx: Cell Identity editor, per-service config sections
  (network, wireguard, email, calendar, files, routing, vault) with
  collapsible cards, appropriate input types, and per-section Save buttons
- Added Backup & Restore panel with create/restore/delete actions
- Added Export (download JSON) and Import (upload JSON) panel
- Added PUT /api/config identity field persistence (_identity key in cell_config.json)
  so cell_name/domain/ip_range/wireguard_port survive restarts
- GET /api/config now returns service_configs separately and prefers stored identity
- Added DELETE /api/config/backups/<id> endpoint
- Extended cellAPI in api.js with createBackup, listBackups, restoreBackup,
  deleteBackup, exportConfig, importConfig

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 03:19:46 -04:00
roof 8e1814c7d2 fix: spurious health alerts, show rotated logs, clear history button
app.py:
- Alert logic now checks status.running (container up/down) instead of healthy
  (which requires connectivity tests) — services are only alerted when actually down
- Add POST /api/health/history/clear endpoint to reset history + alert counters

log_manager.py:
- get_all_log_file_infos: include rotated backup files (*.log.1, *.log.2 ...) in listing,
  marked with backup=true so UI can dim them and hide rotate button

api.js: add monitoringAPI.clearHealthHistory

Logs page:
- Health History: add Clear button with confirmation
- File panel: show full filename (including .log.1 backups), explain host path and naming,
  hide rotate button for backup files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 03:05:04 -04:00
roof a5381b2ebc fix: health history all-down — connectivity checks and UI data path
Service manager fixes (connectivity tests):
- email_manager: replace telnet with socket.create_connection for SMTP/IMAP;
  replace nslookup with socket.getaddrinfo for DNS; exclude unconfigured domain
  from success (email healthy=False now correctly means ports refused, not missing domain)
- calendar_manager: replace localhost:5232 with cell-radicale:5232;
  fix database check to test dir writability instead of file existence (files created on demand)
- file_manager: replace localhost:8080 with cell-webdav:80; add top-level success key
- network_manager: replace nslookup with socket.getaddrinfo;
  add success key to dhcp_test and ntp_test return values
- routing_manager: exclude iptables_access from success
  (iptables runs in cell-wireguard, not API container)
- wireguard_manager: add success key to no-arg test_connectivity result

Health history UI:
- SvcCol reads data?.status?.running || data?.status?.status — handles nested health check shape

Result: network/wireguard/calendar/files/routing/vault all healthy=True.
Email healthy=False is correct — mail server needs ≥1 account before Dovecot starts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 02:18:23 -04:00
roof f848a1d056 feat: proper logging architecture — Docker rotation, persisted service logs, verbosity config
docker-compose.yml:
- Add json-file logging driver (max-size: 10m, max-file: 5) to all 13 containers
- Docker now owns container stdout/stderr rotation automatically
- Add ./data/logs:/app/api/data/logs volume to API — service logs now persist across restarts

log_manager.py:
- Remove container log collection hack (Docker handles it natively)
- Add set_service_level(service, level) — change log level at runtime without restart
- Add get_service_levels() — return current per-service levels
- Simplify get_all_log_file_infos to return only service log files

app.py:
- Add GET /api/logs/verbosity — return current per-service log levels
- Add PUT /api/logs/verbosity — update levels at runtime, persist to config/log_levels.json
- Load persisted log level overrides at startup from log_levels.json
- Simplify rotate endpoint (service logs only, container logs owned by Docker)

wireguard_manager.py:
- get_keys(): return empty strings if key files don't exist (prevents get_status crash
  when wg0.conf is missing at startup and falls through to generate_config)

Logs page (4 tabs):
- API Service Logs: structured JSON logs from Python managers, with search/filter/rotate panel
- Container Logs: live docker logs (read via existing /api/containers/<name>/logs endpoint)
- Verbosity Config: per-service level dropdowns, apply immediately + persist
- Health History: existing health poll table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 02:07:57 -04:00
roof 7b39331417 feat: persistent container log collection, unified rotation, logs page redesign
- log_manager: add collect_container_logs (appends docker logs to container_<name>.log),
  get_container_log_lines, rotate_container_log, get_all_log_file_infos
- app.py: new endpoints /api/logs/files (all log file sizes), /api/logs/containers/<name>
  (collect+return stored container logs); rotate endpoint now handles both service and container logs
- Logs page: split into API Service Logs tab (python manager logs) and Container Logs tab
  (persistent docker stdout/stderr); Statistics tab shows both kinds with per-row rotate;
  each tab has a description explaining what it shows and where files live
- wireguard_manager: test_connectivity peer_ip=None guard (already in previous commit, now rebuilt)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 01:54:33 -04:00
roof 50f2200b45 fix: wireguard health_check error + logs page ALL service + rotate confirmation
- WireGuardManager.test_connectivity: make peer_ip optional so health_check
  can call it without args (was logging ERROR on every health poll)
- Logs page: add ALL option to service selector (uses search across all services)
- Logs page: show service tag on each log line when in ALL/search mode
- Logs page: require window.confirm before rotating logs to prevent accidental data loss

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 01:47:41 -04:00
roof 67ddc97795 feat: overhaul Logs page with search, container logs, statistics, and rotation
- Added 4-tab layout: Service Logs, Container Logs, Statistics & Rotation, Health History
- Service Logs: service/level/line-count selector, keyword search, auto-refresh (5s)
- Container Logs: container picker, tail lines selector, auto-refresh
- Statistics & Rotation: per-service file size, entry/error/warning counts, per-service and bulk rotate buttons
- Added logsAPI in api.js: getServiceLogs, searchLogs, exportLogs, getStatistics, rotateLogs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 01:38:08 -04:00
roof 1a5da3a207 docs(ui): clarify rule source separation on routing tabs
Added info banners on Firewall, Peer Routes, and Live iptables tabs
explaining that stored rules (NAT/Firewall/Peer Routes forms) and live
rules (pic-peer-* from Peers page, PostUp from wg0.conf) are separate
by design — Live iptables shows everything, each form tab shows only
what it manages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 01:32:24 -04:00
roof 901094f60a feat: routing page — port forwarding tab, live iptables, diagnostics, firewall delete
Backend:
- routing_manager.remove_firewall_rule(): remove stored rule + iptables -D
- routing_manager.get_live_iptables(): dump filter/nat tables from cell-wireguard
- DELETE /api/routing/firewall/<rule_id> endpoint (was missing)
- GET /api/routing/live-iptables endpoint

Frontend Routing.jsx — 7 tabs:
- Overview: proper routing table with destination/gateway/interface columns
- Port Forwarding: clean DNAT form (protocol, ext port → internal IP:port)
- NAT Rules: MASQUERADE/SNAT only, cleaner layout
- Peer Routes: IP route entries through VPN peers
- Firewall: custom rules with working delete button
- Live iptables: read-only terminal view of actual running rules in cell-wireguard
- Diagnostics: ping + traceroute test from server with output display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 01:14:49 -04:00
roof 53c7661812 feat: per-peer access enforcement, live peer status, auto IP assignment
Server-side access control:
- firewall_manager.py: per-peer iptables FORWARD rules in WireGuard container;
  virtual IPs on Caddy (172.20.0.21-24) for per-service DROP/ACCEPT targeting
- CoreDNS Corefile regenerated with ACL blocks for blocked services per peer
- POST /api/wireguard/apply-enforcement re-applies rules after WireGuard restart;
  wg0.conf PostUp calls it via curl so rules restore automatically on container start

WireGuard fixes:
- _syncconf uses `wg set peer` instead of `wg syncconf` to avoid resetting ListenPort
- add_peer validates AllowedIPs must be /32 — rejects full/split tunnel CIDRs that
  would route internet or LAN traffic to that peer
- _config_file() checks for linuxserver wg_confs/ subdirectory first

UI:
- Peers page fetches /api/wireguard/peers/statuses for live handshake data;
  status badge now shows real Online/Offline + seconds since last handshake
- IP field removed from Add Peer form (auto-assigned from 10.0.0.0/24)

Tests (246 pass):
- test_firewall_manager.py: 22 tests for ACL generation, iptables rule correctness,
  comment tagging, clear_peer_rules filter logic
- test_peer_wg_integration.py: 10 tests for /32 enforcement, IP auto-assignment,
  syncconf called on add/remove
- test_wireguard_manager.py: updated to reflect correct IPs and /32 requirement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 01:01:07 -04:00
roof 8e41568964 feat: peer access config, DNS fix, real routing table, reinstall notifications
Peer creation/edit form now configures:
- Tunnel mode: full (0.0.0.0/0) or split (PIC only)
- Per-service access toggles (calendar, files, mail, webdav)
- Peer-to-peer communication toggle
- Optional calendar account creation
- Access capability badges in peer list

Bug fixes:
- DNS in client configs was 8.8.8.8 / 172.20.0.2 — now 172.20.0.3 (CoreDNS)
  This was why .cell domains didn't resolve on connected VPN peers
- get_peer_config API uses stored internet_access to set AllowedIPs
- New PUT /api/peers/<name> endpoint with config_changed detection
- POST /api/peers/<name>/clear-reinstall clears reinstall flag after download
- Routing page reads real host routes via /proc/1/net/route (pid: host)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 15:40:19 -04:00
roof 9d7d74f3f4 fix: full-tunnel default, real host routing table, peer config tunnel mode
- WireGuard default changed to full tunnel (0.0.0.0/0) — all peer traffic
  routes through PIC server so internet latency matches server's clean 41ms
- UI tunnel toggle now defaults to Full tunnel
- API /peers/config accepts allowed_ips param so UI toggle wires through
- Routing page reads real host routes via /proc/1/net/route (pid: host)
  instead of mock data; shows ens18/192.168.31.1 correctly
- Add iproute2 + util-linux to API Dockerfile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 15:20:55 -04:00
roof 0b5a5b23e8 fix: split-tunnel default for peers, port check via wg interface, tunnel mode toggle in UI
- check_port_open now checks if wg0 interface is actually listening (via
  'wg show wg0') instead of requiring a live peer handshake. This means
  the port shows 'Open' whenever WireGuard is running, not only when a
  peer has connected recently.

- get_peer_config defaults to split-tunnel AllowedIPs (10.0.0.0/24,
  172.20.0.0/16) so VPN clients only route cell service traffic through
  the tunnel. Local LAN traffic (192.168.x.x etc.) stays direct, fixing
  the 60-120ms penalty when pinging local hosts while on VPN.

- Peer config modal now uses cell DNS (172.20.0.2) so .cell domains
  resolve correctly with both split and full tunnel.

- Added split/full tunnel toggle in the peer config modal so users can
  download either config variant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 14:18:43 -04:00
roof e79ee08c63 fix: WireGuard routing, DNS, service access, and UI improvements
- Fix CoreDNS not loading .cell zones (wrong Corefile path, now uses -conf flag)
- Fix WireGuard server address conflict (172.20.0.1/16 overlapped with Docker
  network; changed to 10.0.0.1/24 to eliminate duplicate routes)
- Add SERVERMODE=true and sysctls to WireGuard docker-compose for server mode
- Fix DNS zone file parser to handle 4-field records (name IN type value)
- Add get_dns_records() to NetworkManager; mount data/dns into API container
- Fix peer config endpoint: look up IP/key from registry, use real endpoint
- Add bulk peer statuses endpoint keyed by public_key
- Normalize snake_case API fields to camelCase in WireGuard UI
- Add port check endpoint (checks via live handshake, not unreliable TCP probe)
- Add Caddy virtual hosts for ui/calendar/files/mail .cell domains (HTTP only)
- Fix cell config domain default from cell.local to cell
- Fix Routing Network Config tab (was calling hardcoded localhost:3000)
- Fix DNS records display (record.value not record.ip)
- Move service access guide to top of Dashboard with login hints
- Add /api/routing/setup endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 12:43:23 -04:00
roof bd67764bf4 feat: external IP detection, port status, fix peer config generation
- WireGuardManager: get_external_ip() (cached 1h), check_port_open(),
  get_server_config() returning public_key + detected endpoint
- API: /api/wireguard/server-config returns real external IP;
  /api/wireguard/refresh-ip forces re-detection;
  /api/wireguard/peers/config now looks up peer IP + private key from
  registry and uses real server endpoint automatically
- Fix doubled port in Endpoint (178.x:51820:51820 → 178.x:51820)
- Fix Address=/32 when peer_ip already has mask
- WebUI nginx: proxy /api/ and /health to cell-api (fixes localhost:3000
  hardcode — UI now works from any machine)
- api.js: baseURL='' so all calls go through nginx proxy
- WireGuard page: show Server Endpoint card with external IP, endpoint,
  public key, and Refresh IP button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 02:41:50 -04:00
Cloud bb6ccfe023 wip: wireguard 2025-09-14 03:31:14 -05:00
Cloud 5bd7443681 wip: peer make work with qr code 2025-09-13 12:08:28 -05:00
Constantin 4f65f95ac9 wip: peers 2025-09-13 18:56:00 +03:00
Constantin 36776353b9 wip: Fix ContainerDashboard 2025-09-13 15:49:32 +03:00
Constantin b40e4f277e fix uptime on dashboard 2025-09-13 14:42:44 +03:00
Constantin f0b6d1cff1 wip: make work Services Status 2025-09-13 14:23:31 +03:00