Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
135fb5650b | ||
|
|
1de4a0943e | ||
|
|
b5e33784e6 | ||
|
|
2b7e92e770 | ||
|
|
7c34df36cd | ||
|
|
4b4e1ab1a3 | ||
|
|
d05fa6988d | ||
|
|
2399eeac66 | ||
|
|
63cb9dd22c | ||
|
|
246916c77b | ||
|
|
00416c3c24 | ||
|
|
1dc6d3e0e3 | ||
|
|
816a06747a | ||
|
|
9243babcdb | ||
|
|
6672d978f7 | ||
|
|
a80712963c | ||
|
|
76e6f06995 | ||
|
|
b7ba35477c | ||
|
|
e87e8017bf | ||
|
|
6168f6a7d0 | ||
|
|
fdd26ba1e3 | ||
|
|
a7a6528b08 | ||
|
|
e751b7c6f9 | ||
|
|
a500a75235 | ||
|
|
71a57c3ec1 | ||
|
|
b67e1527a2 |
+23
-1
@@ -1,7 +1,29 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Unreleased
|
## v1.8.3-alpha (2026-08-14)
|
||||||
|
|
||||||
|
- **The network map on TVs: no more blank page, no more frozen page — and it moves again.** The map's entrance animation needed a smoothness that TV kiosk hardware can't always deliver, so the page could sit blank until a refresh; the previous fix cured the freeze by stopping the animation entirely, which went too far. Now the map appears instantly with everything already in place, then resumes its calm orbital motion at a gentler pace suited to TVs. Resizing or rotating any screen also redraws the map properly instead of leaving it tiny, stretched, or empty.
|
||||||
|
- **The dashboard's corner logo is back to normal.** The new glossy paint finish was meant for the big emblem on the screensaver, intro, and login screens — it had quietly spread to the small logo in the dashboard header, where it looked wrong. Each screen now gets exactly the treatment intended for it.
|
||||||
|
- **App icons no longer vanish in My Apps.** The freshly restyled Alby Hub and phoenixd icons could render as blank squares in some views — a subtlety in how the icon files declared their size. Fixed at the source, and the icon tool app developers use now produces immune files.
|
||||||
|
|
||||||
|
## v1.8.2-alpha (2026-08-13)
|
||||||
|
|
||||||
|
- **An app that can't be shown inside the dashboard now becomes a tab app by itself.** A few apps refuse to render inside another page no matter what — they break out with their own code or insist on owning the whole browser window. Opening one used to mean staring at a grey pane. Now the dashboard notices, offers the app in its own tab, and remembers: from then on that app's button opens a tab directly (with the little launch icon that tab apps carry), first click, every time. If a later update makes the app embeddable after all, the dashboard notices that too and goes back to embedding it.
|
||||||
|
- **The logo emblem got its glossy black paint finish — properly this time.** The circle behind the A on the screensaver, intro, and login now wears a deep wet-paint look: warm light blooming from the top edge, fine grain so the dark tones stay smooth instead of banding, and no more ring border. (An earlier rougher version of this experiment briefly shipped by accident and then vanished depending on which screen you were on — this is the finished, deliberate one, everywhere.)
|
||||||
|
- **New app icons now match the store's look, on every screen.** Alby Hub and phoenixd arrived with edge-to-edge logos that ignored the breathing room every other app icon has, and the app detail page skipped the icon backdrop entirely. Both icons are re-set on the standard canvas, the detail page now applies the same icon treatment as the store tiles, and app developers get a one-command tool that puts any logo onto the house canvas automatically.
|
||||||
|
|
||||||
|
## v1.8.1-alpha (2026-08-13)
|
||||||
|
|
||||||
|
- **Apps that refused to open inside the dashboard now embed like everything else.** Some apps ship browser headers that forbid being shown inside another page — correct hardening on the open web, but inside Archipelago it produced a dead grey pane when you opened them from My Apps (Alby Hub was the first to hit it). The app gate, which already checks your login on every request to an app, now removes just those framing headers on the way through; each app's own content-security rules pass through untouched. No more per-app proxy workarounds.
|
||||||
|
- **The network map no longer freezes kiosk TVs.** The animated federation map at 4K was too much for the deliberately conservative graphics settings the on-screen display used on every machine — settings chosen years back to stop audio crackle on much older hardware. Two fixes: on kiosk screens the map now opens in its flat 2D view (the 3D globe is one tap away, and remembered) and animates at half rate — invisible from the couch, half the work. And the display itself now recognizes what machine it runs on: older kiosk boxes keep the proven careful settings, modern ones finally get real GPU rendering.
|
||||||
|
- **New Settings → Display → Graphics choice for the on-screen display.** Auto (recommended) picks the right rendering mode for the machine by itself; Compatibility forces the most conservative mode if a screen ever stutters, tears, or crackles; Quality forces full GPU rendering on hardware the automatic detection doesn't recognize. Changing it restarts the on-screen display, like the size presets.
|
||||||
|
|
||||||
|
## v1.8.0-alpha (2026-08-12)
|
||||||
|
|
||||||
|
- **Archipelago is now open source.** The full source code of the node you are running — the orchestrator, the dashboard, the app platform, the mesh, the release tooling — is published for anyone to read, build and audit at source.archipelago-foundation.org/lfg2025/archy. A node that holds your money, your files and your communications should not ask to be taken on faith: from this release onward you, or anyone you trust, can see exactly what it does and follow every change we make in the open.
|
||||||
|
- **Installing an update is reliable again, and tells you what happened when it isn't.** Some nodes could download an update but never apply it — the button stayed on "Install", and no amount of retrying worked. The cause: applying the update consumed the downloaded files as it went, so if any one step hit a snag partway through, the leftover files were incomplete and every later attempt failed the safety re-check forever, needing a technician to recover. Applying no longer consumes the download — a failed apply can always be retried from the same files — and the pieces are now applied in a fixed order with the program itself last, so a hiccup can't leave a half-swapped node. When an apply does fail, the screen now shows the real reason and what to do ("download the update again"), and offers Download again instead of a dead "Install" button, rather than a generic "it failed".
|
||||||
|
- **Video on the kiosk stops tearing.** The kiosk's display had no vertical sync at all, so fast motion — IndeedHub films especially — showed horizontal tearing lines. The display driver now syncs every frame to the panel (no extra hardware needed, existing kiosks pick it up with this update), and on machines with a GPU, video decoding moves off the CPU onto the video hardware — smoother playback that also leaves more headroom for audio, not less.
|
||||||
|
- **The Back button finally does what you expect.** Pressing Back — the mouse's side button on a kiosk, a swipe on a phone, the toolbar button in any browser — used to navigate the screen underneath an open window, or leave the dashboard entirely. Back now closes the topmost open window first, one per press, exactly like a native app; closing a window yourself never leaves a phantom entry that makes you press Back twice.
|
||||||
- **No more bare IP addresses in your update or app-registry settings.** The update mirrors and the app registry each listed the same server twice — once by its proper name, once as a raw `http://146…` address left over from before the domain existed. The raw-address entries are retired: new nodes never see them, and existing nodes clean them out of their saved lists automatically on the next read. Everything now goes through the named, TLS-protected origin — which was always the same machine.
|
- **No more bare IP addresses in your update or app-registry settings.** The update mirrors and the app registry each listed the same server twice — once by its proper name, once as a raw `http://146…` address left over from before the domain existed. The raw-address entries are retired: new nodes never see them, and existing nodes clean them out of their saved lists automatically on the next read. Everything now goes through the named, TLS-protected origin — which was always the same machine.
|
||||||
- **The phone companion app downloads over the proper domain.** The download QR pointed at a raw address over plain HTTP; it now points at the same file on the https domain. Scanning it gets you an encrypted download from a named server.
|
- **The phone companion app downloads over the proper domain.** The download QR pointed at a raw address over plain HTTP; it now points at the same file on the https domain. Scanning it gets you an encrypted download from a named server.
|
||||||
- **The Receive window now tells you when the money is on its way.** Previously it showed a QR code and left you to check elsewhere whether anything happened. Now, the moment the sender's transaction is broadcast, the QR gives way to a clock: the amount, the transaction ID (tap to copy), and a note that the funds arrive on their own — with a single Done button. If you keep the window open, the clock becomes a green check at the first confirmation. Verified live on a real node: payment detected within seconds of broadcast.
|
- **The Receive window now tells you when the money is on its way.** Previously it showed a QR code and left you to check elsewhere whether anything happened. Now, the moment the sender's transaction is broadcast, the QR gives way to a clock: the amount, the transaction ID (tap to copy), and a note that the funds arrive on their own — with a single Done button. If you keep the window open, the clock becomes a green check at the first confirmation. Verified live on a real node: payment detected within seconds of broadcast.
|
||||||
|
|||||||
@@ -374,7 +374,7 @@
|
|||||||
"id": "pine",
|
"id": "pine",
|
||||||
"title": "Pine",
|
"title": "Pine",
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"description": "A private voice assistant for your home. Pine runs speech-to-text (Whisper), text-to-speech (Piper) and wake-word detection (openWakeWord) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. Ask it about your node — block height, sync, peers, Lightning balance — and, when a Claude API key is set, anything else.",
|
"description": "A private voice assistant for your home. Pine runs speech-to-text (Whisper), text-to-speech (Piper) and wake-word detection (openWakeWord) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. Ask it about your node \u2014 block height, sync, peers, Lightning balance \u2014 and, when a Claude API key is set, anything else.",
|
||||||
"icon": "/assets/img/app-icons/pine.svg",
|
"icon": "/assets/img/app-icons/pine.svg",
|
||||||
"author": "Archipelago",
|
"author": "Archipelago",
|
||||||
"category": "home",
|
"category": "home",
|
||||||
@@ -459,7 +459,7 @@
|
|||||||
"id": "netbird",
|
"id": "netbird",
|
||||||
"title": "NetBird",
|
"title": "NetBird",
|
||||||
"version": "2.38.0",
|
"version": "2.38.0",
|
||||||
"description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN. The user-facing entry point — a TLS proxy in front of the dashboard + server.",
|
"description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN. The user-facing entry point \u2014 a TLS proxy in front of the dashboard + server.",
|
||||||
"icon": "/assets/img/app-icons/netbird.svg",
|
"icon": "/assets/img/app-icons/netbird.svg",
|
||||||
"author": "NetBird",
|
"author": "NetBird",
|
||||||
"category": "networking",
|
"category": "networking",
|
||||||
@@ -547,6 +547,30 @@
|
|||||||
"/var/lib/archipelago/nextcloud:/var/www/html"
|
"/var/lib/archipelago/nextcloud:/var/www/html"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "alby-hub",
|
||||||
|
"title": "Alby Hub",
|
||||||
|
"version": "1.23.0",
|
||||||
|
"description": "Self-custodial Lightning wallet hub. Runs its own Lightning node on your Archipelago and connects your apps to it over Nostr Wallet Connect \u2014 one hub, every app pays through it.",
|
||||||
|
"icon": "/assets/img/app-icons/alby-hub.svg",
|
||||||
|
"author": "Alby",
|
||||||
|
"category": "money",
|
||||||
|
"tier": "optional",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/alby-hub:v1.23.0",
|
||||||
|
"repoUrl": "https://github.com/getAlby/hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "phoenixd",
|
||||||
|
"title": "phoenixd",
|
||||||
|
"version": "0.9.0",
|
||||||
|
"description": "Headless Lightning daemon by ACINQ (the Phoenix wallet team). No screen of its own \u2014 it exposes a small local API that other apps and tools use to send and receive Lightning payments. Channel liquidity is managed automatically for a fee.",
|
||||||
|
"icon": "/assets/img/app-icons/phoenixd.svg",
|
||||||
|
"author": "ACINQ",
|
||||||
|
"category": "money",
|
||||||
|
"tier": "optional",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/phoenixd:0.9.0",
|
||||||
|
"repoUrl": "https://github.com/ACINQ/phoenixd"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
app:
|
||||||
|
id: alby-hub
|
||||||
|
name: Alby Hub
|
||||||
|
version: 1.23.0
|
||||||
|
description: Self-custodial Lightning wallet hub. Runs its own Lightning node on your Archipelago and connects your apps to it over Nostr Wallet Connect — one hub, every app pays through it.
|
||||||
|
category: money
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: source.archipelago-foundation.org/lfg2025/alby-hub:v1.23.0
|
||||||
|
pull_policy: if-not-present
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- storage: 1Gi
|
||||||
|
|
||||||
|
resources:
|
||||||
|
cpu_limit: 1
|
||||||
|
memory_limit: 512Mi
|
||||||
|
disk_limit: 2Gi
|
||||||
|
|
||||||
|
security:
|
||||||
|
capabilities: []
|
||||||
|
readonly_root: true
|
||||||
|
no_new_privileges: true
|
||||||
|
network_policy: bridge
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- host: 8187
|
||||||
|
container: 8080
|
||||||
|
protocol: tcp
|
||||||
|
bind: 127.0.0.1
|
||||||
|
auth: gated
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /var/lib/archipelago/alby-hub
|
||||||
|
target: /data
|
||||||
|
options: [rw]
|
||||||
|
|
||||||
|
environment:
|
||||||
|
- WORK_DIR=/data
|
||||||
|
- PORT=8080
|
||||||
|
# LDK peers are dialed outbound-only in v1; no inbound p2p port is
|
||||||
|
# advertised, so no extra port mapping is needed for payments to work.
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
|
||||||
|
health_check:
|
||||||
|
type: http
|
||||||
|
endpoint: http://localhost:8080
|
||||||
|
path: /
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
interfaces:
|
||||||
|
main:
|
||||||
|
name: Web UI
|
||||||
|
description: Alby Hub wallet interface
|
||||||
|
type: ui
|
||||||
|
port: 8187
|
||||||
|
protocol: http
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
icon: /assets/img/app-icons/alby-hub.svg
|
||||||
|
repo: https://github.com/getAlby/hub
|
||||||
|
tier: optional
|
||||||
|
launch:
|
||||||
|
# Embedded: the gate neutralizes Alby Hub's X-Frame-Options: DENY on
|
||||||
|
# proxied responses. Nodes older than the gate fix show a blocked
|
||||||
|
# frame — flip to true only if targeting such nodes.
|
||||||
|
open_in_new_tab: false
|
||||||
|
features:
|
||||||
|
- Self-custodial Lightning node (LDK) with a friendly wallet UI
|
||||||
|
- Connect wallets and apps via Nostr Wallet Connect (NWC)
|
||||||
|
- Per-app budgets and isolated sub-wallets
|
||||||
|
- Works with the Alby browser extension and mobile app
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
app:
|
||||||
|
id: phoenixd
|
||||||
|
name: phoenixd
|
||||||
|
version: 0.9.0
|
||||||
|
description: Headless Lightning daemon by ACINQ (the Phoenix wallet team). No screen of its own — it exposes a small local API that other apps and tools use to send and receive Lightning payments. Channel liquidity is managed automatically for a fee.
|
||||||
|
category: money
|
||||||
|
|
||||||
|
container:
|
||||||
|
# Image entrypoint already runs with --agree-to-terms-of-service and
|
||||||
|
# --http-bind-ip 0.0.0.0, as user "phoenix"; no custom args needed.
|
||||||
|
image: source.archipelago-foundation.org/lfg2025/phoenixd:0.9.0
|
||||||
|
pull_policy: if-not-present
|
||||||
|
# The image runs as user phoenix (1000:1000); the datadir bind source
|
||||||
|
# must be chowned to that identity or phoenixd dies on
|
||||||
|
# "Failed to open /data/phoenix.conf with Permission denied".
|
||||||
|
data_uid: "1000:1000"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- storage: 500Mi
|
||||||
|
|
||||||
|
resources:
|
||||||
|
cpu_limit: 1
|
||||||
|
memory_limit: 512Mi
|
||||||
|
disk_limit: 1Gi
|
||||||
|
|
||||||
|
security:
|
||||||
|
capabilities: []
|
||||||
|
readonly_root: true
|
||||||
|
no_new_privileges: true
|
||||||
|
network_policy: bridge
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- host: 9740
|
||||||
|
container: 9740
|
||||||
|
protocol: tcp
|
||||||
|
bind: 127.0.0.1
|
||||||
|
auth: none
|
||||||
|
auth_rationale: >-
|
||||||
|
Loopback-only JSON API, not a web page. Every request is
|
||||||
|
authenticated by the http password phoenixd generates in its own
|
||||||
|
data directory on first run; the app gate's browser login page
|
||||||
|
would break the API clients this port exists for.
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# The wallet seed (seed.dat) and phoenix.conf live here. This directory
|
||||||
|
# must survive reinstall/migration like any other app data dir —
|
||||||
|
# losing it means losing funds.
|
||||||
|
# Target is /data (via PHOENIX_DATADIR below), NOT the image's default
|
||||||
|
# /phoenix/.phoenix: the orchestrator treats any bind path containing a
|
||||||
|
# dot as a file mount and skips creating its source directory, so a
|
||||||
|
# hidden-dir target never gets its host dir and the unit crash-loops.
|
||||||
|
- type: bind
|
||||||
|
source: /var/lib/archipelago/phoenixd
|
||||||
|
target: /data
|
||||||
|
options: [rw]
|
||||||
|
|
||||||
|
environment:
|
||||||
|
- PHOENIX_DATADIR=/data
|
||||||
|
|
||||||
|
health_check:
|
||||||
|
type: tcp
|
||||||
|
endpoint: localhost:9740
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
icon: /assets/img/app-icons/phoenixd.svg
|
||||||
|
repo: https://github.com/ACINQ/phoenixd
|
||||||
|
tier: optional
|
||||||
|
features:
|
||||||
|
- Ultra-light Lightning node — no bitcoin node required
|
||||||
|
- Automated channel and liquidity management (fees apply)
|
||||||
|
- Simple HTTP API + websockets for payments
|
||||||
|
- Backed by the team behind the Phoenix mobile wallet
|
||||||
Generated
+1
-1
@@ -104,7 +104,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.129-alpha"
|
version = "1.8.3-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.129-alpha"
|
version = "1.8.3-alpha"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ pub(super) fn sanitize_error_message(msg: &str) -> String {
|
|||||||
"must be",
|
"must be",
|
||||||
"cannot",
|
"cannot",
|
||||||
"Password",
|
"Password",
|
||||||
|
// OTA apply/download errors are all operator-actionable ("download it
|
||||||
|
// again", "download first") — sanitizing them to "Operation failed"
|
||||||
|
// left users stuck with no idea what to do, and hid the "already
|
||||||
|
// running" text the update UI matches on to join an in-flight apply
|
||||||
|
// instead of showing a false failure. Every such message starts "Update".
|
||||||
|
"Update",
|
||||||
// The federation escalation sentinel. "Password" above does NOT cover
|
// The federation escalation sentinel. "Password" above does NOT cover
|
||||||
// it — starts_with is case-sensitive and the sentinel is ALL-CAPS —
|
// it — starts_with is case-sensitive and the sentinel is ALL-CAPS —
|
||||||
// so the frontend's isPasswordRequired() never saw it and the
|
// so the frontend's isPasswordRequired() never saw it and the
|
||||||
|
|||||||
@@ -1268,7 +1268,14 @@ impl RpcHandler {
|
|||||||
} else {
|
} else {
|
||||||
"auto"
|
"auto"
|
||||||
};
|
};
|
||||||
Ok(serde_json::json!({ "has_kiosk": has_kiosk, "preset": preset }))
|
let graphics = if conf.contains("KIOSK_GRAPHICS=performance") {
|
||||||
|
"performance"
|
||||||
|
} else if conf.contains("KIOSK_GRAPHICS=quality") {
|
||||||
|
"quality"
|
||||||
|
} else {
|
||||||
|
"auto"
|
||||||
|
};
|
||||||
|
Ok(serde_json::json!({ "has_kiosk": has_kiosk, "preset": preset, "graphics": graphics }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// system.kiosk-display.set — Write the kiosk display preset and restart
|
/// system.kiosk-display.set — Write the kiosk display preset and restart
|
||||||
@@ -1279,24 +1286,56 @@ impl RpcHandler {
|
|||||||
params: Option<serde_json::Value>,
|
params: Option<serde_json::Value>,
|
||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||||
let preset = params
|
let preset = params.get("preset").and_then(|v| v.as_str());
|
||||||
.get("preset")
|
let graphics = params.get("graphics").and_then(|v| v.as_str());
|
||||||
.and_then(|v| v.as_str())
|
if preset.is_none() && graphics.is_none() {
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing preset"))?;
|
anyhow::bail!("Missing preset or graphics");
|
||||||
|
}
|
||||||
|
|
||||||
let conf = match preset {
|
// The conf carries two independent settings (display scale preset +
|
||||||
|
// graphics tier). A set of one must not clobber the other, so the
|
||||||
|
// half not being changed is carried over from the file as-is.
|
||||||
|
let existing = tokio::fs::read_to_string(KIOSK_DISPLAY_CONF)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let display_part = match preset {
|
||||||
// Resolution-derived default: 4K -> 2.0 (1920-wide layout),
|
// Resolution-derived default: 4K -> 2.0 (1920-wide layout),
|
||||||
// 1080p TV -> 1.5, laptop panels -> 1.0.
|
// 1080p TV -> 1.5, laptop panels -> 1.0.
|
||||||
"auto" => String::new(),
|
Some("auto") => String::new(),
|
||||||
// Biggest UI: every panel targets a 1280-wide layout.
|
// Biggest UI: every panel targets a 1280-wide layout.
|
||||||
"large" => "ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH=1280\n".to_string(),
|
Some("large") => "ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH=1280\n".to_string(),
|
||||||
// Full-HD layout on any panel that can carry it.
|
// Full-HD layout on any panel that can carry it.
|
||||||
"balanced" => "ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH=1920\n".to_string(),
|
Some("balanced") => "ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH=1920\n".to_string(),
|
||||||
// No scaling: native CSS viewport, most content, smallest UI.
|
// No scaling: native CSS viewport, most content, smallest UI.
|
||||||
"native" => "ARCHIPELAGO_KIOSK_SCALE=1\n".to_string(),
|
Some("native") => "ARCHIPELAGO_KIOSK_SCALE=1\n".to_string(),
|
||||||
other => anyhow::bail!("Unknown display preset: {other}"),
|
Some(other) => anyhow::bail!("Unknown display preset: {other}"),
|
||||||
|
None => existing
|
||||||
|
.lines()
|
||||||
|
.filter(|l| l.starts_with("ARCHIPELAGO_KIOSK_"))
|
||||||
|
.map(|l| format!("{l}\n"))
|
||||||
|
.collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let graphics_part = match graphics {
|
||||||
|
// Auto: the launcher classifies the hardware itself (CPU/iGPU
|
||||||
|
// generation) — legacy boxes keep the choppy-audio-safe flags,
|
||||||
|
// modern iGPUs get GPU rasterization.
|
||||||
|
Some("auto") => String::new(),
|
||||||
|
// Force the conservative legacy flag set (troubleshooting).
|
||||||
|
Some("performance") => "KIOSK_GRAPHICS=performance\n".to_string(),
|
||||||
|
// Force the modern flag set even on unclassified hardware.
|
||||||
|
Some("quality") => "KIOSK_GRAPHICS=quality\n".to_string(),
|
||||||
|
Some(other) => anyhow::bail!("Unknown graphics mode: {other}"),
|
||||||
|
None => existing
|
||||||
|
.lines()
|
||||||
|
.find(|l| l.starts_with("KIOSK_GRAPHICS="))
|
||||||
|
.map(|l| format!("{l}\n"))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let conf = format!("{display_part}{graphics_part}");
|
||||||
|
|
||||||
host_sudo(&["/usr/bin/mkdir", "-p", "/etc/archipelago"]).await?;
|
host_sudo(&["/usr/bin/mkdir", "-p", "/etc/archipelago"]).await?;
|
||||||
if conf.is_empty() {
|
if conf.is_empty() {
|
||||||
let _ = host_sudo(&["/usr/bin/rm", "-f", KIOSK_DISPLAY_CONF]).await;
|
let _ = host_sudo(&["/usr/bin/rm", "-f", KIOSK_DISPLAY_CONF]).await;
|
||||||
@@ -1332,8 +1371,8 @@ impl RpcHandler {
|
|||||||
])
|
])
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
info!(preset, "Kiosk display preset applied");
|
info!(?preset, ?graphics, "Kiosk display settings applied");
|
||||||
Ok(serde_json::json!({ "preset": preset, "applied": true }))
|
Ok(serde_json::json!({ "preset": preset, "graphics": graphics, "applied": true }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -520,11 +520,63 @@ async fn proxy_to_app(
|
|||||||
|
|
||||||
let client = hyper::Client::new();
|
let client = hyper::Client::new();
|
||||||
match client.request(Request::from_parts(parts, body)).await {
|
match client.request(Request::from_parts(parts, body)).await {
|
||||||
Ok(resp) => resp,
|
Ok(mut resp) => {
|
||||||
|
neutralize_frame_blocking(resp.headers_mut());
|
||||||
|
resp
|
||||||
|
}
|
||||||
Err(_) => app_down_page(app),
|
Err(_) => app_down_page(app),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Make gate-proxied app responses embeddable by the dashboard's My Apps
|
||||||
|
/// iframe. Apps that were never designed for framing ship
|
||||||
|
/// `X-Frame-Options: DENY` (Alby Hub) or a CSP `frame-ancestors` directive,
|
||||||
|
/// and either one makes the embedded app session a dead grey pane — the
|
||||||
|
/// historical workaround was a bespoke per-app nginx proxy (gitea), which is
|
||||||
|
/// exactly the per-app patching the manifest platform exists to delete.
|
||||||
|
///
|
||||||
|
/// Framing protection exists to stop a FOREIGN origin from framing an authed
|
||||||
|
/// page and clickjacking it. Behind the gate that threat model is already
|
||||||
|
/// handled the way the gate's own pages handle it: every proxied request is
|
||||||
|
/// authenticated by the gate first, and the gate's own responses declare
|
||||||
|
/// `frame-ancestors 'self' http://*:* https://*:*` (see `page()`) because the
|
||||||
|
/// dashboard is reached by LAN IP, mDNS name, and onion alike. Upstream
|
||||||
|
/// X-Frame-Options is dropped entirely; only the `frame-ancestors` directive
|
||||||
|
/// is removed from the app's CSP — the rest of the app's policy (script-src,
|
||||||
|
/// connect-src, …) is the app's business and passes through untouched.
|
||||||
|
fn neutralize_frame_blocking(headers: &mut hyper::HeaderMap) {
|
||||||
|
headers.remove("x-frame-options");
|
||||||
|
let Some(csp) = headers.get("content-security-policy") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(raw) = csp.to_str() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !raw.to_ascii_lowercase().contains("frame-ancestors") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let kept: Vec<&str> = raw
|
||||||
|
.split(';')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|d| !d.to_ascii_lowercase().starts_with("frame-ancestors") && !d.is_empty())
|
||||||
|
.collect();
|
||||||
|
if kept.is_empty() {
|
||||||
|
headers.remove("content-security-policy");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match header::HeaderValue::from_str(&kept.join("; ")) {
|
||||||
|
Ok(v) => {
|
||||||
|
headers.insert("content-security-policy", v);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Unrepresentable after filtering — fail open for framing but
|
||||||
|
// closed for the policy: better to drop a mangled CSP than to
|
||||||
|
// serve one we rewrote incorrectly.
|
||||||
|
headers.remove("content-security-policy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Cookie names owned by the gate/daemon, never the app's to see.
|
/// Cookie names owned by the gate/daemon, never the app's to see.
|
||||||
const GATE_COOKIE_NAMES: &[&str] = &["session", "csrf_token"];
|
const GATE_COOKIE_NAMES: &[&str] = &["session", "csrf_token"];
|
||||||
|
|
||||||
@@ -1072,6 +1124,50 @@ mod tests {
|
|||||||
/// 2026-08-05). It must still be uncacheable, and still refuse to be
|
/// 2026-08-05). It must still be uncacheable, and still refuse to be
|
||||||
/// framed by a foreign origin, which `frame-ancestors` expresses and
|
/// framed by a foreign origin, which `frame-ancestors` expresses and
|
||||||
/// `X-Frame-Options` cannot.
|
/// `X-Frame-Options` cannot.
|
||||||
|
/// Upstream frame-blocking must not survive the proxy: X-Frame-Options
|
||||||
|
/// goes away entirely, CSP loses ONLY its frame-ancestors directive —
|
||||||
|
/// the app's remaining policy must pass through byte-preserving in
|
||||||
|
/// content (Alby Hub's DENY + strict CSP was the real-world case,
|
||||||
|
/// archi-dev-box 2026-08-12).
|
||||||
|
#[test]
|
||||||
|
fn proxied_responses_lose_frame_blocking_but_keep_the_apps_csp() {
|
||||||
|
let mut headers = hyper::HeaderMap::new();
|
||||||
|
headers.insert("x-frame-options", "DENY".parse().unwrap());
|
||||||
|
headers.insert(
|
||||||
|
"content-security-policy",
|
||||||
|
"default-src 'self'; frame-ancestors 'none'; img-src 'self' https://cdn.example"
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
neutralize_frame_blocking(&mut headers);
|
||||||
|
assert!(!headers.contains_key("x-frame-options"));
|
||||||
|
let csp = headers["content-security-policy"].to_str().unwrap();
|
||||||
|
assert!(!csp.contains("frame-ancestors"));
|
||||||
|
assert!(csp.contains("default-src 'self'"));
|
||||||
|
assert!(csp.contains("img-src 'self' https://cdn.example"));
|
||||||
|
|
||||||
|
// CSP that is ONLY a frame-ancestors directive disappears entirely.
|
||||||
|
let mut only = hyper::HeaderMap::new();
|
||||||
|
only.insert(
|
||||||
|
"content-security-policy",
|
||||||
|
"frame-ancestors 'self'".parse().unwrap(),
|
||||||
|
);
|
||||||
|
neutralize_frame_blocking(&mut only);
|
||||||
|
assert!(!only.contains_key("content-security-policy"));
|
||||||
|
|
||||||
|
// No frame directives at all → CSP untouched.
|
||||||
|
let mut plain = hyper::HeaderMap::new();
|
||||||
|
plain.insert(
|
||||||
|
"content-security-policy",
|
||||||
|
"default-src 'self'".parse().unwrap(),
|
||||||
|
);
|
||||||
|
neutralize_frame_blocking(&mut plain);
|
||||||
|
assert_eq!(
|
||||||
|
plain["content-security-policy"].to_str().unwrap(),
|
||||||
|
"default-src 'self'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn challenge_pages_are_uncacheable_and_framable_only_by_this_node() {
|
fn challenge_pages_are_uncacheable_and_framable_only_by_this_node() {
|
||||||
let resp = login_page(&app(), None, StatusCode::UNAUTHORIZED);
|
let resp = login_page(&app(), None, StatusCode::UNAUTHORIZED);
|
||||||
|
|||||||
@@ -1013,7 +1013,7 @@ pub async fn dismiss_update(data_dir: &Path) -> Result<()> {
|
|||||||
/// partially-corrupt resume still fails cleanly.
|
/// partially-corrupt resume still fails cleanly.
|
||||||
pub async fn download_update(data_dir: &Path) -> Result<DownloadProgress> {
|
pub async fn download_update(data_dir: &Path) -> Result<DownloadProgress> {
|
||||||
let _op = UPDATE_OP_LOCK.try_lock().map_err(|_| {
|
let _op = UPDATE_OP_LOCK.try_lock().map_err(|_| {
|
||||||
anyhow::anyhow!("another update operation (download or apply) is already running")
|
anyhow::anyhow!("Update already in progress — another download or apply is already running")
|
||||||
})?;
|
})?;
|
||||||
let mut state = load_state(data_dir).await?;
|
let mut state = load_state(data_dir).await?;
|
||||||
if state.available_update.is_none() {
|
if state.available_update.is_none() {
|
||||||
@@ -1406,8 +1406,8 @@ async fn verify_staged_components(staging_dir: &Path, manifest: &UpdateManifest)
|
|||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
if len != component.size_bytes {
|
if len != component.size_bytes {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"staged component {} is {} bytes but the manifest says {} — \
|
"Update staging is inconsistent: component {} is {} bytes but the manifest says {} — \
|
||||||
refusing to apply (incomplete or concurrently-rewritten download)",
|
re-download before applying (incomplete or concurrently-rewritten download)",
|
||||||
component.name,
|
component.name,
|
||||||
len,
|
len,
|
||||||
component.size_bytes
|
component.size_bytes
|
||||||
@@ -1519,11 +1519,11 @@ pub(crate) async fn host_sudo_output(args: &[&str]) -> Result<std::process::Outp
|
|||||||
/// Apply a downloaded update. Backs up current binaries, replaces with staged versions.
|
/// Apply a downloaded update. Backs up current binaries, replaces with staged versions.
|
||||||
pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
||||||
let _op = UPDATE_OP_LOCK.try_lock().map_err(|_| {
|
let _op = UPDATE_OP_LOCK.try_lock().map_err(|_| {
|
||||||
anyhow::anyhow!("another update operation (download or apply) is already running")
|
anyhow::anyhow!("Update already in progress — another download or apply is already running")
|
||||||
})?;
|
})?;
|
||||||
let staging_dir = data_dir.join("update-staging");
|
let staging_dir = data_dir.join("update-staging");
|
||||||
if !staging_dir.exists() {
|
if !staging_dir.exists() {
|
||||||
anyhow::bail!("No staged update found. Download first.");
|
anyhow::bail!("Update not staged — download it first, then apply.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gate 1: the completion marker is written only after EVERY component
|
// Gate 1: the completion marker is written only after EVERY component
|
||||||
@@ -1531,7 +1531,7 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
|||||||
// or in-flight download — exactly what got installed on .198.
|
// or in-flight download — exactly what got installed on .198.
|
||||||
if !has_staged_update(data_dir).await {
|
if !has_staged_update(data_dir).await {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"Staged update is incomplete (no completion marker) — download the update again before applying"
|
"Update download was incomplete (no completion marker) — download the update again before applying"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1540,9 +1540,7 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
|||||||
.await?
|
.await?
|
||||||
.available_update
|
.available_update
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
anyhow::anyhow!(
|
anyhow::anyhow!("Update manifest missing from state — re-download the update")
|
||||||
"no update manifest in state to verify staged files against — re-download the update"
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
verify_staged_components(&staging_dir, &manifest).await?;
|
verify_staged_components(&staging_dir, &manifest).await?;
|
||||||
|
|
||||||
@@ -1578,41 +1576,83 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
|||||||
info!("Current binary backed up");
|
info!("Current binary backed up");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply staged components
|
// Apply staged components in a DETERMINISTIC order, binary LAST.
|
||||||
|
// read_dir order is filesystem-arbitrary, and each component used to be
|
||||||
|
// consumed destructively — so a mid-apply failure could leave staging
|
||||||
|
// half-emptied and un-reappliable (Gate 2 re-verifies EVERY manifest
|
||||||
|
// component against staging, so a missing one wedges every retry:
|
||||||
|
// "doesn't apply, still says install, can never apply again"). Two
|
||||||
|
// guards against that now: (a) nothing is removed from staging here —
|
||||||
|
// the binary is copied, not moved (see its block) — so a failed apply
|
||||||
|
// is always retryable from the same staged files; (b) the binary, the
|
||||||
|
// one component whose swap changes what runs after restart, is applied
|
||||||
|
// only after the frontend/runtime succeed, so a frontend failure never
|
||||||
|
// leaves a new binary staged to run against an old frontend on the next
|
||||||
|
// restart.
|
||||||
|
let mut names: Vec<String> = Vec::new();
|
||||||
|
{
|
||||||
let mut entries = fs::read_dir(&staging_dir)
|
let mut entries = fs::read_dir(&staging_dir)
|
||||||
.await
|
.await
|
||||||
.context("Failed to read staging dir")?;
|
.context("Failed to read staging dir")?;
|
||||||
|
|
||||||
while let Some(entry) = entries.next_entry().await? {
|
while let Some(entry) = entries.next_entry().await? {
|
||||||
let name = entry.file_name().to_string_lossy().to_string();
|
names.push(entry.file_name().to_string_lossy().to_string());
|
||||||
let src = entry.path();
|
}
|
||||||
|
}
|
||||||
|
names.sort_by_key(|n| match n.as_str() {
|
||||||
|
"archipelago" => 2, // binary last
|
||||||
|
n if n.contains("runtime") && n.ends_with(".tar.gz") => 1,
|
||||||
|
_ => 0, // frontend and everything else first
|
||||||
|
});
|
||||||
|
|
||||||
match name.as_str() {
|
for name in &names {
|
||||||
|
let name = name.as_str();
|
||||||
|
let src = staging_dir.join(name);
|
||||||
|
|
||||||
|
match name {
|
||||||
"archipelago" => {
|
"archipelago" => {
|
||||||
// Two namespace gotchas this block works around:
|
// Three constraints this block works around:
|
||||||
// 1. We're running FROM /usr/local/bin/archipelago, so
|
// 1. We're running FROM /usr/local/bin/archipelago, so
|
||||||
// `install`/`cp` (O_TRUNC + write) fail with ETXTBSY.
|
// `install`/`cp` (O_TRUNC + write) fail with ETXTBSY.
|
||||||
// Use `mv`, which is atomic rename() and tolerates a
|
// rename() over a busy destination is fine.
|
||||||
// busy destination.
|
|
||||||
// 2. archipelago.service sets ProtectSystem=strict, so
|
// 2. archipelago.service sets ProtectSystem=strict, so
|
||||||
// even `sudo mv` into /usr/local/bin/ fails EROFS —
|
// even `sudo mv` into /usr/local/bin/ fails EROFS —
|
||||||
// sudo inherits the service's mount namespace. Route
|
// sudo inherits the service's mount namespace. Route
|
||||||
// the rename through systemd-run so it runs in a
|
// through host_sudo (systemd-run transient unit with
|
||||||
// transient unit with default protections.
|
// default protections).
|
||||||
|
// 3. The staged binary must SURVIVE this so a later
|
||||||
|
// component's failure leaves the apply retryable. So we
|
||||||
|
// COPY the staged file to a sibling temp in the target
|
||||||
|
// dir, then atomic-rename the temp over the target —
|
||||||
|
// the staging copy is never moved. (mv'ing the staged
|
||||||
|
// file itself was the wedging bug: binary applied, then
|
||||||
|
// frontend fails, staging now missing the binary, every
|
||||||
|
// retry fails re-verification forever.)
|
||||||
let staged = src.to_string_lossy().to_string();
|
let staged = src.to_string_lossy().to_string();
|
||||||
let _ = host_sudo(&["chmod", "0755", &staged]).await;
|
let tmp = format!(
|
||||||
let _ = host_sudo(&["chown", "root:root", &staged]).await;
|
"/usr/local/bin/.archipelago.new.{}",
|
||||||
let status = host_sudo(&["mv", &staged, "/usr/local/bin/archipelago"])
|
chrono::Utc::now().timestamp_millis()
|
||||||
|
);
|
||||||
|
let cp = host_sudo(&["cp", "-f", &staged, &tmp])
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Failed to copy staged binary for {}", name))?;
|
||||||
|
if !cp.success() {
|
||||||
|
let _ = host_sudo(&["rm", "-f", &tmp]).await;
|
||||||
|
anyhow::bail!("copy of staged binary failed for {}", name);
|
||||||
|
}
|
||||||
|
let _ = host_sudo(&["chmod", "0755", &tmp]).await;
|
||||||
|
let _ = host_sudo(&["chown", "root:root", &tmp]).await;
|
||||||
|
let status = host_sudo(&["mv", &tmp, "/usr/local/bin/archipelago"])
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("Failed to spawn mv for {}", name))?;
|
.with_context(|| format!("Failed to spawn mv for {}", name))?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
|
let _ = host_sudo(&["rm", "-f", &tmp]).await;
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"mv into /usr/local/bin failed for {} (exit {:?})",
|
"mv into /usr/local/bin failed for {} (exit {:?})",
|
||||||
name,
|
name,
|
||||||
status.code()
|
status.code()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
info!(name = %name, "Backend binary applied");
|
info!(name = %name, "Backend binary applied (staging preserved)");
|
||||||
}
|
}
|
||||||
_ if name.contains("frontend") && name.ends_with(".tar.gz") => {
|
_ if name.contains("frontend") && name.ends_with(".tar.gz") => {
|
||||||
// Tarball contents are the *inside* of web-ui/ (root entries
|
// Tarball contents are the *inside* of web-ui/ (root entries
|
||||||
@@ -2738,9 +2778,20 @@ mod tests {
|
|||||||
save_state(dir.path(), &state).await.unwrap();
|
save_state(dir.path(), &state).await.unwrap();
|
||||||
let err = apply_update(dir.path()).await.unwrap_err();
|
let err = apply_update(dir.path()).await.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
err.to_string().contains("refusing to apply"),
|
err.to_string().contains("re-download before applying"),
|
||||||
"got: {err:#}"
|
"got: {err:#}"
|
||||||
);
|
);
|
||||||
|
// Resilience: a refused apply must leave the update still available and
|
||||||
|
// still staged, so the user can re-download and retry — never a wedge.
|
||||||
|
let loaded = load_state(dir.path()).await.unwrap();
|
||||||
|
assert!(
|
||||||
|
loaded.available_update.is_some(),
|
||||||
|
"a refused apply must not clear the available update"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
loaded.update_in_progress,
|
||||||
|
"a refused apply must leave the staged-update flag set for retry"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
+120
-13
@@ -130,7 +130,46 @@ app:
|
|||||||
|
|
||||||
Additional extension keys may exist for current integrations, for example Bitcoin, Lightning, or app-specific launch/interface metadata. Treat extension keys as transitional unless they are documented as reusable platform primitives.
|
Additional extension keys may exist for current integrations, for example Bitcoin, Lightning, or app-specific launch/interface metadata. Treat extension keys as transitional unless they are documented as reusable platform primitives.
|
||||||
|
|
||||||
Use `metadata.launch.open_in_new_tab: true` when the app UI is known to reject iframe embedding with headers such as `X-Frame-Options` or restrictive CSP. The frontend app-session metadata is generated from this flag during release work.
|
### Iframe embedding — the rules
|
||||||
|
|
||||||
|
The dashboard opens apps in an **embedded frame** (My Apps → app session) by
|
||||||
|
default. Whether that works is decided by HTTP headers, not by wishes, so
|
||||||
|
know the mechanics:
|
||||||
|
|
||||||
|
- Browsers refuse to render a page in an iframe when the response carries
|
||||||
|
`X-Frame-Options: DENY`/`SAMEORIGIN` (the dashboard and the app are
|
||||||
|
different origins — different port at minimum) or a CSP `frame-ancestors`
|
||||||
|
directive that excludes the dashboard's origin.
|
||||||
|
- Many upstream apps ship exactly those headers (Alby Hub sends
|
||||||
|
`X-Frame-Options: DENY`). In a normal deployment that is correct hardening;
|
||||||
|
behind Archipelago's app gate the clickjacking threat those headers address
|
||||||
|
is already handled — every proxied request is authenticated by the gate
|
||||||
|
first.
|
||||||
|
- Therefore **the gate neutralizes frame blocking on proxied responses**: it
|
||||||
|
removes `X-Frame-Options` and strips only the `frame-ancestors` directive
|
||||||
|
from the app's CSP. The rest of the app's CSP (script-src, connect-src, …)
|
||||||
|
passes through untouched — the gate never weakens the app's own content
|
||||||
|
policy, only its framing policy. You do not need a bespoke reverse proxy,
|
||||||
|
header patches, or app config to be embeddable.
|
||||||
|
|
||||||
|
Set `metadata.launch.open_in_new_tab: true` only when embedding is broken by
|
||||||
|
things headers can't fix — the app frame-busts in JavaScript, requires being
|
||||||
|
the top-level origin (OAuth redirect flows, WebAuthn), or sets
|
||||||
|
`SameSite=Strict` session cookies that never accompany framed requests. Test
|
||||||
|
in the real embedded app session, **not** a plain browser tab: tabs don't
|
||||||
|
enforce framing headers, so a tab proves nothing about the iframe.
|
||||||
|
|
||||||
|
**The dashboard also self-heals**: if a running app's frame fails to load,
|
||||||
|
the session offers "open in tab" and *remembers the app as a tab app* — every
|
||||||
|
later launch opens a tab directly, with the tab-launch icon on its button.
|
||||||
|
The memory clears itself when the app later embeds successfully, and expires
|
||||||
|
weekly so fixes get re-probed. This safety net is not a licence to skip the
|
||||||
|
manifest flag: declaring `open_in_new_tab: true` up front spares your users
|
||||||
|
the one dead-pane encounter the detector needs.
|
||||||
|
|
||||||
|
(Historical note: before the gate handled this, embeddable-but-blocking apps
|
||||||
|
each carried a hand-built nginx strip proxy — gitea's port-3000 proxy is the
|
||||||
|
surviving example. Do not copy that pattern for new apps.)
|
||||||
|
|
||||||
### Launch Interfaces
|
### Launch Interfaces
|
||||||
|
|
||||||
@@ -406,19 +445,73 @@ curl http://localhost:8180/health
|
|||||||
podman logs my-app
|
podman logs my-app
|
||||||
```
|
```
|
||||||
|
|
||||||
### On an Archipelago Node
|
### On an Archipelago Node (before your app is in the catalog)
|
||||||
|
|
||||||
1. Install via the marketplace UI or RPC:
|
The App Store lists **signed-catalog apps and Nostr-discovered apps only** —
|
||||||
```bash
|
a manifest on the node's disk never appears in the store by itself. That is
|
||||||
curl -b cookies.txt -X POST http://archipelago.local/rpc/v1 \
|
deliberate: the store is a trust surface. But the orchestrator installs from
|
||||||
-d '{"method":"package.install","params":{"id":"my-app","dockerImage":"docker.io/myorg/my-app:1.0.0"}}'
|
disk manifests just fine, so you can test the complete install/run/uninstall
|
||||||
```
|
lifecycle on your own node before your app is published anywhere.
|
||||||
2. Verify the container is running:
|
|
||||||
```bash
|
**1. Stage the manifest where it survives reboots.**
|
||||||
curl -b cookies.txt -X POST http://archipelago.local/rpc/v1 \
|
|
||||||
-d '{"method":"container-list"}'
|
`/opt/archipelago/apps/` is *rebuilt on every backend start* from the runtime
|
||||||
```
|
payload that ships inside the frontend bundle
|
||||||
3. Check the UI. The app's detail page is `http://archipelago.local/dashboard/apps/my-app`; the embedded launch surface is `http://archipelago.local/dashboard/app-session/my-app`
|
(`/opt/archipelago/web-ui/archipelago-runtime/apps/`). If you copy your
|
||||||
|
manifest only into `/opt/archipelago/apps/`, the next restart silently deletes
|
||||||
|
it. Stage into the payload directory instead — the boot sync then promotes it
|
||||||
|
for you:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /opt/archipelago/web-ui/archipelago-runtime/apps/my-app
|
||||||
|
sudo cp apps/my-app/manifest.yml /opt/archipelago/web-ui/archipelago-runtime/apps/my-app/
|
||||||
|
sudo systemctl restart archipelago # manifests are loaded at startup
|
||||||
|
```
|
||||||
|
|
||||||
|
Watch `journalctl -u archipelago` after the restart — the orchestrator
|
||||||
|
validates every manifest on load and tells you about problems immediately
|
||||||
|
(for example a host-port collision with another installed app).
|
||||||
|
|
||||||
|
**2. Install over JSON-RPC.**
|
||||||
|
|
||||||
|
The repo ships the same session helper the release lifecycle gate uses.
|
||||||
|
Three things to know before using it: it needs `jq`; it reuses a cached
|
||||||
|
session from `/tmp/archy-rpc-session-<uid>` unless `ARCHY_FORCE_LOGIN=1` is
|
||||||
|
set (a stale cache fails every call quietly); and it sets `set -euo pipefail`,
|
||||||
|
so run it inside a script or subshell — sourcing it into your interactive
|
||||||
|
shell makes the first failed step kill the whole chain without printing
|
||||||
|
anything.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <<'EOF'
|
||||||
|
export ARCHY_PASSWORD='<your dashboard password>' ARCHY_FORCE_LOGIN=1
|
||||||
|
# Stock nodes serve HTTPS on 443; dev boxes behind plain nginx use:
|
||||||
|
# export ARCHY_HOST=127.0.0.1 ARCHY_SCHEME=http
|
||||||
|
source tests/lifecycle/lib/rpc.bash
|
||||||
|
rpc_login && echo "login ok"
|
||||||
|
# Both fields are required: `dockerImage` is normally supplied by the App
|
||||||
|
# Store from the signed catalog — pre-catalog, you pass your manifest's
|
||||||
|
# image yourself (it must match, and must come from a trusted registry).
|
||||||
|
rpc_call package.install '{"id":"my-app","dockerImage":"docker.io/myorg/my-app:1.0.0"}'
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Verify the lifecycle, not just the install:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rpc_call package.status '{"id":"my-app"}' # state + health (run inside the same subshell pattern)
|
||||||
|
podman ps --filter name=my-app # container is up
|
||||||
|
rpc_call package.stop '{"id":"my-app"}' # …and start, restart
|
||||||
|
sudo systemctl restart archipelago # app must survive this
|
||||||
|
rpc_call package.uninstall '{"id":"my-app","preserve_data":true}'
|
||||||
|
rpc_call package.install '{"id":"my-app"}' # data still there?
|
||||||
|
```
|
||||||
|
|
||||||
|
The app's detail page is `https://<node>/dashboard/apps/my-app`; a gated web
|
||||||
|
UI is reachable through the app gate on its manifest port once running.
|
||||||
|
|
||||||
|
Only after this loop is green does the app belong in a catalog submission —
|
||||||
|
catalog inclusion is what makes it appear in the App Store.
|
||||||
|
|
||||||
### Validate Manifest
|
### Validate Manifest
|
||||||
|
|
||||||
@@ -438,6 +531,20 @@ The broader app update policy for `1.8-alpha` is still being finalized. Until th
|
|||||||
|
|
||||||
## App Icon
|
## App Icon
|
||||||
|
|
||||||
|
- **The tile plate (dark rounded background, border, sheen) is applied by
|
||||||
|
the system** — every surface renders your icon with the house
|
||||||
|
`archy-app-icon` treatment automatically. What the system deliberately
|
||||||
|
does NOT add is an inner margin (icons that already carry whitespace
|
||||||
|
would double-margin), so your file needs it baked in: **square canvas,
|
||||||
|
~12% margin per side**. Edge-to-edge marks look wrong next to every
|
||||||
|
other tile. Don't hand-tune it: run your mark through the normalizer
|
||||||
|
and commit its output —
|
||||||
|
```bash
|
||||||
|
scripts/normalize-app-icon.py your-mark.svg apps-icon-output.svg
|
||||||
|
```
|
||||||
|
It wraps any SVG (full-bleed upstream logos included) onto the house
|
||||||
|
canvas with the standard margin, preserving the original artwork
|
||||||
|
untouched inside.
|
||||||
- Provide a URL to your app icon (PNG, WebP, or SVG)
|
- Provide a URL to your app icon (PNG, WebP, or SVG)
|
||||||
- Recommended size: 256x256 pixels
|
- Recommended size: 256x256 pixels
|
||||||
- Square aspect ratio
|
- Square aspect ratio
|
||||||
|
|||||||
@@ -408,6 +408,10 @@ DOCKERFILE_HEAD
|
|||||||
xorg \
|
xorg \
|
||||||
xdotool \
|
xdotool \
|
||||||
chromium \
|
chromium \
|
||||||
|
mesa-va-drivers \
|
||||||
|
intel-media-va-driver \
|
||||||
|
i965-va-driver \
|
||||||
|
vainfo \
|
||||||
pipewire \
|
pipewire \
|
||||||
pipewire-pulse \
|
pipewire-pulse \
|
||||||
pipewire-alsa \
|
pipewire-alsa \
|
||||||
|
|||||||
@@ -1,5 +1,28 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
# TearFree BEFORE X starts: bare Xorg with the stock modesetting driver has
|
||||||
|
# no vsync and no compositor, so video page-flips land mid-scanout — visible
|
||||||
|
# tearing on every kiosk (reported 2026-08-11, "really bad" on IndeedHub
|
||||||
|
# playback). The modesetting driver's TearFree option double-buffers the
|
||||||
|
# scanout at the driver level: no compositor needed, one frame of latency,
|
||||||
|
# no interaction with the 2026-06-28 choppy-audio GPU decisions. Written
|
||||||
|
# here (idempotently) rather than baked into the image so existing kiosk
|
||||||
|
# nodes pick it up through the launcher's own OTA path (bootstrap.rs
|
||||||
|
# reinstalls this script on every node).
|
||||||
|
XORG_CONF_DIR=/etc/X11/xorg.conf.d
|
||||||
|
XORG_TEARFREE="$XORG_CONF_DIR/20-archipelago-kiosk-tearfree.conf"
|
||||||
|
mkdir -p "$XORG_CONF_DIR"
|
||||||
|
if [ ! -f "$XORG_TEARFREE" ] || ! grep -q TearFree "$XORG_TEARFREE"; then
|
||||||
|
cat > "$XORG_TEARFREE" <<'EOF'
|
||||||
|
# Written by archipelago-kiosk-launcher — vsynced scanout for kiosk video.
|
||||||
|
Section "Device"
|
||||||
|
Identifier "Archipelago Kiosk GPU"
|
||||||
|
Driver "modesetting"
|
||||||
|
Option "TearFree" "true"
|
||||||
|
EndSection
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
# Start a dedicated X server for the attached kiosk display.
|
# Start a dedicated X server for the attached kiosk display.
|
||||||
/usr/bin/Xorg :0 vt1 -nolisten tcp -keeptty &
|
/usr/bin/Xorg :0 vt1 -nolisten tcp -keeptty &
|
||||||
XPID=$!
|
XPID=$!
|
||||||
@@ -149,16 +172,71 @@ xset s noblank 2>/dev/null || true
|
|||||||
pkill -u archipelago -f 'chromium.*localhost' 2>/dev/null || true
|
pkill -u archipelago -f 'chromium.*localhost' 2>/dev/null || true
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
# GPU vs headless (#36, choppy-audio incident 2026-06-28). --enable-gpu-rasterization
|
# ── Graphics tier ────────────────────────────────────────────────────────
|
||||||
# spins a dedicated GPU process at 55-92% CPU even on real GPU hardware (Intel HD 5500)
|
# One flag set does not fit all kiosk hardware. The June-2026 choppy-audio
|
||||||
# because under X11 it falls back to software compositing anyway — that CPU
|
# incident (#36) proved HD 5500-era boxes melt with GPU rasterization on
|
||||||
# starvation is what caused choppy HDMI audio. --in-process-gpu avoids the
|
# (under X11 they fall back to software compositing while a GPU process
|
||||||
# separate process; GpuRasterization is also disabled via --disable-features below.
|
# burns 55-92% CPU) — but holding MODERN iGPUs to those same defensive
|
||||||
# On a GPU-less / headless server (no /dev/dri), disable GPU entirely instead.
|
# flags (single raster thread, raster ban) froze framework-pt outright on
|
||||||
|
# the animated network map at 4K (2026-08-13). So: two tiers.
|
||||||
|
#
|
||||||
|
# legacy — the proven HD 5500 tuning: --in-process-gpu, ONE raster
|
||||||
|
# thread, GpuRasterization banned via --disable-features.
|
||||||
|
# modern — --in-process-gpu, Chromium's default raster threads,
|
||||||
|
# GpuRasterization allowed.
|
||||||
|
#
|
||||||
|
# KIOSK_GRAPHICS in /etc/archipelago/kiosk-display.conf overrides:
|
||||||
|
# auto (default) | performance (force legacy) | quality (force modern)
|
||||||
|
# Set from Settings → Display; sourced with the rest of the conf above.
|
||||||
|
#
|
||||||
|
# Auto classifies by CPU model string — transparent and greppable, and the
|
||||||
|
# iGPU generation tracks the CPU generation on this hardware. Rules:
|
||||||
|
# * "NNth Gen Intel" → only stamped on gen 10+ model names → modern
|
||||||
|
# * AMD Ryzen → modern
|
||||||
|
# * Intel iN-NNNNN (5 dig)→ gen 10+ desktop → modern
|
||||||
|
# * Intel iN-8xxx/9xxx → gen 8/9 → modern
|
||||||
|
# * anything else → legacy (fail conservative: slow-but-stable)
|
||||||
|
detect_graphics_tier() {
|
||||||
|
case "${KIOSK_GRAPHICS:-auto}" in
|
||||||
|
performance) echo legacy; return ;;
|
||||||
|
quality) echo modern; return ;;
|
||||||
|
esac
|
||||||
|
_cpu=$(grep -m1 '^model name' /proc/cpuinfo 2>/dev/null || true)
|
||||||
|
case "$_cpu" in
|
||||||
|
*"Gen Intel"*) echo modern; return ;;
|
||||||
|
*AMD*Ryzen*) echo modern; return ;;
|
||||||
|
esac
|
||||||
|
_num=$(printf '%s' "$_cpu" | grep -oE 'i[3579]-[0-9]{4,5}' | head -1 | cut -d- -f2)
|
||||||
|
case "$_num" in
|
||||||
|
[0-9][0-9][0-9][0-9][0-9]) echo modern; return ;; # 5 digits = gen 10+
|
||||||
|
[89][0-9][0-9][0-9]) echo modern; return ;; # gen 8/9
|
||||||
|
esac
|
||||||
|
echo legacy
|
||||||
|
}
|
||||||
|
|
||||||
if [ -e /dev/dri/card0 ] || [ -e /dev/dri/renderD128 ]; then
|
if [ -e /dev/dri/card0 ] || [ -e /dev/dri/renderD128 ]; then
|
||||||
|
GRAPHICS_TIER=$(detect_graphics_tier)
|
||||||
|
if [ "$GRAPHICS_TIER" = "modern" ]; then
|
||||||
|
GPU_FLAGS="--in-process-gpu"
|
||||||
|
EXTRA_DISABLED_FEATURES=""
|
||||||
|
else
|
||||||
GPU_FLAGS="--in-process-gpu --num-raster-threads=1"
|
GPU_FLAGS="--in-process-gpu --num-raster-threads=1"
|
||||||
|
EXTRA_DISABLED_FEATURES=",GpuRasterization"
|
||||||
|
fi
|
||||||
|
# Hardware VIDEO DECODE (VA-API) on GPU hardware — both tiers. Decode
|
||||||
|
# offload REDUCES the CPU pressure that caused the choppy-audio
|
||||||
|
# incident, it doesn't re-create it. Falls back silently to software
|
||||||
|
# decode when the platform lacks a va driver — never a black player.
|
||||||
|
# IgnoreDriverChecks: older Intel gens (HD 5500-era kiosks) are wrongly
|
||||||
|
# blocklisted upstream.
|
||||||
|
ENABLE_FEATURES="OverlayScrollbar,VaapiVideoDecodeLinuxGL,VaapiIgnoreDriverChecks"
|
||||||
|
echo "archipelago-kiosk: graphics tier=$GRAPHICS_TIER (KIOSK_GRAPHICS=${KIOSK_GRAPHICS:-auto})"
|
||||||
else
|
else
|
||||||
|
# GPU-less / headless server (no /dev/dri): no GPU at all.
|
||||||
|
GRAPHICS_TIER=headless
|
||||||
GPU_FLAGS="--disable-gpu --num-raster-threads=1"
|
GPU_FLAGS="--disable-gpu --num-raster-threads=1"
|
||||||
|
EXTRA_DISABLED_FEATURES=",GpuRasterization"
|
||||||
|
ENABLE_FEATURES="OverlayScrollbar"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ARCHIPELAGO_UID=$(id -u archipelago)
|
ARCHIPELAGO_UID=$(id -u archipelago)
|
||||||
@@ -193,8 +271,8 @@ while true; do
|
|||||||
--disable-translate \
|
--disable-translate \
|
||||||
--no-first-run \
|
--no-first-run \
|
||||||
--check-for-update-interval=31536000 \
|
--check-for-update-interval=31536000 \
|
||||||
--disable-features=TranslateUI,MetricsReporting,AutofillServerCommunication,PasswordManagerEnabled,GpuRasterization \
|
--disable-features=TranslateUI,MetricsReporting,AutofillServerCommunication,PasswordManagerEnabled${EXTRA_DISABLED_FEATURES} \
|
||||||
--enable-features=OverlayScrollbar \
|
--enable-features=$ENABLE_FEATURES \
|
||||||
--disable-session-crashed-bubble \
|
--disable-session-crashed-bubble \
|
||||||
--disable-save-password-bubble \
|
--disable-save-password-bubble \
|
||||||
--disable-suggestions-service \
|
--disable-suggestions-service \
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.129-alpha",
|
"version": "1.8.3-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.129-alpha",
|
"version": "1.8.3-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scure/bip39": "^2.2.0",
|
"@scure/bip39": "^2.2.0",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.129-alpha",
|
"version": "1.8.3-alpha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "./start-dev.sh",
|
"start": "./start-dev.sh",
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 100 100">
|
||||||
|
<!-- normalized by scripts/normalize-app-icon.py: margin=0.12 per side -->
|
||||||
|
<svg x="12.000" y="12.000" width="76.000" height="76.000" viewBox="0 0 8000 8000" preserveAspectRatio="xMidYMid meet">
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8000 8000">
|
||||||
|
<path d="M1404.94034,2404.75143c.19948.39893.416.79745.63896,1.19934-.21965-.3998-.4293-.7996-.63896-1.19934ZM1398.36987,2392.55825c.01.02001.01999.03996.01999.04996.01999.02001.03998.04996.04998.06997-.01999-.03996-.04998-.07997-.06997-.11993ZM7957.22848,6407.80899c-75.3022-453.82005-341.31246-1051.68618-631.76608-1536.83905-217.58172,280.41299-443.70125,543.12568-695.66263,796.78364,151.96083,272.22676,381.52012,831.33939,371.78586,1131.07073-3.34955,103.13742-84.59871,186.38404-187.48323,193.03587-323.69068,20.92769-761.15088-169.80941-1126.96417-352.01125-274.08918,250.36976-539.73074,478.97192-815.02811,690.20321,570.85548,338.56749,1305.54247,669.74202,1904.46573,668.08293,872.77622,46.26671,1378.68446-776.20339,1180.65263-1590.32607ZM5249.15884,5683.56479c1101.91285-1101.01171,2353.03599-2527.31198,2708.11113-4090.5697,253.93721-1273.80049-760.34667-1869.98142-1895.16006-1469.66567,211.65045,278.23428,364.22738,589.33979,457.73054,914.98744,86.80988-20.20216,196.93112-34.80914,289.99476-31.50156,103.33929,3.67279,186.93541,86.38156,191.60732,189.77353,13.96581,309.07109-235.73659,902.8025-402.1382,1193.8406,0,.01-.01.02001-.01.03001l-.04998.07991c-624.44753,1143.09071-1591.89389,2208.42396-2600.29743,3077.06633l-.22964-.19984c-263.64443,226.46298-526.16056,433.98687-804.2439,626.56893.39938.34966.79882.69956,1.19826,1.04946-509.31622,353.04862-1419.46673,925.33606-2012.13405,893.44541-103.82534-5.58671-186.23795-90.1864-188.67838-194.22674-6.97909-297.53161,215.6413-843.71786,363.83869-1111.11806-237.05325-259.94423-464.71032-529.69327-686.31643-814.38364-1228.33988,1900.32534-772.83666,3911.20697,1772.96127,2814.86279,1004.95316-454.63593,1968.00538-1208.05697,2803.81609-2000.0392ZM2528.2177,4000.38756c-468.17105-551.20508-806.89093-1020.88409-1125.16461-1599.13417h-.01c-.00197-.02709-.02111-.04517-.02999-.07997h-.01c-.00225-.03154-.04191-.05163-.03986-.08992,0,0-.01,0-.01-.01-1.46642-2.7001-3.08021-5.72568-4.5634-8.46529.01999.02001.03998.04996.04998.06997-165.01013-287.23702-415.64692-882.63022-403.53149-1193.46936,4.07951-104.66591,88.1281-188.31126,192.71288-192.04153,92.1143-3.28547,200.69379,10.46969,286.62767,30.61985,93.58312-325.69743,246.26999-636.85321,458.06014-915.09749C797.66386-275.84563-214.1867,319.25478,39.32792,1592.99819c208.75964,1126.24494,1140.55524,2354.14531,1865.30175,3206.20191-1.24817,1.76903,1.23818-1.76903,0,0,177.91975,202.67638,363.17881,406.52227,558.17368,604.08153,267.29912-191.50259,530.82373-396.10806,795.10723-617.7438-259.10108-257.46588-503.73334-520.6384-729.69288-785.15026ZM1420.59738,2433.86513c.84886,1.56913,1.71746,3.17821,2.5963,4.82725-.85885-1.58908-1.72745-3.19816-2.5963-4.82725ZM1457.40366,2502.26682c.92857,1.699,1.83716,3.39806,2.73599,5.07711-.89883-1.64904-1.80742-3.3481-2.73599-5.07711ZM1403.01311,2401.17342c-.01-.01-.01987-.02995-.02986-.03996.01.01.01987.02995.01987.03996.01999.03001.02999.04996.03998.06997-.01-.02001-.01999-.03996-.02999-.06997ZM1404.94034,2404.75143c.19948.39893.416.79745.63896,1.19934-.21965-.3998-.4293-.7996-.63896-1.19934Z" fill="#ffe480"/>
|
||||||
|
<path d="M2762.56314,2987.43935c-683.41752-683.42045-683.41752-1791.46134,0-2474.87781,683.41752-683.41755,1791.45841-683.41755,2474.87885,0,683.41068,683.41648,683.41068,1791.45737,0,2474.87781l-1237.44089,1237.44089-1237.43796-1237.44089Z" fill="#fff"/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 100 100">
|
||||||
|
<!-- normalized by scripts/normalize-app-icon.py: margin=0.12 per side -->
|
||||||
|
<svg x="12.000" y="12.000" width="76.000" height="76.000" viewBox="0 0 94 94" preserveAspectRatio="xMidYMid meet">
|
||||||
|
<svg viewBox="0 0 94 94" id="vector" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="group" transform="matrix(1, 0, 0, 1, -6.999915, -7)">
|
||||||
|
<path id="path" d="M 30 81 C 23 80 21.63 71.49 29.16 45.87 C 35 26 46.18 26 58 26 C 73.17 26 81.61 29.18 83.79 35.73 C 84.243 36.883 84.39 38.135 84.214 39.362 C 84.039 40.589 83.548 41.75 82.79 42.73 C 82.613 42.96 82.419 43.178 82.21 43.38 C 85.1 45.31 86.88 48.02 86.52 51.9 C 86.13 56.26 83.16 58.55 78.52 59.66 C 78.849 60.396 79.013 61.194 79 62 C 78.988 63.199 78.695 64.378 78.147 65.444 C 77.598 66.51 76.808 67.433 75.84 68.14 C 70.09 72.76 60 75 46 73 C 43 72.57 41 75 39 77 C 36.62 79.38 34.24 81.61 30 81 Z" fill="#25A5FF"/>
|
||||||
|
<path id="path_1" d="M 58 30 C 66 30 78 31 80 37 C 82.24 43.71 69 43 59 42 C 68 43 83.18 44 82.5 51.5 C 82 57 72 57 55 56 C 63 57 75 58 75 62 C 75 68.28 58.74 71 47.67 69.31 C 39 68 38.06 76.77 31 75 C 27 74 28 63 33 47 C 38.29 30.09 46.49 30 58 30 Z" fill="#ffffff" stroke="#25A5FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -374,7 +374,7 @@
|
|||||||
"id": "pine",
|
"id": "pine",
|
||||||
"title": "Pine",
|
"title": "Pine",
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"description": "A private voice assistant for your home. Pine runs speech-to-text (Whisper), text-to-speech (Piper) and wake-word detection (openWakeWord) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. Ask it about your node — block height, sync, peers, Lightning balance — and, when a Claude API key is set, anything else.",
|
"description": "A private voice assistant for your home. Pine runs speech-to-text (Whisper), text-to-speech (Piper) and wake-word detection (openWakeWord) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. Ask it about your node \u2014 block height, sync, peers, Lightning balance \u2014 and, when a Claude API key is set, anything else.",
|
||||||
"icon": "/assets/img/app-icons/pine.svg",
|
"icon": "/assets/img/app-icons/pine.svg",
|
||||||
"author": "Archipelago",
|
"author": "Archipelago",
|
||||||
"category": "home",
|
"category": "home",
|
||||||
@@ -459,7 +459,7 @@
|
|||||||
"id": "netbird",
|
"id": "netbird",
|
||||||
"title": "NetBird",
|
"title": "NetBird",
|
||||||
"version": "2.38.0",
|
"version": "2.38.0",
|
||||||
"description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN. The user-facing entry point — a TLS proxy in front of the dashboard + server.",
|
"description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN. The user-facing entry point \u2014 a TLS proxy in front of the dashboard + server.",
|
||||||
"icon": "/assets/img/app-icons/netbird.svg",
|
"icon": "/assets/img/app-icons/netbird.svg",
|
||||||
"author": "NetBird",
|
"author": "NetBird",
|
||||||
"category": "networking",
|
"category": "networking",
|
||||||
@@ -547,6 +547,30 @@
|
|||||||
"/var/lib/archipelago/nextcloud:/var/www/html"
|
"/var/lib/archipelago/nextcloud:/var/www/html"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "alby-hub",
|
||||||
|
"title": "Alby Hub",
|
||||||
|
"version": "1.23.0",
|
||||||
|
"description": "Self-custodial Lightning wallet hub. Runs its own Lightning node on your Archipelago and connects your apps to it over Nostr Wallet Connect \u2014 one hub, every app pays through it.",
|
||||||
|
"icon": "/assets/img/app-icons/alby-hub.svg",
|
||||||
|
"author": "Alby",
|
||||||
|
"category": "money",
|
||||||
|
"tier": "optional",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/alby-hub:v1.23.0",
|
||||||
|
"repoUrl": "https://github.com/getAlby/hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "phoenixd",
|
||||||
|
"title": "phoenixd",
|
||||||
|
"version": "0.9.0",
|
||||||
|
"description": "Headless Lightning daemon by ACINQ (the Phoenix wallet team). No screen of its own \u2014 it exposes a small local API that other apps and tools use to send and receive Lightning payments. Channel liquidity is managed automatically for a fee.",
|
||||||
|
"icon": "/assets/img/app-icons/phoenixd.svg",
|
||||||
|
"author": "ACINQ",
|
||||||
|
"category": "money",
|
||||||
|
"tier": "optional",
|
||||||
|
"dockerImage": "source.archipelago-foundation.org/lfg2025/phoenixd:0.9.0",
|
||||||
|
"repoUrl": "https://github.com/ACINQ/phoenixd"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"versionName": "0.5.27",
|
||||||
|
"versionCode": 47
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ import { ref, computed, watch } from 'vue'
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||||
import { useBodyScrollLock } from '@/composables/useBodyScrollLock'
|
import { useBodyScrollLock } from '@/composables/useBodyScrollLock'
|
||||||
|
import { useModalHistory } from '@/composables/useModalHistory'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
show: boolean
|
show: boolean
|
||||||
@@ -105,6 +106,10 @@ function close() {
|
|||||||
|
|
||||||
useModalKeyboard(modalRef, computed(() => props.show), close)
|
useModalKeyboard(modalRef, computed(() => props.show), close)
|
||||||
useBodyScrollLock(computed(() => props.show))
|
useBodyScrollLock(computed(() => props.show))
|
||||||
|
// Browser/mouse/gesture Back closes the modal instead of navigating the
|
||||||
|
// router out from under it — the native-app behaviour kiosk and mobile
|
||||||
|
// browsers expect (the companion webview already provides it natively).
|
||||||
|
useModalHistory(computed(() => props.show), close)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -71,6 +71,11 @@
|
|||||||
I've installed it
|
I've installed it
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Which version the Download button installs — read from the
|
||||||
|
metadata staged beside the APK; absent file, absent note -->
|
||||||
|
<p v-if="companionVersion" class="text-xs text-white/40 text-center mt-3">
|
||||||
|
Version {{ companionVersion.versionName }}<template v-if="companionVersion.versionCode"> (build {{ companionVersion.versionCode }})</template>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Screen 2: pair the app with this node -->
|
<!-- Screen 2: pair the app with this node -->
|
||||||
@@ -153,6 +158,22 @@ const DEFAULT_DOWNLOAD_URL = IS_DEMO
|
|||||||
? `${window.location.origin}/packages/archipelago-companion.apk`
|
? `${window.location.origin}/packages/archipelago-companion.apk`
|
||||||
: 'https://source.archipelago-foundation.org/packages/archipelago-companion.apk'
|
: 'https://source.archipelago-foundation.org/packages/archipelago-companion.apk'
|
||||||
|
|
||||||
|
// Version note for the download step. Read from the node's own copy of the
|
||||||
|
// metadata (ships in the frontend beside the APK at /packages/), written by
|
||||||
|
// publish-companion-apk.sh from the same gradle config that built the APK.
|
||||||
|
// Best-effort: no file, no note.
|
||||||
|
const companionVersion = ref<{ versionName: string; versionCode: number } | null>(null)
|
||||||
|
async function loadCompanionVersion() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/packages/archipelago-companion.json', { cache: 'no-store' })
|
||||||
|
if (!res.ok) return
|
||||||
|
const meta = await res.json()
|
||||||
|
if (meta && typeof meta.versionName === 'string' && meta.versionName) {
|
||||||
|
companionVersion.value = { versionName: meta.versionName, versionCode: Number(meta.versionCode) || 0 }
|
||||||
|
}
|
||||||
|
} catch { /* metadata is a nicety — the download works without it */ }
|
||||||
|
}
|
||||||
|
|
||||||
// Deep-link scheme the companion app registers; carries the server entry the
|
// Deep-link scheme the companion app registers; carries the server entry the
|
||||||
// app should create (see docs/companion-pairing-qr.md for the contract).
|
// app should create (see docs/companion-pairing-qr.md for the contract).
|
||||||
const PAIR_SCHEME = 'archipelago://pair'
|
const PAIR_SCHEME = 'archipelago://pair'
|
||||||
@@ -239,6 +260,7 @@ watch(companionIntroRequested, (requested) => {
|
|||||||
|
|
||||||
watch(visible, async (isVisible) => {
|
watch(visible, async (isVisible) => {
|
||||||
if (!isVisible) return
|
if (!isVisible) return
|
||||||
|
if (!companionVersion.value) void loadCompanionVersion()
|
||||||
// Generate large and let CSS scale down — at 112px source a ~45-module QR
|
// Generate large and let CSS scale down — at 112px source a ~45-module QR
|
||||||
// is 2.5px/module, which camera decoders (the companion app included)
|
// is 2.5px/module, which camera decoders (the companion app included)
|
||||||
// routinely fail on. 512px keeps every module crisp.
|
// routinely fail on. 512px keeps every module crisp.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="logo-gradient-border screensaver-logo-cycle relative w-48 h-48 sm:w-64 sm:h-64 md:w-80 md:h-80 flex items-center justify-center overflow-hidden">
|
<div class="logo-gradient-border logo-gloss screensaver-logo-cycle relative w-48 h-48 sm:w-64 sm:h-64 md:w-80 md:h-80 flex items-center justify-center overflow-hidden">
|
||||||
<!-- Squares logo -->
|
<!-- Squares logo -->
|
||||||
<div class="screensaver-logo-squares absolute inset-[3px] flex items-center justify-center">
|
<div class="screensaver-logo-squares absolute inset-[3px] flex items-center justify-center">
|
||||||
<AnimatedLogo size="xl" no-border fit />
|
<AnimatedLogo size="xl" no-border fit />
|
||||||
|
|||||||
@@ -853,14 +853,34 @@ function render() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Kiosk TVs run the kiosk image's conservative rasterizer (single raster
|
||||||
|
* thread, GPU raster off — the June choppy-audio tuning), so a 60fps
|
||||||
|
* SVG-attribute animation at 4K froze the browser outright (framework-pt,
|
||||||
|
* 2026-08-12). On kiosks the ticker renders every OTHER frame (~30fps at a
|
||||||
|
* 60Hz panel) with the skipped frame's delta carried over so spin speed is
|
||||||
|
* unchanged — at couch distance the half rate is invisible, the paint cost
|
||||||
|
* halves. */
|
||||||
|
const kioskLowPower =
|
||||||
|
typeof document !== 'undefined' && document.documentElement.classList.contains('kiosk-mode')
|
||||||
|
let lowPowerPhase = 0
|
||||||
|
let carriedDeltaMS = 0
|
||||||
|
|
||||||
function tick(_time: number, deltaMS: number) {
|
function tick(_time: number, deltaMS: number) {
|
||||||
|
if (kioskLowPower) {
|
||||||
|
carriedDeltaMS += deltaMS
|
||||||
|
if ((lowPowerPhase++ & 1) === 1) return
|
||||||
|
deltaMS = carriedDeltaMS
|
||||||
|
carriedDeltaMS = 0
|
||||||
|
}
|
||||||
elapsed += deltaMS / 1000
|
elapsed += deltaMS / 1000
|
||||||
if (!dragging) cam.rotY += cam.spin * (deltaMS / 1000)
|
if (!dragging) cam.rotY += cam.spin * (deltaMS / 1000)
|
||||||
render()
|
render()
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachTicker() {
|
function attachTicker() {
|
||||||
if (tickerAttached || staticMode) return
|
// Kiosks run staticMode for PLACEMENT (instant, intro-free) but still get
|
||||||
|
// the calm orbit — half-rate via the kioskLowPower frame-skip in tick().
|
||||||
|
if (tickerAttached || (staticMode && !kioskLowPower)) return
|
||||||
gsap.ticker.add(tick)
|
gsap.ticker.add(tick)
|
||||||
tickerAttached = true
|
tickerAttached = true
|
||||||
}
|
}
|
||||||
@@ -1161,24 +1181,39 @@ function measure() {
|
|||||||
}
|
}
|
||||||
// Resize: snap to the current mode's fit (no tween — tracks the drag)
|
// Resize: snap to the current mode's fit (no tween — tracks the drag)
|
||||||
applyMode(viewMode.value, false)
|
applyMode(viewMode.value, false)
|
||||||
|
// No ticker (reduced-motion) means nobody repaints after this resize —
|
||||||
|
// without an explicit render the map keeps its stale (possibly zero-size)
|
||||||
|
// projection: the "loads blank / wrong scale until refresh" bug.
|
||||||
|
if (built && !tickerAttached) render()
|
||||||
}
|
}
|
||||||
|
|
||||||
function initialBuild() {
|
function initialBuild() {
|
||||||
staticMode = prefersReducedMotion()
|
// Kiosk TVs take the static PLACEMENT path (the GSAP entrance intro needs
|
||||||
|
// healthy rAF delivery to reach opacity 1 — on a paint-starved kiosk it
|
||||||
|
// stalls and the screen reads as BLANK until a lucky refresh,
|
||||||
|
// framework-pt 2026-08-14) but keep the calm orbital motion: the ticker
|
||||||
|
// re-attaches below at half rate. Fully-static-everything was tried and
|
||||||
|
// rejected — it also killed the animation on screens that could carry it.
|
||||||
|
staticMode = prefersReducedMotion() || kioskLowPower
|
||||||
built = true
|
built = true
|
||||||
applyGraph()
|
applyGraph()
|
||||||
|
|
||||||
// First measurement decides the default projection: saved preference wins,
|
// First measurement decides the default projection: saved preference wins,
|
||||||
// otherwise portrait containers (phone/companion) open in the flat 2D view.
|
// otherwise portrait containers (phone/companion) AND kiosk TVs open in the
|
||||||
|
// flat 2D view — the depth view stays one tap away and is remembered. The
|
||||||
|
// kiosk default exists for the same reason as the half-rate ticker above.
|
||||||
modeInitialized = true
|
modeInitialized = true
|
||||||
let saved: string | null = null
|
let saved: string | null = null
|
||||||
try { saved = localStorage.getItem('federation-map-projection') } catch { /* private mode */ }
|
try { saved = localStorage.getItem('federation-map-projection') } catch { /* private mode */ }
|
||||||
viewMode.value = saved === '2d' || saved === '3d' ? saved : (height > width ? '2d' : '3d')
|
viewMode.value =
|
||||||
|
saved === '2d' || saved === '3d' ? saved : (kioskLowPower || height > width ? '2d' : '3d')
|
||||||
applyMode(viewMode.value, false)
|
applyMode(viewMode.value, false)
|
||||||
|
|
||||||
if (staticMode) {
|
if (staticMode) {
|
||||||
introPlayed = true
|
introPlayed = true
|
||||||
playIntro() // static branch: everything lands in place
|
playIntro() // static branch: everything lands in place
|
||||||
|
attachTicker() // no-op for reduced-motion; kiosks resume the calm orbit
|
||||||
|
render() // paint the landed state now — nothing else repaints until a tick
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
attachTicker()
|
attachTicker()
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// Back/forward integration for modals (kiosk, remote browsers, mobile).
|
||||||
|
//
|
||||||
|
// Without this, the browser's Back control (mouse side-button on kiosk,
|
||||||
|
// gesture on mobile, toolbar button in a remote browser) navigates the
|
||||||
|
// ROUTER while a modal is open — at best closing the whole screen under a
|
||||||
|
// dialog, at worst leaving the app. The native-app expectation, and what
|
||||||
|
// the companion webview already provides, is: Back closes the topmost
|
||||||
|
// dialog first.
|
||||||
|
//
|
||||||
|
// Mechanics: opening a modal pushes one history entry (same URL, a depth
|
||||||
|
// marker in state — router keys are preserved by spreading the existing
|
||||||
|
// state). A popstate that lands BELOW our depth means the user pressed
|
||||||
|
// Back over an open modal: close the topmost one. A UI-side close (X,
|
||||||
|
// backdrop, Esc) consumes its own entry with history.back() so Back never
|
||||||
|
// needs pressing twice — guarded by the depth marker so it can never eat
|
||||||
|
// a router entry. One module-level stack serves every BaseModal instance,
|
||||||
|
// so stacked modals close one per Back, top first.
|
||||||
|
import { watch, type Ref } from 'vue'
|
||||||
|
|
||||||
|
type Entry = { close: () => void }
|
||||||
|
|
||||||
|
const stack: Entry[] = []
|
||||||
|
// Set when a popstate initiated the close: the history entry is already
|
||||||
|
// gone, so the close-side cleanup must not call history.back() again.
|
||||||
|
let poppedClose = false
|
||||||
|
let listening = false
|
||||||
|
|
||||||
|
function modalDepth(state: unknown): number {
|
||||||
|
return (state as { __archyModal?: number } | null)?.__archyModal ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureListener() {
|
||||||
|
if (listening || typeof window === 'undefined') return
|
||||||
|
listening = true
|
||||||
|
window.addEventListener('popstate', (e) => {
|
||||||
|
// Landed at a depth below the open-modal count → this Back was aimed
|
||||||
|
// at the topmost modal. One entry per Back press: close exactly one.
|
||||||
|
// (A popstate at or above our depth is someone else's navigation —
|
||||||
|
// e.g. our own cleanup back, or a forward — leave it alone.)
|
||||||
|
if (modalDepth(e.state) < stack.length) {
|
||||||
|
const top = stack[stack.length - 1]
|
||||||
|
if (top) {
|
||||||
|
poppedClose = true
|
||||||
|
top.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Call from a modal component with its visibility and close trigger. */
|
||||||
|
export function useModalHistory(show: Ref<boolean>, close: () => void) {
|
||||||
|
ensureListener()
|
||||||
|
const entry: Entry = { close }
|
||||||
|
watch(show, (open, was) => {
|
||||||
|
if (open === was) return
|
||||||
|
if (open) {
|
||||||
|
stack.push(entry)
|
||||||
|
try {
|
||||||
|
// Preserve vue-router's own keys in state — clobbering them breaks
|
||||||
|
// its scroll restoration and position tracking.
|
||||||
|
window.history.pushState(
|
||||||
|
{ ...(window.history.state ?? {}), __archyModal: stack.length },
|
||||||
|
'',
|
||||||
|
)
|
||||||
|
} catch { /* history can throw in exotic embeds — modal still works */ }
|
||||||
|
} else {
|
||||||
|
const wasTop = stack[stack.length - 1] === entry
|
||||||
|
const i = stack.indexOf(entry)
|
||||||
|
if (i >= 0) stack.splice(i, 1)
|
||||||
|
if (poppedClose) {
|
||||||
|
poppedClose = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// UI-side close of the top modal: consume the entry we pushed, but
|
||||||
|
// only if it is still the current one (a route change after opening
|
||||||
|
// moves history past it — backing out then would eat a real entry).
|
||||||
|
if (wasTop && modalDepth(window.history.state) > stack.length) {
|
||||||
|
try { window.history.back() } catch { /* same guard as above */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1128,6 +1128,60 @@ html.controller-nav [data-controller-container]:focus {
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* KAMMERGUT GLOSS v3 TEST (2026-08-14, UNCOMMITTED until approved) —
|
||||||
|
wet black paint, PURE CSS (the SVG text filter embosses badly on a
|
||||||
|
disc; headless A/B renders picked this). Borderless per operator:
|
||||||
|
the painted disc IS the badge. Layers: warm ambient top sheen, wet
|
||||||
|
lower reflection arc, bottom depth vignette, paint-black base; inset
|
||||||
|
rim lights; and a curved liquid "window streak" on ::before (under
|
||||||
|
the badge's z-1 content, above the paint). Revert: delete this block. */
|
||||||
|
.logo-gradient-border.logo-gloss,
|
||||||
|
.tap-to-start-logo .logo-gradient-border {
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.logo-gradient-border.logo-gloss::after,
|
||||||
|
.tap-to-start-logo .logo-gradient-border::after {
|
||||||
|
inset: 0;
|
||||||
|
/* Dense intermediate stops + a fine turbulence grain (top layer,
|
||||||
|
overlay-blended) dither the near-black falloffs — without them the
|
||||||
|
big disc shows visible banding rings. */
|
||||||
|
background:
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E"),
|
||||||
|
radial-gradient(115% 60% at 50% -10%, rgba(255,244,220,0.22) 0%, rgba(255,244,220,0.14) 18%, rgba(255,244,220,0.08) 32%, rgba(255,244,220,0.04) 43%, rgba(255,244,220,0.015) 52%, rgba(255,244,220,0) 62%),
|
||||||
|
radial-gradient(90% 34% at 50% 108%, rgba(255,238,200,0.12) 0%, rgba(255,238,200,0.07) 30%, rgba(255,238,200,0.03) 48%, rgba(255,238,200,0) 68%),
|
||||||
|
radial-gradient(140% 120% at 50% 135%, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.62) 22%, rgba(0,0,0,0.36) 36%, rgba(0,0,0,0.16) 46%, rgba(0,0,0,0.05) 52%, rgba(0,0,0,0) 58%),
|
||||||
|
linear-gradient(180deg, #26261f 0%, #20201b 22%, #141412 44%, #0b0b09 68%, #040403 100%);
|
||||||
|
background-blend-mode: overlay, normal, normal, normal, normal;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 2px 2px rgba(255,246,224,0.42),
|
||||||
|
inset 0 -1px 1px rgba(255,238,200,0.16),
|
||||||
|
inset 0 -10px 22px rgba(0,0,0,0.82),
|
||||||
|
inset 0 0 0 1px rgba(255,242,212,0.08),
|
||||||
|
0 12px 34px rgba(0,0,0,0.65);
|
||||||
|
}
|
||||||
|
.logo-gradient-border.logo-gloss::before,
|
||||||
|
.tap-to-start-logo .logo-gradient-border::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 8%;
|
||||||
|
right: 8%;
|
||||||
|
top: 3%;
|
||||||
|
height: 46%;
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: 50% / 62%;
|
||||||
|
/* Radial (2D) falloff, not linear: a linear streak is equally bright
|
||||||
|
across its full width, so its left/right TIPS end in a hard seam
|
||||||
|
against the rim (operator screenshot, 2026-08-14). The ellipse
|
||||||
|
fades toward every edge — no tips, no seam, and the lower edge
|
||||||
|
dissolves long before the clipped shape ends. */
|
||||||
|
background: radial-gradient(75% 105% at 50% -6%, rgba(255,250,236,0.55) 0%, rgba(255,246,224,0.30) 34%, rgba(255,246,224,0.12) 55%, rgba(255,244,220,0.04) 72%, rgba(255,244,220,0) 86%);
|
||||||
|
filter: blur(2px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.logo-gradient-border img,
|
.logo-gradient-border img,
|
||||||
.logo-gradient-border svg {
|
.logo-gradient-border svg {
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Auto-tab memory — apps observed to refuse iframe embedding.
|
||||||
|
*
|
||||||
|
* The gate strips frame-blocking headers for gated apps (1.8.1+), but three
|
||||||
|
* failure modes survive any header fix: JS frame-busting, apps that must be
|
||||||
|
* the top-level origin (OAuth/WebAuthn), and SameSite=Strict session cookies.
|
||||||
|
* Cross-origin embed failure is not reliably detectable up front from the
|
||||||
|
* browser, so the app session's load-timeout is the detector — and this
|
||||||
|
* module is the memory: once an app is seen blocked while RUNNING, it is
|
||||||
|
* remembered here and every later launch opens it as a tab app directly from
|
||||||
|
* the click (a user gesture, so never popup-blocked). The dead grey pane
|
||||||
|
* happens at most once per app.
|
||||||
|
*
|
||||||
|
* Entries expire after 7 days so a node update that fixes embedding (for
|
||||||
|
* example a gate improvement) gets re-probed instead of being remembered as
|
||||||
|
* broken forever; a successful iframe load also clears the entry immediately.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const KEY = 'archipelago_auto_new_tab_apps'
|
||||||
|
const EXPIRY_MS = 7 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
function read(): Record<string, number> {
|
||||||
|
try {
|
||||||
|
const raw = JSON.parse(localStorage.getItem(KEY) || '{}')
|
||||||
|
if (raw && typeof raw === 'object' && !Array.isArray(raw)) return raw
|
||||||
|
} catch { /* corrupt/unavailable storage reads as empty */ }
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function write(entries: Record<string, number>) {
|
||||||
|
try { localStorage.setItem(KEY, JSON.stringify(entries)) } catch { /* full/denied */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAutoTabApp(id: string): boolean {
|
||||||
|
const at = read()[id]
|
||||||
|
return typeof at === 'number' && Date.now() - at < EXPIRY_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rememberAutoTabApp(id: string): void {
|
||||||
|
const entries = read()
|
||||||
|
entries[id] = Date.now()
|
||||||
|
write(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forgetAutoTabApp(id: string): void {
|
||||||
|
const entries = read()
|
||||||
|
if (id in entries) {
|
||||||
|
delete entries[id]
|
||||||
|
write(entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,6 +105,7 @@ import { useAppLauncherStore } from '@/stores/appLauncher'
|
|||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useScreensaverStore } from '@/stores/screensaver'
|
import { useScreensaverStore } from '@/stores/screensaver'
|
||||||
import NostrIdentityPicker from '@/components/NostrIdentityPicker.vue'
|
import NostrIdentityPicker from '@/components/NostrIdentityPicker.vue'
|
||||||
|
import { isAutoTabApp, rememberAutoTabApp, forgetAutoTabApp } from '@/utils/autoTabApps'
|
||||||
import AppSessionHeader from './appSession/AppSessionHeader.vue'
|
import AppSessionHeader from './appSession/AppSessionHeader.vue'
|
||||||
import AppSessionFrame from './appSession/AppSessionFrame.vue'
|
import AppSessionFrame from './appSession/AppSessionFrame.vue'
|
||||||
import MobileGamepad from './appSession/MobileGamepad.vue'
|
import MobileGamepad from './appSession/MobileGamepad.vue'
|
||||||
@@ -199,9 +200,27 @@ function updateIsMobile() { isMobile.value = window.innerWidth < 768 }
|
|||||||
// same-origin by the mock backend, so the prod new-tab list doesn't apply.
|
// same-origin by the mock backend, so the prod new-tab list doesn't apply.
|
||||||
const mustOpenNewTab = computed(() =>
|
const mustOpenNewTab = computed(() =>
|
||||||
(NEW_TAB_APPS.has(appId.value) && !(IS_DEMO && isDemoApp(appId.value))) ||
|
(NEW_TAB_APPS.has(appId.value) && !(IS_DEMO && isDemoApp(appId.value))) ||
|
||||||
|
// Remembered embed-refusers (frame-busting, top-level-origin apps): the
|
||||||
|
// first blocked encounter records them, every later launch is a tab app.
|
||||||
|
isAutoTabApp(appId.value) ||
|
||||||
(IS_DEMO && isDemoExternal(appId.value))
|
(IS_DEMO && isDemoExternal(appId.value))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The auto-tab detector: the load-timeout marked the frame blocked, the
|
||||||
|
// warming-up retry loop has given up, and the backend says the app is
|
||||||
|
// actually RUNNING — that combination is the embed-refusal signature (a
|
||||||
|
// down app is "warming up" or shows a blocked reason instead). Remember it
|
||||||
|
// so this app never shows the dead pane again; a later successful iframe
|
||||||
|
// load (onLoad) clears the memory, and entries expire on their own.
|
||||||
|
watch([iframeBlocked, warmingUp], ([blocked, warming]) => {
|
||||||
|
if (
|
||||||
|
blocked && !warming && !blockedReason.value &&
|
||||||
|
(packageEntry.value?.state === PackageState.Running)
|
||||||
|
) {
|
||||||
|
rememberAutoTabApp(appId.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// ElectrumX shows a sync screen before its real UI (the Electrum server only
|
// ElectrumX shows a sync screen before its real UI (the Electrum server only
|
||||||
// serves clients once its index is built). Poll /electrs-status while this is
|
// serves clients once its index is built). Poll /electrs-status while this is
|
||||||
// the Electrum app; pass the status to the frame only while still syncing.
|
// the Electrum app; pass the status to the frame only while still syncing.
|
||||||
@@ -344,6 +363,10 @@ function onLoad() {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
isRefreshing.value = false
|
isRefreshing.value = false
|
||||||
autoRetryCount.value = 0
|
autoRetryCount.value = 0
|
||||||
|
// A frame that loads embeds fine — heal any stale auto-tab memory (for
|
||||||
|
// example an app remembered as blocked before the gate learned to strip
|
||||||
|
// frame headers).
|
||||||
|
forgetAutoTabApp(appId.value)
|
||||||
// TV/keyboard: hand focus to the app so keys (incl. the gamepad bridge's
|
// TV/keyboard: hand focus to the app so keys (incl. the gamepad bridge's
|
||||||
// virtual keyboard) flow into the iframe without needing a pointer click.
|
// virtual keyboard) flow into the iframe without needing a pointer click.
|
||||||
try { frameRef.value?.iframeRef?.focus() } catch { /* cross-origin is fine */ }
|
try { frameRef.value?.iframeRef?.focus() } catch { /* cross-origin is fine */ }
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
>
|
>
|
||||||
<!-- Logo - half in, half out of container -->
|
<!-- Logo - half in, half out of container -->
|
||||||
<div class="absolute -top-10 left-1/2 -translate-x-1/2 z-10">
|
<div class="absolute -top-10 left-1/2 -translate-x-1/2 z-10">
|
||||||
<div class="logo-gradient-border w-20 h-20">
|
<div class="logo-gradient-border logo-gloss w-20 h-20">
|
||||||
<AnimatedLogo no-border fit />
|
<AnimatedLogo no-border fit />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
v-if="app.icon"
|
v-if="app.icon"
|
||||||
:src="app.icon"
|
:src="app.icon"
|
||||||
:alt="app.title"
|
:alt="app.title"
|
||||||
class="w-20 h-20 rounded-xl shadow-xl flex-shrink-0"
|
class="w-20 h-20 rounded-xl shadow-xl flex-shrink-0 archy-app-icon"
|
||||||
@error="handleImageError"
|
@error="handleImageError"
|
||||||
/>
|
/>
|
||||||
<div v-else class="w-20 h-20 rounded-xl bg-white/10 flex items-center justify-center flex-shrink-0">
|
<div v-else class="w-20 h-20 rounded-xl bg-white/10 flex items-center justify-center flex-shrink-0">
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
v-if="app.icon"
|
v-if="app.icon"
|
||||||
:src="app.icon"
|
:src="app.icon"
|
||||||
:alt="app.title"
|
:alt="app.title"
|
||||||
class="w-20 h-20 rounded-xl shadow-xl"
|
class="w-20 h-20 rounded-xl shadow-xl archy-app-icon"
|
||||||
@error="handleImageError"
|
@error="handleImageError"
|
||||||
/>
|
/>
|
||||||
<div v-else class="w-20 h-20 rounded-xl bg-white/10 flex items-center justify-center">
|
<div v-else class="w-20 h-20 rounded-xl bg-white/10 flex items-center justify-center">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="glass-card p-8 pt-16 sm:p-12 sm:pt-20 text-center relative overflow-visible onb-card">
|
<div class="glass-card p-8 pt-16 sm:p-12 sm:pt-20 text-center relative overflow-visible onb-card">
|
||||||
<!-- Logo - half in, half out of container -->
|
<!-- Logo - half in, half out of container -->
|
||||||
<div class="absolute -top-8 sm:-top-10 left-0 right-0 flex justify-center z-10 onb-logo">
|
<div class="absolute -top-8 sm:-top-10 left-0 right-0 flex justify-center z-10 onb-logo">
|
||||||
<div class="logo-gradient-border w-16 h-16 sm:w-20 sm:h-20">
|
<div class="logo-gradient-border logo-gloss w-16 h-16 sm:w-20 sm:h-20">
|
||||||
<AnimatedLogo no-border fit />
|
<AnimatedLogo no-border fit />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1087,7 +1087,18 @@ async function applyUpdate() {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
showStatus(t('systemUpdate.applyFailed'), true)
|
// Surface the backend's actual, actionable message when it gave one
|
||||||
|
// ("Update download was incomplete — download again", etc.) instead of a
|
||||||
|
// generic dead end. The staged files are preserved across a failed apply,
|
||||||
|
// so re-download stays available and a retry is always possible.
|
||||||
|
const detail = errorMessage(e)
|
||||||
|
const actionable = /^Update\b/.test(detail)
|
||||||
|
showStatus(actionable ? detail : t('systemUpdate.applyFailed'), true)
|
||||||
|
// A staging inconsistency means the download is the thing to redo — drop
|
||||||
|
// the downloaded flag so the button offers Download again, not Apply.
|
||||||
|
if (/incomplete|not staged|re-download|inconsistent/i.test(detail)) {
|
||||||
|
downloaded.value = false
|
||||||
|
}
|
||||||
if (import.meta.env.DEV) console.warn('Apply failed', e)
|
if (import.meta.env.DEV) console.warn('Apply failed', e)
|
||||||
applying.value = false
|
applying.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { Ref } from 'vue'
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { PackageState, type PackageDataEntry } from '@/types/api'
|
import { PackageState, type PackageDataEntry } from '@/types/api'
|
||||||
import { resolveAppUrl } from '../appSession/appSessionConfig'
|
import { resolveAppUrl } from '../appSession/appSessionConfig'
|
||||||
|
import { isAutoTabApp } from '@/utils/autoTabApps'
|
||||||
|
|
||||||
export type AppsTab = 'apps' | 'websites' | 'services'
|
export type AppsTab = 'apps' | 'websites' | 'services'
|
||||||
|
|
||||||
@@ -173,7 +174,10 @@ export const TAB_LAUNCH_APPS = new Set([
|
|||||||
])
|
])
|
||||||
|
|
||||||
export function opensInTab(id: string): boolean {
|
export function opensInTab(id: string): boolean {
|
||||||
return TAB_LAUNCH_APPS.has(id)
|
// The launch icon and launch behavior follow the dynamic verdict too:
|
||||||
|
// apps observed refusing the iframe (remembered by the app session) show
|
||||||
|
// the same tab-launch affordance as the statically-known tab apps.
|
||||||
|
return TAB_LAUNCH_APPS.has(id) || isAutoTabApp(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backend services that ship no icon of their own reuse their PARENT app's icon
|
// Backend services that ship no icon of their own reuse their PARENT app's icon
|
||||||
|
|||||||
@@ -362,6 +362,58 @@ init()
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||||
|
<!-- v1.8.3-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.8.3-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">August 14, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>**The network map on TVs: no more blank page, no more frozen page — and it moves again.** The map's entrance animation needed a smoothness that TV kiosk hardware can't always deliver, so the page could sit blank until a refresh; the previous fix cured the freeze by stopping the animation entirely, which went too far. Now the map appears instantly with everything already in place, then resumes its calm orbital motion at a gentler pace suited to TVs. Resizing or rotating any screen also redraws the map properly instead of leaving it tiny, stretched, or empty.</p>
|
||||||
|
<p>**The dashboard's corner logo is back to normal.** The new glossy paint finish was meant for the big emblem on the screensaver, intro, and login screens — it had quietly spread to the small logo in the dashboard header, where it looked wrong. Each screen now gets exactly the treatment intended for it.</p>
|
||||||
|
<p>**App icons no longer vanish in My Apps.** The freshly restyled Alby Hub and phoenixd icons could render as blank squares in some views — a subtlety in how the icon files declared their size. Fixed at the source, and the icon tool app developers use now produces immune files.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- v1.8.2-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.8.2-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">August 13, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>**An app that can't be shown inside the dashboard now becomes a tab app by itself.** A few apps refuse to render inside another page no matter what — they break out with their own code or insist on owning the whole browser window. Opening one used to mean staring at a grey pane. Now the dashboard notices, offers the app in its own tab, and remembers: from then on that app's button opens a tab directly (with the little launch icon that tab apps carry), first click, every time. If a later update makes the app embeddable after all, the dashboard notices that too and goes back to embedding it.</p>
|
||||||
|
<p>**The logo emblem got its glossy black paint finish — properly this time.** The circle behind the A on the screensaver, intro, and login now wears a deep wet-paint look: warm light blooming from the top edge, fine grain so the dark tones stay smooth instead of banding, and no more ring border. (An earlier rougher version of this experiment briefly shipped by accident and then vanished depending on which screen you were on — this is the finished, deliberate one, everywhere.)</p>
|
||||||
|
<p>**New app icons now match the store's look, on every screen.** Alby Hub and phoenixd arrived with edge-to-edge logos that ignored the breathing room every other app icon has, and the app detail page skipped the icon backdrop entirely. Both icons are re-set on the standard canvas, the detail page now applies the same icon treatment as the store tiles, and app developers get a one-command tool that puts any logo onto the house canvas automatically.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- v1.8.1-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.8.1-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">August 13, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>**Apps that refused to open inside the dashboard now embed like everything else.** Some apps ship browser headers that forbid being shown inside another page — correct hardening on the open web, but inside Archipelago it produced a dead grey pane when you opened them from My Apps (Alby Hub was the first to hit it). The app gate, which already checks your login on every request to an app, now removes just those framing headers on the way through; each app's own content-security rules pass through untouched. No more per-app proxy workarounds.</p>
|
||||||
|
<p>**The network map no longer freezes kiosk TVs.** The animated federation map at 4K was too much for the deliberately conservative graphics settings the on-screen display used on every machine — settings chosen years back to stop audio crackle on much older hardware. Two fixes: on kiosk screens the map now opens in its flat 2D view (the 3D globe is one tap away, and remembered) and animates at half rate — invisible from the couch, half the work. And the display itself now recognizes what machine it runs on: older kiosk boxes keep the proven careful settings, modern ones finally get real GPU rendering.</p>
|
||||||
|
<p>**New Settings → Display → Graphics choice for the on-screen display.** Auto (recommended) picks the right rendering mode for the machine by itself; Compatibility forces the most conservative mode if a screen ever stutters, tears, or crackles; Quality forces full GPU rendering on hardware the automatic detection doesn't recognize. Changing it restarts the on-screen display, like the size presets.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- v1.8.0-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.8.0-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">August 12, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>**Archipelago is now open source.** The full source code of the node you are running — the orchestrator, the dashboard, the app platform, the mesh, the release tooling — is published for anyone to read, build and audit at source.archipelago-foundation.org/lfg2025/archy. A node that holds your money, your files and your communications should not ask to be taken on faith: from this release onward you, or anyone you trust, can see exactly what it does and follow every change we make in the open.</p>
|
||||||
|
<p>**Installing an update is reliable again, and tells you what happened when it isn't.** Some nodes could download an update but never apply it — the button stayed on "Install", and no amount of retrying worked. The cause: applying the update consumed the downloaded files as it went, so if any one step hit a snag partway through, the leftover files were incomplete and every later attempt failed the safety re-check forever, needing a technician to recover. Applying no longer consumes the download — a failed apply can always be retried from the same files — and the pieces are now applied in a fixed order with the program itself last, so a hiccup can't leave a half-swapped node. When an apply does fail, the screen now shows the real reason and what to do ("download the update again"), and offers Download again instead of a dead "Install" button, rather than a generic "it failed".</p>
|
||||||
|
<p>**Video on the kiosk stops tearing.** The kiosk's display had no vertical sync at all, so fast motion — IndeedHub films especially — showed horizontal tearing lines. The display driver now syncs every frame to the panel (no extra hardware needed, existing kiosks pick it up with this update), and on machines with a GPU, video decoding moves off the CPU onto the video hardware — smoother playback that also leaves more headroom for audio, not less.</p>
|
||||||
|
<p>**The Back button finally does what you expect.** Pressing Back — the mouse's side button on a kiosk, a swipe on a phone, the toolbar button in any browser — used to navigate the screen underneath an open window, or leave the dashboard entirely. Back now closes the topmost open window first, one per press, exactly like a native app; closing a window yourself never leaves a phantom entry that makes you press Back twice.</p>
|
||||||
|
<p>**No more bare IP addresses in your update or app-registry settings.** The update mirrors and the app registry each listed the same server twice — once by its proper name, once as a raw http://146… address left over from before the domain existed. The raw-address entries are retired: new nodes never see them, and existing nodes clean them out of their saved lists automatically on the next read. Everything now goes through the named, TLS-protected origin — which was always the same machine.</p>
|
||||||
|
<p>**The phone companion app downloads over the proper domain.** The download QR pointed at a raw address over plain HTTP; it now points at the same file on the https domain. Scanning it gets you an encrypted download from a named server.</p>
|
||||||
|
<p>**The Receive window now tells you when the money is on its way.** Previously it showed a QR code and left you to check elsewhere whether anything happened. Now, the moment the sender's transaction is broadcast, the QR gives way to a clock: the amount, the transaction ID (tap to copy), and a note that the funds arrive on their own — with a single Done button. If you keep the window open, the clock becomes a green check at the first confirmation. Verified live on a real node: payment detected within seconds of broadcast.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- v1.7.129-alpha -->
|
<!-- v1.7.129-alpha -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { rpcClient } from '@/api/rpc-client'
|
|||||||
// have a kiosk display (has_kiosk from the backend).
|
// have a kiosk display (has_kiosk from the backend).
|
||||||
const hasKiosk = ref(false)
|
const hasKiosk = ref(false)
|
||||||
const preset = ref('auto')
|
const preset = ref('auto')
|
||||||
|
const graphics = ref('auto')
|
||||||
const applying = ref(false)
|
const applying = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|
||||||
@@ -32,11 +33,35 @@ const presets = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Graphics tier: which browser rendering flags the on-screen display runs
|
||||||
|
// with. Auto classifies the hardware (older kiosk boxes keep the proven
|
||||||
|
// conservative flags; modern chips get GPU rendering); the two overrides
|
||||||
|
// exist for troubleshooting and unclassified hardware.
|
||||||
|
const graphicsModes = [
|
||||||
|
{
|
||||||
|
id: 'auto',
|
||||||
|
label: 'Auto (recommended)',
|
||||||
|
description: 'Detect this machine’s graphics hardware and pick the right rendering mode for it.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'performance',
|
||||||
|
label: 'Compatibility',
|
||||||
|
description: 'Most conservative rendering — use if the screen stutters, tears, or the audio crackles.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quality',
|
||||||
|
label: 'Quality',
|
||||||
|
description: 'Full GPU rendering — smoothest animations on capable hardware. If unsure, use Auto.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await rpcClient.call<{ has_kiosk: boolean; preset: string }>({ method: 'system.kiosk-display.get' })
|
const res = await rpcClient.call<{ has_kiosk: boolean; preset: string; graphics?: string }>({ method: 'system.kiosk-display.get' })
|
||||||
hasKiosk.value = res.has_kiosk
|
hasKiosk.value = res.has_kiosk
|
||||||
preset.value = res.preset
|
preset.value = res.preset
|
||||||
|
// Older backend without the graphics field: hide nothing, default Auto.
|
||||||
|
graphics.value = res.graphics ?? 'auto'
|
||||||
} catch { /* backend without the RPC — leave the section hidden */ }
|
} catch { /* backend without the RPC — leave the section hidden */ }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -55,6 +80,22 @@ async function apply(id: string) {
|
|||||||
applying.value = false
|
applying.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyGraphics(id: string) {
|
||||||
|
if (applying.value || id === graphics.value) return
|
||||||
|
applying.value = true
|
||||||
|
error.value = ''
|
||||||
|
const prev = graphics.value
|
||||||
|
graphics.value = id
|
||||||
|
try {
|
||||||
|
await rpcClient.call({ method: 'system.kiosk-display.set', params: { graphics: id }, timeout: 20000 })
|
||||||
|
} catch (e: unknown) {
|
||||||
|
graphics.value = prev
|
||||||
|
error.value = e instanceof Error ? e.message : 'Failed to apply graphics setting'
|
||||||
|
} finally {
|
||||||
|
applying.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -77,6 +118,25 @@ async function apply(id: string) {
|
|||||||
<p class="text-sm text-white/60">{{ p.description }}</p>
|
<p class="text-sm text-white/60">{{ p.description }}</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-semibold text-white/96 mt-8 mb-2">Graphics</h3>
|
||||||
|
<p class="text-sm text-white/60 mb-6">
|
||||||
|
How the on-screen display uses this machine’s graphics hardware. Changing this restarts the on-screen display.
|
||||||
|
</p>
|
||||||
|
<div data-controller-container tabindex="0" class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<button
|
||||||
|
v-for="g in graphicsModes"
|
||||||
|
:key="g.id"
|
||||||
|
:disabled="applying"
|
||||||
|
@click="applyGraphics(g.id)"
|
||||||
|
class="path-option-card text-left p-5 disabled:opacity-60"
|
||||||
|
:class="{ 'path-option-card--selected': graphics === g.id }"
|
||||||
|
>
|
||||||
|
<div class="font-medium text-white/90 mb-1">{{ g.label }}</div>
|
||||||
|
<p class="text-sm text-white/60">{{ g.description }}</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="mt-4 alert-error text-sm">{{ error }}</div>
|
<div v-if="error" class="mt-4 alert-error text-sm">{{ error }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+17
-20
@@ -1,32 +1,29 @@
|
|||||||
{
|
{
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"**Every app is now supervised the same way — the last stragglers moved under systemd.** Five apps (Jellyfin, Nextcloud, Home Assistant, Uptime Kuma, Vaultwarden) still ran outside the node's per-app service management for a technical reason: their networking style died with whatever process started it, so they were kept alive by a separate workaround. That workaround is retired: these apps now migrate themselves onto the same managed units as everything else — own service, restart-on-anything, a ten-second breather between restarts so their networking can release its ports cleanly. The migration happens automatically on the node's next housekeeping pass, touches no app data, and was watched live on a real node: both test apps moved over on the first pass and came back healthy.",
|
"**The network map on TVs: no more blank page, no more frozen page — and it moves again.** The map's entrance animation needed a smoothness that TV kiosk hardware can't always deliver, so the page could sit blank until a refresh; the previous fix cured the freeze by stopping the animation entirely, which went too far. Now the map appears instantly with everything already in place, then resumes its calm orbital motion at a gentler pace suited to TVs. Resizing or rotating any screen also redraws the map properly instead of leaving it tiny, stretched, or empty.",
|
||||||
"**Leftover companion screens are cleaned up again — driven by real records this time.** When an app is uninstalled, its helper screen (the UI tile that fronts it) should go too. That cleanup was switched off in an earlier release after it wrongly removed the Bitcoin screen from a node whose Bitcoin was installed — it had been guessing \"installed\" from what happened to be running, and a separate bug made a running app look absent. The node now keeps a durable record of what you have installed, written at install time and cleared only by a real uninstall, and the cleanup consults only that record. If the record can't be read, the cleanup does nothing at all — \"I couldn't check\" is never treated as \"nothing is installed\" — and a helper must be orphaned for a sustained period before it is touched.",
|
"**The dashboard's corner logo is back to normal.** The new glossy paint finish was meant for the big emblem on the screensaver, intro, and login screens — it had quietly spread to the small logo in the dashboard header, where it looked wrong. Each screen now gets exactly the treatment intended for it.",
|
||||||
"**A warning that fired every minute on every node is gone.** The app catalog and the node disagreed about where Grafana's software comes from, so the node ignored the catalog's answer and logged a complaint roughly every 75 seconds, forever. The catalog was right — Grafana is served from the fleet's own registry, like Bitcoin Knots — and the node's records now agree with it.",
|
"**App icons no longer vanish in My Apps.** The freshly restyled Alby Hub and phoenixd icons could render as blank squares in some views — a subtlety in how the icon files declared their size. Fixed at the source, and the icon tool app developers use now produces immune files."
|
||||||
"**The federation map became a real map.** The network view is now a 3D orbital scene of your federation — nodes as a point-cloud globe with calm motion, auto-fit centring, and a 2D top-down toggle that portrait and mobile screens use by default, with the scene filling the viewport instead of sitting in a letterbox. Inbound peer requests appear live on the map as blinking nodes you can accept or reject in place, and revisiting the view no longer replays the whole intro — the scene updates in place.",
|
|
||||||
"**An app that's mid-restart shows a page that says so — and comes back by itself.** When an app's screen was briefly unreachable behind the gate, the browser got a bare error; it now gets a named page for that app that retries on its own until the app answers.",
|
|
||||||
"Known gaps, disclosed rather than buried: three voice-assistant ports remain open without authentication. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release; the supervision migration and the cleanup re-enable were verified live on one node (both apps migrated and healthy, cleanup correctly idle)."
|
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"current_version": "1.7.129-alpha",
|
"current_version": "1.8.3-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.7.129-alpha/archipelago",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.3-alpha/archipelago",
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"new_version": "1.7.129-alpha",
|
"new_version": "1.8.3-alpha",
|
||||||
"sha256": "675e7dafc855d59b38c5a12d8b9405894677ed8701580227beca95ec2912e3f2",
|
"sha256": "23b608bfce575212edb873ded3e125505f42be0db6dc06ce5f9c1e51c232c22d",
|
||||||
"size_bytes": 59531400
|
"size_bytes": 59900696
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current_version": "1.7.129-alpha",
|
"current_version": "1.8.3-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.7.129-alpha/archipelago-frontend-1.7.129-alpha.tar.gz",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.3-alpha/archipelago-frontend-1.8.3-alpha.tar.gz",
|
||||||
"name": "archipelago-frontend-1.7.129-alpha.tar.gz",
|
"name": "archipelago-frontend-1.8.3-alpha.tar.gz",
|
||||||
"new_version": "1.7.129-alpha",
|
"new_version": "1.8.3-alpha",
|
||||||
"sha256": "53af2743308f4ae6a627255aaa288706d331d567c99e1cb615684dc3abba534d",
|
"sha256": "13ca772e9a36c266b67e6eff2d51366616cdbedeab3a05ba5eba340332d3d6d4",
|
||||||
"size_bytes": 95452033
|
"size_bytes": 97615528
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"release_date": "2026-08-10",
|
"release_date": "2026-08-14",
|
||||||
"signature": "902778710674486d5919e4abb1bf5540521c9ef55b50a44a9d64b750812738d51cc193f6675a9969b1c45ae86c2e9e44ab3d8daf8aa1d439799d7f7846e27d04",
|
"signature": "35d5f56ef77cda1866051a3ff06f6e09ff0a23ac7b000a1ac4cf24e04cecabff00be7f3e240fc82ec605bff80c5690c55c296a300018562940d71ffc5df26c00",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"version": "1.7.129-alpha"
|
"version": "1.8.3-alpha"
|
||||||
}
|
}
|
||||||
|
|||||||
+258
-97
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"apps": {
|
"apps": {
|
||||||
"adguardhome": {
|
"adguardhome": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/adguardhome:v0.107.55",
|
"image": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.55",
|
||||||
"version": "v0.107.55"
|
"version": "v0.107.55"
|
||||||
},
|
},
|
||||||
"aiui": {
|
"aiui": {
|
||||||
@@ -51,6 +51,93 @@
|
|||||||
},
|
},
|
||||||
"version": "0.1.0"
|
"version": "0.1.0"
|
||||||
},
|
},
|
||||||
|
"alby-hub": {
|
||||||
|
"manifest": {
|
||||||
|
"app": {
|
||||||
|
"category": "money",
|
||||||
|
"container": {
|
||||||
|
"image": "source.archipelago-foundation.org/lfg2025/alby-hub:v1.23.0",
|
||||||
|
"pull_policy": "if-not-present"
|
||||||
|
},
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"storage": "1Gi"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Self-custodial Lightning wallet hub. Runs its own Lightning node on your Archipelago and connects your apps to it over Nostr Wallet Connect — one hub, every app pays through it.",
|
||||||
|
"environment": [
|
||||||
|
"WORK_DIR=/data",
|
||||||
|
"PORT=8080",
|
||||||
|
"LOG_LEVEL=info"
|
||||||
|
],
|
||||||
|
"health_check": {
|
||||||
|
"endpoint": "http://localhost:8080",
|
||||||
|
"interval": "30s",
|
||||||
|
"path": "/",
|
||||||
|
"retries": 5,
|
||||||
|
"timeout": "5s",
|
||||||
|
"type": "http"
|
||||||
|
},
|
||||||
|
"id": "alby-hub",
|
||||||
|
"interfaces": {
|
||||||
|
"main": {
|
||||||
|
"description": "Alby Hub wallet interface",
|
||||||
|
"name": "Web UI",
|
||||||
|
"port": 8187,
|
||||||
|
"protocol": "http",
|
||||||
|
"type": "ui"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"features": [
|
||||||
|
"Self-custodial Lightning node (LDK) with a friendly wallet UI",
|
||||||
|
"Connect wallets and apps via Nostr Wallet Connect (NWC)",
|
||||||
|
"Per-app budgets and isolated sub-wallets",
|
||||||
|
"Works with the Alby browser extension and mobile app"
|
||||||
|
],
|
||||||
|
"icon": "/assets/img/app-icons/alby-hub.svg",
|
||||||
|
"launch": {
|
||||||
|
"open_in_new_tab": false
|
||||||
|
},
|
||||||
|
"repo": "https://github.com/getAlby/hub",
|
||||||
|
"tier": "optional"
|
||||||
|
},
|
||||||
|
"name": "Alby Hub",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"auth": "gated",
|
||||||
|
"bind": "127.0.0.1",
|
||||||
|
"container": 8080,
|
||||||
|
"host": 8187,
|
||||||
|
"protocol": "tcp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"cpu_limit": 1,
|
||||||
|
"disk_limit": "2Gi",
|
||||||
|
"memory_limit": "512Mi"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"capabilities": [],
|
||||||
|
"network_policy": "bridge",
|
||||||
|
"no_new_privileges": true,
|
||||||
|
"readonly_root": true
|
||||||
|
},
|
||||||
|
"version": "1.23.0",
|
||||||
|
"volumes": [
|
||||||
|
{
|
||||||
|
"options": [
|
||||||
|
"rw"
|
||||||
|
],
|
||||||
|
"source": "/var/lib/archipelago/alby-hub",
|
||||||
|
"target": "/data",
|
||||||
|
"type": "bind"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": "1.23.0"
|
||||||
|
},
|
||||||
"archy-btcpay-db": {
|
"archy-btcpay-db": {
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
@@ -60,7 +147,7 @@
|
|||||||
},
|
},
|
||||||
"container": {
|
"container": {
|
||||||
"data_uid": "100998:100998",
|
"data_uid": "100998:100998",
|
||||||
"image": "146.59.87.168:3000/lfg2025/postgres:15.17",
|
"image": "source.archipelago-foundation.org/lfg2025/postgres:15.17",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -129,7 +216,7 @@
|
|||||||
},
|
},
|
||||||
"container": {
|
"container": {
|
||||||
"data_uid": "100998:100998",
|
"data_uid": "100998:100998",
|
||||||
"image": "146.59.87.168:3000/lfg2025/mariadb:11.4.10",
|
"image": "source.archipelago-foundation.org/lfg2025/mariadb:11.4.10",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -201,7 +288,7 @@
|
|||||||
"sync_required": false
|
"sync_required": false
|
||||||
},
|
},
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.1",
|
"image": "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.0.1",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -257,7 +344,7 @@
|
|||||||
"sync_required": true
|
"sync_required": true
|
||||||
},
|
},
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/nbxplorer:2.6.0",
|
"image": "source.archipelago-foundation.org/lfg2025/nbxplorer:2.6.0",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -347,7 +434,7 @@
|
|||||||
"name": "barkd-secret"
|
"name": "barkd-secret"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/barkd:0.3.0",
|
"image": "source.archipelago-foundation.org/lfg2025/barkd:0.3.0",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -434,7 +521,7 @@
|
|||||||
"sh",
|
"sh",
|
||||||
"-lc"
|
"-lc"
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin:28.4",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin:28.4",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -516,47 +603,47 @@
|
|||||||
"versions": [
|
"versions": [
|
||||||
{
|
{
|
||||||
"default": true,
|
"default": true,
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin:latest",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin:latest",
|
||||||
"version": "latest"
|
"version": "latest"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin:31.0",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin:31.0",
|
||||||
"version": "31.0"
|
"version": "31.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin:30.2",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin:30.2",
|
||||||
"version": "30.2"
|
"version": "30.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin:29.3",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin:29.3",
|
||||||
"version": "29.3"
|
"version": "29.3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin:29.2",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin:29.2",
|
||||||
"version": "29.2"
|
"version": "29.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin:28.4",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin:28.4",
|
||||||
"version": "28.4.0"
|
"version": "28.4.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin:27.2",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin:27.2",
|
||||||
"version": "27.2"
|
"version": "27.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"deprecated": true,
|
"deprecated": true,
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin:26.2",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin:26.2",
|
||||||
"version": "26.2"
|
"version": "26.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"deprecated": true,
|
"deprecated": true,
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin:25.2",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin:25.2",
|
||||||
"version": "25.2"
|
"version": "25.2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"bitcoin-knots": {
|
"bitcoin-knots": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.3.knots20260210",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"bitcoin_integration": {
|
"bitcoin_integration": {
|
||||||
@@ -580,7 +667,7 @@
|
|||||||
"sh",
|
"sh",
|
||||||
"-lc"
|
"-lc"
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.3.knots20260210",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -662,25 +749,25 @@
|
|||||||
"versions": [
|
"versions": [
|
||||||
{
|
{
|
||||||
"default": true,
|
"default": true,
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.3.knots20260210",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210",
|
||||||
"version": "29.3.knots20260210"
|
"version": "29.3.knots20260210"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.3.knots20260508",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260508",
|
||||||
"version": "29.3.knots20260508"
|
"version": "29.3.knots20260508"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.3.knots20260507",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260507",
|
||||||
"version": "29.3.knots20260507"
|
"version": "29.3.knots20260507"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.2.knots20251110",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.2.knots20251110",
|
||||||
"version": "29.2.knots20251110"
|
"version": "29.2.knots20251110"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"bitcoin-ui": {
|
"bitcoin-ui": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/bitcoin-ui:1.7.123-alpha",
|
"image": "source.archipelago-foundation.org/lfg2025/bitcoin-ui:1.7.123-alpha",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
@@ -751,7 +838,7 @@
|
|||||||
"name": "botfights-jwt-secret"
|
"name": "botfights-jwt-secret"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/botfights:1.2.11",
|
"image": "source.archipelago-foundation.org/lfg2025/botfights:1.2.11",
|
||||||
"pull_policy": "always",
|
"pull_policy": "always",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
{
|
{
|
||||||
@@ -856,8 +943,8 @@
|
|||||||
"btcpay": {
|
"btcpay": {
|
||||||
"image": "docker.io/btcpayserver/btcpayserver:2.4.2",
|
"image": "docker.io/btcpayserver/btcpayserver:2.4.2",
|
||||||
"images": {
|
"images": {
|
||||||
"archy-btcpay-db": "146.59.87.168:3000/lfg2025/postgres:15.17",
|
"archy-btcpay-db": "source.archipelago-foundation.org/lfg2025/postgres:15.17",
|
||||||
"archy-nbxplorer": "146.59.87.168:3000/lfg2025/nbxplorer:2.6.0",
|
"archy-nbxplorer": "source.archipelago-foundation.org/lfg2025/nbxplorer:2.6.0",
|
||||||
"btcpay-server": "docker.io/btcpayserver/btcpayserver:2.4.2"
|
"btcpay-server": "docker.io/btcpayserver/btcpayserver:2.4.2"
|
||||||
},
|
},
|
||||||
"version": "2.4.2"
|
"version": "2.4.2"
|
||||||
@@ -1069,7 +1156,7 @@
|
|||||||
"version": "23.08.2"
|
"version": "23.08.2"
|
||||||
},
|
},
|
||||||
"cryptpad": {
|
"cryptpad": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/cryptpad:2024.12.0",
|
"image": "source.archipelago-foundation.org/lfg2025/cryptpad:2024.12.0",
|
||||||
"version": "2024.12.0"
|
"version": "2024.12.0"
|
||||||
},
|
},
|
||||||
"did-wallet": {
|
"did-wallet": {
|
||||||
@@ -1143,7 +1230,7 @@
|
|||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
"electrs-ui": {
|
"electrs-ui": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/electrs-ui:1.7.123-alpha",
|
"image": "source.archipelago-foundation.org/lfg2025/electrs-ui:1.7.123-alpha",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
@@ -1190,7 +1277,7 @@
|
|||||||
"version": "1.7.123-alpha"
|
"version": "1.7.123-alpha"
|
||||||
},
|
},
|
||||||
"electrumx": {
|
"electrumx": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/electrumx:v1.18.0",
|
"image": "source.archipelago-foundation.org/lfg2025/electrumx:v1.18.0",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"bitcoin_integration": {
|
"bitcoin_integration": {
|
||||||
@@ -1207,7 +1294,7 @@
|
|||||||
"sh",
|
"sh",
|
||||||
"-lc"
|
"-lc"
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/electrumx:v1.18.0",
|
"image": "source.archipelago-foundation.org/lfg2025/electrumx:v1.18.0",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -1291,7 +1378,7 @@
|
|||||||
"version": "v1.18.0"
|
"version": "v1.18.0"
|
||||||
},
|
},
|
||||||
"fedimint": {
|
"fedimint": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/fedimintd:v0.10.0",
|
"image": "source.archipelago-foundation.org/lfg2025/fedimintd:v0.10.0",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"bitcoin_integration": {
|
"bitcoin_integration": {
|
||||||
@@ -1321,7 +1408,7 @@
|
|||||||
"sh",
|
"sh",
|
||||||
"-lc"
|
"-lc"
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/fedimintd:v0.10.0",
|
"image": "source.archipelago-foundation.org/lfg2025/fedimintd:v0.10.0",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -1428,7 +1515,7 @@
|
|||||||
"name": "fmcd-password"
|
"name": "fmcd-password"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/fmcd:0.8.1",
|
"image": "source.archipelago-foundation.org/lfg2025/fmcd:0.8.1",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -1500,7 +1587,7 @@
|
|||||||
"version": "0.8.0"
|
"version": "0.8.0"
|
||||||
},
|
},
|
||||||
"fedimint-gateway": {
|
"fedimint-gateway": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/gatewayd:v0.10.0",
|
"image": "source.archipelago-foundation.org/lfg2025/gatewayd:v0.10.0",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"bitcoin_integration": {
|
"bitcoin_integration": {
|
||||||
@@ -1528,7 +1615,7 @@
|
|||||||
"name": "fedimint-gateway-hash"
|
"name": "fedimint-gateway-hash"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/gatewayd:v0.10.0",
|
"image": "source.archipelago-foundation.org/lfg2025/gatewayd:v0.10.0",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -1616,7 +1703,7 @@
|
|||||||
"version": "v0.10.0"
|
"version": "v0.10.0"
|
||||||
},
|
},
|
||||||
"filebrowser": {
|
"filebrowser": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/filebrowser:v2.27.0",
|
"image": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.27.0",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"bitcoin_integration": {
|
"bitcoin_integration": {
|
||||||
@@ -1629,7 +1716,7 @@
|
|||||||
"/data/.filebrowser.json"
|
"/data/.filebrowser.json"
|
||||||
],
|
],
|
||||||
"data_uid": "100000:100000",
|
"data_uid": "100000:100000",
|
||||||
"image": "146.59.87.168:3000/lfg2025/filebrowser:v2.27.0",
|
"image": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.27.0",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -1699,7 +1786,7 @@
|
|||||||
"version": "v2.27.0"
|
"version": "v2.27.0"
|
||||||
},
|
},
|
||||||
"fips": {
|
"fips": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/fips:v0.1.0",
|
"image": "source.archipelago-foundation.org/lfg2025/fips:v0.1.0",
|
||||||
"version": "v0.1.0"
|
"version": "v0.1.0"
|
||||||
},
|
},
|
||||||
"fips-ui": {
|
"fips-ui": {
|
||||||
@@ -1870,12 +1957,12 @@
|
|||||||
"version": "1.23"
|
"version": "1.23"
|
||||||
},
|
},
|
||||||
"grafana": {
|
"grafana": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/grafana:10.2.0",
|
"image": "source.archipelago-foundation.org/lfg2025/grafana:10.2.0",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
"data_uid": "472:472",
|
"data_uid": "472:472",
|
||||||
"image": "grafana/grafana:10.2.0",
|
"image": "source.archipelago-foundation.org/lfg2025/grafana:10.2.0",
|
||||||
"image_signature": "cosign://...",
|
"image_signature": "cosign://...",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -1945,11 +2032,11 @@
|
|||||||
"version": "10.2.0"
|
"version": "10.2.0"
|
||||||
},
|
},
|
||||||
"homeassistant": {
|
"homeassistant": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/home-assistant:2026.7.3",
|
"image": "source.archipelago-foundation.org/lfg2025/home-assistant:2026.7.3",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/home-assistant:2026.7.3",
|
"image": "source.archipelago-foundation.org/lfg2025/home-assistant:2026.7.3",
|
||||||
"network": "pasta",
|
"network": "pasta",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -2038,16 +2125,16 @@
|
|||||||
"version": "2026.7.3"
|
"version": "2026.7.3"
|
||||||
},
|
},
|
||||||
"immich": {
|
"immich": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/immich-server:release",
|
"image": "source.archipelago-foundation.org/lfg2025/immich-server:release",
|
||||||
"images": {
|
"images": {
|
||||||
"immich_postgres": "146.59.87.168:3000/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0",
|
"immich_postgres": "source.archipelago-foundation.org/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0",
|
||||||
"immich_redis": "146.59.87.168:3000/lfg2025/redis:7.4.8",
|
"immich_redis": "source.archipelago-foundation.org/lfg2025/redis:7.4.8",
|
||||||
"immich_server": "146.59.87.168:3000/lfg2025/immich-server:release"
|
"immich_server": "source.archipelago-foundation.org/lfg2025/immich-server:release"
|
||||||
},
|
},
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/immich-server:release",
|
"image": "source.archipelago-foundation.org/lfg2025/immich-server:release",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -2150,7 +2237,7 @@
|
|||||||
"name": "immich-db-password"
|
"name": "immich-db-password"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0",
|
"image": "source.archipelago-foundation.org/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -2215,7 +2302,7 @@
|
|||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/valkey:7-alpine",
|
"image": "source.archipelago-foundation.org/lfg2025/valkey:7-alpine",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -2251,17 +2338,17 @@
|
|||||||
"version": "7-alpine"
|
"version": "7-alpine"
|
||||||
},
|
},
|
||||||
"indeedhub": {
|
"indeedhub": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/indeedhub:1.0.0",
|
"image": "source.archipelago-foundation.org/lfg2025/indeedhub:1.0.0",
|
||||||
"images": {
|
"images": {
|
||||||
"indeedhub": "146.59.87.168:3000/lfg2025/indeedhub:1.0.0",
|
"indeedhub": "source.archipelago-foundation.org/lfg2025/indeedhub:1.0.0",
|
||||||
"indeedhub-api": "146.59.87.168:3000/lfg2025/indeedhub-api:1.0.0",
|
"indeedhub-api": "source.archipelago-foundation.org/lfg2025/indeedhub-api:1.0.0",
|
||||||
"indeedhub-ffmpeg": "146.59.87.168:3000/lfg2025/indeedhub-ffmpeg:1.0.0"
|
"indeedhub-ffmpeg": "source.archipelago-foundation.org/lfg2025/indeedhub-ffmpeg:1.0.0"
|
||||||
},
|
},
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"category": "community",
|
"category": "community",
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/indeedhub:1.0.0",
|
"image": "source.archipelago-foundation.org/lfg2025/indeedhub:1.0.0",
|
||||||
"network": "indeedhub-net",
|
"network": "indeedhub-net",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -2404,7 +2491,7 @@
|
|||||||
"name": "indeedhub-jwt"
|
"name": "indeedhub-jwt"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/indeedhub-api:1.0.0",
|
"image": "source.archipelago-foundation.org/lfg2025/indeedhub-api:1.0.0",
|
||||||
"network": "indeedhub-net",
|
"network": "indeedhub-net",
|
||||||
"network_aliases": [
|
"network_aliases": [
|
||||||
"api"
|
"api"
|
||||||
@@ -2485,7 +2572,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"category": "community",
|
"category": "community",
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/indeedhub-ffmpeg:1.0.0",
|
"image": "source.archipelago-foundation.org/lfg2025/indeedhub-ffmpeg:1.0.0",
|
||||||
"network": "indeedhub-net",
|
"network": "indeedhub-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -2553,7 +2640,7 @@
|
|||||||
"name": "indeedhub-minio-password"
|
"name": "indeedhub-minio-password"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/minio:RELEASE.2024-11-07T00-52-20Z",
|
"image": "source.archipelago-foundation.org/lfg2025/minio:RELEASE.2024-11-07T00-52-20Z",
|
||||||
"network": "indeedhub-net",
|
"network": "indeedhub-net",
|
||||||
"network_aliases": [
|
"network_aliases": [
|
||||||
"minio"
|
"minio"
|
||||||
@@ -2622,7 +2709,7 @@
|
|||||||
"name": "indeedhub-db-password"
|
"name": "indeedhub-db-password"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/postgres:16.13-alpine",
|
"image": "source.archipelago-foundation.org/lfg2025/postgres:16.13-alpine",
|
||||||
"network": "indeedhub-net",
|
"network": "indeedhub-net",
|
||||||
"network_aliases": [
|
"network_aliases": [
|
||||||
"postgres"
|
"postgres"
|
||||||
@@ -2691,7 +2778,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"category": "community",
|
"category": "community",
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/redis:7.4.8-alpine",
|
"image": "source.archipelago-foundation.org/lfg2025/redis:7.4.8-alpine",
|
||||||
"network": "indeedhub-net",
|
"network": "indeedhub-net",
|
||||||
"network_aliases": [
|
"network_aliases": [
|
||||||
"redis"
|
"redis"
|
||||||
@@ -2749,7 +2836,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"category": "community",
|
"category": "community",
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/nostr-rs-relay:0.9.0",
|
"image": "source.archipelago-foundation.org/lfg2025/nostr-rs-relay:0.9.0",
|
||||||
"network": "indeedhub-net",
|
"network": "indeedhub-net",
|
||||||
"network_aliases": [
|
"network_aliases": [
|
||||||
"relay"
|
"relay"
|
||||||
@@ -2799,11 +2886,11 @@
|
|||||||
"version": "0.9.0"
|
"version": "0.9.0"
|
||||||
},
|
},
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/jellyfin:10.8.13",
|
"image": "source.archipelago-foundation.org/lfg2025/jellyfin:10.8.13",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/jellyfin:10.8.13",
|
"image": "source.archipelago-foundation.org/lfg2025/jellyfin:10.8.13",
|
||||||
"network": "pasta",
|
"network": "pasta",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -2983,7 +3070,7 @@
|
|||||||
"version": "0.12.0"
|
"version": "0.12.0"
|
||||||
},
|
},
|
||||||
"lnd": {
|
"lnd": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta",
|
"image": "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"bitcoin_integration": {
|
"bitcoin_integration": {
|
||||||
@@ -2998,7 +3085,7 @@
|
|||||||
"template": "{{BITCOIN_HOST}}"
|
"template": "{{BITCOIN_HOST}}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta",
|
"image": "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -3088,7 +3175,7 @@
|
|||||||
"version": "v0.18.4-beta"
|
"version": "v0.18.4-beta"
|
||||||
},
|
},
|
||||||
"lnd-ui": {
|
"lnd-ui": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/lnd-ui:1.7.123-alpha",
|
"image": "source.archipelago-foundation.org/lfg2025/lnd-ui:1.7.123-alpha",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
@@ -3139,11 +3226,11 @@
|
|||||||
"version": "1.7.123-alpha"
|
"version": "1.7.123-alpha"
|
||||||
},
|
},
|
||||||
"mempool": {
|
"mempool": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.1",
|
"image": "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.0.1",
|
||||||
"images": {
|
"images": {
|
||||||
"archy-mempool-db": "146.59.87.168:3000/lfg2025/mariadb:11.4.10",
|
"archy-mempool-db": "source.archipelago-foundation.org/lfg2025/mariadb:11.4.10",
|
||||||
"archy-mempool-web": "146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.1",
|
"archy-mempool-web": "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.0.1",
|
||||||
"mempool-api": "146.59.87.168:3000/lfg2025/mempool-backend:v3.0.0"
|
"mempool-api": "source.archipelago-foundation.org/lfg2025/mempool-backend:v3.0.0"
|
||||||
},
|
},
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
@@ -3152,7 +3239,7 @@
|
|||||||
"sync_required": true
|
"sync_required": true
|
||||||
},
|
},
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.1",
|
"image": "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.0.1",
|
||||||
"image_signature": "cosign://...",
|
"image_signature": "cosign://...",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -3237,7 +3324,7 @@
|
|||||||
"template": "{{BITCOIN_HOST}}"
|
"template": "{{BITCOIN_HOST}}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/mempool-backend:v3.0.0",
|
"image": "source.archipelago-foundation.org/lfg2025/mempool-backend:v3.0.0",
|
||||||
"network": "archy-net",
|
"network": "archy-net",
|
||||||
"pull_policy": "if-not-present",
|
"pull_policy": "if-not-present",
|
||||||
"secret_env": [
|
"secret_env": [
|
||||||
@@ -3713,11 +3800,11 @@
|
|||||||
"version": "0.71.2"
|
"version": "0.71.2"
|
||||||
},
|
},
|
||||||
"nextcloud": {
|
"nextcloud": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/nextcloud:29",
|
"image": "source.archipelago-foundation.org/lfg2025/nextcloud:29",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/nextcloud:29",
|
"image": "source.archipelago-foundation.org/lfg2025/nextcloud:29",
|
||||||
"network": "pasta",
|
"network": "pasta",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -3796,11 +3883,11 @@
|
|||||||
"version": "29"
|
"version": "29"
|
||||||
},
|
},
|
||||||
"nginx-proxy-manager": {
|
"nginx-proxy-manager": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/nginx-proxy-manager:latest",
|
"image": "source.archipelago-foundation.org/lfg2025/nginx-proxy-manager:latest",
|
||||||
"version": "latest"
|
"version": "latest"
|
||||||
},
|
},
|
||||||
"nostr-rs-relay": {
|
"nostr-rs-relay": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/nostr-rs-relay:0.9.0",
|
"image": "source.archipelago-foundation.org/lfg2025/nostr-rs-relay:0.9.0",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
@@ -3875,30 +3962,104 @@
|
|||||||
"version": "0.9.0"
|
"version": "0.9.0"
|
||||||
},
|
},
|
||||||
"nostr-vpn": {
|
"nostr-vpn": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/nostr-vpn:v0.3.7",
|
"image": "source.archipelago-foundation.org/lfg2025/nostr-vpn:v0.3.7",
|
||||||
"version": "v0.3.7"
|
"version": "v0.3.7"
|
||||||
},
|
},
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/ollama:latest",
|
"image": "source.archipelago-foundation.org/lfg2025/ollama:latest",
|
||||||
"version": "latest"
|
"version": "latest"
|
||||||
},
|
},
|
||||||
"penpot": {
|
"penpot": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/penpot-frontend:2.4",
|
"image": "source.archipelago-foundation.org/lfg2025/penpot-frontend:2.4",
|
||||||
"images": {
|
"images": {
|
||||||
"penpot-backend": "146.59.87.168:3000/lfg2025/penpot-backend:2.4",
|
"penpot-backend": "source.archipelago-foundation.org/lfg2025/penpot-backend:2.4",
|
||||||
"penpot-exporter": "146.59.87.168:3000/lfg2025/penpot-exporter:2.4",
|
"penpot-exporter": "source.archipelago-foundation.org/lfg2025/penpot-exporter:2.4",
|
||||||
"penpot-frontend": "146.59.87.168:3000/lfg2025/penpot-frontend:2.4",
|
"penpot-frontend": "source.archipelago-foundation.org/lfg2025/penpot-frontend:2.4",
|
||||||
"penpot-postgres": "146.59.87.168:3000/lfg2025/postgres:15",
|
"penpot-postgres": "source.archipelago-foundation.org/lfg2025/postgres:15",
|
||||||
"penpot-valkey": "146.59.87.168:3000/lfg2025/valkey:8.1"
|
"penpot-valkey": "source.archipelago-foundation.org/lfg2025/valkey:8.1"
|
||||||
},
|
},
|
||||||
"version": "2.4"
|
"version": "2.4"
|
||||||
},
|
},
|
||||||
|
"phoenixd": {
|
||||||
|
"manifest": {
|
||||||
|
"app": {
|
||||||
|
"category": "money",
|
||||||
|
"container": {
|
||||||
|
"data_uid": "1000:1000",
|
||||||
|
"image": "source.archipelago-foundation.org/lfg2025/phoenixd:0.9.0",
|
||||||
|
"pull_policy": "if-not-present"
|
||||||
|
},
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"storage": "500Mi"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Headless Lightning daemon by ACINQ (the Phoenix wallet team). No screen of its own — it exposes a small local API that other apps and tools use to send and receive Lightning payments. Channel liquidity is managed automatically for a fee.",
|
||||||
|
"environment": [
|
||||||
|
"PHOENIX_DATADIR=/data"
|
||||||
|
],
|
||||||
|
"health_check": {
|
||||||
|
"endpoint": "localhost:9740",
|
||||||
|
"interval": "30s",
|
||||||
|
"retries": 5,
|
||||||
|
"timeout": "5s",
|
||||||
|
"type": "tcp"
|
||||||
|
},
|
||||||
|
"id": "phoenixd",
|
||||||
|
"metadata": {
|
||||||
|
"features": [
|
||||||
|
"Ultra-light Lightning node — no bitcoin node required",
|
||||||
|
"Automated channel and liquidity management (fees apply)",
|
||||||
|
"Simple HTTP API + websockets for payments",
|
||||||
|
"Backed by the team behind the Phoenix mobile wallet"
|
||||||
|
],
|
||||||
|
"icon": "/assets/img/app-icons/phoenixd.svg",
|
||||||
|
"repo": "https://github.com/ACINQ/phoenixd",
|
||||||
|
"tier": "optional"
|
||||||
|
},
|
||||||
|
"name": "phoenixd",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"auth": "none",
|
||||||
|
"auth_rationale": "Loopback-only JSON API, not a web page. Every request is authenticated by the http password phoenixd generates in its own data directory on first run; the app gate's browser login page would break the API clients this port exists for.",
|
||||||
|
"bind": "127.0.0.1",
|
||||||
|
"container": 9740,
|
||||||
|
"host": 9740,
|
||||||
|
"protocol": "tcp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"cpu_limit": 1,
|
||||||
|
"disk_limit": "1Gi",
|
||||||
|
"memory_limit": "512Mi"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"capabilities": [],
|
||||||
|
"network_policy": "bridge",
|
||||||
|
"no_new_privileges": true,
|
||||||
|
"readonly_root": true
|
||||||
|
},
|
||||||
|
"version": "0.9.0",
|
||||||
|
"volumes": [
|
||||||
|
{
|
||||||
|
"options": [
|
||||||
|
"rw"
|
||||||
|
],
|
||||||
|
"source": "/var/lib/archipelago/phoenixd",
|
||||||
|
"target": "/data",
|
||||||
|
"type": "bind"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": "0.9.0"
|
||||||
|
},
|
||||||
"photoprism": {
|
"photoprism": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/photoprism:240915",
|
"image": "source.archipelago-foundation.org/lfg2025/photoprism:240915",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/photoprism:240915",
|
"image": "source.archipelago-foundation.org/lfg2025/photoprism:240915",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -4377,13 +4538,13 @@
|
|||||||
"version": "3.4.2"
|
"version": "3.4.2"
|
||||||
},
|
},
|
||||||
"portainer": {
|
"portainer": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/portainer:2.39.1",
|
"image": "source.archipelago-foundation.org/lfg2025/portainer:2.39.1",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"category": "development",
|
"category": "development",
|
||||||
"container": {
|
"container": {
|
||||||
"data_uid": "1000:1000",
|
"data_uid": "1000:1000",
|
||||||
"image": "146.59.87.168:3000/lfg2025/portainer:2.39.1",
|
"image": "source.archipelago-foundation.org/lfg2025/portainer:2.39.1",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -4574,15 +4735,15 @@
|
|||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
"routstr": {
|
"routstr": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/routstr:v0.4.3",
|
"image": "source.archipelago-foundation.org/lfg2025/routstr:v0.4.3",
|
||||||
"version": "v0.4.3"
|
"version": "v0.4.3"
|
||||||
},
|
},
|
||||||
"searxng": {
|
"searxng": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/searxng:latest",
|
"image": "source.archipelago-foundation.org/lfg2025/searxng:latest",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/searxng:latest",
|
"image": "source.archipelago-foundation.org/lfg2025/searxng:latest",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -4724,11 +4885,11 @@
|
|||||||
"version": "0.9.0"
|
"version": "0.9.0"
|
||||||
},
|
},
|
||||||
"tailscale": {
|
"tailscale": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/tailscale:stable",
|
"image": "source.archipelago-foundation.org/lfg2025/tailscale:stable",
|
||||||
"version": "stable"
|
"version": "stable"
|
||||||
},
|
},
|
||||||
"uptime-kuma": {
|
"uptime-kuma": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/uptime-kuma:1",
|
"image": "source.archipelago-foundation.org/lfg2025/uptime-kuma:1",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
@@ -4737,7 +4898,7 @@
|
|||||||
"node",
|
"node",
|
||||||
"server/server.js"
|
"server/server.js"
|
||||||
],
|
],
|
||||||
"image": "146.59.87.168:3000/lfg2025/uptime-kuma:1",
|
"image": "source.archipelago-foundation.org/lfg2025/uptime-kuma:1",
|
||||||
"network": "pasta",
|
"network": "pasta",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -4809,11 +4970,11 @@
|
|||||||
"version": "1"
|
"version": "1"
|
||||||
},
|
},
|
||||||
"vaultwarden": {
|
"vaultwarden": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine",
|
"image": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.30.0-alpine",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
"container": {
|
"container": {
|
||||||
"image": "146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine",
|
"image": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.30.0-alpine",
|
||||||
"network": "pasta",
|
"network": "pasta",
|
||||||
"pull_policy": "if-not-present"
|
"pull_policy": "if-not-present"
|
||||||
},
|
},
|
||||||
@@ -4893,7 +5054,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"signature": "c7b1901c5b67b7f83140cc12fc96c8bb17a696272afc1915a4df72d1c38c5c8d8691c528af102a3d34b00daa40317a08c394d51f806c6e67262b232dd2bb220a",
|
"signature": "0aaf681435434b6e325269745d1fc3f90686c5391525d6b591666d6e36097bef0e5b999e2b9fe37d7a945e59aa40db74ed618824ab78c7fc6af73271c62ff20a",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"updated": "2026-08-09"
|
"updated": "2026-08-13"
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-20
@@ -1,32 +1,29 @@
|
|||||||
{
|
{
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"**Every app is now supervised the same way — the last stragglers moved under systemd.** Five apps (Jellyfin, Nextcloud, Home Assistant, Uptime Kuma, Vaultwarden) still ran outside the node's per-app service management for a technical reason: their networking style died with whatever process started it, so they were kept alive by a separate workaround. That workaround is retired: these apps now migrate themselves onto the same managed units as everything else — own service, restart-on-anything, a ten-second breather between restarts so their networking can release its ports cleanly. The migration happens automatically on the node's next housekeeping pass, touches no app data, and was watched live on a real node: both test apps moved over on the first pass and came back healthy.",
|
"**The network map on TVs: no more blank page, no more frozen page — and it moves again.** The map's entrance animation needed a smoothness that TV kiosk hardware can't always deliver, so the page could sit blank until a refresh; the previous fix cured the freeze by stopping the animation entirely, which went too far. Now the map appears instantly with everything already in place, then resumes its calm orbital motion at a gentler pace suited to TVs. Resizing or rotating any screen also redraws the map properly instead of leaving it tiny, stretched, or empty.",
|
||||||
"**Leftover companion screens are cleaned up again — driven by real records this time.** When an app is uninstalled, its helper screen (the UI tile that fronts it) should go too. That cleanup was switched off in an earlier release after it wrongly removed the Bitcoin screen from a node whose Bitcoin was installed — it had been guessing \"installed\" from what happened to be running, and a separate bug made a running app look absent. The node now keeps a durable record of what you have installed, written at install time and cleared only by a real uninstall, and the cleanup consults only that record. If the record can't be read, the cleanup does nothing at all — \"I couldn't check\" is never treated as \"nothing is installed\" — and a helper must be orphaned for a sustained period before it is touched.",
|
"**The dashboard's corner logo is back to normal.** The new glossy paint finish was meant for the big emblem on the screensaver, intro, and login screens — it had quietly spread to the small logo in the dashboard header, where it looked wrong. Each screen now gets exactly the treatment intended for it.",
|
||||||
"**A warning that fired every minute on every node is gone.** The app catalog and the node disagreed about where Grafana's software comes from, so the node ignored the catalog's answer and logged a complaint roughly every 75 seconds, forever. The catalog was right — Grafana is served from the fleet's own registry, like Bitcoin Knots — and the node's records now agree with it.",
|
"**App icons no longer vanish in My Apps.** The freshly restyled Alby Hub and phoenixd icons could render as blank squares in some views — a subtlety in how the icon files declared their size. Fixed at the source, and the icon tool app developers use now produces immune files."
|
||||||
"**The federation map became a real map.** The network view is now a 3D orbital scene of your federation — nodes as a point-cloud globe with calm motion, auto-fit centring, and a 2D top-down toggle that portrait and mobile screens use by default, with the scene filling the viewport instead of sitting in a letterbox. Inbound peer requests appear live on the map as blinking nodes you can accept or reject in place, and revisiting the view no longer replays the whole intro — the scene updates in place.",
|
|
||||||
"**An app that's mid-restart shows a page that says so — and comes back by itself.** When an app's screen was briefly unreachable behind the gate, the browser got a bare error; it now gets a named page for that app that retries on its own until the app answers.",
|
|
||||||
"Known gaps, disclosed rather than buried: three voice-assistant ports remain open without authentication. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release; the supervision migration and the cleanup re-enable were verified live on one node (both apps migrated and healthy, cleanup correctly idle)."
|
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"current_version": "1.7.129-alpha",
|
"current_version": "1.8.3-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.7.129-alpha/archipelago",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.3-alpha/archipelago",
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"new_version": "1.7.129-alpha",
|
"new_version": "1.8.3-alpha",
|
||||||
"sha256": "675e7dafc855d59b38c5a12d8b9405894677ed8701580227beca95ec2912e3f2",
|
"sha256": "23b608bfce575212edb873ded3e125505f42be0db6dc06ce5f9c1e51c232c22d",
|
||||||
"size_bytes": 59531400
|
"size_bytes": 59900696
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current_version": "1.7.129-alpha",
|
"current_version": "1.8.3-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.7.129-alpha/archipelago-frontend-1.7.129-alpha.tar.gz",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.3-alpha/archipelago-frontend-1.8.3-alpha.tar.gz",
|
||||||
"name": "archipelago-frontend-1.7.129-alpha.tar.gz",
|
"name": "archipelago-frontend-1.8.3-alpha.tar.gz",
|
||||||
"new_version": "1.7.129-alpha",
|
"new_version": "1.8.3-alpha",
|
||||||
"sha256": "53af2743308f4ae6a627255aaa288706d331d567c99e1cb615684dc3abba534d",
|
"sha256": "13ca772e9a36c266b67e6eff2d51366616cdbedeab3a05ba5eba340332d3d6d4",
|
||||||
"size_bytes": 95452033
|
"size_bytes": 97615528
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"release_date": "2026-08-10",
|
"release_date": "2026-08-14",
|
||||||
"signature": "902778710674486d5919e4abb1bf5540521c9ef55b50a44a9d64b750812738d51cc193f6675a9969b1c45ae86c2e9e44ab3d8daf8aa1d439799d7f7846e27d04",
|
"signature": "35d5f56ef77cda1866051a3ff06f6e09ff0a23ac7b000a1ac4cf24e04cecabff00be7f3e240fc82ec605bff80c5690c55c296a300018562940d71ffc5df26c00",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"version": "1.7.129-alpha"
|
"version": "1.8.3-alpha"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,16 @@
|
|||||||
"docker.io",
|
"docker.io",
|
||||||
"ghcr.io",
|
"ghcr.io",
|
||||||
"localhost",
|
"localhost",
|
||||||
"146.59.87.168:3000"
|
"146.59.87.168:3000",
|
||||||
|
"source.archipelago-foundation.org"
|
||||||
],
|
],
|
||||||
"pending": {
|
"pending": {},
|
||||||
|
"promotions": {
|
||||||
"source.archipelago-foundation.org": {
|
"source.archipelago-foundation.org": {
|
||||||
"trusted_from_binary": "unreleased",
|
"trusted_from_binary": "1.8.0-alpha",
|
||||||
"note": "Added to TRUSTED_REGISTRIES 2026-08-07. Not yet shipped in any OTA, so no deployed node accepts it. Promote to `hosts` only after the fleet is confirmed on a binary that includes it."
|
"confirmed": "2026-08-13",
|
||||||
|
"note": "Fleet confirmed on 1.8.0-alpha (TRUSTED_REGISTRIES includes this host since 2026-08-07): archi-dev-box, zaza-optiplex, archipelago-1, shorty-s, austin-sapien all report current_version 1.8.0-alpha via update_state.json on 2026-08-13. Exception: archy-x250-beta is stranded pre-v1.7.122 (old release-root pin, cannot OTA at all) and needs a full re-image — it cannot install catalog apps from ANY host today, so it does not hold the floor down."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updated": "2026-08-07"
|
"updated": "2026-08-13"
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+84
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Normalize a third-party SVG mark onto the Archipelago app-icon canvas.
|
||||||
|
|
||||||
|
Store tiles render icons as-is — there is deliberately no runtime inset
|
||||||
|
(it would double-margin every icon that already ships whitespace), so the
|
||||||
|
margin must be baked into the file. House icons carry it naturally;
|
||||||
|
third-party marks are usually full-bleed (Alby Hub's tile icon was the
|
||||||
|
first to land edge-to-edge in the store, 2026-08-14). This wraps any SVG
|
||||||
|
in a square canvas with a uniform inner margin, preserving the original
|
||||||
|
untouched as a nested <svg>.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
scripts/normalize-app-icon.py in.svg out.svg [--margin 0.12]
|
||||||
|
|
||||||
|
Margin is a fraction of the canvas per side (default 12%, the house look).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("src")
|
||||||
|
ap.add_argument("dst")
|
||||||
|
ap.add_argument("--margin", type=float, default=0.12,
|
||||||
|
help="inner margin per side, fraction of canvas (default 0.12)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
svg = open(args.src, encoding="utf-8").read()
|
||||||
|
|
||||||
|
m = re.search(r"<svg\b[^>]*>", svg, re.S)
|
||||||
|
if not m:
|
||||||
|
print("error: no <svg> root element found", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
root = m.group(0)
|
||||||
|
|
||||||
|
vb = re.search(r'viewBox\s*=\s*["\']([\d.eE+\s,-]+)["\']', root)
|
||||||
|
if vb:
|
||||||
|
nums = [float(x) for x in re.split(r"[\s,]+", vb.group(1).strip())]
|
||||||
|
if len(nums) != 4:
|
||||||
|
print("error: unparseable viewBox", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
_, _, w, h = nums
|
||||||
|
inner_viewbox = vb.group(1).strip()
|
||||||
|
else:
|
||||||
|
wm = re.search(r'width\s*=\s*["\']([\d.]+)', root)
|
||||||
|
hm = re.search(r'height\s*=\s*["\']([\d.]+)', root)
|
||||||
|
if not (wm and hm):
|
||||||
|
print("error: no viewBox and no width/height to derive one", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
w, h = float(wm.group(1)), float(hm.group(1))
|
||||||
|
inner_viewbox = f"0 0 {w} {h}"
|
||||||
|
|
||||||
|
# Square canvas fitting the larger dimension; the mark centers in it.
|
||||||
|
side = max(w, h)
|
||||||
|
canvas = 100.0
|
||||||
|
margin = canvas * args.margin
|
||||||
|
avail = canvas - 2 * margin
|
||||||
|
scale_w = avail * (w / side)
|
||||||
|
scale_h = avail * (h / side)
|
||||||
|
x = (canvas - scale_w) / 2
|
||||||
|
y = (canvas - scale_h) / 2
|
||||||
|
|
||||||
|
# Strip any XML prolog from the original; it nests inside the wrapper.
|
||||||
|
inner = re.sub(r"^\s*<\?xml[^>]*\?>\s*", "", svg)
|
||||||
|
|
||||||
|
out = (
|
||||||
|
f'<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 {canvas:g} {canvas:g}">\n'
|
||||||
|
f' <!-- normalized by scripts/normalize-app-icon.py: margin={args.margin:g} per side -->\n'
|
||||||
|
f' <svg x="{x:.3f}" y="{y:.3f}" width="{scale_w:.3f}" height="{scale_h:.3f}" '
|
||||||
|
f'viewBox="{inner_viewbox}" preserveAspectRatio="xMidYMid meet">\n'
|
||||||
|
f'{inner}\n'
|
||||||
|
f' </svg>\n'
|
||||||
|
f'</svg>\n'
|
||||||
|
)
|
||||||
|
open(args.dst, "w", encoding="utf-8").write(out)
|
||||||
|
print(f"wrote {args.dst}: {canvas:g}x{canvas:g} canvas, mark {scale_w:.1f}x{scale_h:.1f} at {args.margin:.0%} margin")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -85,6 +85,20 @@ echo "publish-companion-apk: verified v1 + v2 + v3 signatures." >&2
|
|||||||
mkdir -p "$(dirname "$DEST")"
|
mkdir -p "$(dirname "$DEST")"
|
||||||
cp "$SIGNED" "$DEST"
|
cp "$SIGNED" "$DEST"
|
||||||
|
|
||||||
|
# Version metadata beside the APK: the dashboard's companion overlay reads
|
||||||
|
# this to show which version the Download button installs. Extracted from
|
||||||
|
# the gradle config that just built the APK, so it can never drift from it.
|
||||||
|
META="${DEST%.apk}.json"
|
||||||
|
V_NAME="$(sed -n 's/^[[:space:]]*versionName = "\(.*\)"/\1/p' Android/app/build.gradle.kts | head -1)"
|
||||||
|
V_CODE="$(sed -n 's/^[[:space:]]*versionCode = \([0-9]*\).*/\1/p' Android/app/build.gradle.kts | head -1)"
|
||||||
|
if [ -n "$V_NAME" ] && [ -n "$V_CODE" ]; then
|
||||||
|
printf '{\n "versionName": "%s",\n "versionCode": %s\n}\n' "$V_NAME" "$V_CODE" > "$META"
|
||||||
|
git add "$META"
|
||||||
|
echo "publish-companion-apk: staged $META (v$V_NAME, build $V_CODE)" >&2
|
||||||
|
else
|
||||||
|
echo "publish-companion-apk: WARNING could not extract version from build.gradle.kts — $META not updated" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
# Drop the legacy zipped artifact so the served download is the raw APK only.
|
# Drop the legacy zipped artifact so the served download is the raw APK only.
|
||||||
if [ -f "$OLD_ZIP" ]; then
|
if [ -f "$OLD_ZIP" ]; then
|
||||||
git rm -q --ignore-unmatch "$OLD_ZIP" 2>/dev/null || rm -f "$OLD_ZIP"
|
git rm -q --ignore-unmatch "$OLD_ZIP" 2>/dev/null || rm -f "$OLD_ZIP"
|
||||||
|
|||||||
Reference in New Issue
Block a user