Compare commits
16
Commits
v1.0.4
...
a814ed332f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a814ed332f | ||
|
|
94e4412e67 | ||
|
|
670cfb9acd | ||
|
|
2015a3ecd4 | ||
|
|
2622223033 | ||
|
|
d0ec6dd760 | ||
|
|
90b1c45a0d | ||
|
|
0ac1529a1d | ||
|
|
1b579271e5 | ||
|
|
c6b4b89be3 | ||
|
|
a8ff366992 | ||
|
|
ddba59cce8 | ||
|
|
e12e9ba01b | ||
|
|
853c3edf92 | ||
|
|
4a6ba3eacc | ||
|
|
537d4781f2 |
No files matched your search
@@ -7,6 +7,65 @@ 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
|
||||||
|
- **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
|
||||||
|
- **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
|
||||||
|
- 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`)
|
||||||
|
|
||||||
|
### 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
|
## [1.0.3] - 2026-07-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -3,10 +3,18 @@
|
|||||||
# Backs up Sovran_SystemsOS data to an external USB hard drive using rsync.
|
# Backs up Sovran_SystemsOS data to an external USB hard drive using rsync.
|
||||||
# Designed for the Hub web UI (no GUI dependencies).
|
# Designed for the Hub web UI (no GUI dependencies).
|
||||||
#
|
#
|
||||||
# Your Sovran Pro already backs up your data automatically to its
|
# On Server + Desktop and Node systems, your Sovran Pro already backs up
|
||||||
# internal second drive (BTCEcoandBackup at /run/media/Second_Drive).
|
# your data automatically to its internal second drive (BTCEcoandBackup at
|
||||||
# This script creates an additional copy on an external USB drive —
|
# /run/media/Second_Drive); this script stores a copy in a third location.
|
||||||
# storing your data in a third location for maximum protection.
|
# 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
|
# The external drive must be formatted as ext4. Files are stored as
|
||||||
# directly browsable files under Sovran_SystemsOS_Backup/current/.
|
# directly browsable files under Sovran_SystemsOS_Backup/current/.
|
||||||
@@ -344,6 +352,19 @@ case "$ROLE" in
|
|||||||
esac
|
esac
|
||||||
log "Detected role: $ROLE_LABEL"
|
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 ──────────────────────────────────────────
|
# ── Detect target drive ──────────────────────────────────────────
|
||||||
|
|
||||||
if [[ -n "${BACKUP_TARGET:-}" ]]; then
|
if [[ -n "${BACKUP_TARGET:-}" ]]; then
|
||||||
@@ -385,22 +406,25 @@ log "Backup destination: $BACKUP_DIR"
|
|||||||
|
|
||||||
ETC_NIXOS_BYTES=$(estimate_path_bytes /etc/nixos)
|
ETC_NIXOS_BYTES=$(estimate_path_bytes /etc/nixos)
|
||||||
HOME_BYTES=$(estimate_path_bytes /home --exclude='*/.cache' --exclude='*/.local/share/Trash' --exclude='*/Trash')
|
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
|
SECRETS_BYTES=0
|
||||||
|
VAR_LIB_BYTES=0
|
||||||
if [[ "$ROLE" != "desktop" ]]; then
|
if [[ "$ROLE" != "desktop" ]]; then
|
||||||
SECRETS_BYTES=$(estimate_path_bytes /etc/nix-bitcoin-secrets)
|
SECRETS_BYTES=$(estimate_path_bytes /etc/nix-bitcoin-secrets)
|
||||||
|
VAR_LIB_BYTES=$(estimate_path_bytes /var/lib \
|
||||||
|
--exclude='postgresql' \
|
||||||
|
--exclude='mysql' \
|
||||||
|
--exclude='mariadb' \
|
||||||
|
--exclude='bitcoind' \
|
||||||
|
--exclude='electrs' \
|
||||||
|
--exclude='*/log' \
|
||||||
|
--exclude='*/logs' \
|
||||||
|
--exclude='*/cache' \
|
||||||
|
--exclude='*/tmp')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
VAR_LIB_BYTES=$(estimate_path_bytes /var/lib \
|
|
||||||
--exclude='postgresql' \
|
|
||||||
--exclude='mysql' \
|
|
||||||
--exclude='mariadb' \
|
|
||||||
--exclude='bitcoind' \
|
|
||||||
--exclude='electrs' \
|
|
||||||
--exclude='*/log' \
|
|
||||||
--exclude='*/logs' \
|
|
||||||
--exclude='*/cache' \
|
|
||||||
--exclude='*/tmp')
|
|
||||||
|
|
||||||
ESTIMATED_BYTES=$(( ETC_NIXOS_BYTES + HOME_BYTES + SECRETS_BYTES + VAR_LIB_BYTES ))
|
ESTIMATED_BYTES=$(( ETC_NIXOS_BYTES + HOME_BYTES + SECRETS_BYTES + VAR_LIB_BYTES ))
|
||||||
# Require 20% growth headroom plus a fixed 1 GiB safety margin.
|
# Require 20% growth headroom plus a fixed 1 GiB safety margin.
|
||||||
# Later incremental runs need far less space, but a conservative first-run
|
# Later incremental runs need far less space, but a conservative first-run
|
||||||
@@ -418,10 +442,10 @@ log "Free space on drive: ${FREE_GB} GB"
|
|||||||
(( FREE_BYTES >= REQUIRED_BYTES )) || \
|
(( FREE_BYTES >= REQUIRED_BYTES )) || \
|
||||||
fail "Not enough free space on drive (${FREE_GB} GB available, ${REQUIRED_GB} GB required)."
|
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 ""
|
||||||
log "── Stage 1/4: NixOS configuration (/etc/nixos) ──────────────"
|
log "── Stage 1/${TOTAL_STAGES}: NixOS configuration (/etc/nixos) ──────────────"
|
||||||
if [[ -d /etc/nixos ]]; then
|
if [[ -d /etc/nixos ]]; then
|
||||||
sync_tree "/etc/nixos" no /etc/nixos/ "$BACKUP_DIR/etc/nixos/"
|
sync_tree "/etc/nixos" no /etc/nixos/ "$BACKUP_DIR/etc/nixos/"
|
||||||
log "Stage 1 complete."
|
log "Stage 1 complete."
|
||||||
@@ -429,26 +453,30 @@ else
|
|||||||
log "WARNING: /etc/nixos not found — skipping."
|
log "WARNING: /etc/nixos not found — skipping."
|
||||||
fi
|
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.
|
||||||
|
|
||||||
log ""
|
if [[ "$ROLE" != "desktop" ]]; then
|
||||||
log "── Stage 2/4: Secrets (/etc/nix-bitcoin-secrets) ───────────"
|
log ""
|
||||||
if [[ "$ROLE" == "desktop" ]]; then
|
log "── Stage 2/${TOTAL_STAGES}: Secrets (/etc/nix-bitcoin-secrets) ───────────"
|
||||||
log "Skipping /etc/nix-bitcoin-secrets — not applicable for Desktop Only role."
|
if [[ -e /etc/nix-bitcoin-secrets ]]; then
|
||||||
elif [[ -e /etc/nix-bitcoin-secrets ]]; then
|
sync_tree "/etc/nix-bitcoin-secrets" no /etc/nix-bitcoin-secrets/ "$BACKUP_DIR/etc/nix-bitcoin-secrets/"
|
||||||
sync_tree "/etc/nix-bitcoin-secrets" no /etc/nix-bitcoin-secrets/ "$BACKUP_DIR/etc/nix-bitcoin-secrets/"
|
else
|
||||||
else
|
log "(not found: /etc/nix-bitcoin-secrets — skipping)"
|
||||||
log "(not found: /etc/nix-bitcoin-secrets — skipping)"
|
fi
|
||||||
|
log "Stage 2 complete."
|
||||||
fi
|
fi
|
||||||
log "Stage 2 complete."
|
|
||||||
|
|
||||||
# ── 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
|
# Rsync exit code 24 (vanished source files) is treated as nonfatal here
|
||||||
# because the desktop may be active and files can disappear between the
|
# because the desktop may be active and files can disappear between the
|
||||||
# directory scan and the copy. All other nonzero exit codes remain fatal.
|
# directory scan and the copy. All other nonzero exit codes remain fatal.
|
||||||
|
|
||||||
log ""
|
log ""
|
||||||
log "── Stage 3/4: Home directory (/home) ───────────────────────"
|
log "── Stage ${HOME_STAGE_NUM}/${TOTAL_STAGES}: Home directory (/home) ───────────────────────"
|
||||||
if [[ -d /home ]]; then
|
if [[ -d /home ]]; then
|
||||||
sync_tree "/home" yes /home/ "$BACKUP_DIR/home/" \
|
sync_tree "/home" yes /home/ "$BACKUP_DIR/home/" \
|
||||||
--exclude='.cache/' \
|
--exclude='.cache/' \
|
||||||
@@ -467,32 +495,36 @@ if [[ -d /home ]]; then
|
|||||||
--exclude='.thumbnails/' \
|
--exclude='.thumbnails/' \
|
||||||
--exclude='.xsession-errors' \
|
--exclude='.xsession-errors' \
|
||||||
--exclude='.xsession-errors.old'
|
--exclude='.xsession-errors.old'
|
||||||
log "Stage 3 complete."
|
log "Stage ${HOME_STAGE_NUM} complete."
|
||||||
else
|
else
|
||||||
log "WARNING: /home not found — skipping."
|
log "WARNING: /home not found — skipping."
|
||||||
fi
|
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
|
# PostgreSQL/MariaDB raw database directories are excluded. Application
|
||||||
# databases must be backed up separately with native database tools.
|
# databases must be backed up separately with native database tools.
|
||||||
# Bitcoin/Electrs data are excluded; they live on the internal second drive.
|
# Bitcoin/Electrs data are excluded; they live on the internal second drive.
|
||||||
|
|
||||||
log ""
|
if [[ "$ROLE" != "desktop" ]]; then
|
||||||
log "── Stage 4/4: System data (/var/lib) ───────────────────────"
|
log ""
|
||||||
if [[ -d /var/lib ]]; then
|
log "── Stage 4/${TOTAL_STAGES}: System data (/var/lib) ───────────────────────"
|
||||||
sync_tree "/var/lib" no /var/lib/ "$BACKUP_DIR/var/lib/" \
|
if [[ -d /var/lib ]]; then
|
||||||
--exclude='postgresql/' \
|
sync_tree "/var/lib" no /var/lib/ "$BACKUP_DIR/var/lib/" \
|
||||||
--exclude='mysql/' \
|
--exclude='postgresql/' \
|
||||||
--exclude='mariadb/' \
|
--exclude='mysql/' \
|
||||||
--exclude='bitcoind/' \
|
--exclude='mariadb/' \
|
||||||
--exclude='electrs/' \
|
--exclude='bitcoind/' \
|
||||||
--exclude='*/log/' \
|
--exclude='electrs/' \
|
||||||
--exclude='*/logs/' \
|
--exclude='*/log/' \
|
||||||
--exclude='*/cache/' \
|
--exclude='*/logs/' \
|
||||||
--exclude='*/tmp/'
|
--exclude='*/cache/' \
|
||||||
log "Stage 4 complete."
|
--exclude='*/tmp/'
|
||||||
else
|
log "Stage 4 complete."
|
||||||
log "WARNING: /var/lib not found — skipping."
|
else
|
||||||
|
log "WARNING: /var/lib not found — skipping."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Generate manifest ────────────────────────────────────────────
|
# ── 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/"
|
echo "- /etc/nix-bitcoin-secrets (when present) → current/etc/nix-bitcoin-secrets/"
|
||||||
fi
|
fi
|
||||||
echo "- /home → current/home/"
|
echo "- /home → current/home/"
|
||||||
echo "- /var/lib → current/var/lib/"
|
if [[ "$ROLE" != "desktop" ]]; then
|
||||||
|
echo "- /var/lib → current/var/lib/"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo "Exclusions:"
|
echo "Exclusions:"
|
||||||
echo "- /var/lib/postgresql (PostgreSQL raw database files — not included)"
|
if [[ "$ROLE" != "desktop" ]]; then
|
||||||
echo "- /var/lib/mysql, /var/lib/mariadb (MariaDB raw database files — not included)"
|
echo "- /var/lib/postgresql (PostgreSQL raw database files — not included)"
|
||||||
echo "- /var/lib/bitcoind (Bitcoin blockchain — excluded; lives on internal second drive)"
|
echo "- /var/lib/mysql, /var/lib/mariadb (MariaDB raw database files — not included)"
|
||||||
echo "- /var/lib/electrs (Electrs index — excluded; lives on internal second drive)"
|
echo "- /var/lib/bitcoind (Bitcoin blockchain — excluded; lives on internal second drive)"
|
||||||
echo "- /run/media/Second_Drive (internal second drive — never traversed)"
|
echo "- /var/lib/electrs (Electrs index — excluded; lives on internal second drive)"
|
||||||
echo "- /var/lib/*/log, /var/lib/*/logs, /var/lib/*/cache, /var/lib/*/tmp"
|
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 "- Browser disk caches, thumbnail caches, trash directories, X session error logs"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Important limitations:"
|
echo "Important limitations:"
|
||||||
echo "- PostgreSQL and MariaDB/MySQL application databases are NOT included in this"
|
if [[ "$ROLE" != "desktop" ]]; then
|
||||||
echo " backup. If you use Nextcloud, Matrix/Synapse, or other database-backed"
|
echo "- PostgreSQL and MariaDB/MySQL application databases are NOT included in this"
|
||||||
echo " applications, their data must be backed up separately using native tools."
|
echo " backup. If you use Nextcloud, Matrix/Synapse, or other database-backed"
|
||||||
echo "- Bitcoin blockchain data and Electrs indexes are NOT included; they are"
|
echo " applications, their data must be backed up separately using native tools."
|
||||||
echo " reconstructable or stored on the internal second drive."
|
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 "- 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 " Files being written during the backup may be in an inconsistent state."
|
||||||
echo ""
|
echo ""
|
||||||
@@ -542,7 +580,9 @@ MANIFEST_FILE="$BACKUP_DIR/BACKUP_MANIFEST.txt"
|
|||||||
echo "- To restore a directory:"
|
echo "- To restore a directory:"
|
||||||
echo " sudo rsync -aAXH --numeric-ids current/etc/nixos/ /etc/nixos/"
|
echo " sudo rsync -aAXH --numeric-ids current/etc/nixos/ /etc/nixos/"
|
||||||
echo " sudo rsync -aAXH --numeric-ids current/home/ /home/"
|
echo " sudo rsync -aAXH --numeric-ids current/home/ /home/"
|
||||||
echo " sudo rsync -aAXH --numeric-ids current/var/lib/ /var/lib/"
|
if [[ "$ROLE" != "desktop" ]]; then
|
||||||
|
echo " sudo rsync -aAXH --numeric-ids current/var/lib/ /var/lib/"
|
||||||
|
fi
|
||||||
echo "- To copy individual files:"
|
echo "- To copy individual files:"
|
||||||
echo " sudo cp -a current/home/username/ /home/username/"
|
echo " sudo cp -a current/home/username/ /home/username/"
|
||||||
echo "- When restoring /etc/nixos to replacement hardware, regenerate"
|
echo "- When restoring /etc/nixos to replacement hardware, regenerate"
|
||||||
@@ -556,10 +596,12 @@ MANIFEST_FILE="$BACKUP_DIR/BACKUP_MANIFEST.txt"
|
|||||||
echo "- $warning"
|
echo "- $warning"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
echo ""
|
if [[ "$ROLE" != "desktop" ]]; then
|
||||||
echo "Note: Bitcoin blockchain and Electrs index data are intentionally excluded"
|
echo ""
|
||||||
echo "from manual external backup because they already live on the internal second drive"
|
echo "Note: Bitcoin blockchain and Electrs index data are intentionally excluded"
|
||||||
echo "(/run/media/Second_Drive) and are reconstructable/internal-backup data."
|
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"
|
} > "$MANIFEST_FILE"
|
||||||
|
|
||||||
log "Manifest written to $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 "vanished during backup, which is normal on an active desktop."
|
||||||
log ""
|
log ""
|
||||||
fi
|
fi
|
||||||
log "All Finished! Your data is now backed up to a third location."
|
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 "Files are directly browsable on the drive under: ${BACKUP_DIR}"
|
||||||
log "Please eject the drive safely before removing it from your Sovran Pro."
|
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
|
# Systemd service that rewrites the Sovran-managed /etc/hosts loopback block
|
||||||
SOVRAN_HOSTS_SERVICE = "sovran-hosts-update.service"
|
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
|
# Domain keys that produce a public HTTPS virtual host via Caddy
|
||||||
_SERVICE_DOMAIN_KEYS = frozenset([
|
_SERVICE_DOMAIN_KEYS = frozenset([
|
||||||
"matrix", "wordpress", "nextcloud", "btcpayserver",
|
"matrix", "wordpress", "nextcloud", "btcpayserver",
|
||||||
@@ -2275,18 +2281,39 @@ async def api_logout(request: Request):
|
|||||||
|
|
||||||
|
|
||||||
def _get_sovran_version() -> str:
|
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:
|
try:
|
||||||
with open("/etc/nixos/VERSION", "r") as f:
|
version = load_config().get("sovran_version")
|
||||||
return f.read().strip()
|
if version:
|
||||||
except FileNotFoundError:
|
return str(version).strip().lstrip("vV")
|
||||||
try:
|
|
||||||
with open("VERSION", "r") as f: # fallback for development
|
|
||||||
return f.read().strip()
|
|
||||||
except FileNotFoundError:
|
|
||||||
return "dev"
|
|
||||||
except Exception:
|
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)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
@@ -4329,6 +4356,33 @@ def _run_njalla_ddns() -> None:
|
|||||||
pass
|
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.
|
# 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.
|
# Each label must start and end with a letter or digit; no consecutive dots.
|
||||||
_HOSTNAME_RE = re.compile(
|
_HOSTNAME_RE = re.compile(
|
||||||
@@ -4450,6 +4504,16 @@ async def api_domains_set(req: DomainSetRequest):
|
|||||||
if req.domain_name in _SERVICE_DOMAIN_KEYS:
|
if req.domain_name in _SERVICE_DOMAIN_KEYS:
|
||||||
_trigger_hosts_update()
|
_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}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@@ -4465,6 +4529,12 @@ async def api_domains_set_email(req: DomainSetEmailRequest):
|
|||||||
with open(email_path, "w") as f:
|
with open(email_path, "w") as f:
|
||||||
f.write(req.email.strip())
|
f.write(req.email.strip())
|
||||||
_chown_to_caddy(email_path)
|
_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}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,14 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.header-buttons {
|
.header-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -58,8 +66,8 @@
|
|||||||
color: #4ade80;
|
color: #4ade80;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
margin-left: 12px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.os-version-badge:hover {
|
.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 ────────────────────────────────────────────────── */
|
/* ── Update modal ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -607,10 +658,12 @@ button.btn-reboot:hover:not(:disabled) {
|
|||||||
|
|
||||||
.nwc-tab-intro {
|
.nwc-tab-intro {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
align-items: stretch;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nwc-tab-intro-title {
|
.nwc-tab-intro-title {
|
||||||
|
|||||||
@@ -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 domainPlaceholder = isWalletConnections ? "lightning.yourdomain.com" : "myservice.example.com";
|
||||||
var domainLabelExample = isWalletConnections ? "lightning.yourdomain.com" : "call.yourdomain.com";
|
var domainLabelExample = isWalletConnections ? "lightning.yourdomain.com" : "call.yourdomain.com";
|
||||||
|
|
||||||
var introHtml;
|
// Shared instructions (single source of truth: static/js/domain-prereqs.js) —
|
||||||
if (_currentRole === "node") {
|
// identical wording to the Server + Desktop onboarding wizard and every other
|
||||||
introHtml =
|
// domain-based feature, regardless of role (Node / Desktop / Server+Desktop).
|
||||||
'<p>To enable <strong>' + escHtml(feat.name) + '</strong>, it needs its own domain from Njal.la.</p>' +
|
var hostExample = isWalletConnections ? "lightning" : "call";
|
||||||
'<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>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$domainSetupBody.innerHTML =
|
$domainSetupBody.innerHTML =
|
||||||
'<div class="domain-setup-intro">' +
|
'<div class="domain-setup-intro">' +
|
||||||
nwcWarning +
|
nwcWarning +
|
||||||
introHtml +
|
renderDomainNeedsHtml({ serviceName: feat.name, hostExample: hostExample }) +
|
||||||
'<details style="margin-top:10px;">' +
|
renderNjallaStepsHtml({ hostExample: hostExample, pasteHint: "below" }) +
|
||||||
'<summary style="cursor:pointer;font-weight:600;">Option A — Free subdomain (recommended)</summary>' +
|
'<div class="onboarding-port-warn" id="domain-router-box" style="margin-top:12px;"></div>' +
|
||||||
'<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>' +
|
|
||||||
'<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>' +
|
'<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>' +
|
||||||
'<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>' +
|
'<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");
|
$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) {
|
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>' +
|
'<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 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>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>' +
|
'</ol>' +
|
||||||
|
'<div class="onboarding-port-warn" id="domain-router-box" style="margin-top:12px;"></div>' +
|
||||||
'</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-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>' +
|
'<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");
|
$domainSetupModal.classList.add("open");
|
||||||
|
|
||||||
|
// Fill the router port-forwarding box with this computer's LAN IP (best-effort)
|
||||||
|
renderRouterPortsBox("domain-router-box");
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDomainSetupModal() {
|
function closeDomainSetupModal() {
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ function _nwcRenderWalletState() {
|
|||||||
var selectedLimited = state.createForm.access_preset === "send_receive_limited";
|
var selectedLimited = state.createForm.access_preset === "send_receive_limited";
|
||||||
html +=
|
html +=
|
||||||
'<div class="nwc-tab-intro-title">Create a Wallet</div>' +
|
'<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>' +
|
'<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>' +
|
'<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>' +
|
'<div class="matrix-form-group"><label class="matrix-form-label" for="nwc-wallet-alias">Lightning Address Alias</label>' +
|
||||||
@@ -330,9 +330,9 @@ function _nwcRenderWalletState() {
|
|||||||
|
|
||||||
if (!state.wallets || state.wallets.length === 0) {
|
if (!state.wallets || state.wallets.length === 0) {
|
||||||
html += '<div class="nwc-empty-state">' +
|
html += '<div class="nwc-empty-state">' +
|
||||||
'<div class="nwc-empty-icon">👛</div>' +
|
'<div class="nwc-empty-icon">⚡</div>' +
|
||||||
'<div class="nwc-empty-title">No wallets yet</div>' +
|
'<div class="nwc-empty-title">Ready to start spending</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>' +
|
'<p class="nwc-empty-desc">Create your first isolated wallet to connect to apps like Zeus or Nostr. Experience faster, more secure Lightning payments today.</p>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
host.innerHTML = html;
|
host.innerHTML = html;
|
||||||
_nwcWireDomainLink();
|
_nwcWireDomainLink();
|
||||||
@@ -761,9 +761,32 @@ async function openServiceDetailModal(unit, name, icon) {
|
|||||||
if (isNwc) {
|
if (isNwc) {
|
||||||
if (effectiveEnabled || data.enabled) {
|
if (effectiveEnabled || data.enabled) {
|
||||||
html += '<div class="nwc-tab-intro">' +
|
html += '<div class="nwc-tab-intro">' +
|
||||||
'<div>' +
|
'<div class="nwc-intro-header">' +
|
||||||
'<div class="nwc-tab-intro-title">Your Lightning Wallets</div>' +
|
'<div class="nwc-tab-intro-title">Lightning Wallet Connections</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>' +
|
'<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</strong>' +
|
||||||
|
'<p>Connect instantly to Zeus via NWC. Manage your spending and receive payments right from your phone, wherever you are.</p>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div id="nwc-wallets-body"><p class="creds-loading">Loading wallets…</p></div>' +
|
'<div id="nwc-wallets-body"><p class="creds-loading">Loading wallets…</p></div>' +
|
||||||
|
|||||||
@@ -473,19 +473,73 @@ function renderBackupReady(drives) {
|
|||||||
].join("");
|
].join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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. ',
|
||||||
|
'This manual backup lets you create an additional copy on an external USB drive \u2014 ',
|
||||||
|
'storing your data in a third location, outside the computer, for maximum protection ',
|
||||||
|
'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 = [
|
$supportBody.innerHTML = [
|
||||||
'<div class="support-section">',
|
'<div class="support-section">',
|
||||||
'<div class="support-icon-big">\ud83d\udcbe</div>',
|
'<div class="support-icon-big">\ud83d\udcbe</div>',
|
||||||
'<h3 class="support-heading">Manual Backup</h3>',
|
'<h3 class="support-heading">Manual Backup</h3>',
|
||||||
|
|
||||||
'<div class="support-wallet-box support-wallet-protected" style="margin-bottom:16px;">',
|
introHtml,
|
||||||
'<p class="support-wallet-desc">',
|
|
||||||
'Your Sovran Pro already backs up your data automatically to its internal second drive. ',
|
|
||||||
'This manual backup lets you create an additional copy on an external USB drive \u2014 ',
|
|
||||||
'storing your data in a third location, outside the computer, for maximum protection ',
|
|
||||||
'against hardware failure or physical damage.',
|
|
||||||
'</p>',
|
|
||||||
'</div>',
|
|
||||||
|
|
||||||
'<div class="support-steps">',
|
'<div class="support-steps">',
|
||||||
'<div class="support-steps-title">Requirements</div>',
|
'<div class="support-steps-title">Requirements</div>',
|
||||||
@@ -500,20 +554,11 @@ function renderBackupReady(drives) {
|
|||||||
'<div class="support-steps">',
|
'<div class="support-steps">',
|
||||||
'<div class="support-steps-title">What gets backed up</div>',
|
'<div class="support-steps-title">What gets backed up</div>',
|
||||||
'<ol class="support-backup-steps">',
|
'<ol class="support-backup-steps">',
|
||||||
'<li>NixOS configuration (<code>/etc/nixos</code>)</li>',
|
backupItemsHtml,
|
||||||
'<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>',
|
|
||||||
'</ol>',
|
'</ol>',
|
||||||
'</div>',
|
'</div>',
|
||||||
|
|
||||||
'<div class="support-wallet-box support-wallet-warning">',
|
dbNoteHtml,
|
||||||
'<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>',
|
|
||||||
|
|
||||||
'<div class="support-wallet-box support-wallet-protected">',
|
'<div class="support-wallet-box support-wallet-protected">',
|
||||||
'<div class="support-wallet-header">',
|
'<div class="support-wallet-header">',
|
||||||
|
|||||||
@@ -357,38 +357,24 @@ async function loadStep3() {
|
|||||||
if (relevantDomains.length === 0) {
|
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>';
|
html += '<p class="onboarding-body-text">No domain-based services are enabled for your role. You can skip this step.</p>';
|
||||||
} else {
|
} 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;">'
|
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>'
|
+ renderDomainNeedsHtml({ serviceName: null, hostExample: "call" })
|
||||||
+ '<ol style="margin:8px 0 0 16px; padding:0; line-height:1.7;">'
|
+ renderNjallaStepsHtml({ hostExample: "call", pasteHint: "next to its service below" })
|
||||||
+ '<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>'
|
|
||||||
+ '</div>';
|
+ '</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>';
|
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
|
// Router note (the same wording is shown again, per-service, whenever a
|
||||||
// enabled, and lives on each service tile afterwards)
|
// domain-based 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';
|
|
||||||
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
|
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 '
|
+ renderRouterPortsHtml({
|
||||||
+ 'port <strong>80 (TCP)</strong> and port <strong>443 (TCP)</strong>'
|
internalIp: internalIp,
|
||||||
+ routerIpPart + '. Use the <strong>same number for the internal and external port</strong>. '
|
plural: true,
|
||||||
+ 'These are required for HTTPS and SSL certificates — without them your services cannot be reached from outside your home network. '
|
includeSsh: true,
|
||||||
+ 'Add port <strong>22 (TCP)</strong> as well if you want remote SSH access. '
|
extraNote: "Element Call needs a few extra ports (some UDP), and you’ll be shown exactly which when you enable it.",
|
||||||
+ 'Element Call needs a few extra ports (some UDP), and you’ll be shown exactly which when you enable it.'
|
})
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
|
|
||||||
relevantDomains.forEach(function(d) {
|
relevantDomains.forEach(function(d) {
|
||||||
|
|||||||
@@ -21,13 +21,16 @@
|
|||||||
<!-- Header bar -->
|
<!-- Header bar -->
|
||||||
<header class="header-bar">
|
<header class="header-bar">
|
||||||
<img src="/static/sovran-hub-icon.svg" alt="Sovran Hub" class="header-logo" />
|
<img src="/static/sovran-hub-icon.svg" alt="Sovran Hub" class="header-logo" />
|
||||||
<span class="title">Sovran_SystemsOS Hub</span>
|
|
||||||
|
<div class="title-group">
|
||||||
<!-- OS Version Badge -->
|
<span class="title">Sovran_SystemsOS Hub</span>
|
||||||
<span class="os-version-badge" id="os-version-badge" title="Sovran_SystemsOS Version">
|
|
||||||
<span class="version-label">v</span>
|
<!-- OS Version Badge -->
|
||||||
<span class="version-number" id="version-number">{{ sovran_version }}</span>
|
<span class="os-version-badge" id="os-version-badge" title="Sovran_SystemsOS Version">
|
||||||
</span>
|
<span class="version-label">v</span>
|
||||||
|
<span class="version-number" id="version-number">{{ sovran_version }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="header-buttons">
|
<div class="header-buttons">
|
||||||
<span class="role-badge" id="role-badge">Loading…</span>
|
<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/constants.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/state.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/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/tiles.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/service-detail.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>
|
<script src="/static/js/support.js?v={{ asset_version }}"></script>
|
||||||
|
|||||||
@@ -125,9 +125,8 @@
|
|||||||
<span class="onboarding-step-icon">🌐</span>
|
<span class="onboarding-step-icon">🌐</span>
|
||||||
<h2 class="onboarding-step-title">Domain Configuration</h2>
|
<h2 class="onboarding-step-title">Domain Configuration</h2>
|
||||||
<p class="onboarding-step-desc">
|
<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.
|
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.
|
||||||
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.
|
Everything you need — Njal.la account, Dynamic records, and the one router task — is laid out step by step below.
|
||||||
Paste the curl command from your Njal.la dashboard for each service.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="onboarding-card" id="step-3-body">
|
<div class="onboarding-card" id="step-3-body">
|
||||||
@@ -170,6 +169,7 @@
|
|||||||
</div><!-- /panel-wrap -->
|
</div><!-- /panel-wrap -->
|
||||||
</div><!-- /shell -->
|
</div><!-- /shell -->
|
||||||
|
|
||||||
|
<script src="/static/js/domain-prereqs.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/onboarding.js?v={{ onboarding_js_hash }}"></script>
|
<script src="/static/onboarding.js?v={{ onboarding_js_hash }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -3,6 +3,16 @@
|
|||||||
let
|
let
|
||||||
cfg = config.sovran_systemsOS;
|
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 =
|
monitoredServices =
|
||||||
# ── Infrastructure — System Passwords (always present) ─────
|
# ── Infrastructure — System Passwords (always present) ─────
|
||||||
[
|
[
|
||||||
@@ -116,6 +126,7 @@ let
|
|||||||
role = activeRole;
|
role = activeRole;
|
||||||
services = monitoredServices;
|
services = monitoredServices;
|
||||||
feature_manager = true;
|
feature_manager = true;
|
||||||
|
sovran_version = sovranVersion;
|
||||||
});
|
});
|
||||||
|
|
||||||
generatedVersions = pkgs.writeText "sovran-hub-versions.json" (builtins.toJSON {
|
generatedVersions = pkgs.writeText "sovran-hub-versions.json" (builtins.toJSON {
|
||||||
@@ -331,6 +342,7 @@ let
|
|||||||
|
|
||||||
cp ${generatedConfig} $out/lib/sovran-hub-web/config.json
|
cp ${generatedConfig} $out/lib/sovran-hub-web/config.json
|
||||||
cp ${generatedVersions} $out/lib/sovran-hub-web/versions.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
|
install -d $out/share/sovran-hub/icons
|
||||||
cp icons/* $out/share/sovran-hub/icons/ 2>/dev/null || true
|
cp icons/* $out/share/sovran-hub/icons/ 2>/dev/null || true
|
||||||
|
|||||||
+165
-33
@@ -29,11 +29,31 @@ CYAN='\033[0;36m'
|
|||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
GITEA_REMOTE="gitea"
|
GITEA_REMOTE_DEFAULT="gitea"
|
||||||
GITHUB_REMOTE="origin"
|
GITHUB_REMOTE_DEFAULT="origin"
|
||||||
GITEA_API_URL="https://git.sovransystems.com/api/v1"
|
GITEA_API_URL="https://git.sovransystems.com/api/v1"
|
||||||
CHANGELOG_FILE="CHANGELOG.md"
|
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}╔════════════════════════════════════════════════════════════╗${NC}"
|
||||||
echo -e "${BLUE}║ Sovran_SystemsOS Automated Stable Release Script ║${NC}"
|
echo -e "${BLUE}║ Sovran_SystemsOS Automated Stable Release Script ║${NC}"
|
||||||
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
|
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
|
||||||
@@ -110,7 +130,7 @@ done
|
|||||||
|
|
||||||
if [[ -z "$RELEASE_MESSAGE" ]]; then
|
if [[ -z "$RELEASE_MESSAGE" ]]; then
|
||||||
echo
|
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
|
if [[ -n "$input_msg" ]]; then
|
||||||
RELEASE_MESSAGE="$input_msg"
|
RELEASE_MESSAGE="$input_msg"
|
||||||
else
|
else
|
||||||
@@ -118,6 +138,91 @@ if [[ -z "$RELEASE_MESSAGE" ]]; then
|
|||||||
fi
|
fi
|
||||||
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
|
||||||
echo -e "${YELLOW}════════════════════════════════════════════════════════════${NC}"
|
echo -e "${YELLOW}════════════════════════════════════════════════════════════${NC}"
|
||||||
echo -e "${YELLOW} Preparing Release${NC}"
|
echo -e "${YELLOW} Preparing Release${NC}"
|
||||||
@@ -135,11 +240,16 @@ if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Step 1: Push main to stable
|
# Step 1: Push tested code to Gitea (stable & staging-dev)
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
echo
|
echo
|
||||||
echo -e "${BLUE}Step 1: Pushing main → stable on Gitea...${NC}"
|
echo -e "${BLUE}Step 1: Pushing tested code to Gitea (stable & staging-dev)...${NC}"
|
||||||
git push "${GITEA_REMOTE}" main:stable --force-with-lease
|
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
|
# 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}
|
git tag -a "${TAG}" -m "${RELEASE_MESSAGE}
|
||||||
|
|
||||||
- Stable release of Sovran_SystemsOS
|
- 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
|
# Update VERSION file for ISO builds
|
||||||
echo "${VERSION}" > VERSION
|
echo "${VERSION}" > VERSION
|
||||||
@@ -167,19 +285,11 @@ echo -e "${BLUE}Step 3: Updating ${CHANGELOG_FILE}...${NC}"
|
|||||||
|
|
||||||
TODAY=$(date +%Y-%m-%d)
|
TODAY=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
# Create new changelog entry
|
# Create new changelog entry from the generated notes (no placeholders)
|
||||||
NEW_ENTRY="## [${VERSION}] - ${TODAY}
|
NEW_ENTRY="## [${VERSION}] - ${TODAY}
|
||||||
|
|
||||||
### Added
|
${RELEASE_NOTES}
|
||||||
- (Add new features here)
|
[${VERSION}]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/${TAG}
|
||||||
|
|
||||||
### Changed
|
|
||||||
- (Add changes here)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- (Add bug fixes here)
|
|
||||||
|
|
||||||
[${VERSION}]: ${GITEA_API_URL%/*}/Sovran_Systems/Sovran_SystemsOS/releases/tag/${TAG}
|
|
||||||
"
|
"
|
||||||
|
|
||||||
# Prepend to changelog (after the header)
|
# Prepend to changelog (after the header)
|
||||||
@@ -199,6 +309,7 @@ if [ -f "$CHANGELOG_FILE" ]; then
|
|||||||
}
|
}
|
||||||
{ print }
|
{ print }
|
||||||
' "$CHANGELOG_FILE" > "${CHANGELOG_FILE}.tmp" && mv "${CHANGELOG_FILE}.tmp" "$CHANGELOG_FILE"
|
' "$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}"
|
echo -e " ${GREEN}✓${NC} CHANGELOG.md updated with new section for ${TAG}"
|
||||||
else
|
else
|
||||||
@@ -215,11 +326,24 @@ else
|
|||||||
|
|
||||||
# Ask if user wants to push
|
# Ask if user wants to push
|
||||||
echo
|
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
|
if [[ "$push_confirm" =~ ^[Yy]$ ]]; then
|
||||||
echo -e "${BLUE}Pushing changelog commit...${NC}"
|
if git remote | grep -q -x "${GITHUB_REMOTE}"; then
|
||||||
git push "${GITHUB_REMOTE}" main
|
echo -e "${BLUE}Pushing changelog commit to GitHub main...${NC}"
|
||||||
echo -e " ${GREEN}✓${NC} Changelog pushed to GitHub"
|
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
|
else
|
||||||
echo " (Changelog commit left local — remember to push later)"
|
echo " (Changelog commit left local — remember to push later)"
|
||||||
fi
|
fi
|
||||||
@@ -234,8 +358,8 @@ echo -e "${BLUE}Step 4: Creating GitHub Release...${NC}"
|
|||||||
if command -v gh &>/dev/null; then
|
if command -v gh &>/dev/null; then
|
||||||
if gh release create "${TAG}" \
|
if gh release create "${TAG}" \
|
||||||
--repo naturallaw777/Sovran_SystemsOS \
|
--repo naturallaw777/Sovran_SystemsOS \
|
||||||
--title "${TAG}" \
|
--title "${TAG} — ${RELEASE_MESSAGE#Sovran_SystemsOS v* — }" \
|
||||||
--notes "${RELEASE_MESSAGE}" \
|
--notes-file "${NOTES_FILE}" \
|
||||||
--target main 2>/dev/null; then
|
--target main 2>/dev/null; then
|
||||||
echo -e " ${GREEN}✓${NC} GitHub release created successfully"
|
echo -e " ${GREEN}✓${NC} GitHub release created successfully"
|
||||||
else
|
else
|
||||||
@@ -266,16 +390,21 @@ fi
|
|||||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||||
GITEA_REPO="Sovran_Systems/Sovran_SystemsOS"
|
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 \
|
RESPONSE=$(curl -s -X POST \
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{
|
-d "${PAYLOAD}" \
|
||||||
\"tag_name\": \"${TAG}\",
|
|
||||||
\"name\": \"${TAG}\",
|
|
||||||
\"body\": \"${RELEASE_MESSAGE}\",
|
|
||||||
\"draft\": false,
|
|
||||||
\"prerelease\": false
|
|
||||||
}" \
|
|
||||||
"${GITEA_API_URL}/repos/${GITEA_REPO}/releases" 2>/dev/null || echo "")
|
"${GITEA_API_URL}/repos/${GITEA_REPO}/releases" 2>/dev/null || echo "")
|
||||||
|
|
||||||
if echo "$RESPONSE" | grep -q '"id"'; then
|
if echo "$RESPONSE" | grep -q '"id"'; then
|
||||||
@@ -296,8 +425,11 @@ echo -e "${GREEN}╚════════════════════
|
|||||||
echo
|
echo
|
||||||
echo "Next manual steps (recommended):"
|
echo "Next manual steps (recommended):"
|
||||||
echo " • Review and enhance the new section in CHANGELOG.md"
|
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 " • Verify releases on both GitHub and Gitea"
|
||||||
echo
|
echo
|
||||||
echo -e "${CYAN}Tag created: ${TAG}${NC}"
|
echo -e "${CYAN}Tag created: ${TAG}${NC}"
|
||||||
git show "${TAG}" --quiet
|
git show "${TAG}" --quiet
|
||||||
|
|
||||||
|
# Clean up temp notes file
|
||||||
|
rm -f "${NOTES_FILE}"
|
||||||
Reference in new issue
Block a user