diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md
deleted file mode 100644
index 026cb8ef..00000000
--- a/.claude/agents/code-reviewer.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-name: code-reviewer
-description: Reviews Archipelago code changes for quality — frontend patterns, Rust safety, container security, crypto rules, and project conventions.
-tools: Read, Grep, Glob
-model: sonnet
----
-
-You are an Archipelago code reviewer. Check changes against project standards.
-
-## Frontend (neode-ui/)
-- `';
- sub_filter_once on;
- sub_filter_types text/html;
-
- # Required: disable upstream compression
- proxy_set_header Accept-Encoding "";
-}
-```
-
-**Use cases:**
-- Injecting a postMessage bridge (e.g., NIP-07 Nostr provider)
-- Adding resize reporting scripts
-- Injecting theme CSS
-- Adding custom error handlers
-
-**Safety rules:**
-- Only inject into `text/html` responses
-- Inject before `` or after `
` — never in the middle of content
-- The injected script should check `if (window === window.top) return` to only activate inside iframes
-- Use `sub_filter_once on` to prevent double-injection
-
----
-
-## 11. Performance Considerations
-
-### iframe Resource Impact
-
-Each iframe creates:
-- Separate browsing context (DOM, CSS engine, JS runtime)
-- 10-50MB memory per iframe depending on app complexity
-- Own JavaScript execution on main thread
-
-### Mitigation
-
-- Only load visible iframes (`loading="lazy"` or Intersection Observer)
-- Destroy iframes when hidden (remove from DOM, not just `display:none`)
-- Use `about:blank` for pre-created iframe elements, set real src when needed
-- Limit concurrent iframes to 3-5 for acceptable performance
-- Consider `credentialless` for public content (lighter weight)
-
-### Caching
-
-- iframes follow standard HTTP caching (Cache-Control, ETag)
-- Setting `src` to the same URL does NOT trigger reload
-- To force reload: append query param (`?t=${Date.now()}`) or call `iframe.contentWindow.location.reload()` (same-origin only)
-
----
-
-## 12. Debugging Checklist
-
-When an app doesn't work in an iframe, check in this order:
-
-1. **Check response headers:**
- ```bash
- curl -sI http://localhost:{PORT} | grep -iE 'x-frame|content-security|cross-origin'
- ```
-
-2. **Check if Nginx is stripping headers:**
- ```bash
- curl -sI http://{node-ip}/app/{id}/ | grep -iE 'x-frame|content-security'
- ```
-
-3. **Check browser console** for:
- - "Refused to display in a frame" → XFO or frame-ancestors blocking
- - "Mixed Content" → HTTP iframe on HTTPS page
- - "WebSocket connection failed" → Missing WebSocket proxy config
- - "net::ERR_BLOCKED_BY_RESPONSE" → COEP/CORP/COOP headers blocking
-
-4. **Check if app has JavaScript frame-busting:**
- - Open the app directly, view source, search for `window.top`, `window.parent`, `frameElement`
-
-5. **Check if cookies/auth work:**
- - Open DevTools → Application → Cookies in the iframe context
- - Look for blocked cookies (yellow warning triangle)
-
-6. **Check base path issues:**
- - DevTools → Network tab → look for 404s on CSS/JS/API requests
- - If assets load from `/` instead of `/app/{id}/`, the app needs base path config
-
-7. **Check WebSocket connections:**
- - DevTools → Network → WS tab → check if WebSocket connections upgrade successfully
-
----
-
-## 13. Archipelago-Specific Patterns
-
-### Port-to-Proxy Mapping
-
-The `appLauncher.ts` store maintains `PORT_TO_PROXY` mapping: direct ports → `/app/{name}/` paths. When running on HTTPS, direct HTTP port URLs are rewritten to same-origin proxy paths via `toEmbeddableUrl()`.
-
-### mustOpenInNewTab Detection
-
-Apps that cannot work in iframes are listed in `IFRAME_BLOCKED_HOSTS` (external sites) and port-based checks (local apps with unstrippable restrictions). These automatically open in a new browser tab.
-
-### Nostr Provider Injection
-
-All proxied apps receive `/nostr-provider.js` via `sub_filter` injection. This provides `window.nostr` (NIP-07) inside iframes, allowing apps to request signing, key access, and encryption from the parent portal without exposing secret keys.
-
-### Identity Protocol
-
-Identity-aware apps (IndeedHub) receive user identity via `archipelago:identity` postMessage after an identity picker modal. Identity includes DID, pubkey, npub, and a signed challenge for verification.
-
-### Payment Protocol
-
-Apps can request Bitcoin payments via `archipelago:payment-request` postMessage. The parent validates, shows a confirmation modal, executes the payment (ecash/LN/on-chain based on amount), and responds with a receipt.
-
-### iframe Load Fallback
-
-If an iframe fails to load within 15 seconds or loads empty content, a fallback UI is shown with a "Can't display in frame" message and an "Open in new tab" button.
-
----
-
-## Decision Framework
-
-When adding a new app to Archipelago:
-
-```
-1. Does the app set X-Frame-Options or CSP frame-ancestors?
- ├── No → iframe via /app/{id}/ proxy, done
- └── Yes →
- 2. Can you strip headers at Nginx?
- ├── Yes, and app works → iframe via /app/{id}/ proxy
- └── App still broken after stripping →
- 3. Does the app have JavaScript frame-busting?
- ├── Yes → Open in new tab (add to mustOpenInNewTab)
- └── No →
- 4. Is it a base path issue?
- ├── Yes → Configure app's native base path or use sub_filter
- └── No →
- 5. Is it a WebSocket issue?
- ├── Yes → Add WebSocket proxy config
- └── No →
- 6. Is it a cookie/auth issue?
- ├── Yes → Same-origin proxy should fix it
- └── No → Debug with browser DevTools, check console errors
-```
diff --git a/.claude/hooks/block-risky-bash.sh b/.claude/hooks/block-risky-bash.sh
deleted file mode 100755
index 0a68d6b9..00000000
--- a/.claude/hooks/block-risky-bash.sh
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/env bash
-# PreToolUse Bash guard: block dangerous shell commands.
-# Denies: rm -rf, git reset --hard, git push -f, git clean -fd, chmod -R 777,
-# fork bombs, block device overwrites, mkfs, building Rust on macOS for Linux.
-set -euo pipefail
-
-INPUT=$(cat)
-CMD=$(python3 -c "
-import json, sys
-try:
- data = json.loads(sys.stdin.read())
- print(data.get('tool_input', {}).get('command', ''))
-except: pass
-" <<< "$INPUT")
-BASE="${CLAUDE_PROJECT_DIR:-}"
-[[ -z "$BASE" ]] && BASE=$(python3 -c "
-import json, sys
-try:
- data = json.loads(sys.stdin.read())
- print(data.get('cwd', ''))
-except: pass
-" <<< "$INPUT")
-[[ -z "$BASE" ]] && BASE="$(pwd)"
-
-# Normalize: collapse whitespace, strip leading/trailing
-CMD_NORM=$(echo "$CMD" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
-
-deny() {
- local reason="$1"
- python3 -c "
-import json
-print(json.dumps({
- 'hookSpecificOutput': {
- 'hookEventName': 'PreToolUse',
- 'permissionDecision': 'deny',
- 'permissionDecisionReason': '$reason'
- }
-}))
-"
- exit 0
-}
-
-# Dangerous patterns
-case "$CMD_NORM" in
- *"rm -rf"*|*"rm -fr"*|*"rm -f -r"*|*"rm -r -f"*) deny "Destructive rm -rf blocked by security hook" ;;
- *"git reset --hard"*) deny "git reset --hard would lose uncommitted work" ;;
- *"git push --force"*|*"git push -f"*|*"git push -f "*) deny "git push --force would rewrite history" ;;
- *"git clean -fd"*|*"git clean -f -d"*) deny "git clean -fd deletes untracked files" ;;
- *"chmod -R 777"*|*"chmod -R 0777"*) deny "chmod -R 777 is a security risk" ;;
- *":(){ :"*"};:"*) deny "Fork bomb pattern blocked" ;;
- *"> /dev/sd"*|*">/dev/sd"*) deny "Block device overwrite blocked" ;;
- *"mkfs "*|*"mkfs."*) deny "Disk format command blocked" ;;
-esac
-
-# Block building Rust locally on macOS (should always build on dev server)
-if [[ "$(uname)" == "Darwin" ]]; then
- if echo "$CMD_NORM" | grep -qE '^\s*cargo\s+build'; then
- # Allow if it's clearly an SSH command (building on remote)
- if ! echo "$CMD_NORM" | grep -qE 'ssh|sshpass'; then
- deny "NEVER build Rust on macOS — use ./scripts/deploy-to-target.sh --live or build on dev server via SSH"
- fi
- fi
-fi
-
-# Check for path traversal escaping project root
-if [[ -n "$BASE" ]] && [[ -d "$BASE" ]]; then
- if echo "$CMD_NORM" | grep -qE '\.\./|/\.\.'; then
- if echo "$CMD_NORM" | grep -qE '(rm|mv|cp|cat|chmod|chown)\s+.*\.\.'; then
- if echo "$CMD_NORM" | grep -qE '\brm\b.*\.\.'; then
- deny "Path traversal with rm blocked"
- fi
- fi
- fi
-fi
-
-exit 0
diff --git a/.claude/hooks/post-deploy-check.sh b/.claude/hooks/post-deploy-check.sh
deleted file mode 100755
index ea49b0ac..00000000
--- a/.claude/hooks/post-deploy-check.sh
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/usr/bin/env bash
-# PostToolUse Bash hook: detect deploy commands and remind to test.
-# Triggers after deploy-to-target.sh runs.
-set -euo pipefail
-
-INPUT=$(cat)
-
-CMD=$(python3 -c "
-import json, sys
-try:
- data = json.loads(sys.stdin.read())
- print(data.get('tool_input', {}).get('command', ''))
-except: pass
-" <<< "$INPUT")
-
-# Only trigger on deploy commands or git push
-if ! echo "$CMD" | grep -qE 'deploy-to-target|git\s+push'; then
- exit 0
-fi
-
-TIMESTAMP=$(date '+%Y-%m-%d %H:%M')
-
-python3 -c "
-import json
-
-message = '''Deploy detected at $TIMESTAMP.
-
-Post-deploy checklist:
-1. Test the web UI at http://192.168.1.228
-2. Verify modified apps load correctly
-3. Check backend logs: sudo journalctl -u archipelago -n 20
-4. Check nginx: sudo tail -f /var/log/nginx/error.log
-5. If building ISO, sync system configs to image-recipe/configs/
-6. Update CHANGELOG.md if this is a notable change'''
-
-output = {
- 'hookSpecificOutput': {
- 'hookEventName': 'PostToolUse',
- 'deployReminder': message
- }
-}
-print(json.dumps(output))
-"
diff --git a/.claude/hooks/protect-files.sh b/.claude/hooks/protect-files.sh
deleted file mode 100755
index 3ff5453a..00000000
--- a/.claude/hooks/protect-files.sh
+++ /dev/null
@@ -1,82 +0,0 @@
-#!/usr/bin/env bash
-# PreToolUse Edit|Write guard: block edits outside project and to protected paths.
-# Denies: paths outside project, .git/, .env*, lockfiles, node_modules/, deploy-config.sh
-set -euo pipefail
-
-INPUT=$(cat)
-FILE_PATH=$(python3 -c "
-import json, sys
-try:
- data = json.loads(sys.stdin.read())
- print(data.get('tool_input', {}).get('file_path', ''))
-except: pass
-" <<< "$INPUT")
-BASE="${CLAUDE_PROJECT_DIR:-}"
-[[ -z "$BASE" ]] && BASE=$(python3 -c "
-import json, sys
-try:
- data = json.loads(sys.stdin.read())
- print(data.get('cwd', ''))
-except: pass
-" <<< "$INPUT")
-[[ -z "$BASE" ]] && BASE="$(pwd)"
-
-# Resolve to absolute path
-if [[ -z "$FILE_PATH" ]]; then
- exit 0
-fi
-ABS_BASE=$(cd "$BASE" 2>/dev/null && pwd) || true
-[[ -z "$ABS_BASE" ]] && ABS_BASE=$(python3 -c "import os,sys; print(os.path.abspath(os.path.normpath(sys.argv[1])))" "$BASE" 2>/dev/null) || true
-[[ -z "$ABS_BASE" ]] && ABS_BASE="$BASE"
-[[ "$ABS_BASE" != */ ]] && ABS_BASE="${ABS_BASE}/"
-if [[ "$FILE_PATH" != /* ]]; then
- ABS_PATH="$ABS_BASE${FILE_PATH#./}"
-else
- ABS_PATH="$FILE_PATH"
-fi
-ABS_PATH=$(python3 -c "import os,sys; print(os.path.abspath(os.path.normpath(sys.argv[1])))" "$ABS_PATH" 2>/dev/null) || true
-[[ -z "$ABS_PATH" ]] && ABS_PATH="$ABS_BASE${FILE_PATH#./}"
-
-deny() {
- local reason="$1"
- echo "Blocked: $ABS_PATH — $reason" >&2
- python3 -c "
-import json
-print(json.dumps({
- 'hookSpecificOutput': {
- 'hookEventName': 'PreToolUse',
- 'permissionDecision': 'deny',
- 'permissionDecisionReason': '$reason'
- }
-}))
-"
- exit 0
-}
-
-# Protected patterns
-PROTECTED_PATTERNS=(
- ".git/"
- ".env"
- ".env.local"
- "node_modules/"
- "package-lock.json"
- "scripts/deploy-config.sh"
-)
-
-for pattern in "${PROTECTED_PATTERNS[@]}"; do
- if [[ "$ABS_PATH" == *"$pattern"* ]] || [[ "$ABS_PATH" == *"/$pattern" ]]; then
- deny "Edit blocked: path matches protected pattern ($pattern)"
- fi
-done
-
-# .env.*.local
-if [[ "$ABS_PATH" =~ \.env\..*\.local$ ]]; then
- deny "Edit blocked: .env.*.local files contain secrets"
-fi
-
-# Ensure path is under project root
-if [[ "$ABS_PATH" != "$ABS_BASE"* ]] && [[ "$ABS_PATH" != "$BASE"* ]]; then
- deny "Edit blocked: path is outside project directory"
-fi
-
-exit 0
diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md
deleted file mode 100644
index 0584a700..00000000
--- a/.claude/memory/MEMORY.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# Archipelago Project Memory Index
-
-## Setup & Architecture
-- [claude-proxy-setup.md](claude-proxy-setup.md) — Claude proxy OAuth setup details
-- [deploy-automation.md](deploy-automation.md) — Deploy script automation TODOs (API key, AIUI nginx, swap)
-
-## Servers & Deploy
-- [project_environments.md](project_environments.md) — Four environments: dev mode, dev server/prod, demo
-- [tailscale_servers.md](tailscale_servers.md) — Tailscale server details (archipelago-2, archipelago-3)
-- [reference_tailscale_nodes.md](reference_tailscale_nodes.md) — All node IPs and SSH commands
-- [second-server.md](second-server.md) — Second dev server (archipelago-2 via Tailscale)
-- [third-server.md](third-server.md) — Third dev server (archipelago-3 via Tailscale)
-
-## Features & Plans
-- [pending-features.md](pending-features.md) — Feature requests: kiosk mode, sideloading, Nostr login, etc.
-- [project-plan.md](project-plan.md) — Overall project plan status
-- [web-only-apps.md](web-only-apps.md) — Web-only apps (L484 category) and iframe compatibility
-
-## User Feedback
-- [feedback_app_display_modes.md](feedback_app_display_modes.md) — App browser: 3 display modes with persistent setting
-- [feedback_fullscreen_modals.md](feedback_fullscreen_modals.md) — Fullscreen modal preferences
-- [feedback_local_dev.md](feedback_local_dev.md) — Local dev: use `cd neode-ui && ./start-dev.sh`
-- [feedback_apps_always_direct_port.md](feedback_apps_always_direct_port.md) — Apps MUST open at direct port, NEVER proxy paths
-- [feedback_indeedhub_nginx_ips.md](feedback_indeedhub_nginx_ips.md) — IndeedHub nginx must use hardcoded container IPs
-- [feedback_searxng_no_cap_drop.md](feedback_searxng_no_cap_drop.md) — SearXNG: no cap-drop ALL
-
-## ISO Build
-- [iso-build-session-2026-03-10.md](iso-build-session-2026-03-10.md) — ISO build session notes
-- [unbundled-iso.md](unbundled-iso.md) — Unbundled ISO approach notes
-
-## Infrastructure
-- [project_bitcoin_rpc_auth.md](project_bitcoin_rpc_auth.md) — Bitcoin rpcauth, system Tor, reboot survival, container resilience
-
-## Deploy & Container Fixes
-- [project_deploy_session_2026_03_22.md](project_deploy_session_2026_03_22.md) — Fleet deploy fixes: credential mismatches, restart storms, rootless port 80, deploy script hardening
-
-## Gamepad Navigation
-- [project_gamepad_nav.md](project_gamepad_nav.md) — Controller nav system, key files, patterns, Chromium gotchas
-
-## Completed Work
-- [project_mesh_198_issue.md](project_mesh_198_issue.md) — Mesh .198: 3 bugs fixed and deployed
-- [project_indeedhub_arch3_fix.md](project_indeedhub_arch3_fix.md) — IndeedHub Arch 3: corrupted combined tarball fixed
-- [project_demo_deploy.md](project_demo_deploy.md) — Demo prod deployment via Portainer
diff --git a/.claude/memory/claude-proxy-setup.md b/.claude/memory/claude-proxy-setup.md
deleted file mode 100644
index b79565c4..00000000
--- a/.claude/memory/claude-proxy-setup.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: claude-proxy-oauth-primary-only
-description: Only the primary server (192.168.1.228) uses OAuth proxy for Claude. Second server uses standard API key.
-type: feedback
----
-
-- Only the primary dev server (192.168.1.228) uses the Claude Max OAuth proxy for AIUI
-- The second dev server and any other deployments should use a standard Anthropic API key (`ANTHROPIC_API_KEY` env var)
-- Do not replicate the OAuth proxy setup to other servers
diff --git a/.claude/memory/deploy-automation.md b/.claude/memory/deploy-automation.md
deleted file mode 100644
index 7444e928..00000000
--- a/.claude/memory/deploy-automation.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-name: deploy-automation-todos
-description: Items to incorporate into the deploy script for automated setup — Claude API key, AIUI nginx config, swap
-type: project
----
-
-## Deploy Script Automation TODOs
-
-These items should be added to `scripts/deploy-to-target.sh` so new server deploys are self-contained:
-
-1. **Claude API key setup**: After deploying the proxy, auto-configure `/opt/archipelago/claude-proxy/.env.local` with the `ANTHROPIC_API_KEY`. On non-primary servers, use the standard API key (not OAuth). The key is: `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`
-
-2. **AIUI nginx location block**: Ensure nginx config includes a proper `/aiui/` location block so static JS/CSS files are served with correct MIME types. Without this, AIUI fails to load modules.
-
-3. **Swap space**: Deploy script should check for swap and create 4GB if missing (`fallocate -l 4G /swapfile && mkswap && swapon + fstab entry`).
-
-4. **Primary server (192.168.1.228)**: 4GB swap configured on 2026-03-11.
-5. **Second server (archipelago-2)**: 4GB swap configured on 2026-03-11.
diff --git a/.claude/memory/feedback_app_display_modes.md b/.claude/memory/feedback_app_display_modes.md
deleted file mode 100644
index 320ee986..00000000
--- a/.claude/memory/feedback_app_display_modes.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-name: App display modes
-description: App session browser should support 3 display modes - right panel, full overlay, and fullscreen - with a persistent setting
-type: feedback
----
-
-App session views (the built-in browser for launching apps) should support three display modes, controlled by a setting dropdown in the header bar:
-
-1. **Display in right panel** — app loads inside the dashboard's right content area (sidebar visible)
-2. **Display over whole app** — app overlays the entire viewport including sidebar (like old AppLauncherOverlay with `fixed inset-0 z-[2400]`)
-3. **Open fullscreen** — uses browser Fullscreen API for true fullscreen
-
-**Why:** The user likes the right-panel approach (screenshot showed it working well) but also wants the option to go full overlay or fullscreen. The setting should persist (localStorage) and apply to all apps globally.
-
-**How to apply:** Store the preference in localStorage. The header bar should have a dropdown/toggle with icons for the three modes. Default to "right panel" mode.
diff --git a/.claude/memory/feedback_apps_always_direct_port.md b/.claude/memory/feedback_apps_always_direct_port.md
deleted file mode 100644
index c58ecfdc..00000000
--- a/.claude/memory/feedback_apps_always_direct_port.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-name: Apps MUST open at direct port — NEVER proxy paths
-description: CRITICAL — All apps in iframes must open at their direct port (http(s)://{host}:{port}), NEVER through /app/{id}/ proxy paths. This is the #1 cause of broken app loading across all nodes.
-type: feedback
----
-
-## CRITICAL RULE: Apps load at DIRECT PORT, never proxy paths
-
-All Archipelago apps that open in iframes MUST use the direct port URL:
-```
-{protocol}://{hostname}:{port}
-```
-
-**NEVER** use path-based proxy URLs like `/app/indeedhub/` or `/app/mempool/` for iframe loading. Path proxies break apps because:
-1. The main nginx SPA catch-all serves the Archipelago dashboard instead of the app
-2. sub_filter URL rewrites break client-side routing in Vue/React apps
-3. Different nodes have different nginx configs — path proxies are unreliable
-
-**Why:** This was broken THREE TIMES in one session (2026-03-17). Every time the iframe URL used a proxy path instead of the direct port, the app showed the Archipelago dashboard or a blank page. .228 and .198 work correctly because they use HTTP which naturally hits the direct port. Tailscale nodes use HTTPS which was falling through to the proxy path.
-
-**How to apply:**
-- In `AppSession.vue`, apps like IndeedHub must ALWAYS construct `{protocol}://{hostname}:{port}` — even on HTTPS
-- The `HTTPS_PROXY_PATHS` mapping should NOT include apps that have X-Frame-Options removed (like IndeedHub)
-- When adding new apps: use PORT_APPS for the port mapping, do NOT add to HTTPS_PROXY_PATHS unless absolutely necessary
-- The deploy script removes X-Frame-Options from IndeedHub's internal nginx, enabling direct port iframe access
-
-**Also critical for IndeedHub specifically:**
-- IndeedHub nginx MUST use hardcoded container IPs (not DNS names) — see feedback_indeedhub_nginx_ips.md
-- nostr-provider.js must be injected via sub_filter in the IndeedHub internal nginx
-- SearXNG must NOT use --cap-drop ALL — see feedback_searxng_no_cap_drop.md
-
-**When recreating containers:**
-- NEVER recreate containers without reapplying ALL patches (X-Frame-Options removal, nostr-provider injection, IP hardcoding)
-- After any container IP change (restart, recreation), update the hardcoded IPs in IndeedHub's nginx config
-- Deploy the SAME frontend build to ALL nodes — version mismatch causes different behavior
diff --git a/.claude/memory/feedback_asset_workflow.md b/.claude/memory/feedback_asset_workflow.md
deleted file mode 100644
index 4dc21386..00000000
--- a/.claude/memory/feedback_asset_workflow.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-name: Asset workflow - designer makes images
-description: User is a designer — never generate PNG/JPEG/SVG assets, only provide specs. TUI/text animations are Claude's job.
-type: feedback
----
-
-Never generate PNG, JPEG, or SVG image assets. The user is a designer and will always create these manually.
-
-**Claude's job:** TUI text, animations, shell scripts, code
-**User's job:** PNG, JPEG, SVG, any visual/graphic assets
-
-When images are needed, provide clear specs (dimensions, format, constraints, where they go) and let the user create them.
-
-**Why:** User is a professional designer. Auto-generated pixel art looks generic compared to their actual brand artwork.
-
-**How to apply:** When boot splash, logos, icons, or any visual assets are needed, output a spec sheet with dimensions/format/constraints. Never run image generation scripts as part of the build.
diff --git a/.claude/memory/feedback_deploy_patterns.md b/.claude/memory/feedback_deploy_patterns.md
deleted file mode 100644
index be0095e8..00000000
--- a/.claude/memory/feedback_deploy_patterns.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-name: Deploy container patterns
-description: Hard-won deploy patterns — rootless port 80, credential sync, health checks, image export
-type: feedback
----
-
-Container deploy patterns learned from fleet-wide deploy sessions.
-
-**Rootless port 80:** Containers binding port 80 MUST use `--user 0:0`. `NET_BIND_SERVICE` cap doesn't work in rootless Podman.
-
-**Why:** Discovered across multiple containers (FileBrowser, Nextcloud, Vaultwarden, Jellyfin) that `--cap-add NET_BIND_SERVICE` is silently ignored in rootless mode. Only `--user 0:0` works.
-
-**Credential sync:** MariaDB/Postgres only read env vars on FIRST init. If deploy generates new random passwords in `secrets/` but the DB data dir already exists, the DB keeps the OLD password. Fix: either wipe data dir + reinit, or `ALTER USER` to sync.
-
-**Image export:** Always export custom images as INDIVIDUAL tarballs (`podman save -o name.tar`). Combined tarballs corrupt image IDs.
-
-**Health checks:** Every container should have `--health-cmd`. Currently 25+ containers have them.
-
-**How to apply:** Check these patterns in any deploy script changes or new container additions.
diff --git a/.claude/memory/feedback_fullscreen_modals.md b/.claude/memory/feedback_fullscreen_modals.md
deleted file mode 100644
index 50c84e99..00000000
--- a/.claude/memory/feedback_fullscreen_modals.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-name: Full-screen modals
-description: App session modals and overlays must cover the full viewport, not just the right panel area of the dashboard
-type: feedback
----
-
-Modals and app session overlays must be **full screen** — covering the entire viewport including the sidebar/nav. Do NOT constrain them to just the right content panel of the dashboard layout.
-
-**Why:** The user has corrected this multiple times. Modals that only cover the right panel look wrong and don't provide an immersive app experience.
-
-**How to apply:** When creating overlays, modals, or app session views, use `position: fixed; inset: 0; z-index: 2400+` to cover the entire screen. The existing AppLauncherOverlay already does this correctly with `class="fixed inset-0 z-[2400]"` — follow that pattern. On mobile it should be truly fullscreen (no padding/margins). On desktop, the glass panel with margins (md:p-10, md:rounded-2xl) is fine.
diff --git a/.claude/memory/feedback_gamepad_unfinished.md b/.claude/memory/feedback_gamepad_unfinished.md
deleted file mode 100644
index 1a8e415e..00000000
--- a/.claude/memory/feedback_gamepad_unfinished.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-name: Gamepad navigation unfinished
-description: Gamepad/controller nav rewrite (aada1975) shipped but has issues — needs further work
-type: feedback
----
-
-Gamepad navigation rewrite was committed (aada1975) and included in CI ISO builds, but user reports it's not working correctly. Issues:
-- Can't exit input fields with up/down arrow keys when other elements are available
-- Navigation behavior not right (unspecified details — need to investigate)
-
-**Why:** The rewrite was a major change to `useControllerNav.ts` and focus management. Shipped in beta but needs polish.
-**How to apply:** When touching gamepad/controller nav, treat as unfinished work. Test arrow key behavior on inputs, focus trap logic, and spatial navigation thoroughly.
diff --git a/.claude/memory/feedback_indeedhub_nginx_ips.md b/.claude/memory/feedback_indeedhub_nginx_ips.md
deleted file mode 100644
index 4e1c48e3..00000000
--- a/.claude/memory/feedback_indeedhub_nginx_ips.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-name: IndeedHub nginx must use hardcoded container IPs
-description: IndeedHub's internal nginx config MUST use hardcoded container IPs, not DNS names — podman DNS resolver 127.0.0.11 is unreliable and causes 502 errors
-type: feedback
----
-
-IndeedHub's internal nginx config MUST use hardcoded container IPs (e.g., `http://10.89.2.11:4000`) for API, MinIO, and relay upstreams — NEVER DNS names like `http://api:4000` with `resolver 127.0.0.11`.
-
-**Why:** Podman's embedded DNS resolver (127.0.0.11) is unreliable inside containers. Using DNS names with `resolver 127.0.0.11 valid=30s ipv6=off;` causes nginx to return 502 Bad Gateway errors. This broke IndeedHub on all Tailscale nodes — frontend loaded but API calls failed.
-
-**How to apply:** After creating/recreating IndeedHub containers, the deploy script must:
-1. Look up container IPs: `podman inspect indeedhub-build_api_1 --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"`
-2. Patch the nginx config inside the container to replace DNS names with IPs
-3. Also patch `$host` → `$http_host` in proxy_set_header Host directives
-4. Reload nginx inside the container
-
-This is now handled in `deploy-to-target.sh` and `fix-indeedhub-containers.sh`.
diff --git a/.claude/memory/feedback_local_dev.md b/.claude/memory/feedback_local_dev.md
deleted file mode 100644
index 8f4e4bf7..00000000
--- a/.claude/memory/feedback_local_dev.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-name: Local Frontend Dev Workflow
-description: How to start the local frontend dev environment — use start-dev.sh from neode-ui/, NOT npm start from root
-type: feedback
----
-
-Run local frontend dev from `neode-ui/` directory: `./start-dev.sh` (NOT `npm start` from project root — there's no root package.json).
-
-**Why:** The project root has no package.json. Running `npm start` there fails with ENOENT. The frontend dev script lives in `neode-ui/start-dev.sh`.
-
-**How to apply:**
-- `cd neode-ui && ./start-dev.sh` — clears ports, starts Docker apps, runs `npm run dev:mock` (mock backend on :5959, Vite on :8100)
-- Stop with `./stop-dev.sh` or Ctrl+C
-- Login password in dev mode: `password123`
-- When telling the user how to test locally, always reference `cd neode-ui && ./start-dev.sh`
diff --git a/.claude/memory/feedback_logo_ascii.md b/.claude/memory/feedback_logo_ascii.md
deleted file mode 100644
index 262e8c80..00000000
--- a/.claude/memory/feedback_logo_ascii.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-name: Archipelago ASCII logo — never change
-description: The block-letter ASCII art logo for Archipelago is locked in. Use this exact design everywhere.
-type: feedback
----
-
-The Archipelago ASCII block-letter logo is finalized. Never change it.
-
-```
-█▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ █▀█ █▀▀ █▀█
-█▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █
-▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀
-```
-
-Uses ▀ ▄ █ block characters. 45 chars wide, fits in any 52+ col box.
-Render in Bitcoin orange (`\033[38;5;208m`) by default.
-
-**Why:** User explicitly approved this logo and said "save that never change."
-**How to apply:** Use this for all TUI contexts — install screens, MOTD, menu banners, boot displays. Replace the old spaced-out `a r c h i p e l a g o` text with this wherever a banner is needed.
diff --git a/.claude/memory/feedback_searxng_no_cap_drop.md b/.claude/memory/feedback_searxng_no_cap_drop.md
deleted file mode 100644
index bbf2ef30..00000000
--- a/.claude/memory/feedback_searxng_no_cap_drop.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-name: SearXNG must NOT use --cap-drop ALL
-description: SearXNG container needs write access to /etc/searxng/ for settings.yml — cap-drop ALL causes Permission denied and exit 127
-type: feedback
----
-
-Do NOT use `--cap-drop ALL` or `--security-opt no-new-privileges:true` when creating the SearXNG container. SearXNG needs to create `/etc/searxng/settings.yml` on first run.
-
-**Why:** SearXNG's entrypoint creates a settings file from a template. With `--cap-drop ALL`, it gets "Permission denied: can't create '/etc/searxng/settings.yml'" and exits with code 127. The .228 reference server runs SearXNG with default capabilities (only drops CAP_AUDIT_WRITE, CAP_MKNOD, CAP_NET_RAW).
-
-**How to apply:** When creating SearXNG containers, use:
-```bash
-sudo podman run -d --name searxng --restart unless-stopped -p 8888:8080 docker.io/searxng/searxng:latest
-```
-No `--cap-drop ALL`, no `--security-opt no-new-privileges:true`.
diff --git a/.claude/memory/iso-build-session-2026-03-10.md b/.claude/memory/iso-build-session-2026-03-10.md
deleted file mode 100644
index e7755ef2..00000000
--- a/.claude/memory/iso-build-session-2026-03-10.md
+++ /dev/null
@@ -1,84 +0,0 @@
-# ISO Build Session — 2026-03-10
-
-## Status: Changes ready, NOT yet deployed or built
-
-All changes are local. Servers were unreachable at end of session (network issue, not crash).
-Need to: deploy to .228 → build new ISO → copy to File Browser Builds folder.
-
-## Changes Made (Local, Uncommitted)
-
-### 1. ISO Login Fix (`image-recipe/build-auto-installer-iso.sh`)
-- **Problem**: `chpasswd` fails silently in chroot (PAM not available), leaving password locked
-- **Fix**: Direct `/etc/shadow` manipulation with `sed` using SHA-512 hash from `openssl passwd -6`
-- Pre-computed hash as fallback if openssl unavailable
-- Verification check + chpasswd fallback
-- Also added `root:archipelago` password in Dockerfile
-- **Credentials**: `archipelago` / `archipelago` (TTY/SSH), `password123` (Web UI)
-
-### 2. Onboarding "Server Starting Up" UX (4 Vue files)
-- **Problem**: On fresh install, backend takes 2-5 min to start. Onboarding shows scary error messages.
-- **OnboardingDid.vue**: Replaced 3-attempt retry with persistent auto-retry every 4s. Shows "Server starting up" with elapsed timer (e.g. `1:23`) to the right. Keeps trying until backend responds.
-- **OnboardingIdentity.vue**: Detects 502/503, shows orange "Server is still starting up" instead of red error.
-- **OnboardingBackup.vue**: Same friendly server-starting message.
-- **OnboardingVerify.vue**: Same friendly server-starting message.
-
-### 3. First-Boot Container Fixes (`scripts/first-boot-containers.sh`)
-- **Problem**: Race conditions — services start before dependencies are ready
-- Added `wait_for_container()` function with configurable timeout and logging
-- **Bitcoin Knots**: Added RPC health check wait (up to 60s) before LND/NBXplorer/mempool start
-- **BTCPay PostgreSQL**: Replaced `sleep 3` with `pg_isready` health check (up to 30s)
-- **Mempool MariaDB**: Replaced `sleep 3` with connection check (up to 30s)
-- **File Browser**: Removed `--read-only` and `--cap-drop ALL` (was preventing database creation). Added separate `/database` volume mount.
-
-### 4. Build Skill Updated (`.claude/skills/build-iso/SKILL.md`)
-- Added "Post-build: Publish to File Browser" step
-- ISO gets copied to `/var/lib/archipelago/filebrowser/Builds/` after every build
-
-## Fresh Install Issues Found on .198
-- Login was broken (fixed in #1)
-- Onboarding showed 502 errors at every step (fixed in #2)
-- Containers not launching: Bitcoin Knots, BTCPay, File Browser, Grafana, LND (fixed in #3)
-- File Browser specifically: `--read-only` prevented database creation (fixed in #3)
-- Could not fully diagnose .198 — went offline before SSH diagnostic completed
-
-## Deploy Steps When Servers Are Back
-```bash
-# 1. Deploy to live server
-./scripts/deploy-to-target.sh --live
-
-# 2. Sync build script
-rsync -avz -e "ssh -i ~/.ssh/archipelago-deploy" \
- image-recipe/build-auto-installer-iso.sh \
- archipelago@192.168.1.228:~/archy/image-recipe/
-
-# 3. Sync first-boot script
-rsync -avz -e "ssh -i ~/.ssh/archipelago-deploy" \
- scripts/first-boot-containers.sh \
- archipelago@192.168.1.228:~/archy/scripts/
-
-# 4. Build ISO on server
-ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \
- 'cd ~/archy/image-recipe && sudo ./build-auto-installer-iso.sh'
-
-# 5. Copy to File Browser
-ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \
- 'sudo mkdir -p /var/lib/archipelago/filebrowser/Builds && \
- sudo cp ~/archy/image-recipe/results/archipelago-installer-x86_64.iso \
- /var/lib/archipelago/filebrowser/Builds/'
-
-# 6. Download to Mac
-scp -i ~/.ssh/archipelago-deploy \
- archipelago@192.168.1.228:~/archy/image-recipe/results/archipelago-installer-x86_64.iso \
- ~/Downloads/
-```
-
-## Files Modified (git diff summary)
-- `image-recipe/build-auto-installer-iso.sh` — password fix + Dockerfile root password
-- `scripts/first-boot-containers.sh` — health checks + filebrowser fix
-- `scripts/deploy-to-target.sh` — Tor permission fixes (from earlier)
-- `neode-ui/src/views/OnboardingDid.vue` — auto-retry with timer
-- `neode-ui/src/views/OnboardingIdentity.vue` — server-starting detection
-- `neode-ui/src/views/OnboardingBackup.vue` — server-starting detection
-- `neode-ui/src/views/OnboardingVerify.vue` — server-starting detection
-- `.claude/skills/build-iso/SKILL.md` — added File Browser publish step
-- Frontend already built: `web/dist/neode-ui/` is up to date
diff --git a/.claude/memory/pending-features.md b/.claude/memory/pending-features.md
deleted file mode 100644
index 3f7a74da..00000000
--- a/.claude/memory/pending-features.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-name: pending-ui-features
-description: Feature requests — completed and pending items for the next deployment cycle
-type: project
----
-
-## Completed (2026-03-11)
-
-1. **IndieHub in iframe** — Restored. Removed forced new-tab check in `mustOpenInNewTab()`.
-2. **App uninstall fix** — Backend now logs errors and returns structured response instead of silently swallowing.
-3. **Login music stops after auth** — Added `stopAllAudio()` + router afterEach guard.
-4. **Container scanner dev_mode gate removed** — Scanner runs always now.
-5. **BotFights app** — Added as web-only app with SVG icon. Opens in new tab (X-Frame-Options blocks iframe).
-6. **L484 web apps** — Added 6 web-only apps: NWNN, 484 Kitchen, Call the Operator, Arch Presentation, Syntropy Institute, T-0. L484 category in marketplace.
-7. **Kiosk mode** — `/kiosk` route added, `setup-kiosk.sh` installs systemd service, systemd units in image-recipe/configs/. No full-screen iframe overlay — uses standard appLauncher.
-8. **AIUI first-install fix** — nginx `try_files` changed to `=404`, Chat.vue probes AIUI availability before loading iframe.
-9. **Web-only apps in My Apps** — Injected synthetic PackageDataEntry objects in Apps.vue. Web-only apps sorted first (alphabetically before container apps). No uninstall/start/stop buttons. Launch uses appLauncher with correct URLs.
-
-## Pending
-
-1. **Nostr NIP-07 login for containers** — Sign into container apps using onboarding Nostr keys. Not started.
-2. **App sideloading** — Settings page to load apps via Docker/OCI image URL. Not started.
-3. **Encrypted Nostr peer handshake (NIP-04/NIP-44)** — Exchange Tor onion addresses via encrypted DMs instead of public relay events. Not started. Currently onion addresses are published in plaintext on relays.
-4. **Third server deploy** — archipelago-3.tail2b6225.ts.net needs SSH key setup and first deploy.
-5. **Kiosk auto-start on servers** — setup-kiosk.sh exists but needs to be run on each server that has a display attached. Not confirmed running on .228.
-6. **Deploy to .198** — Secondary server not yet deployed with latest changes.
diff --git a/.claude/memory/project-plan.md b/.claude/memory/project-plan.md
deleted file mode 100644
index b25ed994..00000000
--- a/.claude/memory/project-plan.md
+++ /dev/null
@@ -1,292 +0,0 @@
-# Archipelago 3-Year Project Plan
-
-**Version**: 1.0
-**Period**: March 2026 -- March 2029
-**Goal**: Production-ready Bitcoin Node OS with zero issues for end users
-**Visual constraint**: NEVER change animations, user experience, or visuals -- only neater layouts where highlighted
-
-## Current Status: Year 1, Q1, Sprint 1 (Starting)
-
----
-
-## Year 1: Foundation & Core Functionality (March 2026 -- February 2027)
-
-### Q1 2026 (March -- May): Fix Broken UI, Testing Infrastructure, Networking
-
-#### Sprint 1: Test Infrastructure (Week 1-2)
-- [ ] Install Vitest and configure frontend test runner
-- [ ] Create first frontend unit tests: RPC client (8+ test cases)
-- [ ] Create frontend unit tests: app store (6+ test cases)
-- [ ] Create frontend unit tests: container store (5+ test cases)
-- [ ] Create frontend unit tests: router guards (6+ test cases)
-- [ ] Create backend integration test scaffolding
-- [ ] Create backend unit tests: auth module (6+ test cases)
-- [ ] Create backend unit tests: identity module (5+ test cases)
-- [ ] Add CI-compatible test runner script (scripts/run-tests.sh)
-
-#### Sprint 2: Fix Broken UI (Week 3-4)
-- [ ] Fix Settings.vue: replace .path-option-card with .glass-card
-- [ ] Fix Web5.vue top bar: verify glass sub-card consistency with Server.vue
-- [ ] Remove duplicate network diagnostics from Settings.vue
-- [ ] Server.vue: wire real RPC data to Local Network card
-- [ ] Server.vue: wire real RPC data to Web3 card (show "Coming Soon")
-
-#### Sprint 3: Backend Robustness (Week 5-6)
-- [ ] Add system monitoring RPC endpoints (system.stats, system.processes, system.temperature)
-- [ ] Add system monitoring to frontend Dashboard (CPU/RAM/Disk gauges)
-- [ ] Add WiFi/Ethernet configuration RPC endpoints
-- [ ] Add WiFi/Ethernet UI to Server.vue
-- [ ] Implement CSRF protection on RPC layer
-- [ ] Fix CORS policy: restrict to same-origin
-- [ ] Add Nginx security headers
-
-#### Sprint 4: Quality Baseline (Week 7-8)
-- [ ] Run full sweep and record baseline in docs/quality-baseline.md
-- [ ] Fix all silent catch blocks
-- [ ] Remove all console.log in production paths
-- [ ] Eliminate any-type usage in frontend
-- [ ] Health-gated deploy: add pre-deploy health check
-- [ ] Run canary deploy to secondary server
-
-### Q2 2026 (June -- August): DWN, Backup/Restore, Kiosk Mode, StartOS Independence
-
-#### Sprint 5: DWN Protocol Implementation (Week 1-3)
-- [ ] Implement DWN message store (dwn_store.rs)
-- [ ] Implement DWN HTTP API (POST /dwn)
-- [ ] Implement DWN peer sync protocol
-- [ ] Add DWN management UI (DwnManager.vue)
-- [ ] Add DWN RPC endpoints for protocol management
-
-#### Sprint 6: Full Backup/Restore System (Week 4-5)
-- [ ] Extend backup module for full system backup
-- [ ] Add backup/restore RPC endpoints
-- [ ] Add backup/restore UI to Settings
-- [ ] Add backup to USB drive support
-
-#### Sprint 7: Kiosk Mode Hardening (Week 6-7)
-- [ ] Add kiosk mode crash recovery
-- [ ] Add kiosk failsafe route (/recovery)
-- [ ] Add kiosk-specific keyboard shortcuts
-- [ ] Create kiosk systemd service
-
-#### Sprint 8: StartOS Independence (Week 8-10)
-- [ ] Audit StartOS code usage → docs/startos-dependency-audit.md
-- [ ] Migrate essential StartOS utilities to archipelago
-- [ ] Remove core/startos from workspace
-- [ ] Run full regression test after removal
-
-### Q3 2026 (September -- November): App Integration, Auto-Updates, ARM64
-
-#### Sprint 9: App Integration Testing (Week 1-3)
-- [ ] Create app integration test suite (scripts/test-all-apps.sh)
-- [ ] Fix all app integration failures
-- [ ] Test dependency chains
-- [ ] Test fresh install end-to-end
-
-#### Sprint 10: Auto-Update System (Week 4-6)
-- [ ] Implement update download and apply
-- [ ] Add update notification to frontend
-- [ ] Implement automatic update scheduling
-- [ ] Create release manifest infrastructure
-
-#### Sprint 11: ARM64 Support (Week 7-9)
-- [ ] Set up ARM64 cross-compilation
-- [ ] Test ARM64 container images
-- [ ] Build ARM64 ISO
-- [ ] Test ARM64 on Raspberry Pi 5
-
-#### Sprint 12: Quality Hardening (Week 10-12)
-- [ ] Achieve 50% frontend test coverage
-- [ ] Achieve 50% backend test coverage
-- [ ] Run overnight chaos test
-- [ ] Run full quality sweep vs baseline
-
-### Q4 2026 (December -- February 2027): Security, Performance, Beta
-
-#### Sprint 13: Security Hardening (Week 1-3)
-- [ ] Implement session expiry and rotation
-- [ ] Harden container security profiles
-- [ ] Add secrets rotation mechanism
-- [ ] Sanitize FileBrowser path traversal
-- [ ] Remove FileBrowser token from URLs
-- [ ] Run automated security scan
-
-#### Sprint 14: Performance Optimization (Week 4-6)
-- [ ] Profile and optimize backend startup (<3s)
-- [ ] Optimize frontend bundle size (<500KB gzipped)
-- [ ] Add WebSocket connection pooling and heartbeat
-- [ ] Optimize container image pull performance
-
-#### Sprint 15: Beta Release Prep (Week 7-10)
-- [ ] Create comprehensive user documentation
-- [ ] Create beta testing checklist
-- [ ] Build and test beta ISO
-- [ ] Publish v0.5.0-beta release
-- [ ] Run 72-hour stability test
-
----
-
-## Year 2: Feature Completeness & Reliability (March 2027 -- February 2028)
-
-### Q1 2027 (March -- May): W3C DIDs, JSON-LD VCs, Hardware Wallet
-
-#### Sprint 16: W3C-Compliant DIDs (Week 1-3)
-- [ ] Implement W3C DID Document format
-- [ ] Implement DID Document verification
-- [ ] Update DID display in Web5.vue
-- [ ] Add DID resolution across peers
-
-#### Sprint 17: JSON-LD Verifiable Credentials (Week 4-6)
-- [ ] Implement JSON-LD credential format
-- [ ] Add credential presentation protocol
-- [ ] Add credential management UI
-
-#### Sprint 18: Hardware Wallet Integration (Week 7-10)
-- [ ] Research and document hardware wallet integration
-- [ ] Implement PSBT signing flow in LND RPC
-- [ ] Add hardware wallet UI flow
-- [ ] Add USB hardware wallet detection
-
-### Q2 2027 (June -- August): Multi-Node, VPN, Community Marketplace
-
-#### Sprint 19: Multi-Node Orchestration (Week 1-4)
-- [ ] Design multi-node architecture
-- [ ] Implement node federation protocol
-- [ ] Add multi-node dashboard
-- [ ] Implement federated app deployment
-
-#### Sprint 20: VPN and Mesh Networking (Week 5-8)
-- [ ] Add Tailscale/WireGuard VPN integration
-- [ ] Add VPN status to Server.vue
-- [ ] Implement mesh networking discovery
-- [ ] Add DNS-over-HTTPS configuration
-
-#### Sprint 21: Community App Marketplace (Week 9-12)
-- [ ] Design decentralized marketplace protocol
-- [ ] Implement marketplace manifest discovery
-- [ ] Implement app manifest publishing
-- [ ] Add community marketplace tab to frontend
-
-### Q3 2027 (September -- November): Documentation, Reliability, Pre-Release
-
-#### Sprint 22: Comprehensive Documentation (Week 1-3)
-- [ ] Write developer documentation
-- [ ] Write API documentation
-- [ ] Write app developer SDK documentation
-- [ ] Create Architecture Decision Records
-
-#### Sprint 23: Reliability Engineering (Week 4-8)
-- [ ] Implement graceful shutdown
-- [ ] Add crash recovery
-- [ ] Implement disk space management
-- [ ] Add container health monitoring and auto-recovery
-- [ ] Run 1-week continuous uptime test
-
-#### Sprint 24: Pre-Release Quality (Week 9-12)
-- [ ] Achieve 70% frontend test coverage
-- [ ] Achieve 70% backend test coverage
-- [ ] Run full regression screenshot comparison
-- [ ] Publish v0.8.0-rc1 release candidate
-
-### Q4 2027 (December -- February 2028): Polish, Community, v0.9.0
-
-#### Sprint 25: User Experience Polish (Week 1-4)
-- [ ] Run complete UX audit
-- [ ] Fix all UX audit findings
-- [ ] Polish error handling across entire frontend
-- [ ] Polish all forms
-
-#### Sprint 26: Community Infrastructure (Week 5-8)
-- [ ] Set up update server infrastructure
-- [ ] Create community contribution guidelines
-- [ ] Set up issue tracker and roadmap
-- [ ] Publish v0.9.0 release
-
----
-
-## Year 3: Production Polish & Scale (March 2028 -- March 2029)
-
-### Q1 2028 (March -- May): Monitoring, Remote Management, Accessibility
-
-#### Sprint 27: Advanced Monitoring (Week 1-4)
-- [ ] Implement real-time metrics collection
-- [ ] Add monitoring dashboard page
-- [ ] Implement alerting system
-- [ ] Add historical data export
-
-#### Sprint 28: Remote Management (Week 5-8)
-- [ ] Implement Tailscale-based remote access
-- [ ] Add mobile-optimized remote management
-- [ ] Implement remote notification system
-
-#### Sprint 29: Accessibility and i18n (Week 9-12)
-- [ ] Add ARIA labels and roles
-- [ ] Add keyboard navigation testing
-- [ ] Set up i18n infrastructure
-
-### Q2 2028 (June -- August): Pen Testing, Final QA
-
-#### Sprint 30: Security Penetration Testing (Week 1-4)
-- [ ] Run automated penetration test suite
-- [ ] Manual security review of all RPC endpoints
-- [ ] Harden Podman container isolation
-- [ ] Add rate limiting to all sensitive endpoints
-
-#### Sprint 31: End-to-End QA (Week 5-8)
-- [ ] Create golden path test suite
-- [ ] Run regression test across all hardware
-- [ ] Achieve 80% test coverage
-- [ ] Run 30-day soak test
-
-#### Sprint 32: Documentation and Community (Week 9-12)
-- [ ] Write troubleshooting guide
-- [ ] Create walkthrough documentation
-- [ ] Finalize all ADRs
-- [ ] Publish v0.95.0-rc2
-
-### Q3 2028 (September -- November): v1.0 Release
-
-#### Sprint 33: Final Polish (Week 1-4)
-- [ ] Final UX audit
-- [ ] Final security audit
-- [ ] Final sweep
-- [ ] Performance benchmark and optimize
-
-#### Sprint 34: Release Engineering (Week 5-8)
-- [ ] Create release automation
-- [ ] Set up download/update infrastructure
-- [ ] Write v1.0 release notes
-- [ ] Build v1.0.0 release ISOs
-
-#### Sprint 35: Launch (Week 9-12)
-- [ ] Tag and publish v1.0.0
-- [ ] Run 7-day post-release monitoring
-- [ ] Create v1.1 roadmap
-
-### Q4 2028 (December -- February 2029): Maintenance
-
-#### Sprint 36-39: Ongoing
-- [ ] Monthly dependency update cycle
-- [ ] Monthly security scan
-- [ ] Quarterly quality sweep
-- [ ] Community app reviews
-- [ ] Plan v2.0 features
-
----
-
-## Milestone Summary
-
-| Date | Milestone | Key Deliverables |
-|------|-----------|-----------------|
-| May 2026 | Q1 Complete | Tests, UI fixes, security, quality baseline |
-| Aug 2026 | Q2 Complete | DWN, backup/restore, kiosk, StartOS independence |
-| Nov 2026 | Q3 Complete | App testing, auto-updates, ARM64 |
-| Feb 2027 | **v0.5.0-beta** | First public beta |
-| Nov 2027 | **v0.8.0-rc1** | Release candidate |
-| Feb 2028 | **v0.9.0** | Pre-release |
-| Nov 2028 | **v1.0.0** | Production release |
-
-## Execution Method
-- Execute via `/overnight` skill — each session picks up next uncompleted tasks
-- Full detailed acceptance criteria in the original plan conversation
-- Track progress by checking off items in this file as [x]
diff --git a/.claude/memory/project_app_registry.md b/.claude/memory/project_app_registry.md
deleted file mode 100644
index cea2b218..00000000
--- a/.claude/memory/project_app_registry.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-name: App Registry Setup
-description: Archipelago app container registry at 80.71.235.15:3000 (Gitea) — marketplace images mirrored there
-type: project
----
-
-Archipelago app registry running on Gitea at `80.71.235.15:3000`, org `archipelago`.
-
-**Why:** Self-hosted container registry so Archipelago nodes pull app images from our infrastructure instead of Docker Hub/ghcr.io. Critical for unbundled ISO installs where apps are downloaded on-demand.
-
-**How to apply:**
-- Registry URL: `80.71.235.15:3000/archipelago/:`
-- HTTP only (insecure) — nodes need `registries.conf` with `insecure = true`
-- ISO build bakes the insecure registry config into `/home/archipelago/.config/containers/registries.conf`
-- Marketplace data in `neode-ui/src/views/marketplace/marketplaceData.ts` uses `REGISTRY` constant
-- 34 images pushed from .228 on 2026-03-26
-- NOT pushed yet: Thunderhub, Penpot (not on .228)
-- Gitea instance deployed via Portainer on `80.71.235.15:9443`
-- Login: podman login 80.71.235.15:3000 (credentials set up on .228)
diff --git a/.claude/memory/project_bitcoin_rpc_auth.md b/.claude/memory/project_bitcoin_rpc_auth.md
deleted file mode 100644
index 17d716f4..00000000
--- a/.claude/memory/project_bitcoin_rpc_auth.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-name: Bitcoin RPC rpcauth architecture
-description: Bitcoin uses rpcauth (salted hash in config, password in secrets file), system Tor for containers, reboot survival
-type: project
----
-
-Bitcoin RPC uses `rpcauth` — salted HMAC-SHA256 hash in bitcoin.conf, plaintext password in `/var/lib/archipelago/secrets/bitcoin-rpc-password`. Credentials are STABLE across reboots, restarts, deploys.
-
-**Why:** Cookie auth rotates on every Bitcoin restart, breaking all dependent containers with env-var-only credentials. The `rpcauth` approach keeps the password stable while never exposing plaintext in config files or CLI args.
-
-**How to apply:**
-- Bitcoin: reads rpcauth from bitcoin.conf (no CLI credential flags, config generated by first-boot or deploy)
-- LND: `bitcoind.rpcuser/rpcpass` in lnd.conf (NOT rpccookie — LND v0.18.4 doesn't support it)
-- All containers: read password from secrets file at creation time, passed via env vars
-- Rust backend `bitcoin_rpc.rs`: reads from secrets file, cached with OnceCell
-- bitcoin-ui: mounts `/var/lib/archipelago/secrets:/secrets:ro`, start.sh reads password and injects nginx auth header
-- System Tor: `SocksPort 0.0.0.0:9050` + SocksPolicy, containers use `host.containers.internal:9050`
-- `podman-restart.service` enabled for container auto-start after reboot
-- Tor hidden service hostnames copied to `/var/lib/archipelago/tor-hostnames/` for readable access
-- .198 ElectrumX points at .228's full Bitcoin node (pruned node can't run ElectrumX locally)
-- Health monitor interval: 60 seconds — UI may briefly show "crashed" during restarts
diff --git a/.claude/memory/project_cicd_setup.md b/.claude/memory/project_cicd_setup.md
deleted file mode 100644
index 094a4744..00000000
--- a/.claude/memory/project_cicd_setup.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: CI/CD Setup
-description: Gitea Actions CI/CD — runner on .228, workflow builds unbundled ISO on push to main
-type: project
----
-
-CI/CD pipeline using Gitea Actions on git.tx1138.com.
-
-**Why:** Automatic ISO builds on every push to main. ISOs copied to FileBrowser /Builds/ for download.
-
-**How to apply:**
-- Gitea repo: `git.tx1138.com/lfg2025/archy`
-- Runner: .228 registered as `archipelago-builder` with label `ubuntu-latest:host`
-- Runner service: `gitea-runner.service` (systemd, runs as archipelago user)
-- Runner config: `~/.runner` on .228
-- Workflow: `.gitea/workflows/build-iso.yml` — unbundled ISO only
-- Uses `https://git.tx1138.com/actions/checkout@v4` (NOT github.com actions)
-- Builds: backend (cargo), frontend (npm), then ISO with `UNBUNDLED=1`
-- Output: copied to `/var/lib/archipelago/filebrowser/Builds/`
-- act_runner v0.2.11 installed at `/usr/local/bin/act_runner`
diff --git a/.claude/memory/project_container_orchestration.md b/.claude/memory/project_container_orchestration.md
deleted file mode 100644
index 64b6dd29..00000000
--- a/.claude/memory/project_container_orchestration.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: Container Orchestration Hardening
-description: Container orchestration overhaul — stop grace periods, pull retry, persistent restart tracking, scheduled remediation, failsafe install, boot reconciliation
-type: project
----
-
-Container orchestration hardening implemented on dev-iso branch (2026-03-28).
-
-**Why:** Gitea issue requesting true orchestration. Containers were unreliable — 10s stop timeout risked Bitcoin Core UTXO corruption, image pulls failed silently, restart counters reset on process restart enabling infinite loops, doctor/reconcile scripts only ran manually.
-
-**What was done (7 changes):**
-1. Per-container stop grace periods (600s bitcoin, 330s lnd, 300s electrs, 120s databases, 60s btcpay, 30s default) + systemd TimeoutStopSec=660
-2. Image pull retry with exponential backoff (3 attempts: 5s/15s/45s) + post-pull verification + stacks.rs error propagation instead of silent swallow
-3. Resolved container/health_monitor.rs TODO (documented as orchestrator-level responsibility)
-4. Persistent restart tracking to restart-tracker.json (survives process restarts, seeded on startup)
-5. Scheduled systemd timers: container-doctor every 30min, reconcile-containers every 6h
-6. Failsafe install: post-pull image verify, rollback on start failure, 30s post-start health check with crash diagnosis
-7. Boot reconciliation: runs reconcile-containers.sh after crash recovery completes
-
-**How to apply:** These changes affect beta reliability. The other programmer is working on custom base ISO on the same branch — coordinate on build-auto-installer-iso.sh changes.
diff --git a/.claude/memory/project_demo_deploy.md b/.claude/memory/project_demo_deploy.md
deleted file mode 100644
index d81a83e2..00000000
--- a/.claude/memory/project_demo_deploy.md
+++ /dev/null
@@ -1,62 +0,0 @@
----
-name: Demo Deploy Status
-description: Status and details of the demo prod server deployment via Portainer Stacks from Gitea repos
-type: project
----
-
-## Demo Prod Deployment — In Progress (2026-03-17)
-
-### Two Separate Portainer Stacks
-
-**1. IndeedHub** — DEPLOYED SUCCESSFULLY on :7755
-- Repo: `https://git.tx1138.com/lfg2025/indee-demo`
-- Compose: `docker-compose.yml` (root)
-- Env vars loaded from `.env.portainer` — update DOMAIN, FRONTEND_URL, S3_PUBLIC_BUCKET_URL
-- APP_PORT defaulted to 7755 (changed from 7777 to avoid conflicts)
-- Healthcheck fix: pg_isready uses `${POSTGRES_USER}` env var (was hardcoded)
-- Full 7-service stack: app, api, postgres, redis, minio, minio-init, relay, ffmpeg-worker
-- Nostr auth is built-in (NIP-98) — users sign in with browser extension (Alby, nos2x)
-
-**2. Archipelago** — DEPLOYING (last attempt pending)
-- Repo: `https://git.tx1138.com/lfg2025/archy-demo`
-- Compose: `docker-compose.demo.yml`
-- Env vars: `ANTHROPIC_API_KEY` for Claude chat
-- Port: 4848
-- Pre-built frontend in `web-dist/` (built locally on Mac, no server-side build)
-- Backend: `neode-ui/Dockerfile.backend` (Node mock backend on :5959)
-- Web: `neode-ui/Dockerfile.web` (nginx serving pre-built static files)
-
-### Issues Resolved So Far
-- IndeedHub postgres healthcheck hardcoded username → fixed to use env var
-- Port 7777 conflict → changed to 7755
-- Archy repo too large (8GB) for Portainer clone → created lightweight `archy-demo` repo
-- Frontend build failing on server → switched to pre-built static files (no npm/vite on server)
-- `.dockerignore` blocking `neode-ui/dist` → moved to `web-dist/` at repo root
-- Docker build cache stale → moved dist outside neode-ui to avoid gitignore conflicts
-
-### Current Blocker
-- Last deploy attempt: Docker build cache may still be referencing old paths
-- If still failing: need to prune Docker build cache on server (`docker builder prune`)
-
-### Frontend Changes Made
-- `Apps.vue` and `AppDetails.vue`: IndeedHub removed from WEB_ONLY_APP_URLS (linter change)
-- IndeedHub will be accessed as a real container or via direct URL to :7755
-
-### Repo Structure (archy-demo)
-```
-archy-demo/
-├── docker-compose.demo.yml
-├── .dockerignore
-├── web-dist/ ← pre-built Vue frontend (from local Mac build)
-├── demo/aiui/ ← pre-built AIUI chat app
-└── neode-ui/ ← source + mock backend + docker configs
- ├── Dockerfile.web ← nginx + copy web-dist (no build)
- ├── Dockerfile.backend ← Node mock backend
- ├── docker/nginx-demo.conf
- ├── docker/docker-entrypoint.sh
- ├── mock-backend.js
- └── src/...
-```
-
-**Why:** Demo for showcasing Archipelago + IndeedHub together. Needs to be functional with nostr signing.
-**How to apply:** When resuming, check if Portainer deploy succeeded. If not, may need to SSH to prune Docker cache or debug further.
diff --git a/.claude/memory/project_deploy_session_2026_03_22.md b/.claude/memory/project_deploy_session_2026_03_22.md
deleted file mode 100644
index b8f0e53d..00000000
--- a/.claude/memory/project_deploy_session_2026_03_22.md
+++ /dev/null
@@ -1,98 +0,0 @@
----
-name: Deploy session 2026-03-22 findings
-description: Comprehensive deploy/build fixes made overnight — container issues, image tags, script improvements, remaining work
-type: project
----
-
-## Session Summary (2026-03-22 overnight)
-
-Massive deploy infrastructure overhaul across all 5 nodes (.228, .198, Arch 1/2/3).
-
-### Fixed in deploy-tailscale.sh
-- **Image tags**: Bitcoin Knots `28.1` (not `v28.1`), BTCPay `1.13.7` (not `1.14.5`), SearXNG `2026.3.20-6c7e9c197`
-- **Removed Immich** (3 containers) and **Penpot** (5 containers) from deploy + build
-- **Fedimint**: `FM_REL_NOTES_ACK=0_4_xyz` env var (NOT `FM_SKIP_REL_NOTES_ACK` or `FM_REQ_RELEASE_NOTES_ACK_V0_4`)
-- **Fedimint-gateway**: `--password` instead of `--bcrypt-password-hash` (v0.5.1 CLI change)
-- **FileBrowser**: added `--cap-add NET_BIND_SERVICE` for port 80 binding
-- **SearXNG**: added `/var/lib/archipelago/searxng:/etc/searxng` volume mount + caps
-- **Postgres**: pinned to `postgres:15` (data initialized with 15, incompatible with 16)
-- **Migration**: one-time flag file `/var/lib/archipelago/.rootless-migrated`
-- **Recreate-if-broken pattern**: containers that exist but are stopped get deleted and recreated
-- **Arch 2 hostname**: fixed from hardcoded hostname to `$TAILSCALE_ARCH2`
-- **Custom UI images**: graceful skip if not available, source extracted to repo (`docker/bitcoin-ui/`, `docker/electrs-ui/`)
-- **AIUI tar xattr**: silenced with `--no-xattrs` (only in deploy-tailscale.sh, NOT deploy-to-target.sh yet)
-- **Nginx MIME warning**: removed `text/html` from `sub_filter_types`
-
-### Added
-- `--fleet` flag in deploy-to-target.sh: deploys .228 → .198 → Arch 1/2/3
-- `--both` lock fix: releases lock before recursive `--live` call
-- Container verification step (Step 26b): restarts exited containers, fixes permissions, checks Tor
-- IndeedHub backend stack rebuilt on .228 (7 containers)
-- IndeedHub nginx patched with direct IPs (podman DNS doesn't work with nginx resolver)
-
-### Frontend changes
-- Replaced Immich with FileBrowser on Setup homescreen (`goals.ts`, `EasyHome.vue`)
-- `MEMPOOL_API_IMAGE` renamed to `MEMPOOL_BACKEND_IMAGE` in image-versions.sh
-- Nextcloud downgraded from 30 to 29 (one major version upgrade at a time)
-
-### Session 2 fixes (same day)
-
-**Critical pattern found: Container credential mismatches**
-- Deploy generates random passwords stored in `secrets/`. MariaDB/Postgres only use env vars on FIRST init — subsequent restarts ignore them. Container recreation with new passwords → auth failures → crash loops.
-- 50,000+ cumulative container restarts across fleet from this single root cause.
-
-**Fixes applied to all nodes:**
-1. LND: `lnd.conf` rpcpass synced from `secrets/bitcoin-rpc-password` (was hardcoded `archipelago123`)
-2. MariaDB mempool: data dirs wiped + reinitialized (password mismatch unrecoverable)
-3. BTCPay Postgres: `ALTER USER` to sync password with secrets
-4. FileBrowser: `--user 0:0` instead of `--cap-add NET_BIND_SERVICE` (rootless port 80 fix)
-5. Nextcloud: same `--user 0:0` fix
-6. Tailscale container on .228: removed (2,685 restarts — unauthenticated, host already has TS)
-
-**Deploy script fixes:**
-- `deploy-tailscale.sh`: LND config always synced before start, `eval "$DB_PASSWORDS"` → safe individual reads, MariaDB password sync step, filebrowser `--user 0:0`
-- `deploy-to-target.sh`: LND stale config check now compares passwords (not just cookie/localhost), filebrowser `--user 0:0`
-
-**Rootless port 80 rule**: Containers binding port 80 MUST use `--user 0:0`. `NET_BIND_SERVICE` cap doesn't work in rootless (UID 0 → host 100000, unprivileged).
-
-### Session 3 fixes (2026-03-22 to 2026-03-24)
-
-**Additional container fixes applied live:**
-- PhotoPrism: recreated with proper `/photoprism/storage`, `/photoprism/originals`, `/photoprism/import` volume mounts (all 3 nodes)
-- Vaultwarden/Jellyfin: recreated with `--user 0:0` + health checks (Arch 1/2)
-- Nextcloud: downgraded image to v29 (data initialized with v28, can't skip to v30)
-- Fedimint: upgraded v0.5.1 → v0.10.0 on all Tailscale nodes
-- Fedimint-gateway: bcrypt hash passed via file mount (shell escaping workaround)
-- SearXNG: recreated with proper caps on Arch 2
-- Arch 3 right-sized: stopped immich (3), jellyfin, vaultwarden, nbxplorer (7.3GB RAM)
-
-**Deploy script improvements (6 commits pushed):**
-1. `d37165ca` — Credential sync, health checks, rootless port binding
-2. `f5714a5b` — Fleet deploy falls back to Tailscale when LAN unreachable, `--all` alias
-3. `028248df` — Suppress tar xattr spam in AIUI deploy (`--no-xattrs`)
-4. `f5802f9e` — Fix LND config SSH escaping, Tailscale fallback for BUILD_SOURCE
-5. `06d85e1d` — Fix health check escaping for SSH heredoc (`--health-cmd 'cmd'` not `"cmd"`)
-6. `a7920de8` — Correct health check endpoints (fedimint→8175, nextcloud→`/`, filebrowser→`/`)
-
-**Health checks added to deploy-tailscale.sh:**
-- 25 containers now have `--health-cmd` in deploy-tailscale.sh (was zero)
-- Key corrections: fedimint checks port 8175 (UI) not 8174 (websocket), nextcloud/filebrowser check `/` not custom endpoints
-
-**Fleet status at end of session:**
-
-| Node | Status | Notes |
-|------|--------|-------|
-| .228 | 36/36, 0 unhealthy, load 1.0 | Fully stable |
-| Arch 1 | 25/25, 0 unhealthy, load 0.5 | Fully stable |
-| Arch 2 | 25/25, 0 unhealthy, load 0.2 | Fully stable |
-| Arch 3 | 24/28, 0 unhealthy, load 7.7 | Right-sized for 7.3GB RAM, Bitcoin IBD at 97.8% |
-| .198 | Bitcoin chain data empty (4KB) | Needs full IBD — will take days. Not pruned. |
-
-### Remaining for next session
-- **.198**: Bitcoin doing full IBD from scratch (chain data was lost/empty). No prune flag set. Will take days.
-- **Arch 3**: Bitcoin IBD was at 97.8% — check if complete, then start LND/nbxplorer
-- **Tor config Python syntax errors** in deploy-to-target.sh step 33 (cosmetic, falls back to system Tor)
-- **deploy-to-target.sh** still missing health checks (only deploy-tailscale.sh has them)
-- **first-boot-containers.sh** needs same rootless fixes (filebrowser `--user 0:0`, credential sync)
-- **Fedimint guardian setup** not done on any node — all in "Setup UI" mode
-- User needs to `git pull && ./scripts/deploy-to-target.sh --all` to deploy latest fixes to Tailscale nodes
diff --git a/.claude/memory/project_environments.md b/.claude/memory/project_environments.md
deleted file mode 100644
index 58134979..00000000
--- a/.claude/memory/project_environments.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-name: Four Environments
-description: Clear distinction between dev mode (local mock), dev server (228), demo (Portainer), and prod (same as dev server)
-type: project
----
-
-Four distinct environments — use correct terminology:
-
-| Name | What | Where | Backend | Deploy |
-|------|------|-------|---------|--------|
-| **Dev mode** | Local macOS, mock backend | `localhost:8100` | `mock-backend.js` on `:5959` | `npm run dev:mock` |
-| **Dev server / Prod** | Primary build/test/live server | `192.168.1.228` (+ fleet) | Real Rust backend + Podman | `deploy-to-target.sh --live` |
-| **Demo** | Public demo instance | Remote server | Mock Node.js via Docker | Portainer Stacks / `docker-compose.demo.yml` |
-
-- Dev server and prod are the SAME machine (192.168.1.228) — "prod" just means "the live deployment"
-- Demo is completely separate — user deploys via Portainer UI, Claude has no SSH access
-- Dev mode is local-only, no containers needed, fastest iteration
-
-**Why:** User corrected ambiguous usage of "dev servers (prod)" — these are the same thing, not two separate environments.
-
-**How to apply:** Always say "dev mode" for local mock, "dev server" or "prod" for 228, "demo" for the Portainer instance. Never conflate them.
diff --git a/.claude/memory/project_gamepad_nav.md b/.claude/memory/project_gamepad_nav.md
deleted file mode 100644
index b0e35f40..00000000
--- a/.claude/memory/project_gamepad_nav.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-name: Gamepad Navigation System
-description: Controller/gamepad navigation architecture, key decisions, known issues, and the nav map doc location
-type: project
----
-
-Gamepad/controller navigation is a core feature of Archipelago — the UI runs on a kiosk with Xbox-style controller input.
-
-**Why:** Archipelago runs on dedicated hardware with a TV/monitor + gamepad. Every page must be fully navigable without a mouse.
-
-**How to apply:** When modifying any page's interactive elements, check that `data-controller-container` and `tabindex` are set correctly. Read `neode-ui/docs/GAMEPAD-NAV-MAP.md` for the full per-page navigation spec and implementation notes.
-
-## Key files
-- `neode-ui/src/composables/useControllerNav.ts` — all navigation logic
-- `neode-ui/docs/GAMEPAD-NAV-MAP.md` — full nav spec with per-page tables, implementation notes, and Chromium gotchas
-
-## Critical patterns
-- Cards on grid pages: `glass-card transition-all hover:-translate-y-1` + `data-controller-container tabindex="0"`
-- Settings page is a MIXED page (containers + standalone buttons) — nav searches both together
-- ToggleSwitch has `tabindex="-1"` + `data-controller-ignore` so gamepad skips it
-- Focus glow uses blurred box-shadow, NOT `0 0 0 Npx` spread (Chromium compositor bug with translateZ(0))
-- `outline: none !important` on all containers to kill browser default focus rings
diff --git a/.claude/memory/project_indeedhub_arch3_fix.md b/.claude/memory/project_indeedhub_arch3_fix.md
deleted file mode 100644
index a949969f..00000000
--- a/.claude/memory/project_indeedhub_arch3_fix.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-name: IndeedHub Arch 3 Fix — 2026-03-17
-description: Fixed IndeedHub on Arch 3 (100.124.105.113) — corrupted image tarball was root cause, all 7 containers now running
-type: project
----
-
-## Status: FIXED and working (verified 2026-03-17)
-
-IndeedHub on Arch 3 (`100.124.105.113`) is fully operational — all 7 containers running, frontend on :7777, API healthy, NIP-07 nostr-provider injected.
-
-## Root Cause
-
-The `/tmp/indeedhub-all-images.tar` on Arch 3 was corrupted — `podman save` with multiple images collapsed ALL 7 images to the same image ID (the frontend nginx image `7222645f0b38`). So redis, minio, API, ffmpeg-worker, postgres, and relay were all running the frontend nginx binary.
-
-**Why:** `podman save` with multiple images sharing layers can produce broken tarballs where all images get the same config/ID.
-
-## What Was Done
-
-1. Removed all broken containers and images
-2. Pulled fresh standard images from Docker Hub (postgres:16-alpine, redis:7-alpine, minio:latest, nostr-rs-relay:latest)
-3. Exported each custom image as **individual tarballs** from .228 (NOT combined):
- - `indeedhub-frontend.tar` (149MB, ID: `7222645f0b38`)
- - `indeedhub-api.tar` (403MB, ID: `2ae2665fc6c7`)
- - `indeedhub-ffmpeg.tar` (525MB, ID: `cb05b5cf8c25`)
-4. Transferred via Mac (`.228` → Mac → Arch 3 over Tailscale)
-5. Loaded images individually, created all 7 containers manually (bypassed the deploy script's broken `podman load` step)
-6. Copied nostr-provider.js + nginx config with sub_filter from .228 container into Arch 3 container via `podman cp`
-
-## Remaining Issue — Deploy Script
-
-The deploy script at `/tmp/deploy-indeedhub.sh` on Arch 3 still references the broken `/tmp/indeedhub-all-images.tar`. If it's run again it will re-corrupt the images. The individual tarballs (`/tmp/indeedhub-frontend.tar`, `/tmp/indeedhub-api.tar`, `/tmp/indeedhub-ffmpeg.tar`) are on Arch 3 and should be used instead.
-
-**How to apply:** Next time deploying IndeedHub to any node, always export images individually, never as a combined tarball. Consider updating the deploy script to load individual tarballs.
diff --git a/.claude/memory/project_iso_size_reduction.md b/.claude/memory/project_iso_size_reduction.md
deleted file mode 100644
index 6111c48d..00000000
--- a/.claude/memory/project_iso_size_reduction.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-name: ISO Size Reduction Plan
-description: Plan to reduce ISO from 3.9GB — prioritized phases for post-beta
-type: project
----
-
-Current ISO: ~3.9GB (unbundled). Target: <1.5GB.
-
-**Why:** Debian Live base (~800MB) + rootfs with kiosk/Podman/firmware (~2.1GB) + squashfs overhead.
-
-**Phase 1 — Quick wins (post-beta, ~500MB-1GB savings):**
-- Strip unused firmware blobs (WiFi chipsets, GPU)
-- Remove build-only packages from rootfs (not needed at runtime)
-- `--no-install-recommends` in all apt installs
-- Strip debug symbols from binaries
-- Remove man pages, docs, locale data (`localepurge`)
-
-**Phase 2 — Minimal base (~1-1.5GB savings):**
-- Replace Debian Live ISO with custom `debootstrap --variant=minbase` live image
-- Make kiosk (X11 + Chromium ~400MB) optional / separate overlay
-- Alpine-based rootfs alternative
-
-**Phase 3 — Long term (<1GB target):**
-- Custom kernel with only needed modules
-- A/B read-only root partition (no live boot infrastructure)
-- Network installer variant (tiny ISO, needs internet)
-- Reproducible builds with exact dep trees
-
-**How to apply:** Each phase is independent. Phase 1 is safe to do anytime. Phase 2 requires testing the boot chain. Phase 3 is architectural.
diff --git a/.claude/memory/project_mesh_198_issue.md b/.claude/memory/project_mesh_198_issue.md
deleted file mode 100644
index c5a3799c..00000000
--- a/.claude/memory/project_mesh_198_issue.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: Mesh .198 fix — COMPLETED
-description: Fixed mesh radio on .198 — duplicate init, no reconnect on write fail, wrong device path. All deployed.
-type: project
----
-
-## Status: COMPLETED (2026-03-17)
-
-Three bugs were found and fixed:
-
-1. **Duplicate mesh init in `server.rs`** — removed duplicate block
-2. **Serial write failures don't trigger reconnection** — added `consecutive_write_failures` counter, bail after 3
-3. **Device path on .198** — set `/var/lib/archipelago/mesh-config.json` to `/dev/ttyUSB1`
-
-All changes deployed to both .228 and .198.
-
-### Files Changed
-- `core/archipelago/src/server.rs` — removed duplicate mesh/transport init block
-- `core/archipelago/src/mesh/listener.rs` — added write failure tracking + reconnection
-- `neode-ui/src/stores/mesh.ts` — fixed TS union type for `typed_payload`
diff --git a/.claude/memory/project_repo_cleanup_and_dev_env.md b/.claude/memory/project_repo_cleanup_and_dev_env.md
deleted file mode 100644
index 4cfc1ab7..00000000
--- a/.claude/memory/project_repo_cleanup_and_dev_env.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-name: v1.3.0 Session Status (March 20)
-description: Tor management system, bug fixes, federation name sync — cloud files working both ways
-type: project
----
-
-## Deployed to .228 + .198
-
-### What's Live
-- Full Tor hidden service management (systemd path unit pattern — tor-helper.sh)
-- Container doctor: system Tor preferred, archy-tor container removed
-- Federation name sync: server rename pushes to peers
-- Cloud files working both ways over Tor
-- Arch channel local echo for sent messages
-- Web5 Message button → Mesh redirect
-- Node names in federation/peers
-- PeerFiles header shows name + DID (not onion)
-- Connected Nodes flex height
-- Server name persistence (root-owned file fixed)
-- Tor services UI: add from installed apps, delete, restart, auth/protocol badges
-- Layout: Network Interfaces + Tor Services stack on normal screens
-
-### Architecture: Tor Management
-- Backend writes staged torrc + action file to /var/lib/archipelago/tor-config/
-- systemd path unit (archipelago-tor-helper.path) triggers root-level service
-- tor-helper.sh processes actions: write-torrc-and-restart, restart, delete-service, sync-hostnames
-- NoNewPrivileges=yes safe — no sudo from backend
-- Container doctor ensures system Tor stays running after deploys
-- Web apps: port 80 on .onion → local app port; Protocol services: direct port
-
-### Onion Addresses (current)
-- .228 archipelago: r33p5uzk2vxhdte4a5pfqgeax44a7b2lx57q32dxmx5llzyfz42lwnyd.onion
-- .198 archipelago: mxn62m4odavwctlpsq2ozvhy3ibjpenlzemumwtkev7wviikttxvjhyd.onion
-
-### Still TODO
-1. **Tor channel chat** — messages via Archipelago channel need testing/polish
-2. **ISO build** — update build-auto-installer-iso.sh with tor-helper, systemd units, container doctor changes
-3. **Better error messaging** — when nodes are down, addresses changed, all situations
-4. **File access permissions** — public (no auth), federated (full access), peer-set (specific files)
-5. **Auth on Tor app access** — login before accessing app via .onion (post-beta candidate)
-6. **.198 health check** — deploy health check times out on .198 (backend works, likely timing)
-
-**Why:** Session continuity for v1.3.0 beta stabilization effort.
-**How to apply:** Read at start of next session. Work on TODO items in order.
diff --git a/.claude/memory/project_session_20260328.md b/.claude/memory/project_session_20260328.md
deleted file mode 100644
index 3ff749a3..00000000
--- a/.claude/memory/project_session_20260328.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-name: ISO Session 2026-03-28 Handoff
-description: Session handoff — branding overhaul, ISOLINUX config updated, terminal banners redesigned, UEFI still broken
-type: project
----
-
-## Session State (2026-03-28 ~latest)
-
-### Branding Overhaul (this session)
-
-**ISOLINUX boot menu:**
-- Config updated: menu centered (HSHIFT 28, WIDTH 26), title "Bitcoin Node OS"
-- Selection: white on dark, hotkeys in Bitcoin orange (#fb923c)
-- Tab message: "Press TAB to edit | https://archipelago.sh"
-- MENU RESOLUTION kept at 1024x768 (uses GRUB background.png)
-- Three options: Install Archipelago, Install (verbose), Boot from local disk
-
-**Terminal banners — unified design across all screens:**
-- Name: "A R C H I P E L A G O" (uppercase, spaced, bold white)
-- Separator: orange line
-- Subtitle: dim text (varies by context)
-- Colors: basic ANSI (works on bare-metal console, not 256-color)
-- Width: fits 80-col terminals (no overflow/clipping)
-- Build script auto-install.sh: centered + adaptive-width boxes
-- Standalone scripts: fixed 52-char boxes
-
-**Files changed:**
-- build-auto-installer-iso.sh: ISOLINUX config, colors (256 to basic ANSI), case, header + completion
-- build/debian-iso/custom/etc/profile.d/z99-archipelago.sh: full rewrite
-- build/debian-iso/custom/archipelago/auto-start.sh: full rewrite
-- archipelago-scripts/archipelago-menu.sh: full rewrite
-- build/debian-iso/custom/isolinux/stdmenu.cfg, menu.cfg, live.cfg: updated
-- branding/generate-isolinux-splash.py: new file (640x480 splash generator, optional)
-
-### Outstanding Issues
-- UEFI boot broken — drops to grub> prompt, only Legacy BIOS works
-- ISOLINUX resolution kept at 1024x768, may clip on some hardware
-- Install + onboarding logs confirmed present on .198 (5 log files)
-- Need to review actual log content from .198
-
-### Target Machine
-- Dell on .198, Legacy BIOS, password: archipelago
diff --git a/.claude/memory/reference_tailscale_nodes.md b/.claude/memory/reference_tailscale_nodes.md
deleted file mode 100644
index cf5b9084..00000000
--- a/.claude/memory/reference_tailscale_nodes.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-name: Node inventory and SSH access
-description: Complete list of all Archipelago nodes — LAN and Tailscale IPs, SSH commands, build capabilities, deploy methods
-type: reference
----
-
-## LAN Nodes
-| Name | IP | SSH | Notes |
-|------|-----|-----|-------|
-| Primary (.228) | 192.168.1.228 | `ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228` | Full build env, CI runner, OAuth proxy |
-| Secondary (.198) | 192.168.1.198 | `ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.198` | Full build env |
-
-## Tailscale Nodes
-| Name | Tailscale IP | Hostname | SSH | Build? |
-|------|-------------|----------|-----|--------|
-| Arch 1 | 100.82.97.63 | — | `ssh -i ~/.ssh/archipelago-deploy archipelago@100.82.97.63` | Unknown |
-| Arch 2 | 100.122.84.60 | archipelago-2.tail2b6225.ts.net | `ssh -i ~/.ssh/archipelago-deploy archipelago@archipelago-2.tail2b6225.ts.net` | Yes (Node, Rust, Podman) |
-| Arch 3 | 100.124.105.113 | archipelago-3.tail2b6225.ts.net | `ssh -i ~/.ssh/archipelago-deploy archipelago@100.124.105.113` | No (Podman only, copy pre-built artifacts) |
-| Arch Atob | 100.113.33.31 | — | `ssh -i ~/.ssh/archipelago-deploy archipelago@100.113.33.31` | Unknown |
-
-## Deploy Methods
-- **LAN nodes (.228, .198):** `./scripts/deploy-to-target.sh --both`
-- **Arch 2:** `ARCHIPELAGO_TARGET="archipelago@archipelago-2.tail2b6225.ts.net" ./scripts/deploy-to-target.sh --live`
-- **Arch 3:** SCP pre-built binary + frontend tarball (no build tools). Do NOT relay through .228 — SSH directly from Mac.
-- **All nodes:** Use `~/.ssh/archipelago-deploy` key
diff --git a/.claude/memory/second-server.md b/.claude/memory/second-server.md
deleted file mode 100644
index 626062e0..00000000
--- a/.claude/memory/second-server.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-name: second-dev-server
-description: Second dev server accessible via Tailscale at archipelago-2.tail2b6225.ts.net, Ryzen 7 7840U, 14GB RAM
-type: project
----
-
-- Hostname: archipelago-2.tail2b6225.ts.net (Tailscale)
-- SSH: `ssh -i ~/.ssh/archipelago-deploy archipelago@archipelago-2.tail2b6225.ts.net`
-- Password: ThunderDome6574839201!
-- CPU: AMD Ryzen 7 7840U (faster than primary i3-8100T)
-- RAM: 14GB
-- Disk: 916GB NVMe
-- OS: Debian 12 (Bookworm) x86_64
-- Has: Podman 4.3.1, Node.js v20.20.1, Rust 1.94.0, Nginx 1.22.1
-- Swap: 4GB configured
-- Deploy: `ARCHIPELAGO_TARGET="archipelago@archipelago-2.tail2b6225.ts.net" ./scripts/deploy-to-target.sh --live`
-- Does NOT use OAuth proxy — uses standard ANTHROPIC_API_KEY for Claude/AIUI
-- First-boot containers created on 2026-03-11 (Bitcoin Knots, LND, Fedimint, PhotoPrism, Ollama, etc.)
-
-## Pending Fixes for Next Deploy
-- **AIUI MIME type error**: Nginx needs a `/aiui/` location block serving correct MIME types for JS files. Currently JS files get wrong content-type causing module load failures.
-- **Self-signed cert warnings**: Expected on fresh deploy, not a bug.
-- **Container connection errors in AIUI console**: Expected until all containers finish starting and syncing.
diff --git a/.claude/memory/tailscale_servers.md b/.claude/memory/tailscale_servers.md
deleted file mode 100644
index 82639dc0..00000000
--- a/.claude/memory/tailscale_servers.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: Tailscale Servers
-description: Archipelago Tailscale servers (archipelago-2, archipelago-3) — hostnames, SSH access, and deploy notes
-type: reference
----
-
-## Tailscale Servers
-
-- **archipelago-2**: `archipelago@archipelago-2.tail2b6225.ts.net`
- - SSH key auth works (`~/.ssh/archipelago-deploy`)
- - Has Node.js, npm, Cargo/Rust, Podman — can do full builds
- - Deploy: `ARCHIPELAGO_TARGET="archipelago@archipelago-2.tail2b6225.ts.net" ./scripts/deploy-to-target.sh --live`
-
-- **archipelago-3**: `archipelago@archipelago-3.tail2b6225.ts.net` (IP: 100.124.105.113)
- - SSH key auth works (key added 2026-03-12)
- - Has Podman only — NO Node.js, NO Rust/Cargo
- - Cannot build on-server; must copy pre-built binary + frontend tarball
- - Deploy method: SCP binary from archipelago-2 or local, upload frontend tarball, extract to `/opt/archipelago/web-ui/`
-
-**How to apply:** For archipelago-2, use the standard deploy script with `ARCHIPELAGO_TARGET`. For archipelago-3, copy pre-built artifacts (binary + frontend tarball) since it lacks build tools.
diff --git a/.claude/memory/third-server.md b/.claude/memory/third-server.md
deleted file mode 100644
index 6b7f3b07..00000000
--- a/.claude/memory/third-server.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-name: third-dev-server
-description: Third dev server accessible via Tailscale at archipelago-3.tail2b6225.ts.net, password ThisIsWeb54321@
-type: project
----
-
-- Hostname: archipelago-3.tail2b6225.ts.net (Tailscale)
-- SSH: `sshpass -p 'ThisIsWeb54321@' ssh -o StrictHostKeyChecking=no archipelago@archipelago-3.tail2b6225.ts.net`
-- Password: ThisIsWeb54321@
-- Deploy: `ARCHIPELAGO_TARGET="archipelago@archipelago-3.tail2b6225.ts.net" ./scripts/deploy-to-target.sh --live`
-- SSH key NOT yet installed — need to copy `~/.ssh/archipelago-deploy.pub` manually
-- Added 2026-03-11
diff --git a/.claude/memory/unbundled-iso.md b/.claude/memory/unbundled-iso.md
deleted file mode 100644
index 3fc612ee..00000000
--- a/.claude/memory/unbundled-iso.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# Unbundled ISO Build (In Progress)
-
-## Status: NOT YET BUILT
-- Server was unreachable (SSH timeout) when we tried to build — user rebooting
-- Changes are in working tree only, NOT YET COMMITTED
-
-## What Was Done
-- Created `image-recipe/build-unbundled-iso.sh` — thin wrapper that sets `UNBUNDLED=1` and delegates to main script
-- Modified `image-recipe/build-auto-installer-iso.sh` to support `UNBUNDLED=1` env var
-
-## Changes to build-auto-installer-iso.sh
-1. Added `UNBUNDLED="${UNBUNDLED:-0}"` config variable
-2. Step 3b: Skips container image capture from server AND registry pull (~20 tars)
-3. Skips `first-boot-containers.sh` bundling (no images to create containers from)
-4. Skips docker UI source bundling (bitcoin-ui, lnd-ui, electrs-ui)
-5. Different ISO filename: `archipelago-installer-unbundled-x86_64.iso`
-6. Updated installer completion message (tells user to install from Marketplace)
-7. Updated build summary output
-
-## What Still Works in Unbundled
-- Full rootfs (Debian 12 + Podman + nginx + SSH)
-- Backend binary + web UI captured from server
-- Tor setup on first boot
-- Image loader service (harmlessly handles empty dir)
-- `package.install` already does `podman pull` — Marketplace works out of the box
-
-## Next Steps
-1. Rsync updated scripts to dev server (192.168.1.228)
-2. Run: `sudo ./build-unbundled-iso.sh`
-3. Result appears in: `image-recipe/results/archipelago-installer-unbundled-x86_64.iso`
diff --git a/.claude/memory/web-only-apps.md b/.claude/memory/web-only-apps.md
deleted file mode 100644
index 8b2ac365..00000000
--- a/.claude/memory/web-only-apps.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-name: web-only-apps
-description: Web-only apps (no container) — L484 category, BotFights, IndieHub. Iframe compatibility, nginx proxying, My Apps injection.
-type: project
----
-
-## Web-Only Apps (added 2026-03-11)
-
-These apps are external websites embedded via iframe — no Docker container. They show as "installed" in both the marketplace and My Apps.
-
-### L484 Category
-- **NWNN** (nwnn.l484.com) — News aggregator. No X-Frame-Options. Works in iframe directly.
-- **484 Kitchen** (484.kitchen) — K484 platform. X-Frame-Options: SAMEORIGIN. Proxied via `/ext/484-kitchen/`.
-- **Call the Operator** (cta.tx1138.com) — Decentralization portal. No X-Frame-Options. Works in iframe directly.
-- **Arch Presentation** (present.l484.com) — Archipelago presentation. X-Frame-Options: SAMEORIGIN. Proxied via `/ext/arch-presentation/`.
-- **Syntropy Institute** (syntropy.institute) — Medicine Reimagined. No X-Frame-Options. Works in iframe directly.
-- **T-0** (teeminuszero.net) — Decentralization documentary. No X-Frame-Options. Works in iframe directly.
-
-### Other Web-Only Apps
-- **BotFights** (botfights.net) — X-Frame-Options: SAMEORIGIN + CSP + COEP/COOP/CORP. Proxied via `/ext/botfights/`. Nginx strips all blocking headers.
-- **IndeeHub** (archipelago.indeehub.studio) — No X-Frame-Options. Works in iframe directly.
-
-### Nginx External Proxies
-Sites with X-Frame-Options get reverse-proxied through nginx at `/ext/{app-id}/`:
-- `proxy_hide_header X-Frame-Options` strips upstream header
-- `add_header X-Content-Type-Options "nosniff" always` prevents server-level X-Frame-Options inheritance
-- BotFights also strips `Cross-Origin-Embedder-Policy`, `Cross-Origin-Opener-Policy`, `Cross-Origin-Resource-Policy`
-- Proxy locations in both HTTP and HTTPS server blocks of nginx-archipelago.conf
-
-### Frontend Implementation
-- **appLauncher.ts**: `EXTERNAL_PROXY` map rewrites external URLs to proxy paths in `toEmbeddableUrl()`
-- **Apps.vue**: `WEB_ONLY_APPS` constant with synthetic `PackageDataEntry` objects. Sorted first alphabetically. No uninstall/start/stop buttons.
-- **Marketplace.vue**: `dockerImage: ''` + `webUrl` in `getCuratedAppList()`. L484 category.
-- **Icons**: `neode-ui/public/assets/img/app-icons/{app-id}.png` (or .svg)
diff --git a/.claude/plans/luminous-snacking-snowflake.md b/.claude/plans/luminous-snacking-snowflake.md
deleted file mode 100644
index addab609..00000000
--- a/.claude/plans/luminous-snacking-snowflake.md
+++ /dev/null
@@ -1,138 +0,0 @@
-# Phase 3 & 4: Encrypted Mesh Messaging + Off-Grid Bitcoin Operations
-
-## Context
-
-Phase 1 built the mesh radio layer (Meshcore protocol, serial driver, basic chat). Phase 2 added transport abstraction (Mesh>LAN>Tor routing, CBOR delta sync, Reed-Solomon chunking). Current encryption is static X25519 shared secret per peer — no forward secrecy, no message type discrimination, no store-and-forward.
-
-Phase 3 adds Signal-style Double Ratchet for forward secrecy, typed messages (ALERT, INVOICE, COORDINATE, PSBT_HASH), and store-and-forward relay. Phase 4 adds off-grid Bitcoin operations: block header relay, transaction relay, Lightning invoice relay, and emergency alert system with dead man's switch.
-
-## Dependencies to Add
-
-```toml
-hkdf = "0.12" # KDF for Double Ratchet chains
-lightning-invoice = "0.34" # BOLT11 parsing (LDK standard, MIT)
-```
-
-Custom Double Ratchet from existing crypto (ed25519-dalek, curve25519-dalek, chacha20poly1305, sha2, hmac) — no DR crate needed.
-
-## Architecture
-
-```
-mesh/
-├── x3dh.rs — X3DH key agreement (prekey bundles, 3-way ECDH)
-├── ratchet.rs — Double Ratchet state machine (forward secrecy)
-├── session.rs — Per-peer session manager (ratchet state persistence)
-├── prekey.rs — Prekey store (signed + one-time prekeys, rotation)
-├── message_types.rs — Typed message envelope (TEXT/ALERT/INVOICE/COORDINATE/PSBT_HASH)
-├── outbox.rs — Store-and-forward queue (24h TTL, relay hops)
-├── bitcoin_relay.rs — TX relay, Lightning relay, block header announce
-├── alerts.rs — Emergency alerts, dead man's switch
-└── (existing files extended: crypto.rs, listener.rs, types.rs, mod.rs)
-```
-
-## Implementation Steps
-
-### Week 1: X3DH + HKDF Foundation
-
-**New**: `mesh/x3dh.rs`, `mesh/prekey.rs`
-**Modify**: `Cargo.toml` (+hkdf), `mesh/crypto.rs`, `mesh/mod.rs`
-
-- `PrekeyBundle`: identity_key + signed_prekey + one_time_prekeys (CBOR, ~200B)
-- `PrekeyStore`: disk persistence at `{data_dir}/prekeys/`, rotation, consumption
-- X3DH: 3-way ECDH → HKDF-SHA256 → root key for Double Ratchet
-- ARCHY:3 identity broadcast with embedded prekey bundle
-
-### Week 2: Double Ratchet Protocol
-
-**New**: `mesh/ratchet.rs` (~500 LOC), `mesh/session.rs` (~300 LOC)
-
-`RatchetState`: DH ratchet keypair, root key, send/recv chain keys, counters, skipped keys (max 100). HKDF-SHA256 chains + ChaCha20-Poly1305 per-message.
-
-Wire format: 40B header (DH pub + counters) + 12 nonce + ciphertext + 16 tag = 68B overhead. Single frame: 64B plaintext. Chunked: ~2.4KB.
-
-`SessionManager`: HashMap, lazy load from `{data_dir}/ratchet/{did_hash}.json`. Backward compat: falls back to static shared secret for ARCHY:2 peers.
-
-### Week 3: Typed Messages + Store-and-Forward
-
-**New**: `mesh/message_types.rs`, `mesh/outbox.rs`
-**Modify**: `mesh/types.rs`, `mesh/listener.rs`
-
-CBOR envelope: `[0x02] [{ t: u8, v: bytes, ts: u32, sig?: bytes }]`
-
-Types: TEXT(0), ALERT(1), INVOICE(2), PSBT_HASH(3), COORDINATE(4), PREKEY_BUNDLE(5), SESSION_INIT(6)
-
-GPS as `Coordinate { lat_microdeg: i32, lng_microdeg: i32 }` — integer only, no float.
-
-`MeshOutbox`: VecDeque, 24h TTL, max 3 relay hops, disk persistence. Checked every 10s tick.
-
-### Week 4: RPC Endpoints + Session Bootstrap
-
-**Modify**: `api/rpc/mesh.rs`, `api/rpc/mod.rs`, `mesh/listener.rs`
-
-New RPC: `mesh.send-invoice`, `mesh.send-coordinate`, `mesh.send-alert`, `mesh.outbox`, `mesh.session-status`, `mesh.rotate-prekeys`
-
-Prekey distribution via ARCHY:3 broadcasts. Session init via X3DH on first message to new peer.
-
-### Week 5: Off-Grid Bitcoin (Phase 4)
-
-**New**: `mesh/bitcoin_relay.rs`, `mesh/block_headers.rs`
-**Modify**: `Cargo.toml` (+lightning-invoice), `api/rpc/mesh.rs`
-
-Block header relay: Internet node broadcasts `BlockHeaderAnnouncement` (height, hash, Ed25519 sig) on new block. Mesh-only peers display "SPV sync via mesh".
-
-TX relay: Mesh-only node sends raw tx hex → internet peer calls `sendrawtransaction` → returns txid.
-
-Lightning relay: Create invoice → send bolt11 → peer pays → proof-of-payment returned.
-
-### Week 6: Emergency Alerts + Dead Man's Switch
-
-**New**: `mesh/alerts.rs`
-
-`DeadManSwitch`: Background task, configurable interval (default 6h), broadcasts signed ALERT with GPS to emergency contacts when triggered. Auto-check-in on any authenticated RPC.
-
-RPC: `mesh.alert-configure`, `mesh.alert-checkin`, `mesh.alert-test`, `mesh.alert-status`
-
-### Week 7: Frontend
-
-**Modify**: `stores/mesh.ts`, `views/Mesh.vue`, `mock-backend.js`
-
-Message rendering by type: invoice (orange card + Pay button), alert (red card), coordinate (blue card + OSM link), psbt_hash (gray card + Review).
-
-Session indicator: shield icon (green=ratchet, yellow=static, gray=none).
-
-Block height in off-grid banner. Alert config panel. Dead man switch toggle.
-
-### Week 8: Integration Test + Deploy
-
-E2E on .228 (internet) + .198 (mesh-only): X3DH handshake, 50-message ratchet, invoice relay, TX relay, block headers, dead man switch. Deploy to both servers.
-
-## New Files (8)
-
-1. `core/archipelago/src/mesh/x3dh.rs`
-2. `core/archipelago/src/mesh/prekey.rs`
-3. `core/archipelago/src/mesh/ratchet.rs`
-4. `core/archipelago/src/mesh/session.rs`
-5. `core/archipelago/src/mesh/message_types.rs`
-6. `core/archipelago/src/mesh/outbox.rs`
-7. `core/archipelago/src/mesh/bitcoin_relay.rs`
-8. `core/archipelago/src/mesh/alerts.rs`
-
-## Modified Files (8)
-
-1. `core/archipelago/Cargo.toml` — +hkdf, +lightning-invoice
-2. `core/archipelago/src/mesh/crypto.rs` — +hkdf_sha256, +ephemeral keygen
-3. `core/archipelago/src/mesh/types.rs` — +message_type, +typed payloads
-4. `core/archipelago/src/mesh/listener.rs` — typed dispatch, session bootstrap, relay
-5. `core/archipelago/src/mesh/mod.rs` — new submodules, new MeshService methods
-6. `core/archipelago/src/api/rpc/mesh.rs` — ~12 new RPC endpoints
-7. `core/archipelago/src/api/rpc/mod.rs` — register new routes
-8. `neode-ui/src/views/Mesh.vue` — typed rendering, alert UI, session badges
-
-## Verification
-
-```bash
-cargo test --all-features -- mesh::ratchet mesh::x3dh mesh::session
-cargo clippy --all-targets --all-features
-cd neode-ui && npm run type-check
-./scripts/deploy-to-target.sh --both
-```
diff --git a/.claude/plans/memoized-plotting-sifakis.md b/.claude/plans/memoized-plotting-sifakis.md
deleted file mode 100644
index c0c0c1e0..00000000
--- a/.claude/plans/memoized-plotting-sifakis.md
+++ /dev/null
@@ -1,145 +0,0 @@
-# Architecture Review — Fix Remaining Issues
-
-## Context
-
-The architecture review (`docs/architecture-review.html`) identified 4 P0, 6 P1, and 6 medium-priority issues across the codebase. After research, **all 4 P0s and 4 of 6 P1s are already fixed**. This plan addresses the remaining open items that improve reliability and security during the beta freeze.
-
-**What's already fixed:** P0-1 (health RPC), P0-2 (health checks), P0-3 (backup rollback), P0-4 (nginx protections), P1-B (rate limiter cleanup), P1-C (systemd limits), P1-E (WS reconnect), P1-F (Vue error handler), Issue 11 (session async I/O).
-
-**What we're fixing now (4 items):**
-
----
-
-## Item 1: Add 10s timeout to 6 bare `client.connect()` calls — DONE
-
-**Why:** A down Nostr relay hangs the async task indefinitely, blocking identity publishing, node discovery, and marketplace operations. Direct uptime impact.
-
-### Files & locations
-
-| File | Line | Function |
-|------|------|----------|
-| `core/archipelago/src/identity_manager.rs` | 409 | `publish_profile()` |
-| `core/archipelago/src/nostr_discovery.rs` | 113 | `publish_node_revocation()` |
-| `core/archipelago/src/nostr_discovery.rs` | 200 | `verify_revocation()` |
-| `core/archipelago/src/nostr_discovery.rs` | 264 | `discover_archipelago_nodes()` |
-| `core/archipelago/src/marketplace.rs` | 298 | `discover()` |
-| `core/archipelago/src/marketplace.rs` | 406 | `publish()` |
-
-### Pattern (from `nostr_handshake.rs:126`)
-
-Replace each `client.connect().await;` with:
-```rust
-if tokio::time::timeout(Duration::from_secs(10), client.connect()).await.is_err() {
- tracing::warn!("Nostr relay connection timed out after 10s, continuing anyway");
-}
-```
-
-Ensure `use std::time::Duration;` is imported in each file. `tracing::warn!` is already available in all three files.
-
-### Risk: LOW — Mechanical pattern replication, no logic changes.
-
----
-
-## Item 2: Pin all crypto dependency versions exactly — DONE
-
-**Why:** Floating versions (`"2.1"` instead of `"2.2.0"`) allow `cargo update` to silently change crypto libraries. Supply chain risk + project rules violation.
-
-### Versions (verified from Cargo.lock)
-
-**`core/archipelago/Cargo.toml`:**
-
-| Line | Current | Pin to |
-|------|---------|--------|
-| 44 | `sha2 = "0.10"` | `"0.10.9"` |
-| 45 | `hmac = "0.12"` | `"0.12.1"` |
-| 50 | `ed25519-dalek = { version = "2.1", ... }` | `version = "2.2.0"` |
-| 51 | `curve25519-dalek = "4"` | `"4.1.3"` |
-| 52 | `rand = "0.8"` | `"0.8.5"` |
-| 69 | `argon2 = "0.5"` | `"0.5.3"` |
-| 70 | `chacha20poly1305 = "0.10"` | `"0.10.1"` |
-| 81 | `zeroize = { version = "1.7", ... }` | `version = "1.8.2"` |
-| 92 | `hkdf = "0.12"` | `"0.12.4"` |
-
-**`core/security/Cargo.toml`:**
-
-| Line | Current | Pin to |
-|------|---------|--------|
-| 16 | `aes-gcm = "0.10"` | `"0.10.3"` |
-| 17 | `rand = "0.8"` | `"0.8.5"` |
-| 19 | `zeroize = { version = "1", ... }` | `version = "1.8.2"` |
-
-**Note:** `core/models/Cargo.toml` has `ed25519-dalek = "2.0.0"` but this crate is NOT in the workspace — it's dead code. Skip it.
-
-### Risk: LOW — Pins to versions already resolved in Cargo.lock. No actual dependency changes.
-
----
-
-## Item 3: Pin all floating container image tags — DONE
-
-**Why:** Floating tags (`:1`, `:7`, `:alpine`, `:main`) mean two installs a week apart get different software. Supply chain risk and a support nightmare.
-
-### File: `scripts/image-versions.sh`
-
-| Line | Variable | Current Tag | Action |
-|------|----------|-------------|--------|
-| 16 | `MARIADB_IMAGE` | `:11.4` | SSH -> get exact patch version |
-| 21 | `POSTGRES_IMAGE` | `:15` | SSH -> get exact patch version |
-| 22 | `BTCPAY_POSTGRES_IMAGE` | `:15` | SSH -> get exact patch version |
-| 25 | `HOMEASSISTANT_IMAGE` | `:2024.12` | SSH -> get exact patch version |
-| 27 | `UPTIME_KUMA_IMAGE` | `:1` | SSH -> get exact patch version |
-| 32 | `NEXTCLOUD_IMAGE` | `:29` | SSH -> get exact patch version |
-| 34 | `ONLYOFFICE_IMAGE` | `:8.2` | SSH -> get exact patch version |
-| 35 | `FILEBROWSER_IMAGE` | `:v2` | SSH -> get exact patch version |
-| 36 | `NPM_IMAGE` | `:2` | SSH -> get exact patch version |
-| 49 | `REDIS_IMAGE` | `:7` | SSH -> get exact patch version |
-| 52 | `VALKEY_IMAGE` | `:8` | SSH -> get exact patch version |
-| 60 | `INDEEDHUB_POSTGRES_IMAGE` | `:16-alpine` | SSH -> get exact patch version |
-| 61 | `INDEEDHUB_REDIS_IMAGE` | `:7-alpine` | SSH -> get exact patch version |
-| 64 | `DWN_SERVER_IMAGE` | `:main` | SSH -> get image digest, pin by SHA or tag |
-| 68 | `NGINX_ALPINE_IMAGE` | `:alpine` | SSH -> get exact version |
-
-### Pre-work required
-Run on 192.168.1.228: `podman images --format '{{.Repository}}:{{.Tag}}'` to get exact versions currently deployed. Pin to THOSE — don't upgrade.
-
-### Risk: MEDIUM — Must match what's actually running. Wrong pin = containers fail on next creation.
-
----
-
-## Item 4: Add CI pipeline for Rust + frontend checks — DONE
-
-**Why:** No tests or linting run in CI. Regressions from Items 1-3 (and all future beta fixes) go undetected until they hit the server.
-
-### File to create: `.github/workflows/ci.yml`
-
-Two parallel jobs:
-1. **`rust`** (ubuntu-latest): `cargo fmt --check` -> `cargo clippy -D warnings` -> `cargo test`
-2. **`frontend`** (ubuntu-latest): `npm ci` -> `npm run type-check` -> `npm test`
-
-Trigger: push to `main` + all PRs. Reference existing `build-macos.yml` for action versions (checkout@v4, setup-node@v4 with Node 18).
-
-### Risk: LOW — Additive only, new file, doesn't affect existing workflows.
-
----
-
-## Execution Order
-
-1. **Item 1** (Nostr timeouts) — lowest risk, immediate reliability gain
-2. **Item 2** (crypto pins) — batch with Item 1 for single deploy
-3. **Item 3** (container image pins) — requires SSH query first
-4. **Item 4** (CI) — validates everything, no deploy needed
-
-Items 1+2 deploy together. Item 3 deploys separately (script only). Item 4 is push-only.
-
-## Verification
-
-- Items 1+2: `cargo clippy --all-targets --all-features` on dev server (zero warnings), then deploy + test identity/discovery/marketplace features
-- Item 3: `source scripts/image-versions.sh` + verify all vars have exact patch versions
-- Item 4: Push to branch, verify both CI jobs pass green on GitHub Actions
-
-## Deferred (post-beta)
-
-- Issue 6: Generate TS types from Rust (ts-rs) — new dependency
-- Issue 7: Consolidate container metadata to single source — structural refactor
-- Issue 8: Split deploy/ISO scripts into modules — already planned in script comments
-- Issue 9: Single app manifest driving all 6+ locations — architectural change
-- Issue 12: useAsyncState composable — touches 14+ views, risky during freeze
diff --git a/.claude/plans/mutable-roaming-pancake.md b/.claude/plans/mutable-roaming-pancake.md
deleted file mode 100644
index 2f2600d8..00000000
--- a/.claude/plans/mutable-roaming-pancake.md
+++ /dev/null
@@ -1,357 +0,0 @@
-# Gold Standard Claude Code Configuration — Archipelago
-
-## Context
-
-The last optimization (2026-03-28) cut CLAUDE.md from 130→101 lines and skills from 33→11. That was the right first pass. This plan is the second pass: fixing structural issues the first cleanup didn't address — hook duplication, memory chaos, a leaked API key, missing path scoping, context budget waste, and underutilized agent/permission systems. The goal is a configuration so tight that re-running this audit would produce zero suggestions.
-
-**Research base**: Every file in `.claude/` (project + global), all 26 project memories, all 8 auto-memories, all 11 skills, all 5 rules, all 11 hooks, both settings files, the iframe-specialist agent, the full project structure (core/, neode-ui/, scripts/, image-recipe/, apps/, .gitea/), latest Claude Code docs (CLAUDE.md best practices, hooks v2.1.85+, skills frontmatter, agents, memory, permissions, MCP, context management, agent teams), and the 2026-03-28 cleanup feedback.
-
-**Governing principle** (carried from cleanup): *Every line must prevent a specific mistake Claude would otherwise make. If Claude does it right without the instruction, it's noise.*
-
----
-
-## Phase 0: CRITICAL — Remove Leaked Secret
-
-**File**: `.claude/memory/deploy-automation.md` (line 11)
-Contains a plaintext Anthropic API key: `sk-ant-api03-...`
-
-**Action**: Remove the key immediately. Replace with: `"ANTHROPIC_API_KEY from secrets store (never stored in memory files)"`
-
-This is the only blocking item. Everything else is optimization.
-
----
-
-## Phase 1: CLAUDE.md — Trim to ~75 Lines
-
-**File**: `/Users/dorian/Projects/archy/CLAUDE.md`
-**Current**: 101 lines | **Target**: ~75 lines | **Saves**: ~500 tokens/session
-
-### What to cut (reference data that doesn't prevent mistakes)
-
-| Section | Lines | Action | Reason |
-|---------|-------|--------|--------|
-| Infrastructure table | 21-30 | Move to auto-memory | Reference data, not a rule. Already in memory files |
-| ISO debug commands | 79-84 | Move to `iso-debug` skill reference | Diagnostic commands, not rules |
-| Kiosk toggle info | 85-86 | Move to auto-memory or delete | Reference, not a rule |
-| "Backend binds 127.0.0.1" | 63 | Move to new backend rule | Claude can read the code |
-| "Timeouts on all external operations" | 65 | Move to new backend rule | Already in `rules/api.md` |
-
-### What to add
-
-```markdown
-## Compact Instructions
-When compacting, preserve: list of modified files, test results, deploy target state, current branch.
-```
-
-This costs 2 lines but saves entire sessions from losing critical context.
-
-### Resulting structure (~75 lines)
-
-```
-Lines 1-2: Project description + stack
-Lines 3-6: Beta freeze notice
-Lines 7-12: Quick reference (dev, build, deploy commands)
-Lines 13-18: Architecture diagram (compact)
-Lines 19-20: Data paths
-Lines 21-26: Critical Rules (5 rules)
-Lines 27-33: App Integration Checklist
-Lines 34-36: Git conventions
-Lines 37-39: Compact instructions
-```
-
-Infrastructure table moves to auto-memory where it's still loaded at session start.
-
----
-
-## Phase 2: Hook Deduplication — Eliminate Double Execution
-
-### Problem
-
-Every `Bash` call runs **both** global `pretooluse-bash.sh` AND project `block-risky-bash.sh`. Every `Edit|Write` call runs **both** global `pretooluse-files.sh` AND project `protect-files.sh`. They overlap on ~80% of patterns (rm -rf, git reset --hard, .git/ edits, .env files, etc.).
-
-**Cost**: 2 extra Python processes per tool call, checking the same patterns twice.
-
-### Solution: Project hooks become project-specific only
-
-**File**: `.claude/hooks/block-risky-bash.sh`
-**Action**: Strip all patterns already covered by global hook. Keep ONLY:
-- Cargo build on macOS (Archy-specific: "build on dev server via SSH")
-- Path traversal with rm (more aggressive check than global)
-
-~15 lines instead of ~80.
-
-**File**: `.claude/hooks/protect-files.sh`
-**Action**: Strip all patterns already covered by global hook. Keep ONLY:
-- `scripts/deploy-config.sh` (Archy-specific credential file)
-- Path-outside-project check (project-specific boundary)
-
-~20 lines instead of ~75.
-
-**Global hooks stay unchanged** — they're the universal baseline.
-
-### Result
-- Before: 4 Python processes per Bash call (2 global + 2 project parsing same JSON)
-- After: 2 Python processes per Bash call (1 global comprehensive + 1 tiny project-specific)
-
----
-
-## Phase 3: Memory System — Consolidate and Clean
-
-### Problem
-
-Two separate memory systems with overlapping content:
-1. **Auto-memory** (`~/.claude/projects/-Users-dorian-Projects-archy/memory/`) — 8 files, auto-loaded
-2. **Project memory** (`.claude/memory/`) — 26 files, NOT auto-loaded
-
-Claude sees auto-memory every session. Project memory only loads if Claude manually reads it.
-
-### Solution: Curate auto-memory, keep project memory as archive
-
-**Auto-memory MEMORY.md** — restructure to ~25 lines with the most critical feedback:
-
-```markdown
-# Archipelago Project Memory
-
-## Critical Feedback (prevent recurring mistakes)
-- [Direct Port Rule](feedback_apps_always_direct_port.md) — Apps MUST use direct port, NEVER proxy paths
-- [External URLs](feedback_external_urls_iframe.md) — Open https:// directly, never /ext/
-- [Deploy All Nodes](feedback_indeedhub_deploy_all_servers.md) — Deploy to ALL nodes
-- [No Tor Publishing](feedback_no_tor_relay_publishing.md) — Never publish .onion to relays
-- [UFW Forward](feedback_podman_ufw_forward.md) — DEFAULT_FORWARD_POLICY=ACCEPT
-- [Deploy Patterns](feedback_deploy_patterns.md) — Rootless port 80, cred sync, image export
-- [Asset Workflow](feedback_asset_workflow.md) — Never generate images, user is designer
-- [ASCII Logo](feedback_logo_ascii.md) — Block-letter logo locked, never change
-- [Claude Cleanup](feedback_claude_cleanup.md) — Instruction optimization principles
-
-## Infrastructure
-- [CI/CD & Registry](reference_cicd_registry.md) — git.tx1138.com, act_runner, insecure registry
-- [Multi-Node Deploy](reference_multi_node_deploy.md) — 5 nodes, SSH keys, deploy methods
-- [Infrastructure Quick Ref](reference_infrastructure.md) — IPs, passwords, SSH keys (moved from CLAUDE.md)
-
-## Project State
-- [ISO Testing](project_iso_testing_plan.md) — Hardware matrix, boot compatibility
-- [ISO Custom Base](project_iso_size_reduction.md) — Debootstrap ISO, remaining issues
-
-## Archive
-Detailed project memory in .claude/memory/MEMORY.md (26 files, not auto-loaded).
-```
-
-**New auto-memory files to create** (migrated from project memory):
-- `feedback_apps_always_direct_port.md` — Broken THREE TIMES, highest-value feedback
-- `feedback_deploy_patterns.md` — Hard-won container patterns
-- `feedback_asset_workflow.md` — Prevents wasted effort generating images
-- `feedback_logo_ascii.md` — Prevents changing locked-in branding
-- `reference_infrastructure.md` — Infrastructure table from CLAUDE.md (IPs, SSH, passwords)
-
-**Project memory (.claude/memory/)**:
-- Add comment at top of MEMORY.md: ``
-- Fix `deploy-automation.md` (Phase 0 — remove API key)
-- Update `unbundled-iso.md` (still says "NOT YET BUILT")
-
----
-
-## Phase 4: Permissions — Auto-Approve Safe Commands
-
-**File**: `.claude/settings.local.json`
-
-**Current**: Only `ssh:*` and `gh api:*` allowed.
-
-**Updated** — add read-only and build/test commands:
-
-```json
-{
- "permissions": {
- "allow": [
- "Bash(ssh:*)",
- "Bash(gh api:*)",
- "Bash(cd neode-ui*)",
- "Bash(npm run *)",
- "Bash(npm test*)",
- "Bash(npm start*)",
- "Bash(npx vue-tsc*)",
- "Bash(npx vitest*)",
- "Bash(git log*)",
- "Bash(git diff*)",
- "Bash(git status*)",
- "Bash(git branch*)",
- "Bash(git show*)",
- "Bash(git stash*)",
- "Bash(cargo check*)",
- "Bash(cargo clippy*)",
- "Bash(cargo test*)",
- "Bash(journalctl*)",
- "Bash(systemctl status*)",
- "Bash(ls *)",
- "Bash(wc *)",
- "Bash(file *)",
- "Bash(xxd *)",
- "Bash(df *)",
- "Bash(du *)"
- ]
- }
-}
-```
-
-**NOT auto-approved** (still require confirmation):
-- `git push/commit` — Affects remote/creates state
-- `cargo build` — Blocked by hook on macOS anyway
-- `npm install` — Modifies dependencies
-- `./scripts/deploy-*` — Deploys to servers
-- `rm`, `mv`, `cp` — Potentially destructive
-
----
-
-## Phase 5: Merge iso-branding into build-iso
-
-**Problem**: `iso-branding` is a pure design reference, only relevant during ISO builds. Its description consumes skill budget.
-
-**Action**:
-1. Move `.claude/skills/iso-branding/SKILL.md` content → `.claude/skills/build-iso/references/branding.md`
-2. Update `build-iso/SKILL.md` to reference the branding file
-3. Delete `.claude/skills/iso-branding/` directory
-
-**Skill count**: 11 → 10
-
----
-
-## Phase 6: Add Backend Rule File
-
-**Problem**: No path-scoped rule for Rust backend. 3 backend rules sit in CLAUDE.md (loaded every session even for frontend-only work).
-
-**New file**: `.claude/rules/backend.md`
-
-```markdown
----
-globs:
- - "core/**/*.rs"
- - "core/**/Cargo.toml"
----
-
-# Backend Rules (Archipelago — Rust)
-
-- Backend binds `127.0.0.1` only — nginx handles external access
-- Validate all input before path construction — reject `..`, `/`, null bytes
-- Timeouts on all external operations (10s default, 30s heavy)
-- Use `anyhow::Result` for error propagation, not `.unwrap()` in handlers
-- Log with `tracing`, never `println!` or `eprintln!` in production paths
-- Container commands through `PodmanClient` (core/container/), never raw Command::new("podman")
-```
-
-Delete the Backend section from CLAUDE.md (moved here).
-
----
-
-## Phase 7: Tighten prompt-injection-detect.sh
-
-**Problem**: `context_manipulation` pattern matches `IMPORTANT:`, `CRITICAL:`, `` — normal in code/docs. Creates false positive warnings.
-
-**Action**: Tighten the `context_manipulation` regex to require injection-specific signatures:
-
-```bash
-# OLD (too broad):
-"IMPORTANT:|CRITICAL:|SYSTEM:|ADMIN:|||"
-
-# NEW (specific):
-"(?:^|\s)(?:SYSTEM|ADMIN):\s*(?:you are|ignore|forget|override|new instructions)|<(?:system|instructions)>.*(?:ignore|override|forget)"
-```
-
----
-
-## Phase 8: Add 2 Focused Agents
-
-**Current**: 1 agent (iframe-specialist, 678 lines)
-
-**Add**:
-
-### `.claude/agents/deploy-specialist.md`
-```yaml
----
-name: deploy-specialist
-description: Deploys to all 5 Archipelago nodes. Knows SSH access, build capabilities, post-deploy verification.
-tools: Bash, Read, Grep, Glob
-model: sonnet
----
-```
-Body: Node inventory, deploy workflow, IndeedHub multi-node rules, post-deploy checklist.
-
-### `.claude/agents/code-reviewer.md`
-```yaml
----
-name: code-reviewer
-description: Reviews code against Archipelago standards — frontend patterns, Rust safety, container security, crypto rules.
-tools: Read, Grep, Glob
-model: sonnet
----
-```
-Body: Frontend rules, backend rules, container rules, security checklist.
-
-**Agent count**: 1 → 3
-
----
-
-## Phase 9: Skill Frontmatter Audit
-
-**Problem**: Action skills that have side effects should have `disable-model-invocation: true` to prevent Claude from auto-invoking them.
-
-| Skill | Has `disable-model-invocation: true`? | Needs it? |
-|-------|--------------------------------------|-----------|
-| add-app | Yes | Yes (side effects) |
-| add-web-app | Verify | Yes |
-| build-iso | Verify | Yes (builds ISO) |
-| iso-debug | Verify | Yes (runs diagnostics) |
-| podman | Verify | Yes (modifies containers) |
-| polish | Verify | Yes (modifies code) |
-| sweep | Verify | Yes (runs checks, may fix) |
-| mesh | No | No (reference knowledge) |
-| design-pixel-retro | No | No (reference knowledge) |
-| gamepad-nav | No | No (reference knowledge) |
-
-Action: Verify and add `disable-model-invocation: true` to all 7 action skills.
-
----
-
-## Summary
-
-| Phase | Impact | Files Changed | Benefit |
-|-------|--------|---------------|---------|
-| 0. Remove API key | CRITICAL | 1 | Security |
-| 1. Trim CLAUDE.md | HIGH | 1 | ~500 tokens/session saved |
-| 2. Dedup hooks | HIGH | 2 | ~200ms faster per tool call |
-| 3. Memory consolidate | HIGH | ~8 | Cleaner context, no stale data |
-| 4. Permissions | MEDIUM | 1 | ~3s saved per safe command |
-| 5. Merge iso-branding | LOW | 3 | 1 less skill description |
-| 6. Backend rule | MEDIUM | 2 | Path-scoped, not always-loaded |
-| 7. Injection hook | LOW | 1 | Fewer false positives |
-| 8. New agents | MEDIUM | 2 new | Better delegation |
-| 9. Skill frontmatter | LOW | ~5 | Prevents unintended auto-invoke |
-
-**Net changes**: CLAUDE.md 101→~75 lines, skills 11→10, agents 1→3, rules 5→6, hooks 60% smaller
-
----
-
-## What This Plan Does NOT Change (and why each was evaluated)
-
-- **Global CLAUDE.md** (36 lines) — Already optimized, passes the "would removing cause mistakes?" test
-- **Global hooks** (8 scripts) — Universal baseline, well-tuned, no project overlap
-- **Global rules** (api, crypto, bitcoin) — Correct glob scoping, concise content
-- **Global settings.json** — Plugins, effort level, hook config all justified
-- **iframe-specialist agent** — Deep reference, correctly scoped, rarely loaded
-- **Skills mesh/gamepad-nav/design-pixel-retro** — Tiny description cost (~120 chars each), valuable on-demand
-- **MCP servers** — Not needed (self-hosted infra, no external API integrations)
-- **Agent teams** — Experimental, single-developer project doesn't benefit
-- **Project .claude/memory/ (26 files)** — Kept as archive with annotation
-
----
-
-## Verification Checklist
-
-After implementation:
-- [ ] `grep -r "sk-ant" .claude/` returns zero results
-- [ ] New session auto-loads MEMORY.md with all critical feedback
-- [ ] `git status` auto-approves without permission prompt
-- [ ] `/sweep` skill loads and executes correctly
-- [ ] Project hooks run fast (no duplicate pattern checks)
-- [ ] `cd neode-ui && npx vue-tsc -b --noEmit` passes
-- [ ] Spawning deploy-specialist agent works
-- [ ] CLAUDE.md is ≤80 lines
-- [ ] `/context` shows reasonable token budget
diff --git a/.claude/plans/plan.md b/.claude/plans/plan.md
deleted file mode 100644
index b0429b08..00000000
--- a/.claude/plans/plan.md
+++ /dev/null
@@ -1,803 +0,0 @@
-# Archipelago: Production Excellence Plan
-
-**Duration**: 12 months (48 weeks)
-**Goal**: Code so good no developer could question any decision. Apple-level reliability. Every failure visible and recoverable. Every operation bounded. Every line justified.
-**Audited**: 2026-03-20 — 122 Rust files, 38 Vue views, 180+ frontend files, 80+ shell scripts
-
-## CONSTRAINTS
-
-- **DEPLOY ONLY TO .198** — Never .228. All verification on .198.
-- **BETA FREEZE** — Behavior-preserving only. No new features/UI/endpoints.
-- **Tests before every refactor** — Capture current behavior first. Tests must pass unchanged after.
-- **Atomic commits** — One logical change per commit. Every step compiles + passes tests.
-
-```bash
-ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.198
-```
-
----
-
-## COMPLETE ISSUE REGISTRY
-
-### Backend Rust — 122 files audited
-
-| ID | Issue | File(s) | Severity |
-|----|-------|---------|----------|
-| R1 | Health RPC endpoint has no handler — returns "Unknown method" | `api/rpc/mod.rs` | P0 |
-| R2 | Nostr client.connect() hangs indefinitely (4 calls, no timeout) | `nostr_handshake.rs:124,161,262,282` | P0 |
-| R3 | Backup restore extracts directly to live dir — no atomic rollback | `backup/full.rs:122-149` | P0 |
-| R4 | Rate limiter cleanup() never spawned — HashMap grows forever | `session.rs:566-579` | P1 |
-| R5 | Login rate limiter same issue — entries never evicted | `session.rs:452-472` | P1 |
-| R6 | Blocking std::fs in async — session.rs (6 calls) | `session.rs:77,128,370,413,423,425` | P1 |
-| R7 | Blocking std::fs in async — docker_packages.rs | `docker_packages.rs:561,573` | P1 |
-| R8 | Blocking std::fs in async — port_allocator.rs | `port_allocator.rs:59,73,77` | P1 |
-| R9 | Blocking std::fs in async — peers.rs, node_message.rs | `peers.rs:30`, `node_message.rs:65` | P1 |
-| R10 | Blocking std::fs in async — identity.rs, identity_manager.rs | `identity.rs:50`, `identity_manager.rs:164` | P1 |
-| R11 | Blocking std::fs in async — nostr_discovery.rs | `nostr_discovery.rs:55` | P1 |
-| R12 | Sync TCP I/O in async context — electrs_status.rs | `electrs_status.rs:5,40,78,81` | P1 |
-| R13 | .expect() in main.rs startup | `main.rs:124,159` | P2 |
-| R14 | .parse().unwrap() in session.rs rate limiting | `session.rs:665,676,688` | P1 |
-| R15 | 7 .unwrap()/.expect() in mesh/protocol.rs | `protocol.rs:582,592,614,649,679,713,728` | P1 |
-| R16 | .expect() in identity.rs crypto | `identity.rs:114,119` | P2 |
-| R17 | .unwrap() in helpers/lib.rs (5 calls) | `helpers/lib.rs:167,172,180,233,253` | P2 |
-| R18 | .unwrap() in helpers/rsync.rs (5 calls) | `rsync.rs:196,199,202,210,220` | P2 |
-| R19 | .unwrap() in js-engine/lib.rs | `js-engine/lib.rs:130,249` | P2 |
-| R20 | 14 #[allow(dead_code)] suppressions in mesh/mod.rs | `mesh/mod.rs:7-25` | P2 |
-| R21 | Dead code in lnd.rs, data_manager.rs, dev_orchestrator.rs | Multiple | P2 |
-| R22 | Bitcoin RPC URL hardcoded in 4+ files | `bitcoin.rs:89`, `mesh/mod.rs:624,649,663`, `listener.rs:1509+` | P2 |
-| R23 | DWN health URL hardcoded | `dwn_sync.rs:76` | P2 |
-| R24 | Update manifest URL hardcoded | `update.rs:11` | P3 |
-| R25 | DNS-over-HTTPS URLs hardcoded (4 providers) | `network/dns.rs:98,102,106,110` | P3 |
-| R26 | DWN protocol URIs hardcoded in server.rs | `server.rs:453-456` | P3 |
-| R27 | Missing timeouts on mesh Bitcoin RPC calls | `mesh/mod.rs:624,649,663` | P1 |
-| R28 | Missing timeouts on LND proxy calls (68 .send() calls) | `api/rpc/lnd.rs` | P2 |
-| R29 | Missing timeout on DWN health check | `dwn_sync.rs:76` | P2 |
-| R30 | TODO: track last-seen timestamp | `handshake.rs:77` | P3 |
-| R31 | TODO: lnd.lookupinvoice RPC endpoint | `marketplace.rs:183` | P3 |
-| R32 | TODO: trigger auto-restart or alert | `container/health_monitor.rs:140` | P3 |
-| R33 | TODO: configure Podman to use AppArmor profile | `security/container_policies.rs:68` | P3 |
-| R34 | Tor rotation deletes old .onion immediately — no transition | `api/rpc/tor.rs:184-240` | P1 |
-| R35 | package.rs god file — 1,795 lines | `api/rpc/package.rs` | P2 |
-| R36 | mesh/listener.rs god file — 1,799 lines | `mesh/listener.rs` | P2 |
-| R37 | rpc/mod.rs god file — 1,092 lines | `api/rpc/mod.rs` | P2 |
-| R38 | lnd.rs god file — 1,068 lines | `api/rpc/lnd.rs` | P2 |
-| R39 | monitoring/mod.rs — 993 lines | `monitoring/mod.rs` | P3 |
-| R40 | api/handler.rs — 911 lines | `api/handler.rs` | P3 |
-| R41 | 30+ functions exceed 50 lines across codebase | Multiple | P3 |
-
-### Frontend — 180+ files audited
-
-| ID | Issue | File(s) | Severity |
-|----|-------|---------|----------|
-| F1 | WebSocket subscription registered multiple times — race condition | `stores/app.ts:88-134` | P0 |
-| F2 | Unprotected concurrent mesh state mutations | `stores/mesh.ts:249-268,294-324` | P0 |
-| F3 | No global Vue error handler — white screen on error | `main.ts` | P0 |
-| F4 | Stale data after WebSocket reconnect — no full refresh | `stores/app.ts:88-163` | P1 |
-| F5 | Message polling timer never stopped after logout | `composables/useMessageToast.ts:60` | P1 |
-| F6 | AppLauncher NIP-07 message listener leak on close | `stores/appLauncher.ts:295-301` | P1 |
-| F7 | Audio player listeners stack — never cleaned up | `composables/useAudioPlayer.ts:1-91` | P1 |
-| F8 | WebSocket reconnection race — parallel connect() attempts | `api/websocket.ts:212-238` | P2 |
-| F9 | WebSocket parse error silently caught — stale UI forever | `api/websocket.ts:164-172` | P2 |
-| F10 | WebSocket stale connection detection too aggressive (5min) | `api/websocket.ts:284-299` | P2 |
-| F11 | RPC client backoff + timeout = 40s max wait | `api/rpc-client.ts:31-117` | P2 |
-| F12 | No code splitting — monolithic bundle | `vite.config.ts` | P2 |
-| F13 | v-html on QR code without DOMPurify | `views/Settings.vue:441` | P2 |
-| F14 | Goals store O(n) alias lookup on every computed | `stores/goals.ts:16-20,38-89` | P2 |
-| F15 | localStorage save without try/catch (5+ instances) | `stores/goals.ts:34-36` + others | P2 |
-| F16 | FileBrowser auth token duality — memory + cookie | `api/filebrowser-client.ts:39,50-68` | P2 |
-| F17 | CSRF token cookie parsing brittle — regex only | `api/rpc-client.ts:18-21` | P2 |
-| F18 | aiPermissions.ts Set uses unsafe type assertion | `stores/aiPermissions.ts:91-103` | P3 |
-| F19 | Untracked setTimeout in AppSession — fires after unmount | `views/AppSession.vue:507` | P3 |
-| F20 | Dashboard navigation missing aria-current="page" | `views/Dashboard.vue` | P3 |
-| F21 | Search performance — string re-lowercasing every keystroke | `views/Apps.vue:510-537` | P3 |
-| F22 | 30+ backdrop-filter blur elements — GPU overload on mobile | `style.css` | P3 |
-| F23 | Record on sensitive DID operations | `types/api.ts` + `rpc-client.ts` | P3 |
-| F24 | checkInterval timer leak on connect race | `api/websocket.ts:82-96` | P3 |
-| F25 | Web5.vue god component — 3,940 lines | `views/Web5.vue` | P2 |
-| F26 | Mesh.vue — 2,106 lines | `views/Mesh.vue` | P2 |
-| F27 | Dashboard.vue — 1,819 lines | `views/Dashboard.vue` | P2 |
-| F28 | Settings.vue — 1,792 lines | `views/Settings.vue` | P2 |
-| F29 | Marketplace.vue — 1,293 lines | `views/Marketplace.vue` | P3 |
-| F30 | Server.vue — 1,132 lines | `views/Server.vue` | P3 |
-| F31 | Home.vue — 1,059 lines | `views/Home.vue` | P3 |
-| F32 | AppDetails.vue — 1,036 lines | `views/AppDetails.vue` | P3 |
-| F33 | useAppStore god store — 324 lines, 16 methods, 8+ responsibilities | `stores/app.ts` | P2 |
-
-### Shell Scripts — 80+ files audited
-
-| ID | Issue | File(s) | Severity |
-|----|-------|---------|----------|
-| S1 | 60+ instances of `sudo podman` — should be rootless | `fix-indeedhub(28)`, `deploy-bitcoin(11)`, `deploy-tailscale(2+)` | P0 |
-| S2 | Zero container health checks in first-boot (30 containers) | `first-boot-containers.sh` | P0 |
-| S3 | 50+ `:latest` image tags across all scripts | `first-boot(15)`, `deploy(11)`, `tailscale(18)`, `iso(7)` | P1 |
-| S4 | No `set -e` in first-boot — silent container failures | `first-boot-containers.sh:1-9` | P1 |
-| S5 | `eval "$DB_PASSWORDS"` — code injection risk | `deploy-to-target.sh:940` | P1 |
-| S6 | No deploy locking — concurrent deploys corrupt state | `deploy-to-target.sh` | P1 |
-| S7 | No deploy rollback — failed deploy leaves broken system | `deploy-to-target.sh` | P1 |
-| S8 | sshpass usage in trust-archipelago-cert.sh | `trust-archipelago-cert.sh:23-26` | P1 |
-| S9 | MariaDB password in command line — visible in ps | `first-boot-containers.sh:285` | P1 |
-| S10 | 80+ instances of `2>/dev/null \|\| true` masking errors | `deploy-to-target.sh` | P2 |
-| S11 | No trap cleanup for temp files | Multiple scripts | P2 |
-| S12 | Unquoted variables (word splitting risk) | Multiple scripts | P2 |
-| S13 | Hardcoded IPs in 6+ scripts | `deploy-to-target.sh:26`, `deploy-tailscale.sh:26`, etc. | P2 |
-| S14 | No input validation on deploy targets | `deploy-tailscale.sh` | P2 |
-| S15 | Missing memory limits on some containers in deploy | `deploy-to-target.sh:842-880` | P2 |
-| S16 | ISO build not reproducible — dynamic image capture + :latest | `build-auto-installer-iso.sh:500-594` | P2 |
-| S17 | No disk space pre-flight in deploy | `deploy-to-target.sh` | P2 |
-| S18 | deploy-to-target.sh — 1,728 lines monolith | `deploy-to-target.sh` | P3 |
-| S19 | build-auto-installer-iso.sh — 1,850 lines monolith | `build-auto-installer-iso.sh` | P3 |
-| S20 | first-boot-containers.sh — 855 lines monolith | `first-boot-containers.sh` | P3 |
-| S21 | No shared script library — duplicated functions | `scripts/` | P3 |
-
-### Infrastructure
-
-| ID | Issue | File(s) | Severity |
-|----|-------|---------|----------|
-| I1 | Nginx: /archipelago/, /content, /dwn missing timeout+rate-limit+body-size | `nginx-archipelago.conf:116-180` | P0 |
-| I2 | Systemd: no MemoryMax, LimitNOFILE, TasksMax | `archipelago.service` | P1 |
-| I3 | Tor rotation kills old address immediately — federation downtime | `api/rpc/tor.rs:184-240` | P1 |
-
----
-
-## MONTH 1: CRASH PREVENTION (Weeks 1–4)
-
-> Fix every issue that can crash the system, hang indefinitely, or lose data.
-
-### Week 1: P0 Backend — Things That Hang or Lose Data
-
-**R1 — Health endpoint handler**
-- File: `core/archipelago/src/api/rpc/mod.rs`
-- Add handler for `"health"` method that checks: crash recovery complete, Podman socket responsive, session store loaded
-- Tests: health returns JSON status, degraded when Podman unreachable, degraded during recovery
-- Verify: `curl http://192.168.1.198/rpc/v1 -d '{"method":"health"}'` returns real status
-
-**R2 — Nostr connect timeout**
-- File: `core/archipelago/src/nostr_handshake.rs` lines 124, 161, 262, 282
-- Wrap all 4 `client.connect().await` in `tokio::time::timeout(Duration::from_secs(10), ...)`
-- Tests: connect timeout returns Err after 10s, successful connect within timeout works
-
-**R3 — Backup restore atomic rollback**
-- File: `core/archipelago/src/backup/full.rs` lines 122-149
-- Rewrite: decrypt → extract to staging dir → validate required files → atomic rename → rollback on failure
-- Tests: valid backup restores, corrupt backup fails without touching live data, partial extraction rolls back, disk space check fails early
-
-**I1 — Nginx unauthenticated endpoint protection**
-- File: `image-recipe/configs/nginx-archipelago.conf` lines 116-180
-- Add to `/archipelago/`, `/content`, `/dwn`:
- - `limit_req zone=peer burst=20 nodelay;`
- - `client_max_body_size 10m;`
- - `proxy_connect_timeout 30s; proxy_read_timeout 60s; proxy_send_timeout 30s;`
-- Tests: >10MB payload → 413, slow client → timeout, burst 30 → 429 after 20
-
-### Week 2: P0 Frontend + Scripts — Things That Break UI or Containers
-
-**F1 — WebSocket subscription race condition**
-- File: `neode-ui/src/stores/app.ts` lines 88-134
-- Fix: Return unsubscribe function from `wsClient.subscribe()`, call it before re-subscribing. Use a subscription ID to prevent duplicates.
-- Tests: rapid connectWebSocket() calls produce only one active subscription
-
-**F2 — Mesh concurrent state mutations**
-- File: `neode-ui/src/stores/mesh.ts` lines 249-324
-- Fix: Add `isSending` ref as mutex. Queue concurrent sends. `fetchMessages()` called once after all sends complete.
-- Tests: 3 concurrent sendMessage() calls → all succeed, messages list consistent
-
-**F3 — Global error handler**
-- File: `neode-ui/src/main.ts`
-- Add `app.config.errorHandler` that shows toast + logs structured error
-- Tests: thrown error in component shows toast, nested errors don't crash handler
-
-**S1 — Eliminate all `sudo podman`**
-- Files: `fix-indeedhub-containers.sh` (28), `deploy-bitcoin-knots.sh` (11), `deploy-tailscale.sh` (2+), `uptime-monitor.sh` (1), `setup-aiui-server.sh`
-- Replace every `sudo podman` with `podman` (runs as archipelago user)
-- Tests: grep for `sudo podman` across all scripts returns zero matches
-
-**S2 — Container health checks for all 30 containers**
-- File: `scripts/first-boot-containers.sh`
-- Add `--health-cmd`, `--health-interval=30s`, `--health-timeout=5s`, `--health-retries=3` to every `$DOCKER run`
-- Health commands per type:
- - Bitcoin: `bitcoin-cli -rpcuser=... getblockchaininfo || exit 1`
- - HTTP apps: `curl -sf http://localhost:{port}/ || exit 1`
- - LND: `curl -sf --insecure https://localhost:8080/v1/getinfo || exit 1`
- - Databases: `mariadb -u root -p... -e "SELECT 1" || exit 1`
-- Tests: script grep confirms every `$DOCKER run` has `--health-cmd`
-
-### Week 3: P1 Backend — Blocking I/O and Memory Leaks
-
-**R4+R5 — Rate limiter cleanup**
-- File: `core/archipelago/src/session.rs`
-- Spawn background tasks for both `EndpointRateLimiter::cleanup()` and `LoginRateLimiter` cleanup, every 5 min
-- Tests: after cleanup, stale entries removed; active entries preserved
-
-**R6 — session.rs blocking I/O (6 calls)**
-- Replace `std::fs::read_to_string` → `tokio::fs::read_to_string` at lines 77, 370, 413
-- Replace `std::fs::write` → `tokio::fs::write` at lines 128, 425
-- Replace `std::fs::create_dir_all` → `tokio::fs::create_dir_all` at line 423
-- Tests: session load/save/persist still works correctly
-
-**R7 — docker_packages.rs blocking I/O**
-- Replace `std::fs::read_to_string` → `tokio::fs::read_to_string` at lines 561, 573
-- Tests: app metadata loading works
-
-**R8 — port_allocator.rs blocking I/O**
-- Replace all 3 std::fs calls → tokio::fs at lines 59, 73, 77
-- Tests: port allocation/persistence works
-
-**R9+R10+R11 — Remaining blocking I/O**
-- `peers.rs:30`, `node_message.rs:65`, `identity.rs:50`, `identity_manager.rs:164`, `nostr_discovery.rs:55`
-- Convert all to tokio::fs
-- Tests: each module's file operations still work
-
-**R12 — electrs_status.rs sync TCP I/O**
-- Convert synchronous TCP client to async (tokio::net::TcpStream)
-- Tests: ElectrumX status query works, timeout on connection failure
-
-### Week 4: P1 Frontend — Memory Leaks and Stale State
-
-**F4 — WebSocket reconnect full state refresh**
-- File: `neode-ui/src/stores/app.ts`
-- After reconnect, call `rpcClient.call({method: 'server.get-state'})` to get fresh state before accepting patches
-- Tests: after simulated disconnect+reconnect, state matches server
-
-**F5 — Message polling timer cleanup**
-- File: `neode-ui/src/composables/useMessageToast.ts`
-- Tie polling lifecycle to auth state: stop on logout, start on login. Export cleanup function.
-- Tests: polling stops when auth false, restarts when auth true, no timer after unmount
-
-**F6 — AppLauncher message listener leak**
-- File: `neode-ui/src/stores/appLauncher.ts`
-- Ensure listener is removed when app closes (even if not via close button — e.g., route navigation)
-- Tests: navigate away from app → listener removed, new app opens clean
-
-**F7 — Audio player listener stacking**
-- File: `neode-ui/src/composables/useAudioPlayer.ts`
-- Create Audio element once, register listeners once. Track initialization flag.
-- Tests: calling play() 10 times → still only 6 listeners total (not 60)
-
-**S3 — Pin all container images (remove :latest)**
-- Files: `first-boot-containers.sh` (15), `deploy-to-target.sh` (11), `deploy-tailscale.sh` (18), `build-auto-installer-iso.sh` (7)
-- Replace every `:latest` with specific version tag
-- Create `image-versions.env` sourced by all scripts — single source of truth
-- Tests: `grep -r ':latest' scripts/ image-recipe/` returns zero matches (excluding comments)
-
----
-
-## MONTH 2: OPERATIONAL SAFETY (Weeks 5–8)
-
-> Fix everything that makes deploys dangerous, scripts unreliable, or operations opaque.
-
-### Week 5: Deploy Script Hardening
-
-**S4 — first-boot error handling**
-- Add per-section error checking: if Bitcoin fails, skip dependent containers (LND, Mempool, BTCPay)
-- Add `wait_for_container` return value checking
-- Tests: first-boot with broken Bitcoin image → Bitcoin deps skipped, independent apps still start
-
-**S5 — Replace eval with safe construct**
-- File: `deploy-to-target.sh:940`
-- Replace `eval "$DB_PASSWORDS"` with explicit variable assignment from SSH output
-- Tests: passwords parsed correctly without eval
-
-**S6 — Deploy locking**
-- File: `deploy-to-target.sh`
-- Add remote `flock` on `/var/lock/archipelago-deploy.lock`. Second deploy fails immediately with message. Stale lock (>30 min) broken automatically.
-- Tests: two parallel deploys → second fails, stale lock → broken and deploy proceeds
-
-**S7 — Deploy rollback**
-- File: `deploy-to-target.sh`
-- Before overwriting binary: `cp archipelago archipelago.bak`
-- Before overwriting frontend: `cp -r web-ui web-ui.bak`
-- If health check fails post-restart: restore from .bak, restart again
-- Tests: intentionally broken binary → deploy detects, rolls back, system healthy
-
-**S8 — Eliminate sshpass**
-- File: `trust-archipelago-cert.sh`
-- Rewrite to use SSH key only: `ssh -i ~/.ssh/archipelago-deploy`
-- Tests: script works with key auth, fails gracefully without key
-
-### Week 6: Script Quality
-
-**S9 — MariaDB password not on command line**
-- File: `first-boot-containers.sh:285`
-- Use `$DOCKER exec -i ... mariadb -uroot < /dev/stdin <<< "SET PASSWORD..."`
-- Tests: `ps aux` during execution doesn't show password
-
-**S10 — Replace silent error masking**
-- File: `deploy-to-target.sh` (80+ instances)
-- Pattern: replace `2>/dev/null || echo ""` with `|| { log_warn "..."; echo ""; }`
-- At minimum, log what failed before masking
-- Tests: failed health check produces log entry
-
-**S11 — Trap cleanup for temp files**
-- All scripts that create /tmp files: add `trap "rm -rf /tmp/deploy-$$" EXIT` at start
-- Files: deploy-to-target.sh, deploy-tailscale.sh, build-auto-installer-iso.sh
-- Tests: script interrupted mid-execution → temp files cleaned up
-
-**S12 — Quote all variables**
-- Audit and fix unquoted `$VARIABLE` in command arguments across all scripts
-- Tests: shellcheck passes on all modified scripts
-
-**S13 — Extract hardcoded IPs to config**
-- Create `scripts/deploy-config-defaults.sh` with all node IPs as named variables
-- Source from all scripts instead of hardcoding
-- Tests: changing IP in config → all scripts use new IP
-
-### Week 7: Infrastructure Hardening
-
-**I2 — Systemd resource limits**
-- File: `image-recipe/configs/archipelago.service`
-- Add: `MemoryMax=4G`, `LimitNOFILE=65535`, `TasksMax=2048`
-- Tests: `systemctl show archipelago` confirms limits applied, service starts normally
-
-**I3 — Tor rotation transition period**
-- File: `core/archipelago/src/api/rpc/tor.rs`
-- Keep old hidden service running for 24h after rotation. Both addresses active. Notify peers of new address. Schedule old deletion.
-- Tests: after rotation old address still resolves, peers receive notification, old removed after transition
-
-**S14 — Input validation on deploy targets**
-- Add regex validation for hostnames/IPs before SSH
-- Tests: invalid hostname → clear error, valid hostname → proceeds
-
-**S15 — Memory limits on all deploy containers**
-- File: `deploy-to-target.sh` lines 842-880
-- Add `--memory=$(mem_limit ...)` to all UI container builds
-- Tests: every container in deploy has `--memory` flag
-
-**S17 — Disk space pre-flight**
-- File: `deploy-to-target.sh`
-- Check target disk <85% before deploying. Abort with clear message if full.
-- Tests: deploy to 90% full disk → aborted, deploy to 50% full → succeeds
-
-### Week 8: Remaining P1 Backend
-
-**R14 — Fix .parse().unwrap() in session rate limiting**
-- File: `session.rs:665,676,688`
-- Replace `.parse().unwrap()` with `.parse().context("...")?`
-- Tests: invalid IP handling works gracefully
-
-**R15 — Fix 7 unwrap/expect in mesh/protocol.rs**
-- File: `mesh/protocol.rs:582,592,614,649,679,713,728`
-- Replace all with `?` operator + proper error types
-- Tests: protocol parsing with malformed data returns error, not panic
-
-**R27 — Add timeouts to mesh Bitcoin RPC calls**
-- File: `mesh/mod.rs:624,649,663`
-- Add `tokio::time::timeout(Duration::from_secs(10), ...)` to all Bitcoin RPC calls
-- Tests: RPC timeout returns error after 10s
-
-**R34 — Tor rotation transition**
-- (Covered by I3 above)
-
----
-
-## MONTH 3: PRODUCTION POLISH (Weeks 9–12)
-
-> Fix every remaining P2 issue — unwraps, hardcoded values, frontend quality, resilience.
-
-### Week 9: Remaining Backend Unwraps + Dead Code
-
-**R13 — main.rs .expect() → .context()**
-- Replace 2 `.expect()` calls with `.context("...")?` and proper startup error handling
-
-**R16 — identity.rs .expect() → safe handling**
-- Replace 2 `.expect()` in crypto operations with result propagation
-
-**R17+R18 — helpers unwraps**
-- Fix 10 `.unwrap()` calls in `helpers/lib.rs` and `helpers/rsync.rs`
-- Replace with `?` operator or `.context()`
-
-**R19 — js-engine unwraps**
-- Fix 2 `.unwrap()` in `js-engine/lib.rs:130,249`
-
-**R20+R21 — Dead code elimination**
-- Remove all 14 `#[allow(dead_code)]` in `mesh/mod.rs`. Either use the fields or delete them.
-- Same for `lnd.rs`, `data_manager.rs`, `dev_orchestrator.rs`
-- Tests: `cargo clippy` zero warnings, `cargo test` passes
-
-### Week 10: Hardcoded Values → Constants
-
-**R22 — Bitcoin RPC URL constant**
-- Create `const BITCOIN_RPC_URL: &str = "http://127.0.0.1:8332/";` in a shared constants module
-- Use across `bitcoin.rs`, `mesh/mod.rs`, `mesh/listener.rs`
-- Tests: all Bitcoin RPC calls still work
-
-**R23 — DWN health URL constant**
-**R24 — Update manifest URL constant**
-**R25 — DNS-over-HTTPS URLs → constants array**
-**R26 — DWN protocol URIs → constants**
-- Centralize all hardcoded URLs/URIs into `core/archipelago/src/constants.rs`
-- Tests: all modules reference constants, no hardcoded strings remain
-
-**R28 — LND proxy timeouts**
-- Audit all 68 `.send()` calls in `api/rpc/lnd.rs`. Ensure each has explicit timeout.
-- Tests: LND proxy call with unresponsive LND → timeout error, not hang
-
-**R29 — DWN health check timeout**
-- Add timeout to `dwn_sync.rs:76` health check
-
-**R30-R33 — Resolve all TODOs**
-- Either implement the TODO or remove the dead code path. Per project rules: no TODO/FIXME in commits.
-
-### Week 11: Frontend P2 Fixes
-
-**F8 — WebSocket reconnection race**
-- Add `isReconnecting` flag. Skip if already reconnecting.
-- Tests: rapid close events → only one reconnect attempt
-
-**F9 — WebSocket parse error handling**
-- Count consecutive parse errors. After 3, force reconnect.
-- Tests: 3 malformed messages → reconnect triggered; single bad message → logged only
-
-**F10 — Stale connection detection tuning**
-- Require mutual pong response within 30s. Don't close valid connections that are simply quiet.
-- Tests: quiet but healthy connection → stays open; no pong for 30s → reconnects
-
-**F11 — RPC client backoff reduction**
-- Reduce default timeout from 30s to 15s. Add jitter to backoff. Cap total retry time at 20s.
-- Tests: server outage → user sees error within 20s, not 40s
-
-**F12 — Code splitting**
-- Lazy-load all routes: `() => import('./views/Web5.vue')`
-- Add manual chunks in vite.config.ts for vendor/api
-- Tests: build produces multiple chunks, initial bundle < 200KB gzipped
-
-**F13 — DOMPurify on QR v-html**
-- Add DOMPurify.sanitize() to QR SVG before v-html rendering
-- Tests: XSS payload in QR content → sanitized
-
-### Week 12: Frontend P2 Continued + Performance
-
-**F14 — Goals computed memoization**
-- Replace O(n) alias lookup with Map. Add deep equality check.
-- Tests: goalStatuses computed runs in <1ms with 100 apps
-
-**F15 — localStorage error handling**
-- Wrap all localStorage.setItem in try/catch. Show toast on quota exceeded.
-- Tests: full localStorage → toast shown, app continues
-
-**F16 — FileBrowser auth consolidation**
-- Use cookie-only auth. Remove in-memory token.
-- Tests: login persists across page reload, logout clears cookie
-
-**F17 — CSRF token parsing robustness**
-- Add header fallback for CSRF token. Handle edge cases.
-- Tests: missing cookie → falls back to header, both missing → error
-
-**F22 — CSS backdrop-filter mobile performance**
-- Add media query: reduce blur to 8px on mobile. Remove backdrop-filter from non-visible elements.
-- Tests: mobile Lighthouse performance score > 80
-
----
-
-## MONTH 4-5: BACKEND ARCHITECTURE (Weeks 13–20)
-
-> Split every Rust god file. Target: no file > 500 lines.
-
-### Week 13–14: Split package.rs (1,795 lines)
-
-```
-api/rpc/package/
-├── mod.rs — Re-exports (~50 lines)
-├── config.rs — get_app_config(), get_app_capabilities(), needs_archy_net()
-├── lifecycle.rs — install, start, stop, restart, uninstall
-├── validation.rs — Input validation, dependency checking, image validation
-└── progress.rs — Progress streaming, install status tracking
-```
-
-Pre-split tests: test every `get_app_config()` variant, validation path, lifecycle transition
-Post-split: all RPC calls return identical responses, `cargo test` passes
-
-### Week 15–16: Split mesh/listener.rs (1,799 lines)
-
-```
-mesh/listener/
-├── mod.rs — Re-exports + spawn_mesh_listener()
-├── session.rs — run_mesh_session() loop
-├── frames.rs — handle_frame() dispatcher
-├── identity.rs — handle_identity_received(), handle_typed_message()
-├── sync.rs — sync_queued_messages(), store_typed_message()
-└── bitcoin.rs — Bitcoin relay operations, RPC calls
-```
-
-### Week 17–18: Split rpc/mod.rs (1,092 lines) + lnd.rs (1,068 lines)
-
-**rpc/mod.rs** → `dispatcher.rs` (method routing), `middleware.rs` (CSRF/session/rate-limit), `response.rs` (response building)
-
-**lnd.rs** → `lnd/wallet.rs`, `lnd/channels.rs`, `lnd/info.rs`, `lnd/payments.rs`
-
-### Week 19–20: Split monitoring (993), handler (911), mesh (865)
-
-Split each into sub-modules. Target: no file > 500 lines.
-All pre-split tests, all post-split verification.
-
----
-
-## MONTH 6-8: FRONTEND ARCHITECTURE (Weeks 21–32)
-
-> Split every Vue god component. Target: no component > 500 lines.
-
-### Week 21–22: Split Web5.vue (3,940 lines → 8 sub-views)
-
-```
-views/web5/
-├── Web5.vue — Router shell (~150 lines)
-├── Web5Identity.vue — DID management
-├── Web5Wallet.vue — Wallet operations
-├── Web5Nostr.vue — Nostr relays/profiles
-├── Web5Credentials.vue — Verifiable Credentials
-├── Web5Peers.vue — P2P federation nodes
-├── Web5Storage.vue — DWN storage/explorer
-├── Web5Goals.vue — Goals/voting
-└── Web5Marketplace.vue — Decentralized marketplace
-```
-
-Add nested routes. Component tests for each section. All sections render identically.
-
-### Week 23–24: Split Mesh.vue (2,106) + Dashboard.vue (1,819)
-
-**Mesh.vue** → `MeshRadio.vue`, `MeshChat.vue`, `MeshNetwork.vue`, `MeshFederation.vue`
-**Dashboard.vue** → `DashboardHome.vue`, `DashboardApps.vue`, `DashboardSystem.vue`
-
-### Week 25–26: Split Settings.vue (1,792) + Server.vue (1,132)
-
-**Settings.vue** → `SettingsAccount.vue`, `SettingsSystem.vue`, `SettingsNetwork.vue`, `SettingsAppearance.vue`
-**Server.vue** → `ServerOverview.vue`, `ServerContainers.vue`, `ServerLogs.vue`
-
-### Week 27–28: Split Marketplace.vue (1,293) + AppDetails.vue (1,036) + Home.vue (1,059)
-
-Each into 3-4 focused sub-components.
-
-### Week 29–30: Decompose useAppStore (324 lines, 16 methods)
-
-```
-stores/
-├── app.ts — Thin re-export for backward compat (~50 lines)
-├── auth.ts — Login, logout, session, password, TOTP
-├── server.ts — Server info, system stats, reboot/shutdown
-├── realtime.ts — WebSocket connection, subscriptions, heartbeat
-└── packages.ts — Package install/uninstall, marketplace data
-```
-
-Tests: every existing import of `useAppStore` still works. State transitions identical.
-
-### Week 31–32: Remaining frontend P3 issues
-
-**F18** — aiPermissions runtime validation
-**F19** — Track AppSession timeout
-**F20** — Dashboard aria-current
-**F21** — Debounce search + memoize
-**F23** — Branded types for DID operations
-**F24** — Fix checkInterval leak
-
----
-
-## MONTH 9-10: SCRIPT ARCHITECTURE + ISO (Weeks 33–40)
-
-> Split every monolithic script. Target: no script > 400 lines.
-
-### Week 33–34: Create shared script library
-
-```
-scripts/lib/
-├── common.sh — Colors, logging, error handling, SSH helpers
-├── health.sh — Health check polling, container status
-├── deploy-utils.sh — Rsync, file sync, backup/restore
-├── container.sh — Podman helpers, image management, mem_limit()
-└── network.sh — IP validation, port checking
-```
-
-Tests: each library function tested in `scripts/tests/`
-
-### Week 35–36: Split deploy-to-target.sh (1,728 lines)
-
-```
-scripts/
-├── deploy-to-target.sh — Orchestrator + arg parsing (~300 lines)
-├── deploy/
-│ ├── frontend.sh — Build + sync frontend
-│ ├── backend.sh — Build + sync binary
-│ ├── configs.sh — Sync nginx, systemd, scripts
-│ ├── containers.sh — Container creation/update
-│ ├── verify.sh — Post-deploy health checks
-│ └── rollback.sh — Rollback on failure
-```
-
-### Week 37–38: Split ISO build (1,850 lines) + first-boot (855 lines)
-
-**build-auto-installer-iso.sh** → `build/capture-images.sh`, `build/create-rootfs.sh`, `build/install-packages.sh`, `build/bundle-configs.sh`, `build/package-iso.sh`
-
-**first-boot-containers.sh** → `first-boot/databases.sh`, `first-boot/bitcoin.sh`, `first-boot/lightning.sh`, `first-boot/apps.sh`, `first-boot/networking.sh`
-
-### Week 39–40: ISO Reproducibility + Integration Tests
-
-**S16 — Make ISO builds reproducible**
-- Create `image-versions.env` with pinned digests for every container image
-- ISO build sources this file, never pulls `:latest`
-- Build manifest records exactly what shipped
-- Tests: two consecutive ISO builds produce identical image sets
-
-**E2E smoke test script**
-```bash
-# scripts/smoke-test.sh — Run against .198
-# 1. curl /health → OK
-# 2. Login → get session
-# 3. Get server info → valid JSON
-# 4. List containers → all healthy
-# 5. Check every /app/* proxy → responds
-# 6. Check Tor hidden service → resolves
-# 7. Check WebSocket upgrade → 101
-# Exit 0 only if all pass
-```
-
----
-
-## MONTH 11: INTEGRATION TESTS (Weeks 41–44)
-
-> Comprehensive test suites that prove everything works.
-
-### Week 41–42: Backend Integration Tests
-
-```
-core/archipelago/tests/
-├── test_auth_flow.rs — Login → session → CSRF → auth request → logout
-├── test_container_lifecycle.rs — Install → start → health → stop → uninstall
-├── test_federation.rs — Generate invite → join → sync → verify
-├── test_rpc_validation.rs — Every endpoint with invalid input → proper error
-├── test_session_persist.rs — Create session → restart → session survives
-├── test_rate_limiting.rs — Flood → 429 → wait → allowed
-├── test_backup_restore.rs — Create → verify → restore → validate
-├── test_health_endpoint.rs — Healthy → degraded → recovery
-```
-
-Target: 25+ backend integration tests passing
-
-### Week 43–44: Frontend Integration Tests
-
-```
-neode-ui/src/__tests__/integration/
-├── auth-flow.spec.ts — Login → dashboard → timeout → redirect
-├── app-lifecycle.spec.ts — Marketplace → install → progress → launch → uninstall
-├── websocket.spec.ts — Connect → update → disconnect → reconnect → state consistent
-├── settings-flow.spec.ts — Change password → re-login → 2FA setup → verify
-├── spotlight.spec.ts — Open → search → navigate → close
-├── mesh-chat.spec.ts — Connect → send → receive → disconnect
-├── error-handling.spec.ts — Network error → toast → retry → success
-├── code-splitting.spec.ts — Route navigation → chunks loaded lazily
-```
-
-Target: 20+ frontend integration tests passing
-
----
-
-## MONTH 12: TYPE SYNC + CI/CD PLAN (Weeks 45–48)
-
-### Week 45–46: Rust↔TypeScript Type Sync
-
-**Approach**: `ts-rs` crate to auto-generate TypeScript types from Rust structs
-
-1. Add `ts-rs` to `core/models/Cargo.toml`
-2. Add `#[derive(TS)]` to all API request/response types
-3. Build script generates `neode-ui/src/types/generated.ts`
-4. Replace manual types in `types/api.ts` with imports from generated file
-5. Verification: regenerate → diff → must be zero (types committed)
-
-Tests: frontend type-check passes with generated types, manual api.ts reduced to non-API types
-
-### Week 47–48: CI/CD Planning (Document Only — Execute Later)
-
-> This section is the PLAN for CI/CD. Do not execute during this phase. Document everything needed so it can be implemented in a future sprint.
-
-**CI Pipeline Design** (`.github/workflows/ci.yml`):
-
-```yaml
-# Triggers: push to main, all PRs
-# Jobs:
-# rust-checks (Linux runner):
-# - cargo clippy --all-targets --all-features (zero warnings gate)
-# - cargo fmt --all -- --check (formatting gate)
-# - cargo test --all-features (all tests gate)
-#
-# frontend-checks (Node 20):
-# - npm run type-check (TypeScript strictness gate)
-# - npm run lint (ESLint gate)
-# - npm test (Vitest suite gate)
-#
-# integration (Linux runner, optional):
-# - scripts/smoke-test.sh against staging
-#
-# Merge policy: all checks must pass before merge
-# Branch protection: require PR, require checks, no force push to main
-```
-
-**Release Pipeline Design** (`.github/workflows/release.yml`):
-```yaml
-# Triggers: tag push (v*)
-# Jobs:
-# build-linux-binary:
-# - Cross-compile Rust for x86_64 + ARM64
-# build-frontend:
-# - npm run build
-# build-iso:
-# - SSH to build server, run ISO build
-# - Upload ISO as release asset
-# smoke-test:
-# - Boot ISO in QEMU
-# - Run smoke-test.sh
-# - Gate release on pass
-```
-
-**Pre-requisites to implement**:
-- [ ] GitHub Actions runner with Rust toolchain + cross-compilation
-- [ ] Node.js 20 runner for frontend
-- [ ] SSH key for build server accessible from CI
-- [ ] Branch protection rules configured
-- [ ] Image digest manifest for reproducible ISO builds
-- [ ] QEMU-based ISO verification script
-
-**Estimated implementation time**: 2 weeks when ready to execute
-
----
-
-## VERIFICATION PROTOCOL (Every Week)
-
-1. `cargo clippy --all-targets --all-features` — zero warnings
-2. `cargo fmt --all`
-3. `cargo test --all-features` — all pass
-4. `cd neode-ui && npm run type-check` — zero errors
-5. `cd neode-ui && npm test` — all pass
-6. `./scripts/deploy-to-target.sh --target 192.168.1.198` — **ONLY .198**
-7. `curl http://192.168.1.198/health` — returns OK with service status
-8. Navigate all affected views in browser — identical behavior
-9. Atomic commit: `refactor: ` or `fix: `
-
----
-
-## EXIT CRITERIA (Month 12 Complete)
-
-### Reliability (Zero Tolerance)
-- [ ] Health endpoint returns real service status
-- [ ] All async operations have bounded timeouts
-- [ ] Zero blocking I/O in async context (no std::fs in async functions)
-- [ ] Zero .unwrap()/.expect() in production code
-- [ ] All rate limiters have cleanup tasks
-- [ ] Backup restore uses staging + atomic swap + rollback
-- [ ] All 30 containers have health checks + memory limits
-- [ ] All container images pinned to specific versions
-- [ ] Nginx unauthenticated endpoints protected (timeout + rate limit + body size)
-- [ ] Systemd service has resource limits
-- [ ] Tor rotation preserves old address during transition
-- [ ] Deploy has locking + disk check + rollback
-- [ ] Zero `sudo podman` in any script
-- [ ] Zero `:latest` image tags anywhere
-- [ ] Zero silent error masking without logging
-
-### Frontend (Zero Tolerance)
-- [ ] Global error handler catches and displays all errors
-- [ ] WebSocket: single subscription, reconnect refreshes state, bounded retries
-- [ ] All timers/listeners cleaned up on unmount
-- [ ] Code splitting: initial bundle < 200KB gzipped
-- [ ] v-html always uses DOMPurify
-- [ ] All localStorage operations wrapped in try/catch
-
-### Architecture (Target: File Size Limits)
-- [ ] No Rust file > 500 lines (excluding generated code)
-- [ ] No Vue component > 500 lines
-- [ ] No shell script > 400 lines
-- [ ] No Pinia store has more than 1 responsibility
-- [ ] All hardcoded URLs/ports extracted to constants
-- [ ] Shared script library eliminates duplication
-- [ ] TypeScript types auto-generated from Rust structs
-
-### Testing
-- [ ] 25+ backend integration tests passing
-- [ ] 20+ frontend integration tests passing
-- [ ] E2E smoke test script passes on .198
-- [ ] ISO builds are reproducible (pinned digests)
-
-### CI/CD (Planned, Not Executed)
-- [ ] CI pipeline design documented
-- [ ] Release pipeline design documented
-- [ ] Pre-requisites list complete
-- [ ] Ready for 2-week implementation sprint
-
-### Zero Behavior Changes
-Every feature works identically. Every existing test passes. Every user flow unchanged.
diff --git a/.claude/plans/polished-napping-squid.md b/.claude/plans/polished-napping-squid.md
deleted file mode 100644
index 85d1a13d..00000000
--- a/.claude/plans/polished-napping-squid.md
+++ /dev/null
@@ -1,108 +0,0 @@
-# Meshcore Mesh Networking — Phase 1 Implementation Plan
-
-## Context
-
-Adding mesh networking to Archipelago using Heltec V3 devices running Meshcore firmware (Companion USB). Two nodes (.228 and .198) will exchange encrypted identity and text messages over LoRa radio with no internet required. The existing `mesh.rs` wraps the Meshtastic CLI — this replaces it with a native Meshcore serial protocol driver.
-
-## Architecture
-
-Convert `mesh.rs` into `mesh/` module directory:
-
-```
-core/archipelago/src/mesh/
-├── mod.rs — Public API, MeshService, config (migrated from mesh.rs)
-├── types.rs — MeshPeer, MeshMessage, MeshStatus, DeviceType
-├── protocol.rs — Meshcore binary frame protocol (encode/decode/commands)
-├── serial.rs — MeshcoreDevice: async serial driver (serial2-tokio)
-├── crypto.rs — X25519 ECDH + ChaCha20-Poly1305 per-message encryption
-└── listener.rs — Background tokio task: serial reader + message dispatcher
-```
-
-Frontend:
-```
-neode-ui/src/stores/mesh.ts — Pinia store
-neode-ui/src/views/Mesh.vue — Mesh status, peers, messaging UI
-```
-
-## Dependency
-
-Add to `core/archipelago/Cargo.toml`:
-```toml
-serial2-tokio = "0.1"
-```
-
-All crypto deps already present (chacha20poly1305, ed25519-dalek, curve25519-dalek).
-
-## Meshcore Protocol Summary
-
-- **Frame format**: `>` + 2-byte LE length + data (outbound), `<` + 2-byte LE length + data (inbound)
-- **Baud**: 115200, 8N1
-- **Max message**: 160 bytes
-- **Init sequence**: CMD_DEVICE_QUERY (0x16) -> CMD_APP_START (0x01) -> CMD_SET_DEVICE_TIME (0x06)
-- **Key commands**: SEND_TXT_MSG (0x02), SEND_CHANNEL_TXT_MSG (0x03), GET_CONTACTS (0x04), SYNC_NEXT_MESSAGE (0x0A), SEND_SELF_ADVERT (0x07)
-- **Push events** (async, >=0x80): NEW_CONTACT (0x8A), ACK (0x82), MESSAGES_WAITING (0x83)
-
-## Encryption Design
-
-Reuses existing identity.rs X25519 key agreement:
-1. Nodes broadcast identity on mesh channel: `ARCHY:1:{did}:{ed25519_pubkey}:{x25519_pubkey}`
-2. Receiving node derives shared secret: X25519(our_secret, their_x25519_pub)
-3. All DMs encrypted: ChaCha20-Poly1305 with random 12-byte nonce
-4. Wire format: [nonce 12B] + [ciphertext] + [tag 16B] — fits in 160B limit for ~130B plaintext
-
-## RPC Endpoints
-
-| Method | Action |
-|--------|--------|
-| `mesh.status` | Device + mesh status (updated) |
-| `mesh.peers` | **NEW** — list discovered mesh peers |
-| `mesh.messages` | **NEW** — get message history (last 100) |
-| `mesh.send` | **NEW** — send encrypted message to peer |
-| `mesh.broadcast` | Broadcast identity (updated for Meshcore) |
-| `mesh.configure` | Update config (updated) |
-
-## Implementation Steps
-
-1. **Create mesh/ module, migrate existing code** — types.rs + mod.rs from mesh.rs
-2. **protocol.rs** — Binary frame encode/decode, command builders, response parsers + unit tests
-3. **crypto.rs** — X25519 ECDH + ChaCha20-Poly1305 encrypt/decrypt + unit tests
-4. **serial.rs** — MeshcoreDevice with open/init/send/recv + device auto-detection
-5. **listener.rs** — Background task: serial reader, peer cache, message store, reconnect
-6. **mod.rs MeshService** — Wraps listener + config, start/stop lifecycle
-7. **Update RPC handlers** — New endpoints, wire MeshService into RpcHandler
-8. **Update RPC dispatch** — Add routes in mod.rs ~line 622
-9. **Frontend store + view** — mesh.ts Pinia store, Mesh.vue with glass-card UI, router + nav
-10. **Deploy + test** — Deploy to .228 and .198, plug in Heltec V3s, test end-to-end
-
-## Key Files to Modify
-
-- `core/archipelago/src/mesh.rs` -> delete, replace with `mesh/` directory
-- `core/archipelago/src/api/rpc/mesh.rs` — update handlers
-- `core/archipelago/src/api/rpc/mod.rs` — add routes (~line 622)
-- `core/archipelago/Cargo.toml` — add serial2-tokio
-- `neode-ui/src/router/index.ts` — add /dashboard/mesh route
-- `neode-ui/src/views/Dashboard.vue` — add Mesh nav item
-
-## Reusable Existing Code
-
-- `identity.rs` lines 140-152: Ed25519 -> X25519 conversion (CompressedEdwardsY -> Montgomery)
-- `identity.rs` `pubkey_bytes_from_did_key()`: extract raw pubkey from DID string
-- `node_message.rs` pattern: IncomingMessage store with max 100 circular buffer
-- `mesh.rs` `MeshConfig` + `load_config`/`save_config`: migrate directly into mod.rs
-- `mesh.rs` `detect_meshtastic_devices()`: keep as fallback, add Meshcore probe-based detection
-
-## Prerequisites
-
-- Flash both Heltec V3 with Meshcore **Companion USB** role
-- Add `archipelago` user to `dialout` group: `usermod -aG dialout archipelago`
-- Connect Heltec V3 to USB on .228 and .198
-
-## Verification
-
-1. `cargo clippy --all-targets` passes with zero warnings
-2. Unit tests pass: protocol encode/decode, crypto encrypt/decrypt roundtrip
-3. Device detected on /dev/ttyUSB0 or /dev/ttyACM0
-4. Init handshake completes (visible in tracing logs)
-5. Identity broadcast from .228, received on .198
-6. Encrypted DM sent .228 -> .198, decrypted and visible in UI
-7. Mesh.vue shows device status, peer list, message history
diff --git a/.claude/plans/prancy-scribbling-pnueli.md b/.claude/plans/prancy-scribbling-pnueli.md
deleted file mode 100644
index 402d266b..00000000
--- a/.claude/plans/prancy-scribbling-pnueli.md
+++ /dev/null
@@ -1,80 +0,0 @@
-# Plan: Demo Seeding, Dev Environment Fix, and Developer Onboarding
-
-## Context
-After the repo cleanup (docs/scripts archived to `~/Projects/archy-archive/`), several dev scripts reference deleted files. Additionally, the demo needs better seeding for Portainer showcase, ThunderHub + Fedimint need to be visible, and a new developer needs docs to onboard.
-
-## Changes
-
-### 1. Fix broken dev scripts
-
-**`neode-ui/start-dev.sh`** — Remove lines 72-110 (Docker Desktop check + `start-docker-apps.sh` call). Replace with a one-liner noting mock backend handles simulation.
-
-**`neode-ui/stop-dev.sh`** — Remove lines 66-74 (Docker container stop block calling `stop-docker-apps.sh`).
-
-**`neode-ui/package.json`** — Remove the `prebuild` script (line 22) that references archived `../../loop-start.mp3`. File already exists at `public/assets/audio/`.
-
-**`scripts/dev-start.sh`** — Fix option 2 (Full Stack) lines 67-84 that reference `start-docker-apps.sh`. Guard with a skip message instead of failing.
-
-### 2. Add ThunderHub (Lightning management UI)
-
-**Files**: mock-backend.js, Marketplace.vue, appLauncher.ts, new icon SVG
-
-- Port: **3010** (3000 taken by Grafana)
-- Docker image: `apotdevin/thunderhub:v0.13.31`
-- Add to `portMappings`, `marketplaceMetadata`, `staticDevApps`, `marketplace.get()` in mock-backend.js
-- Add to `getCuratedAppList()` in Marketplace.vue (after LND entry)
-- Add to `recommended` tier in `getAppTier()`
-- Add `'3010': 'thunderhub'` to PORT_TO_APP_ID in appLauncher.ts
-- Create `neode-ui/public/assets/img/app-icons/thunderhub.svg` (Bitcoin-orange lightning bolt icon)
-
-### 3. Improve Fedimint in demo
-
-**mock-backend.js**:
-- Add `fedimint` to `staticDevApps` (pre-installed, running, port 8175)
-- Update `marketplace.get()` version from `0.4.3` → `0.10.0`
-- Fix `portMappings.fedimint` from 8174 → 8175 (Guardian UI port)
-
-### 4. Add realistic notifications
-
-**mock-backend.js** — Replace empty `node.notifications` with 5 realistic entries: Bitcoin sync, LND channel opened, disk warning, system update, Fedimint guardian connected.
-
-### 5. Rewrite README for developer onboarding
-
-**`neode-ui/README.md`** — Full rewrite:
-- Quick start (npm install, npm start, localhost:8100, password123)
-- Architecture overview
-- Dev modes (setup/onboarding/existing/boot)
-- Mock backend capabilities (8 static apps, 30+ marketplace, WebSocket, FileBrowser API, Claude proxy)
-- Demo deployment (docker-compose.demo.yml, Portainer, ANTHROPIC_API_KEY)
-- Design system (glassmorphism classes, tokens)
-- Build commands
-- Remove Angular references and outdated sections
-
-**`neode-ui/DEV-SCRIPTS.md`** — Update "Available Test Apps" section to list the 8 actual static apps, remove Docker apps references.
-
-### 6. Verify Docker demo build
-
-Confirm `docker-compose.demo.yml` paths still valid after cleanup:
-- `demo/aiui/` exists (for Dockerfile.web COPY)
-- `neode-ui/docker/nginx-demo.conf` exists
-- `neode-ui/docker/docker-entrypoint.sh` exists
-
-## Files to modify
-1. `neode-ui/start-dev.sh`
-2. `neode-ui/stop-dev.sh`
-3. `neode-ui/package.json`
-4. `scripts/dev-start.sh`
-5. `neode-ui/mock-backend.js`
-6. `neode-ui/src/views/Marketplace.vue`
-7. `neode-ui/src/stores/appLauncher.ts`
-8. `neode-ui/public/assets/img/app-icons/thunderhub.svg` (new)
-9. `neode-ui/README.md`
-10. `neode-ui/DEV-SCRIPTS.md`
-
-## Verification
-1. `cd neode-ui && npm start` — should start cleanly, no errors about missing scripts
-2. Visit localhost:8100 → login → Dashboard shows 8 apps (bitcoin, lnd, electrs, mempool, lorabell, filebrowser, thunderhub, fedimint)
-3. Marketplace shows ThunderHub in Bitcoin category
-4. Notifications bell shows 3 unread
-5. `npm stop` — clean shutdown, no errors
-6. `docker compose -f docker-compose.demo.yml build` — builds successfully
diff --git a/.claude/plans/reflective-meandering-castle.md b/.claude/plans/reflective-meandering-castle.md
deleted file mode 100644
index 0790115e..00000000
--- a/.claude/plans/reflective-meandering-castle.md
+++ /dev/null
@@ -1,145 +0,0 @@
-# Expand AIUI Node Capabilities
-
-## Context
-AIUI currently sees basic app status and file names but can't read files, check Bitcoin/LND details, or view app logs. Expanding these 4 capabilities makes AIUI a truly useful node assistant.
-
----
-
-## 1. File Reading (frontend-only) [DONE]
-
-### `neode-ui/src/api/filebrowser-client.ts`
-Add `readFileAsText(path, maxBytes = 102400)` method:
-- Fetch from existing `/app/filebrowser/api/raw{path}?auth={token}` endpoint
-- Limit response to 100KB (truncate with note)
-- Only allow text-like extensions: `.txt`, `.md`, `.json`, `.csv`, `.log`, `.conf`, `.yaml`, `.yml`, `.toml`, `.xml`, `.html`, `.css`, `.js`, `.ts`, `.py`, `.sh`
-- Return `{ content: string, truncated: boolean, size: number }`
-
-### `neode-ui/src/types/aiui-protocol.ts`
-Add `'read-file'` and `'tail-logs'` to `AIActionType` union.
-
-### `neode-ui/src/services/contextBroker.ts`
-Add `read-file` action handler:
-- Check `files` permission is enabled
-- Validate path param exists, validate extension
-- Call `fileBrowserClient.readFileAsText(path)`
-- Return content in action response
-
-### `AIUI/packages/app/src/composables/useArchy.ts`
-- Add `readFile(path: string)` helper that calls `archyBridge.requestAction('read-file', { path })`
-- Update `buildArchyContext()` files section: mention "You can read file contents by requesting the read-file action with a file path."
-
----
-
-## 2. App Log Viewing (frontend-only) [DONE]
-
-### `neode-ui/src/services/contextBroker.ts`
-Add `tail-logs` action handler:
-- Check `apps` permission is enabled
-- Params: `{ appId: string, lines?: string }` (default 50, max 200)
-- Call existing `rpcClient.call({ method: 'container-logs', params: { app_id, lines } })`
-- Return log lines in action response
-
-### `AIUI/packages/app/src/composables/useArchy.ts`
-- Add `tailLogs(appId: string, lines?: number)` helper
-- Update `buildArchyContext()` apps section: "You can view recent app logs by requesting the tail-logs action with an appId."
-
----
-
-## 3. Bitcoin Deep Data (backend + frontend) [DONE]
-
-### `core/archipelago/src/api/rpc/mod.rs`
-Add routing: `"bitcoin.getinfo" => self.handle_bitcoin_getinfo().await`
-
-### New: `core/archipelago/src/api/rpc/bitcoin.rs`
-Add `handle_bitcoin_getinfo()`:
-- Use `reqwest` to POST to `http://127.0.0.1:8332` with Basic Auth `archipelago:archipelago123`
-- Call `getblockchaininfo` JSON-RPC method
-- Call `getmempoolinfo` JSON-RPC method
-- Return sanitized JSON:
-```json
-{
- "block_height": 800000,
- "sync_progress": 0.9999,
- "chain": "main",
- "difficulty": 72006146,
- "mempool_size": 45000000,
- "mempool_tx_count": 12500,
- "verification_progress": 0.9999
-}
-```
-- Handle connection errors gracefully (Bitcoin Core might be syncing or down)
-
-### `neode-ui/src/services/contextBroker.ts`
-Enrich `bitcoin` category sanitizer:
-- Call `rpcClient.call({ method: 'bitcoin.getinfo' })`
-- Merge with existing container status data
-- Return block height, sync %, chain, mempool stats
-
-### `AIUI/packages/app/src/composables/useArchy.ts`
-- Add `bitcoinInfo` ref with block height, sync %, etc.
-- Update `buildArchyContext()`: "**Bitcoin:** Block 800,000 (99.99% synced), mainnet, mempool: 12,500 txs"
-
----
-
-## 4. LND Deep Data (backend + frontend) [DONE]
-
-### `core/archipelago/src/api/rpc/mod.rs`
-Add routing: `"lnd.getinfo" => self.handle_lnd_getinfo().await`
-
-### New: `core/archipelago/src/api/rpc/lnd.rs`
-Add `handle_lnd_getinfo()`:
-- Read admin macaroon from `/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon`
-- Use `reqwest` to GET `https://127.0.0.1:8080/v1/getinfo` with `Grpc-Metadata-macaroon` header (hex-encoded)
-- GET `https://127.0.0.1:8080/v1/balance/channels` for channel balance
-- GET `https://127.0.0.1:8080/v1/balance/blockchain` for on-chain balance
-- Accept self-signed cert (`reqwest::Client::builder().danger_accept_invalid_certs(true)`)
-- Return sanitized JSON:
-```json
-{
- "alias": "my-node",
- "num_active_channels": 5,
- "num_peers": 8,
- "synced_to_chain": true,
- "block_height": 800000,
- "balance_sats": 1500000,
- "channel_balance_sats": 3000000,
- "pending_open_balance": 0
-}
-```
-- **Never expose**: private keys, seed, macaroon, node pubkey (optional — could include for identification)
-- Handle errors: LND might be locked, syncing, or not installed
-
-### `neode-ui/src/services/contextBroker.ts`
-Enrich `wallet` category:
-- Call `rpcClient.call({ method: 'lnd.getinfo' })`
-- Return alias, channels, balances, sync status
-
-### `AIUI/packages/app/src/composables/useArchy.ts`
-- Add `lndInfo` ref
-- Update `buildArchyContext()`: "**Lightning:** 5 channels, 3M sats in channels, 1.5M on-chain, synced"
-
----
-
-## File Summary
-
-| File | Change |
-|------|--------|
-| `neode-ui/src/api/filebrowser-client.ts` | Add `readFileAsText()` |
-| `neode-ui/src/types/aiui-protocol.ts` | Add `read-file`, `tail-logs` action types |
-| `neode-ui/src/services/contextBroker.ts` | Add 2 action handlers + enrich bitcoin/wallet categories |
-| `neode-ui/src/stores/aiPermissions.ts` | Update category descriptions |
-| `core/archipelago/src/api/rpc/mod.rs` | Add 2 route entries |
-| `core/archipelago/src/api/rpc/bitcoin.rs` | New: Bitcoin Core RPC proxy |
-| `core/archipelago/src/api/rpc/lnd.rs` | New: LND REST proxy |
-| `AIUI/packages/app/src/composables/useArchy.ts` | Add helpers + enrich buildArchyContext() |
-
-## Verification
-1. `cd neode-ui && npm run build` — frontend builds
-2. `./scripts/deploy-to-target.sh --live` — deploys + builds Rust backend on server
-3. Test in AIUI chat:
- - "What files do I have?" → sees file list
- - "Read my config.txt" → gets file content
- - "How's my Bitcoin node?" → block height, sync %, mempool
- - "What's my Lightning balance?" → channel count, sats balance
- - "Why is Mempool not working?" → views recent logs
- - "Show me the last 50 lines of Bitcoin logs" → log output
diff --git a/.claude/plans/rosy-floating-lightning.md b/.claude/plans/rosy-floating-lightning.md
deleted file mode 100644
index 93d50e34..00000000
--- a/.claude/plans/rosy-floating-lightning.md
+++ /dev/null
@@ -1,174 +0,0 @@
-# Plan: Optimize Claude Code Instructions for Maximum Coding Performance
-
-## Context
-
-### The Problem
-Research across Anthropic's official docs, engineering blog, GitHub issues, and academic papers converges on one finding: **instruction overload degrades Claude's coding performance**. The more tokens consumed by rules/instructions, the less attention and context remain for actual code generation.
-
-Key evidence:
-- Anthropic official docs: *"Bloated CLAUDE.md files cause Claude to ignore your actual instructions!"*
-- Boris Cherny (Claude Code creator) uses ~100 lines / ~2,500 tokens for his CLAUDE.md
-- Research (Jaroslawicz et al., 2025): instruction compliance decreases linearly as count increases; frontier models plateau at ~150-200 instructions; Claude Code's system prompt already uses ~50
-- "Lost in the Middle" (Stanford, 2024): LLMs exhibit U-shaped attention — middle content gets least attention
-- Anthropic engineering blog: *"Find the smallest possible set of high-signal tokens that maximize the likelihood of some desired outcome"*
-- Aggressive language (BANNED, NEVER, CRITICAL, Non-Negotiable) overtriggers on Claude 4.5/4.6 — Anthropic explicitly recommends dialing it back
-- Multiple GitHub issues (15443, 28158, 16073, 34197) document systematic instruction ignoring with large CLAUDE.md files
-
-### Current State (Archy Project)
-
-**Always-loaded instruction payload:**
-| Source | Lines | Chars | Est. Tokens |
-|--------|-------|-------|-------------|
-| Global CLAUDE.md | 97 | 5,624 | ~1,400 |
-| Project CLAUDE.md | 130 | 5,270 | ~1,300 |
-| 5 rules files | 119 | 5,123 | ~1,280 |
-| MEMORY.md index | 16 | 1,099 | ~275 |
-| 33 skill descriptions (system) | ~300 | ~13,200 | ~3,300 |
-| **Total always-loaded** | **~662** | **~30,316** | **~7,555** |
-
-Plus ~10 memory files (~290 lines, ~19K chars) loaded on relevance, and 33 skills totaling ~122K chars loaded on demand.
-
-### Key Problems Identified
-
-1. **Global CLAUDE.md is ~60% things Claude already knows** — "Comment WHY not WHAT," "Functions under 50 lines," "Zero compiler warnings" are standard practices Claude follows without being told
-2. **Anti-Hallucination section (28 lines) restates built-in behavior** — package verification is in Claude's training
-3. **Redundancy across files** — security rules appear in global CLAUDE.md + crypto.md + api.md + project CLAUDE.md (4x)
-4. **Aggressive language throughout** — "BANNED," "Non-Negotiable," "MANDATORY," "NEVER" — Anthropic says this causes overtriggering on current models
-5. **Project CLAUDE.md duplicates rules files** — Frontend section repeats frontend.md, Security section repeats crypto.md + api.md
-6. **Philosophy section is ~30 lines that don't affect code generation** — Claude won't suggest altcoins or proprietary deps regardless
-
-### What We Preserve (per user request)
-- All deploy commands, build commands, SSH access, CI/CD info
-- All infrastructure keys/addresses/IPs
-- Security and quality architecture rules that prevent real mistakes
-- All memory files and feedback (operational learnings)
-- All skills (they already use progressive disclosure correctly)
-
----
-
-## The Plan
-
-### Principle: Every line must prevent a specific mistake Claude would otherwise make
-
-If Claude would do the right thing without the instruction -> delete it.
-If Claude does the wrong thing even with the instruction -> make it a hook.
-If it only matters for specific files -> scope it with globs in rules/.
-
-### Step 1: Rewrite Global CLAUDE.md (~97 -> ~35 lines)
-
-**Remove (Claude already knows these):**
-- "Comment WHY not WHAT" — standard practice
-- "Functions under 50 lines, single responsibility" — standard practice
-- "Zero compiler warnings, zero linter errors" — standard practice
-- "Remove dead code entirely" — standard practice
-- "Deploy and verify changes" — project-specific, belongs in project CLAUDE.md
-- Entire "Core Principles" enumeration (5 items) — the one-line philosophy header covers it
-- "Encryption first" details — covered by crypto.md rules file
-- Most of "Anti-Hallucination" section (28 lines) — Claude already verifies packages; keep only "cross-reference existing deps" which is non-obvious
-- "Code Sourcing: What to avoid" items 3-4 — too specific, rarely triggered
-
-**Keep (prevents real mistakes):**
-- Bitcoin-only stance (1 line) — prevents suggesting altcoin libs
-- Open source preference (1 line)
-- Code sourcing core rules (no vibe-code repos, no vendoring without approval)
-- Dependency selection order (rustls > openssl, etc.) — non-obvious preferences
-- Security standards not in rules files (never commit secrets, pin versions)
-- Project ecosystem listing — useful cross-project context
-- Atomic commit format
-
-**Rewrite style:** Calm, direct. No MANDATORY, no bold on every line.
-
-### Step 2: Rewrite Project CLAUDE.md (~130 -> ~75 lines)
-
-**Remove (duplicated in scoped rules files):**
-- Frontend section (lines 70-77) — exact duplicate of .claude/rules/frontend.md
-- Security section (lines 87-94) — duplicates crypto.md + api.md + containers.md
-- "See .claude/rules/ for detailed..." pointer — Claude loads them automatically
-
-**Remove (Claude already knows):**
-- "No unwrap()/expect() — use ? with .context()" — standard Rust practice
-- "tracing for logging, never println!" — standard practice
-- "tokio runtime" — obvious from the codebase
-
-**Keep and tighten (all non-obvious, prevents real mistakes):**
-- Overview + Stack (essential context)
-- Beta freeze status (active project constraint)
-- Quick Reference commands (frequently used, non-guessable)
-- Infrastructure table (IPs, keys, remotes — user explicitly wants these)
-- Architecture diagram (essential mental model)
-- Critical Rules (5 items — all non-obvious)
-- Backend: only non-obvious rules (bind 127.0.0.1, path validation, timeouts)
-- ISO Build commands (operational knowledge)
-- App Integration Checklist (prevents real mistakes)
-- Git conventions (one line)
-
-### Step 3: Tone Adjustment (all files)
-
-Per Anthropic's explicit guidance for Claude 4.5/4.6:
-
-| Before | After |
-|--------|-------|
-| `.gradient-button` is BANNED | Use `.glass-button` for all buttons, not `.gradient-button` |
-| Non-Negotiable | _(remove header, rules speak for themselves)_ |
-| MANDATORY checks | _(remove, rules are clear)_ |
-| NEVER use floating point | Sats are always integers (`u64`/`BigInt`), not floats |
-| NEVER build Rust on macOS | Do not build Rust on macOS — deploy script handles cross-compilation |
-
-This is not cosmetic — Anthropic docs state aggressive language causes overtriggering.
-
-### Step 4: Tighten Rules Files
-
-- **frontend.md** — Tone adjustment only (already 8 good rules, glob-scoped)
-- **containers.md** — Reorder critical rules to top, tone adjustment. Keep UID table and systemd requirements (genuine lookup references)
-- **api.md, bitcoin.md, crypto.md** — Tone adjustment only (already concise and glob-scoped)
-
-### Step 5: Clean Up Memory Index
-
-- Fix duplicate Session 2026-03-28 entry in MEMORY.md
-- Add missing entries for untracked files (feedback_asset_workflow.md, project_iso_size_reduction.md, etc.)
-- All memory file content preserved as-is
-
-### Step 6: No Changes To
-
-- **Skills** — Load on demand (correct architecture). 33 skill descriptions at ~100 tokens each is the design intent.
-- **Hooks** — Already well-structured.
-- **Settings** — Good as-is.
-- **Rules file glob scoping** — Already correct.
-
----
-
-## Expected Impact
-
-| Metric | Before | After | Reduction |
-|--------|--------|-------|-----------|
-| Global CLAUDE.md | 97 lines / 5,624 chars | ~35 lines / ~2,100 chars | 64% |
-| Project CLAUDE.md | 130 lines / 5,270 chars | ~75 lines / ~3,200 chars | 42% |
-| Rules files | 119 lines / 5,123 chars | ~115 lines / ~5,000 chars | 3% |
-| **Total always-loaded** | **346 lines / 16,017 chars** | **~225 lines / ~10,300 chars** | **35%** |
-
-Key outcomes:
-- Every remaining line prevents a specific, real mistake
-- No redundancy between files
-- Calm, direct tone matched to current model behavior
-- Critical rules at top/bottom of files (exploits primacy/recency attention bias)
-- ~1,400 tokens freed for actual code context per session
-
-## Files to Modify
-
-1. `/Users/dorian/.claude/CLAUDE.md` — Rewrite (97 -> ~35 lines)
-2. `/Users/dorian/Projects/archy/CLAUDE.md` — Rewrite (130 -> ~75 lines)
-3. `/Users/dorian/Projects/archy/.claude/rules/frontend.md` — Tone adjustment (BANNED -> positive)
-4. `/Users/dorian/Projects/archy/.claude/rules/containers.md` — Reorder + tone
-5. `/Users/dorian/.claude/rules/bitcoin.md` — Tone adjustment
-6. `/Users/dorian/.claude/rules/crypto.md` — Tone adjustment
-7. `/Users/dorian/.claude/projects/-Users-dorian-Projects-archy/memory/MEMORY.md` — Fix index
-
-## Verification
-
-1. Start a new Claude Code session on archy
-2. Check infrastructure IPs, SSH keys, deploy commands are all accessible
-3. Ask Claude to write a Vue component — should follow glass-button, script setup, style.css
-4. Ask Claude to write Rust backend code — should use ?, bind 127.0.0.1
-5. Ask Claude about deploying — should know deploy-to-target.sh, .228, .198
-6. Ask Claude to add a container — should follow rootless Podman, UID mapping
-7. Observe: faster responses, less hedging, more focused output
diff --git a/.claude/plans/sequential-jingling-moth.md b/.claude/plans/sequential-jingling-moth.md
deleted file mode 100644
index 571f03ca..00000000
--- a/.claude/plans/sequential-jingling-moth.md
+++ /dev/null
@@ -1,244 +0,0 @@
-# Manage — Claude Code Configuration Dashboard
-
-## Context
-
-You have 77 skills, 15 hooks, 17 memory files, 19 plans, and settings across 5 projects + global scope. All stored as flat files (markdown with YAML frontmatter, JSON, bash scripts) under `~/.claude/` and `{project}/.claude/`. Currently the only way to manage these is manually editing files. This project creates a visual web dashboard for browsing, creating, editing, and organizing all of it.
-
-**Project location**: `/Users/dorian/Projects/Manage`
-**Stack**: Vue 3 + Vite + TypeScript + Tailwind + Pinia (frontend) + Express + tsx (backend)
-**Design**: Glassmorphism dark theme (matching Archipelago aesthetic)
-
----
-
-## Architecture
-
-```
-Browser (localhost:5173) Express Server (localhost:3141)
-+-----------------------+ +----------------------------+
-| Vue 3 SPA | fetch | /api/projects |
-| +-- Dashboard | ------> | /api/skills (CRUD) |
-| +-- Skills | | /api/hooks (CRUD) |
-| +-- Hooks | SSE | /api/memory (CRUD) |
-| +-- Memory | <------ | /api/plans (CRUD) |
-| +-- Plans | | /api/settings (R/W) |
-| +-- Settings | | /api/claude-md (R/W) |
-| +-- CLAUDE.md | | /api/search |
-+-----------------------+ | /api/events (SSE) |
- +-------------+--------------+
- | chokidar
- +-------------v--------------+
- | ~/.claude/ |
- | ~/Projects/*/.claude/ |
- +----------------------------+
-```
-
-Single command start: `npm start` runs both server + Vite via concurrently.
-
----
-
-## Phase 1: Foundation — Project Setup + Dashboard
-
-### 1.1 Scaffold project
-- `npm create vite@latest` with Vue + TypeScript
-- Install deps: `express`, `cors`, `gray-matter`, `chokidar`, `concurrently`, `tsx`, `@vueuse/core`, `vue-router`, `pinia`, `fuse.js`
-- Configure `vite.config.ts` with `@` alias and `/api` proxy to `:3141`
-- Configure Tailwind with glassmorphism tokens from archy
-
-### 1.2 Design system (`src/style.css`)
-- Port glassmorphism classes from `neode-ui/src/style.css`: `.glass-card`, `.glass-button`, `.path-option-card`, `.info-card`, `.scope-badge`
-- New classes: `.skill-card`, `.hook-node`, `.memory-tree-item`, `.plan-progress-bar`, `.editor-panel`
-- Background: `#0a0a0a`, accent: `#fb923c`
-
-### 1.3 Backend: Project discovery
-- **`server/index.ts`** — Express on :3141 with CORS + JSON body parser
-- **`server/lib/discovery.ts`** — Scan `~/Projects/` for dirs with `.claude/`, decode `~/.claude/projects/` encoded paths, count skills/hooks/memory/plans per project
-- **`GET /api/projects`** — Return project list with counts
-
-### 1.4 Frontend: App shell + Dashboard
-- **`AppShell.vue`** — Sidebar (project switcher + nav links) + router-view content area
-- **`Sidebar.vue`** — "Global" at top, then project list; active project highlighted; click to switch scope
-- **`Dashboard.vue`** — Stats row (total skills/hooks/memory/plans) + project cards grid
-- **`ProjectCard.vue`** — Glass card showing project name, path, skill/hook/memory counts, click to select
-- **`stores/projects.ts`** — Pinia store: `projects[]`, `activeProject`, `fetchProjects()`, `setActiveProject()`
-
-**Verify**: `npm start` opens browser, sidebar shows 5 projects + global, dashboard shows stats.
-
----
-
-## Phase 2: Skills Manager
-
-### 2.1 Backend
-- **`server/lib/skill-parser.ts`** — Parse SKILL.md YAML frontmatter via `gray-matter`, handle both `skills/{name}/SKILL.md` (dir-based) and `skills/{name}.md` (flat) formats
-- **`server/lib/fs-utils.ts`** — Safe read/write/delete/mkdir helpers with atomic writes
-- **`server/routes/skills.ts`** — Full CRUD + `POST /api/skills/move` for scope transfers
-
-### 2.2 Frontend
-- **`Skills.vue`** — Top bar: scope filter, grid/list toggle, category dropdown, search. Grid of SkillCards. FAB for "New Skill"
-- **`SkillCard.vue`** — Name, description (truncated), scope badge, category color stripe, allowed-tools pills. Click opens editor.
-- **`SkillEditor.vue`** — Slide-in panel: frontmatter form (name, description, category, tags, allowed-tools, disable-model-invocation toggle) + Monaco editor for markdown body + live preview
-- **`InheritanceMap.vue`** — Two-column view: global skills left, project skills right, connecting lines for name-matched overrides
-- **Drag-and-drop**: Drag SkillCard between global/project columns to move/copy. Uses `vue-draggable-plus`.
-
-**Verify**: Browse all 77 skills, create/edit/delete, drag between scopes, see inheritance.
-
----
-
-## Phase 3: Hooks Manager
-
-### 3.1 Backend
-- **`server/lib/hook-parser.ts`** — Parse `settings.json` hook entries + read referenced `.sh` files. Detect orphaned scripts.
-- **`server/routes/hooks.ts`** — CRUD + `PUT /toggle` for enable/disable. Creates .sh + updates settings.json atomically.
-
-### 3.2 Frontend
-- **`Hooks.vue`** — Grouped by event type (PreToolUse, PostToolUse, UserPromptSubmit, Stop, SessionEnd)
-- **`HookPipeline.vue`** — Visual flow per hook: `[Event Badge] -> [Matcher Pill] -> [Script Name] -> [Action]` with CSS-drawn connecting arrows
-- **`HookCard.vue`** — Event type badge (color-coded), matcher, script filename, enabled/disabled toggle switch
-- **`HookEditor.vue`** — Monaco editor for `.sh` script + form for event type and matcher pattern
-- Orphaned scripts in "Unlinked Scripts" section with "Link" button
-
-**Verify**: See all 15 hooks in pipeline view, toggle enable/disable, edit scripts, create new hook.
-
----
-
-## Phase 4: Memory Browser
-
-### 4.1 Backend
-- **`server/lib/memory-parser.ts`** — Parse from both locations: `{project}/.claude/memory/` (git-tracked) and `~/.claude/projects/{encoded}/memory/` (private). Parse YAML frontmatter.
-- **`server/routes/memory.ts`** — CRUD + auto-sync MEMORY.md index on create/delete
-
-### 4.2 Frontend
-- **`Memory.vue`** — Split layout: tree panel (left 300px) + content panel (right)
-- **`MemoryTree.vue`** — Collapsible tree: Project -> Scope -> Type -> Files. Type badges: user (blue), feedback (orange), project (green), reference (purple)
-- **`MemoryEditor.vue`** — Frontmatter form (name, description, type dropdown) + Monaco editor + markdown preview toggle
-- Search input at top filters across titles and content
-
-**Verify**: Browse all 17 memory files in tree, types color-coded, edit with preview, create new, MEMORY.md auto-updates.
-
----
-
-## Phase 5: Plans Tracker
-
-### 5.1 Backend
-- **`server/lib/plan-parser.ts`** — Extract title from `#`, phases from `##`, tasks from `- [ ]`/`- [x]` with line numbers. Calculate completion percentages.
-- **`server/routes/plans.ts`** — CRUD + `PUT /task` for toggling single checkbox by line number
-
-### 5.2 Frontend
-- **`PlanCard.vue`** — Title, overall progress bar, phase count, "12/47 tasks" text
-- **`PlanDetail.vue`** — Expanded: title, summary, phases as sections with TaskCheckboxes
-- **`PhaseBar.vue`** — Segmented bar: green (done) / amber (in-progress) / gray (pending)
-- **`TaskCheckbox.vue`** — Click toggles checkbox, instant API call to update file
-- "Edit Raw" switches to Monaco. "New Plan" uses overnight template.
-
-**Verify**: See all 19 plans with progress bars, toggle checkboxes that persist, create new plan.
-
----
-
-## Phase 6: Settings + CLAUDE.md Editor
-
-### 6.1 Settings
-- **`Settings.vue`** — Scope tabs (Global / Project). Sections:
- - Permissions: toggle switches for allowed tools
- - Hooks: visual tree of event -> matcher -> command with add/remove
- - Plugins: installed plugin cards with enable/disable
- - Effort Level: dropdown
- - Raw JSON: toggle to edit settings.json directly in Monaco
-
-### 6.2 CLAUDE.md
-- **`ClaudeMd.vue`** — Scope tabs. Monaco editor with markdown syntax. Live preview panel. Unsaved changes indicator. Save button.
-
-**Verify**: Edit settings, toggle permissions, edit CLAUDE.md with preview, confirm files updated.
-
----
-
-## Phase 7: Polish — File Watching, Search, Animations
-
-### 7.1 Live file watching
-- **`server/lib/file-watcher.ts`** — chokidar watches all `.claude/` dirs. Debounce 300ms. Push SSE events.
-- **`useFileWatcher.ts`** composable — EventSource connection, triggers store refresh on changes
-
-### 7.2 Global search
-- **`GET /api/search?q=bitcoin`** — Full-text across skills, memory, plans, CLAUDE.md
-- **`TopBar.vue`** — Cmd+K search input with dropdown results
-
-### 7.3 Drag-and-drop refinement
-- `vue-draggable-plus` for skills between scopes and plan task reordering
-
-### 7.4 Final polish
-- Loading skeletons, empty states, confirm dialogs on deletes
-- Keyboard shortcuts: Cmd+K (search), Cmd+S (save), Escape (close panels)
-- View transitions (fade + slide)
-
-**Verify**: External file edits trigger UI refresh. Cmd+K searches everything. Drag skills between scopes.
-
----
-
-## Project Structure
-
-```
-Manage/
-+-- package.json
-+-- tsconfig.json
-+-- vite.config.ts
-+-- tailwind.config.ts
-+-- index.html
-+-- .gitignore
-+-- server/
-| +-- index.ts
-| +-- tsconfig.json
-| +-- routes/
-| | +-- projects.ts, skills.ts, hooks.ts, memory.ts
-| | +-- plans.ts, settings.ts, claude-md.ts, search.ts
-| +-- lib/
-| | +-- discovery.ts, skill-parser.ts, hook-parser.ts
-| | +-- memory-parser.ts, plan-parser.ts, settings-parser.ts
-| | +-- file-watcher.ts, fs-utils.ts
-| +-- types/
-| +-- index.ts
-+-- src/
-| +-- main.ts, App.vue, style.css
-| +-- api/client.ts
-| +-- router/index.ts
-| +-- stores/ (projects, skills, hooks, memory, plans, settings, search)
-| +-- types/ (skill, hook, memory, plan, project, settings)
-| +-- composables/ (useFileWatcher, useMarkdownPreview, useMonaco)
-| +-- views/ (Dashboard, Skills, Hooks, Memory, Plans, Settings, ClaudeMd)
-| +-- components/
-| +-- layout/ (AppShell, Sidebar, TopBar)
-| +-- shared/ (GlassCard, GlassButton, ScopeBadge, MonacoEditor, etc.)
-| +-- dashboard/ (ProjectCard, QuickStats)
-| +-- skills/ (SkillCard, SkillEditor, SkillList, InheritanceMap)
-| +-- hooks/ (HookPipeline, HookCard, HookEditor)
-| +-- memory/ (MemoryTree, MemoryCard, MemoryEditor)
-| +-- plans/ (PlanCard, PlanDetail, PhaseBar, TaskCheckbox)
-| +-- settings/ (PermissionToggle, HookConfig, PluginCard)
-+-- public/
- +-- favicon.svg
-```
-
----
-
-## Key Libraries
-
-| Library | Purpose |
-|---------|---------|
-| `express` + `cors` | Backend HTTP server |
-| `tsx` | Run TypeScript server without build step |
-| `concurrently` | Run server + Vite in one command |
-| `gray-matter` | Parse YAML frontmatter from markdown |
-| `chokidar` | Watch filesystem for live updates |
-| `monaco-editor` + `@monaco-editor/loader` | Code editor (md, bash, json, yaml) |
-| `marked` + `highlight.js` | Markdown rendering with syntax highlighting |
-| `vue-draggable-plus` | Drag-and-drop for skills and plan tasks |
-| `fuse.js` | Client-side fuzzy search |
-| `@vueuse/core` | Vue utilities (useEventSource, useDebounceFn) |
-
----
-
-## Key Decisions
-
-- **Express over Bun**: More predictable on macOS, better middleware ecosystem
-- **SSE over WebSocket**: File watching is server->client only. SSE auto-reconnects, simpler.
-- **Monaco over CodeMirror**: VS Code-like editing for all 4 file types
-- **Atomic settings.json writes**: Read-modify-write with temp file + rename
-- **MEMORY.md auto-sync**: Create/delete memory files auto-updates the index
-- **Both skill formats**: Parser handles dir-based and flat-file skills
diff --git a/.claude/plans/shiny-bouncing-raven.md b/.claude/plans/shiny-bouncing-raven.md
deleted file mode 100644
index ae48761b..00000000
--- a/.claude/plans/shiny-bouncing-raven.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# Plan: Fix Iframe Apps, Detail Pages, Kiosk, Identity Pairing, NIP-07
-
-## Context
-
-Three web-only apps (BotFights, 484 Kitchen, Arch Presentation) show black screens in iframe despite nginx reverse proxies being set up. The kiosk on .228 isn't running. Web-only apps need proper detail pages. The user wants Nostr identity formally paired with DID and NIP-07 browser integration for frictionless login to embedded apps.
-
----
-
-## Task 1: Fix iframe black screen (HIGH)
-
-**Root cause**: Proxied HTML contains root-relative paths (`href="/css/main.css"`). Browser resolves these against the origin root, not `/ext/botfights/`, so all assets 404.
-
-**Fix**: Add `sub_filter` to nginx proxy blocks to rewrite root-relative paths.
-
-**File**: `image-recipe/configs/nginx-archipelago.conf` (6 location blocks — 3 HTTP, 3 HTTPS)
-
-Key additions per block:
-```nginx
-proxy_set_header Accept-Encoding ""; # Disable gzip so sub_filter works
-sub_filter_once off;
-sub_filter_types text/html text/css application/javascript;
-sub_filter 'href="/' 'href="/ext/{app}/';
-sub_filter 'src="/' 'src="/ext/{app}/';
-sub_filter 'action="/' 'action="/ext/{app}/';
-sub_filter "href='/" "href='/ext/{app}/";
-sub_filter "src='/" "src='/ext/{app}/";
-```
-
-Deploy + nginx reload. Verify in browser DevTools (Network tab — no 404s on assets).
-
----
-
-## Task 2: Detail pages for web-only apps (MEDIUM)
-
-**Problem**: Clicking a web-only app card navigates to `/dashboard/apps/{id}`. AppDetails.vue can't resolve it because web-only apps aren't in `store.packages` or `dummyApps`.
-
-**Fix**:
-1. Add 7 web-only apps to `dummyApps` in AppDetails.vue (botfights, nwnn, 484-kitchen, call-the-operator, arch-presentation, syntropy-institute, t-zero) — same pattern as IndeeHub
-2. Add URL mappings in AppDetails.vue `appUrls` for all 7 (if not already present)
-3. Hide uninstall/start/stop buttons for web-only apps in AppDetails.vue
-
-**Files**: `neode-ui/src/views/AppDetails.vue`
-
----
-
-## Task 3: Kiosk on .228 (MEDIUM)
-
-**Problem**: Code exists but was never installed on server. No X11/Chromium packages.
-
-**Steps** (SSH to .228, no code changes):
-1. `sudo apt-get install -y xorg chromium unclutter xinit`
-2. `cd ~/archy && sudo ./scripts/setup-kiosk.sh archipelago`
-3. `sudo systemctl enable --now archipelago-kiosk.service`
-4. Verify on monitor
-
----
-
-## Task 4: Pair Nostr identity with DID (LOW)
-
-**Current state**: Ed25519 (DID) and secp256k1 (Nostr) are separate key pairs, both generated at startup. Not formally linked.
-
-**Fix**: Include the Nostr secp256k1 pubkey in the DID Document as an additional verification method:
-- Modify `did_document_from_pubkey_hex()` in `identity.rs` to accept optional Nostr pubkey
-- Add `EcdsaSecp256k1VerificationKey2019` entry to `verificationMethod` array
-- Pass Nostr pubkey from server startup context
-
-**Files**: `core/archipelago/src/identity.rs`, `core/archipelago/src/server.rs`
-
----
-
-## Task 5: NIP-07 Nostr login via iframe injection (EXPLORATORY)
-
-**Goal**: Web apps in iframe (like IndeeHub) can call `window.nostr.getPublicKey()` and `window.nostr.signEvent()` for frictionless Nostr login.
-
-**Approach**: Inject a `window.nostr` shim into proxied pages via `sub_filter`, communicating with the parent Archipelago frame via `postMessage`.
-
-**Steps**:
-1. Create `neode-ui/public/nostr-provider.js` — implements `window.nostr` interface, uses `postMessage` to parent
-2. Add `sub_filter '' '';` to nginx ext proxy blocks
-3. Add `postMessage` listener in AppLauncherOverlay that handles `nostr-getPublicKey` and `nostr-signEvent` by calling backend RPC
-4. Backend already has `identity.nostr-sign` and `node.nostr-pubkey` RPC endpoints
-
-**Security**: Validate postMessage origin, prompt user before signing, never expose secret key to frontend.
-
-**Files**: new `neode-ui/public/nostr-provider.js`, `image-recipe/configs/nginx-archipelago.conf`, AppLauncherOverlay component, `neode-ui/src/stores/appLauncher.ts`
-
----
-
-## Execution Order
-
-1. Task 1 — fix iframe black screen (deploy nginx)
-2. Task 2 — detail pages (deploy frontend)
-3. Task 3 — kiosk on .228 (SSH ops)
-4. Task 4 — DID+Nostr pairing (deploy backend)
-5. Task 5 — NIP-07 injection (deploy full)
-
-## Verification
-
-- Task 1: Open BotFights/484 Kitchen/Arch Presentation in iframe — page renders with styles and interactivity
-- Task 2: Click web-only app card → detail page shows with title, description, launch button, no container buttons
-- Task 3: .228 monitor shows kiosk app grid
-- Task 4: `node.did` RPC returns DID Document with Nostr pubkey in verificationMethod
-- Task 5: Open IndeeHub in iframe, browser console `window.nostr.getPublicKey()` returns hex pubkey
diff --git a/.claude/plans/silly-wondering-flamingo.md b/.claude/plans/silly-wondering-flamingo.md
deleted file mode 100644
index c6e6903f..00000000
--- a/.claude/plans/silly-wondering-flamingo.md
+++ /dev/null
@@ -1,243 +0,0 @@
-# ISO Overhaul: Custom Minimal Base + Branding + Size Optimization
-
-## Context
-
-The Archipelago ISO is ~3.9GB — too large. The root cause is a ~800MB Debian Live ISO used as the boot base, plus a ~2.1GB rootfs with no `--no-install-recommends`. We're replacing the Debian Live dependency entirely with a custom debootstrap-built installer, adding full Archipelago branding to the boot chain, and stripping the rootfs. Target: sub-2GB ISO.
-
-All work on `dev-iso` branch with its own CI workflow. Main branch stays untouched.
-
----
-
-## Phase 0: Branch + CI Setup
-
-**Create `dev-iso` branch and separate CI workflow.**
-
-1. Branch from current `main`
-2. Create `.gitea/workflows/build-iso-dev.yml`:
- - Trigger: `push: branches: [dev-iso]` + `workflow_dispatch`
- - Same structure as `build-iso.yml` (131 lines) but:
- - Remove "Cache Debian Live ISO" step (no longer needed)
- - Add `debootstrap`, `squashfs-tools`, `isolinux`, `syslinux-common`, `mtools`, `grub-efi-amd64-bin`, `grub-pc-bin` to tool dependencies
- - Output naming: `archipelago-dev-unbundled-{date}.iso`
- - Keep: backend build, frontend build, type check, tests, build report
-3. Push and verify CI triggers on .228 runner
-
-**Files:**
-- New: `.gitea/workflows/build-iso-dev.yml`
-
----
-
-## Phase 1: Rootfs Size Optimizations
-
-**Shrink rootfs.tar from ~2.1GB to ~1.5GB. Only touches the Dockerfile heredoc in Step 1 (lines 210-335).**
-
-### 1.1 Add `--no-install-recommends`
-- Line 229: `apt-get install -y` → `apt-get install -y --no-install-recommends`
-- Line 269: Same for Tailscale install
-- Explicitly add packages that may be needed as recommends: `fonts-liberation`, `xfonts-base` (for Chromium kiosk)
-- **Saves: ~150-300MB**
-
-### 1.2 Remove `firmware-misc-nonfree`
-- Line 257: Remove `firmware-misc-nonfree` from package list
-- Keep: `firmware-realtek`, `firmware-iwlwifi`, `intel-microcode`, `amd64-microcode`
-- **Saves: ~50-80MB**
-
-### 1.3 Strip docs/man/locales
-- Add after line 264 (after apt-get clean):
- ```dockerfile
- RUN find /usr/share/doc -depth -type f ! -name copyright -delete 2>/dev/null; \
- find /usr/share/doc -empty -delete 2>/dev/null; \
- rm -rf /usr/share/man /usr/share/info /usr/share/lintian /usr/share/linda; \
- find /usr/share/locale -maxdepth 1 -mindepth 1 ! -name 'en_US' ! -name 'locale.alias' -exec rm -rf {} +
- ```
-- **Saves: ~50-80MB**
-
-### 1.4 Remove `wget` and `htop`
-- Lines 244, 246: Remove `wget` (curl covers it) and `htop` (luxury tool)
-- Keep `git` (used by self-update system)
-- **Saves: ~5MB** (minor but removes unnecessary surface)
-
-### Verification
-- Build ISO, compare rootfs.tar size
-- Boot in QEMU, verify: kiosk renders, SSH works, nginx serves UI, podman runs
-
-**Files modified:**
-- `image-recipe/build-auto-installer-iso.sh` (Step 1 Dockerfile heredoc, lines 210-335)
-
----
-
-## Phase 2: Replace Debian Live with Custom Debootstrap Base
-
-**The big one. Replaces Steps 2, 5, and parts of 4 and 6.**
-
-### 2.1 New Step 2: Build Minimal Installer Environment
-
-Replace lines 420-502 entirely. Run debootstrap inside a container to produce:
-- `vmlinuz` — kernel (reused from linux-image-amd64)
-- `initrd.img` — custom initramfs with ISO-mount hook
-- `filesystem.squashfs` — minimal Debian root (~120-180MB)
-
-The installer squashfs contains only what's needed to run the auto-install script:
-- `debootstrap --variant=minbase --include=systemd,systemd-sysv,udev,bash,coreutils,mount,util-linux,cryptsetup,parted,dosfstools,e2fsprogs,kmod,procps,iproute2,ca-certificates,gdisk`
-- Auto-login on tty1 via getty override
-- systemd service that auto-starts the installer (replaces profile.d hack)
-
-**Key: Custom initramfs hook** (`local-bottom/archipelago-mount`) that:
-1. Scans `/dev/sr0`, `/dev/sd*` for a partition containing `archipelago/auto-install.sh`
-2. Mounts it read-only at `/run/archiso`
-3. This replaces Debian Live's `boot=live components` mechanism
-
-### 2.2 New Step 5: Assemble ISO Directory
-
-Replace lines 2236-2448 entirely. Much simpler — no squashfs overlay mechanism, no tools extraction (tools are in the squashfs), no profile.d manipulation.
-
-New Step 5 just assembles the directory structure:
-```
-$INSTALLER_ISO/
- live/
- vmlinuz
- initrd.img
- filesystem.squashfs
- boot/grub/
- grub.cfg
- themes/archipelago/ (Phase 3)
- efi.img (built with grub-mkimage)
- isolinux/
- isolinux.bin
- ldlinux.c32
- isolinux.cfg
- EFI/BOOT/
- BOOTX64.EFI (built with grub-mkimage)
- archipelago/
- auto-install.sh
- rootfs.tar
- bin/archipelago
- web-ui/
- scripts/
- container-images/ (if bundled)
-```
-
-Generate EFI boot image with `grub-mkimage` and ISOLINUX files from the `isolinux` package. No more extracting MBR from Debian Live.
-
-### 2.3 Updated Step 6: ISO Creation
-
-Replace lines 2461-2511 (MBR extraction + EFI image search). Use:
-- MBR: `/usr/lib/ISOLINUX/isohdpfx.bin` (from `isolinux` package)
-- EFI: `boot/grub/efi.img` (built in Step 5)
-- xorriso command stays the same structure
-
-### 2.4 Update Boot Media Paths in Step 4 (auto-install.sh)
-
-Lines 1154-1155: Add `/run/archiso` as first search path:
-```bash
-for dev in /run/archiso /cdrom /media/cdrom /run/live/medium /lib/live/mount/medium; do
-```
-
-Also update lines 2326, 2377 (no longer needed — replaced by systemd service in installer squashfs).
-
-### 2.5 Remove Debian Live cleanup from auto-install.sh
-
-The installed system's auto-install script currently removes `live-boot`, `live-boot-initramfs-tools`, `live-config` (around line 1872). With the custom base, these packages won't exist in the rootfs, so this cleanup becomes a harmless no-op — but should be cleaned up for clarity.
-
-### Verification
-- Build ISO, verify size < 2GB
-- Boot in QEMU (UEFI mode): verify GRUB menu → installer → full install → reboot
-- Boot in QEMU (BIOS mode): verify ISOLINUX → installer → full install → reboot
-- After install: SSH, web UI, kiosk, container loading all work
-- Test `test-iso-qemu.sh` (may need minor path updates)
-
-**Files modified:**
-- `image-recipe/build-auto-installer-iso.sh` (Steps 2, 4, 5, 6 — major rewrite)
-
----
-
-## Phase 3: Archipelago Boot Branding
-
-**Custom GRUB theme, installer banner, installed system GRUB.**
-
-### 3.1 Create GRUB Theme
-
-New directory: `image-recipe/branding/grub-theme/`
-- `theme.txt` — dark background (#0a0a0a), white text, Bitcoin orange (#f7931a) highlight
-- `background.png` — 1920x1080 dark with subtle Archipelago logo watermark
-- Font files (`.pf2`) — generated with `grub-mkfont` from DejaVu Sans during build
-
-GRUB menu entries:
-- "Install Archipelago" (default, quiet boot)
-- "Install Archipelago (verbose)" (no `quiet`, for debugging)
-- "Boot from local disk" (chainloader)
-
-### 3.2 Create ISOLINUX Theme
-
-New file: `image-recipe/branding/isolinux.cfg`
-- Matching dark theme for legacy BIOS boot
-- Same menu entries as GRUB
-
-### 3.3 Branded Installer Banner
-
-The systemd service's start script displays:
-```
- ARCHIPELAGO BITCOIN NODE OS
- Automatic Installer v0.1.0
-
- Press Enter to start installation...
-```
-
-### 3.4 Install GRUB Theme to Target System
-
-In Step 4 (auto-install.sh), before `update-grub` (around line 1888):
-- Copy GRUB theme from ISO to `/mnt/target/boot/grub/themes/archipelago/`
-- Add `GRUB_THEME="/boot/grub/themes/archipelago/theme.txt"` to `/mnt/target/etc/default/grub`
-- The installed system boots with Archipelago branding, not Debian default
-
-### 3.5 Create Background Image
-
-Render from existing SVG favicon (`neode-ui/public/assets/icon/favico-black-v2.svg`) to PNG at appropriate sizes. Dark background with subtle centered logo.
-
-### Verification
-- Boot ISO: GRUB shows Archipelago theme (dark + orange)
-- No Debian branding visible anywhere
-- After install: target system GRUB also shows Archipelago theme
-
-**Files:**
-- New: `image-recipe/branding/grub-theme/theme.txt`
-- New: `image-recipe/branding/grub-theme/background.png`
-- New: `image-recipe/branding/isolinux.cfg`
-- Modified: `image-recipe/build-auto-installer-iso.sh` (Steps 5, 4)
-
----
-
-## Risk Areas
-
-| Risk | Severity | Mitigation |
-|------|----------|------------|
-| Custom initramfs fails to find USB media | High | Test multiple USB controller types in QEMU; add verbose fallback boot option |
-| Missing packages in minbase break install | Medium | Trace auto-install.sh dependencies; test full install flow |
-| GRUB EFI image missing modules | High | Include all common modules in grub-mkimage; test UEFI + BIOS |
-| Kiosk breaks without recommends | Medium | Explicitly add Chromium/X11 font deps; test kiosk before merge |
-| initramfs overlayfs mount fails | High | Follow well-established patterns from Arch/Ubuntu live ISOs |
-
----
-
-## Implementation Order
-
-1. **Phase 0** — branch + CI (~1 hour)
-2. **Phase 1** — rootfs size opts (~2 hours, push + verify)
-3. **Phase 2** — custom base (~8-10 hours, iterative QEMU testing)
-4. **Phase 3** — branding (~3 hours)
-
-Phases are sequential — each builds on the previous. Push after each phase, verify CI passes.
-
----
-
-## Key Files
-
-| File | Role |
-|------|------|
-| `image-recipe/build-auto-installer-iso.sh` | Main build script — most changes here |
-| `.gitea/workflows/build-iso-dev.yml` | New CI workflow for dev-iso branch |
-| `image-recipe/branding/grub-theme/*` | New GRUB theme assets |
-| `image-recipe/branding/isolinux.cfg` | New ISOLINUX config |
-| `image-recipe/test-iso-qemu.sh` | QEMU test script (minor updates) |
-| `.gitea/workflows/build-iso.yml` | Reference for new CI workflow |
-| `scripts/image-versions.sh` | Unchanged — container image versions |
diff --git a/.claude/plans/smooth-roaming-wadler.md b/.claude/plans/smooth-roaming-wadler.md
deleted file mode 100644
index 1f52da3e..00000000
--- a/.claude/plans/smooth-roaming-wadler.md
+++ /dev/null
@@ -1,241 +0,0 @@
-# Container Orchestration Dev Testing Infrastructure
-
-## Context
-
-Container orchestration has been unreliable for months. Every fix requires a full deploy to .228 (5+ minutes), manual SSH debugging, and prayer. No way to test orchestration logic locally or catch regressions before deploy. We need three layers of testing so orchestration is bulletproof before it ever touches a server.
-
-## Three Layers
-
-### Layer C: Mock Podman in Rust Unit Tests (runs on macOS, instant)
-
-Tests the orchestration LOGIC without any containers. Runs in `cargo test`, takes seconds.
-
-**What it tests:** Retry backoff timing, restart tracker persistence, tier ordering, stop grace periods, failsafe install flow, health monitor state machine, crash recovery.
-
-**Implementation:**
-
-Create `core/archipelago/src/container/mock_podman.rs` — a fake podman command executor:
-
-```rust
-pub struct MockPodman {
- containers: Arc>>,
- fail_pull: Arc, // simulate registry down
- fail_start: Arc, // simulate container crash on start
- pull_delay_ms: Arc, // simulate slow pull
-}
-
-struct MockContainer {
- name: String,
- image: String,
- state: ContainerState, // Created/Running/Exited/Stopped
- exit_code: i32,
- created_at: DateTime,
-}
-```
-
-Key trait to add in `runtime.rs`:
-```rust
-#[async_trait]
-pub trait CommandExecutor: Send + Sync {
- async fn execute(&self, program: &str, args: &[&str]) -> Result;
-}
-```
-
-Production uses `RealExecutor` (calls `tokio::process::Command`). Tests use `MockPodman`.
-
-**Test file:** `core/archipelago/tests/orchestration_tests.rs`
-
-Tests to write:
-1. `test_stop_grace_periods` — bitcoin gets 600s, lnd 330s, unknown gets 30s
-2. `test_pull_retry_backoff` — fail twice, succeed third, verify 5s/15s delays
-3. `test_pull_all_attempts_fail` — fail 3x, verify error returned
-4. `test_restart_tracker_persistence` — save to disk, reload, verify counters survive
-5. `test_restart_tracker_stability_reset` — after 1h, counters clear
-6. `test_failsafe_install_rollback` — container exits immediately, verify cleanup
-7. `test_failsafe_install_image_missing` — pull succeeds but image not found, verify error
-8. `test_health_monitor_tier_ordering` — databases restart before apps
-9. `test_health_monitor_skips_user_stopped` — user-stopped containers not restarted
-10. `test_health_monitor_max_attempts` — stops after 3 failures
-11. `test_crash_recovery_loads_snapshot` — PID file + snapshot → containers restarted
-12. `test_crash_recovery_skips_user_stopped` — user-stopped not recovered
-
-**Files to modify:**
-- `core/archipelago/src/container/mod.rs` — add `pub mod mock_podman;`
-- `core/archipelago/src/container/mock_podman.rs` — NEW mock implementation
-- `core/archipelago/tests/orchestration_tests.rs` — NEW test file
-- `core/archipelago/src/health_monitor.rs` — extract logic into testable functions (pure functions that take data, not functions that call podman)
-- `core/archipelago/src/api/rpc/package/runtime.rs` — make `stop_timeout_secs` public for testing
-
-**Key refactors to make code testable:**
-- Extract `stop_timeout_secs()` → `pub fn` so tests can call it directly
-- Extract health monitor `check_and_restart()` into a function that takes container list + tracker + user_stopped, returns actions to take (restart X, notify Y, skip Z) — pure logic, no IO
-- Extract `RestartTracker` + `RestartHistory` into own file for independent testing
-- Make `pull_image_with_progress` retry logic independent of progress streaming
-
----
-
-### Layer A: SSH Dev Loop in dev-start.sh (real containers on .228)
-
-New option 9 in `dev-start.sh`: "Container orchestration dev (live on .228)"
-
-**What it does:**
-1. Rsync code to .228 (2 seconds)
-2. Build backend on .228 (incremental: 5-15 seconds)
-3. Restart archipelago service
-4. Run orchestration smoke tests via RPC
-5. Show container status + health monitor logs
-6. Loop: edit locally → press Enter → rsync+rebuild+test
-
-**What it tests:** Real podman, real containers, real networking. The actual install/start/stop/restart/health cycle.
-
-**Implementation:**
-
-Add option 9 to `scripts/dev-start.sh`:
-```bash
-9)
- echo "Container Orchestration Dev (live testing on .228)"
- exec "$SCRIPT_DIR/dev-container-test.sh"
- ;;
-```
-
-Create `scripts/dev-container-test.sh` (~150 lines):
-```bash
-#!/bin/bash
-# Fast edit-build-test loop for container orchestration on .228
-#
-# Usage: ./scripts/dev-container-test.sh [--once]
-#
-# Syncs code, builds, restarts, runs orchestration smoke tests.
-# Press Enter to re-run, Ctrl+C to stop.
-
-SSH="ssh -o StrictHostKeyChecking=no -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228"
-
-sync_and_build() {
- rsync (same excludes as deploy script)
- ssh: cargo build --release -p archipelago (incremental)
- ssh: sudo systemctl restart archipelago
- ssh: wait for health endpoint (15s timeout)
-}
-
-run_smoke_tests() {
- # Test 1: Container list works
- curl -s /rpc/v1 -d '{"method":"container.list"}'
-
- # Test 2: Install filebrowser (small, fast, no deps)
- curl -s /rpc/v1 -d '{"method":"package.install","params":{"id":"filebrowser","dockerImage":"..."}}'
- # Wait for running state
-
- # Test 3: Stop with grace period
- curl -s /rpc/v1 -d '{"method":"package.stop","params":{"id":"filebrowser"}}'
- # Verify stopped
-
- # Test 4: Start
- curl -s /rpc/v1 -d '{"method":"package.start","params":{"id":"filebrowser"}}'
- # Verify running
-
- # Test 5: Health check
- curl -s /rpc/v1 -d '{"method":"container.health"}'
-
- # Test 6: Check restart-tracker.json exists
- ssh: cat /var/lib/archipelago/restart-tracker.json
-
- # Test 7: Check health monitor logs for errors
- ssh: journalctl -u archipelago --since "2 min ago" | grep -i "error\|panic\|fail"
-
- # Test 8: Uninstall
- curl -s /rpc/v1 -d '{"method":"package.uninstall","params":{"id":"filebrowser"}}'
-}
-
-# Main loop
-while true; do
- sync_and_build
- run_smoke_tests
- echo "Press Enter to re-run, Ctrl+C to stop"
- read
-done
-```
-
-**Files:**
-- `scripts/dev-start.sh` — add option 9
-- `scripts/dev-container-test.sh` — NEW
-
----
-
-### Layer B: CI Integration Tests (runs on .228 via Gitea Actions)
-
-Extend the existing CI to run container orchestration tests on every push to dev-iso.
-
-**What it tests:** Full lifecycle on real hardware after every code change. Catches regressions automatically.
-
-**Implementation:**
-
-Create `.gitea/workflows/container-tests.yml`:
-```yaml
-name: Container Orchestration Tests
-on:
- push:
- branches: [dev-iso, main]
- paths:
- - 'core/**'
- - 'scripts/container-*.sh'
- - 'scripts/reconcile-*.sh'
-
-jobs:
- unit-tests:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: Rust unit tests (orchestration)
- run: cargo test -p archipelago -- orchestration --no-fail-fast
-
- integration-tests:
- runs-on: ubuntu-latest
- needs: unit-tests
- steps:
- - uses: actions/checkout@v4
- - name: Deploy to test node
- run: |
- # Rsync + build on .228
- # Run orchestration smoke tests
- bash scripts/run-container-tests.sh
-```
-
-Create `scripts/run-container-tests.sh` (~200 lines):
-Reuses the smoke test logic from dev-container-test.sh but structured for CI:
-- JSON output for CI parsing
-- Exit codes for pass/fail
-- Timeout handling (5 min max)
-- Cleanup after test (remove test containers)
-- Tests: install, start, stop, restart, uninstall, health check, restart tracker, reconciliation
-
-**Files:**
-- `.gitea/workflows/container-tests.yml` — NEW
-- `scripts/run-container-tests.sh` — NEW
-
----
-
-## Execution Order
-
-1. **Layer C first** (mock tests) — Get the logic tested, runs locally, fast feedback
-2. **Layer A second** (dev loop) — Test against real containers with fast iteration
-3. **Layer B last** (CI) — Automate regression catching
-
-## Files Summary
-
-| File | Action | Layer |
-|------|--------|-------|
-| `core/archipelago/src/container/mock_podman.rs` | NEW | C |
-| `core/archipelago/src/container/mod.rs` | MODIFY | C |
-| `core/archipelago/tests/orchestration_tests.rs` | NEW | C |
-| `core/archipelago/src/health_monitor.rs` | REFACTOR (extract pure logic) | C |
-| `core/archipelago/src/api/rpc/package/runtime.rs` | MODIFY (pub fn) | C |
-| `scripts/dev-start.sh` | MODIFY (add option 9) | A |
-| `scripts/dev-container-test.sh` | NEW | A |
-| `.gitea/workflows/container-tests.yml` | NEW | B |
-| `scripts/run-container-tests.sh` | NEW | B |
-
-## Verification
-
-- Layer C: `cargo test -p archipelago -- orchestration` — all pass on macOS
-- Layer A: `./scripts/dev-start.sh` → option 9 → green smoke tests on .228
-- Layer B: Push to dev-iso → CI green on container-tests workflow
diff --git a/.claude/plans/synchronous-greeting-rose.md b/.claude/plans/synchronous-greeting-rose.md
deleted file mode 100644
index a4cf0f7c..00000000
--- a/.claude/plans/synchronous-greeting-rose.md
+++ /dev/null
@@ -1,173 +0,0 @@
-# Mesh Phase 4 Completion + Phase 5 Implementation
-
-## Context
-
-Mesh Phases 1-3 are complete: serial driver, transport layer (Mesh>LAN>Tor), Double Ratchet encryption, typed messages, store-and-forward, chat UI. Phase 4 is 40% done — data structures, builders, and tests exist (`bitcoin_relay.rs`, `alerts.rs`, `message_types.rs`) but nothing is wired into the listener, MeshService, or RPC layer. Phase 5 (steganographic modes, adaptive routing, multi-hardware) is not started.
-
-## Phase 4: Wire Up Off-Grid Bitcoin Operations (Weeks 8-11)
-
-### Week 8: Typed Message Dispatch in Listener
-
-**The critical foundation — everything else depends on this.**
-
-**`mesh/listener.rs`:**
-- Add `MeshCommand::SendRaw { dest_pubkey_prefix: [u8; 6], payload: Vec }` and `BroadcastChannel { channel: u8, payload: Vec }` variants
-- In `handle_frame()`: after extracting message bytes, check for `0x02` TypedEnvelope prefix
-- New `handle_typed_message()` dispatches by type:
- - `BlockHeader` → validate Ed25519 sig, store in `BlockHeaderCache`, emit event
- - `TxRelay` → spawn task: Bitcoin RPC `sendrawtransaction`, send `TxRelayResponse` back
- - `TxRelayResponse` → complete pending in `RelayTracker`, store as MeshMessage
- - `LightningRelay` → spawn task: LND REST `payinvoice`, send response back
- - `LightningRelayResponse` → complete pending, store
- - `Alert` → verify sig, store, emit `MeshEvent::AlertReceived`
-- Handle `SendRaw` and `BroadcastChannel` in `tokio::select!` command dispatch
-
-**`mesh/types.rs`:** New `MeshEvent` variants: `BlockHeaderReceived`, `AlertReceived`, `TxRelayCompleted`, `LightningRelayCompleted`
-
-**Key design:** Spawn separate tokio tasks for Bitcoin/LND HTTP calls (don't block serial read loop). Response sent back via `cmd_tx` channel.
-
-### Week 9: MeshService Integration + Dead Man's Switch Task
-
-**`mesh/mod.rs`:**
-- Add fields: `block_header_cache: Arc`, `relay_tracker: Arc`, `dead_man_switch: Arc`, `signing_key: ed25519_dalek::SigningKey`
-- Init in `new()`, pass cache + tracker into listener via `MeshState`
-- Accessor methods for RPC layer
-
-**Dead Man background task** (spawned in `start()`):
-- Check every 60s: if triggered → build signed alert → broadcast on channel 0 + direct to emergency contacts
-- Persist `last_check_in_time` as unix timestamp on disk (survives restarts)
-
-### Week 10: RPC Endpoints
-
-**`api/rpc/mesh.rs`** — New handlers:
-
-| Endpoint | Params | Description |
-|----------|--------|-------------|
-| `mesh.relay-tx` | `{ tx_hex }` | Queue TX for relay via internet peer |
-| `mesh.block-headers` | `{ count? }` | Return cached block headers |
-| `mesh.relay-lightning` | `{ bolt11, amount_sats }` | Queue LN invoice for payment |
-| `mesh.deadman-status` | — | Query switch state |
-| `mesh.deadman-configure` | `{ enabled, interval_secs, lat, lng, contacts, custom_message }` | Configure |
-| `mesh.deadman-checkin` | — | Heartbeat reset |
-
-**Fix `mesh.send-invoice`:** Replace placeholder bolt11 with real LND `POST /v1/invoices` call.
-
-**`api/rpc/mod.rs`:** Register all new routes (~line 643).
-
-### Week 11: Block Header Announcer + Frontend
-
-**Backend:** Optional background task: poll Bitcoin Core `getblockchaininfo` every 30s → on new block → signed announcement → broadcast channel 0. Config: `announce_block_headers: bool`.
-
-**Frontend `stores/mesh.ts`:** New methods for all Phase 4 RPC calls.
-
-**Frontend `views/Mesh.vue`:**
-- "Off-Grid Bitcoin" panel: block height, headers, TX relay form, LN relay form
-- "Dead Man's Switch" panel: enable/disable, interval, GPS, contacts, countdown, check-in
-- Uses `.path-option-card`, `.glass-button`, `.info-card`
-
-## Phase 5: Mesh Network Intelligence (Weeks 12-15)
-
-### Week 12: Steganographic Modes
-
-**New: `mesh/steganography.rs`**
-
-- `SteganographyMode` enum: `Normal`, `WeatherStation`, `SensorNetwork`
-- **Weather Station:** Map payload bytes → plausible weather readings (temp, humidity, pressure, wind). Marker `0xAA` replaces `0x02`.
-- **Sensor Network:** Industrial sensor format (voltage, current, vibration)
-- `to_wire_steganographic(mode)` / `from_wire_steganographic(data)` on TypedEnvelope
-- Listener detects `0xAA` → decode stego → normal dispatch
-- Config: `steganography_mode` in `MeshConfig`
-- Budget: ~80 bytes real data per 160-byte LoRa frame with stego overhead
-
-### Week 13: Adaptive Routing & Signal Intelligence
-
-**New: `mesh/routing.rs`**
-
-- `LinkQuality` per peer: RSSI/SNR rolling 1h history, packet loss, hop count
-- `RoutingTable`: link quality per peer + best route per destination DID
-- Score: `(rssi+120)*0.4 + (snr+20)*0.3 + (1-loss)*100*0.3`
-- Best relay selection for TX/LN relay (highest quality peer with internet)
-- Multi-hop forwarding: if dest DID != ours and hops < 3, forward to best next-hop
-- Extract RSSI from v3 frames (bytes 1-2, currently unused)
-- RPC: `mesh.routing-table`
-
-### Week 14: LoRa Radio Parameter Control
-
-**`mesh/protocol.rs`:** Builders for `SET_RADIO_PARAMS` (0x0B), `SET_TX_POWER` (0x0C), `SET_TUNING_PARAMS` (0x15). Parse `RESP_STATS` (0x18).
-
-**RPC:** `mesh.set-radio-params`, `mesh.set-tx-power`, `mesh.get-radio-stats`
-
-**Auto-adaptive SF:** If link quality drops → increase spreading factor (longer range, slower). Config toggle.
-
-**Frontend:** Radio tuning panel with SF/TX power sliders, stats, auto-adaptive toggle.
-
-### Week 15: Multi-Hardware + Topology UI
-
-**New: `mesh/device_trait.rs`**
-
-```rust
-#[async_trait]
-pub trait MeshDevice: Send + Sync {
- async fn open(path: &str) -> Result where Self: Sized;
- async fn initialize(&mut self) -> Result;
- async fn send_text(&mut self, dest: &[u8; 6], msg: &[u8]) -> Result<()>;
- async fn try_recv_frame(&mut self) -> Result