Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
991c790816 | ||
|
|
a343afffaa | ||
|
|
eeef81ffc7 | ||
|
|
bc6b3f0843 | ||
|
|
f01e776f34 | ||
|
|
a19fa53468 | ||
|
|
baefca0c68 | ||
|
|
9ed6040425 | ||
|
|
ca17b381f6 | ||
|
|
3f74388359 | ||
|
|
a814ed332f | ||
|
|
94e4412e67 | ||
|
|
670cfb9acd | ||
|
|
2015a3ecd4 | ||
|
|
2622223033 | ||
|
|
d0ec6dd760 | ||
|
|
90b1c45a0d | ||
|
|
0ac1529a1d | ||
|
|
1b579271e5 | ||
|
|
c6b4b89be3 | ||
|
|
a8ff366992 | ||
|
|
ddba59cce8 | ||
|
|
e12e9ba01b | ||
|
|
853c3edf92 |
+49
-4
@@ -7,18 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **Manual Backup now matches the system role**: on the Desktop Only role the Hub's
|
||||
"What gets backed up" list no longer shows Node / Server + Desktop items
|
||||
(nix-bitcoin secrets, `/var/lib` system service data, and the database/blockchain
|
||||
caveats), and the backup script skips those stages entirely — Desktop Only backups
|
||||
now mirror only the NixOS configuration (`/etc/nixos`) and home directory (`/home`).
|
||||
The free-space estimate, stage numbering, backup manifest, and completion message
|
||||
are all role-aware; Node and Server + Desktop backups are unchanged.
|
||||
|
||||
---
|
||||
|
||||
## [1.0.4] - 2026-07-29
|
||||
|
||||
### Added
|
||||
- (Add new features here)
|
||||
- **Lightning Wallet Connections (NWC)** — Hub-managed Nostr Wallet Connect powered by Alby Hub + LND:
|
||||
- Create, view, and delete wallet connections directly from the Sovran Hub with a tabbed modal UI
|
||||
- Downloadable/printable LNURL QR codes for connecting external wallets
|
||||
- Channel liquidity guide and onboarding guidance for new LND nodes
|
||||
- Unique-hostname enforcement with conflict validation and UI guidance
|
||||
- Official NWC branding and logo
|
||||
- **Version visibility across the Hub**:
|
||||
- Sovran_SystemsOS version badge displayed under the Hub title
|
||||
- Version numbers shown on all Hub service tiles and next to service modal titles
|
||||
- Deployed PHP app versions surfaced in service titles
|
||||
- Build-time version reference file (`VERSION`) so version lookups are instantaneous and consistent
|
||||
- **Automated stable release workflow** (`scripts/release-stable.sh`) with versioned ISO naming from the `VERSION` file
|
||||
- `CONTRIBUTING.md` and expanded project documentation
|
||||
|
||||
### Changed
|
||||
- (Add changes here)
|
||||
- **Manual Backup overhauled**: replaced tar+DB+LND archive approach with a reliable ext4 + rsync workflow, including
|
||||
mount checks, path safety, atomic completion markers, stale-marker cleanup, and behavioral test coverage
|
||||
- **Port-forwarding UX simplified**: removed onboarding Step 4 and the misleading local "ready" status;
|
||||
Njal.la DDNS now runs automatically when the feature is enabled
|
||||
- README restructured for clarity, links, and accuracy; added router/ISP port-forwarding requirements
|
||||
for Server + Desktop mode; acknowledged LiveKit and Alby Hub
|
||||
- Updated nixpkgs and Bitcoin clients
|
||||
- Repository cleanup: removed unused `.github`, `.tests`, `nix/`, and `docs/ai` directories
|
||||
|
||||
### Fixed
|
||||
- (Add bug fixes here)
|
||||
- NWC wallet certificate path wiring (now uses the nix-bitcoin LND cert path)
|
||||
- Deterministic LND/Alby Hub port collision
|
||||
- Alby Hub executable resolution and v1.23.0 patches regenerated against exact upstream source
|
||||
- Manual Backup exit-code failures (bash/gawk in service PATH, tar tolerance, flock, browser-cache exclusions)
|
||||
- rsync destination-directory failures in backups (auto `mkdir -p`, mount check, 19 behavioral tests)
|
||||
- `sovran-hosts-update` converted to `writeShellApplication` with explicit runtime inputs
|
||||
- Incorrect `lib.mkIf` usage in the NWC wallets module
|
||||
- Duplicate systemd LND strings
|
||||
- `git fetch` tag-clobber errors against Gitea (now uses `--force`)
|
||||
|
||||
[1.0.4]: https://git.sovransystems.com/api/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.4
|
||||
### Security
|
||||
- Hardened Lightning Wallet Connections (NWC): restricted `ReadOnlyPaths` for the unlock password,
|
||||
fixed an Authorization-header bug, eliminated stack-trace exposure flagged by CodeQL,
|
||||
and tightened credential access and amount validation
|
||||
|
||||
[1.0.4]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.4
|
||||
|
||||
|
||||
## [1.0.3] - 2026-07-29
|
||||
|
||||
@@ -3,10 +3,18 @@
|
||||
# Backs up Sovran_SystemsOS data to an external USB hard drive using rsync.
|
||||
# Designed for the Hub web UI (no GUI dependencies).
|
||||
#
|
||||
# Your Sovran Pro already backs up your data automatically to its
|
||||
# internal second drive (BTCEcoandBackup at /run/media/Second_Drive).
|
||||
# This script creates an additional copy on an external USB drive —
|
||||
# storing your data in a third location for maximum protection.
|
||||
# On Server + Desktop and Node systems, your Sovran Pro already backs up
|
||||
# your data automatically to its internal second drive (BTCEcoandBackup at
|
||||
# /run/media/Second_Drive); this script stores a copy in a third location.
|
||||
# Desktop Only systems have no internal second drive, so the external copy
|
||||
# is the second location.
|
||||
#
|
||||
# What gets mirrored depends on the system role:
|
||||
# - Node / Server + Desktop: /etc/nixos, /etc/nix-bitcoin-secrets,
|
||||
# /home, and /var/lib (minus databases, blockchain data, logs, caches).
|
||||
# - Desktop Only: /etc/nixos and /home. Desktop Only runs no server or
|
||||
# Bitcoin services, so there are no nix-bitcoin secrets or system
|
||||
# service data to back up.
|
||||
#
|
||||
# The external drive must be formatted as ext4. Files are stored as
|
||||
# directly browsable files under Sovran_SystemsOS_Backup/current/.
|
||||
@@ -344,6 +352,19 @@ case "$ROLE" in
|
||||
esac
|
||||
log "Detected role: $ROLE_LABEL"
|
||||
|
||||
# Backup scope depends on the role. Desktop Only systems run no server or
|
||||
# Bitcoin services, so only the NixOS configuration and home directory are
|
||||
# mirrored (2 stages). Node and Server + Desktop systems also mirror the
|
||||
# nix-bitcoin secrets and /var/lib system service data (4 stages).
|
||||
if [[ "$ROLE" == "desktop" ]]; then
|
||||
TOTAL_STAGES=2
|
||||
HOME_STAGE_NUM=2
|
||||
log "Desktop Only role: backing up the NixOS configuration (/etc/nixos) and home directory (/home) only."
|
||||
else
|
||||
TOTAL_STAGES=4
|
||||
HOME_STAGE_NUM=3
|
||||
fi
|
||||
|
||||
# ── Detect target drive ──────────────────────────────────────────
|
||||
|
||||
if [[ -n "${BACKUP_TARGET:-}" ]]; then
|
||||
@@ -385,11 +406,13 @@ log "Backup destination: $BACKUP_DIR"
|
||||
|
||||
ETC_NIXOS_BYTES=$(estimate_path_bytes /etc/nixos)
|
||||
HOME_BYTES=$(estimate_path_bytes /home --exclude='*/.cache' --exclude='*/.local/share/Trash' --exclude='*/Trash')
|
||||
|
||||
# nix-bitcoin secrets and /var/lib system service data exist only on the
|
||||
# Node and Server + Desktop roles — they are skipped entirely on Desktop Only.
|
||||
SECRETS_BYTES=0
|
||||
VAR_LIB_BYTES=0
|
||||
if [[ "$ROLE" != "desktop" ]]; then
|
||||
SECRETS_BYTES=$(estimate_path_bytes /etc/nix-bitcoin-secrets)
|
||||
fi
|
||||
|
||||
VAR_LIB_BYTES=$(estimate_path_bytes /var/lib \
|
||||
--exclude='postgresql' \
|
||||
--exclude='mysql' \
|
||||
@@ -400,6 +423,7 @@ VAR_LIB_BYTES=$(estimate_path_bytes /var/lib \
|
||||
--exclude='*/logs' \
|
||||
--exclude='*/cache' \
|
||||
--exclude='*/tmp')
|
||||
fi
|
||||
|
||||
ESTIMATED_BYTES=$(( ETC_NIXOS_BYTES + HOME_BYTES + SECRETS_BYTES + VAR_LIB_BYTES ))
|
||||
# Require 20% growth headroom plus a fixed 1 GiB safety margin.
|
||||
@@ -418,10 +442,10 @@ log "Free space on drive: ${FREE_GB} GB"
|
||||
(( FREE_BYTES >= REQUIRED_BYTES )) || \
|
||||
fail "Not enough free space on drive (${FREE_GB} GB available, ${REQUIRED_GB} GB required)."
|
||||
|
||||
# ── Stage 1/4: NixOS configuration ──────────────────────────────
|
||||
# ── Stage 1: NixOS configuration ────────────────────────────────
|
||||
|
||||
log ""
|
||||
log "── Stage 1/4: NixOS configuration (/etc/nixos) ──────────────"
|
||||
log "── Stage 1/${TOTAL_STAGES}: NixOS configuration (/etc/nixos) ──────────────"
|
||||
if [[ -d /etc/nixos ]]; then
|
||||
sync_tree "/etc/nixos" no /etc/nixos/ "$BACKUP_DIR/etc/nixos/"
|
||||
log "Stage 1 complete."
|
||||
@@ -429,26 +453,30 @@ else
|
||||
log "WARNING: /etc/nixos not found — skipping."
|
||||
fi
|
||||
|
||||
# ── Stage 2/4: Secrets ──────────────────────────────────────────
|
||||
# ── Stage 2: Secrets ────────────────────────────────────────────
|
||||
# Only applies to the Node and Server + Desktop roles. Desktop Only systems
|
||||
# run no nix-bitcoin services, so there are no secrets to back up and this
|
||||
# stage does not exist for them.
|
||||
|
||||
if [[ "$ROLE" != "desktop" ]]; then
|
||||
log ""
|
||||
log "── Stage 2/4: Secrets (/etc/nix-bitcoin-secrets) ───────────"
|
||||
if [[ "$ROLE" == "desktop" ]]; then
|
||||
log "Skipping /etc/nix-bitcoin-secrets — not applicable for Desktop Only role."
|
||||
elif [[ -e /etc/nix-bitcoin-secrets ]]; then
|
||||
log "── Stage 2/${TOTAL_STAGES}: Secrets (/etc/nix-bitcoin-secrets) ───────────"
|
||||
if [[ -e /etc/nix-bitcoin-secrets ]]; then
|
||||
sync_tree "/etc/nix-bitcoin-secrets" no /etc/nix-bitcoin-secrets/ "$BACKUP_DIR/etc/nix-bitcoin-secrets/"
|
||||
else
|
||||
log "(not found: /etc/nix-bitcoin-secrets — skipping)"
|
||||
fi
|
||||
log "Stage 2 complete."
|
||||
fi
|
||||
|
||||
# ── Stage 3/4: Home directory ───────────────────────────────────
|
||||
# ── Home directory ──────────────────────────────────────────────
|
||||
# Stage 2/2 on Desktop Only, stage 3/4 on Node and Server + Desktop.
|
||||
# Rsync exit code 24 (vanished source files) is treated as nonfatal here
|
||||
# because the desktop may be active and files can disappear between the
|
||||
# directory scan and the copy. All other nonzero exit codes remain fatal.
|
||||
|
||||
log ""
|
||||
log "── Stage 3/4: Home directory (/home) ───────────────────────"
|
||||
log "── Stage ${HOME_STAGE_NUM}/${TOTAL_STAGES}: Home directory (/home) ───────────────────────"
|
||||
if [[ -d /home ]]; then
|
||||
sync_tree "/home" yes /home/ "$BACKUP_DIR/home/" \
|
||||
--exclude='.cache/' \
|
||||
@@ -467,18 +495,21 @@ if [[ -d /home ]]; then
|
||||
--exclude='.thumbnails/' \
|
||||
--exclude='.xsession-errors' \
|
||||
--exclude='.xsession-errors.old'
|
||||
log "Stage 3 complete."
|
||||
log "Stage ${HOME_STAGE_NUM} complete."
|
||||
else
|
||||
log "WARNING: /home not found — skipping."
|
||||
fi
|
||||
|
||||
# ── Stage 4/4: System data ──────────────────────────────────────
|
||||
# ── Stage 4: System data ────────────────────────────────────────
|
||||
# Only applies to the Node and Server + Desktop roles — Desktop Only systems
|
||||
# run no server services, so /var/lib holds no service data worth mirroring.
|
||||
# PostgreSQL/MariaDB raw database directories are excluded. Application
|
||||
# databases must be backed up separately with native database tools.
|
||||
# Bitcoin/Electrs data are excluded; they live on the internal second drive.
|
||||
|
||||
if [[ "$ROLE" != "desktop" ]]; then
|
||||
log ""
|
||||
log "── Stage 4/4: System data (/var/lib) ───────────────────────"
|
||||
log "── Stage 4/${TOTAL_STAGES}: System data (/var/lib) ───────────────────────"
|
||||
if [[ -d /var/lib ]]; then
|
||||
sync_tree "/var/lib" no /var/lib/ "$BACKUP_DIR/var/lib/" \
|
||||
--exclude='postgresql/' \
|
||||
@@ -494,6 +525,7 @@ if [[ -d /var/lib ]]; then
|
||||
else
|
||||
log "WARNING: /var/lib not found — skipping."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Generate manifest ────────────────────────────────────────────
|
||||
|
||||
@@ -517,23 +549,29 @@ MANIFEST_FILE="$BACKUP_DIR/BACKUP_MANIFEST.txt"
|
||||
echo "- /etc/nix-bitcoin-secrets (when present) → current/etc/nix-bitcoin-secrets/"
|
||||
fi
|
||||
echo "- /home → current/home/"
|
||||
if [[ "$ROLE" != "desktop" ]]; then
|
||||
echo "- /var/lib → current/var/lib/"
|
||||
fi
|
||||
echo ""
|
||||
echo "Exclusions:"
|
||||
if [[ "$ROLE" != "desktop" ]]; then
|
||||
echo "- /var/lib/postgresql (PostgreSQL raw database files — not included)"
|
||||
echo "- /var/lib/mysql, /var/lib/mariadb (MariaDB raw database files — not included)"
|
||||
echo "- /var/lib/bitcoind (Bitcoin blockchain — excluded; lives on internal second drive)"
|
||||
echo "- /var/lib/electrs (Electrs index — excluded; lives on internal second drive)"
|
||||
echo "- /run/media/Second_Drive (internal second drive — never traversed)"
|
||||
echo "- /var/lib/*/log, /var/lib/*/logs, /var/lib/*/cache, /var/lib/*/tmp"
|
||||
fi
|
||||
echo "- Browser disk caches, thumbnail caches, trash directories, X session error logs"
|
||||
echo ""
|
||||
echo "Important limitations:"
|
||||
if [[ "$ROLE" != "desktop" ]]; then
|
||||
echo "- PostgreSQL and MariaDB/MySQL application databases are NOT included in this"
|
||||
echo " backup. If you use Nextcloud, Matrix/Synapse, or other database-backed"
|
||||
echo " applications, their data must be backed up separately using native tools."
|
||||
echo "- Bitcoin blockchain data and Electrs indexes are NOT included; they are"
|
||||
echo " reconstructable or stored on the internal second drive."
|
||||
fi
|
||||
echo "- This is a live file-level mirror, not a transactional database backup."
|
||||
echo " Files being written during the backup may be in an inconsistent state."
|
||||
echo ""
|
||||
@@ -542,7 +580,9 @@ MANIFEST_FILE="$BACKUP_DIR/BACKUP_MANIFEST.txt"
|
||||
echo "- To restore a directory:"
|
||||
echo " sudo rsync -aAXH --numeric-ids current/etc/nixos/ /etc/nixos/"
|
||||
echo " sudo rsync -aAXH --numeric-ids current/home/ /home/"
|
||||
if [[ "$ROLE" != "desktop" ]]; then
|
||||
echo " sudo rsync -aAXH --numeric-ids current/var/lib/ /var/lib/"
|
||||
fi
|
||||
echo "- To copy individual files:"
|
||||
echo " sudo cp -a current/home/username/ /home/username/"
|
||||
echo "- When restoring /etc/nixos to replacement hardware, regenerate"
|
||||
@@ -556,10 +596,12 @@ MANIFEST_FILE="$BACKUP_DIR/BACKUP_MANIFEST.txt"
|
||||
echo "- $warning"
|
||||
done
|
||||
fi
|
||||
if [[ "$ROLE" != "desktop" ]]; then
|
||||
echo ""
|
||||
echo "Note: Bitcoin blockchain and Electrs index data are intentionally excluded"
|
||||
echo "from manual external backup because they already live on the internal second drive"
|
||||
echo "(/run/media/Second_Drive) and are reconstructable/internal-backup data."
|
||||
fi
|
||||
} > "$MANIFEST_FILE"
|
||||
|
||||
log "Manifest written to $MANIFEST_FILE"
|
||||
@@ -576,7 +618,11 @@ if [[ "${#RSYNC_WARNINGS[@]}" -gt 0 ]]; then
|
||||
log "vanished during backup, which is normal on an active desktop."
|
||||
log ""
|
||||
fi
|
||||
if [[ "$ROLE" == "desktop" ]]; then
|
||||
log "All Finished! Your data is now backed up to a second, external location."
|
||||
else
|
||||
log "All Finished! Your data is now backed up to a third location."
|
||||
fi
|
||||
log "Files are directly browsable on the drive under: ${BACKUP_DIR}"
|
||||
log "Please eject the drive safely before removing it from your Sovran Pro."
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -397,8 +403,8 @@ SERVICE_DESCRIPTIONS: dict[str, str] = {
|
||||
),
|
||||
"lnd.service": (
|
||||
"Your Lightning Network node for instant, low-fee Bitcoin payments. "
|
||||
"LND powers your Zeus wallet, Ride The Lightning dashboard, and BTCPayServer's "
|
||||
"Lightning capabilities. With Sovran_SystemsOS, it's always connected and ready."
|
||||
"LND powers your Zeus wallet (via direct LND REST or Tor), Ride The Lightning dashboard, and BTCPayServer's "
|
||||
"Lightning capabilities. For mobile spending with sandboxed wallets, use Lightning Wallet Connections (NWC) instead."
|
||||
),
|
||||
"rtl.service": (
|
||||
"Your personal Lightning Network command center. Open channels, manage liquidity, "
|
||||
@@ -411,9 +417,9 @@ SERVICE_DESCRIPTIONS: dict[str, str] = {
|
||||
"Sovran_SystemsOS makes running a production-grade payment gateway as simple as flipping a switch."
|
||||
),
|
||||
"zeus-connect-setup.service": (
|
||||
"Connect the Zeus mobile wallet to your Lightning node. Send and receive "
|
||||
"Lightning payments from your phone, backed by your own infrastructure. "
|
||||
"Scan the QR code and your phone becomes a sovereign wallet."
|
||||
"Connect the Zeus mobile wallet to your Lightning node via LND REST. Send and receive "
|
||||
"Lightning payments from your phone using a direct node connection. "
|
||||
"Scan the QR code to add your node to Zeus — this gives full node admin access."
|
||||
),
|
||||
"mempool.service": (
|
||||
"Your own blockchain explorer and mempool visualizer. Monitor transactions, "
|
||||
@@ -2275,18 +2281,39 @@ async def api_logout(request: Request):
|
||||
|
||||
|
||||
def _get_sovran_version() -> str:
|
||||
"""Read the OS version from the VERSION file."""
|
||||
"""Read the OS version to display in the Hub header.
|
||||
|
||||
Resolution order (first match wins):
|
||||
1. ``sovran_version`` baked into the Nix-generated config.json — this
|
||||
is the authoritative, build-time value and is what's used on a
|
||||
real Sovran_SystemsOS install.
|
||||
2. ``/etc/nixos/VERSION`` — present on an installed system.
|
||||
3. A ``VERSION`` file shipped next to this package (installed by the
|
||||
Nix build) or in the repo root (local development checkout).
|
||||
4. A hard-coded fallback so the badge never renders a broken/empty
|
||||
value such as the old "vdev" placeholder.
|
||||
"""
|
||||
try:
|
||||
with open("/etc/nixos/VERSION", "r") as f:
|
||||
return f.read().strip()
|
||||
except FileNotFoundError:
|
||||
try:
|
||||
with open("VERSION", "r") as f: # fallback for development
|
||||
return f.read().strip()
|
||||
except FileNotFoundError:
|
||||
return "dev"
|
||||
version = load_config().get("sovran_version")
|
||||
if version:
|
||||
return str(version).strip().lstrip("vV")
|
||||
except Exception:
|
||||
return "dev"
|
||||
pass
|
||||
|
||||
for candidate in (
|
||||
"/etc/nixos/VERSION",
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "VERSION"),
|
||||
"VERSION",
|
||||
):
|
||||
try:
|
||||
with open(candidate, "r") as f:
|
||||
version = f.read().strip().lstrip("vV")
|
||||
if version and version.lower() != "dev":
|
||||
return version
|
||||
except (FileNotFoundError, OSError):
|
||||
continue
|
||||
|
||||
return "1.0.0"
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
@@ -4329,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(
|
||||
@@ -4450,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}
|
||||
|
||||
|
||||
@@ -4465,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}
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,14 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -58,8 +66,8 @@
|
||||
color: #4ade80;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin-left: 12px;
|
||||
white-space: nowrap;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.os-version-badge:hover {
|
||||
|
||||
@@ -1,5 +1,56 @@
|
||||
/* ── NWC Benefits Grid ─────────────────────────────────────────── */
|
||||
|
||||
.nwc-benefits-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.nwc-benefit-item {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.nwc-benefit-item:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.nwc-benefit-icon {
|
||||
font-size: 1.25rem;
|
||||
margin-top: 2px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.nwc-benefit-content strong {
|
||||
display: block;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.nwc-benefit-content p {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.nwc-benefits-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Update modal ────────────────────────────────────────────────── */
|
||||
|
||||
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
@@ -607,10 +658,12 @@ button.btn-reboot:hover:not(:disabled) {
|
||||
|
||||
.nwc-tab-intro {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.nwc-tab-intro-title {
|
||||
@@ -982,6 +1035,74 @@ button.btn-reboot:hover:not(:disabled) {
|
||||
background: #1c2a24;
|
||||
}
|
||||
|
||||
/* ── NWC Connect Guide (shown after wallet creation) ────────────── */
|
||||
|
||||
.nwc-connect-guide {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: rgba(109, 191, 139, 0.04);
|
||||
}
|
||||
|
||||
.nwc-connect-guide-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.nwc-connect-guide-intro {
|
||||
margin: 0 0 14px;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.nwc-connect-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.nwc-connect-step {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.nwc-step-num {
|
||||
background: var(--accent-color);
|
||||
color: #0A1A10;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.78rem;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.nwc-connect-note {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border-left: 3px solid #f59e0b;
|
||||
padding: 10px 12px;
|
||||
border-radius: 0 8px 8px 0;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.nwc-connect-note strong {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* ── Narrow screens ─────────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 720px) {
|
||||
|
||||
@@ -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 <script> tag. */
|
||||
"use strict";
|
||||
|
||||
/* Escape helper local to this file so it is self-contained on both pages. */
|
||||
function dpEsc(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/* ── "What you'll need" intro ──────────────────────────────────────
|
||||
opts.serviceName — e.g. "Lightning Wallet Connections", or null for
|
||||
the multi-service (onboarding) wording.
|
||||
opts.hostExample — e.g. "lightning" → lightning.yourdomain.com */
|
||||
function renderDomainNeedsHtml(opts) {
|
||||
var serviceName = opts.serviceName || null;
|
||||
var hostExample = dpEsc(opts.hostExample || "myservice");
|
||||
var html = "";
|
||||
|
||||
if (serviceName) {
|
||||
html += "<p>To enable <strong>" + dpEsc(serviceName) + "</strong>, you'll need two things first:</p>";
|
||||
html += '<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">';
|
||||
html += "<li><strong>A domain of your own from <a href=\"https://njal.la\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:var(--accent-color);\">Njal.la</a></strong> "
|
||||
+ "— privacy-friendly, no personal details required, accepts Bitcoin. "
|
||||
+ dpEsc(serviceName) + " gets its own hostname: a subdomain (e.g. <code>" + hostExample + ".yourdomain.com</code>) "
|
||||
+ "or a separate domain — your choice. Subdomains are free, and one domain can have many.</li>";
|
||||
html += "<li><strong>Access to your router</strong> — you'll forward ports <strong>80</strong> and <strong>443</strong> (TCP) "
|
||||
+ "to this computer once. All domain-based services share these two ports; "
|
||||
+ "they're required for HTTPS and SSL certificates.</li>";
|
||||
html += "</ol>";
|
||||
} else {
|
||||
html += "<p><strong>Each service below needs two things set up:</strong></p>";
|
||||
html += '<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">';
|
||||
html += "<li><strong>A domain of your own from <a href=\"https://njal.la\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:var(--accent-color);\">Njal.la</a></strong> "
|
||||
+ "— privacy-friendly, no personal details required, accepts Bitcoin. "
|
||||
+ "Each service gets its own hostname: its own subdomain (e.g. <code>" + hostExample + ".yourdomain.com</code>) "
|
||||
+ "or a separate domain — your choice. Subdomains are free, and one domain can have many, "
|
||||
+ "so a single domain can serve every service.</li>";
|
||||
html += "<li><strong>Access to your router</strong> — you'll forward ports <strong>80</strong> and <strong>443</strong> (TCP) "
|
||||
+ "to this computer once. All domain-based services share these two ports; "
|
||||
+ "they're required for HTTPS and SSL certificates.</li>";
|
||||
html += "</ol>";
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/* ── "How to set it up at Njal.la" steps ───────────────────────────
|
||||
opts.hostExample — host part used in the examples (default "call").
|
||||
opts.pasteHint — where the curl command goes:
|
||||
"below" (single service) or "next to its service below"
|
||||
(onboarding, many services). */
|
||||
function renderNjallaStepsHtml(opts) {
|
||||
var hostExample = dpEsc((opts && opts.hostExample) || "call");
|
||||
var pasteHint = (opts && opts.pasteHint) || "below";
|
||||
var html = "";
|
||||
|
||||
html += "<p style=\"margin-top:12px;\"><strong>How to set it up at Njal.la:</strong></p>";
|
||||
html += '<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">';
|
||||
html += "<li>Create an account at <a href=\"https://njal.la\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:var(--accent-color);\">https://njal.la</a> and buy a domain.</li>";
|
||||
html += "<li>Add a <strong>Dynamic</strong> record for the hostname:"
|
||||
+ '<ul style="margin:4px 0 0 16px;padding:0;line-height:1.7;">'
|
||||
+ "<li>In the Njal.la <strong>Name</strong> field, type ONLY the host part — the word before your domain.<br>"
|
||||
+ "(For "" + hostExample + ".yourdomain.com" you'd type just: <code>" + hostExample + "</code>.)<br>"
|
||||
+ "⚠ Do NOT type the full domain here — Njal.la adds it automatically.</li>"
|
||||
+ "<li>Dedicating a whole separate domain to this service? Leave the Name field blank or use <code>@</code>.</li>"
|
||||
+ "</ul>"
|
||||
+ "</li>";
|
||||
html += "<li>A Dynamic record has <strong>NO IP field</strong> — you don't enter an IP anywhere. "
|
||||
+ "It auto-fills once Sovran_SystemsOS runs the update command (on save, and again after every reboot).</li>";
|
||||
html += "<li>Njal.la gives you a curl command, e.g.:<br>"
|
||||
+ '<code style="font-size:0.8em;">curl "https://njal.la/update/?h=' + hostExample + '.yourdomain.com&k=abc123&auto"</code><br>'
|
||||
+ "Copy it and paste it " + pasteHint + ".</li>";
|
||||
html += "</ol>";
|
||||
return html;
|
||||
}
|
||||
|
||||
/* ── "One router task" port-forwarding box ─────────────────────────
|
||||
opts.internalIp — LAN IP string, or empty/null for generic wording.
|
||||
opts.plural — true for multi-service (onboarding) wording.
|
||||
opts.includeSsh — also mention port 22 for SSH (onboarding).
|
||||
opts.extraNote — extra sentence appended at the end (optional). */
|
||||
function renderRouterPortsHtml(opts) {
|
||||
opts = opts || {};
|
||||
var ipPart = opts.internalIp
|
||||
? " to this computer’s internal IP <strong>" + dpEsc(opts.internalIp) + "</strong>"
|
||||
: " to this computer’s internal IP";
|
||||
var serviceWord = opts.plural ? "services" : "service";
|
||||
|
||||
var html = "";
|
||||
html += "🔌 <strong>One router task:</strong> in your router’s <strong>port forwarding</strong> settings, "
|
||||
+ "forward port <strong>80 (TCP)</strong> and port <strong>443 (TCP)</strong>"
|
||||
+ ipPart + ". Use the <strong>same number for the internal and external port</strong>. "
|
||||
+ "This only needs to be done once — all domain services share these ports — "
|
||||
+ "but HTTPS and SSL certificates won’t work without them, so your " + serviceWord + " "
|
||||
+ "can’t be reached from outside your home network. "
|
||||
+ "You’ll need normal access to your router’s settings with working port forwarding — "
|
||||
+ "if your ISP blocks it (e.g. CGNAT), domain-based services can’t be reached from the internet. ";
|
||||
if (opts.includeSsh) {
|
||||
html += "Add port <strong>22 (TCP)</strong> as well if you want remote SSH access. ";
|
||||
}
|
||||
if (opts.extraNote) {
|
||||
html += dpEsc(opts.extraNote);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/* ── Async helper: fill a router-box container with the internal IP ──
|
||||
Renders generic text immediately, then upgrades to the concrete LAN IP
|
||||
if /api/network provides one. Best-effort — never blocks the UI. */
|
||||
function renderRouterPortsBox(elId, opts) {
|
||||
var el = document.getElementById(elId);
|
||||
if (!el) return;
|
||||
el.innerHTML = renderRouterPortsHtml(opts || {});
|
||||
fetch("/api/network")
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var target = document.getElementById(elId);
|
||||
if (!target) return;
|
||||
var ip = data && data.internal_ip;
|
||||
if (ip && ip !== "unavailable") {
|
||||
var next = {};
|
||||
for (var k in (opts || {})) next[k] = opts[k];
|
||||
next.internalIp = String(ip).trim();
|
||||
target.innerHTML = renderRouterPortsHtml(next);
|
||||
}
|
||||
})
|
||||
.catch(function() { /* generic wording already shown — fine */ });
|
||||
}
|
||||
@@ -86,45 +86,17 @@ function openDomainSetupModal(feat, onSaved) {
|
||||
var domainPlaceholder = isWalletConnections ? "lightning.yourdomain.com" : "myservice.example.com";
|
||||
var domainLabelExample = isWalletConnections ? "lightning.yourdomain.com" : "call.yourdomain.com";
|
||||
|
||||
var introHtml;
|
||||
if (_currentRole === "node") {
|
||||
introHtml =
|
||||
'<p>To enable <strong>' + escHtml(feat.name) + '</strong>, it needs its own domain from Njal.la.</p>' +
|
||||
'<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">' +
|
||||
'<li>Create an account at <a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">njal.la</a>.</li>' +
|
||||
'<li>Set up a domain for it — either a free subdomain or a separate domain. Pick one option:</li>' +
|
||||
'</ol>';
|
||||
} else {
|
||||
introHtml =
|
||||
'<p>To enable <strong>' + escHtml(feat.name) + '</strong>, it needs its own domain from Njal.la. ' +
|
||||
'In your Njal.la account, set up a domain for it — either a free subdomain or a separate domain. Pick one option:</p>';
|
||||
}
|
||||
// Shared instructions (single source of truth: static/js/domain-prereqs.js) —
|
||||
// identical wording to the Server + Desktop onboarding wizard and every other
|
||||
// domain-based feature, regardless of role (Node / Desktop / Server+Desktop).
|
||||
var hostExample = isWalletConnections ? "lightning" : "call";
|
||||
|
||||
$domainSetupBody.innerHTML =
|
||||
'<div class="domain-setup-intro">' +
|
||||
nwcWarning +
|
||||
introHtml +
|
||||
'<details style="margin-top:10px;">' +
|
||||
'<summary style="cursor:pointer;font-weight:600;">Option A — Free subdomain (recommended)</summary>' +
|
||||
'<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">' +
|
||||
'<li>In Njal.la, open a domain you own and click "Add record".</li>' +
|
||||
'<li>Set record type to <strong>Dynamic</strong>.</li>' +
|
||||
'<li>In the <strong>Name</strong> field, type ONLY the host part — the word before your domain.<br>' +
|
||||
'(Example only, your choice — for "' + domainLabelExample + '" you'd type just: <code>' + (isWalletConnections ? 'lightning' : 'call') + '</code>)<br>' +
|
||||
'⚠ Do NOT type the full domain here — Njal.la adds it automatically.</li>' +
|
||||
'<li>A Dynamic record has NO IP field — the IP auto-fills after the rebuild/reboot.</li>' +
|
||||
'<li>Copy the curl command Njal.la gives you, e.g.:<br>' +
|
||||
'<code style="font-size:0.8em;">curl "https://njal.la/update/?h=' + domainLabelExample + '&k=abc123&auto"</code></li>' +
|
||||
'</ol>' +
|
||||
'</details>' +
|
||||
'<details style="margin-top:6px;">' +
|
||||
'<summary style="cursor:pointer;font-weight:600;">Option B — Separate / new domain</summary>' +
|
||||
'<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">' +
|
||||
'<li>In Njal.la, buy the domain you want.</li>' +
|
||||
'<li>Add a Dynamic record as in Option A. If this domain is dedicated to the service, leave the Name field blank or use <code>@</code>.</li>' +
|
||||
'<li>Copy the curl command Njal.la gives you.</li>' +
|
||||
'</ol>' +
|
||||
'</details>' +
|
||||
renderDomainNeedsHtml({ serviceName: feat.name, hostExample: hostExample }) +
|
||||
renderNjallaStepsHtml({ hostExample: hostExample, pasteHint: "below" }) +
|
||||
'<div class="onboarding-port-warn" id="domain-router-box" style="margin-top:12px;"></div>' +
|
||||
'<p style="margin-top:10px;">Below, enter the full domain for this service — a subdomain (e.g. ' + domainLabelExample + ') or a separate domain — and paste its curl command.</p>' +
|
||||
'</div>' +
|
||||
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" /></div>' +
|
||||
@@ -170,6 +142,9 @@ function openDomainSetupModal(feat, onSaved) {
|
||||
});
|
||||
|
||||
$domainSetupModal.classList.add("open");
|
||||
|
||||
// Fill the router port-forwarding box with this computer's LAN IP (best-effort)
|
||||
renderRouterPortsBox("domain-router-box");
|
||||
}
|
||||
|
||||
function openDomainReconfigureModal(feat, existingDomain, onSaved) {
|
||||
@@ -218,7 +193,9 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
|
||||
'<span style="display:inline-block;margin-top:4px;padding:4px 10px;background:var(--card-color);border:1px solid var(--border-color);border-radius:6px;font-family:monospace;font-size:1em;font-weight:700;">' + escHtml(externalIp) + '</span></li>' +
|
||||
'<li>If the IP is wrong or the record is missing, update it</li>' +
|
||||
'<li>If you changed the DDNS curl command, paste the updated one below</li>' +
|
||||
'<li>Confirm ports <strong>80</strong> and <strong>443</strong> (TCP) are still forwarded on your router to this computer — see the reminder below:</li>' +
|
||||
'</ol>' +
|
||||
'<div class="onboarding-port-warn" id="domain-router-box" style="margin-top:12px;"></div>' +
|
||||
'</div>' +
|
||||
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" value="' + escHtml(currentDomain) + '" /></div>' +
|
||||
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl "https://njal.la/update/?h=' + domainPlaceholder + '&k=abc123&auto"" /><p class="domain-field-hint">ℹ Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
|
||||
@@ -263,6 +240,9 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
|
||||
});
|
||||
|
||||
$domainSetupModal.classList.add("open");
|
||||
|
||||
// Fill the router port-forwarding box with this computer's LAN IP (best-effort)
|
||||
renderRouterPortsBox("domain-router-box");
|
||||
}
|
||||
|
||||
function closeDomainSetupModal() {
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
// ── Service detail modal ──────────────────────────────────────────
|
||||
|
||||
function _getZeusConnectGuideHtml() {
|
||||
return '<div class="nwc-connect-guide">' +
|
||||
'<div class="nwc-connect-guide-title">📱 Connect to Zeus</div>' +
|
||||
'<p class="nwc-connect-guide-intro">This connection URL is an <strong>LND REST</strong> connection — the direct way to use Zeus with your node for full admin access. It connects securely through Tor, allowing you to manage channels, balances, and your full node on the go.</p>' +
|
||||
'<div class="nwc-connect-steps">' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">1</div><div><strong>Download Zeus</strong> from the App Store or Google Play.</div></div>' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">2</div><div>Open Zeus and open the <strong>Wallets</strong> screen.</div></div>' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">3</div><div>Tap the <strong>+ (Add Wallet)</strong> button in the top-right corner.</div></div>' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">4</div><div>On <strong>Wallet Configuration</strong>, tap the <strong>scan icon</strong> in the top-right corner, then scan the QR code above.</div></div>' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">5</div><div>Zeus detects the LND REST QR and fills in the connection details. Review them, then tap <strong>Save Wallet Config</strong>.</div></div>' +
|
||||
'</div>' +
|
||||
'<div class="nwc-connect-note"><strong>💡 Note:</strong> This is <em>not</em> the same as the NWC pairing QR shown in Lightning Wallet Connections — that gives your wallet sandboxed, limited access for everyday spending. LND REST connects Zeus directly to your node for full admin control.</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function _renderCredsHtml(credentials, unit) {
|
||||
var html = "";
|
||||
for (var i = 0; i < credentials.length; i++) {
|
||||
@@ -9,7 +24,10 @@ function _renderCredsHtml(credentials, unit) {
|
||||
var id = "cred-" + Math.random().toString(36).substring(2, 8);
|
||||
var qrBlock = "";
|
||||
if (cred.qrcode) {
|
||||
qrBlock = '<div class="creds-qr-wrap"><img class="creds-qr-img" src="' + cred.qrcode + '" alt="QR Code for ' + escHtml(cred.label) + '"><div class="creds-qr-hint">Scan with Zeus app on your phone</div></div>';
|
||||
var qrHint = (unit === "zeus-connect-setup.service")
|
||||
? "This is an <strong>LND REST</strong> connection QR — in Zeus, add a wallet and use the scan icon (see steps below)."
|
||||
: "In Zeus: <em>Wallets → + → scan icon</em>. This is an <strong>LND REST</strong> QR for direct node access.";
|
||||
qrBlock = '<div class="creds-qr-wrap"><img class="creds-qr-img" src="' + cred.qrcode + '" alt="QR Code for ' + escHtml(cred.label) + '"><div class="creds-qr-hint">' + qrHint + '</div></div>';
|
||||
}
|
||||
// If qronly, render the label + QR block only — skip value and copy button
|
||||
if (cred.qronly) {
|
||||
@@ -174,7 +192,7 @@ function _nwcRenderWalletState() {
|
||||
var selectedLimited = state.createForm.access_preset === "send_receive_limited";
|
||||
html +=
|
||||
'<div class="nwc-tab-intro-title">Create a Wallet</div>' +
|
||||
'<p class="nwc-tab-intro-desc">An isolated wallet you pair to a single app, with its own Lightning Address.</p>' +
|
||||
'<p class="nwc-tab-intro-desc">Create a secure, sandboxed wallet for a specific app. Each wallet gets its own Lightning Address and optional spending limit.</p>' +
|
||||
'<div class="matrix-form-group"><label class="matrix-form-label" for="nwc-wallet-name">Wallet Name</label>' +
|
||||
'<input class="matrix-form-input" id="nwc-wallet-name" type="text" placeholder="My Wallet" value="' + escHtml(state.createForm.name || "") + '" autocomplete="off"></div>' +
|
||||
'<div class="matrix-form-group"><label class="matrix-form-label" for="nwc-wallet-alias">Lightning Address Alias</label>' +
|
||||
@@ -218,7 +236,8 @@ function _nwcRenderWalletState() {
|
||||
var pairId = "nwc-pairing-uri-" + Math.random().toString(36).substring(2, 8);
|
||||
html += '<div class="nwc-secret-warning">⚠ One-time pairing secret. Save it now — it will not be shown again.</div>';
|
||||
if (created.pairing_qrcode) {
|
||||
html += '<div class="creds-qr-wrap"><img class="creds-qr-img" src="' + created.pairing_qrcode + '" alt="QR code for Lightning Wallet Connections pairing secret"><div class="creds-qr-hint">Scan now in Zeus or copy the URI below.</div></div>';
|
||||
html += '<div class="creds-row"><div class="creds-label">QR Code</div>' +
|
||||
'<div class="creds-qr-wrap"><img class="creds-qr-img" src="' + created.pairing_qrcode + '" alt="QR code for Lightning Wallet Connections pairing secret"><div class="creds-qr-hint">This is an <strong>NWC</strong> pairing QR — in Zeus, add a wallet and use the scan icon (see steps below).</div></div></div>';
|
||||
}
|
||||
html += '<div class="creds-row"><div class="creds-label">Pairing URI</div>' +
|
||||
'<div class="creds-value-wrap"><div class="creds-value" id="' + pairId + '">' + escHtml(created.pairing_uri || "Unavailable") + '</div><button class="creds-copy-btn" data-target="' + pairId + '">Copy</button></div></div>';
|
||||
@@ -226,6 +245,18 @@ function _nwcRenderWalletState() {
|
||||
html += '<div class="creds-row"><div class="creds-label">Lightning Address</div>' +
|
||||
'<div class="creds-value-wrap"><div class="creds-value">' + escHtml(created.wallet.lightning_address) + '</div></div></div>';
|
||||
}
|
||||
html += '<div class="nwc-connect-guide">' +
|
||||
'<div class="nwc-connect-guide-title">📱 Connect to Zeus</div>' +
|
||||
'<p class="nwc-connect-guide-intro">This pairing URI is an <strong>NWC (Nostr Wallet Connect)</strong> connection — the modern, mobile-friendly way to use Zeus with your node. It connects directly through your Lightning domain, so no Tor or port forwarding is needed on your phone.</p>' +
|
||||
'<div class="nwc-connect-steps">' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">1</div><div><strong>Download Zeus</strong> from the App Store or Google Play.</div></div>' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">2</div><div>Open Zeus and open the <strong>Wallets</strong> screen.</div></div>' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">3</div><div>Tap the <strong>+ (Add Wallet)</strong> button in the top-right corner.</div></div>' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">4</div><div>On <strong>Wallet Configuration</strong>, tap the <strong>scan icon</strong> in the top-right corner, then scan the QR code above.</div></div>' +
|
||||
'<div class="nwc-connect-step"><div class="nwc-step-num">5</div><div>Zeus detects the NWC QR and fills in <strong>Nostr Wallet Connect</strong>. Review it, then tap <strong>Save Wallet Config</strong>.</div></div>' +
|
||||
'</div>' +
|
||||
'<div class="nwc-connect-note"><strong>💡 Note:</strong> This is <em>not</em> the same as the LND REST / Tor QR shown on your LND tile — that connects Zeus directly to your Lightning node for full admin control. NWC gives your wallet sandboxed, limited access for everyday spending.</div>' +
|
||||
'</div>';
|
||||
html += '<div class="matrix-form-actions">' +
|
||||
'<button class="matrix-form-back" id="nwc-created-another-btn"' + (state.busy ? " disabled" : "") + '>Create Another Wallet</button>' +
|
||||
'<button class="matrix-form-submit" id="nwc-created-continue-btn"' + (state.busy ? " disabled" : "") + '>I Saved This Secret</button>' +
|
||||
@@ -330,9 +361,9 @@ function _nwcRenderWalletState() {
|
||||
|
||||
if (!state.wallets || state.wallets.length === 0) {
|
||||
html += '<div class="nwc-empty-state">' +
|
||||
'<div class="nwc-empty-icon">👛</div>' +
|
||||
'<div class="nwc-empty-title">No wallets yet</div>' +
|
||||
'<p class="nwc-empty-desc">Create your first wallet to get a one-time pairing secret for an app, plus a reusable Lightning Address.</p>' +
|
||||
'<div class="nwc-empty-icon">⚡</div>' +
|
||||
'<div class="nwc-empty-title">Ready to start spending</div>' +
|
||||
'<p class="nwc-empty-desc">Create your first isolated wallet to connect to apps like Zeus (via NWC) or Nostr. Experience faster, more secure Lightning payments today.</p>' +
|
||||
'</div>';
|
||||
host.innerHTML = html;
|
||||
_nwcWireDomainLink();
|
||||
@@ -761,9 +792,32 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
if (isNwc) {
|
||||
if (effectiveEnabled || data.enabled) {
|
||||
html += '<div class="nwc-tab-intro">' +
|
||||
'<div>' +
|
||||
'<div class="nwc-tab-intro-title">Your Lightning Wallets</div>' +
|
||||
'<p class="nwc-tab-intro-desc">Each wallet is isolated: pair it to one app with a one-time secret, and share its Lightning Address to get paid.</p>' +
|
||||
'<div class="nwc-intro-header">' +
|
||||
'<div class="nwc-tab-intro-title">Lightning Wallet Connections</div>' +
|
||||
'<p class="nwc-tab-intro-desc">Powerful, isolated wallets for your daily spending and modern apps.</p>' +
|
||||
'</div>' +
|
||||
'<div class="nwc-benefits-grid">' +
|
||||
'<div class="nwc-benefit-item">' +
|
||||
'<div class="nwc-benefit-icon">🛡️</div>' +
|
||||
'<div class="nwc-benefit-content">' +
|
||||
'<strong>Isolated & Secure</strong>' +
|
||||
'<p>Create sandboxed wallets for quick spending while keeping your main node protected. Isolated access is faster than Tor and perfect for budgeting.</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="nwc-benefit-item">' +
|
||||
'<div class="nwc-benefit-icon">🌐</div>' +
|
||||
'<div class="nwc-benefit-content">' +
|
||||
'<strong>Modern Ecosystem</strong>' +
|
||||
'<p>Easily spend and receive bitcoin using LNURL and Nostr (NWC) across the growing ecosystem of decentralized apps.</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="nwc-benefit-item">' +
|
||||
'<div class="nwc-benefit-icon">📱</div>' +
|
||||
'<div class="nwc-benefit-content">' +
|
||||
'<strong>Zeus on the Go (via NWC)</strong>' +
|
||||
'<p>Connect Zeus to your wallet using NWC — no Tor, no port forwarding. Create a wallet below, scan the pairing QR, and start spending from your phone.</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="nwc-wallets-body"><p class="creds-loading">Loading wallets…</p></div>' +
|
||||
@@ -813,6 +867,7 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
html += '<div class="svc-detail-section">' +
|
||||
'<div class="svc-detail-section-title">Credentials & Access</div>' +
|
||||
_renderCredsHtml(data.credentials, unit) +
|
||||
(unit === "zeus-connect-setup.service" ? _getZeusConnectGuideHtml() : "") +
|
||||
(unit === "matrix-synapse.service" ?
|
||||
'<hr class="matrix-actions-divider"><div class="matrix-actions-row">' +
|
||||
'<button class="matrix-action-btn" id="matrix-add-user-btn">➕ Add New User</button>' +
|
||||
@@ -1030,6 +1085,9 @@ async function openCredsModal(unit, name, icon) {
|
||||
'<button class="matrix-action-btn" id="matrix-change-pw-btn">🔑 Change Password</button>' +
|
||||
'</div>';
|
||||
}
|
||||
if (unit === "zeus-connect-setup.service") {
|
||||
html += _getZeusConnectGuideHtml();
|
||||
}
|
||||
$credsBody.innerHTML = html;
|
||||
_attachCopyHandlers($credsBody);
|
||||
if (unit === "matrix-synapse.service") {
|
||||
|
||||
@@ -473,11 +473,27 @@ function renderBackupReady(drives) {
|
||||
].join("");
|
||||
}
|
||||
|
||||
$supportBody.innerHTML = [
|
||||
'<div class="support-section">',
|
||||
'<div class="support-icon-big">\ud83d\udcbe</div>',
|
||||
'<h3 class="support-heading">Manual Backup</h3>',
|
||||
// ── Role-aware backup description ─────────────────────────────
|
||||
// Desktop Only systems run no server or Bitcoin services and have no
|
||||
// internal second drive, so the backup mirrors only the NixOS configuration
|
||||
// and home directory. nix-bitcoin secrets, /var/lib system service data,
|
||||
// and the database/blockchain caveats apply only to the Node and
|
||||
// Server + Desktop roles.
|
||||
var isDesktopOnly = (_currentRole === "desktop");
|
||||
|
||||
var introHtml;
|
||||
if (isDesktopOnly) {
|
||||
introHtml = [
|
||||
'<div class="support-wallet-box support-wallet-protected" style="margin-bottom:16px;">',
|
||||
'<p class="support-wallet-desc">',
|
||||
'This manual backup lets you create a copy of your system on an external USB drive \u2014 ',
|
||||
'storing your data in a second location, outside the computer, for maximum protection ',
|
||||
'against hardware failure or physical damage.',
|
||||
'</p>',
|
||||
'</div>',
|
||||
].join("");
|
||||
} else {
|
||||
introHtml = [
|
||||
'<div class="support-wallet-box support-wallet-protected" style="margin-bottom:16px;">',
|
||||
'<p class="support-wallet-desc">',
|
||||
'Your Sovran Pro already backs up your data automatically to its internal second drive. ',
|
||||
@@ -486,6 +502,44 @@ function renderBackupReady(drives) {
|
||||
'against hardware failure or physical damage.',
|
||||
'</p>',
|
||||
'</div>',
|
||||
].join("");
|
||||
}
|
||||
|
||||
var backupItemsHtml;
|
||||
if (isDesktopOnly) {
|
||||
backupItemsHtml = [
|
||||
'<li>NixOS configuration (<code>/etc/nixos</code>)</li>',
|
||||
'<li>Home directory (<code>/home</code>)</li>',
|
||||
].join("");
|
||||
} else {
|
||||
backupItemsHtml = [
|
||||
'<li>NixOS configuration (<code>/etc/nixos</code>)</li>',
|
||||
'<li>nix-bitcoin secrets (<code>/etc/nix-bitcoin-secrets</code>)</li>',
|
||||
'<li>System service data (<code>/var/lib</code>) — excluding databases and blockchain data (see note below)</li>',
|
||||
'<li>Home directory (<code>/home</code>)</li>',
|
||||
].join("");
|
||||
}
|
||||
|
||||
// The database/blockchain caveat is only relevant when server services exist.
|
||||
var dbNoteHtml = "";
|
||||
if (!isDesktopOnly) {
|
||||
dbNoteHtml = [
|
||||
'<div class="support-wallet-box support-wallet-warning">',
|
||||
'<div class="support-wallet-header">',
|
||||
'<span class="support-wallet-icon">\u2139\ufe0f</span>',
|
||||
'<span class="support-wallet-title">Database and Blockchain Data</span>',
|
||||
'</div>',
|
||||
'<p class="support-wallet-desc">Application databases stored in PostgreSQL or MariaDB/MySQL are <strong>not included</strong> in Manual Backup. Bitcoin blockchain and Electrs index data are also excluded (they are stored on the internal second drive). If you use Nextcloud, Matrix, or other database-backed applications, back up those databases separately with their native tools.</p>',
|
||||
'</div>',
|
||||
].join("");
|
||||
}
|
||||
|
||||
$supportBody.innerHTML = [
|
||||
'<div class="support-section">',
|
||||
'<div class="support-icon-big">\ud83d\udcbe</div>',
|
||||
'<h3 class="support-heading">Manual Backup</h3>',
|
||||
|
||||
introHtml,
|
||||
|
||||
'<div class="support-steps">',
|
||||
'<div class="support-steps-title">Requirements</div>',
|
||||
@@ -500,20 +554,11 @@ function renderBackupReady(drives) {
|
||||
'<div class="support-steps">',
|
||||
'<div class="support-steps-title">What gets backed up</div>',
|
||||
'<ol class="support-backup-steps">',
|
||||
'<li>NixOS configuration (<code>/etc/nixos</code>)</li>',
|
||||
'<li>nix-bitcoin secrets (<code>/etc/nix-bitcoin-secrets</code>)</li>',
|
||||
'<li>System service data (<code>/var/lib</code>) — excluding databases and blockchain data (see note below)</li>',
|
||||
'<li>Home directory (<code>/home</code>)</li>',
|
||||
backupItemsHtml,
|
||||
'</ol>',
|
||||
'</div>',
|
||||
|
||||
'<div class="support-wallet-box support-wallet-warning">',
|
||||
'<div class="support-wallet-header">',
|
||||
'<span class="support-wallet-icon">\u2139\ufe0f</span>',
|
||||
'<span class="support-wallet-title">Database and Blockchain Data</span>',
|
||||
'</div>',
|
||||
'<p class="support-wallet-desc">Application databases stored in PostgreSQL or MariaDB/MySQL are <strong>not included</strong> in Manual Backup. Bitcoin blockchain and Electrs index data are also excluded (they are stored on the internal second drive). If you use Nextcloud, Matrix, or other database-backed applications, back up those databases separately with their native tools.</p>',
|
||||
'</div>',
|
||||
dbNoteHtml,
|
||||
|
||||
'<div class="support-wallet-box support-wallet-protected">',
|
||||
'<div class="support-wallet-header">',
|
||||
|
||||
@@ -357,38 +357,24 @@ async function loadStep3() {
|
||||
if (relevantDomains.length === 0) {
|
||||
html += '<p class="onboarding-body-text">No domain-based services are enabled for your role. You can skip this step.</p>';
|
||||
} else {
|
||||
// Shared instructions (single source of truth: static/js/domain-prereqs.js) —
|
||||
// identical wording to the feature-enable domain modal shown for NWC,
|
||||
// BTCPay Server, and every other domain-based feature, in every role.
|
||||
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
|
||||
+ '<p style="margin:0 0 8px;"><strong>Sovran_SystemsOS uses Njal.la for domains and Dynamic DNS.</strong></p>'
|
||||
+ '<ol style="margin:8px 0 0 16px; padding:0; line-height:1.7;">'
|
||||
+ '<li>Create an account at <a href="https://njal.la" target="_blank" style="color:var(--accent-color);">https://njal.la</a>.</li>'
|
||||
+ '<li>Buy at least one domain. Each service below needs its own domain — you can either give each service its own subdomain of a single domain you buy (subdomains are free, and one domain can have many), OR use a separate domain for each. Your choice.</li>'
|
||||
+ '<li>For each service, add a <strong>Dynamic</strong> record in Njal.la:'
|
||||
+ '<ul style="margin:4px 0 0 16px;padding:0;line-height:1.7;">'
|
||||
+ '<li>In the Njal.la <strong>Name</strong> field, type ONLY the host part — the word before your domain.<br>'
|
||||
+ '(Example only, your choice — for "call.yourdomain.com" you'd type just: <code>call</code>.)<br>'
|
||||
+ 'If you bought a whole separate domain just for this service, leave Name blank or use <code>@</code>.<br>'
|
||||
+ '⚠ Do NOT type the full domain in the Name field — Njal.la adds it automatically.</li>'
|
||||
+ '<li>A Dynamic record has NO IP field. You don't enter an IP anywhere — it auto-fills once Sovran_SystemsOS updates it (on save, and again after reboot).</li>'
|
||||
+ '</ul>'
|
||||
+ '</li>'
|
||||
+ '<li>Njal.la gives you a curl command like:<br>'
|
||||
+ '<code style="font-size:0.8em;">curl "https://njal.la/update/?h=call.yourdomain.com&k=abc123&auto"</code></li>'
|
||||
+ '</ol>'
|
||||
+ renderDomainNeedsHtml({ serviceName: null, hostExample: "call" })
|
||||
+ renderNjallaStepsHtml({ hostExample: "call", pasteHint: "next to its service below" })
|
||||
+ '</div>';
|
||||
html += '<p class="onboarding-hint">Enter each service\'s full domain — a subdomain (e.g. <code>call.yourdomain.com</code>) or a separate domain (e.g. <code>call.com</code>) — and its Njal.la DDNS curl command.</p>';
|
||||
|
||||
// Compact router note (full port guidance is shown when a feature is
|
||||
// enabled, and lives on each service tile afterwards)
|
||||
var routerIpPart = internalIp
|
||||
? ' to this computer’s internal IP <strong>' + escHtml(internalIp) + '</strong>'
|
||||
: ' to this computer’s internal IP';
|
||||
// Router note (the same wording is shown again, per-service, whenever a
|
||||
// domain-based feature is enabled, and lives on each service tile afterwards)
|
||||
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
|
||||
+ '🔌 <strong>One router task:</strong> in your router’s <strong>port forwarding</strong> settings, forward '
|
||||
+ 'port <strong>80 (TCP)</strong> and port <strong>443 (TCP)</strong>'
|
||||
+ routerIpPart + '. Use the <strong>same number for the internal and external port</strong>. '
|
||||
+ 'These are required for HTTPS and SSL certificates — without them your services cannot be reached from outside your home network. '
|
||||
+ 'Add port <strong>22 (TCP)</strong> as well if you want remote SSH access. '
|
||||
+ 'Element Call needs a few extra ports (some UDP), and you’ll be shown exactly which when you enable it.'
|
||||
+ renderRouterPortsHtml({
|
||||
internalIp: internalIp,
|
||||
plural: true,
|
||||
includeSsh: true,
|
||||
extraNote: "Element Call needs a few extra ports (some UDP), and you’ll be shown exactly which when you enable it.",
|
||||
})
|
||||
+ '</div>';
|
||||
|
||||
relevantDomains.forEach(function(d) {
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
<!-- Header bar -->
|
||||
<header class="header-bar">
|
||||
<img src="/static/sovran-hub-icon.svg" alt="Sovran Hub" class="header-logo" />
|
||||
|
||||
<div class="title-group">
|
||||
<span class="title">Sovran_SystemsOS Hub</span>
|
||||
|
||||
<!-- OS Version Badge -->
|
||||
@@ -28,6 +30,7 @@
|
||||
<span class="version-label">v</span>
|
||||
<span class="version-number" id="version-number">{{ sovran_version }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="header-buttons">
|
||||
<span class="role-badge" id="role-badge">Loading…</span>
|
||||
@@ -309,6 +312,7 @@
|
||||
<script src="/static/js/constants.js?v={{ asset_version }}"></script>
|
||||
<script src="/static/js/state.js?v={{ asset_version }}"></script>
|
||||
<script src="/static/js/helpers.js?v={{ asset_version }}"></script>
|
||||
<script src="/static/js/domain-prereqs.js?v={{ asset_version }}"></script>
|
||||
<script src="/static/js/tiles.js?v={{ asset_version }}"></script>
|
||||
<script src="/static/js/service-detail.js?v={{ asset_version }}"></script>
|
||||
<script src="/static/js/support.js?v={{ asset_version }}"></script>
|
||||
|
||||
@@ -125,9 +125,8 @@
|
||||
<span class="onboarding-step-icon">🌐</span>
|
||||
<h2 class="onboarding-step-title">Domain Configuration</h2>
|
||||
<p class="onboarding-step-desc">
|
||||
Sovran_SystemsOS uses <strong><a href="https://njal.la" target="_blank" style="color: var(--accent-color);">Njal.la</a></strong> for domains and Dynamic DNS.
|
||||
Create an account at Njal.la, then for each service below, add a <strong>Dynamic</strong> 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 <strong><a href="https://njal.la" target="_blank" style="color: var(--accent-color);">Njal.la</a></strong> for domains and Dynamic DNS, and your router needs ports <strong>80</strong> and <strong>443</strong> (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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="onboarding-card" id="step-3-body">
|
||||
@@ -170,6 +169,7 @@
|
||||
</div><!-- /panel-wrap -->
|
||||
</div><!-- /shell -->
|
||||
|
||||
<script src="/static/js/domain-prereqs.js?v={{ asset_version }}"></script>
|
||||
<script src="/static/onboarding.js?v={{ onboarding_js_hash }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Generated
+24
-24
@@ -5,11 +5,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1784932759,
|
||||
"narHash": "sha256-44/iCx+wiYukHGhvPm65ppJZ3FZZbp6f9JE5isz8TsA=",
|
||||
"lastModified": 1785778881,
|
||||
"narHash": "sha256-yHJJTm3o7ZhiVp68G+hh+dz1GKV+ACOT6mQj+eYvJLQ=",
|
||||
"owner": "emmanuelrosa",
|
||||
"repo": "btc-clients-nix",
|
||||
"rev": "8aab86c245ab9a2bea0d72175d6fd663a892af9f",
|
||||
"rev": "fc1aca94d839f82e7501fe0d01279d5c1fba6e63",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -92,11 +92,11 @@
|
||||
"nixpkgs-unstable": "nixpkgs-unstable"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1779253922,
|
||||
"narHash": "sha256-k5DpYVfyy27ELuEiV+51EfVg7B6vKUW63NWeA6eKGd0=",
|
||||
"lastModified": 1785177868,
|
||||
"narHash": "sha256-zlAZZkokXpeHHBP8Nt4MvLXOn1Js1qVi81VPjtVbRV8=",
|
||||
"owner": "fort-nix",
|
||||
"repo": "nix-bitcoin",
|
||||
"rev": "1496f842477976c085cd96f1837ea12444014088",
|
||||
"rev": "360e30fee5ba32f9fecc89bc35628195d9d2dbbe",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -108,11 +108,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1782911660,
|
||||
"narHash": "sha256-PbR+tJ5E/Ux+01UtdFKqblccVA4/FgWbkym4ev3VHHQ=",
|
||||
"lastModified": 1785590095,
|
||||
"narHash": "sha256-CNO2szJbdLjVN/Hi1BML9MSALz1GM2fIdwnzs404QO8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "cf720c15e108d432d29041cc5a185630809acefb",
|
||||
"rev": "e568f3b19d54b08f48bfae9b12b3e124d1a28002",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -139,11 +139,11 @@
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1784856561,
|
||||
"narHash": "sha256-J+Bx1Z6Oeoj2FgnBhRMKyUhhtDoOpTgXYaVLZpDjW4A=",
|
||||
"lastModified": 1785734586,
|
||||
"narHash": "sha256-ODZkEK9Gy50yg6h98u7KkitZ3oc/uuTFK00bh1CRdNA=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "597283ad8aa0b331c788e97c4c262d58877074ef",
|
||||
"rev": "531670d871c0e29724a02f3cbcac170adc65b58c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -155,11 +155,11 @@
|
||||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1778869304,
|
||||
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
|
||||
"lastModified": 1784453100,
|
||||
"narHash": "sha256-zpGZb8FYui+i8KLtC0nlcmb0voR2zB80Qn8pe7lzIbk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
|
||||
"rev": "b471514bed69eff5255c8e63c1f80e5fe56c616f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -171,11 +171,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1778737229,
|
||||
"narHash": "sha256-6xWoytx8jFW4PF1GjRm/i/53trbpKGfz6zjzQGBr4cI=",
|
||||
"lastModified": 1782847189,
|
||||
"narHash": "sha256-twXPFqFsrrY5r28Zh7Homgcp2gUMBgQ6WDS98Q/3xFI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d7a713c0b7e47c908258e71cba7a2d77cc8d71d5",
|
||||
"rev": "b6018f87da91d19d0ab4cf979885689b469cdd41",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -187,11 +187,11 @@
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1784796856,
|
||||
"narHash": "sha256-wWFrV5/Qbm+lyt5x20E/bSbfJiGKMo4RCxZV8cl/WZI=",
|
||||
"lastModified": 1785828668,
|
||||
"narHash": "sha256-8fsyqeO+mJqvIzeO4xIpgJe/f7MTbbVTEC6RT6WSXNs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e2587caef70cea85dd97d7daab492899902dbf5d",
|
||||
"rev": "e72e4f299401a3689d4b3d5fc6496b11db7064eb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -224,11 +224,11 @@
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1784814601,
|
||||
"narHash": "sha256-T32JXjZ7kIbhBn8/Har171yGg6IBdl97cxAWameqZDE=",
|
||||
"lastModified": 1785763201,
|
||||
"narHash": "sha256-wA373y/B9orM3HatLu9oS+Ke5lmdZyBl/bdjd2gLMq4=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixvim",
|
||||
"rev": "f316e949e0ed9df0e1e0bf645c6dce721d4e230e",
|
||||
"rev": "c7be49306b23a952c0151cf4bbeacd944ed82f2a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
let
|
||||
cfg = config.sovran_systemsOS;
|
||||
|
||||
# Read the OS version from the repo-root VERSION file at eval time so the
|
||||
# Hub always ships a real, baked-in version string instead of relying on
|
||||
# a runtime file lookup (which returns "dev" when /etc/nixos/VERSION is
|
||||
# missing, e.g. in a dev checkout — this is the "vdev" bug in the Hub UI).
|
||||
versionFile = ../../VERSION;
|
||||
sovranVersion =
|
||||
if builtins.pathExists versionFile
|
||||
then builtins.replaceStrings [ "v" "\n" "\r" ] [ "" "" "" ] (builtins.readFile versionFile)
|
||||
else "1.0.0";
|
||||
|
||||
monitoredServices =
|
||||
# ── Infrastructure — System Passwords (always present) ─────
|
||||
[
|
||||
@@ -55,7 +65,6 @@ let
|
||||
]; }
|
||||
{ name = "Zeus Connect"; unit = "zeus-connect-setup.service"; type = "system"; icon = "zeus"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
||||
{ label = "QR Code"; file = "/var/lib/secrets/zeus-connect-url"; qrcode = true; qronly = true; }
|
||||
{ label = "How to Connect"; value = "1. Download Zeus from App Store or Google Play\n2. Open Zeus → Scan Node Config\n3. Scan the QR code above"; }
|
||||
]; }
|
||||
{ name = "Sparrow Auto-Link"; unit = "sparrow-autoconnect.service"; type = "system"; icon = "sparrow"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
||||
{ label = "Server"; value = "tcp://127.0.0.1:50001 (Electrs)"; }
|
||||
@@ -116,6 +125,7 @@ let
|
||||
role = activeRole;
|
||||
services = monitoredServices;
|
||||
feature_manager = true;
|
||||
sovran_version = sovranVersion;
|
||||
});
|
||||
|
||||
generatedVersions = pkgs.writeText "sovran-hub-versions.json" (builtins.toJSON {
|
||||
@@ -331,6 +341,7 @@ let
|
||||
|
||||
cp ${generatedConfig} $out/lib/sovran-hub-web/config.json
|
||||
cp ${generatedVersions} $out/lib/sovran-hub-web/versions.json
|
||||
printf '%s' "${sovranVersion}" > $out/lib/sovran-hub-web/VERSION
|
||||
|
||||
install -d $out/share/sovran-hub/icons
|
||||
cp icons/* $out/share/sovran-hub/icons/ 2>/dev/null || true
|
||||
|
||||
+165
-33
@@ -29,11 +29,31 @@ CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Configuration
|
||||
GITEA_REMOTE="gitea"
|
||||
GITHUB_REMOTE="origin"
|
||||
GITEA_REMOTE_DEFAULT="gitea"
|
||||
GITHUB_REMOTE_DEFAULT="origin"
|
||||
GITEA_API_URL="https://git.sovransystems.com/api/v1"
|
||||
CHANGELOG_FILE="CHANGELOG.md"
|
||||
|
||||
# Auto-detect remotes
|
||||
detect_remote() {
|
||||
local preferred="$1"
|
||||
local keyword="$2"
|
||||
if git remote | grep -q -x "$preferred"; then
|
||||
echo "$preferred"
|
||||
return
|
||||
fi
|
||||
local found
|
||||
found=$(git remote -v | grep -i "$keyword" | head -n 1 | awk '{print $1}')
|
||||
if [[ -n "$found" ]]; then
|
||||
echo "$found"
|
||||
return
|
||||
fi
|
||||
echo "$preferred"
|
||||
}
|
||||
|
||||
GITEA_REMOTE=$(detect_remote "$GITEA_REMOTE_DEFAULT" "sovransystems\|gitea")
|
||||
GITHUB_REMOTE=$(detect_remote "$GITHUB_REMOTE_DEFAULT" "github")
|
||||
|
||||
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║ Sovran_SystemsOS Automated Stable Release Script ║${NC}"
|
||||
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
|
||||
@@ -110,7 +130,7 @@ done
|
||||
|
||||
if [[ -z "$RELEASE_MESSAGE" ]]; then
|
||||
echo
|
||||
read -rp "Enter release message (or press Enter for default): " input_msg
|
||||
read -rp "Enter release headline (or press Enter for default): " input_msg
|
||||
if [[ -n "$input_msg" ]]; then
|
||||
RELEASE_MESSAGE="$input_msg"
|
||||
else
|
||||
@@ -118,6 +138,91 @@ if [[ -z "$RELEASE_MESSAGE" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Helper: Generate categorized release notes from commit history
|
||||
# Groups commits since the last tag into Keep-a-Changelog sections based on
|
||||
# conventional-commit prefixes (feat/fix/docs/security/etc.) and keywords.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
generate_release_notes() {
|
||||
local range="$1"
|
||||
|
||||
local added="" changed="" fixed="" security="" docs=""
|
||||
|
||||
while IFS= read -r subject; do
|
||||
# Skip noise commits
|
||||
case "$subject" in
|
||||
"Initial plan"|"initial plan") continue ;;
|
||||
"Merge pull request"*|"Merge branch"*) continue ;;
|
||||
"chore: bump VERSION"*|"docs: update CHANGELOG"*) continue ;;
|
||||
"Address code review"*|"Address review"*|"Address validation"*) continue ;;
|
||||
esac
|
||||
|
||||
# Strip conventional-commit prefix for display
|
||||
local clean
|
||||
clean="$(echo "$subject" | sed -E 's/^(feat|fix|docs|chore|refactor|test|security|perf|style|ci|build)(\([^)]*\))?!?:[[:space:]]*//')"
|
||||
# Capitalize first letter
|
||||
clean="$(echo "${clean:0:1}" | tr '[:lower:]' '[:upper:]')${clean:1}"
|
||||
|
||||
case "$subject" in
|
||||
security:*|security\(*) security+="- ${clean}"$'\n' ;;
|
||||
feat:*|feat\(*) added+="- ${clean}"$'\n' ;;
|
||||
fix:*|fix\(*|Fix\ *|fixed\ *) fixed+="- ${clean}"$'\n' ;;
|
||||
docs:*|docs\(*) docs+="- ${clean}"$'\n' ;;
|
||||
refactor:*|refactor\(*|chore:*|chore\(*|removed\ *|Updated\ *|updated\ *) changed+="- ${clean}"$'\n' ;;
|
||||
test:*|test\(*) continue ;;
|
||||
*) added+="- ${clean}"$'\n' ;;
|
||||
esac
|
||||
done < <(git log --no-merges --format='%s' "$range" 2>/dev/null | awk '!seen[$0]++')
|
||||
|
||||
local notes=""
|
||||
if [[ -n "$added" ]]; then notes+=$'### Added\n'"$added"$'\n'; fi
|
||||
if [[ -n "$changed" ]]; then notes+=$'### Changed\n'"$changed"$'\n'; fi
|
||||
if [[ -n "$fixed" ]]; then notes+=$'### Fixed\n'"$fixed"$'\n'; fi
|
||||
if [[ -n "$security" ]]; then notes+=$'### Security\n'"$security"$'\n'; fi
|
||||
if [[ -n "$docs" ]]; then notes+=$'### Documentation\n'"$docs"$'\n'; fi
|
||||
|
||||
if [[ -z "$notes" ]]; then
|
||||
notes=$'### Changed\n- Incremental stable updates\n'
|
||||
fi
|
||||
|
||||
printf '%s' "$notes"
|
||||
}
|
||||
|
||||
# Build the notes from commits since the previous tag
|
||||
if [[ -n "$LATEST_TAG" ]] && git rev-parse -q --verify "$LATEST_TAG" >/dev/null; then
|
||||
COMMIT_RANGE="${LATEST_TAG}..HEAD"
|
||||
else
|
||||
COMMIT_RANGE="HEAD"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo -e "${BLUE}Generating draft release notes from ${COMMIT_RANGE}...${NC}"
|
||||
RELEASE_NOTES="$(generate_release_notes "$COMMIT_RANGE")"
|
||||
|
||||
# Let the user review/edit the generated notes before publishing
|
||||
NOTES_FILE="$(mktemp "/tmp/release-notes-${TAG}.XXXXXX.md")"
|
||||
{
|
||||
echo "## Sovran_SystemsOS ${TAG}"
|
||||
echo
|
||||
echo "${RELEASE_MESSAGE}"
|
||||
echo
|
||||
echo "$RELEASE_NOTES"
|
||||
echo "**Full changelog:** [CHANGELOG.md](https://github.com/naturallaw777/Sovran_SystemsOS/blob/main/CHANGELOG.md)"
|
||||
} > "$NOTES_FILE"
|
||||
|
||||
echo -e " ${GREEN}✓${NC} Draft notes written to: ${CYAN}${NOTES_FILE}${NC}"
|
||||
echo
|
||||
echo "──────────────── Draft Release Notes ────────────────"
|
||||
cat "$NOTES_FILE"
|
||||
echo "──────────────────────────────────────────────────────"
|
||||
echo
|
||||
read -rp "Edit the notes before publishing? (y/N): " edit_confirm
|
||||
if [[ "$edit_confirm" =~ ^[Yy]$ ]]; then
|
||||
"${EDITOR:-nano}" "$NOTES_FILE"
|
||||
RELEASE_NOTES="$(sed -n '/^###/,$p' "$NOTES_FILE" | sed '/^\*\*Full changelog/d')"
|
||||
fi
|
||||
RELEASE_BODY="$(cat "$NOTES_FILE")"
|
||||
|
||||
echo
|
||||
echo -e "${YELLOW}════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${YELLOW} Preparing Release${NC}"
|
||||
@@ -135,11 +240,16 @@ if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 1: Push main to stable
|
||||
# Step 1: Push tested code to Gitea (stable & staging-dev)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
echo
|
||||
echo -e "${BLUE}Step 1: Pushing main → stable on Gitea...${NC}"
|
||||
git push "${GITEA_REMOTE}" main:stable --force-with-lease
|
||||
echo -e "${BLUE}Step 1: Pushing tested code to Gitea (stable & staging-dev)...${NC}"
|
||||
if git remote | grep -q -x "${GITEA_REMOTE}"; then
|
||||
git push "${GITEA_REMOTE}" HEAD:stable --force-with-lease || echo -e " ${YELLOW}⚠ Push to ${GITEA_REMOTE} (stable) failed.${NC}"
|
||||
git push "${GITEA_REMOTE}" HEAD:staging-dev --force-with-lease || echo -e " ${YELLOW}⚠ Push to ${GITEA_REMOTE} (staging-dev) failed.${NC}"
|
||||
else
|
||||
echo -e " ${YELLOW}⚠ Remote '${GITEA_REMOTE}' not found in git — skipping Gitea push.${NC}"
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 2: Create annotated tag
|
||||
@@ -149,9 +259,17 @@ echo -e "${BLUE}Step 2: Creating annotated tag ${TAG}...${NC}"
|
||||
git tag -a "${TAG}" -m "${RELEASE_MESSAGE}
|
||||
|
||||
- Stable release of Sovran_SystemsOS
|
||||
- See CHANGELOG.md for full details"
|
||||
- See CHANGELOG.md for full details" || true
|
||||
|
||||
git push "${GITEA_REMOTE}" "${TAG}"
|
||||
if git remote | grep -q -x "${GITEA_REMOTE}"; then
|
||||
git push "${GITEA_REMOTE}" "${TAG}" || echo -e " ${YELLOW}⚠ Tag push to ${GITEA_REMOTE} failed.${NC}"
|
||||
else
|
||||
echo -e " ${YELLOW}⚠ Remote '${GITEA_REMOTE}' not found in git — skipping Gitea tag push.${NC}"
|
||||
fi
|
||||
|
||||
if git remote | grep -q -x "${GITHUB_REMOTE}"; then
|
||||
git push "${GITHUB_REMOTE}" "${TAG}" || echo -e " ${YELLOW}⚠ Tag push to ${GITHUB_REMOTE} failed.${NC}"
|
||||
fi
|
||||
|
||||
# Update VERSION file for ISO builds
|
||||
echo "${VERSION}" > VERSION
|
||||
@@ -167,19 +285,11 @@ echo -e "${BLUE}Step 3: Updating ${CHANGELOG_FILE}...${NC}"
|
||||
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
|
||||
# Create new changelog entry
|
||||
# Create new changelog entry from the generated notes (no placeholders)
|
||||
NEW_ENTRY="## [${VERSION}] - ${TODAY}
|
||||
|
||||
### Added
|
||||
- (Add new features here)
|
||||
|
||||
### Changed
|
||||
- (Add changes here)
|
||||
|
||||
### Fixed
|
||||
- (Add bug fixes here)
|
||||
|
||||
[${VERSION}]: ${GITEA_API_URL%/*}/Sovran_Systems/Sovran_SystemsOS/releases/tag/${TAG}
|
||||
${RELEASE_NOTES}
|
||||
[${VERSION}]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/${TAG}
|
||||
"
|
||||
|
||||
# Prepend to changelog (after the header)
|
||||
@@ -199,6 +309,7 @@ if [ -f "$CHANGELOG_FILE" ]; then
|
||||
}
|
||||
{ print }
|
||||
' "$CHANGELOG_FILE" > "${CHANGELOG_FILE}.tmp" && mv "${CHANGELOG_FILE}.tmp" "$CHANGELOG_FILE"
|
||||
rm -f "${CHANGELOG_FILE}.bak"
|
||||
|
||||
echo -e " ${GREEN}✓${NC} CHANGELOG.md updated with new section for ${TAG}"
|
||||
else
|
||||
@@ -215,11 +326,24 @@ else
|
||||
|
||||
# Ask if user wants to push
|
||||
echo
|
||||
read -rp "Push the changelog commit to GitHub now? (y/N): " push_confirm
|
||||
read -rp "Push the changelog commit to GitHub (main) and Gitea (staging-dev) now? (y/N): " push_confirm
|
||||
if [[ "$push_confirm" =~ ^[Yy]$ ]]; then
|
||||
echo -e "${BLUE}Pushing changelog commit...${NC}"
|
||||
git push "${GITHUB_REMOTE}" main
|
||||
echo -e " ${GREEN}✓${NC} Changelog pushed to GitHub"
|
||||
if git remote | grep -q -x "${GITHUB_REMOTE}"; then
|
||||
echo -e "${BLUE}Pushing changelog commit to GitHub main...${NC}"
|
||||
if git push "${GITHUB_REMOTE}" HEAD:main; then
|
||||
echo -e " ${GREEN}✓${NC} Changelog pushed to GitHub main (${GITHUB_REMOTE})"
|
||||
else
|
||||
echo -e " ${YELLOW}⚠ Push to GitHub main failed — verify credentials or remote name.${NC}"
|
||||
fi
|
||||
fi
|
||||
if git remote | grep -q -x "${GITEA_REMOTE}"; then
|
||||
echo -e "${BLUE}Pushing changelog commit to Gitea staging-dev...${NC}"
|
||||
if git push "${GITEA_REMOTE}" HEAD:staging-dev; then
|
||||
echo -e " ${GREEN}✓${NC} Changelog pushed to Gitea staging-dev (${GITEA_REMOTE})"
|
||||
else
|
||||
echo -e " ${YELLOW}⚠ Push to Gitea staging-dev failed.${NC}"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo " (Changelog commit left local — remember to push later)"
|
||||
fi
|
||||
@@ -234,8 +358,8 @@ echo -e "${BLUE}Step 4: Creating GitHub Release...${NC}"
|
||||
if command -v gh &>/dev/null; then
|
||||
if gh release create "${TAG}" \
|
||||
--repo naturallaw777/Sovran_SystemsOS \
|
||||
--title "${TAG}" \
|
||||
--notes "${RELEASE_MESSAGE}" \
|
||||
--title "${TAG} — ${RELEASE_MESSAGE#Sovran_SystemsOS v* — }" \
|
||||
--notes-file "${NOTES_FILE}" \
|
||||
--target main 2>/dev/null; then
|
||||
echo -e " ${GREEN}✓${NC} GitHub release created successfully"
|
||||
else
|
||||
@@ -266,16 +390,21 @@ fi
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
GITEA_REPO="Sovran_Systems/Sovran_SystemsOS"
|
||||
|
||||
# Build JSON payload safely (release body may contain quotes/newlines)
|
||||
if command -v jq &>/dev/null; then
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg tag "${TAG}" \
|
||||
--arg name "${TAG} — Stable Release" \
|
||||
--arg body "${RELEASE_BODY}" \
|
||||
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
|
||||
else
|
||||
PAYLOAD=$(python3 -c "import json,sys; print(json.dumps({'tag_name': sys.argv[1], 'name': sys.argv[1] + ' — Stable Release', 'body': open(sys.argv[2]).read(), 'draft': False, 'prerelease': False}))" "${TAG}" "${NOTES_FILE}")
|
||||
fi
|
||||
|
||||
RESPONSE=$(curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"tag_name\": \"${TAG}\",
|
||||
\"name\": \"${TAG}\",
|
||||
\"body\": \"${RELEASE_MESSAGE}\",
|
||||
\"draft\": false,
|
||||
\"prerelease\": false
|
||||
}" \
|
||||
-d "${PAYLOAD}" \
|
||||
"${GITEA_API_URL}/repos/${GITEA_REPO}/releases" 2>/dev/null || echo "")
|
||||
|
||||
if echo "$RESPONSE" | grep -q '"id"'; then
|
||||
@@ -296,8 +425,11 @@ echo -e "${GREEN}╚════════════════════
|
||||
echo
|
||||
echo "Next manual steps (recommended):"
|
||||
echo " • Review and enhance the new section in CHANGELOG.md"
|
||||
echo " • Push the changelog commit: git push origin main"
|
||||
echo " • Push changes: git push ${GITHUB_REMOTE} HEAD:main && git push ${GITEA_REMOTE} HEAD:staging-dev"
|
||||
echo " • Verify releases on both GitHub and Gitea"
|
||||
echo
|
||||
echo -e "${CYAN}Tag created: ${TAG}${NC}"
|
||||
git show "${TAG}" --quiet
|
||||
|
||||
# Clean up temp notes file
|
||||
rm -f "${NOTES_FILE}"
|
||||
|
||||
Reference in New Issue
Block a user