Compare commits

..
Author SHA1 Message Date
archipelago 2b7e92e770 chore: release v1.8.2-alpha
Demo images / Build & push demo images (push) Successful in 3m43s
2026-08-14 04:28:12 -04:00
archipelagoandClaude Fable 5 7c34df36cd feat(ui): app icons on the house canvas; detail page gets the tile treatment
Demo images / Build & push demo images (push) Successful in 3m33s
- alby-hub + phoenixd icons re-set with the standard 12% inner margin
  (they shipped edge-to-edge; every other icon carries whitespace).
- scripts/normalize-app-icon.py: wraps any third-party SVG mark onto the
  house canvas — the system applies the tile plate (archy-app-icon)
  automatically but deliberately no runtime inset, so the margin must be
  baked; the guide now says exactly that.
- MarketplaceAppDetails: the icon now carries archy-app-icon like the
  store tiles — the treatment no longer stops at the detail page.
- v1.8.2 changelog: third curated bullet (the ceremony gate requires 3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 04:22:28 -04:00
archipelagoandClaude Fable 5 4b4e1ab1a3 feat(ui): Kammergut gloss v3 — wet black paint on the logo badge, approved
Demo images / Build & push demo images (push) Successful in 3m43s
Pure-CSS build (the plan-b SVG text filter embosses artifacts on a disc
— v1 rejected for exactly that): borderless painted disc with warm
Kammergut-toned light, dense gradient stops + turbulence grain dither
(banding), and a radial top bloom instead of a linear streak (a linear
streak's tips seamed against the rim — operator screenshot). Iterated
headlessly + on a live preview server; operator approved 2026-08-14.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 04:11:42 -04:00
archipelagoandClaude Fable 5 d05fa6988d fix(federation): kiosk map goes fully static — kills the blank-load stall
Demo images / Build & push demo images (push) Successful in 3m31s
The GSAP entrance intro needs healthy rAF delivery to reach opacity 1;
on a paint-starved kiosk it stalls and the federation/peers screen reads
as BLANK until a lucky refresh. Kiosks now take the existing staticMode
branch (no intro, no ticker — everything lands in place instantly); the
2D default and 2D/3D toggle stay. Half-rate tick kept for any future
non-static kiosk path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 03:57:05 -04:00
archipelagoandClaude Fable 5 2399eeac66 feat(ui): auto-tab fallback — embed-refusing apps become tab apps
Demo images / Build & push demo images (push) Successful in 3m49s
An app whose frame never loads while its backend reports Running (the
embed-refusal signature: frame-busting JS, top-level-origin apps,
SameSite=Strict logins — everything the gate's header stripping cannot
fix) is remembered in localStorage; every later launch opens a tab
straight from the click (user gesture, so no popup blocker), and
opensInTab() gives it the tab-launch icon. A successful iframe load
clears the memory and entries expire after 7 days, so nodes that gain
embedding (gate improvements) get re-probed instead of being remembered
broken forever. Dev guide updated; v1.8.2 changelog + What's New curated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 20:23:04 -04:00
archipelago 63cb9dd22c Revert "feat(ui): Kammergut gloss test on the logo badge inner circle"
Demo images / Build & push demo images (push) Successful in 3m51s
This reverts commit 6672d978f7.
2026-08-13 15:02:36 -04:00
19 changed files with 313 additions and 88 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog # Changelog
## 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) ## 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. - **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.
+1 -1
View File
@@ -104,7 +104,7 @@ dependencies = [
[[package]] [[package]]
name = "archipelago" name = "archipelago"
version = "1.8.1-alpha" version = "1.8.2-alpha"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"archipelago-container", "archipelago-container",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "archipelago" name = "archipelago"
version = "1.8.1-alpha" version = "1.8.2-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"
+22
View File
@@ -159,6 +159,14 @@ the top-level origin (OAuth redirect flows, WebAuthn), or sets
in the real embedded app session, **not** a plain browser tab: tabs don't 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. 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 (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 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.) surviving example. Do not copy that pattern for new apps.)
@@ -523,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
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "neode-ui", "name": "neode-ui",
"version": "1.8.1-alpha", "version": "1.8.2-alpha",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "neode-ui", "name": "neode-ui",
"version": "1.8.1-alpha", "version": "1.8.2-alpha",
"dependencies": { "dependencies": {
"@scure/bip39": "^2.2.0", "@scure/bip39": "^2.2.0",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "neode-ui", "name": "neode-ui",
"private": true, "private": true,
"version": "1.8.1-alpha", "version": "1.8.2-alpha",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "./start-dev.sh", "start": "./start-dev.sh",
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <svg xmlns="http://www.w3.org/2000/svg" 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"> <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="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"/> <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>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <svg xmlns="http://www.w3.org/2000/svg" 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"> <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)"> <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" 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"/> <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> </g>
</svg> </svg>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

-34
View File
@@ -1,39 +1,5 @@
<template> <template>
<div id="app"> <div id="app">
<!-- KAMMERGUT GLOSS TEST (2026-08-12): SVG paint filter lifted verbatim
from plan-b's Kammergut wordmark (#paintGloss). Consumed by the
.logo-gradient-border::after override in style.css. Revert by
deleting this svg block + that css block (one commit). -->
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<defs>
<filter id="paintGloss" x="-12%" y="-30%" width="124%" height="160%" color-interpolation-filters="sRGB">
<feMorphology in="SourceAlpha" operator="dilate" radius="2.5" result="dilated"/>
<feGaussianBlur in="dilated" stdDeviation="6" result="puddle"/>
<feFlood flood-color="#fff4dc" flood-opacity="0.85" result="puddleColor"/>
<feComposite in="puddleColor" in2="puddle" operator="in" result="puddleHi"/>
<feFlood flood-color="#1A1A18" flood-opacity="0.18" result="puddleShadowColor"/>
<feComposite in="puddleShadowColor" in2="puddle" operator="in" result="puddleShadow"/>
<feOffset in="puddleShadow" dx="0" dy="3" result="puddleShadowOff"/>
<feGaussianBlur in="SourceAlpha" stdDeviation="1.4" result="bump"/>
<feSpecularLighting in="bump" surfaceScale="4" specularConstant="0.9"
specularExponent="40" lighting-color="#fff2d4" result="spec2">
<fePointLight x="600" y="-200" z="380"/>
</feSpecularLighting>
<feComposite in="spec2" in2="SourceAlpha" operator="in" result="specClip2"/>
<feGaussianBlur in="SourceAlpha" stdDeviation="4" result="dsBlur"/>
<feOffset in="dsBlur" dx="0" dy="6" result="dsOff"/>
<feComponentTransfer in="dsOff" result="dsFinal">
<feFuncA type="linear" slope="0.45"/>
</feComponentTransfer>
<feMerge>
<feMergeNode in="dsFinal"/>
<feMergeNode in="SourceGraphic"/>
<feMergeNode in="specClip2"/>
</feMerge>
</filter>
</defs>
</svg>
<!-- Splash Screen (only on first visit) --> <!-- Splash Screen (only on first visit) -->
<SplashScreen v-if="showSplash" @complete="handleSplashComplete" /> <SplashScreen v-if="showSplash" @complete="handleSplashComplete" />
@@ -1182,7 +1182,15 @@ function measure() {
} }
function initialBuild() { function initialBuild() {
staticMode = prefersReducedMotion() // Kiosk TVs get the fully static map: no entrance animation, no ticker.
// The GSAP intro needs healthy rAF delivery to ever 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). Half-rate ticking (below)
// helped but any continuous SVG animation is still the single biggest
// load on the kiosk's compositor, and at TV distance the still map loses
// nothing. Everything lands in place immediately, graph updates apply
// instantly, and the 2D/3D toggle still works.
staticMode = prefersReducedMotion() || kioskLowPower
built = true built = true
applyGraph() applyGraph()
+48 -7
View File
@@ -1128,14 +1128,55 @@ html.controller-nav [data-controller-container]:focus {
z-index: 0; z-index: 0;
} }
/* KAMMERGUT GLOSS TEST (2026-08-12) black gloss paint on the badge's /* KAMMERGUT GLOSS v3 TEST (2026-08-14, UNCOMMITTED until approved)
inner circle, lifted from plan-b's Kammergut wordmark (.paint-3d wet black paint, PURE CSS (the SVG text filter embosses badly on a
gradient + #paintGloss filter, defs injected in App.vue). Revert: disc; headless A/B renders picked this). Borderless per operator:
delete this block + the svg defs block in App.vue (one commit). */ 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 {
background: none;
box-shadow: none;
padding: 0;
overflow: hidden;
}
.logo-gradient-border::after { .logo-gradient-border::after {
background: linear-gradient(180deg, #2a2a26 0%, #1a1a18 45%, #060604 100%); inset: 0;
filter: url(#paintGloss); /* Dense intermediate stops + a fine turbulence grain (top layer,
-webkit-filter: url(#paintGloss); 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::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,
+51
View File
@@ -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)
}
}
+23
View File
@@ -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 */ }
+2 -2
View File
@@ -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">
+5 -1
View File
@@ -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,18 @@ 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.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 --> <!-- v1.8.1-alpha -->
<div> <div>
<div class="flex items-center gap-2 mb-3"> <div class="flex items-center gap-2 mb-3">
+17 -17
View File
@@ -1,29 +1,29 @@
{ {
"changelog": [ "changelog": [
"**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.", "**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 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.", "**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 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." "**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."
], ],
"components": [ "components": [
{ {
"current_version": "1.8.1-alpha", "current_version": "1.8.2-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.1-alpha/archipelago", "download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.2-alpha/archipelago",
"name": "archipelago", "name": "archipelago",
"new_version": "1.8.1-alpha", "new_version": "1.8.2-alpha",
"sha256": "50fe97705717f8176afc03285a3a459b1d4e219dba0442aa4ee5b8cea8e3ec30", "sha256": "505a456d94a17ef336b8573e1423edc2295463e603b0af89131144297cffd5ca",
"size_bytes": 59896032 "size_bytes": 59898960
}, },
{ {
"current_version": "1.8.1-alpha", "current_version": "1.8.2-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.1-alpha/archipelago-frontend-1.8.1-alpha.tar.gz", "download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.2-alpha/archipelago-frontend-1.8.2-alpha.tar.gz",
"name": "archipelago-frontend-1.8.1-alpha.tar.gz", "name": "archipelago-frontend-1.8.2-alpha.tar.gz",
"new_version": "1.8.1-alpha", "new_version": "1.8.2-alpha",
"sha256": "ae77c97e8f1ec7d1044f7d565319ddc503f6f5b87dbc9a524fa9cc9639b04f29", "sha256": "531ea4c5fc6c1116a3c09e0fc81f16acb91a86a0ad25fb4883de499713c530da",
"size_bytes": 97611693 "size_bytes": 97617354
} }
], ],
"release_date": "2026-08-13", "release_date": "2026-08-14",
"signature": "7a8932503d1c33156f79d11671bb275a2bb63347988da02562c3dd138287e7ed77c7d4cb0a1f6bcbcf8665da506fafedd2d81bb844de0cbcbaf2b9045210ca0b", "signature": "e2b5c85707121444aff6ca2150fe04dd7d73ef4f9a183eb962986554f34a20128f087c138250a42043801380f50f2e0aebe48869d1577d0171dfe4cf0ff0ac08",
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT", "signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
"version": "1.8.1-alpha" "version": "1.8.2-alpha"
} }
+17 -17
View File
@@ -1,29 +1,29 @@
{ {
"changelog": [ "changelog": [
"**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.", "**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 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.", "**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 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." "**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."
], ],
"components": [ "components": [
{ {
"current_version": "1.8.1-alpha", "current_version": "1.8.2-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.1-alpha/archipelago", "download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.2-alpha/archipelago",
"name": "archipelago", "name": "archipelago",
"new_version": "1.8.1-alpha", "new_version": "1.8.2-alpha",
"sha256": "50fe97705717f8176afc03285a3a459b1d4e219dba0442aa4ee5b8cea8e3ec30", "sha256": "505a456d94a17ef336b8573e1423edc2295463e603b0af89131144297cffd5ca",
"size_bytes": 59896032 "size_bytes": 59898960
}, },
{ {
"current_version": "1.8.1-alpha", "current_version": "1.8.2-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.1-alpha/archipelago-frontend-1.8.1-alpha.tar.gz", "download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.2-alpha/archipelago-frontend-1.8.2-alpha.tar.gz",
"name": "archipelago-frontend-1.8.1-alpha.tar.gz", "name": "archipelago-frontend-1.8.2-alpha.tar.gz",
"new_version": "1.8.1-alpha", "new_version": "1.8.2-alpha",
"sha256": "ae77c97e8f1ec7d1044f7d565319ddc503f6f5b87dbc9a524fa9cc9639b04f29", "sha256": "531ea4c5fc6c1116a3c09e0fc81f16acb91a86a0ad25fb4883de499713c530da",
"size_bytes": 97611693 "size_bytes": 97617354
} }
], ],
"release_date": "2026-08-13", "release_date": "2026-08-14",
"signature": "7a8932503d1c33156f79d11671bb275a2bb63347988da02562c3dd138287e7ed77c7d4cb0a1f6bcbcf8665da506fafedd2d81bb844de0cbcbaf2b9045210ca0b", "signature": "e2b5c85707121444aff6ca2150fe04dd7d73ef4f9a183eb962986554f34a20128f087c138250a42043801380f50f2e0aebe48869d1577d0171dfe4cf0ff0ac08",
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT", "signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
"version": "1.8.1-alpha" "version": "1.8.2-alpha"
} }
+84
View File
@@ -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" 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())