feat(ui): mobile mesh tabs, AIUI-style audio player, cloud grid + map fixes

UI (this session):
- Global audio player now scales the whole interface into the space above it
  on desktop (sidebar + main) and docks directly above the tab bar on mobile;
  it stays visible while navigating.
- Mesh mobile redesign: floating Chat / BTC / Dead Man / AI / Map tab strip
  with a single fixed, internally-scrolling pane (page no longer scrolls);
  tabs hide while a conversation is open; floating back button; collapsible
  Device panel (starts collapsed); keyboard-aware conversation sizing via
  VisualViewport so the chat sits just above the keyboard.
- Cloud file grid: uniform 4/3 card heights (folders + images match).
- Swipe left/right switches tabs on the Apps and Web5 screens.
- Map tool fills its pane (no bottom gap); fix skewed Share Location toggle
  on mobile (global min-height rule was deforming the switch).
- Trim redundant helper copy from the mesh AI tab.

Also bundles pre-existing in-progress work that was already in the tree:
mesh listener/session + wallet + container + bitcoin-status backend changes,
docker UI updates, and assorted other UI tweaks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-19 09:52:26 -04:00
co-authored by Claude Opus 4.8
parent c4855526fe
commit 1bce694ebb
37 changed files with 1260 additions and 208 deletions
+11 -2
View File
@@ -737,6 +737,15 @@
return response.json();
}
// Snapshot age in ms. Prefer the server-computed age_ms (single clock, no
// skew). Fall back to the old browser-vs-server subtraction only for an
// older backend that doesn't send age_ms. Mixing clocks was why the
// "reconnecting…" banner could stick on nodes whose clock drifted.
function snapshotAgeMs(status) {
if (typeof status.age_ms === 'number') return status.age_ms;
return status.updated_at_ms ? Date.now() - status.updated_at_ms : Number.POSITIVE_INFINITY;
}
function cookieValue(name) {
return document.cookie
.split('; ')
@@ -1127,7 +1136,7 @@
const rpcEl = document.getElementById('settingsRpc');
if (rpcEl) {
const port = chain === 'main' ? 8332 : (chain === 'test' ? 18332 : (chain === 'signet' ? 38332 : 18443));
const statusAgeMs = status.updated_at_ms ? Date.now() - status.updated_at_ms : Number.POSITIVE_INFINITY;
const statusAgeMs = snapshotAgeMs(status);
const displayStale = status.stale === true && statusAgeMs > 30000;
rpcEl.textContent = displayStale
? `Reconnecting on port ${port}`
@@ -1143,7 +1152,7 @@
const diskSize = formatBytes(blockchainInfo.size_on_disk || 0);
const appearsToBeReindexing = initialBlockDownload && blocks === 0 && headers > 0 && (blockchainInfo.size_on_disk || 0) > 1024 * 1024 * 1024;
const previousBlockCount = lastBlockCount;
const statusAgeMs = status.updated_at_ms ? Date.now() - status.updated_at_ms : Number.POSITIVE_INFINITY;
const statusAgeMs = snapshotAgeMs(status);
const snapshotAdvanced = previousBlockCount > 0 && blocks > previousBlockCount;
const displayStale = status.stale === true && !snapshotAdvanced && statusAgeMs > 30000;
+1 -1
View File
@@ -22,6 +22,6 @@ RUN sed -i 's/^user nginx;/user root;/' /etc/nginx/nginx.conf && \
mkdir -p /var/cache/nginx/client_temp /var/cache/nginx/proxy_temp \
/var/cache/nginx/fastcgi_temp /var/cache/nginx/uwsgi_temp \
/var/cache/nginx/scgi_temp
EXPOSE 80
EXPOSE 18083
ENTRYPOINT []
CMD ["nginx", "-g", "daemon off;"]
+30 -32
View File
@@ -433,8 +433,12 @@
const host = window.location.hostname;
function getBackendUrl() {
// Same-origin by default: the app's own nginx (:18083) proxies these
// paths to the archipelago backend, so a relative base ('') avoids
// any cross-origin/CORS issues (which broke this on http-only nodes).
// ?backend=http://HOST:5678 still overrides for local dev.
const params = new URLSearchParams(window.location.search);
return params.get('backend') || (window.location.protocol + '//' + window.location.hostname);
return params.get('backend') || '';
}
function setSettingsTab(tabId) {
@@ -499,42 +503,36 @@
async function loadLogs() {
const logsContent = document.getElementById('logsContent');
const backendUrl = getBackendUrl();
if (backendUrl) {
logsContent.textContent = 'Loading logs...';
try {
const res = await fetch(backendUrl + '/api/container/logs?app_id=lnd&lines=200');
if (!res.ok) throw new Error(res.statusText);
const json = await res.json();
const lines = json.result || json.logs || (Array.isArray(json) ? json : []);
logsContent.textContent = Array.isArray(lines) ? lines.join('\n') : String(lines);
} catch (e) {
logsContent.textContent = 'Could not load logs: ' + e.message;
}
} else {
logsContent.textContent = 'Open this app with ?backend=http://HOST:5678 to load logs from the server.';
logsContent.textContent = 'Loading logs...';
try {
const res = await fetch(backendUrl + '/api/container/logs?app_id=lnd&lines=200', { credentials: 'include' });
if (!res.ok) throw new Error(res.statusText);
const json = await res.json();
const lines = json.result || json.logs || (Array.isArray(json) ? json : []);
logsContent.textContent = Array.isArray(lines) ? lines.join('\n') : String(lines);
} catch (e) {
logsContent.textContent = 'Could not load logs: ' + e.message;
}
}
async function fetchLiveData() {
const backendUrl = getBackendUrl();
const data = { channelCount: 0, restReachable: false, grpcReachable: false };
if (backendUrl) {
try {
const getinfoRes = await fetch(backendUrl + '/proxy/lnd/v1/getinfo');
if (getinfoRes.ok) {
data.getinfo = await getinfoRes.json();
data.restReachable = true;
}
} catch (_) {}
try {
const chRes = await fetch(backendUrl + '/proxy/lnd/v1/channels');
if (chRes.ok) {
const ch = await chRes.json();
data.channelCount = (ch.channels && ch.channels.length) || 0;
}
} catch (_) {}
data.grpcReachable = data.restReachable;
}
try {
const getinfoRes = await fetch(backendUrl + '/proxy/lnd/v1/getinfo', { credentials: 'include' });
if (getinfoRes.ok) {
data.getinfo = await getinfoRes.json();
data.restReachable = true;
}
} catch (_) {}
try {
const chRes = await fetch(backendUrl + '/proxy/lnd/v1/channels', { credentials: 'include' });
if (chRes.ok) {
const ch = await chRes.json();
data.channelCount = (ch.channels && ch.channels.length) || 0;
}
} catch (_) {}
data.grpcReachable = data.restReachable;
applyLiveData(data);
}
@@ -629,7 +627,7 @@
async function fetchConnectInfo() {
try {
const resp = await fetch(window.location.protocol + '//' + window.location.hostname + '/lnd-connect-info', { credentials: 'include' });
const resp = await fetch(getBackendUrl() + '/lnd-connect-info', { credentials: 'include' });
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const data = await resp.json();
if (data.cert_base64url) {
+53 -3
View File
@@ -1,13 +1,63 @@
server {
listen 80;
# Host-networked: listen on the app's own port directly (NOT 80, which the
# host's main nginx already owns). The app is reached at http(s)://<node>:18083.
listen 18083;
server_name _;
root /usr/share/nginx/html;
index index.html;
# lnd-connect-info is fetched via absolute URL path,
# handled by the host nginx → backend at :5678 directly
# Proxy the archipelago backend same-origin so the browser never makes a
# cross-origin request (no CORS, no host-nginx route dependency). The app is
# served on this node's :18083; cookies are scoped by host (not port), so the
# browser already carries the `session` (HttpOnly) and `csrf_token` cookies
# set by the main UI. We forward both, plus the X-CSRF-Token header, to the
# backend on 127.0.0.1:5678 (reachable because this container is host-networked).
#
# This mirrors fips-ui / electrs-ui. The old bridge + 18083→80 mapping forced
# cross-origin fetches that broke on http-only nodes (blank fields, QR
# "failed to fetch").
location = /lnd-connect-info {
proxy_pass http://127.0.0.1:5678/lnd-connect-info;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Cookie $http_cookie;
proxy_set_header X-CSRF-Token $http_x_csrf_token;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 10s;
proxy_read_timeout 60s;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
location /proxy/lnd/ {
proxy_pass http://127.0.0.1:5678/proxy/lnd/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Cookie $http_cookie;
proxy_set_header X-CSRF-Token $http_x_csrf_token;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 10s;
proxy_read_timeout 60s;
add_header Cache-Control "no-store";
}
location /api/container/logs {
proxy_pass http://127.0.0.1:5678/api/container/logs;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Cookie $http_cookie;
proxy_set_header X-CSRF-Token $http_x_csrf_token;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 10s;
proxy_read_timeout 30s;
add_header Cache-Control "no-store";
}
location / {
add_header Cache-Control "no-cache";
try_files $uri $uri/ /index.html;
}
}