diff --git a/docker/bitcoin-ui/Dockerfile b/docker/bitcoin-ui/Dockerfile index 97a90bce..0fa32db0 100644 --- a/docker/bitcoin-ui/Dockerfile +++ b/docker/bitcoin-ui/Dockerfile @@ -2,6 +2,7 @@ FROM git.tx1138.com/lfg2025/nginx:1.27.4-alpine # Static site content. COPY index.html /usr/share/nginx/html/ COPY tailwind.css /usr/share/nginx/html/ +COPY qrcode.js /usr/share/nginx/html/ COPY 50x.html /usr/share/nginx/html/ COPY assets/ /usr/share/nginx/html/assets/ # diff --git a/docker/bitcoin-ui/index.html b/docker/bitcoin-ui/index.html index 89dab4ef..65db83b0 100644 --- a/docker/bitcoin-ui/index.html +++ b/docker/bitcoin-ui/index.html @@ -309,6 +309,126 @@ animation: shimmer 3s infinite; } + /* ── Primary tab bar (umbrelOS's dock, Archipelago-styled) ─────── */ + .tabbar-wrap { margin-bottom: 1.5rem; } + .tabbar { + display: flex; gap: 0.25rem; padding: 0.3rem; border-radius: 0.85rem; + background: rgba(0, 0, 0, 0.55); + backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); + border: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.18); + overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; + } + .tabbar::-webkit-scrollbar { display: none; } + .tab-btn { + flex: 1 0 auto; display: flex; align-items: center; justify-content: center; gap: 0.5rem; + padding: 0.65rem 1rem; border: none; background: none; border-radius: 0.6rem; + font-size: 0.875rem; font-weight: 500; color: rgba(255, 255, 255, 0.6); + cursor: pointer; transition: all 0.25s ease; white-space: nowrap; + } + .tab-btn svg { width: 1.05rem; height: 1.05rem; flex-shrink: 0; } + .tab-btn:hover { color: rgba(255, 255, 255, 0.9); } + .tab-btn.active { + color: #fff; + background: linear-gradient(135deg, rgba(251, 146, 60, 0.28) 0%, rgba(255, 255, 255, 0.08) 100%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22); + } + .tab-panel { display: none; } + .tab-panel.active { display: block; animation: fadeUp 0.28s ease both; } + @keyframes fadeUp { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } + + /* Status pill */ + .pill { + display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.25rem 0.6rem; + border-radius: 9999px; font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.02em; + background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.12); + color: rgba(255, 255, 255, 0.75); white-space: nowrap; + } + .pill.ok { background: rgba(74, 222, 128, 0.14); border-color: rgba(74, 222, 128, 0.3); color: #4ade80; } + .pill.warn { background: rgba(250, 204, 21, 0.14); border-color: rgba(250, 204, 21, 0.3); color: #facc15; } + .pill.bad { background: rgba(248, 113, 113, 0.14); border-color: rgba(248, 113, 113, 0.3); color: #f87171; } + + /* Data tables (peers) */ + .table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; } + .data-table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; } + .data-table th { + text-align: left; padding: 0.6rem 0.75rem; font-size: 0.6875rem; font-weight: 600; + text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255, 255, 255, 0.45); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); white-space: nowrap; + cursor: pointer; user-select: none; + } + .data-table th:hover { color: rgba(255, 255, 255, 0.75); } + .data-table th .caret { opacity: 0.35; font-size: 0.6rem; margin-left: 0.25rem; } + .data-table th.sorted .caret { opacity: 1; } + .data-table td { + padding: 0.65rem 0.75rem; border-bottom: 1px solid rgba(255, 255, 255, 0.06); + color: rgba(255, 255, 255, 0.8); vertical-align: middle; + } + .data-table tr:last-child td { border-bottom: none; } + .data-table tbody tr { transition: background 0.2s ease; } + .data-table tbody tr:hover { background: rgba(255, 255, 255, 0.04); } + .filter-input { + width: 100%; padding: 0.55rem 0.85rem; background: rgba(0, 0, 0, 0.4); + border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 0.5rem; + color: white; font-size: 0.8125rem; outline: none; + } + .filter-input::placeholder { color: rgba(255, 255, 255, 0.35); } + .filter-input:focus { border-color: rgba(251, 146, 60, 0.5); } + + /* Latest blocks strip */ + .blocks-strip { display: flex; gap: 0.75rem; overflow-x: auto; padding-bottom: 0.5rem; scrollbar-width: thin; -webkit-overflow-scrolling: touch; } + .block-tile { + flex: 0 0 auto; width: 8.5rem; padding: 0.9rem; + background: linear-gradient(135deg, rgba(251, 146, 60, 0.14) 0%, rgba(255, 255, 255, 0.04) 100%); + border: 1px solid rgba(251, 146, 60, 0.25); border-radius: 0.75rem; + } + .block-tile .h { font-size: 1rem; font-weight: 700; color: #fff; font-variant-numeric: tabular-nums; } + .block-tile .m { font-size: 0.6875rem; color: rgba(255, 255, 255, 0.5); margin-top: 0.15rem; } + + /* Mini bar charts (Umbrel's Insights charts, CSP-safe / no chart lib) */ + .chart-rows { display: flex; align-items: flex-end; gap: 0.3rem; height: 7rem; } + .chart-col { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 0.3rem; min-width: 0; } + .chart-bar { width: 100%; border-radius: 0.25rem 0.25rem 0 0; min-height: 2px; transition: height 0.5s ease; } + .chart-bar.orange { background: linear-gradient(180deg, #fb923c, rgba(251, 146, 60, 0.35)); } + .chart-bar.yellow { background: linear-gradient(180deg, #facc15, rgba(250, 204, 21, 0.35)); } + .chart-bar.green { background: linear-gradient(180deg, #4ade80, rgba(74, 222, 128, 0.35)); } + .chart-lbl { font-size: 0.5625rem; color: rgba(255, 255, 255, 0.35); font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; } + + /* Field rows with copy (connect tab) */ + .field-label { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255, 255, 255, 0.45); margin-bottom: 0.25rem; } + .field-row { display: flex; align-items: center; background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 0.5rem; overflow: hidden; min-width: 0; } + .field-value { flex: 1; min-width: 0; padding: 0.625rem 0.875rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.8125rem; color: rgba(255, 255, 255, 0.9); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .copy-btn { padding: 0.625rem 0.75rem; background: none; border: none; border-left: 1px solid rgba(255, 255, 255, 0.1); cursor: pointer; color: rgba(255, 255, 255, 0.4); transition: all 0.2s ease; display: flex; align-items: center; flex-shrink: 0; } + .copy-btn:hover { color: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.05); } + .conn-layout { display: flex; flex-direction: column; gap: 1.5rem; } + .conn-fields { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.75rem; } + .qr-box { flex-shrink: 0; width: 100%; max-width: 216px; aspect-ratio: 1; margin: 0 auto; background: white; border-radius: 0.75rem; padding: 0.6rem; display: flex; align-items: center; justify-content: center; } + .qr-box img { width: 100%; height: auto; display: block; image-rendering: pixelated; } + .conn-select { width: 100%; padding: 0.75rem 1rem; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 0.5rem; color: white; font-size: 0.875rem; font-weight: 500; appearance: none; cursor: pointer; outline: none; background-image: url('data:image/svg+xml;utf8,'); background-repeat: no-repeat; background-position: right 12px center; background-size: 20px; } + .conn-select option { background: #1a1a2e; color: white; } + + .empty-state { padding: 2.5rem 1rem; text-align: center; color: rgba(255, 255, 255, 0.4); font-size: 0.875rem; } + .tabular { font-variant-numeric: tabular-nums; } + + @media (min-width: 640px) { + .conn-layout { flex-direction: row; } + .qr-box { margin: 0; width: 216px; } + } + + /* Mobile: the tab bar becomes a bottom dock, like umbrelOS. */ + @media (max-width: 767px) { + .container { padding: 1rem; padding-bottom: 6.5rem; } + .tabbar-wrap { + position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; + margin: 0; padding: 0.5rem 0.75rem calc(0.5rem + env(safe-area-inset-bottom, 0px)); + background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 55%, rgba(0, 0, 0, 0)); + } + .tabbar { border-radius: 1rem; } + .tab-btn { flex: 1 1 0; flex-direction: column; gap: 0.2rem; padding: 0.5rem 0.35rem; font-size: 0.625rem; } + .tab-btn svg { width: 1.25rem; height: 1.25rem; } + .data-table th, .data-table td { padding: 0.55rem 0.5rem; } + } + /* Number increment animation */ @keyframes numberPulse { 0%, 100% { @@ -409,6 +529,35 @@ + +
+ + +Connections
+-
+peers
+Mempool
+-
+unconfirmed
+Blockchain Size
+-
+on disk
+Node Uptime
+-
+since last restart
+Most recent blocks this node has validated.
+MB per block
+ +average sat/vB
+ +subsidy + fees, BTC
+ +Nodes this node is exchanging blocks and transactions with.
+| Peer▼ | +Network▼ | +Relay TXNs▼ | +In/Out▼ | +Connected▼ | +Ping▼ | +
|---|---|---|---|---|---|
Loading peers… | |||||
Point a wallet or app at this node's JSON-RPC interface.
+ ++ The RPC password is a manifest-declared generated secret. It is injected into this app's + nginx upstream by the orchestrator and is deliberately never sent to the browser. +
+Local Network mode sends RPC traffic unencrypted. Only use it on a network you trust.
+Let another Bitcoin node peer directly with this one.
++ For an Electrum wallet, install the electrs app, wait for it to + finish indexing, and use the connection details it publishes. +
+