diff --git a/app/sovran_systemsos_web/static/app.js b/app/sovran_systemsos_web/static/app.js
index 2f039ab..72b0160 100644
--- a/app/sovran_systemsos_web/static/app.js
+++ b/app/sovran_systemsos_web/static/app.js
@@ -85,6 +85,14 @@ function escHtml(str) {
.replace(/'/g, "'");
}
+function linkify(str) {
+ const escaped = escHtml(str);
+ return escaped.replace(
+ /(https?:\/\/[^\s<]+)/g,
+ '$1'
+ );
+}
+
// ── Fetch wrappers ────────────────────────────────────────────────
async function apiFetch(path, options = {}) {
@@ -269,7 +277,7 @@ async function loadNetwork() {
if ($internalIp) $internalIp.textContent = data.internal_ip || "—";
if ($externalIp) $externalIp.textContent = data.external_ip || "—";
} catch (_) {
- if ($internalIp) $internalIp.textContent = "��";
+ if ($internalIp) $internalIp.textContent = "—";
if ($externalIp) $externalIp.textContent = "—";
}
}
@@ -310,11 +318,12 @@ async function openCredsModal(unit, name) {
let html = "";
for (const cred of data.credentials) {
const id = "cred-" + Math.random().toString(36).substring(2, 8);
+ const displayValue = linkify(cred.value);
html += `
${escHtml(cred.label)}
-
${escHtml(cred.value)}
+
${displayValue}
diff --git a/app/sovran_systemsos_web/static/style.css b/app/sovran_systemsos_web/static/style.css
index 62eeee2..e78c96c 100644
--- a/app/sovran_systemsos_web/static/style.css
+++ b/app/sovran_systemsos_web/static/style.css
@@ -780,7 +780,7 @@ button.btn-reboot:hover:not(:disabled) {
justify-content: center;
}
.service-tile {
- width: 160px;
+ : width: 160px;
min-height: 200px;
}
.reboot-card {
@@ -790,4 +790,18 @@ button.btn-reboot:hover:not(:disabled) {
.creds-dialog {
margin: 0 12px;
}
-}
\ No newline at end of file
+
+/* ── Credential links ─────────────────────────────────────── */
+.creds-link {
+ color: #58a6ff;
+ text-decoration: none;
+ word-break: break-all;
+}
+.creds-link:hover {
+ text-decoration: underline;
+ color: #79c0ff;
+}
+
+
+
+}
diff --git a/modules/core/sovran-hub.nix b/modules/core/sovran-hub.nix
index 9e60227..a0f1d1f 100644
--- a/modules/core/sovran-hub.nix
+++ b/modules/core/sovran-hub.nix
@@ -35,14 +35,18 @@ let
]; }
{ name = "LND"; unit = "lnd.service"; type = "system"; icon = "lnd"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = []; }
{ name = "Ride The Lightning"; unit = "rtl.service"; type = "system"; icon = "rtl"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
- { label = "URL"; file = "/var/lib/tor/onion/rtl/hostname"; prefix = "http://"; }
+ { label = "Tor Access"; file = "/var/lib/tor/onion/rtl/hostname"; prefix = "http://"; }
+ { label = "Local Network"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":3050"; }
{ label = "Password"; file = "/etc/nix-bitcoin-secrets/rtl-password"; }
]; }
{ name = "BTCPayserver"; unit = "btcpayserver.service"; type = "system"; icon = "btcpayserver"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
{ label = "URL"; file = "/var/lib/domains/btcpayserver"; prefix = "https://"; }
{ label = "Note"; value = "Create your admin account on first visit"; }
]; }
- { name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; category = "bitcoin-apps"; credentials = []; }
+ { name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; category = "bitcoin-apps"; credentials = [
+ { label = "Tor Access"; file = "/var/lib/tor/onion/mempool-frontend/hostname"; prefix = "http://"; }
+ { label = "Local Network"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":60847"; }
+ ]; }
]
# ── Communication ──────────────────────────────────────────
++ [
@@ -205,6 +209,37 @@ LAUNCHER
in
{
config = {
+ # ── Save internal IP for hub credentials ────────────────────
+ systemd.services.save-internal-ip = {
+ description = "Save internal IP address for hub credentials";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network-online.target" ];
+ wants = [ "network-online.target" ];
+ serviceConfig = {
+ Type = "oneshot";
+ RemainAfterExit = true;
+ };
+ path = [ pkgs.iproute2 pkgs.coreutils pkgs.hostname ];
+ script = ''
+ mkdir -p /var/lib/secrets
+ IP=$(hostname -I | awk '{print $1}')
+ if [ -n "$IP" ]; then
+ echo "$IP" > /var/lib/secrets/internal-ip
+ chmod 644 /var/lib/secrets/internal-ip
+ fi
+ '';
+ };
+
+ # ── Refresh IP periodically (in case DHCP changes it) ──────
+ systemd.timers.save-internal-ip = {
+ wantedBy = [ "timers.target" ];
+ timerConfig = {
+ OnBootSec = "30s";
+ OnUnitActiveSec = "15min";
+ Unit = "save-internal-ip.service";
+ };
+ };
+
# ── Web server as a systemd service ────────────────────────
systemd.services.sovran-hub-web = {
description = "Sovran_SystemsOS Hub Web Interface";