The AIUI-03 stale-response guard used ONE counter for every content:request,
so requests for different kinds cancelled each other.
useArchy.ts init fires content('all','own') and library('own') back to back.
Both sequence numbers are assigned synchronously, before either awaits, so the
first request always resolved with a stale number and was silently discarded.
Films, podcasts and this node's own files never reached the grid no matter what
the user did — only music ever arrived. Nothing logged, because discarding is
the guard working as written.
The guard is now keyed by kind+scope. Different kinds populate different grids
and cannot stale each other by definition; only a newer request for the same
grid can, which is what the guard was actually for. The existing out-of-order
test (same kind twice) is untouched and still passes.
This is the same shape as the defect 13-11 already fixed once: machinery built
and unit-tested end to end, while nothing real ever reached the UI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recurring regression (reported again on .228, 2026-08-06): clicking a tx
opened the tx1138.com consent modal even though the node runs Mempool.
Root cause was never the preference — getAppState() reports
'not-installed' for an app whose container list simply has not been
fetched yet, so a click that landed before the list arrived took the
external path. Timing-dependent, hence 'fixed a thousand times'.
- container store: flag + (shared in-flight
promise) so 'not yet known' is distinguishable from 'not installed'.
- openTx: awaits real data, and the local app wins whenever installed —
including stopped/restarting, where the app session's own controls are
the right landing place. Only a genuinely app-less node goes external.
- 5 regression tests incl. the race itself; vue-tsc -b clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
63 main commits since the fork point — gate cookie-strip fix, named-volume
create fix, appgate catalog classification, RNode error surfacing — merged
so 13-14/13-15 on-device verification runs against current production code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the bespoke doc-page chrome with the app's real components:
DashboardSidebar shell (256px, rgba(0,0,0,.25) + 18px blur, staggered
nav-item entrance), the AnimatedLogo neode mark with its 20 staggered
squares, sidebar-nav-item + nav-tab-active for section state, and the
Settings wallpaper behind it all.
Every bespoke container (.card/.card-sm/.step/.score-card/.callout-*/
.diagram) is gone — .glass-card from style.css is now the only box on
the page, with layout-only grids inside it.
Scroll-spy lives in nav.js rather than inline, since the node's CSP is
script-src 'self'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Layer the page over /assets/img/bg-settings.webp (fixed, cover, with a
dim gradient so prose between glass panels stays readable) and load the
actual Montserrat Bold/ExtraBold faces from the node's own web root,
instead of the flat-black background the /architecture/ guide uses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The LoRa device panel's Reticulum section (operator .126 top priority):
- Shows the device's CURRENT settings first — the radio-confirmed r_*
values from mesh.rnode-config (online badge, port, frequency, bw,
SF, CR, txpower, airtime limits), with a Refresh action.
- Every RNodeInterface parameter is editable: enabled, serial port
(auto-detect when blank), frequency, bandwidth (RNode's discrete
set), SF 5-12, CR 4/5-4/8, txpower, airtime short/long %.
- "Set recommended for <region>" fills the fields from per-region
plans (EU868 = the operator-validated Portugal plan incl. 25%/10%
duty-cycle locks); driven by the existing region selector above.
- Apply & Confirm on Device: persists, restarts the radio daemon, and
reports the radio's own confirmation (green ✓ only when the device
read-back matches; amber/red messages say what actually happened).
- Action buttons stack in a column (operator layout request).
- Reboot Radio surfaces the backend's real acknowledgement message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Standalone static guide (same pattern as /architecture/) covering how the
master seed entropy is drawn (explicit OsRng, sealed KeyGenRng allowlist,
degenerate-draw refusal, CSPRNG readiness ledger), how it is stored
(Argon2 + ChaCha20-Poly1305 envelope), the full derivation tree (HKDF
labels, NIP-06, LND aezeed one-way gate, second-order keys), what is NOT
seed-derived, every failure/fallback path, and the restore flow — in
paired layman/technical language. Linked from the Recovery-phrase card
in Settings → Backup. CSP-safe: no inline scripts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Operator-reported (2026-08-05):
- Send/Receive modals reset to a blank slate on every open. Stale state —
destination, amount, memo, and above all an armed "send all funds"
toggle — silently carried into the next payment.
- Arming "send all funds" now shows the swept balance in the (disabled)
amount field instead of a confusing 0; disarming or leaving the
on-chain tab clears it.
- The scan modal no longer hides "Scan with camera" on plain-http desktop
(browsers only allow getUserMedia on secure origins): the option stays
visible with a one-line explanation, and choosing it surfaces the HTTPS
requirement with photo/paste fallbacks. The companion app's native
scanner path is untouched and still takes priority.
- What's New entry for v1.7.125-alpha.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Declaring the Guardian UI port on the fedimint app made the orchestrator try
to publish 8175 from fedimintd, colliding with archy-fedimint-ui which
already holds it: start_container failed on every reconcile and fedimint
crash-looped (100.82.34.38). The companion's nginx pinned to 127.0.0.1 is
what actually closes that port; the gate reports it rather than fronting it.
Also: app-login page uses the sidebar's 'A' mark instead of the full
wordmark, is pinned to the small viewport so it stays centred and the
keyboard overlays rather than scrolls it, and the install-version modal
icon uses object-contain so a non-square icon is no longer cropped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
130s TTL predated the UAT timeout bump — it disarmed the approve/deny
listener while the dialog was still legitimately open (self-healing via
the next poll's re-announce, but a click in the gap dropped silently).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AIUI asks for the library the same way it asks for content, and the
fetch now actually fires without anyone typing a magic phrase:
- useArchy.ts: requestArchyLibrary(scope) sibling of requestArchyContent
(13-06), same bridge call with kind: 'library', routed through the
existing setArchyContent so the songs bucket fills exactly the way
films already does.
- init() now calls both requestArchyContent('all','own') and
requestArchyLibrary('own') once, fire-and-forget, immediately after
archyBridge.init() — the GAP-FOUND fix. 13-06 built the whole
content:request/content:push machinery and unit-tested it end to end,
but nothing in the live UI ever called it (13-06-SUMMARY.md's Known
Limitations); the fetch is now triggered by a real init-time UI event,
not merely callable.
- useContentPanel.ts's setArchyContent now also opens the panel and
populates availableTabs/activeTab/panelTitle when Archy supplied
non-empty content — previously only the data refs were set while the
tab bar and panelOpen stayed whatever the last regex-driven chat turn
left them, so real content could sit fully populated and still never
render. An empty bundle never force-opens the panel.
Deviation (Rule 2, mirrors 13-06's own archyBridge.ts precedent): kind:
'library' genuinely needs a different node-side RPC (music.list-tracks,
real tag-extracted metadata) than content.* (ContentItem has no artist/
album/duration field at all) — contextBroker.ts's handleContentRequest
gained one branch (fetchLibraryContent) to route it, and
aiui-protocol.ts's AIUIContentRequest.kind union gained the 'library'
literal, and archyBridge.ts's requestArchyContent kind param widened to
match. No second channel, no new message type, no new listener — the
existing content:request/content:push channel and its kind discriminator
carry this exactly as 13-06 designed it to. Full detail in the SUMMARY.
neode-ui: 926/926 tests green, vue-tsc -b clean. aiui: 341/344 (3
pre-existing, documented failures unrelated to this plan — 13-06/13-10
already recorded them), vue-tsc --noEmit clean.
Adds the four missing audio extensions to ShareModal.vue's extension-to-
MIME map (m4a->audio/mp4, aac->audio/aac, opus->audio/opus, wma->audio/
x-ms-wma), extracted to an exported module-scope SHARE_MIME_MAP so it's
directly fixture-testable (useFileType.test.ts convention). All three
maps agree that these eight extensions are audio/*: SHARE_MIME_MAP,
archyContentAdapter.ts's classifyByMime (13-06), and content.rs's
auto-filing check, which is prefix-only (mime_type.starts_with("audio/"))
so any correct audio/* value here already satisfies it. Existing four
entries (mp3/flac/ogg/wav) and the generic-fallback behavior for unknown
extensions are unchanged. Whole neode-ui suite green (924/924).
adaptLibraryTracks/adaptLibraryAlbums in archyContentAdapter.ts: real
tag-extracted title/artist/album/duration from the music.* index (13-07),
artist falls back to album_artist then '', order preserved from the
index's own deterministic sort (never re-sorted browser-side), no
cover-art URL (Track carries no artwork field — SongGrid's no-artwork
state renders), own-library tracks resolve through the existing
FileBrowser raw-file route, peer tracks through the existing Range-
streaming proxy, no credential ever in a query string. 34/34 tests green.
Portainer's image reaches the public catalog (the release gate caught the
manifest and catalog disagreeing), and fips-ui 8336 joins the mesh relay's
port list now that it declares a port — it is auth: gated, so the relay
withholds it rather than bridging it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-bumped so the release gate compiles the test profile at the final
version — create-release bumps after the gate, so the gate would otherwise
run on the old version and the bump would invalidate the cache, timing out
cargo-test-weekly on the compile rather than the tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Leads with the update that switched nodes off and left them unable to
switch back on — the one an operator most needs to understand, and the
reason to take this release promptly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two more on-device UAT findings:
1. Deny-retry loop: the model, told 'the user declined', simply called the
tool again — each retry minted a fresh pending and re-opened the dialog
(T-13-50 habituation, mechanized). ToolExecCtx now remembers declined
actions for the turn, keyed by confirm::action_key — the same canonical
(tool_name, validated_args) identity the nonce binds — and execute_tool
refuses a re-ask before the gate, minting nothing. Regression test
declined_action_never_reprompts_same_turn.
2. Timeout chain: rpcClient's 15s default aborted every confirmable turn
client-side while the node kept the pending alive — the next turn then
re-announced it (modal over and over) and every wait read as 'timed
out'. assistant.chat now rides a 420s timeout; AIUI's bridge goes
180s→430s so the host's error path (which also expires the dialog)
always fires first. Declined ToolResult text now also tells the model
to stop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
auth.session-policy.get/set plus a card under Account. Presented as two
plain questions rather than the token mechanism underneath, because the
distinction that matters to an operator is which control actually ends a
session: the dashboard polls constantly, so an idle timeout alone never
fires on an open tab — the absolute cap is what guarantees it.
Values are clamped server-side and the stored result is echoed back, so
the bounds are discoverable instead of an error. Presets rather than a free
number field: a box accepting '5' invites locking yourself out. A short
idle choice warns that it is the payments-industry posture.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On-device UAT: the operator was timed out mid-read (120s), the chat turn
returned 'declined' while the dialog was still up, and their Approve then
hit a dead entry ('no such pending confirmation', 13:37:12 log). Nothing
executed — the gate failed safe — but the UX was a lie in both directions.
- CONFIRM_TIMEOUT 120s→300s: human-speed per T-13-51's own rubric.
- ContextBroker dispatches aiui:tool-confirm-expired when a pending action
vanishes node-side (poll) or the turn ends; Chat.vue closes the modal on
it. Same host-only CustomEvent discipline; iframe has no path to it.
- Two new tests; 21/21 green across toolConfirm + chatAiuiEmbed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mesh right panel: a >=2560px screen hid the tab bar and stacked all five
tool panels in fixed grid rows. On a real display that clipped the Bitcoin,
Dead Man and AI headings to a few pixels each, letterboxed the map, and
pushed Radio Settings into a scroll — more screen producing a worse view.
Very wide now uses the same tabbed column as every other desktop width,
with the selected panel filling the column and the map running edge to edge
(it is the one panel with nothing to scroll).
Session policy: idle timeout, absolute cap and a re-prompt-for-funds flag,
persisted and clamped. Two tokens already existed — a session token and a
30-day login token — so the knob changes how long a quiet tab stays usable
without putting a long-lived credential on every request. Kiosk screens are
exempt from the idle timeout (nobody is there to log a TV back in) but keep
the absolute cap so a stolen box does not stay authenticated forever. The
cap is not optional theatre: idle alone never fires on a polling dashboard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
npm run build runs vue-tsc -b (project references, noUncheckedIndexedAccess),
stricter than the flat --noEmit used during Task 2 verification: indexed
CustomEvent accesses need non-null assertions, the suspended-chat Promise
needs an explicit <unknown> ctor, and one unused import. 41/41 still green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Executor stopped deliberately for a session restart (bypass-permissions relaunch).
Executor's final report: 'cargo test assistant confirm-gate suite 28/28 green,
individual nonce test passes; committing Task 1 next — first verify the
tools.rs/grants.rs/backends diffs are formatting-only.'
Task 1 (D-07/D-11 confirm gate, backend) is implemented and test-green but this
checkpoint is verbatim-uncommitted-state, NOT the reviewed atomic Task 1 commit:
continuation executor should verify diffs, then reset --soft or commit-on-top
into proper feat(13-08) task commits. Task 2 (ToolConfirmModal.vue trusted
chrome, Chat.vue + contextBroker.ts wiring) is partially built, tests written.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Leads with the honest version: five screens were open and the previous
release's own audit reported them as fine, found by scanning from another
machine rather than asking the node. States plainly that what leaked was
the page, not credentials — the macaroon path was verified, not assumed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Leads with what changes for the operator: app screens now require the node
password across LAN, Tailscale, mesh and Tor; the wallet/protocol ports that
must stay open stayed open; the mesh leak found during on-node verification;
nodes repairing their own legacy containers; and the signing-key rotation.
Known gaps disclosed, including the eleven still-undeclared ports and that
non-browser clients will now meet the login page.
The new block uses <strong> rather than the literal ** markers in earlier
entries, which render as asterisks in the modal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a Content-Security-Policy header to both nginx `location /aiui/`
blocks whose connect-src is scoped to the AIUI path prefix, so AIUI's
own JavaScript is browser-prevented from issuing a same-origin fetch
to /rpc/v1 with the ambient session cookie. Explicitly rejects the
`sandbox` iframe attribute (allow-scripts + allow-same-origin is the
known escape; dropping allow-same-origin breaks AIUI's storage and
its origin-checked bridge) and records why in both the nginx comment
and a new comment above the Chat.vue iframe. Adds
referrerpolicy="no-referrer" to the iframe so a media URL or page path
never leaks upstream via Referer.
Also adds an explicit `location /aiui/api/openrouter/ { return 404; }`
to both server blocks, closing 13-02's Task 3 checkpoint finding
(operator-accepted deviation 2026-08-03): the relay was already
structurally gone but the SPA catch-all served 200/405 instead of 404.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lane merged main at 0c4826f8, one commit before 0de67ca6 added
auth/auth_rationale to PortMapping's test constructors in prod_orchestrator.rs.
That left the lane unable to compile ANY test in the archipelago crate, which
is why 13-05 could not observe its 13 tests pass (window 19). Not a defect in
this phase's work — just staleness.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
# core/archipelago/src/main.rs
The release gate requires every CHANGELOG version to have a matching
block in Settings > What's New. Generated by scripts/sync-whats-new.py.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- aiui-protocol.ts: AIUIContentRequest (kind + optional scope, no RPC
method or params) and ArchyContentPush (adapted bundle + permitted
flag).
- contextBroker.ts: handleContentRequest gates on the media/files
permission categories (either grants access), resolves scope to
content.list-mine / content.browse-peer (fanned out across every known
federation peer) / content.owned-list, and routes results through
archyContentAdapter's adaptContentItems before crossing the iframe
boundary. contentRequestSeq is a monotonic guard: a stale RPC response
that resolves after a newer content:request has started is discarded
rather than posted (AIUI-03 concurrency edge).
- contextBroker.test.ts: permission-denied, own-scope, and stale/
out-of-order coverage. Fixed a pre-existing latent flake risk in this
file — perms.toggle() is not idempotent across tests because the
permissions store persists to localStorage, which vi.clearAllMocks()
does not reset; switched the new tests to perms.enableAll().
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- archyContentAdapter.ts: hand-written adaptContentItems mapping (D-12),
fixture-pinned at the adjacency, empty, ordering and paid-lock edges
named in AIUI-03; classifyByMime covers the m4a/aac/opus/wma extension
gap ShareModal.vue's mime map leaves today; buildMediaUrl never puts a
credential in a query string (T-13-32).
- filebrowser-client.ts: streamUrl now returns a query-free same-origin
raw-file URL, relying on the path=/ cookie login() already sets instead
of also putting the JWT in the URL (T-13-39 — closes the pre-existing
leak CONTEXT.md names, rather than merely not repeating it).
- filebrowserStreamUrl.test.ts: regression pin for the fix, including a
traversal case confirming sanitizePath behavior is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cmd/Ctrl+K search could only match text against known screens; anything it
did not recognise dead-ended at 'No results'. That text is now handed to the
assistant instead: a blue accented row (chat-bubble + sparkle) appears while
there is a query, always last in the keyboard order, so Cmd+K -> type -> Enter
reaches AIUI without the mouse. On a zero-match query it is the only option.
The prompt travels by postMessage, NOT as an iframe URL param. Chat.vue's
aiuiUrl is deliberately free of reactive dependencies so the iframe src stays
byte-identical and AIUI survives a tab switch (see the D14_FLAGS comment);
threading the question through the URL would reload AIUI and discard the
conversation on every ask — the opposite of the intent. Two regression tests
pin this: the src is byte-identical across an ask, and ask/askedAt are
stripped afterwards so a refresh cannot silently re-ask.
The ask is queued and flushed on AIUI's 'ready' handshake, because arriving
from Cmd+K on a cold Chat tab means the iframe has not connected yet.
AIUI-side receiver lands separately; until then this posts a message AIUI
ignores, which is inert rather than broken.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A vertical line crossing both dashboard cards, appearing at random on
hover and hard to catch deliberately.
Diagnosed from the screenshot rather than by reproduction. Decoding it
and scanning column by column found a lone brightness step at CSS x=633
that never returns — every legitimate container edge in the page shows
up as a PAIR of steps 2px apart (the card borders at CSS 255, 288, 850,
875, 1437), so an unpaired one is not a border. Sampling by region
placed it inside the cards and nowhere else: 10/13 rows inside My Apps,
11/11 inside Wallet, 2/10 in the gap between them, 2/13 above them. Same
screen x in both cards, which means the boundary lives in screen space
and cuts whatever backdrop-filter surface it crosses.
style.css already neutralises backdrop-filter for the shared glass
classes inside the dashboard's animated perspective/scroll containers,
because Chromium/Brave mis-rasterise it there — that block was written
for the black-rectangle corruption. `.home-card-shell` declares its own
`backdrop-filter: blur(18px)` in Home.vue and was never added to the
list, so it was the only unmitigated blur surface on the dashboard.
That is exactly the set of pixels the seam appears in. A hover repaint
re-rasterises part of the backdrop, and the refreshed half meets the
stale half at the damage boundary.
Adding it to the existing list also makes the shell consistent with the
tiles beside it: its fill is already rgba(0,0,0,0.65), the same as
.glass-card, which renders unblurred here.
The list is hand-maintained, which is how this shipped — a component
declaring backdrop-filter in its own <style> is simply not covered and
nothing fails. So the fix comes with a test that parses Home.vue for
locally-declared backdrop-filter rules and asserts each is in the
mitigation list. Verified it catches the real bug: reverting the
one-line fix makes it fail naming `.home-card-shell`.
Tests: 3/3 new, vue-tsc clean, mitigation confirmed in the built CSS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings AIUI's full 230-commit history under aiui/ via git subtree, plus main's
current head. Operator decision 2026-08-03: AIUI moves into this repo rather
than staying at git.tx1138.com. This also lands e30ac1d (13-01 Task 3), which
was stranded local-only while that remote was unreachable.
Plans 13-06, 13-09 and 13-11 still target /home/archipelago/Projects/AIUI paths
and must be re-planned against aiui/ before wave 2 runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds chat:request/chat:response to the AIUI postMessage protocol
(AIUIChatRequest, ArchyChatResponse) and a handleChatRequest handler in
contextBroker.ts that calls assistant.chat over rpcClient on the page's own
session, then posts the result back through the existing postToIframe
helper. Reuses the broker's existing allowedOrigin guard unchanged — no
second postMessage channel, no relaxed origin check.
No permission category is threaded through the chat handler on purpose:
authority is resolved node-side from CallerScope (Task 1), and duplicating
a browser-side gate here would recreate the second, divergent security
model D-02 exists to prevent. tool-call is deliberately NOT added to
AIActionType — tool selection stays node-side by D-01/D-03.
On RPC failure the handler posts only the error message, never the raw
exception object.
Verified: contextBroker.test.ts (16/16) and chatAiuiEmbed.test.ts (7/7)
green; vue-tsc --noEmit clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Promotion to Trusted is a privilege escalation — a Trusted peer can read
node state, be deployed to, and is exempt from the `!= Untrusted` gates
federation/DWN/messaging use. It must therefore cost a fresh proof that
the person at the keyboard is the operator, not merely that a session
cookie exists. Same reasoning as node.rotate-identity and TOTP setup,
both of which already re-verify.
Both entry points are covered:
- `federation.invite` gates on the RESOLVED level, not on an explicit
request for Trusted: "Link Your Nodes" sends no `trust_level` at all
and falls through to the Trusted default. The invite is a bearer grant
of Trusted to whoever redeems it, so minting it IS the escalation.
Observer invites are untouched.
- `federation.set-trust` gates only when the peer is not already
Trusted, so the dropdown re-emitting its own value doesn't demand a
password for a no-op.
Demotion is deliberately NOT gated: making something less privileged
must never be harder than leaving it alone, or the safe action becomes
the inconvenient one.
The backend is the sole authority on what counts as an escalation — it
returns a `PASSWORD_REQUIRED:`-prefixed error and the UI prompts and
retries only on that, so the rule lives in exactly one place and the
frontend never pre-judges. TrustPasswordModal.vue (modelled on
RotateDidModal.vue) serves both flows. NodeDetailModal's select snaps
back to the node's real level on change, since a cancelled or failed
promotion would otherwise leave the dropdown displaying a level the node
never accepted.
The operator path stamps TrustSource::Manual; set_trust_level grew an
`Option<TrustSource>` so automatic adjustments (the discovery-handshake
demotion safety net) pass None and leave the recorded provenance alone
rather than laundering an uninvited-join peer into looking approved.
Follow-up, deliberately out of scope: `federation.join` also reaches
Trusted when redeeming someone else's Trusted invite, with no re-auth.
Tests: 44/44 federation, 79/79 rpc-client, vue-tsc clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Generated by scripts/sync-whats-new.py, which the release gate checks.
Without it the Settings > What's New modal would have skipped straight
from v1.7.119 to v1.7.121 — the release notes users actually read, as
opposed to CHANGELOG.md which they do not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported: the nav above the bottom bar — back buttons, the mesh tabs —
stayed stuck across other screens.
Cause is the KeepAlive work from phase 2, and specifically the half of it
that is invisible from the view's own file. Main tabs are KeepAlive'd, so
navigating DEACTIVATES a view instead of unmounting it. Content the view
Teleports to <body> is not in the view's DOM subtree, so deactivation
does not remove it and it keeps rendering over the destination screen.
Two offenders, matching the report exactly:
- Mesh.vue teleports its mobile TAB BAR and its chat BACK BUTTON to
<body>, gated only on `mobileShowChat` — never on whether Mesh was the
screen you were looking at.
- components/BackButton.vue teleports the shared mobile back button with
NO gate at all, so it leaked out of every view that uses it. Fixing the
shared component fixes every caller at once: Vue propagates
activated/deactivated from the KeepAlive boundary down through the
subtree, so a child can guard itself.
BaseModal already solved the transient-dialog half of this class in
204d4523 by closing on route change. That is the right fix for a dialog
and the wrong one for chrome: a tab bar has no "closed" state to fall
back to, and forcing one would lose the user's place. New
useViewActive() composable instead — chrome is simply not rendered while
its owner is off screen, and returns exactly as it was.
THE PERFORMANCE IS NOT SACRIFICED, which was the explicit constraint.
The Teleport is gated, not the view, so the instance stays cached and
revisiting a tab is still instant. A test pins this: setup() must run
exactly ONCE across a navigate-away-and-back round trip. If someone
later "fixes" this by dropping KeepAlive, that test fails.
Deliberately untouched: AppSession.vue, whose teleport is load-bearing —
its own comment records that moving the iframe node reloads the app, and
app-session is excluded from KeepAlive anyway so it cannot leak. Toasts,
the app launcher and the connection banner are app-level rather than
view-owned; gating those would be wrong.
Verified: 3 new tests; full suite 105 files / 848 tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A failed federation sync existed only as a `debug!` line on the node, so a
peer that had not synced in days looked identical in the UI to one that
synced a minute ago. Now the failure is persisted per peer and rendered.
- `FederatedNode.last_sync_error` / `.last_sync_error_at` — the failure-side
mirror of the existing `last_transport` / `last_transport_at` pair.
- `federation::record_sync_result(data_dir, did, outcome)` — records the
message on `Err`, CLEARS both fields on `Ok` so the badge disappears when
the peer recovers. Runs under FEDERATION_STORE_LOCK via the `*_inner`
load/save convention established by plan 01-01. An unknown DID is a silent
Ok that writes nothing, so a peer removed mid-pass is never resurrected by
an in-flight sync's error write. Skips the save entirely when nothing
changed, keeping the steady state read-only rather than rewriting
nodes.json (and contending for the lock) every 90s.
- Message truncated to MAX_SYNC_ERROR_CHARS (256), counted in chars not
bytes so truncation cannot split a UTF-8 sequence (T-01-18).
- The 90s auto-sync loop calls it on both arms; the existing `debug!` line
is kept — persisting is additive, not a replacement for logs.
- `federation.list-nodes` emits both fields when set, omits them when unset.
- NodeList renders a red SYNC badge beside the transport badge on both the
trusted-node and peer rows, message + age in the `title` so the row stays
single-line.
Tests (written first, confirmed failing — 16 compile errors, E0425 on
`record_sync_result` and E0609 on `last_sync_error`):
- persists_error / success_clears_error / missing_did_is_noop /
on_empty_store_is_noop / truncates_long_error
- NodeList: badge present when set, ABSENT when unset (the guard against a
badge that always renders), and present on an observer peer row.
cargo test -p archipelago federation — 42 passed, 0 failed.
vitest NodeList.test.ts — 4 passed. npm run build — green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Audited every transport-pill render site in the cloud surfaces at 390x740
and 320x640 in a real browser. Two sites render a pill (Cloud.vue peer
cards, PeerFiles.vue header) and both already appear on a phone; three
file-level sites carry none, by decision recorded in the SUMMARY.
- Cloud.vue peer-card badge row: add flex-wrap + shrink-0 on the transport
badge. Measured at 320px, a longer trust label squeezed the badge until
its own text broke mid-label ("TOR ." / "120.0s"). It now drops to a
second line intact. Inert whenever the row fits, so desktop is unchanged.
- New TransportPills.test.ts: one site-specific assertion per render site,
so removing a pill fails the build. Dorian asked that these never be
removed in a future cleanup; nothing in the repo pinned them before.
- Unknown-transport cases assert no pill is fabricated (T-01-78), and the
labels/colours are asserted against PeerFiles.vue's canonical mapping
rather than a duplicated table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clicking "Open a channel" or "Setup Guide" navigated correctly but left the
wallet's send/receive modal floating over the destination. The Lightning modal
itself did close — the parent did not. Tab views are KeepAlive'd, so
navigating deactivates the owner rather than unmounting it, and its Teleported
modal keeps rendering.
BaseModal now emits close on any route change while shown, fixing the class in
one place rather than per button. Every modal here is a transient dialog; none
should survive navigation. Two tests pin it, including that a hidden modal
stays quiet.
Also fixes a test-only regression from fa26c5fc: useLightningRequired()
resolved the Pinia store at composable-call time, so merely having the gate in
SendBitcoinModal made it unmountable without an active Pinia (PaidTick mounts
it bare). The store is now resolved lazily inside the function that needs it —
a gate should never be what breaks a component's ability to mount. That one
shipped because I verified fa26c5fc with targeted tests and a build but had
not re-run the full suite since 5718179e.
Verified: full suite 103 files / 827 tests green; npm run build clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A running LND with zero channels happily mints an invoice — it is simply
unpayable, because nobody has a route in. So the state-only gate let receive
through and handed the user a useless invoice, and let send walk to confirm.
Neither errored, so the funding modal (wired to failures) never fired.
requireLightningReady(direction) now asks lnd.listchannels and checks the
liquidity that actually matters for the attempt: total_inbound to receive,
total_outbound to send. It fails OPEN on an RPC error — a transient blip
should not block a working wallet.
The no-funds mode says plainly that a channel is needed, in the direction's
own terms (inbound vs outbound), and offers both routes: "Open a channel"
straight to the channels screen where the Zeus/Olympus flow is already
prefilled, and "Setup Guide" to the run-lightning-node walkthrough for someone
who wants the whole path explained. Buttons wrap rather than squeeze on
narrow screens.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Icons: each node choice now shows its app icon (lnd.png; Core Lightning's is
vendored from the Umbrel gallery as core-lightning.svg). Vendored rather than
hotlinked on purpose — these nodes run offline/airgapped, and a remote image
would both break there and leak a request to a third-party host on every
render. A missing asset falls back to a neutral bolt glyph so a row can never
render a broken-image box.
Mobile: the choice row keeps icon + name + blurb together and drops the action
to its own full-width line under 26rem, instead of squeezing the description
into a two-word column next to a button.
Funding mode: a node that is running but has no funds / no inbound liquidity
is neither "install one" nor "start it", so the same modal gains a third mode
that explains it and routes to the run-lightning-node goal, where funding and
channel-opening already live — reusing that flow rather than duplicating it.
It fires where the user actually meets the problem: on a failed attempt.
handleLightningFailure() maps a running node's send/receive failure onto the
funding modal, matched on message text because LND surfaces "no route", "no
channels" and "insufficient balance" as plain strings with no distinct code —
and all three mean the same thing to a user: fund me.
Verified: 5 gate tests; npm run build clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three defects from testing the previous commit on archi-dev-box:
1. The gate keyed on `id in packages`, which is not "installed and usable" —
package-data carries an entry for a Lightning app that is known but not
running. On a box with no lnd container at all the gate passed and the raw
error came through as "Operation failed. Check server logs for details."
Now keyed on PackageState.Running.
2. Because installed-but-stopped is a real and different situation, the modal
has two modes: absent offers the install choices, stopped says the node
isn't running and offers "Open My Apps". Neither dead-ends in an error.
3. Lightning SEND let you walk all the way to confirm-send with no node. The
gate now runs in review(), before the confirm step — failing at submit
after a review screen is the defect, not a smaller version of it.
Also adds CopyButton, the start of one consistent copy affordance: icon +
label, an emerald tick held 1.6s, a fixed box so the width never jumps, and a
document.execCommand fallback so copy still works over plain http on a LAN IP
(navigator.clipboard rejects on insecure origins, which is how a lot of nodes
are reached). Converted the wallet's own copies — the lightning invoice the
user reported, plus the on-chain/Ark addresses and the payment hash/txid.
20 of 25 copy sites across 15 other files still use ad-hoc markup; converting
them is mechanical but was not attempted here rather than half-done.
Verified: 5 gate tests; npm run build clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Creating a Lightning invoice with no Lightning implementation installed failed
at the RPC layer — lnd.createinvoice returned connection-refused and the
Receive screen rendered it as a red error. That reads as the wallet being
broken when the node simply has no Lightning node installed yet.
useLightningRequired() gates the three invoice paths (wallet Receive, the Web5
send/receive sheet, and the app launcher's paywall — both arms there, since
paying an invoice needs a node as much as minting one). With none installed it
raises a modal offering to install one and the caller bails without surfacing
an error at all.
The modal lists the choice rather than assuming LND: LND installs today, Core
Lightning is listed greyed as "Coming soon" so the platform doesn't read as
LND-only. When CLN ships it is two lines — flip `available` and add the id to
LIGHTNING_NODE_APP_IDS.
Detection is install state, NOT reachability, deliberately: an installed node
that is merely stopped or still starting is a different problem ("start it")
and must not be answered with "install a Lightning node".
Also fixes the credentials modal, which painted its own rgba(8,10,18,.98)
navy card instead of the house glass-card — it read as blue against every
other modal. It existed twice (Apps.vue and apps/AppIconGrid.vue); both now
use BaseModal, so they also inherit Esc/focus handling, body scroll lock and
the standard pinned-header/footer scroll contract they were missing. Dead
panel CSS removed from both.
Verified: 4 new tests; full suite 103 files / 826 tests green; npm run build
clean with the new strings present in the built bundle.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reactions, replies, read-receipts, edits, deletes, forwards and channel sends
shared one bare `{ ok: true, sent: true }` case, so none of them rendered on
the demo — the UI derives reaction chips and reply quotes from the message
store, and there was nothing in it to derive from.
Each now mirrors its daemon counterpart. Reactions/replies/receipts push typed
messages carrying the { sender_pubkey, sender_seq } target key Mesh.vue's
reactionIndex and replyTargetPreview read. Edits rewrite the text and set
edited_at; deletes tombstone IN PLACE (plaintext, typed_payload.deleted,
message_type 'delete') because that is what mesh/mod.rs apply_local_delete
does — it does not remove the row.
Edits and deletes go through a per-session overrides overlay keyed by
sender_seq, because mesh.messages rebuilds its seed array on every read, so
in-place mutation would only ever work for messages sent this session.
mesh.refresh and mesh.reboot-radio stay acknowledgements on purpose — the
daemon's handlers have no message-store effect either — with a comment saying
so, so a later reader does not "fix" them into divergence.
Also completes the phase bookkeeping for 01-02/03/11/12/13/14/15 and lands the
orphaned 01-12/01-14 SUMMARYs.
Verified: parity harness 17/17 live assertions; full frontend suite 102 files
/ 822 tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ten methods the UI calls had no case at all in mock-backend.js, so the demo
answered them with "Method not found" and the frontend swallowed it in a
try/catch — peer renaming, scheduling, clear-all, the assistant panel and two
federation actions were all silently inert on the demo.
Each new handler mirrors its daemon counterpart and cites the Rust source it
mirrors, per the house convention above mesh.transport-advice:
mesh.contacts-list/-save typed_messages.rs (contacts merged over peers by
pubkey_hex; absent params leave stored fields)
mesh.clear-all status.rs ({ status: "cleared" })
mesh.schedule/list/cancel assistant.rs + scheduler.rs ScheduledMessage
mesh.assistant-status/-configure assistant.rs (key-presence semantics)
federation.cancel-request handlers.rs (outbound+sent only, notify defaults
true) — plus an outbound seed request, since
without one the demo's cancel path was
unexercisable
federation.notify-did-change handlers.rs ({ notified, failed, results })
mesh.peers and mesh.contacts-list now share one DEMO_MESH_PEERS list so they
cannot disagree about who is on the mesh, and the peer with no pubkey_hex is
omitted from contacts exactly as the daemon omits it.
scripts/mock-rpc-parity.mjs cross-references UI call sites against mock cases
and then drives a live scripted RPC sequence against an ephemeral-port
instance (MOCK_BACKEND_PORT). It matches only `method: '<x>'`, NOT bare string
literals: Mesh.vue and Federation.vue use the same dotted names as
resource-cache keys, and matching those would report permanent phantom gaps.
Fail-first proof: disabling the mesh.clear-all case makes the harness exit 1
naming the gap; restored, it exits 0 twice in a row with no stray listener.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both paid-tick surfaces now show the EQ-segment ring instead of a CSS ripple
burst (send modal) and a plain circle (scan modal), via a new `badge` size
variant at 160px/192px with matching --viz-radius. A transform scale of the
compact variant was ruled out in 01-UI-SPEC.md because it would scale segment
stroke width and blur along with the geometry.
Both surfaces use the identical composition — a badge-sized relative container
with the checkmark core absolutely centred over the ring — so the two ticks
cannot drift apart visually.
ScreensaverRing also gains the prefers-reduced-motion guard it never had, on
the component rather than per call site, so the screensaver and
SystemDangerZone variants are covered too.
Also records live-browser verification for 01-13's scroll cue (three
viewports, plus a geometry probe of the hide condition).
Verified: 5 new tests; full suite 102 files / 822 tests green; npm run build
clean with viz-ring-badge present in the built bundle. The live visual
no-clipping observation Task 2 asks for is explicitly NOT done — recorded as
deferred to plan 01-07's consolidated sign-off.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>