fix: federation peer-joined updates empty onion addresses
Some checks failed
Some checks failed
When a node was already known (via link-node) but had an empty onion address, the peer-joined handler returned early without updating the onion. Now it patches missing onion/pubkey fields on existing nodes. Also adds update_node() to federation storage and updates the architecture comparison doc with system resources, StartOS/umbrelOS tabs, Web5 section, and comparison view. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -119,6 +119,9 @@
|
||||
.proto-card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; padding: 14px 16px; cursor: pointer; transition: all 0.2s; overflow-wrap: break-word; word-break: break-word; }
|
||||
.proto-card:hover { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.12); }
|
||||
.proto-card.expanded .proto-details { display: block; }
|
||||
.proto-grid.all-expanded { align-items: stretch; }
|
||||
.proto-grid.all-expanded .proto-card { display: flex; flex-direction: column; }
|
||||
.proto-grid.all-expanded .proto-details { flex: 1; }
|
||||
.proto-card-name { font-size: 14px; font-weight: 600; color: #fff; }
|
||||
.proto-card-desc { font-size: 12px; color: #8b8fa3; margin-top: 4px; }
|
||||
.proto-card-layman { font-size: 11px; color: #6b7280; margin-top: 2px; font-style: italic; }
|
||||
@@ -315,6 +318,76 @@
|
||||
body.light .l-cnt { background: rgba(16,185,129,0.05); border-color: rgba(16,185,129,0.15); }
|
||||
body.light .l-ui { background: rgba(236,72,153,0.05); border-color: rgba(236,72,153,0.15); }
|
||||
body.light .l-kiosk { background: rgba(251,146,60,0.05); border-color: rgba(251,146,60,0.15); }
|
||||
|
||||
/* ═══ MOBILE ═══ */
|
||||
@media (max-width: 768px) {
|
||||
/* Header: compact, hide subtitle, move controls */
|
||||
.header { padding: 12px 16px; }
|
||||
.header h1 { font-size: 18px; }
|
||||
.header p { display: none; }
|
||||
.header-controls { position: static; display: flex; margin-top: 8px; }
|
||||
|
||||
/* System selector: scroll horizontally, smaller text */
|
||||
.sys-selector { padding: 0 8px; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
|
||||
.sys-selector::-webkit-scrollbar { display: none; }
|
||||
.sys-btn { padding: 8px 14px; font-size: 13px; white-space: nowrap; flex-shrink: 0; }
|
||||
.sys-version { display: none; }
|
||||
|
||||
/* Nav: scroll horizontally, smaller */
|
||||
.nav { padding: 0; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
|
||||
.nav::-webkit-scrollbar { display: none; }
|
||||
.nav-btn { padding: 10px 14px; font-size: 12px; flex-shrink: 0; }
|
||||
|
||||
/* Section content: reduce padding */
|
||||
.section { padding: 16px; }
|
||||
|
||||
/* Stats: 2 columns on mobile */
|
||||
.stats { grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||||
.stat { padding: 10px 8px; }
|
||||
.stat-value { font-size: 18px; }
|
||||
.stat-label { font-size: 10px; }
|
||||
|
||||
/* Cards: single column */
|
||||
.cards { grid-template-columns: 1fr; }
|
||||
.proto-grid { grid-template-columns: 1fr; }
|
||||
.sec-grid { grid-template-columns: 1fr; }
|
||||
|
||||
/* Layers: tighter padding */
|
||||
.layer { padding: 12px 14px; }
|
||||
|
||||
/* Dep chain: smaller font */
|
||||
.dep-chain { font-size: 11px; padding: 12px 14px; overflow-x: auto; }
|
||||
|
||||
/* Path tree: smaller font */
|
||||
.path-tree { font-size: 10px; padding: 12px 14px; }
|
||||
|
||||
/* Comparison table: horizontal scroll */
|
||||
.cmp-table { display: block; overflow-x: auto; }
|
||||
|
||||
/* Glossary: single column */
|
||||
.glossary { column-count: 1; }
|
||||
|
||||
/* Resource grid: single column */
|
||||
.section > div[style*="grid-template-columns: 1fr 1fr"] { display: flex !important; flex-direction: column !important; }
|
||||
|
||||
/* Popover: full width on mobile */
|
||||
.popover { width: calc(100vw - 32px); left: 16px !important; }
|
||||
|
||||
/* Boot steps: tighter */
|
||||
.boot-step { gap: 12px; }
|
||||
.boot-num { width: 24px; height: 24px; font-size: 11px; }
|
||||
|
||||
/* Toggle buttons */
|
||||
.toggle-btn { padding: 6px 10px; font-size: 11px; }
|
||||
}
|
||||
|
||||
/* Small phones */
|
||||
@media (max-width: 400px) {
|
||||
.stats { grid-template-columns: repeat(2, 1fr); }
|
||||
.sys-btn { padding: 8px 10px; font-size: 12px; }
|
||||
.nav-btn { padding: 8px 10px; font-size: 11px; }
|
||||
.header h1 { font-size: 16px; }
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
|
||||
@@ -355,11 +428,11 @@
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat"><div class="stat-value">34</div><div class="stat-label">Containers</div></div>
|
||||
<div class="stat"><div class="stat-value">8</div><div class="stat-label">System Layers</div></div>
|
||||
<div class="stat"><div class="stat-value">260+</div><div class="stat-label">RPC Methods</div></div>
|
||||
<div class="stat"><div class="stat-value">9</div><div class="stat-label">Protocols</div></div>
|
||||
<div class="stat"><div class="stat-value">LUKS2</div><div class="stat-label">Encryption</div></div>
|
||||
<div class="stat"><div class="stat-value">Rootless</div><div class="stat-label">Containers</div></div>
|
||||
<div class="stat"><div class="stat-value">Tor</div><div class="stat-label">Privacy Layer</div></div>
|
||||
<div class="stat"><div class="stat-value">Rootless</div><div class="stat-label">Podman</div></div>
|
||||
<div class="stat"><div class="stat-value">8 GB+</div><div class="stat-label">Recommended RAM</div></div>
|
||||
</div>
|
||||
|
||||
<div class="layer-stack">
|
||||
@@ -461,6 +534,85 @@
|
||||
<span class="hl" onclick="showContainers('vaultwarden')">vaultwarden</span> <span class="hl" onclick="showContainers('nextcloud')">nextcloud</span> <span class="hl" onclick="showContainers('searxng')">searxng</span> <span class="hl" onclick="showContainers('uptime-kuma')">uptime-kuma</span> <span class="hl" onclick="showContainers('ollama')">ollama</span>
|
||||
<span class="hl" onclick="showContainers('onlyoffice')">onlyoffice</span> <span class="hl" onclick="showContainers('nginx-proxy-manager')">nginx-pm</span> <span class="hl" onclick="showContainers('portainer')">portainer</span></div>
|
||||
</div>
|
||||
|
||||
<h3 class="subsection-title">System Resources</h3>
|
||||
|
||||
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px;">
|
||||
|
||||
<div style="background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.06); border-radius:10px; padding:16px;">
|
||||
<div style="font-size:13px; font-weight:600; color:#fff; margin-bottom:10px; border-bottom:1px solid rgba(255,255,255,0.06); padding-bottom:8px;">Hardware Requirements</div>
|
||||
<table class="cmp-table" style="margin:0;">
|
||||
<tr><td class="cmp-label">Minimum RAM</td><td>4 GB</td></tr>
|
||||
<tr><td class="cmp-label">Recommended RAM</td><td>8 GB+ (core stack uses ~8–10 GB)</td></tr>
|
||||
<tr><td class="cmp-label">Minimum Disk</td><td>32 GB SSD</td></tr>
|
||||
<tr><td class="cmp-label">Recommended Disk</td><td>1 TB+ NVMe SSD</td></tr>
|
||||
<tr><td class="cmp-label">CPU</td><td>x86_64 or ARM64, 4+ cores recommended</td></tr>
|
||||
<tr><td class="cmp-label">Network</td><td>Ethernet recommended (WiFi supported)</td></tr>
|
||||
<tr><td class="cmp-label">Targets</td><td>HP ProDesk, Intel NUC, any standard PC</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.06); border-radius:10px; padding:16px;">
|
||||
<div style="font-size:13px; font-weight:600; color:#fff; margin-bottom:10px; border-bottom:1px solid rgba(255,255,255,0.06); padding-bottom:8px;">Memory Budget (all containers)</div>
|
||||
<table class="cmp-table" style="margin:0;">
|
||||
<tr><td class="cmp-label">Bitcoin Knots</td><td>2 GB (1 GB low-memory mode)</td></tr>
|
||||
<tr><td class="cmp-label">ElectrumX</td><td>1 GB</td></tr>
|
||||
<tr><td class="cmp-label">LND</td><td>512 MB</td></tr>
|
||||
<tr><td class="cmp-label">BTCPay + DB</td><td>1.5 GB (1 GB + 512 MB)</td></tr>
|
||||
<tr><td class="cmp-label">Mempool stack</td><td>1.3 GB (512+256+512 MB)</td></tr>
|
||||
<tr><td class="cmp-label">Fedimint + GW</td><td>1 GB (512+512 MB)</td></tr>
|
||||
<tr><td class="cmp-label">Ollama (AI)</td><td>4 GB (1 GB low-memory)</td></tr>
|
||||
<tr><td class="cmp-label">All other apps</td><td>128–1024 MB each</td></tr>
|
||||
<tr><td class="cmp-label" style="color:#fff;"><b>Total allocated</b></td><td style="color:#fff;"><b>~20 GB</b> (not all run simultaneously)</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px;">
|
||||
|
||||
<div style="background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.06); border-radius:10px; padding:16px;">
|
||||
<div style="font-size:13px; font-weight:600; color:#fff; margin-bottom:10px; border-bottom:1px solid rgba(255,255,255,0.06); padding-bottom:8px;">Disk Usage by Component</div>
|
||||
<table class="cmp-table" style="margin:0;">
|
||||
<tr><td class="cmp-label">Bitcoin blockchain (full)</td><td>~600 GB</td></tr>
|
||||
<tr><td class="cmp-label">Bitcoin (pruned)</td><td>~550 MB</td></tr>
|
||||
<tr><td class="cmp-label">ElectrumX index</td><td>~50 GB</td></tr>
|
||||
<tr><td class="cmp-label">LND channels + wallet</td><td>~1 GB</td></tr>
|
||||
<tr><td class="cmp-label">Databases (all)</td><td>~2–10 GB</td></tr>
|
||||
<tr><td class="cmp-label">Container images</td><td>~15 GB</td></tr>
|
||||
<tr><td class="cmp-label">Ollama models</td><td>1–50 GB (varies)</td></tr>
|
||||
<tr><td class="cmp-label">Media (Jellyfin/Photos)</td><td>User-determined</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.06); border-radius:10px; padding:16px;">
|
||||
<div style="font-size:13px; font-weight:600; color:#fff; margin-bottom:10px; border-bottom:1px solid rgba(255,255,255,0.06); padding-bottom:8px;">Network Ports (External)</div>
|
||||
<table class="cmp-table" style="margin:0;">
|
||||
<tr><td class="cmp-label">80 / 443</td><td>Nginx → Web UI, app proxies</td></tr>
|
||||
<tr><td class="cmp-label">8333</td><td>Bitcoin P2P (node discovery)</td></tr>
|
||||
<tr><td class="cmp-label">9735</td><td>Lightning P2P (payment routing)</td></tr>
|
||||
<tr><td class="cmp-label">50001</td><td>Electrum protocol (wallet queries)</td></tr>
|
||||
<tr><td class="cmp-label">22</td><td>SSH (admin access)</td></tr>
|
||||
<tr style="border-top:1px solid rgba(255,255,255,0.06);"><td class="cmp-label" style="color:#6b7280; font-style:italic;">Internal only</td><td style="color:#6b7280; font-style:italic;">8332 (RPC), 10009 (gRPC), 8080 (REST), 8999, 4080, 3000, 3001, 8082–8096, 9000…</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="display:grid; grid-template-columns: 1fr; gap: 12px; margin-top: 12px;">
|
||||
<div style="background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.06); border-radius:10px; padding:16px;">
|
||||
<div style="font-size:13px; font-weight:600; color:#fff; margin-bottom:10px; border-bottom:1px solid rgba(255,255,255,0.06); padding-bottom:8px;">Container Security Defaults</div>
|
||||
<table class="cmp-table" style="margin:0;">
|
||||
<tr><td class="cmp-label">Capabilities</td><td><code>--cap-drop=ALL</code> then add only needed: CHOWN, FOWNER, SETUID, SETGID, DAC_OVERRIDE. Some get NET_RAW (LND), NET_BIND_SERVICE (Vaultwarden, nginx-pm, LND-UI).</td></tr>
|
||||
<tr><td class="cmp-label">Privileges</td><td><code>--security-opt=no-new-privileges</code> on all containers</td></tr>
|
||||
<tr><td class="cmp-label">Health checks</td><td>All containers: <code>--health-interval=120s --health-timeout=5s --health-retries=3</code></td></tr>
|
||||
<tr><td class="cmp-label">Low-memory mode</td><td>Auto-detected: Bitcoin 2G→1G, PhotoPrism 1G→512M, OnlyOffice 2G→1G, Ollama 4G→1G</td></tr>
|
||||
<tr><td class="cmp-label">Disk mode</td><td>Auto: if disk <1TB → Bitcoin prune=550, dbcache=512M. If ≥1TB → full txindex, dbcache=4G</td></tr>
|
||||
<tr><td class="cmp-label">RPC methods</td><td>260+ registered across 20+ namespaces (auth, seed, package, bitcoin, lnd, identity, tor, nostr, mesh, federation, dwn, system, monitoring…)</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
@@ -1402,7 +1554,7 @@
|
||||
|
||||
<div class="proto-grid">
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">JSON-RPC 2.0</div>
|
||||
<div class="proto-card-desc">Primary protocol between the web UI and the Rust backend. All commands are RPC calls.</div>
|
||||
<div class="proto-card-layman">Like texting the backend: you send a message ("please start this app"), it texts back ("done" or "error").</div>
|
||||
@@ -1417,7 +1569,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">WebSocket</div>
|
||||
<div class="proto-card-desc">Real-time bidirectional channel for live updates — container status changes, logs, events.</div>
|
||||
<div class="proto-card-layman">An open phone line between your browser and the server. Instead of asking "any updates?" every second, the server just tells you when something changes.</div>
|
||||
@@ -1430,7 +1582,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">Bitcoin RPC (JSON-RPC 1.0)</div>
|
||||
<div class="proto-card-desc">How apps talk to the Bitcoin node. Authenticated with username + HMAC-hashed password.</div>
|
||||
<div class="proto-card-layman">The language apps use to ask Bitcoin questions: "what's the current block?" or "send this transaction."</div>
|
||||
@@ -1443,7 +1595,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">gRPC</div>
|
||||
<div class="proto-card-desc">High-performance RPC protocol used by LND for admin operations. Binary format, strongly typed.</div>
|
||||
<div class="proto-card-layman">A fast, structured way for apps to control the Lightning node. More efficient than regular HTTP for complex operations.</div>
|
||||
@@ -1456,7 +1608,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">Electrum Protocol</div>
|
||||
<div class="proto-card-desc">Lightweight protocol for wallet address lookups. JSON-RPC over raw TCP sockets.</div>
|
||||
<div class="proto-card-layman">How Bitcoin wallets check their balance without downloading the entire blockchain. Ask "what transactions touched this address?" and get an instant answer.</div>
|
||||
@@ -1468,7 +1620,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">ZMQ (ZeroMQ)</div>
|
||||
<div class="proto-card-desc">Publish-subscribe messaging from Bitcoin node. Instant notifications for new blocks and transactions.</div>
|
||||
<div class="proto-card-layman">A broadcasting system. When a new Bitcoin block is found, Bitcoin instantly shouts it out and everyone listening hears immediately.</div>
|
||||
@@ -1481,7 +1633,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">Tor (SOCKS5 + Hidden Services)</div>
|
||||
<div class="proto-card-desc">Privacy layer. Routes Bitcoin P2P through onion routing, exposes services as .onion addresses.</div>
|
||||
<div class="proto-card-layman">Like sending a letter through 3 random post offices so nobody knows where it came from. Also lets people reach your node without knowing your real IP.</div>
|
||||
@@ -1494,7 +1646,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">Nostr (NIP-01)</div>
|
||||
<div class="proto-card-desc">Decentralized social protocol. WebSocket-based relay communication for events (posts, follows, messages).</div>
|
||||
<div class="proto-card-layman">A social media protocol where no company controls the network. Your posts live on relays, and you own your identity with a cryptographic key.</div>
|
||||
@@ -1507,7 +1659,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">DWN (Decentralized Web Node)</div>
|
||||
<div class="proto-card-desc">W3C protocol for storing encrypted data and messages in a decentralized way. Identity-linked storage.</div>
|
||||
<div class="proto-card-layman">A personal data vault. Apps can store data here that only you control. Like a safety deposit box that follows you across the internet.</div>
|
||||
@@ -2413,7 +2565,7 @@
|
||||
<div class="section" id="sec-start9-protocols">
|
||||
|
||||
<div class="proto-grid">
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">JSON-RPC (Host ↔ Service)</div>
|
||||
<div class="proto-card-desc">All communication between the Rust backend and services uses JSON-RPC over Unix domain sockets.</div>
|
||||
<div class="proto-card-layman">Apps and the system talk through a structured messaging format over local socket files — fast and secure.</div>
|
||||
@@ -2426,7 +2578,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">JSON-RPC (UI ↔ Backend)</div>
|
||||
<div class="proto-card-desc">The Angular frontend communicates with startd via JSON-RPC over HTTP. 100+ API methods. State sync via Patch-DB WebSocket.</div>
|
||||
<div class="proto-card-layman">The dashboard sends commands and gets responses in JSON format. Live updates stream automatically through a WebSocket.</div>
|
||||
@@ -2438,7 +2590,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">Patch-DB (CBOR Reactive Sync)</div>
|
||||
<div class="proto-card-desc">Custom reactive database using CBOR encoding. Backend pushes diffs over WebSocket — UI updates automatically without polling.</div>
|
||||
<div class="proto-card-layman">Instead of the UI constantly asking "what changed?", the backend pushes only what changed, in a compact binary format.</div>
|
||||
@@ -2450,7 +2602,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">HTTPS / TLS (Built-in)</div>
|
||||
<div class="proto-card-desc">TLS termination handled directly by the Rust backend (tokio-rustls). Self-signed root CA per server + ACME for public domains.</div>
|
||||
<div class="proto-card-layman">The backend IS the web server — no nginx or caddy needed. It handles encryption directly.</div>
|
||||
@@ -2462,7 +2614,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">WireGuard (VPN Tunnels)</div>
|
||||
<div class="proto-card-desc">First-class WireGuard support for remote access. Users add WireGuard configs as "gateways." Managed by tunnelbox daemon.</div>
|
||||
<div class="proto-card-layman">Built-in VPN for accessing your node from anywhere. Add a WireGuard config and get a secure tunnel.</div>
|
||||
@@ -2473,7 +2625,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">DNS (hickory-server)</div>
|
||||
<div class="proto-card-desc">Built-in DNS server for service discovery and resolution. Also uses mDNS (avahi) for .local domain access on LAN.</div>
|
||||
<div class="proto-card-layman">The system runs its own DNS so containers can find each other by name. Your phone finds the node via .local address.</div>
|
||||
@@ -2879,7 +3031,7 @@
|
||||
|
||||
<div class="section" id="sec-umbrelos-protocols">
|
||||
<div class="proto-grid">
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">tRPC (UI ↔ Backend)</div>
|
||||
<div class="proto-card-desc">TypeScript-first RPC framework with end-to-end type safety. Runs over both HTTP and WebSocket on port 80.</div>
|
||||
<div class="proto-card-layman">A typed communication channel between the dashboard and the backend. If the API changes, TypeScript catches errors automatically.</div>
|
||||
@@ -2892,7 +3044,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">Docker Compose (App Lifecycle)</div>
|
||||
<div class="proto-card-desc">Each app managed via Docker Compose v2. Install/start/stop/update handled by a bash script (app-script) calling docker compose.</div>
|
||||
<div class="proto-card-layman">Apps are defined as Docker Compose projects. A bash script handles the lifecycle by calling docker compose commands.</div>
|
||||
@@ -2904,7 +3056,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">exports.sh (Dependency Resolution)</div>
|
||||
<div class="proto-card-desc">Shell scripts that export environment variables (IPs, ports, RPC credentials). When app B depends on app A, A's exports.sh is sourced first.</div>
|
||||
<div class="proto-card-layman">Apps share their connection details through environment variables set by shell scripts.</div>
|
||||
@@ -2916,7 +3068,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">Tor (Optional, Containerized)</div>
|
||||
<div class="proto-card-desc">Toggle per-system. tor_proxy container provides SOCKS5 at 10.21.21.11. Per-app tor_server containers create hidden services.</div>
|
||||
<div class="proto-card-layman">Tor is optional and runs in its own container. When enabled, each app gets its own .onion address for remote access.</div>
|
||||
@@ -2928,7 +3080,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">JWT + Proxy Tokens (Auth)</div>
|
||||
<div class="proto-card-desc">JWT for API authentication. Separate "proxy tokens" validate iframe requests to app_proxy containers. bcrypt password hashing.</div>
|
||||
<div class="proto-card-layman">Login tokens that prove who you are. Separate tokens for the dashboard API and for accessing individual apps.</div>
|
||||
@@ -2940,7 +3092,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proto-card" onclick="this.classList.toggle('expanded')">
|
||||
<div class="proto-card" onclick="toggleProto(this)">
|
||||
<div class="proto-card-name">Git (App Store)</div>
|
||||
<div class="proto-card-desc">App store is a Git repository cloned locally via isomorphic-git. Pulled every 5 minutes for updates.</div>
|
||||
<div class="proto-card-layman">The app catalog is just a Git repo. Umbrel checks for new apps and updates by pulling the latest commits every 5 minutes.</div>
|
||||
@@ -3533,6 +3685,138 @@ function syncRowHeights(cards) {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleProto(el) {
|
||||
const grid = el.closest('.proto-grid')
|
||||
if (grid) {
|
||||
const isExpanding = !el.classList.contains('expanded')
|
||||
const cards = Array.from(grid.querySelectorAll('.proto-card'))
|
||||
|
||||
if (isExpanding) {
|
||||
// Convert free-form <b>Label:</b> HTML into structured detail-rows
|
||||
cards.forEach(structureProtoDetails)
|
||||
// Normalize so all cards have the same rows in same order
|
||||
normalizeProtoRows(cards)
|
||||
cards.forEach(c => c.classList.add('expanded'))
|
||||
grid.classList.add('all-expanded')
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => syncProtoHeights(cards)))
|
||||
} else {
|
||||
cards.forEach(c => c.classList.remove('expanded'))
|
||||
grid.classList.remove('all-expanded')
|
||||
grid.querySelectorAll('.proto-card-name, .proto-card-desc, .proto-card-layman, .detail-row').forEach(e => e.style.minHeight = '')
|
||||
}
|
||||
} else {
|
||||
el.classList.toggle('expanded')
|
||||
}
|
||||
}
|
||||
|
||||
// Convert <b>Label:</b> value<br> into structured detail-rows
|
||||
function structureProtoDetails(card) {
|
||||
const details = card.querySelector('.proto-details')
|
||||
if (!details || details.dataset.structured) return
|
||||
details.dataset.structured = '1'
|
||||
|
||||
const html = details.innerHTML
|
||||
// Split on <br> or <br/> or <br />
|
||||
const lines = html.split(/<br\s*\/?>/).map(l => l.trim()).filter(Boolean)
|
||||
const rows = []
|
||||
let currentLabel = null
|
||||
let currentValue = ''
|
||||
|
||||
lines.forEach(line => {
|
||||
// Check if line starts with <b>Label:</b>
|
||||
const match = line.match(/^<b>([^<]+?):?<\/b>\s*(.*)/)
|
||||
if (match) {
|
||||
if (currentLabel) rows.push({ label: currentLabel, value: currentValue.trim() })
|
||||
currentLabel = match[1].replace(/:$/, '').trim()
|
||||
currentValue = match[2] || ''
|
||||
} else if (currentLabel) {
|
||||
// Continuation line (like bullet points)
|
||||
currentValue += (currentValue ? '<br>' : '') + line
|
||||
} else {
|
||||
// Standalone line without label
|
||||
currentLabel = line.replace(/<[^>]+>/g, '').substring(0, 20).trim()
|
||||
currentValue = line
|
||||
}
|
||||
})
|
||||
if (currentLabel) rows.push({ label: currentLabel, value: currentValue.trim() })
|
||||
|
||||
if (rows.length > 0) {
|
||||
details.innerHTML = rows.map(r =>
|
||||
'<div class="detail-row"><span class="detail-label">' + r.label + '</span><span class="detail-value">' + (r.value || '\u2014') + '</span></div>'
|
||||
).join('')
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize proto detail rows across cards (same as normalizeDetailRows)
|
||||
function normalizeProtoRows(cards) {
|
||||
const allLabels = []
|
||||
const seen = new Set()
|
||||
cards.forEach(card => {
|
||||
const details = card.querySelector('.proto-details')
|
||||
if (!details) return
|
||||
details.querySelectorAll('.detail-row .detail-label').forEach(lbl => {
|
||||
const t = lbl.textContent.trim()
|
||||
if (!seen.has(t)) { seen.add(t); allLabels.push(t) }
|
||||
})
|
||||
})
|
||||
if (allLabels.length === 0) return
|
||||
|
||||
cards.forEach(card => {
|
||||
const details = card.querySelector('.proto-details')
|
||||
if (!details || details.dataset.normalized) return
|
||||
details.dataset.normalized = '1'
|
||||
|
||||
const existing = {}
|
||||
details.querySelectorAll('.detail-row').forEach(row => {
|
||||
const lbl = row.querySelector('.detail-label')
|
||||
if (lbl) existing[lbl.textContent.trim()] = row
|
||||
})
|
||||
|
||||
const frag = document.createDocumentFragment()
|
||||
allLabels.forEach(label => {
|
||||
if (existing[label]) {
|
||||
frag.appendChild(existing[label])
|
||||
} else {
|
||||
const row = document.createElement('div')
|
||||
row.className = 'detail-row'
|
||||
row.innerHTML = '<span class="detail-label">' + label + '</span><span class="detail-value" style="color:#3b3f50;">\u2014</span>'
|
||||
frag.appendChild(row)
|
||||
}
|
||||
})
|
||||
details.appendChild(frag)
|
||||
})
|
||||
}
|
||||
|
||||
function syncProtoHeights(cards) {
|
||||
if (cards.length < 2) return
|
||||
|
||||
function syncByClass(cls) {
|
||||
const els = cards.map(c => c.querySelector('.' + cls)).filter(Boolean)
|
||||
if (els.length < 2) return
|
||||
els.forEach(e => e.style.minHeight = '')
|
||||
let max = 0
|
||||
els.forEach(e => { max = Math.max(max, e.offsetHeight) })
|
||||
if (max > 0) els.forEach(e => e.style.minHeight = max + 'px')
|
||||
}
|
||||
|
||||
syncByClass('proto-card-name')
|
||||
syncByClass('proto-card-desc')
|
||||
syncByClass('proto-card-layman')
|
||||
|
||||
// Sync detail rows by index
|
||||
const maxRows = Math.max(...cards.map(c => c.querySelectorAll('.detail-row').length))
|
||||
for (let i = 0; i < maxRows; i++) {
|
||||
const rows = cards.map(c => {
|
||||
const all = c.querySelectorAll('.detail-row')
|
||||
return all[i] || null
|
||||
}).filter(Boolean)
|
||||
rows.forEach(r => r.style.minHeight = '')
|
||||
let maxH = 0
|
||||
rows.forEach(r => { maxH = Math.max(maxH, r.offsetHeight) })
|
||||
if (maxH > 0) rows.forEach(r => r.style.minHeight = maxH + 'px')
|
||||
}
|
||||
}
|
||||
|
||||
function find(name) {
|
||||
event.stopPropagation()
|
||||
const card = document.querySelector(`[data-name="${name}"]`)
|
||||
|
||||
Reference in New Issue
Block a user