From 262222303386abacc63c727f711ca4a461155200 Mon Sep 17 00:00:00 2001 From: naturallaw777 <99053422+naturallaw777@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:45:36 +0000 Subject: [PATCH 1/2] Unify Njal.la + router port-forwarding guidance across onboarding and feature enable modals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single source of truth (static/js/domain-prereqs.js) for the domain prerequisite instructions so all three surfaces read identically: - Server + Desktop first-boot onboarding wizard (step 3) - Lightning Wallet Connections (NWC) enable modal (Node-only mode) - BTCPay Server (web) enable modal (Node-only mode) Both feature-enable flows share openDomainSetupModal(), which now drops its role-branched intro and 'Option A/B' blocks in favor of the shared renderers. All surfaces now consistently cover: 1. A domain from Njal.la (account, subdomain-vs-separate-domain, Dynamic record with host-part-only Name field, auto-filled IP, DDNS curl command) 2. Router access — forward ports 80 & 443 (TCP) to this computer's internal IP, once, for HTTPS/SSL (with CGNAT note) 3. How to get Njal.la working, step by step The reconfigure/troubleshooting modal gains the same router reminder, since 'domain not reachable' is often the port forwarding rather than DNS. domain-prereqs.js is loaded via asset_version cache busting and ships automatically (installPhase copies the package wholesale). Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .../static/js/domain-prereqs.js | 146 ++++++++++++++++++ .../static/js/features.js | 50 ++---- app/sovran_systemsos_web/static/onboarding.js | 40 ++--- app/sovran_systemsos_web/templates/index.html | 1 + .../templates/onboarding.html | 6 +- 5 files changed, 178 insertions(+), 65 deletions(-) create mode 100644 app/sovran_systemsos_web/static/js/domain-prereqs.js diff --git a/app/sovran_systemsos_web/static/js/domain-prereqs.js b/app/sovran_systemsos_web/static/js/domain-prereqs.js new file mode 100644 index 0000000..436d504 --- /dev/null +++ b/app/sovran_systemsos_web/static/js/domain-prereqs.js @@ -0,0 +1,146 @@ +/* Sovran_SystemsOS Hub — Shared domain-prerequisite instructions. + + SINGLE SOURCE OF TRUTH for the "you need a Njal.la domain + router port + forwarding" guidance that must read identically everywhere it appears: + + • First-boot onboarding wizard (Server + Desktop role) + — onboarding.js step 3 + • Feature-enable domain modal (Node role, and any role) + — features.js openDomainSetupModal() + (Lightning Wallet Connections / NWC, BTCPay Server, Haven, …) + • Domain reconfigure / troubleshooting modal + — features.js openDomainReconfigureModal() + + Keep these three surfaces word-for-word consistent: always edit this + file, never fork the wording inline. Plain classic script (no modules) — + both templates load it with a plain + diff --git a/app/sovran_systemsos_web/templates/onboarding.html b/app/sovran_systemsos_web/templates/onboarding.html index 2048436..e009ad7 100644 --- a/app/sovran_systemsos_web/templates/onboarding.html +++ b/app/sovran_systemsos_web/templates/onboarding.html @@ -125,9 +125,8 @@ 🌐

Domain Configuration

- Sovran_SystemsOS uses Njal.la for domains and Dynamic DNS. - Create an account at Njal.la, then for each service below, add a Dynamic record — no IP needed, it auto-populates once the DDNS curl command runs. - Paste the curl command from your Njal.la dashboard for each service. + Sovran_SystemsOS uses Njal.la for domains and Dynamic DNS, and your router needs ports 80 and 443 (TCP) forwarded to this computer. + Everything you need — Njal.la account, Dynamic records, and the one router task — is laid out step by step below.

@@ -170,6 +169,7 @@
+ From 2015a3ecd4ed101e7d5dee7b84b857044735301e Mon Sep 17 00:00:00 2001 From: naturallaw777 <99053422+naturallaw777@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:50:32 +0000 Subject: [PATCH 2/2] Reload Caddy immediately when a domain (or ACME email) is saved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Caddyfile is generated at runtime by caddy-generate-config.service from /var/lib/domains/*, but the generator only re-runs when caddy.service starts fresh. Saving a domain while Caddy is already running therefore never seats the new virtual host — no proxying and no ACME cert — and the Hub's reachability check shows a misleading 'ports 80/443' router error until the next reboot or rebuild. api_domains_set and api_domains_set_email now restart the generator and reload Caddy (ExecReload: caddy reload --force, no dropped connections) right after saving. Entirely skipped when Caddy is inactive — e.g. Node role before its first domain-based service is enabled — because the rebuild that enables the service starts caddy.service for the first time, runs the generator first (requiredBy), and seats the already-saved domain on its own. Best-effort throughout: a domain save never fails because of a Caddy reload issue. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- app/sovran_systemsos_web/server.py | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index 0102362..1453d27 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -86,6 +86,12 @@ NJALLA_SCRIPT = "/var/lib/njalla/njalla.sh" # Systemd service that rewrites the Sovran-managed /etc/hosts loopback block SOVRAN_HOSTS_SERVICE = "sovran-hosts-update.service" +# Caddy and its runtime Caddyfile generator (see modules/core/caddy.nix). +# caddy-generate-config.service rewrites /run/caddy/Caddyfile from +# /var/lib/domains/*; caddy.service serves that generated config. +CADDY_GENERATE_UNIT = "caddy-generate-config.service" +CADDY_UNIT = "caddy.service" + # Domain keys that produce a public HTTPS virtual host via Caddy _SERVICE_DOMAIN_KEYS = frozenset([ "matrix", "wordpress", "nextcloud", "btcpayserver", @@ -4350,6 +4356,33 @@ def _run_njalla_ddns() -> None: pass +def _reload_caddy_for_domain_change() -> None: + """Regenerate Caddy's runtime Caddyfile and reload it (best-effort). + + The Caddyfile is generated at runtime by caddy-generate-config.service + from /var/lib/domains/* (see modules/core/caddy.nix). The generator only + runs before caddy.service starts — nothing re-runs it while Caddy is up. + So when a domain is saved while Caddy is already running, the new virtual + host never gets seated: no proxying and no ACME certificate, and the + Hub's reachability check wrongly reports a "ports 80/443" error until + the next reboot or a rebuild that happens to start Caddy fresh. Restart + the generator, then reload Caddy so the change takes effect immediately. + + Entirely skipped when Caddy is not active — e.g. the Node role before + its first domain-based service is enabled. In that case the rebuild + that enables the service starts caddy.service for the first time, which + runs the generator first (requiredBy caddy.service) and seats the + already-saved domain on its own. + """ + try: + if sysctl.is_active(CADDY_UNIT) != "active": + return + sysctl.run_action("restart", CADDY_GENERATE_UNIT) + sysctl.run_action("reload", CADDY_UNIT) + except Exception: + pass + + # Hostname characters: letters, digits, hyphens only within labels; dots separate labels. # Each label must start and end with a letter or digit; no consecutive dots. _HOSTNAME_RE = re.compile( @@ -4471,6 +4504,16 @@ async def api_domains_set(req: DomainSetRequest): if req.domain_name in _SERVICE_DOMAIN_KEYS: _trigger_hosts_update() + # If Caddy is already running, regenerate its runtime Caddyfile and reload + # so the saved domain's virtual host is seated immediately. Without this, + # a domain added to an already-running Caddy never gets its site block + # (no proxying, no ACME cert) and the Hub's reachability check shows a + # misleading "ports 80/443" error until reboot. No-op when Caddy is + # inactive — e.g. Node role pre-enable, where the rebuild seats it anyway. + if req.domain_name in _SERVICE_DOMAIN_KEYS: + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, _reload_caddy_for_domain_change) + return {"ok": True} @@ -4486,6 +4529,12 @@ async def api_domains_set_email(req: DomainSetEmailRequest): with open(email_path, "w") as f: f.write(req.email.strip()) _chown_to_caddy(email_path) + + # The ACME email lives in the Caddyfile's global block — regenerate and + # reload so a running Caddy picks it up (no-op when Caddy is inactive). + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, _reload_caddy_for_domain_change) + return {"ok": True}