Systems Operational on roles with no enabled domain services (a
fresh Desktop-only install, or everything turned off) rendered no
Router card but still showed a "Who uses these ports" note listing
services the machine does not have. Any role with no enabled domain
service now gets the calm "No router setup needed yet" card with
role-appropriate wording, and the who-uses note only appears when
a domain service is actually enabled.
Narrow viewports (phones, half-screen RDP) had no layout at all:
the sidebar and topbar forced ~900px of horizontal scroll on every
role. The sidebar now collapses to a 76px icon rail below 920px,
the topbar wraps its search onto a second row below 640px, and the
welcome column reflows to one card per row. Legacy pre-redesign
media rules in onboarding.css (which targeted the old DOM and set
.sidebar{width:100%}) are removed — they silently overrode the new
layout, and one shrank the Zeus QR to 200px; QR codes keep their
repo-original 240px at every width.
Loading indicator:
- Branded boot splash (Hub logo inside an accent spinner ring,
"Starting The Hub") covers the shell while the first services
data loads, then fades out once the welcome dashboard has
rendered; never blocks longer than 25s and reassures the user
after 8s (message about post-reboot delays)
- Fire the network and update checks before the first services
render so the dashboard cards are current at first paint
- Sidebar Update button now adopts the last known update state
when built (order-independent), and the welcome dashboard
re-renders when the update state changes
Icons:
- New monochrome g-pulse glyph (activity line) for Systems
Operational: welcome card, dialog header, and System Status
section — the shield no longer doubles as Security
- Tech Support / Security dialog header gets a standard chip;
Security shows the shield chip with a plain "Security" title
(no emoji), and the shared dialog title now resets correctly
when reopening Tech Support after Security
The drifting orb glow was clipped to the centered 1040px column,
leaving visible walls where it met the content padding. Make the
welcome section full-bleed instead: cancel the content-area padding
with negative margins and stretch it to all four panel edges
(sidebar border, topbar, viewport right edge, bottom). The heading,
cards, and Browse button keep their centered 1040px column via
.welcome-inner.
Replace the All Services grid as the landing view with a
Nextcloud-style welcome dashboard:
- Greeting (time-of-day), "Welcome to Your Sovereign Digital &
Financial Life" headline, version + role meta line
- Status cards: Systems Operational, Network (LAN/WAN/hostname),
Bitcoin sync progress with ETA, and available-updates card
- Calm drifting orb background animation (transform-only,
disabled under prefers-reduced-motion)
- Services grid stays mounted but hidden until the user browses
(Browse button, search, or a nav category); Dashboard nav item
returns to the welcome view
- Move role badge and autolaunch preference out of the sidebar;
autolaunch toggle now lives in the Systems Operational modal
- Remove LAN/WAN chip from the topbar; IPs live on the Network card
The colored updater icon broke the monochrome system-action set in the
sidebar. Replace it with a new g-update glyph — a down arrow dropping
into an open tray ("get / install updates") — drawn in the same 24×24,
2px round-cap stroke style as the other sidebar glyphs.
- Sidebar Update System row: g-update glyph, currentColor like the rest.
- Update dialog header: same glyph in the green chip, matching the
rebuild dialog and Systems Operational header treatment.
- Also fixes a latent sizing bug: the previous colored <img> had no CSS
rule (the .upd-icon rule from the earlier patch never landed), so the
dialog header icon rendered at the source file's intrinsic 128px. The
chip markup uses the existing 54px .upd-chip rules.
- One "Bitcoin" category: the service catalog distinguishes bitcoin-base
from bitcoin-apps, which surfaced as two sidebar menus and two tile
sections. The Hub now normalizes both into a single "Bitcoin" category
(nav item with combined count, one tile section). CATEGORY_ALIASES in
constants.js maps the catalog keys; server config sends one
("bitcoin", "Bitcoin") entry and the node role allowlist is updated
to match. The nix catalog is unchanged.
- "Self-Hosted Apps" is now "Personal Apps" — every service here is
self-hosted, so the label added no distinction.
Coloring: the base surfaces were green-tinted darks, which read as a
green-hued background rather than green highlights. Shift the whole
surface ramp to neutral graphite (slightly cool, and lighter overall)
and keep green strictly for highlights — brand, buttons, switches,
status dots and pills, sync bars, focus rings.
- Tokens: bg #17191d, surface #1c1f24, card #23272c, hover #292e34,
elevated #26292e, inset #121417; text neutrals #e9edec/#a9b0b3/
#7a8388. Accent, borders, radii, shadows unchanged.
- Remove the green ambient radial washes behind the app (and login).
- Reboot / security-reset overlay gradients neutralized.
Updater icon: restore the repo's branded Sovran updater icon
(/static/icons/update.svg, true colors) in the sidebar Update System
row and in the update dialog header, replacing the generic refresh
glyph. The rebuild dialog keeps its glyph chip (no branded icon
exists for it).
- Restart and retry actions (topbar Reboot, Restart Entire System in the
update and rebuild dialogs and the restart confirmation, Retry Update,
Try Again) are now blue instead of amber — amber read as an error.
Status pills keep their semantic colors (amber = restart required).
- The service-modal domain section is no longer a checklist: it is titled
"Domain Status" and shows a single green "Domain is active" line with
the domain (or the backend's not-configured detail and Configure
Domain action when there is no domain yet). No "Step 1" wording.
- Systems Operational keeps the complexity hidden: the Router card now
shows just a verdict — green "Ports 80 and 443 are open" or red
"Ports 80 and 443 are not open" — derived from the same live backend
diagnostics. Port-forwarding instructions are gone (onboarding covers
them); on the Node-only role the card still explains that ports only
matter once BTCPay Server or Lightning Wallet Connections (LNURL) is
enabled.
Eight fixes from live testing:
Systems Operational modal:
- Drop the "test from your phone on mobile data" (hairpin NAT) sentence
from the router note — too technical for the intended audience.
- Node-only role: when BTCPay Server and Lightning Wallet Connections
(LNURL) are both off, the router card becomes a simple "No router
setup needed yet" note explaining that ports 80/443 only matter if
one of those services is turned on. When one is enabled, the card
shows the same port steps and live domain/port diagnostics as the
Desktop + Server role (diagnostics now poll only enabled services).
Service modals:
- The Domain Diagnostic Checklist now shows only the domain-active
step (Domain Configured). DNS and port diagnostics live in Systems
Operational, which shows the full checklist.
- Node-only role: BTCPay Server and Lightning Wallet Connections
modals gain a "Ports to Forward in Your Router" section with the
standard 80/443 wording and this computer's LAN address.
- Domain setup and reconfigure dialogs no longer contain router
port-forwarding instructions (already handled during Desktop +
Server onboarding and shown in Systems Operational).
Lightning Wallet Connections:
- Refresh and New Wallet toolbar buttons now share one height and
baseline (a leftover 12px top margin on Refresh was offsetting it).
- The header status chip has a proper gap between the status dot and
its label.
Zeus Connect / QR codes:
- QR codes render at the original 240px with the white frame and
pixelated upscaling, restoring scannability.
Brand:
- The sidebar logo loads via /static/sovran-hub-icon.svg (same as the
login page) instead of an inline <use> symbol — the gradient-heavy
symbol did not render reliably. The icon sprite is hidden with the
browser-safe zero-size pattern instead of display:none.
The update dialog kept the old bare title + spinner layout from the
previous theme. Rework it (and the rebuild dialog, for consistency) to
the approved The Hub dialog anatomy:
- Header: green chip icon, title, version chip, status pill
(Checking… / Up to date / Updating… / Restart required / Update
failed / Status unknown) and a header close button. A spinner appears
in the header while an update is starting or running.
- System Details card: current version, release channel, and last
checked (relative time, refreshed on every check).
- The log renders as a console with green "ok" and dim hint lines; the
up-to-date result shows as a single green console line exactly once
(the redundant status line is hidden in that state).
- Footer: Close plus a "Check again" primary action that re-runs the
update check. Close and Check again are disabled while a check or
update is in flight; all existing recovery actions (Save Error
Report, Retry Status, Retry Update, Restart Entire System) keep their
exact semantics.
- Opening the dialog now shows a "Checking…" state immediately while
the existing reattach-then-check logic runs; reattaching to an
in-progress update (page reload, RDP reconnect) is unchanged.
Sidebar Update System status tints now use the theme palette (red /
amber / blue / green), and the periodic background check refreshes the
dialog's "last checked" value.
Rebuild dialog gets the same header (icon, version chip, Applying… /
Done / Restart required / Failed pill, header close disabled while a
rebuild runs); its log stays hidden as before.
No API or state-machine changes: same endpoints, same polling, same
reattach and recovery behavior.
Apply the approved "The Hub" redesign to the web admin while keeping every
existing mechanic intact (polling, service-detail modals, Matrix and system
password management, NWC wallet manager, update/rebuild/backup/security/
reboot flows, feature manager, onboarding, role handling).
Layout (templates/index.html):
- Old header bar + IP bar replaced by a sidebar + topbar app shell.
Sidebar carries the brand (The Hub / Sovran_SystemsOS version), category
navigation with live counts, the System actions (Update System, Tech
Support, Manual Backup, Security, node-only Upgrade), Feature Manager /
Preferences, and the role badge.
- Topbar carries the page title, a service search box, the LAN | WAN
network chip (external IP always visible, one line), Reboot and Sign Out.
- New widgets row: Systems Operational summary (opens the new Systems
Operational modal) and Bitcoin Core sync progress with block/ETA.
- New Systems Operational modal: service counts, router port-forwarding
steps (80/443 to this machine's LAN IP), the live domain diagnostics
checklist, and which services use those ports.
New static/js/dashboard.js (namespaced IIFE, no new globals) renders the
nav, search filtering, widgets, and the Systems Operational modal; it is
driven by the existing /api/services payloads via a
window.dashboardServicesUpdated() hook called from buildTiles/updateTiles.
Visual design (static/css/*):
- New token set (softer dark surfaces, lifted contrast, Sovran green
reserved for status and actions) with legacy variable names aliased so
every secondary sheet re-skins automatically.
- Tiles, dialogs, buttons, inputs, toggles, tables, forms, overlays and
the login page restyled to the GNOME/libadwaita-flavored surfaces:
20px cards, 26px dialogs, pill buttons, libadwaita switches, mono value
pills with Copy buttons, consistent modal anatomy.
- Inline SVG symbol set for chrome/nav icons (monochrome, currentColor);
service icons still load from /static/icons/*.svg as before.
- Sidebar system buttons now use vector glyphs instead of emoji.
Behavioral details:
- Service detail modal header gains a status pill next to the version
chip; credentials render with pre-wrap for multiline values.
- First-login security banner now renders as a card inside the content
area instead of a full-width strip above the app.
- Search + category filtering hide/show sections and tiles without
touching the polling or update logic.
Onboarding and login pages rebranded to "The Hub" with aligned palette.
Updater/rebuild self-heal:
- Add a shared run_step wrapper used by both the update and rebuild
scripts. On the first failure matching a transient fetch/cache signature
(truncated tarball, corrupt NAR, hash mismatch, network timeout,
interrupted download), clear Nix's fetch caches and repair the store,
then retry once. Real config errors do not match and still fail loudly.
- The kernel-change boot fallback in the rebuild path is also wrapped.
- Fixes the reported 'cannot read file from tarball: Truncated tar archive
detected' failure, which a plain re-run cannot clear because Nix reuses
the corrupt cached archive.
Failed-update recovery / reporting:
- check_for_updates() now compares the running Hub version against the
branch VERSION, so a failed 'nix flake update' (lock advanced but no
generation staged) can no longer masquerade as 'up to date' and block
retries.
- /api/updates/check surfaces a persistent 'failed' state; /api/updates/run
never blocks a retry after a failure.
- Dashboard shows a red 'Update failed - click to retry' tile; the modal
offers a Retry Update button and stops offering a reboot on failure.
- Replace 3 long instruction bullets with 1 plaintext line
- Shorten intro (drop long parenthetical on IP location)
- Remove 'how to confirm it worked' note — if router applies rules, ports are open
- Keep port table intact (40000-40099 current set)
User-facing polish to match the element-calling port changes:
- helpers.js: port-forward guide example now shows the 40000-40099 range
instead of the removed 30000-40000.
- versions.json: livekit dev fallback 1.5.2 -> 1.13.6 (the Nix-generated
version in sovran-hub.nix already derives from pkgs.livekit.version).
- server.py: docstring/comment examples reference 40000-40099.
- CHANGELOG.md: record the calling changes under [Unreleased].
The tile/modal port tables themselves are data-driven from server.py's
port_requirements / _PORTS_ELEMENT_CALLING (updated in the previous commit),
so no further UI changes are needed.
The full-system updater runs as a detached systemd service and can finish
successfully even when the browser loses its status connection. In that
case the update log and status file correctly report REBOOT_REQUIRED, but
the Hub modal can remain on "Updating..." with its controls disabled.
There were four independent ways for the frontend to get stuck:
* update status fetches had no deadline, so a request that stayed pending
never rejected and never advanced the existing failure counter;
* setInterval started async polls without waiting for the previous poll,
allowing slow requests to overlap and responses to arrive out of order;
* each log chunk used textContent +=, replacing the complete and growing
Nix build log every two seconds, which could stall browser rendering and
was especially visible over RDP; and
* page reload, tab resume, and RDP reconnect did not reattach the modal to
the update status persisted by the backend.
This produced a dangerous UX mismatch: the machine had a fully staged
NixOS generation and was ready to reboot, while the Hub continued telling
the user that the update was still running.
Bound status requests with AbortController, prevent overlapping polls, and
replace the endless spinner after sustained failures with an explicit
"Update status unavailable" state and Retry Status action. Reconcile state
immediately on focus, visibility, online, page startup, and before starting
a new update. Use no-store requests and render verbose logs incrementally
with a bounded visible tail while retaining the complete report in memory.
Apply the same timeout and single-flight protection to rebuild polling.
Record the exact generation produced by `nixos-rebuild boot`. The Hub now
keeps REBOOT_REQUIRED visible until that generation matches
/run/current-system, then clears the marker after reboot. For an update
started by an older updater that did not write the marker, recover the
staged generation from the final nixos-rebuild log line. The dashboard
sidebar also distinguishes update-in-progress and restart-required states.
Regression coverage verifies generation marker/log recovery, pre- versus
post-reboot detection, request timeout wiring, single-flight polling,
connection-loss UX, RDP/tab resume reconciliation, bounded log rendering,
page-reload recovery, and JavaScript syntax.
Validation:
* python3 -m unittest discover -s tests -p 'test_*.py' -v (170 passed)
* node --check app/sovran_systemsos_web/static/js/*.js
* python3 -m py_compile for changed Python modules
* git diff --check
A Nix evaluation was not available in the development sandbox; the NixOS
module should still be evaluated and built in CI or on a test machine before
release.
The LND-only rewrite of lndconnect.nix shipped a wrapper Zeus cannot
use: unknown flags (--cert/--macaroon), a non-existent onion path
(free/lnd.onion), and a REST hidden service that collided with LND's
P2P onion. Restore the nix-bitcoin contract — dedicated lnd-rest
onion on port 8080, --nocert over Tor, admin macaroon in the URI —
and only persist a valid lndconnect:// URI for the Hub QR.
The header badge now uses the exact visual treatment of
.creds-title-version-badge from modals.css: neutral translucent pill
(rgba(255,255,255,0.06) background, 0.08 white border, 12px radius,
--text-secondary text, 0.72rem/600/2px-10px padding), no hover
animation, and a bare version number like the modals show (e.g.
"2.8.4") instead of the green pill with a "v" prefix.
The badge markup flattens to a single text span; the .version-label
and .version-number rules are removed.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Mirrors the service modal pattern where the version badge sits to the
right of the title: .title-group switches from a centered column
(badge under the title, PR #377) to a centered row with the badge
vertically centered directly after "Sovran_SystemsOS Hub".
flex-wrap keeps a graceful fallback: on very narrow screens the badge
wraps below the title, centered — the previous stacked look.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- Fix QR hint for zeus-connect-setup.service to use correct LND REST steps
- Expand 'How to Connect' instructions with title/intro/steps/note
- Add matching styled guide block in credentials modal
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The Manual Backup screen in the Hub always listed the Node / Server +
Desktop items (nix-bitcoin secrets, /var/lib system service data, and the
database/blockchain caveat), and the backup script mirrored /var/lib and
counted it in the free-space estimate even on the Desktop Only role.
Desktop Only systems run no server or Bitcoin services and have no
internal second data drive, so none of that applies.
Hub UI (support.js):
- 'What gets backed up' is role-aware: Desktop Only lists only the NixOS
configuration (/etc/nixos) and home directory (/home)
- Database/blockchain note hidden on Desktop Only
- Intro copy corrected: external USB copy is a second location on
Desktop Only (no internal second drive); third-location wording kept
for Node / Server + Desktop
Backup script (sovran-hub-backup.sh):
- Desktop Only runs 2 stages (1/2 /etc/nixos, 2/2 /home); secrets and
/var/lib stages no longer run on that role
- Free-space estimate skips /var/lib on Desktop Only
- BACKUP_MANIFEST.txt sources/exclusions/limitations/restore guidance
and blockchain note are role-aware
- Completion message role-aware ('second, external location' on
Desktop Only); header comments updated
Node and Server + Desktop behavior is unchanged. Added CHANGELOG entry.
Verified: bash -n / node --check, 6 role-detection cases, manifest
generation for both role groups (non-desktop output identical to
before), and simulated UI renders for all three roles.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
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>
- The Hub header badge could render 'vdev' because the runtime
/etc/nixos/VERSION lookup fell back to the literal string 'dev' when
the file was missing (e.g. dev/test environments, or before the
Nix-generated config carried a version at all).
- modules/core/sovran-hub.nix now reads the repo's VERSION file at
Nix eval time and bakes a real semantic version (sovran_version)
into the generated config.json and a VERSION file shipped with the
package, so the Hub always has a solid value to display.
- server.py's _get_sovran_version() now reads that baked-in
sovran_version first, and explicitly rejects a literal 'dev' value
from any of its file-based fallbacks so the badge never shows
'vdev' again.
- templates/index.html + header.css: wrapped the title and the
version badge in a '.title-group' column so the version badge sits
directly underneath 'Sovran_SystemsOS Hub', left-aligned with it.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- Add release-stable.sh script with automatic tagging, CHANGELOG updates,
GitHub/Gitea release creation, and VERSION file management
- Update iso/common.nix to include version from VERSION file in ISO filename
- Add VERSION file (current: 1.0.3)
- Polish OS version badge in Sovran Hub header (top-right)
- Update README.md download link to versioned ISO
- Update CHANGELOG.md with existing tags (v1.0.0 – v1.0.3)
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Naming: user-facing 'Wallet Connections' -> 'Lightning Wallet Connections'
across the Hub, feature registry, tile, and NixOS modules. Internal ids
(nwc-wallets, albyhub.service, /api/nwc/*) are unchanged.
UX: the service-detail modal put status, domain diagnostics, router ports,
the enable/disable toggle, restart, the liquidity guide and the whole wallet
manager in one cramped scrolling column. For this feature the modal is now
980px wide and split into two tabs:
- Wallets: wallet grid, create/share/verify flows, collapsible liquidity guide
- Service & Setup: description, status, domain checklist, ports, enable, restart
A status dot and domain chip sit in the tab bar so state is visible from both
tabs, and the modal opens on Setup when the service is off or the Lightning
Address domain is unconfigured. Wallet cards gain a balance chip, pending
badge, a prominent address row, and separated destructive actions.
Non-NWC services keep the original single-column layout and width.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Each NWC wallet connection gets its own shareable LNURL QR so the owner
can receive payments from anyone without creating invoices (the core
LNURL value proposition: a static QR that never expires).
Hub UI (service detail modal):
- New 'Share QR' button on every wallet connection card
- Dedicated share view: large QR, Lightning Address + copy, raw bech32
LNURL + copy
- Download PNG (websites/social), download SVG (vector, print-sharp at
any size), and a Print button that opens a print-ready payment card
which auto-invokes the browser print dialog
Backend (FastAPI, session-authenticated):
- GET /api/nwc/wallets/{id}/lnurl -> alias, lightning_address, LNURL
- GET /api/nwc/wallets/{id}/lnurl-qr.png (?download=1, ?scale=)
- GET /api/nwc/wallets/{id}/lnurl-qr.svg (?download=1)
- GET /api/nwc/wallets/{id}/lnurl-qr/print (print-ready HTML card)
- Stdlib bech32 encoder (BIP-173, verified against the official LUD-01
golden vector); LNURL is uppercased for QR alphanumeric-mode density
per LUD-01 guidance; QR rendered with the existing qrencode package
- Resolves wallets by app id or pubkey; 404 unknown wallet, 503 when no
Lightning domain is configured; all routes return 401 unauthenticated
No new system dependencies.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The Element Call tile still appended a synthetic 'Step 4: Router Setup
Needed' to the domain diagnostic checklist, and every port table carried a
'Sovran_SystemsOS Status' column with Ready / Not ready yet verdicts.
Both were misleading: port forwarding happens on the router, which this
computer cannot inspect. A local ss/firewall probe can neither prove nor
disprove that forwarding works — and the LiveKit TURN relay range binds on
demand, so it reported 'Not ready yet' even on a perfectly working system.
- server.py: add ROUTER_FORWARD_ONLY_UNITS ({livekit.service}); skip the
local probe for those units, drop the step-4 append, replace extra_ports
with router_ports (no status field), and exclude router-only ports from
both tile health and /api/ports/health so they can't raise false alarms.
- helpers.js: new shared renderPortForwardGuideHtml() — one intro naming the
internal IP, explicit instructions (same internal/external port, match the
protocol, use port-range fields for 30000-40000), a colour-coded
TCP / UDP / TCP+UDP badge per row, and a closing note that the only real
test is loading the service from a phone on mobile data.
- features.js: enable-time modal uses the shared guide and now always lists
every port to forward (the old local pre-filter hid ports the user still
had to open).
- service-detail.js: tile port section uses the same guide; the SSH/non-domain
branch keeps a small 'not open on this computer yet' hint, which is a real
local fact, separate from router forwarding.
- onboarding.js: step 3 router note reworded to match (same number for
internal/external, notes that Element Call adds UDP ports).
- domain-setup.css: styles for the protocol badges and instruction list.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- Onboarding: remove the redundant/error-prone 'Router Setup' step (5 steps -> 4).
A compact 80/443 (+22 SSH) note now lives inside Domain Configuration,
and the Element Call ports are only shown at the moment they matter:
when enabling the feature, and afterwards on the service tile.
- Onboarding step 3: fix domain prefill bug (API returns {domains: {...}}),
make /api/network fetch best-effort so it can never block the step.
- Enable-time port modal: streamline copy (one intro + table + pointer to
the tile's live status view).
- Element Call tile detail: replace 5 repetitive prose blocks with 2 compact
notes around the live-status port table.
- Njal.la DDNS: run njalla.sh immediately when a DDNS-backed feature is
enabled (previously only ran on domain save or the 15-min cron tick).
- Harden njalla.sh handling: create the base script (shebang + IP lookup)
if missing before appending curl lines; invoke via bash explicitly.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>