# Overnight Plan — Archy Refactoring & App Integration Hardening > Make the Archy codebase rock-solid: fix all broken containers/iframes, perfect app installation/management/icons, get IndeedHub + Nostr signer flawless, and begin critical refactoring. No new features, no design changes. Bitcoin only. > See `docs/refactoring-plan.md` for the full 3-year plan. See `CLAUDE.md` for all project rules and conventions. > Deploy after every change: `./scripts/deploy-to-target.sh --live` — test at http://192.168.1.228 --- ## Phase 1: Fix App Icon Consistency - [x] **Fix PhotoPrism icon typo in backend metadata**: In `core/archipelago/src/container/docker_packages.rs`, the `get_app_metadata()` function references `photoprims.svg` (missing 'h') for the PhotoPrism icon. Search for `photoprims` and replace with `photoprism`. Verify the icon file exists at `neode-ui/public/assets/img/app-icons/photoprism.svg`. Run `cargo clippy --all-targets --all-features` in `core/` on the dev server after the fix. - [x] **Fix IndeedHub duplicate icon — consolidate to indeedhub.png**: Two icon files exist: `neode-ui/public/assets/img/app-icons/indeedhub.ico` and `indeehub.ico` (typo). Delete `indeehub.ico`. Convert `indeedhub.ico` to `indeedhub.png` (better format consistency). Update all references: (1) `neode-ui/src/utils/dummyApps.ts` line ~518 — change `indeehub.ico` to `indeedhub.png`, (2) `neode-ui/src/views/Marketplace.vue` line ~913 — change `indeehub.ico` to `indeedhub.png`, (3) `core/archipelago/src/container/docker_packages.rs` lines ~451-454 — change `indeehub.ico` to `indeedhub.png`. Search the entire codebase for `indeehub` (missing 'd') and fix all occurrences to `indeedhub`. Run `cd neode-ui && npm run type-check` to verify. - [x] **Audit all app icons match their references**: Cross-check every icon path referenced in `docker_packages.rs` `get_app_metadata()` against actual files in `neode-ui/public/assets/img/app-icons/`. Verify each app in the `Marketplace.vue` `getCuratedAppList()` function has an icon that exists. If any icon is missing, check if a similar-named file exists (e.g., wrong extension). Fix all mismatches. Remove orphaned icons that no app references (e.g., `atob.png`, `community-store.png`, `k484.png`, `lorabell.png`, `morphos.png` — verify they're truly unused first). Standardize: prefer `.png` or `.svg` over `.ico` and `.webp` where possible without changing existing working icons. --- ## Phase 2: Fix Container Crash Loops & Health - [x] **Diagnose and fix container networking DNS failures**: SSH to 192.168.1.228 (`sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228`). Run `sudo podman ps -a --format '{{.Names}} {{.Status}}' | grep -i restart` to identify containers in crash loops. The known issue is DNS resolution failures — containers can't resolve each other by name (e.g., mempool-web can't find mempool-api). Check if the `archy-net` Podman network exists: `sudo podman network ls`. If missing, create it: `sudo podman network create archy-net`. Reconnect all containers that need inter-container DNS to this network. Verify with `sudo podman exec archy-mempool-web ping mempool-api`. Restart affected containers and monitor for 2 minutes to confirm no more crash loops. - [x] **Fix .198 server swap and memory**: SSH to 192.168.1.198. Check current swap: `free -h`. If no swap configured, create a 4GB swap file: `sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile`. Add to `/etc/fstab`: `/swapfile none swap sw 0 0`. Verify with `free -h`. This prevents OOM kills that crash containers. - [x] **Stop and remove ollama container if not needed**: SSH to 192.168.1.228. Check ollama status: `sudo podman ps -a | grep ollama`. If it's in "Created" state and never started, remove it: `sudo podman rm ollama`. This frees a container slot and removes clutter from the app list. If the user has ollama in their installed apps, leave it but start it: `sudo podman start ollama`. - [x] **Verify all core Bitcoin containers are healthy**: SSH to 192.168.1.228. Check these containers are running and healthy: `bitcoin-knots`, `lnd`, `mempool-api`, `archy-mempool-web`, `mempool-electrs`, `btcpay-server`, `archy-nbxplorer`. Run `sudo podman ps --format '{{.Names}}\t{{.Status}}' | grep -E "(bitcoin|lnd|mempool|btcpay|nbxplorer|electrs)"`. For any that are not "Up", check logs: `sudo podman logs --tail 50 {container-name}`. Fix the root cause (usually missing network, wrong env var, or dependency not ready). After fixes, run `curl -s http://localhost:5678/health` to verify the Archy backend sees them all. --- ## Phase 3: Fix App Launching — Use Direct IP:Port URLs - [ ] **Fix AppSession.vue — use direct IP:port URLs instead of nginx proxy paths**: The root cause of most iframe issues is `AppSession.vue` lines 240-276. The `APP_URLS` map hardcodes every app to `/app/{id}/` nginx subpath proxies (e.g., `'/app/filebrowser/'`). This breaks apps because root-relative asset paths (like `/static/main.js`) resolve to the Archy root instead of the app. The fix: replace the hardcoded proxy paths with direct `IP:port` URLs. Change `APP_URLS` to map app IDs to their actual ports instead: ```typescript const APP_PORTS: Record = { 'bitcoin-knots': 8334, 'electrs': 50002, 'btcpay-server': 23000, 'lnd': 8081, 'mempool': 4080, 'homeassistant': 8123, 'grafana': 3000, 'searxng': 8888, 'ollama': 11434, 'onlyoffice': 9980, 'penpot': 9001, 'nextcloud': 8085, 'vaultwarden': 8082, 'jellyfin': 8096, 'photoprism': 2342, 'immich': 2283, 'filebrowser': 8083, 'nginx-proxy-manager': 81, 'portainer': 9000, 'uptime-kuma': 3001, 'tailscale': 8240, 'fedimint': 8175, 'nostr-rs-relay': 18081, 'indeedhub': 7777, 'dwn': 3100, 'endurain': 8080, } ``` Then compute the URL dynamically in `appUrl`: - **On HTTP**: `http://${window.location.hostname}:${port}` (direct, no proxy, assets work perfectly) - **On HTTPS**: `${window.location.origin}/app/${appId}/` (proxy needed to avoid mixed-content blocks) - **External sites**: keep direct HTTPS URLs as-is (botfights, nwnn, etc.) This matches what `appLauncher.ts` `toEmbeddableUrl()` already does (lines 70-96) but `AppSession.vue` was bypassing it. Keep the nginx proxy locations for HTTPS — they're still needed there. The `PORT_TO_PROXY` map in `appLauncher.ts` should also be updated to use the same `APP_PORTS` source of truth (import it, or move to a shared `src/data/appPorts.ts`). Run `cd neode-ui && npm run type-check`. - [ ] **For apps that block iframes — still open in new tab at IP:port**: Update `mustOpenInNewTab()` in `appLauncher.ts` to check against `APP_PORTS` rather than hardcoded port strings. In `AppSession.vue`, add the same check: if `mustOpenInNewTab(url)`, redirect to `window.open(url, '_blank')` instead of loading in iframe. The blocked apps (BTCPay 23000, Grafana 3000, Vaultwarden 8082, PhotoPrism 2342, Nextcloud 8085, Uptime Kuma 3001, Home Assistant 8123) should open at `http://192.168.1.228:{port}` in a new tab. Verify each blocked app actually needs blocking by checking headers: `ssh archipelago@192.168.1.228 'for port in 23000 3000 8082 2342 8085 3001 8123; do echo "Port $port:"; curl -sI http://localhost:$port/ | grep -i "x-frame"; done'`. Remove from blocked list if nginx `proxy_hide_header X-Frame-Options` is stripping the header successfully (in which case they CAN iframe). - [ ] **Remove unnecessary nginx sub_filter path rewriting**: With direct `IP:port` URLs on HTTP, the `sub_filter` rules in `image-recipe/configs/nginx-archipelago.conf` that rewrite asset paths (e.g., IndeedHub lines 334-367) are no longer needed for HTTP. Keep them for HTTPS proxy paths only. Review each `/app/{id}/` location block — the `proxy_hide_header X-Frame-Options` and `proxy_pass` are still needed for HTTPS, but `sub_filter` rules that rewrite `/static/` or `/_next/` paths are only needed in HTTPS mode. This simplifies the nginx config significantly. Test: load each app at `http://192.168.1.228:{port}` directly in a browser — all assets should load without any nginx intervention. - [ ] **Inject nostr-provider.js for IndeedHub at IP:port**: When apps load at direct `IP:port` URLs (not through nginx proxy), nginx can't inject `nostr-provider.js` via `sub_filter`. Instead, the `AppSession.vue` iframe wrapper must inject it. In `AppSession.vue`, after the iframe loads, use `iframe.contentWindow.postMessage` to send a script injection request, OR — simpler — add a `` into the HTML response. If missing, add: `sub_filter '' '';` with `sub_filter_once on;` and `sub_filter_types text/html;`. Sync nginx config to server and reload. Verify by loading IndeedHub in the Archy iframe and checking browser dev tools console for `window.nostr` availability — run `JSON.stringify(Object.keys(window.nostr))` in the iframe console, should show `["getPublicKey","signEvent","getRelays","nip04","nip44"]`. - [x] **Test full NIP-07 signing flow with IndeedHub**: Open Archy at `http://192.168.1.228`, go to Apps, click IndeedHub. Expected flow: (1) NostrIdentityPicker modal appears on first launch asking which identity to use, (2) select an identity with a Nostr key, (3) IndeedHub loads in iframe, (4) when IndeedHub requests `window.nostr.getPublicKey()`, the Archy parent responds with the selected identity's Nostr pubkey, (5) when IndeedHub requests `window.nostr.signEvent(event)`, NostrSignConsent modal appears, (6) user approves, event is signed via `identity.nostr-sign` RPC, (7) signed event returned to IndeedHub. Test each step. If NostrIdentityPicker doesn't show, check `AppSession.vue` line ~302-304 `isIdentityAwareApp()` includes 'indeedhub'. If signing fails, check RPC logs: `ssh archipelago@192.168.1.228 'sudo journalctl -u archipelago --since "5 min ago" | grep -i nostr'`. - [x] **Ensure IndeedHub content loads fully — all pages, media, navigation**: After the Nostr flow works, navigate through IndeedHub's content inside the iframe. Check: (1) all pages/routes load (no blank screens), (2) media content (videos, images) loads, (3) navigation within IndeedHub works without breaking the iframe, (4) no console errors related to CORS, mixed content, or CSP. If videos don't load, check if the video hosting domain is blocked by CSP headers — may need to add `Content-Security-Policy` adjustments in the nginx location block. If internal navigation causes the iframe to navigate to a bare URL (not under `/app/indeedhub/`), add `sub_filter` rules to rewrite the app's internal links. - [x] **Test NIP-04 and NIP-44 encryption/decryption**: In IndeedHub (or manually via browser console in the iframe), test the encryption methods: (1) `window.nostr.nip04.encrypt(somePubkey, "test message")` — should return ciphertext, (2) `window.nostr.nip04.decrypt(somePubkey, ciphertext)` — should return "test message", (3) same for `nip44.encrypt` and `nip44.decrypt`. If any fail, check RPC handlers in `core/archipelago/src/api/rpc/identity.rs` — the `handle_identity_nostr_encrypt_nip04/nip44` and decrypt handlers (lines 428-496). Check that the identity manager has the required keys. --- ## Phase 5: App Installation & Management Polish - [x] **Verify install flow for every Bitcoin-related marketplace app**: In the Archy UI at `http://192.168.1.228`, go to Marketplace. For each Bitcoin-related app (Bitcoin Knots, LND, Mempool, BTCPay, Electrs, Fedimint), click through to the detail page. Verify: (1) icon loads correctly (not fallback logo), (2) description is accurate, (3) "Install" button appears if not installed, (4) dependency warnings show correctly (Mempool requires Bitcoin Knots + Electrs, BTCPay requires Bitcoin Knots), (5) if already installed, status shows correctly. Fix any issues found in `neode-ui/src/views/MarketplaceAppDetails.vue`. Note: Archy is Bitcoin only — remove any Monero or Liquid entries from `Marketplace.vue` `getCuratedAppList()` if present. - [x] **Remove non-Bitcoin altcoin entries from marketplace**: Search `neode-ui/src/views/Marketplace.vue` for "monero", "liquid", "litecoin", or any non-Bitcoin cryptocurrency entries in the `getCuratedAppList()` function. Remove them entirely. Archy is a Bitcoin-only platform. Run `cd neode-ui && npm run type-check` after changes. - [x] **Fix dependency checks — frontend must match backend**: In `neode-ui/src/views/MarketplaceAppDetails.vue`, find the hardcoded dependency definitions (around lines 447-456). Cross-reference with `core/archipelago/src/api/rpc/package.rs` lines 64-96 where backend dependency checks are defined. Ensure they match exactly. If backend checks for `has_bitcoin` before installing `electrs`, the frontend dependency list for `electrs` must show `bitcoin-knots` as a prerequisite. Update the frontend to match the backend. Ideally, add an RPC method `package.get-dependencies` that returns the dependency list from the backend, and have the frontend call it instead of hardcoding — but for now, just make the hardcoded lists match. - [x] **Verify start/stop/restart works for all installed apps**: In the Archy UI, go to Apps. For each installed app, test: (1) click Stop — container stops, UI updates to "Stopped" state, (2) click Start — container starts, UI updates to "Running" state with health indicator, (3) click the app — it launches (iframe or new tab as appropriate). Check that the container store (`neode-ui/src/stores/container.ts`) correctly polls for status changes after start/stop actions. If status doesn't update, check the WebSocket state broadcasting in `core/archipelago/src/state.rs`. - [x] **Fix route-to-package-key mapping divergence**: In `neode-ui/src/views/AppDetails.vue` lines 501-529, the route ID to backend container name mapping is hardcoded. Verify every mapping is correct by checking actual container names on the server: `ssh archipelago@192.168.1.228 'sudo podman ps --format "{{.Names}}"'`. Fix any mismatches. Known issues: `mempool` maps to `mempool-web` but backend may use `archy-mempool-web`. Check `electrs` maps to `mempool-electrs` or `archy-electrs`. Run `cd neode-ui && npm run type-check` after changes. --- ## Phase 6: Backend Critical Fixes - [x] **Fix session TTL clock bug — use SystemTime instead of Instant**: Read `core/archipelago/src/session.rs`. Find where `Instant::now()` is used for session TTL/expiry (around line 97). `Instant` is monotonic but can drift on sleep/hibernate — common on NUC/Pi hardware. Replace with `SystemTime::now()` for absolute time comparison. The `FULL_SESSION_TTL` (24 hours) and `PENDING_TOTP_TTL` (5 minutes) checks should use `SystemTime::elapsed()` or store `SystemTime` timestamps and compare with `SystemTime::now()`. Run `cargo test --all-features` in `core/` on the dev server. - [x] **Enforce RBAC in RPC handler**: Read `core/archipelago/src/auth.rs` — find the `UserRole` enum and `can_access()` method. Then read `core/archipelago/src/api/rpc/mod.rs` — find where authenticated requests are dispatched to handlers. Add a role check before dispatching: after validating the session, get the user's role, call `role.can_access(method_name)`, and return an authorization error if denied. For now, all users created via onboarding should default to `Admin` role (single-user system), but this lays the groundwork for multi-user. Run `cargo clippy --all-targets --all-features && cargo test --all-features` on the dev server. - [x] **Remove dead code and #[allow(dead_code)]**: Search `core/` for all `#[allow(dead_code)]` and `#[allow(unused)]` annotations. For each: (1) if the code is genuinely unused and not part of a planned feature, delete it, (2) if it should be used (like RBAC — now wired up in previous task), remove the allow annotation. Key file: `core/archipelago/src/auth.rs` lines ~70, 83, 88. Run `cargo clippy --all-targets --all-features` to verify no new warnings. - [x] **Deploy and verify backend fixes**: Run `./scripts/deploy-to-target.sh --live`. After deploy: (1) verify login still works at `http://192.168.1.228` (password: `password123`), (2) verify session persists after navigating between pages, (3) check logs for any new errors: `ssh archipelago@192.168.1.228 'sudo journalctl -u archipelago --since "2 min ago" | grep -i error'`. --- ## Phase 7: Frontend Cleanup - [x] **Remove dead dockerode dependency**: Run `cd /Users/dorian/Projects/archy/neode-ui && npm uninstall dockerode` and `npm uninstall @types/dockerode` if it exists. Search the codebase for any remaining imports: `grep -r "dockerode" neode-ui/src/`. Remove any dead imports found. Run `npm run type-check` to verify nothing breaks. - [x] **Fix the 10 failing frontend tests**: Run `cd /Users/dorian/Projects/archy/neode-ui && npm run test -- --reporter=verbose 2>&1 | head -100` to see which tests fail. Known failures: (1) `src/stores/__tests__/appLauncher.test.ts` — URL rewriting tests expecting different proxy behavior, (2) `src/views/__tests__/settings.test.ts` — heading selector `h1` not finding the heading element. For each failing test, read the test file and the component/store it tests. Update test expectations to match current implementation. Do NOT change the production code to match tests — fix the tests. Run `npm run test` until all pass. - [x] **Add 404 catch-all route**: In `neode-ui/src/router/index.ts`, add a catch-all route at the end of the routes array: `{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFound.vue') }`. Create `neode-ui/src/views/NotFound.vue` — a simple view using the existing `.glass-card` class with "Page not found" message and a router-link back to `/dashboard`. Use `