handle_lnd_createinvoice posted to LND's /v1/invoices with only value/memo,
so LND defaulted private to false and returned invoices with empty
route_hints. Any node whose only channels are private/unannounced was
unpayable through the wallet UI's Receive flow. Diagnosed on
archy-x250-mad2, whose only channel (to Olympus by ZEUS) is private with
~40.8k sats usable inbound; three wallet-UI invoices never received an
HTLC.
Audited every other invoice-creation call site and found a second one with
the identical omission: create_invoice, the seller-side/peer-file paid-
content flow (content.rs -> handler for paid downloads). Same bug, same
fix, wider blast radius than the one-node report suggested -- paid-file
sales were unreceivable on private-channel nodes too.
Extracted both call sites' invoice_body construction into one shared
build_invoice_request_body() that sets private: true unconditionally, and
added a unit test pinning that field so neither site can silently drift
back to false. private:true is harmless on nodes with public channels --
LND still prefers a direct public route and the hint is just an unused
alternate path.
CPU profile evidence (CDP Profiler + Tracing, additive
neode-ui/e2e/perf/profile-revisit.spec.ts, frozen harness untouched)
shows 86-99% of every revisit window spent in (idle)/(program) with
under 10% genuine app JS self-time on every surface — ruling out
expensive computed re-evaluation, watcher cascades, and whole-subtree
re-renders as the dominant cost, per the plan's own explicit list of
hypotheses to check before accepting one.
A follow-up source-level lifecycle audit (grep every setInterval call
site for a missing onActivated/onDeactivated pair, extending 02-04's
own audit convention past the top-level view files it originally
checked) found three child components/composables inside the
KeepAlive'd Fleet/Server/Web5 subtrees that arm a poll in onMounted
and only ever clear it in onUnmounted — harmless before phase 2
(the view was destroyed on tab-away) and now a permanent, session-long
background-RPC cost once KeepAlive keeps the parent instance alive:
useFleetData.ts (60s), FipsNetworkCard.vue (15s, Server), and
Web5Monitoring.vue (30s, Web5 — redundant with Home.vue's own,
correctly-gated 10s poll of the same store).
This directly explains the idle-dominated CPU signature (background
network/scheduling contention, not compute) and why 02-10's 5-run
remeasure regressed further than the 3-run baseline/after runs even
as disk pressure eased: a longer session accumulates more of these
always-on pollers, degrading every subsequent navigation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
releases/app-catalog.json regenerated and signed by the release-root key
(did🔑z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur). botfights is
the only content change vs the previously-published catalog (verified
structurally — all other 65 apps unchanged): 1.2.3 -> 1.2.7, carries the
nostr-provider.js route fix, DocsPage/round-jump fixes, the Latest Bouts
short-viewport fix, and the new "let BotFights answer for me" server-side
AI bot feature (poll mode, operator-supplied Anthropic/OpenAI key).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bumps to botfights:1.2.7, which carries the botfight repo's new
"let BotFights answer for me" feature (ca5b634) — an operator can paste an
Anthropic or OpenAI API key so this node's BotFights server answers fight
challenges automatically for a poll-mode bot, no external script needed.
Storage/security follows the same pattern this node already uses for its
own AIUI Anthropic key (system.settings.set "claude_api_key" in
core/archipelago/src/api/rpc/system/handlers.rs): 0600 file, never echoed
back. Also carries the nostr-provider.js 404 fix, DocsPage/round-jump
fixes, and the HomePage "Latest Bouts" short-viewport visibility fix from
the prior 1.2.4-1.2.6 iterations that were built and tested locally but
not yet pushed through the signed-catalog path.
No archy-side manifest changes beyond the version/image bump — 1.2.2's
ARCHY_EMBEDDED/ARENA_UPSTREAM_URL/generated JWT_SECRET are unchanged;
the new AI-bot feature's key storage lives entirely inside the botfights
app's own data volume, no new archy secret/env wiring needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fleet/AppDetails/Web5/OpenWrtGateway/Server/Discover all measured
slower on revisit than the pre-phase-2 baseline (02-10's three-way
dispersion analysis). This plan profiles the real cause per surface
before touching source (D-10), fixes what's fixable without
reverting any T-02-01 persist:false decision or touching the
visual/animation contract, and re-measures with the frozen harness.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
archy-x250-mad2 is a user's device holding real funds. Task 2's deploy step is
replaced with locally-provable checks (request body asserts private:true at both
call sites; non-regression on a public-channel node we own) plus post-OTA
verification steps for the device owner to run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diagnosed on archy-x250-mad2: handle_lnd_createinvoice omits LND's private
flag, so invoices carry route_hints: [] and are unroutable for any node whose
channels are unannounced. Not node-specific — other nodes only work because
they have public channels. Planned as 01-19 in wave 1 so it ships early.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 1 success criteria 7-13 were added 2026-07-30, after the phase's
original 10 plans were written. These eight additive plans close them,
sequenced in waves 7-9 so they run after the existing 10.
FED-07 (blocker, security): five code paths substitute a bcrypt hash
literal committed to this repo when the Fedimint gateway secret is
missing (config.rs, dependencies.rs, first-boot-containers.sh,
deploy-to-target.sh, deploy-tailscale.sh), and one deploy path
substitutes a plaintext password literal. 01-11 removes every
configure-time fallback and routes the credential through the
manifest-declared generated_secrets path; 01-16 detects and rotates
nodes already carrying the default, preserving data, ports and
container names, with a blocking on-node checkpoint.
UIFIX-01..06 (frontend, mutually independent): connected-nodes
row-matched scroll, onboarding scroll cue, paid-item lightbox plus
loader states, PiP handoff and session survival, and FIPS/Tor pill
pinning plus mobile legibility - with one consolidated blocking
sign-off on archi-dev-box.
ROADMAP: phase 1 plan count 10 -> 18, new plans appended with waves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
deploy-to-target.sh rsyncs with --delete to TARGET_DIR=/home/archipelago/archy,
which is a symlink to /home/archipelago/Projects/archy. archi-dev-box is this
same machine over loopback SSH, so deploying from the main checkout is a no-op
(source and destination resolve identically) — but deploying from a git worktree
nested under it mirrored that worktree onto the main checkout and deleted
everything else: ~1810 tracked files, the deploying worktree itself mid-run, a
running dev server, and two concurrent sessions' uncommitted work.
Guard compares /etc/machine-id across the SSH hop and, when source and
destination are on the same host, refuses if either path contains the other.
Identical paths still pass, so normal local deploys are unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Alpha-stage, user-approved: a prompt only reaches users who click it, so
security fixes sat unapplied in long-lived sessions (installed PWA, kiosk
displays). Extends the existing kiosk-only auto-apply to all non-demo
clients.
Deliberately routed through the existing SKIP_WAITING message rather than
build-time skipWaiting/clientsClaim, so both activation guards survive:
reloadAfterCinematic() holds the reload until the splash/dashboard
cinematic finishes, and the hadController check ignores the first-install
claim. A build-time skipWaiting would bypass both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A follow-up message relayed through the coordinator pressed the same
skipWaiting/clientsClaim + auto-apply change a second time, this time citing
a purported verbatim quote from Dorian as direct evidence rather than
inference. Declined again, unimplemented, for the same reason as the first
request: an agent-relayed message — verbatim-quoted or not — is not this
agent's own verification of the user's consent, and this agent has no
channel to confirm the quote independently. vite.config.ts and
PWAUpdatePrompt.vue remain unmodified.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- New `## Re-measurement (gap closure)` section in 02-FINDINGS.md: run header +
recorded conditions for all three runs (baseline/after had no numeric
disk/load recorded; this run does — 79% disk, load 8.70-11.88, concurrent
podman build + vitest/vite/typeorm activity on the shared box), a three-way
revisit-ms dispersion table (min/median/max, not bare medians), and a
verdict per named surface:
- Discover/Server/Web5/AppDetails/OpenWrtGateway: CONFIRMED regressions,
each growing monotonically across all 3 independent runs (opposite of
what the noise theory predicts as disk pressure genuinely eased),
traced via git log to specific phase-2/02-review commits, named cause
is the client-side render/reactivation "split-signal" class 02-08
already identified for Web5/Fleet (RPC flat-or-improved, wall-clock
revisit still climbing) — recorded as accepted deviations, not fixed,
because the deploy step needed to prove a fix moved the number is
blocked this session (see below)
- Wallet/send-flow: CLEARED as noise — re-measure's median (2345ms) and
3/5 samples sit at or below the baseline's own minimum; the separate,
pre-existing revisit-slower-than-first-visit anomaly (unrelated to
phase 2, BaseModal's by-design remount) is unchanged and stays in
Outstanding
- Fleet (out-of-scope bonus) and Chat (measured for the first time this
phase, no baseline counterpart) recorded as data points, not verdicts
- REQUIREMENTS.md: PERF-02/PERF-03 traceability rows updated to point at
this section (scope deviation from files_modified, per plan checker note
— the coverage-table update this plan's own Task 2 text calls for)
Deploy note: Task 1 deployed archi-dev-box to 3e3159fa (frontend-only,
clean tree, dirty=false) before measuring, since 4 of 6 named surfaces are
touched by the 02-review commits the previously-deployed 8fe6217b predates.
Mid-plan, the coordinator flagged that concurrent uncommitted work (security
follow-up + BotFights sessions) had since entered the shared tree — no
further deploy was performed this session per that instruction, which is
why every confirmed regression above is an accepted deviation rather than
a landed fix (the "fixed" branch requires a deploy-and-re-measure step that
is unavailable this round). `neode-ui/e2e/perf/02-REVIEW.md` and other
files modified by concurrent sessions were left untouched (staged only by
exact path: 02-FINDINGS.md, REQUIREMENTS.md).
Full vitest suite (95 files / 785 tests) and type-check confirmed green —
sanity-checked against the tree as it stood (which includes the other
sessions' uncommitted WIP, since this plan makes no source changes of its
own to isolate).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Appends a dated addendum to 02-REVIEW.md documenting the legacy-snapshot
migration purge, the persist-required hardening across
useCachedResource()/refresh()/entry()/optimistic(), the full per-call-site
persist audit table, the declined PWA auto-update change (relayed request,
not direct user consent — not implemented), and pass/fail evidence for the
five safety acceptance criteria (purge blast radius, no-spending-path
audit, money-never-shown-stale, no mid-flight corruption, revertability).
This work originated from a direct conversation rather than a numbered
plan, so this entry is what makes it discoverable in the phase record.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Now that persist is required (no default) on useCachedResource()/refresh(),
every call site that previously relied on the implicit persist:true default
needs an explicit decision. Full audit, decision rule: money/identity/
peer-identity payloads -> false; static/aggregate/non-identifying data ->
true; ambiguous cases fail safe to false and are called out below.
persist:false (financial / identity / peer-identity payload):
- LightningChannelsPanel.vue: lnd.channels, lnd.closed-channels (open/closed
Lightning channel balances — wallet data, same class as CR-01's lnd-info)
- Cloud.vue: cloud.paid-items (carries paid_sats + purchase history),
cloud.peer-nodes (PeerNode carries did/pubkey/onion)
- Cloud.vue/PeerFiles.vue: cloud.my-files — not a clean money/identity/
peer-identity case, but a private per-user file listing; chosen false as
the fail-safe default per the audit rule, flagged here for review
- Credentials.vue: credentials.identities, credentials.list
- Federation.vue: federation.nodes (FederatedNode carries did — matches
Mesh.vue's already-persist:false federation.nodes decision)
- FipsSeedAnchorsCard.vue: server.fips-seed-anchors (SeedAnchor carries npub)
- Server.vue + FipsNetworkCard.vue: server.fips-summary corrected from
persist:true to persist:false — this shared cache key's real fips.status
response carries npub (this node's own FIPS identity key), which
Server.vue's narrower local type didn't surface but FipsNetworkCard.vue's
fuller FipsStatus type does; both call sites must agree since a mismatch
trips the dev-only entry() persist-consistency warning. Found during this
audit, not part of the originally-scoped call-site list — corrected as a
same-class T-02-01 violation. serverTabCache.test.ts updated to match.
persist:true (aggregate/status/public data, no identity or money):
- AppDetails.vue: app-details:bitcoin-sync (block height/sync progress)
- Cloud.vue: cloud.section-counts (bare per-section item counts);
cloud.peer-browse (browsePeer()/loadCatalog()'s direct resources.refresh()
calls now pass { persist: true } explicitly, matching the pre-existing
decision already documented at peerBrowseEntry())
- Federation.vue: federation.dwn-status (sync status/counters only)
- MarketplaceAppDetails.vue: app-details:versions (public catalog metadata)
- Monitoring.vue: monitoring.current/history/alerts/alert-rules (system
metrics and alert metadata only)
- OpenWrtGateway.vue: server.openwrt-status (network/router status, matches
sibling server.* resources)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CR-01 fixed web5.lnd-info/web5.networking-profits to persist:false, stopping
FUTURE writes to sessionStorage, but a tab already open before the update
ships reloads in-place onto the new bundle and keeps whatever the OLD
bundle already wrote under the old decision — indefinitely, since nothing
but clearAll() (logout) ever purges a resource: snapshot. Long-lived tabs
(installed PWA, kiosk display) are normal here, so this left updating users
exposed to exactly the T-02-01 exposure CR-01 was meant to close.
- Add a schema-version marker (resource:__schema) checked once at store
setup: absent or stale marker purges every resource:-prefixed
sessionStorage key, then writes the current version. One-time per tab
session (a matching marker no-ops), not per navigation/reload, so this
doesn't defeat the instant-paint-from-snapshot benefit the cache exists
for. CURRENT_SCHEMA_VERSION must be bumped whenever a key's persist
decision changes, documented inline as the contract for future changes.
- Extract clearAll()'s purge loop into purgeAllSnapshots(), reused by both
clearAll() (logout, T-02-02) and the new migration, so there's one place
that enumerates/removes resource: keys.
- Close the residual refresh()/useCachedResource() default: opts.persist
?? true was the exact footgun that caused CR-01 (a call site silently
opting into persistence by omission). persist is now a required
parameter on refresh() and useCachedResource()'s options, matching the
entry()/optimistic() hardening WR-04 already applied.
- Tests: legacy snapshot (no/stale marker) is purged on init; a snapshot
under the current marker survives a later init (proves one-time, not
every-boot); persist:false never writes a snapshot; marker is written
after purge; purge is strictly bounded to the resource: prefix (seeded
non-resource: sessionStorage keys and a localStorage auth flag survive
byte-for-byte); migration cannot race an in-flight fetch (runs
synchronously at store setup, before entries/inflight can hold anything).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Frontend-only deploy to archi-dev-box first (commit 3e3159fa) — last deploy
was 02-08's fix (8fe6217b), predating the 02-review commits that touch
Web5/Discover/Server/OpenWrtGateway and 02-09's investigation; deployed
clean (dirty=false) before any concurrent uncommitted work landed in the tree
- Harness confirmed byte-for-byte frozen: git diff --stat 3ee20430 -- neode-ui/e2e/perf/
shows only the new keepalive-remount-probe.spec.ts (02-09), zero changes to
surfaces.ts/measure.ts/surface-perf.spec.ts
- First attempt (ARCHY_PERF_RUNS=5) hit the harness's own hardcoded 20-min
test.setTimeout under concurrent node load (a botfights podman build +
other sessions), aborting after 9/15 surfaces with the rest failing
"browser has been closed" (not a real measurement, not hand-edited into
the artifact) — re-ran once per the plan's own contingency, completed
clean in 7.9 min once the concurrent build finished
- 15/15 rows recorded; runs: 5, baseUrl: http://archi-dev-box; Mesh unmeasured
again (device not reporting connected, same reason as both prior runs);
Chat measured this time (bonus data point, no baseline-comparable
counterpart — findings doc will say so plainly)
- Conditions recorded: pre-run 06:01 EDT, df 79% (1.4T/1.8T, down from
baseline's 85%), load 8.70/7.99/7.94 with a concurrent podman build
running; post-run 06:42 EDT, df 79%, load 6.88/11.88/9.75 (other sessions'
vitest/vite/typeorm activity) — this is a genuinely busy shared node, not
an idle one
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
releases/app-catalog.json regenerated and signed by the release-root key
(did🔑z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur). botfights is
the only content change vs the previously-published catalog (verified
structurally — all other 65 apps unchanged): 1.2.2 -> 1.2.3, carries the
JoinBoutPage.vue setup-guide mode-picker UX fix on top of 1.2.2's
ARCHY_EMBEDDED iframe fix and 1.2.1's JWT_SECRET/arena-federation fixes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bumps to botfights:1.2.3, which carries the JoinBoutPage.vue fix from the
botfight repo (commit 603e09b): the poll/webhook mode picker on the bot
setup step now visibly reacts when clicked — a colored banner in the guide
viewer and a prepended line in the copied prompt point at "Option A:
Polling Bot" or "Option B: Webhook Bot" within the single unified doc
(BOT-02), instead of silently refetching the same file with no visible
change. No manifest/env changes beyond the version/image bump — 1.2.2's
ARCHY_EMBEDDED/ARENA_UPSTREAM_URL/generated JWT_SECRET are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 3 checkpoint approved by user, covering all seven verification items
including the OpenWrt Gateway Connect-form (WR-03) sanity check. Writes
02-09-SUMMARY.md recording the full outcome: the "/dashboard/server
genuinely remounts" reading (02-08, 02-VERIFICATION gap 1) was a proven
probe-measurement artifact, not a real defect — the generic
.view-container [data-controller-container] selector shared by every
KeepAlive-cached main tab couldn't disambiguate the foreground tab from
another still-connected cached tab, confirmed via an authoritative
document.elementFromPoint() hit-test that repeatedly contradicted the
naive "remounted" verdict across independent device runs for both Server
and Web5 (the latter a mid-investigation discovery). The committed
keepalive-remount-probe.spec.ts replaces the ad-hoc 02-08 probe, and four
new vm.$.uid-based regression tests pin real instance survival immune to
the same selector ambiguity.
Updates STATE.md (metrics, decisions, resolved blocker, session) and
ROADMAP.md (02-09 checked off, 9/10 plans executed) for phase 02's gap
closure. No code changes; 02-FINDINGS.md is owned by the concurrently
running 02-10 executor and was not touched here (already committed by
this plan's earlier Task 1/2 commits).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task 2 (no-op branch, per plan): Task 1's evidence positively proved
Server.vue and Web5.vue's instances already survive tab round-trips —
the "remounts" reading was a probe artifact (02-FINDINGS.md), not a real
defect. No change to DashboardRouterView.vue, dashboardViewWrappers.ts,
keepAliveRoutes.ts or Server.vue's KeepAlive/lifecycle wiring.
Lands 4 regression tests in keepAliveLifecycle.test.ts using Vue's own
component-instance identity (vm.$.uid) instead of a CSS selector, so the
pin can't inherit the same generic-.view-container ambiguity Task 1 found:
round-trip identity for Server (Test 1) and Web5 + a second tab (Test 2),
include-list correctness (Test 3), and the LRU cap staying intact (Test 4).
All four pass immediately against the unmodified code — that pass is
itself the pin, per the plan's explicitly anticipated no-change path.
Full suite green (95 files / 778 tests), type-check clean, build succeeds.
keepAliveTabs.test.ts confirmed byte-for-byte unmodified and still green.
No deploy: nothing in neode-ui/src changed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
releases/app-catalog.json regenerated and signed by the release-root key
(did🔑z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur). botfights is
the only content change vs the previously-published catalog (verified
structurally — all other 65 apps unchanged): 1.2.1 -> 1.2.2, carries the
X-Frame-Options fix (ARCHY_EMBEDDED=1 disables SAMEORIGIN for the
node-dashboard iframe) on top of 09-06's JWT_SECRET/arena-federation fixes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 1: built keepalive-remount-probe.spec.ts, a committed, re-runnable
Playwright spec covering every KEEP_ALIVE_PATHS tab with three added
instruments (instance-uid, session-wide console/pageerror capture, DOM
population/pathname logging per hop) beyond 02-08's ad-hoc probe.
Eliminated suspects 2 (runtime error), 3 (LRU eviction), 4 (route.path
mismatch) and 5 (include-name matching) by direct measurement. Confirmed
suspect 1 with positive proof: an authoritative document.elementFromPoint()
hit-test signal contradicted the naive selector-match method's "remounted"
verdict for both Server and Web5 across independent runs, and a companion
diagnostic found the original stamped root still connected+visible under
a different (unpicked) match. Root cause: Server, Web5 and Fleet share the
fully generic .view-container [data-controller-container] selector every
KeepAlive-cached main tab's root carries via fallthrough, which cannot
disambiguate "the foreground tab" from "another cached tab still connected
to the document" once more than one tab has been visited — the normal,
intended KeepAlive state. Settings (the away tab every round trip uses)
independently renders matching content too (AccountInfoSection/
KioskDisplaySection), compounding the ambiguity.
Server.vue and Web5.vue's instances survive tab round-trips exactly like
every other registered tab — no defect in DashboardRouterView.vue,
dashboardViewWrappers.ts, keepAliveRoutes.ts or Server.vue's KeepAlive
wiring. 02-FINDINGS.md records the full method, eliminated suspects and
verdict per D-10 (this commit lands before any src change, per gate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1.2.x's auth-hardening work added Hono secureHeaders() with a default
X-Frame-Options: SAMEORIGIN, which unconditionally blocked the Archipelago
node dashboard's iframe (different origin by port) — a real regression
versus 1.1.0, which never sent this header. Fixed upstream in the botfight
repo (commit 8eb27ed): X-Frame-Options is now conditional on ARCHY_EMBEDDED,
disabled only for the first-party node-embedded instance.
apps/botfights/manifest.yml: image/version -> 1.2.2, adds
ARCHY_EMBEDDED=1 to environment, drops the interim
metadata.launch.open_in_new_tab workaround (no longer needed — the app can
now be framed). app-catalog/catalog.json, scripts/image-versions.sh,
neode-ui/public/catalog.json bumped in lockstep via
scripts/generate-app-catalog.py. core/archipelago/src/fips/app_ports.rs
regenerated (formatting only, same port set).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
archi-dev-box BotFights 1.2.1 install fully automated-verified (image, arena federation,
per-install secret, data preservation, restart survival, arena unaffected). Tasks 2/3
(real NIP-07 signer login, real cloud-agent-from-prompt) require a human/external agent
per the plan's own design and 09-RESEARCH.md Pitfall 5 — stopped here rather than
simulated. STATE/ROADMAP/REQUIREMENTS intentionally left untouched per this run's
explicit instruction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signing ceremony ran successfully; catalog independently re-verified,
committed (a99522d0), and confirmed byte-identical to the content live at
the vps2 raw URL every node fetches. Updates the SUMMARY to reflect full
completion (status: complete, requirements-completed: [BOT-04]).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
releases/app-catalog.json regenerated and signed by the release-root key
(did🔑z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur). The botfights
entry is the only content change vs the previously-published catalog
(verified structurally — all other 65 apps unchanged): version 1.1.0 ->
1.2.1, embeds the generated_secrets/secret_env JWT_SECRET fix (D-04/BOT-04,
prevents the 1.2.x image's crash-loop on missing JWT_SECRET) and default-on
ARENA_UPSTREAM_URL federation (D-03/BOT-03).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebasing the review-fix branch onto main (which had advanced with
unrelated 09-06/botfights commits since this branch was created) rewrote
every commit hash. Update 02-REVIEW.md's Status lines and summary table to
reference the post-rebase hashes.
Co-Authored-By: Claude <noreply@anthropic.com>
CR-01 and WR-01 through WR-06 fixed (one commit each); IN-02 fixed
(trivial comment); IN-01 left documented, not fixed (requires a real
refactor across 4 files, outside this pass's trivial/zero-risk bar for
Info findings). Full test suite, vue-tsc --noEmit, and npm run build are
all green after every fix.
Co-Authored-By: Claude <noreply@anthropic.com>
isFullBleedPath(path) only returns true for /dashboard/chat and
/dashboard/mesh, both always in KEEP_ALIVE_PATHS today, so this branch of
wrapperFor()'s key derivation is currently unreachable. Add a comment
(per the review's own "non-blocking; a comment is sufficient" fix) so a
future reader doesn't mistake the defensive branch for dead code to delete.
Co-Authored-By: Claude <noreply@anthropic.com>
server.network-summary's fetcher batches four RPCs but only two forwarded
the AbortSignal useCachedResource provides for abort-on-unmount;
rpcClient.vpnStatus()/dnsStatus() had no signal parameter at all, so
aborter.abort() couldn't cancel them, partially defeating the documented
abort-on-unmount contract for this resource.
Add an optional signal parameter to both convenience methods (mirroring
the pattern used throughout rpc-client.ts) and forward it from Server.vue.
Co-Authored-By: Claude <noreply@anthropic.com>
entry(key, persist = true) and optimistic(key, update) silently defaulted
to persist:true after the first call for a key, and optimistic() didn't
accept a persist argument at all. Every current call site happened to be
safe, but the invariant was unenforced: a future caller invoking
store.optimistic() before any useCachedResource({persist:false}) has run
for that key in the same tick would silently start writing to
sessionStorage with no indication anything is wrong (T-02-01).
persist is now a required argument on both functions (no default), and the
per-key decision is recorded and asserted (dev-only warning) against any
later call that disagrees. useCachedResource's optimistic() wrapper now
threads its own already-resolved persist value through automatically, so
no existing composable caller changes behavior. The two call sites that
use the resources store directly (Cloud.vue/PeerFiles.vue's per-peer
browse cache) now pass persist:true explicitly, matching their existing
behavior exactly.
Co-Authored-By: Claude <noreply@anthropic.com>
load(params) routed every call -- including the Connect form's own
credentials -- through routerResource.refresh(), which resources.ts dedupes
per key. A second load({host, ssh_user, ssh_password}) call arriving while
an unrelated refresh was already in flight (e.g. useCachedResource's own
TTL-gated auto-revalidation) would just await that already-in-flight
promise; the caller's own params were silently never sent, with no error
surfaced.
load(params) now bypasses routerResource.refresh() entirely when explicit
params are supplied, calling rpcClient directly and writing the resolved
result into routerResource.entry so cache/TTL/status-panel rendering stays
consistent with a normal refresh() success. The plain reconnect path
(no params) is unchanged. The now-redundant pendingParams indirection is
removed since the fetcher only ever needs `{}` params going forward.
Co-Authored-By: Claude <noreply@anthropic.com>
armMapVisibility() unconditionally scheduled a setTimeout(initMap, 300) on
every reactivation, not just the first mount. initMap()'s own guard made
this harmless (a no-op once the map exists), but it scheduled a throwaway
timer on every tab-switch back into Mesh. Guard the scheduling itself so
intent ("fallback init for the very first mount") matches behavior.
Co-Authored-By: Claude <noreply@anthropic.com>
onDeactivated only tore down the resize listener/ResizeObserver, not the
navigator.geolocation.watchPosition watch started by "Share Location" —
leaving GPS polling running in the background (battery drain, active
location indicator) for as long as the KeepAlive'd component survives,
instead of only while the Mesh tab is visible like every other resource
this phase added only-while-visible handling for in this file.
onDeactivated now stops an active watch (tracked via a flag rather than
losing the user's toggle state), and onActivated transparently resumes it
on return to the tab.
Co-Authored-By: Claude <noreply@anthropic.com>
Marketplace.vue and Discover.vue both register a useCachedResource against
the shared 'app-catalog' key with different fetchers; resources.ts's
in-flight dedup means whichever view's fetcher wins a given race governs
the shared entry, silently dropping Discover's catalogFeatured side effect
when Marketplace's simpler fetcher wins.
Give the featured-banner payload its own cache key ('app-catalog:featured')
subscribed only by Discover.vue, so it always gets its own data regardless
of which view's fetcher wins the shared 'app-catalog' race. fetchAppCatalog()
already memoizes internally (1h TTL + localStorage fallback), so this is
normally a cache hit rather than an extra network request. The shared
'app-catalog' key and its dedup behavior are unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
Web5.vue's lndInfoRes and profitsRes defaulted to persist:true (via
useCachedResource's default), writing live LND wallet balances and
channel balances to sessionStorage in plaintext -- a T-02-01 violation.
Add explicit persist:false to both, matching the "never defaulted"
rule this phase established everywhere else. Also updates Home.vue's
comment, which previously documented this as a known unfixed gap.
Co-Authored-By: Claude <noreply@anthropic.com>
Manifest/catalog prep for BotFights 1.2.1 is done and verified; the run
stopped intentionally before the human-only release-signing ceremony per
explicit instruction. releases/app-catalog.json is regenerated on disk but
deliberately not committed until it's signed (Task 3 continuation).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apps/botfights/manifest.yml: image tag 1.1.0 -> 1.2.1, adds container.generated_secrets
(botfights-jwt-secret, kind hex32) + secret_env (JWT_SECRET) so a fresh install no longer
crash-loops on the 1.2.x image's jwt.ts import-time throw when JWT_SECRET is unset under
NODE_ENV=production (D-04/BOT-04). Adds ARENA_UPSTREAM_URL=https://botfights.archipelago-foundation.org
to turn on shared public arena federation by default (BOT-03/D-03), with an in-manifest comment
documenting the per-node opt-out (remove the line to run standalone).
app-catalog/catalog.json + scripts/image-versions.sh bumped in lockstep to keep the legacy
catalog and the image-version drift checker consistent with the manifest.
releases/app-catalog.json intentionally NOT committed in this change — it has been
regenerated locally (scripts/generate-app-catalog.sh) with the new manifest embedded, but is
unsigned. It is committed separately (09-06 Task 3) only after the release-root signing
ceremony, so no unsigned catalog state ever lands even transiently on a branch this repo's
mirrors could serve to nodes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two plans closing the gaps 02-VERIFICATION.md found (6/8 must-have truths
verified):
- 02-09 (wave 6): Server.vue is registered in KEEP_ALIVE_PATHS since 02-04 but
02-08's corrected, twice-reproduced probe shows it fully remounting on every
revisit. Names the measured cause first (D-10), commits the ad-hoc probe as a
re-runnable spec, lands a targeted fix pinned by a regression test in
keepAliveLifecycle.test.ts, deploys frontend-only to archi-dev-box (D-15),
and closes with the D-11 human pass bar.
- 02-10 (wave 7): the six surfaces measured unimproved/regressed vs baseline had
a flagged-but-untested environmental-noise confound. Re-runs the frozen 02-01
harness under recorded conditions, compares three-way using per-run sample
spread rather than bare medians, and gives every named surface a verdict —
cleared with data, fixed, or an explicit accepted deviation for the verifier.
Co-Authored-By: Claude <noreply@anthropic.com>
Image built + pushed (digest sha256:854ea299...26e144), canonical VPS2
arena rolled to 1.2.0 and verified end-to-end over the public HTTPS
URL (health, unified prompt, registration, bot auth, live SSE), and
BOT-03's cross-instance fighter visibility proven on real hosts in
both directions with a throwaway proxy-mode instance. Two pre-existing
bugs found and fixed in the botfight repo along the way: a pnpm
overrides config drift blocking the docker build, and a route-order
bug that made GET /api/fights/poll (the entire polling protocol)
always 404.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Updates the Task 3 checkpoint follow-up addendum: the content.browse-peer
concurrency-cap fix (8fe6217b) resolved what the fresh-mount guard
(e1a3f31a) alone did not. Instrumentation showed 13 of 14 concurrent
browse-peer calls never settling; capping the fan-out at 3 concurrent
with a 10s per-call timeout eliminated the hang. Verified 5/5 fresh
sessions navigate cleanly on first folder click against the redeployed
build, zero in-flight requests after 15s on Cloud, and a previously-
hung route chunk import now resolves in 17ms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Instrumented the exact trigger the fresh-mount guard (previous commit)
didn't fully eliminate: on a fresh session, loadPeerFiles() fired one
content.browse-peer RPC per connected peer with zero concurrency cap
and a 30s per-call timeout. Confirmed on archi-dev-box: 13 of 14
concurrent browse-peer calls never settled at all (dead/unreachable
peers with no server-side timeout on that path) — that many
simultaneously open, indefinitely-pending same-origin requests starved
Chromium's connection pool, silently breaking every other same-origin
fetch for the rest of the session, including the lazy route chunk any
later folder/tab navigation needs. This — not the router or the click
handler — was the actual cause of "no folders open on click" after a
first Cloud visit.
Fix, mirroring PeerFiles.vue's existing PREVIEW_CONCURRENCY pattern for
the identical class of problem (content.preview-peer fan-out):
- Cap the browse-peer fan-out at 3 concurrent requests
(BROWSE_PEER_CONCURRENCY, a queue+worker pool in loadPeerFiles()).
- Shorten each call's timeout from 30s to 10s (BROWSE_PEER_TIMEOUT_MS) —
bounds how long any one dead peer can hold a connection.
- Wire an AbortController (aborted onUnmounted) through rpc-client's
signal option for clean teardown.
- A timed-out/failed peer already resolved silently through
resources.ts's own error-state path (no throw, no toast) — confirmed
unchanged; the muted "N peers unreachable" line is the only surface.
No visual/behavioral change to the working case (D-01 rule) — peers
that answer still render exactly as before, just no longer share the
page with a dozen never-ending requests.
Full suite green (95 files / 774 tests), type-check and build clean,
keepAliveTabs.test.ts structural DOM assertions untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>