"use strict"; // ── Helpers ─────────────────────────────────────────────────────── function tileId(svc) { return svc.unit + "::" + svc.name; } function statusClass(health) { if (!health) return "unknown"; if (health === "healthy") return "active"; if (health === "needs_attention") return "needs-attention"; if (health === "active") return "active"; // backwards compat if (health === "inactive") return "inactive"; if (health === "failed") return "failed"; if (health === "disabled") return "disabled"; if (health === "syncing") return "syncing"; if (STATUS_LOADING_STATES.has(health)) return "loading"; if (health === "checking_reachability") return "checking-reachability"; return "unknown"; } function statusText(health, enabled) { if (!enabled) return "Disabled"; if (health === "healthy") return "Active"; if (health === "needs_attention") return "Needs Attention"; if (health === "active") return "Active"; if (health === "inactive") return "Inactive"; if (health === "failed") return "Failed"; if (health === "syncing") return "Syncing\u2026"; if (!health || health === "unknown") return "Unknown"; if (STATUS_LOADING_STATES.has(health)) return health; if (health === "checking_reachability") return "Checking\u2026"; return health; } function escHtml(str) { return String(str).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"); } function linkify(str) { return escHtml(str).replace(/(https?:\/\/[^\s<]+)/g, '$1'); } // ── Router port-forwarding guide ────────────────────────────────── // Whether a port is truly reachable can only be judged from OUTSIDE the // network, so we never show a local "ready" verdict here. We only tell the // user exactly what to enter in their router. // Render the protocol cell so TCP / UDP / both is unmistakable. function portProtocolHtml(protocol) { var p = String(protocol || "TCP").toUpperCase(); var isTcp = p.indexOf("TCP") !== -1; var isUdp = p.indexOf("UDP") !== -1; if (isTcp && isUdp) { return 'TCP + UDP' + 'both required'; } if (isUdp) return 'UDP'; return 'TCP'; } // ports: [{ port, protocol, description }] // opts: { internalIp, serviceName, tableClass, introClass, noteClass } function renderPortForwardGuideHtml(ports, opts) { opts = opts || {}; var tableClass = opts.tableClass || "port-req-table"; var introClass = opts.introClass || "port-req-intro"; var noteClass = opts.noteClass || "port-req-hint"; var ipHtml = opts.internalIp ? '' + escHtml(opts.internalIp) + '' : 'this computer’s internal IP (shown as “Internal IP” at the top of the Hub dashboard)'; var rows = (ports || []).map(function(p) { return '' + '' + escHtml(p.port) + '' + '' + portProtocolHtml(p.protocol) + '' + '' + escHtml(p.description || "") + '' + ''; }).join(""); var forWhat = opts.serviceName ? 'For ' + escHtml(opts.serviceName) + ' to be reachable from outside your home network, open' : 'Open'; return '

' + forWhat + ' the ports below in your router’s port forwarding settings ' + 'and point them at ' + ipHtml + '.' + '

' + '' + '' + '' + '' + rows + '' + '
Port(s)ProtocolUsed for
' + '

' + '📱 How to confirm it worked: forwarding happens on your router, so it can only be verified from outside your network. ' + 'Turn Wi-Fi off on your phone and open the service over mobile data — if it loads, your ports are open.' + '

'; } function formatDuration(seconds) { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); if (h > 0) return h + "h " + m + "m " + s + "s"; if (m > 0) return m + "m " + s + "s"; return s + "s"; } // ── Fetch wrappers ──────────────────────────────────────────────── async function apiFetch(path, options) { const res = await fetch(path, options || {}); if (!res.ok) { let detail = res.status + " " + res.statusText; try { const body = await res.json(); if (body && body.detail) { if (typeof body.detail === "string") { detail = body.detail; } else if (body.detail && typeof body.detail.message === "string") { detail = body.detail.message; } } else if (body && typeof body.message === "string") { detail = body.message; } else if (body && typeof body.error === "string") { detail = body.error; } } catch (e) {} throw new Error(detail); } return res.json(); } // ── BIP-110 badge state config ──────────────────────────────────── // Shared lookup used by tiles.js and service-detail.js. // Keys match the "state" values returned by /api/bitcoin/bip110. var BIP110_BADGE_CONFIG = { active: { cls: 'tile-bip110-badge--active', label: 'Active', title: 'BIP-110 is active on this node' }, locked_in: { cls: 'tile-bip110-badge--locked_in', label: 'Locked In', title: 'BIP-110 is locked in and will activate shortly' }, signaling: { cls: 'tile-bip110-badge--signaling', label: 'Signaling', title: 'Node is signaling readiness for BIP-110' }, not_signaling: { cls: 'tile-bip110-badge--not_signaling',label: 'Not Signaling', title: 'Node supports BIP-110 but is not signaling this period' }, unsupported: { cls: 'tile-bip110-badge--unsupported', label: 'Not Supported', title: 'This node build does not include BIP-110' }, unknown: { cls: 'tile-bip110-badge--unknown', label: '\u2014', title: 'Status unavailable (node syncing or RPC not ready)' } };