docs: map existing codebase

This commit is contained in:
archipelago 2026-07-29 10:25:54 -04:00
parent bea7f24a4f
commit 9e77a4229c
7 changed files with 1981 additions and 0 deletions

View File

@ -0,0 +1,333 @@
<!-- refreshed: 2026-07-29 -->
# Architecture
**Analysis Date:** 2026-07-29
## System Overview
```text
┌────────────────────────────────────────────────────────────────┐
│ Frontend Layer (Vue 3) │
`neode-ui/src` (TypeScript + SPA) │
│ Routes → Views → Components → Composables → RPC Client │
└────────────────┬─────────────────────────────────────────────┘
│ WebSocket + HTTP(S)
│ JSON-RPC 2.0 protocol
┌────────────────────────────────────────────────────────────────┐
│ HTTP Server Layer (Hyper) │
`core/archipelago/src/server.rs`
│ TCP Listener → Hyper → Router → ApiHandler/RpcHandler │
└────────────────┬─────────────────────────────────────────────┘
┌──────────┴──────────┬──────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌────────────┐
│ WebSocket │ RPC │ │ Content │
│ Handler │ Handler │ │ Proxy │
│ (state sync) │ (methods)│ │ (app URIs) │
└─────────┘ └──────────┘ └────────────┘
│ │
└──────────┬───────┘
┌─────────────────────────────────────┐
│ Service Layer (Async Tasks) │
`core/archipelago/src/api/rpc/*`
│ │
│ • auth, identity, secrets │
│ • container orchestration │
│ • bitcoin, lightning, wallet │
│ • mesh, federation, FIPS │
│ • content, backup, settings │
└─────────────┬───────────────────────┘
┌───────────┼───────────┬──────────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌────────┐ ┌──────────┐
│Container│ │ State │ │BlobStore
│Orch. │ │Manager │ │ │ │Identity │
│(Podman) │ │(Broadcast
│ │ │ channels)│ │ ContentClient Manager │
└─────────┘ └──────────┘ └────────┘ └──────────┘
│ │ │ │
└───────────┼───────────┼─────────┘
┌─────────────────────────────────────┐
│ Persistent Storage Layer │
│ │
│ • Data directory files (YAML/JSON) │
│ • SQLite (session store) │
│ • Blob store (content-addressed) │
│ • Podman container state │
│ • Secret vaults (encrypted) │
└─────────────────────────────────────┘
```
## Component Responsibilities
| Component | Responsibility | File |
|-----------|----------------|------|
| **Server** | HTTP listener, connection multiplexing, TLS/encryption | `core/archipelago/src/server.rs` |
| **ApiHandler** | HTTP request routing, authentication, response formatting | `core/archipelago/src/api/handler/mod.rs` |
| **RpcHandler** | JSON-RPC 2.0 dispatch, method registration, rate limiting | `core/archipelago/src/api/rpc/mod.rs` |
| **ContainerOrchestrator** | Podman lifecycle, manifest reconciliation, adoption | `core/archipelago/src/container/prod_orchestrator.rs` |
| **StateManager** | Central state broadcast channel, revision tracking | `core/archipelago/src/state.rs` |
| **AuthManager** | User credentials, session validation, password hashing | `core/archipelago/src/auth.rs` |
| **Identity Manager** | Node Ed25519 keys, seed derivation, Tor address | `core/archipelago/src/identity_manager.rs` |
| **BootReconciler** | Periodic manifest sync loop, adoption, remediation | `core/archipelago/src/container/boot_reconciler.rs` |
| **Frontend Router** | Vue Router, page navigation, deep linking | `neode-ui/src/router/index.ts` |
| **Frontend Stores** | Pinia state (apps, settings, user, mesh) | `neode-ui/src/stores/` |
| **Frontend Components** | UI elements, modals, cards, layout primitives | `neode-ui/src/components/` |
## Pattern Overview
**Overall:** Multi-tier async architecture with centralized request dispatch and broadcast state synchronization.
**Key Characteristics:**
- **Async-first (Tokio)** - All I/O operations are non-blocking; task spawning for background work
- **RPC-driven API** - Frontend communicates via JSON-RPC 2.0 (not REST); single `/api/v0` WebSocket + HTTP endpoint
- **State as broadcast** - Global state changes flow through Tokio broadcast channels to all connected WebSocket clients
- **Manifest-driven containers** - App lifecycle controlled by declarative YAML manifests (Archipelago-specific extensions)
- **Plugin architecture** - Apps are isolated Podman containers with declarative interfaces (web UI, ports, secrets)
## Layers
**HTTP/Transport Layer:**
- Purpose: Accept inbound connections, handle TLS termination, demultiplex HTTP/WebSocket
- Location: `core/archipelago/src/server.rs`
- Contains: Hyper listener, TCP accept loop, connection state tracking
- Depends on: Tokio, Hyper, TLS/mTLS libraries (rustls/openssl)
- Used by: All external clients (web UI, companion app, API consumers)
**Request Routing & Auth Layer:**
- Purpose: Dispatch HTTP requests to handlers, validate sessions, enforce CSRF, rate-limit login
- Location: `core/archipelago/src/api/` (handler + rpc submodules)
- Contains: Route matching, middleware chain, cookie extraction, error formatting
- Depends on: Server, StateManager, SessionStore
- Used by: All request paths; gates API access
**RPC Dispatch Layer:**
- Purpose: Deserialize JSON-RPC 2.0 requests, call appropriate service method, serialize responses
- Location: `core/archipelago/src/api/rpc/mod.rs` + subdirectories (auth.rs, container.rs, bitcoin.rs, etc.)
- Contains: Method table, parameter validation, response formatting, rate limit checks
- Depends on: All service modules
- Used by: Frontend (WebSocket + HTTP POST to /api/v0), internal tools
**Service Layer:**
- Purpose: Implement business logic — container lifecycle, identity, auth, content sync, mesh discovery
- Location: `core/archipelago/src/api/rpc/*` (one RPC module per domain), plus `core/archipelago/src/` (background tasks)
- Contains: ~40 RPC method modules + 50+ core service modules (bootstrap.rs, health_monitor.rs, crash_recovery.rs, etc.)
- Depends on: StateManager, ContainerOrchestrator, config/secrets, external services (Bitcoin, Lightning, FIPS)
- Used by: RPC layer; other services for cross-cutting concerns (mesh, federation, webhooks)
**State Management Layer:**
- Purpose: Hold canonical application state, broadcast changes to all connected clients, persist snapshots
- Location: `core/archipelago/src/state.rs` (StateManager + data_model.rs)
- Contains: RwLock<DataModel>, broadcast channel, revision counter
- Depends on: DataModel (serde-serializable struct tree)
- Used by: All services that mutate state (container ops, auth, settings)
**Container Orchestration Layer:**
- Purpose: Podman lifecycle management, image verification, secret injection, crash recovery, adoption
- Location: `core/archipelago/src/container/prod_orchestrator.rs` (1M+ lines; split across boot_reconciler.rs, quadlet.rs, docker_packages.rs, etc.)
- Contains: Manifest parsing, image pull/verify, container create/start/stop, volume mounts, networking
- Depends on: Podman CLI + socket, config parser, image registries, local filesystem
- Used by: RPC container.* methods, BootReconciler loop, crash recovery
**Frontend Layer (Vue 3):**
- Purpose: Render UI, dispatch RPC calls, maintain local UI state, handle user input
- Location: `neode-ui/src/`
- Contains: Views (pages), Components (reusable UI), Composables (logic hooks), Stores (Pinia), Router
- Depends on: Vue 3, Vue Router, Pinia, RPC client library (custom), D3/Leaflet (charts/maps)
- Used by: Browser clients (desktop, mobile, companion app via WebView)
## Data Flow
### Primary Request Path (User Action → Backend → State Sync)
1. **Frontend user interaction** (click button, type input) → Vue component event handler
- Location: `neode-ui/src/views/*.vue` or `neode-ui/src/components/*.vue`
2. **Composable dispatches RPC** (e.g., `useContainerInstall()` calls `rpc.container.install()`)
- Location: `neode-ui/src/composables/` (custom or imported from `api/rpc-client.ts`)
3. **RPC client serializes → HTTP/WebSocket POST to /api/v0**
- Location: `neode-ui/src/api/rpc-client.ts`
- Payload: `{ jsonrpc: "2.0", method: "container.install", params: {...}, id: ... }`
4. **HTTP Server receives, routes to ApiHandler**
- Location: `core/archipelago/src/server.rs` (listener) → `core/archipelago/src/api/handler/mod.rs` (dispatch)
5. **ApiHandler checks auth**, extracts body, calls RpcHandler
- Location: `core/archipelago/src/api/handler/mod.rs:handle_request()`
6. **RpcHandler dispatches by method name** to specific RPC module
- Location: `core/archipelago/src/api/rpc/mod.rs:call()` → routing to `core/archipelago/src/api/rpc/container.rs:install()`
7. **Service method executes** (e.g., `container.rs:install()` calls orchestrator, updates state)
- Location: `core/archipelago/src/api/rpc/container.rs` (calls methods on ContainerOrchestrator)
8. **StateManager.update_data()** broadcasts the new state to all WebSocket subscribers
- Location: `core/archipelago/src/state.rs:update_data()` → broadcast channel
- All connected WebSocket clients receive `{ rev: N, data: {...} }` update
9. **Frontend receives state update**, updates Pinia stores, re-renders UI
- Location: `neode-ui/src/stores/` (Pinia stores mutate) → Vue reactivity chain → DOM update
**State Management:**
- All reads from `StateManager` go through `get_snapshot()` which acquires read-lock
- All writes go through `update_data()` which acquires write-lock + increments revision
- Broadcast channel has ~100-message buffer; slow subscribers may lose old updates (by design — UI only needs latest)
- WebSocket clients re-sync on reconnect via `get_snapshot()` call (full state transfer)
### Secondary Flow: Scheduled Reconciliation (Convergence Loop)
1. **BootReconciler spawned at startup** in `main.rs`
- Location: `core/archipelago/src/main.rs` (line ~338-348)
2. **Reconciler runs every `RECONCILER_DEFAULT_INTERVAL`** (~30s typical)
- Location: `core/archipelago/src/container/boot_reconciler.rs:run_forever()`
3. **Compares desired manifests (disk + registry catalog) vs actual Podman state**
- Looks for: containers missing, containers orphaned, image updates, secret changes
4. **Applies remediation** (create, delete, restart containers)
- Calls: orchestrator.reconcile_*() methods
5. **Logs changes, broadcasts state update if anything changed**
- Frontend receives update, shows user the reconciled app state
This ensures apps survive crashes, OTA updates, or manual Podman edits — the desired state always converges.
## Key Abstractions
**ContainerOrchestrator trait:**
- Purpose: Abstract container lifecycle behind a trait so Prod (Podman-based) and Dev (in-memory) modes can coexist
- Examples: `core/archipelago/src/container/prod_orchestrator.rs`, `core/archipelago/src/container/dev_orchestrator.rs`
- Pattern: Trait-based strategy; RpcHandler holds `Arc<dyn ContainerOrchestrator>`, switches at runtime
- Methods: create, start, stop, delete, adopt, list, reconcile, install, upgrade
**Manifest (YAML-based declarative app):**
- Purpose: Fully describe an app's container, dependencies, secrets, ports, UI in one file
- Examples: `/opt/archipelago/apps/*/manifest.yml` (on-disk) or registry-delivered catalogs
- Pattern: Custom extensions over OCI/Docker Compose (e.g., `interfaces.main.ui`, `generated_secrets`)
- Parsed into: `container::manifest::Manifest` struct, consumed by orchestrator
**RPC Method Modules:**
- Purpose: Group related JSON-RPC methods by domain (auth, container, bitcoin, mesh, etc.)
- Examples: `core/archipelago/src/api/rpc/auth.rs`, `core/archipelago/src/api/rpc/bitcoin.rs`
- Pattern: Each module exports `pub async fn method_name(handler, params) -> Result<Response>`
- Registration: Hardcoded dispatch in `RpcHandler::call()` (no reflection; methods are explicit)
**BlobStore (Content-Addressed):**
- Purpose: Store attachments/files by SHA-256 hash; issue time-limited capability tokens for access
- Examples: Used by mesh.send-content, federation attachments, backup archives
- Pattern: Capability-based access control (CBAC); tokens scoped to issuer pubkey + hash
- Located: `core/archipelago/src/blobs.rs` + `core/archipelago/src/content_server.rs`
**StateManager + DataModel:**
- Purpose: Single source of truth for UI state; broadcast updates to all clients
- Pattern: Read-write lock over a serde-serializable struct tree; broadcast channel for efficiency
- Persistence: Most state is ephemeral (app listings, UI settings); durable state persists to disk separately
- Clients: Frontend (WebSocket subscriber), internal services (read via get_snapshot), monitoring/debug
**Session Store:**
- Purpose: Track authenticated HTTP sessions (cookie → user identity mapping)
- Examples: SQLite-backed or in-memory store
- Pattern: Session token issued at login, validated on each request, expires after TTL
- Used by: ApiHandler auth check, rate limiter (per IP + per user)
## Entry Points
**Backend Daemon (Binary):**
- Location: `core/archipelago/src/main.rs`
- Triggers: `systemd start archipelago.service` or manual `./archipelago` on development node
- Responsibilities: Parse config, init tracing, load/reconcile containers, start HTTP server, spawn background tasks
- Key setup: Load identity → setup auth → spawn orchestrator → load manifests → start reconciler → start server
**Frontend SPA:**
- Location: `neode-ui/src/main.ts`
- Triggers: Browser loads `/index.html` (served by HTTP server from `/opt/archipelago/web-ui/`)
- Responsibilities: Boot Vue app, setup Router, setup Pinia stores, establish WebSocket to backend
- Key setup: Mount app → router ready → fetch initial state → subscribe to updates
**RPC Endpoints (HTTP + WebSocket):**
- Location: `core/archipelago/src/api/` (handler routes requests here)
- Endpoint: `/api/v0` (JSON-RPC 2.0 POST or WebSocket upgrade)
- Methods: ~200+ RPCs across domains (auth, container, bitcoin, mesh, federation, etc.)
- Example: `POST /api/v0` with body `{"jsonrpc": "2.0", "method": "auth.login", "params": {...}, "id": 1}`
**Background Tasks (Spawned at startup):**
- BootReconciler: Periodic manifest reconciliation loop
- Health Monitor: Periodic app health checks + restart
- Update Scheduler: Periodic app update checks
- Mesh Service: P2P mesh listener + sender (federation, LoRa)
- Webhook Relay: Listens for inbound webhooks, broadcasts to subscribers
- WebSocket Listener: Upgraded HTTP connections → broadcast state subscriber
- See: `core/archipelago/src/main.rs` (lines ~400-450 show the spawned tasks)
## Architectural Constraints
- **Single event loop** — All I/O-bound work runs on a single Tokio multi-threaded runtime; no worker threads by default (some container ops are blocking, run in tokio::task::spawn_blocking)
- **Global state via broadcast** — StateManager broadcasts to all WebSocket clients; no request-response for state changes (async by design)
- **Container state mutability** — Podman state can drift from manifest (manual edits, crashes); reconciler runs periodically to converge
- **No in-process data consistency** — Multiple services can mutate StateManager concurrently; last write wins (fine for UI; critical ops use locks)
- **Shared blob store** — All services that need to share content use the same BlobStore instance (single cap_key, single root directory)
- **Rate limiting per IP + method** — Prevents brute-force login, but shared IPs see shared limits (edge case: family users, proxies)
- **Session cookie same-site** — WebSocket + HTTP POST must be same-origin; CORS headers controlled by ApiHandler
## Anti-Patterns
### Circular RPC Dispatches
**What happens:** An RPC method calls back into another RPC method, forming a cycle (e.g., auth.login → container.list → auth.check_permission → auth.login)
**Why it's wrong:** Deadlocks on RwLocks, infinite loops on state broadcasts, unclear error messages, hard to debug
**Do this instead:** Pass check result as a side-effect from the outer method; compute permissions once at the start. Use composable patterns in frontend instead (e.g., `useCanInstall()` checks perms once per component mount).
### Synchronous blocking in RPC handlers
**What happens:** RPC method calls `.unwrap()` on Podman command result, blocking the entire event loop
**Why it's wrong:** One slow container op (e.g., large image pull) blocks all concurrent users
**Do this instead:** Use `tokio::task::spawn_blocking()` for I/O that may take >100ms. See `core/archipelago/src/container/docker_packages.rs` for examples.
### Hardcoding paths in app RPC modules
**What happens:** `bitcoin.rs` hardcodes `/opt/archipelago/data/bitcoin.conf` instead of using `config.data_dir`
**Why it's wrong:** Dev mode, tests, and alternate installs all fail with "not found"
**Do this instead:** Read from `Config` struct, which is passed to every RPC method. See `core/archipelago/src/api/rpc/bitcoin.rs:status()` for correct pattern.
### Frontend state outside Pinia stores
**What happens:** Components use component-local ref<> for app list, duplicate the StateManager's data
**Why it's wrong:** Stale data after OTA updates, inconsistent with other users on the same node, race conditions on install/uninstall
**Do this instead:** Always derive from Pinia stores (e.g., `useAppStore().apps`). Stores subscribe to WebSocket updates. See `neode-ui/src/stores/appStore.ts`.
### Not handling WebSocket reconnection
**What happens:** Frontend goes offline for 10s (network glitch), WebSocket closes, frontend doesn't re-sync state
**Why it's wrong:** UI shows stale data (app still "installing" when actually done), user clicks again, double-action happens
**Do this instead:** WebSocket reconnect handler should re-fetch full state (`node.status`, etc.), re-subscribe. See `neode-ui/src/api/rpc-client.ts` for the reconnect loop.
## Error Handling
**Strategy:** Defensive layering — errors are caught at each tier, logged, and converted to user-facing messages.
**Patterns:**
- HTTP layer: 4xx/5xx with JSON error (no 500s for logic errors; only for crashes)
- RPC layer: Serialize error as `{ error: { code: N, message: "...", data: {...} } }` per JSON-RPC spec
- Service layer: Use `anyhow::Result<T>` + `?` operator for early exit; convert to `RpcError` at handler boundary
- Frontend: Catch RPC errors, show toast/modal, log to console (never crash the app)
**Critical paths:**
- Auth failure: 401 Unauthorized + "Invalid password" (no "user not found" to leak usernames)
- Container ops: If reconciler sees drift, logs it but continues (never crashes the daemon)
- Image pull failure: Fallback to last-cached version if network timeout (user is never blocked on external registries)
- Podman socket unavailable: Return 503 Service Unavailable (user sees "Archipelago is starting")
---
*Architecture analysis: 2026-07-29*

View File

@ -0,0 +1,195 @@
# Codebase Concerns
**Analysis Date:** 2026-07-29
## Tech Debt
**Federation node removal tombstone gap:**
- Issue: `federation::remove_node()` (`core/archipelago/src/federation/storage.rs:180-197`) calls `tombstone_did()` at line 193 but explicitly drops the error with `let _ = …`. If tombstone write fails (disk I/O, permission, transient), the peer is removed from `nodes.json` but never actually recorded as removed, so the next background sync/notify-join silently re-adds it.
- Files: `core/archipelago/src/federation/storage.rs:180-197`, `core/archipelago/src/api/rpc/federation/handlers.rs:272-300`
- Impact: Federation peers marked for removal can reappear after the next sync cycle, confusing the operator and potentially re-establishing unwanted connections.
- Fix approach: Surface the tombstone-write failure instead of swallowing it; consider retry logic with backoff; add integration test via `tests/multinode/smoke.sh` to verify removal sticks across sync cycles.
**Container reconciler observability gap:**
- Issue: No metrics distinguish "settling after restart" from "flapping" — container thrashing is invisible until anecdotal reports. No per-app restart counter or log line when an app restarts >N times in M minutes.
- Files: `core/archipelago/src/container/prod_orchestrator.rs` (reconciler loop), `core/archipelago/src/health_monitor.rs`
- Impact: Silent restart storms go unnoticed; users see frequent service interruptions without diagnostics; operator can't distinguish normal convergence from a crash loop.
- Fix approach: Add per-app restart counter + log line when threshold exceeded; emit metric on each restart; wire restart count into health/status RPC output.
**Failed systemd unit self-healing gap:**
- Issue: When a Quadlet-backed app's `.service` unit enters `failed` state (e.g., exit 255), the reconciler does not automatically `reset-failed` + `start` it. The unit sits failed until the operator manually intervenes or the service restarts.
- Files: `core/archipelago/src/container/prod_orchestrator.rs` (reconcile loop)
- Impact: Apps with transient failures go down and stay down; no automatic recovery; operator must manually reset or restart the orchestrator.
- Fix approach: Add reconcile step: quadlet-backed app whose `.service` is `failed` and not user-stopped → call `systemctl --user reset-failed <unit>` + `start`; add backoff to avoid busy-loop on persistent failures.
**Bitcoin RPC credentials not retrieved from config/secrets:**
- Issue: `core/container/src/bitcoin_simulator.rs:158` has a TODO marking hardcoded (or missing) RPC credentials in the Bitcoin simulator real-mode path. Credentials should be fetched from the secret store.
- Files: `core/container/src/bitcoin_simulator.rs:155-165`
- Impact: Bitcoin simulator in real mode (Testnet/Mainnet) cannot authenticate to the node; RPC calls fail.
- Fix approach: Inject `SecretsProvider` into `BitcoinSimulator::new()` or pass credentials as constructor args; fetch via `config/secrets` at runtime; handle credential rotation.
**Container security policies not wired in:**
- Issue: `core/security/src/container_policies.rs` generates AppArmor/SELinux profiles but the `apply_profile()` function has a TODO at line 71: "Configure Podman to use the profile" — the profiles are generated but never applied to running containers.
- Files: `core/security/src/container_policies.rs:63-75`
- Impact: Security profiles exist but provide zero protection; containers run without the intended isolation constraints.
- Fix approach: Pass `--security-opt apparmor=<profile>` (or SELinux equivalent) to Podman at container creation; verify profile loads via `apparmor_status`; add CI check that profiles compile cleanly.
**Dynamic resource adjustment not implemented:**
- Issue: `core/performance/src/resource_manager.rs:86` has a TODO for dynamic resource adjustment based on usage. The allocator is static; no adaptive rebalancing when load patterns shift.
- Files: `core/performance/src/resource_manager.rs:86-88`
- Impact: Resource allocation is rigid; a node with skewed usage (e.g., one app consuming all memory) has no mechanism to rebalance dynamically.
- Fix approach: Monitor per-app resource usage via cgroup stats; implement feedback loop to adjust limits; gate on production deployment (likely Phase 3+).
## Known Bugs
**Multinode RPC robustness gap:**
- Symptoms: The `node_rpc()` function in `tests/multinode/lib/multinode.bash` lacks `--max-time` on curl calls — a slow server-side RPC can hang the test suite indefinitely with zero feedback.
- Files: `tests/multinode/lib/multinode.bash` (exact line TBD; see grep for `node_rpc`)
- Trigger: Run multinode federation/mesh test against a slow or overloaded node; curl will block forever.
- Workaround: Manually kill the test process and diagnose the hanging RPC manually; no automatic timeout recovery.
- Fix approach: Add `--max-time 30` to all curl calls in `node_rpc()`; re-run `tests/multinode/smoke.sh` to verify.
## Security Considerations
**Secrets environment variable exposure risk:**
- Risk: Bitcoin and other service credentials are materialized as env vars in `ARCHIPELAGO_*` (e.g., `BITCOIN_RPC_PASSWORD`). Env vars are visible via `/proc/<pid>/environ` and potentially logged.
- Files: `core/archipelago/src/container/prod_orchestrator.rs`, `core/container/src/manifest.rs`, `core/archipelago/src/api/rpc/package/config.rs`
- Current mitigation: Secrets are declared as `generated_secrets` in manifests and materialized 0600/rootless; the orchestrator avoids logging values.
- Recommendations: Audit all env-var passing to containers; consider switching high-sensitivity secrets (bitcoin RPC, LND macaroons) to file-based secrets mounted read-only; add audit logging for secret access.
**Federation DID validation incomplete:**
- Risk: Federation peer DIDs are added via the RPC without cryptographic verification of ownership. A compromised peer could advertise arbitrary DIDs.
- Files: `core/archipelago/src/api/rpc/federation/handlers.rs` (add-node path), `core/archipelago/src/federation/storage.rs`
- Current mitigation: DIDs are stored locally; transitive federation discovery uses the tombstone list to block removed peers.
- Recommendations: Add DID-ownership proof (e.g., signed proof-of-identity) before accepting a peer's advertised DID; document the trust model; consider user warnings when adding peers.
**AppArmor profiles overly permissive:**
- Risk: Generated AppArmor profiles use blanket `network,` instead of per-port/protocol rules. Readonly flag is checkbox only, not enforced per actual app needs.
- Files: `core/security/src/container_policies.rs:46-54`
- Current mitigation: None (profiles not applied).
- Recommendations: Refine per-app capabilities based on manifest's declared needs; add integration test verifying readonly mounts are enforced; apply profiles in development before prod.
## Performance Bottlenecks
**Container thrashing during reconcile:**
- Problem: Restarting `archipelago.service` SIGKILLs every container, forcing a full rebuild over several minutes. Uninstall + reinstall loops can cascade-trigger restarts.
- Files: `core/archipelago/src/container/prod_orchestrator.rs` (the reconciler's desired-state machine)
- Cause: Pre-Phase-3 architecture: containers run in systemd cgroup, not as independent Quadlet units.
- Improvement path: Phase-3 Quadlet default-flip (`config.rs:256`) — each app becomes an independent `.container` unit; restart only the affected app, not the entire cgroup.
**Reconciler churn on boot:**
- Problem: Boot reconciler makes multiple passes reconciling drift; during each pass, containers may be recreated. Post-OTA health checks deliberately skip per-app container assertions because of restart-storm unpredictability.
- Files: `core/archipelago/src/container/prod_orchestrator.rs`, `core/archipelago/src/bootstrap.rs`
- Cause: Multi-pass reconciliation + no incremental diff detection.
- Improvement path: Consolidate reconciler into single pass for boot; cache manifest/config diffs to avoid redundant comparisons; add boot-only fast-path.
**Bitcoin IBD on .198 stalled (disk I/O):**
- Problem: .198 bitcoin is mid-IBD with only 21% progress; disk is 448GB (below 1TB archival threshold); load is high (~35).
- Files: `tests/multinode-testing-plan.md` (documented issue)
- Cause: Undersized/slow disk; concurrent workload.
- Improvement path: User decision required: swap in a different node (already done for gate run, using .5 instead) or add storage + wait for sync. Not a code issue.
## Fragile Areas
**Uninstall + reinstall lifecycle:**
- Files: `core/archipelago/src/api/rpc/package/install.rs`, `core/archipelago/src/container/quadlet.rs:disable_remove()`, `neode-ui/src/components/AppCard.vue`
- Why fragile: Pre-2026-07-26, `quadlet::disable_remove()` called systemd + podman with no timeouts, causing hangs. Fixed by commit `71cc9ac4` (added `QUADLET_STOP_TIMEOUT`, SIGKILL escalation, reset-failed). AppCard was hardcoding uninstall bar to "stuck full-red" (fixed `9f17ba68`). Tests for reinstall/cascade are still opt-in.
- Safe modification: Any changes to the uninstall path must be tested via `cascade-uninstall.bats` (7/7 on .228); extend coverage to multi-container stacks (immich, btcpay). Verify on .228 before fleet roll.
- Test coverage: `tests/lifecycle/bats/cascade-uninstall.bats` exists but not in canonical gate; must opt-in with `ARCHY_GATE_CASCADE=1`.
**Production orchestrator state machine:**
- Files: `core/archipelago/src/container/prod_orchestrator.rs` (6291 lines)
- Why fragile: Largest file in the codebase; owns install/start/stop/restart/remove/upgrade for every app; per-app mutex + RwLock concurrency model; complex dependency resolution, adoption scan, Quadlet rendering, and host-port-wait logic interleaved.
- Safe modification: Understand the per-app mutex protocol before touching state mutation; test all changes via the lifecycle gate on .228; use the adoption scan + manifest merge logic for any new manifest evolution.
- Test coverage: 667 unit tests green (2026-07-01); lifecycle gate covers ~8 core apps; ~30 apps untested in gate.
**Mesh radio configuration + boot race:**
- Files: `core/archipelago/src/mesh/meshtastic.rs`, `core/archipelago/src/mesh/mod.rs`, tests at `tests/lifecycle/bats/meshtastic.bats`
- Why fragile: Radio boot-race fixed (2026-07-28, `a8c4694c`/`3f76b496`); on-air config apply must finish before device is used. Earlier versions had probe-boot-race + live config propagation issues. Must verify on real hardware.
- Safe modification: Any mesh changes require E2E test on real LoRa radios (dev-box ↔ x250-dev, or fleet broadcast); unit tests alone won't catch RF timing issues.
- Test coverage: 8-stage on-air smoke test in `tests/multinode/meshtastic.sh` (run manually; not in canonical gate).
**Lightning payment state machine:**
- Files: `core/archipelago/src/api/rpc/lnd/wallet.rs:payinvoice()`
- Why fragile: Slow multi-hop payments (>15s) previously surfaced as "failed" while settling in background; client-side 15s timeout was aborting the wait. Fixed by commit `614a0f5a` (120s wait, pending status, lnd.paymentstatus poll). Must verify on Framework PT with real multi-hop.
- Safe modification: Any lnd state changes must test full payment lifecycle: invoice creation, encoding, send, multi-hop wait, settlement confirmation. Verify on Framework PT before release.
- Test coverage: Local LND payinvoice smoke test; no multinode lightning routing test in gate.
## Scaling Limits
**Uninstall progress bar truthfulness:**
- Current capacity: Uninstall now has timeouts (fixed 2026-07-26) but progress-bar still reports fake stages (full-red full-opacity).
- Limit: Long uninstalls (>30s) show no real progress; bar claims "uninstalling" for the full duration.
- Scaling path: Backend must emit real progress events (% complete, stage name); UI must poll + display truthfully; integrate into all 5 gate iterations (not just 1 throw-away app).
**Federation node list deduplication on disk bloat:**
- Current capacity: `federation/storage.rs:dedup_nodes_by_onion()` reads entire nodes.json into memory each time a node is added/synced. At N federated peers, O(N) memory + O(N²) comparisons per operation.
- Limit: No hard limit measured; scales fine up to hundreds of peers. Beyond 1000+ peers, memory/time may become visible.
- Scaling path: Switch to a disk-backed database (e.g., rocksdb) for federation state if peer count grows; or implement incremental dedup on disk writes (preserve dedup state, only recompute on load).
**Lifecycle gate iteration count:**
- Current capacity: `ARCHY_ITERATIONS=5` runs 5 full cycles (stop/start/restart/survive per app). Entire run takes ~812 hours on .228.
- Limit: Cannot easily scale to 10+ iterations without timeout risks; per-app timeout tuning is manual.
- Scaling path: Add per-app timeout tuning (manifest field); parallelize per-app tests where safe (currently serial to avoid contention).
## Dependencies at Risk
**Reticulum transport daemon process group:**
- Risk: Pre-fix (before `be50c886`), process group wasn't cleaned up on drop. Fork-bombs or dangling processes possible under error conditions.
- Impact: Stale reticulum processes accumulating over time; resource leaks on node.
- Migration plan: Code fix already deployed (commit `7a7fec21`); no active risk. Monitor fleet for stale python processes post-deployment.
**Podman socket mount security model:**
- Risk: Apps mounting `/run/podman/podman.sock` get full container-management access. Not restricted by the security policy (AppArmor profiles not applied).
- Files: `core/archipelago/src/container/prod_orchestrator.rs:135-137` (detection), manifests for apps with podman mounts (e.g., portainer)
- Impact: A compromised app with podman socket access can start/stop/delete any container on the node.
- Recommendation: Restrict podman socket mounts to admin-only apps (portainer, docker-api tools); document risk; consider socket filtering layer (selinux context, etc.) once AppArmor is wired.
**Bitcoin version multi-version branch not fleet-wide:**
- Risk: Branch `bitcoin-version-bulletproof` (base `095a76cd`) carries multi-version support but hasn't been deployed fleet-wide yet. .228 carries it; others still run single version.
- Impact: Users on single-version nodes can't switch versions; version mismatch across fleet breaks federation.
- Migration plan: Coordinated OTA + catalog publish + `:latest` repoint sequencing per `docs/bitcoin-version-bulletproof-rollout.md`. Awaiting user decision on timing.
## Missing Critical Features
**Developer tooling CLI suite:**
- Problem: Third-party developers need `archy app validate/render/local-install/lifecycle-test` tooling before external registry launches.
- Blocks: External marketplace (workstream C); external developer onboarding.
- Status: Not yet built; documented in APP-PACKAGING-MIGRATION-PLAN.md step 5.
**Manifest-distributed registry flip:**
- Problem: Manifests still travel via OTA disk rsync. The signed catalog currently distributes only image overrides, not full manifests. Workstream B phases 1+2 done; not yet fleet-deployed.
- Blocks: Cannot confidently add/bump apps without re-signing the catalog.
- Status: Code ready; flip awaits authorization + timing call from user.
**Phase-3 Quadlet default-flip:**
- Problem: Orchestrator still uses legacy cgroup-based container management; Phase-3 `use_quadlet_backends` switch exists but is opt-in only.
- Blocks: Resolves container thrashing; unlocks independent app restarts; unblocks lifecycle perfection (workstream F).
- Status: Code validated on .228/.198 (commit pending); ready to flip when multinode gate passes.
## Test Coverage Gaps
**~30 apps with zero app-specific assertions:**
- What's not tested: Apps like grafana, jellyfin, vaultwarden, penpot, nextcloud, photoprism, uptime-kuma, homeassistant, etc. have no app-specific health checks beyond "container running."
- Files: `tests/lifecycle/bats/all-apps-matrix.bats`, `tests/lifecycle/bats/all-apps-lifecycle.bats` (generic baseline coverage)
- Risk: App-specific bugs (API down, data corruption, dependency failure) go unnoticed until user encounters them.
- Priority: Medium — baseline coverage is a real safety net; app-specific assertions are a "nice to harden" backlog item, not a gate blocker.
- Approach: Add per-app health RPC endpoints or HTTP probes; wire into the gate as opt-in per-app test suites.
**Progress UI assertions incomplete:**
- What's not tested: Install + uninstall must report monotonic, truthful progress. No stage/percentage assertions in the gate.
- Files: `neode-ui/src/components/AppCard.vue`, `core/archipelago/src/api/rpc/package/install.rs` (backend progress events)
- Risk: Silent hangs or fake progress bars are invisible to the gate.
- Priority: High — immich/grafana uninstall was stuck full-red (fixed); progress truthfulness is part of definition of done for workstream F.
- Approach: Backend must emit real progress events; UI must display & test them; integrate into canonical gate (currently opt-in).
**All-apps matrix in cascade gate:**
- What's not tested: `ARCHY_GATE_CASCADE=1` runs ONE throwaway app's uninstall/reinstall. Must extend to multi-container stacks (immich, btcpay, mempool) and all ~40 installed apps.
- Files: `tests/lifecycle/bats/cascade-uninstall.bats` (single-app variant)
- Risk: Multi-container app uninstall bugs (e.g., orphan postgres container) go undetected.
- Priority: High — part of workstream F definition of done.
- Approach: Parametrize cascade test over all manifest IDs; run 5 cascades total (not 5 per app to save time); gate-pass requires zero ghost containers post-uninstall.
---
*Analysis based on codebase state 2026-07-29. Issues tracked in `docs/UNIFIED-TASK-TRACKER.md` (day-to-day) and `docs/PRODUCTION-MASTER-PLAN.md` (historical narrative).*

View File

@ -0,0 +1,159 @@
# Coding Conventions
**Analysis Date:** 2026-07-29
## Naming Patterns
**Files:**
- TypeScript/Vue: PascalCase for components (e.g., `ToggleSwitch.vue`, `SendBitcoinModal.vue`), camelCase for composables and stores (e.g., `useFileType.ts`, `controller.ts`)
- Rust: snake_case for modules and files (e.g., `bitcoin_rpc.rs`, `storage_crypto.rs`)
- Test files: co-located with source in `__tests__/` subdirectories with `.test.ts` or `.spec.ts` suffix for Vitest, `.bats` for shell tests
- Constants in TypeScript use UPPER_SNAKE_CASE within modules (e.g., `IMAGE_EXTS`, `CATEGORY_COLORS` in `useFileType.ts`)
**Functions:**
- TypeScript/Vue: camelCase for all functions (e.g., `getFileCategory`, `formatSize`, `useFileType`)
- Composables: `use` prefix for Vue composables (e.g., `useFileType`, `useToast`, `useMessageToast`) — exported as named exports or default exports
- Store functions (Pinia): defined with snake_case action names, exported from `defineStore` factory
- Rust: snake_case for all functions and methods (e.g., `doesnt_reallocate`, following Rust conventions)
**Variables:**
- TypeScript: camelCase for local variables and reactive refs (e.g., `modelValue`, `isActive`, `gamepadCount`)
- Refs (Vue 3): prefix not required, but convention is lowercase start (e.g., `const ext = ref('jpg')`)
- Computed properties: camelCase, explicit `.value` suffix in templates when needed
- Parameters: camelCase, typed explicitly in TypeScript (e.g., `password: string`, `isDir: Ref<boolean>`)
**Types:**
- TypeScript: PascalCase for type aliases and interfaces (e.g., `RPCOptions`, `FileCategory`, `CatalogVersionInfo`)
- Union types: PascalCase (e.g., `PendingState = 'pending' | 'sent' | 'approved'`)
- Component props: typed with `defineProps<{ ... }>()` syntax in `<script setup>`
- Rust: PascalCase for structs and enums, snake_case for fields within them
## Code Style
**Formatting:**
- No global Prettier config; code style follows project patterns incrementally
- Vue components: single-file components (`.vue`) with `<template>`, `<script setup>`, optional `<style scoped>`
- TypeScript: indentation is 2 spaces (visible in `vitest.config.ts`, Vue components, test files)
- Line width: no strict enforcement observed; pragmatic wrapping around 80100 characters
- Arrow functions preferred for short callbacks: `(x) => x * 2`
- Template strings for multi-line formatting
**Linting:**
- No `.eslintrc` detected at repo root or `neode-ui/` level
- Rust: Clippy allowances declared at crate level in `main.rs` (`#![allow(...)]`) to suppress stylistic lints and focus CI on correctness issues
- Examples of suppressed Clippy lints: `too_many_arguments`, `type_complexity`, `enum_variant_names`, `unused_io_amount`
## Import Organization
**Order:**
1. Vue framework imports (`import { computed, ref, type Ref } from 'vue'`)
2. Library imports (`import { defineStore } from 'pinia'`, `import { format } from 'date-fns'`)
3. Local module imports (`import { useFileType } from '../useFileType'`, `import { rpcClient } from '../api/rpc-client'`)
4. Types/interfaces (inline in import statements via `type` keyword when needed)
5. No blank lines required between groups in practice
**Path Aliases:**
- TypeScript: `@` alias maps to `src/` (configured in `vitest.config.ts` and `tsconfig.json`)
- Usage: `import { displayVersion } from '@/utils/version'`
- Rust: crate-relative paths (`use crate::module::submodule`) and external crate paths
## Error Handling
**Patterns:**
- TypeScript: explicit try-catch with error type narrowing (e.g., `if (error instanceof Error) { ... }`)
- RPC client (`rpc-client.ts`): catches fetch errors, AbortError, and HTTP errors; distinguishes retryable (502, 503) from permanent (401, 403)
- Rust: `anyhow::Result<T>` for fallible operations; `?` operator for error propagation; `.context("message")` for adding context
- Backend error responses: JSON-RPC format with `error: { code, message, data? }` structure; UI catches and displays via toast system
- Network errors: automatic retry with exponential backoff (600ms × (attempt + 1) with jitter); configurable `maxRetries` per call
## Logging
**Framework:** console object for frontend, `tracing` crate for Rust backend
**Patterns:**
- Frontend: `console.warn`, `console.error` used selectively (e.g., `[RPC]` prefixed logs in `rpc-client.ts` for session/CSRF events)
- Rust: `tracing::info!`, `tracing::warn!` for structured logging; `println!` avoided in production code
- No log levels enforced or documented; pragmatic use based on severity
## Comments
**When to Comment:**
- Explain non-obvious retry logic, timeout decisions, CSRF handling (see `rpc-client.ts` lines 138178 for example)
- Clarify why a workaround exists (e.g., "Already on the login page: redirecting = a full reload")
- Document integration points with backend RPC methods and their expected response shapes
- Avoid redundant comments restating what the code obviously does
**JSDoc/TSDoc:**
- Function parameter types documented inline via TypeScript type annotations (e.g., `ext: Ref<string>`)
- Minimal use of explicit JSDoc blocks; type signature is the primary documentation
- Comments above exports explain purpose in one sentence (e.g., "// RPC Client for connecting to Archipelago backend")
- Optional fields in interfaces documented via property-level comments (e.g., `/** Abort the call from the outside … */`)
## Function Design
**Size:** Functions are typically 550 lines; error-handling paths in `callInner<T>` (`rpc-client.ts`) stretch to 120 lines but remain single-responsibility (retry logic + error classification)
**Parameters:**
- Prefer object parameters for 3+ arguments (e.g., `RPCOptions` object over separate `method, params, timeout`)
- Vue composables accept `Ref<T>` types to maintain reactivity (e.g., `useFileType(ext: Ref<string>, isDir: Ref<boolean>)`)
- Store action functions accept only necessary parameters; broader state via closure
**Return Values:**
- Composables return object with properties (computed values + reactive refs): `{ category, isImage, isAudio, ... }`
- Store actions return `void` or the modified state
- Utilities return simple values or objects (e.g., `formatSize` returns string, `formatDate` returns string)
- Async functions return `Promise<T>` with explicit type parameters (e.g., `async call<T>(options): Promise<T>`)
## Module Design
**Exports:**
- Composables export a single named function and helper functions: `export function useFileType(...)`, `export function getFileCategory(...)`
- Stores export the Pinia store factory: `export const useControllerStore = defineStore(...)`
- RPC client exports as singleton instance: `export const rpcClient = new RPCClient()`
- Utilities export multiple helpers from the same file (e.g., `formatSize`, `formatDate` from same module)
**Barrel Files:**
- Not observed as a primary pattern; each file self-documents its exports
- Imports use direct paths (e.g., `from '../composables/useFileType'`) rather than barrel `index.ts`
- Test files import specific utilities directly to minimize test setup complexity
## Type Safety
**Vue 3 with TypeScript:**
- Components use `<script setup lang="ts">` with `defineProps<{ ... }>()` and `defineEmits<{ ... }>()`
- Props explicitly typed as interfaces/objects with required/optional fields marked
- Events typed as call signatures (e.g., `'update:modelValue': [value: boolean]`)
- Reactive variables typed at declaration: `const isActive = ref<boolean>(false)`, or via inference when obvious
**Rust:**
- Explicit type annotations on public APIs; inference acceptable inside functions
- Generic parameters used to encode interface contracts (e.g., `struct DataUrl<'a>`)
- Pattern matching to handle enums and `Option<T>` / `Result<T, E>` types safely
## Component Architecture (Vue)
**File structure:**
- Single-file components with template → script → (optional) style
- Props come first, emits second, internal state/computed/methods follow
- One component per file (naming matches the file name)
- Slots used minimally; prefer explicit prop configuration over slot forwarding
**Reactivity:**
- `ref()` for primitive/object state; `computed()` for derived values
- `watch()` used for side effects on ref changes (not extensively shown in samples but implied)
- Pinia stores used for global state (authentication, app list, mesh status, etc.)
## Constants and Enums
**Pattern:** Constants are module-level `const` with UPPER_SNAKE_CASE names and immutable type annotations:
```typescript
const IMAGE_EXTS = new Set(['jpg', 'jpeg', ...])
const CATEGORY_COLORS: Record<FileCategory, string> = { ... }
```
**Enums:** Type aliases preferred over TypeScript `enum` keyword (e.g., `type FileCategory = 'folder' | 'image' | ...`)
---
*Convention analysis: 2026-07-29*

View File

@ -0,0 +1,235 @@
# External Integrations
**Analysis Date:** 2026-07-29
## APIs & External Services
**Bitcoin Protocol:**
- Bitcoin Core RPC endpoint - Primary blockchain interaction
- SDK/Client: `bitcoin` crate (v0.32.5), `reqwest` HTTP client
- Endpoint: `http://127.0.0.1:8332/` (configurable)
- Used for: Transaction broadcasting, UTXO validation, network sync status
- Auth: Basic HTTP auth (hardcoded RPC credentials in containers)
**Lightning Network:**
- Lightning Network Daemon (LND) - Layer 2 payments
- SDK/Client: Native REST API (`reqwest` + `serde_json`)
- REST Endpoint: `http://localhost:8080/` (container network)
- gRPC Endpoint: `http://localhost:10009/` (not currently used by backend)
- P2P Port: 9735
- Used for: Channel management, payment invoicing, routing
- Auth: Macaroon-based authentication (stored in `lnd-data:/root/.lnd`)
- Proxied: `/proxy/lnd/` → backend RPC auth + CORS handling
**Nostr Protocol:**
- Nostr Relays - Node discovery and encrypted messaging
- SDK/Client: `nostr-sdk` crate (v0.44, with NIP-04 and NIP-44 support)
- Usage: Optional, opt-in via `NOSTR_DISCOVERY_ENABLED` config
- Relays: Configurable via comma-separated `NOSTR_RELAYS` env var
- Transport: SOCKS5 Tor proxy optional via `NOSTR_TOR_PROXY` config
- Features: Ed25519 node identity publishing, encrypted peer handshake
- Related files: `core/archipelago/src/nostr_relays.rs`, `nostr_discovery.rs`, `nostr_handshake.rs`
**Mesh Networking:**
- Reticulum Protocol (RNS v1.3.5) - Local mesh radio coordination
- Daemon: `reticulum-daemon/` (Python daemon, RNS 1.3.5 + LXMF 1.0.1)
- Interface: Supervised as managed container
- LoRa Radio: Serial2 communication over USB (Meshtastic-compatible radios)
- P2P Discovery: mDNS (multicast DNS via `mdns-sd` crate)
- Mesh Ports: IPv6 dual-stack listeners (port mirroring to containers)
- Related files: `core/archipelago/src/mesh/`, `core/archipelago/src/mesh_ports.rs`
**Tor (Optional):**
- Tor SOCKS5 Proxy - Anonymous network routing
- Endpoint: `socks5h://127.0.0.1:9050` (default, configurable)
- Used for: Nostr relay connections (when routed through Tor)
- Client: `reqwest` with SOCKS feature enabled
- Config key: `nostr_tor_proxy`
**Fedimint:**
- Federated Custody Chaumian Mint - Alternative payment layer
- SDK/Client: JSON-RPC API (`reqwest` + `serde_json`)
- Endpoint: `http://localhost:8174/` (container network)
- P2P Port: 8173
- UI Port: 8175 (guardian management)
- Used for: Custody alternatives, blind signatures
- Related files: `core/archipelago/src/api/rpc/fedimint.rs`
**Decentralized Web Node (DWN):**
- DWN Health Check - Decentralized identity messaging
- Endpoint: `http://127.0.0.1:3100/health`
- Purpose: Node provisioning verification
- Related files: `core/archipelago/src/constants.rs`
## Data Storage
**Databases:**
- **Application Databases (optional, app-managed):**
- PostgreSQL: Immich, IndeedHub, Penpot, Endurain, Nextcloud
- MySQL/MariaDB: Mempool, Nextcloud
- Redis/Valkey: Immich, IndeedHub, Penpot (caching/sessions)
- SQLite: Optional local state (Cargo.toml comments out `sqlx`)
Connection: Via container network (not directly accessible from backend)
- **Key-Value Stores:**
- In-memory cache: Tokio sync primitives (Arc<DashMap>, Mutex)
- Persistent app state: User credentials, device tokens, manifest cache stored in `$DATA_DIR`
**File Storage:**
- Local filesystem only
- User data directory: `$ARCHIPELAGO_DATA_DIR` (default `/var/lib/archipelago/`)
- Subdirectories: `apps/`, `secrets/`, `backups/`, `content/`, `catalog/`
- App-specific: `/var/lib/archipelago/<app>/` (mounted into containers)
- Content sharing: Peer-to-peer via HTTP Range requests (see `content_server.rs`)
**Caching:**
- No external cache service
- In-app caching: Tokio-spawned tasks, Arc<DashMap> for concurrent access
- Browser caching: Workbox service worker (5-min API cache, 30-day asset cache, 1-year font cache)
## Authentication & Identity
**Auth Provider:**
- Custom JWT-based (node-local)
- Implementation: Ed25519 signing (key in `credentials/` directory)
- Session tokens: Stored browser-side via cookies (CSRF token middleware)
- Related files: `core/archipelago/src/auth.rs`, `core/archipelago/src/identity_manager.rs`
**BIP-39 Mnemonic Seed:**
- Seed generation: `bip39` crate (v2.1.0)
- HD key derivation: `bitcoin` crate (v0.32.5) with BIP-32
- Signing: Ed25519 for identity, ECDSA for Bitcoin transactions
- Related files: `core/archipelago/src/seed.rs`
**Identity (Decentralized):**
- Nostr npub (from Ed25519 keys)
- DID (Decentralized Identifier) via did:dht (BitTorrent DHT)
- Related files: `core/archipelago/src/identity.rs`, `core/archipelago/src/nostr_discovery.rs`
**2FA:**
- TOTP (Time-based One-Time Password)
- Library: `totp-rs` (v5.7, with otpauth and gen_secret)
- QR generation: `qrcode` crate (v0.14)
- Encrypted storage: Argon2-derived key + ChaCha20-Poly1305
- Related files: `core/archipelago/src/totp.rs`
## Monitoring & Observability
**Error Tracking:**
- Not integrated (logging only)
**Logs:**
- Approach: Structured logging via `tracing` crate
- Output: stdout (configurable level via `RUST_LOG` or config file)
- Subscriber: `tracing-subscriber` with `env-filter`
- Related files: `core/archipelago/src/monitoring.rs`, `core/archipelago/src/health_monitor.rs`
**Health Monitoring:**
- Health checks: Container liveness probes (Docker/Podman healthcheck)
- System metrics: Disk space, memory, container startup tiers
- Related files: `core/archipelago/src/health_monitor.rs`
## CI/CD & Deployment
**Hosting:**
- Bare metal (Debian Linux) with Podman rootless containers
- Fleet nodes: .198, .228, .116, x250-dev, Framework PT (various test/dev targets)
**CI Pipeline:**
- Git-based CI: Gitea (self-hosted at `.160:3000` and `.168:3000`)
- Push accounts: `gitea-ai` (for protected main branch)
- Build pipeline: Local `cargo build`/`npm run build` (not centralized CI/CD service)
- Release: Manual versioning + signed OTA manifests
- Docker builds: Local `docker-compose` or Dockerfile.web/backend
**Deployment:**
- Container orchestration: Podman with Quadlet systemd units (Phase 3+)
- Legacy fallback: Direct `podman create + systemctl start`
- OTA (Over-The-Air): Signed manifest-driven updates (v1.7+)
- Sideload: Binary + tarball to `/usr/local/bin/archipelago` and `/opt/archipelago/`
## Environment Configuration
**Required env vars:**
- `ARCHIPELAGO_DATA_DIR` - Local state directory (default: platform-specific)
- `ARCHIPELAGO_LOG_LEVEL` - Logging verbosity (default: `info`)
- `CONTAINER_RUNTIME` - `podman` or `docker` (auto-detect by default)
- `NOSTR_DISCOVERY_ENABLED` - Enable node publishing to Nostr relays (false by default)
- `NOSTR_RELAYS` - Comma-separated relay URLs (if discovery enabled)
- `NOSTR_TOR_PROXY` - SOCKS5 proxy address (optional, routes Nostr through Tor)
- `VITE_AIUI_URL` - AIUI chat interface URL (frontend, optional)
- `BACKEND_URL` - Backend target for dev server (frontend, default: `http://localhost:5959`)
**Secrets location:**
- At-rest: `$DATA_DIR/credentials/` (user.json with encrypted TOTP, session keys)
- In-container: Mounted as read-only volumes, never logged
- No `.env` file in production (config-driven)
## Webhooks & Callbacks
**Incoming:**
- Marketplace callbacks - App catalog updates from registry
- Peer discovery webhooks (via Nostr relays)
- Related files: `core/archipelago/src/webhooks.rs`
**Outgoing:**
- Device token push notifications (not yet implemented)
- App install/uninstall event notifications (framework-pt integration)
- Related files: `core/archipelago/src/device_tokens.rs`
## Container-Based Services (Orchestrated by Archipelago)
**Bitcoin Stack:**
- Bitcoin Core (lncm/bitcoind:v27.0 or knots variant)
- ElectrumX (via separate image)
- Electrs (alternative to ElectrumX)
**Lightning/Payments:**
- LND (lightninglabs/lnd:v0.17.4-beta+)
- BTCPay Server (btcpayserver:1.13.5+)
- Fedimint (fedimint/fedimintd:v0.10.0+)
**Media & Content:**
- Immich (self-hosted photo/media library)
- Nextcloud (cloud storage)
- OnlyOffice (document server)
- Penpot (design tool)
- SearXNG (search engine)
- FileBrowser (file management UI)
**Infrastructure:**
- nginx (reverse proxy, CORS, rate limiting)
- Home Assistant (home automation hub)
- Grafana (metrics dashboard)
- ThunderHub (Lightning node UI)
- Mempool Explorer (blockchain monitor)
- Endurain (fitness tracking)
- Morphos (file converter)
- IndeedHub (job board)
- Pine (voice assistant)
## Integration Points (API Contracts)
**Backend ↔ Frontend (gRPC-style RPC):**
- Endpoint: `/rpc/v1/*` (HTTP/REST)
- Related files: `core/archipelago/src/api/rpc/` (all RPC handlers)
- Major modules: `auth.rs`, `bitcoin.rs`, `lnd/`, `container.rs`, `marketplace.rs`, `mesh/`
**Frontend ↔ App Iframes:**
- Endpoint: `/app/<app-name>/*` (proxied to container)
- Sandbox: Cross-origin iframe isolation
**Mesh Node ↔ Reticulum:**
- Serial: USB LoRa radio (Meshtastic-compatible)
- Protocol: Binary Meshcore format
- Related files: `core/archipelago/src/mesh/`
**Catalog ↔ App Registry:**
- Endpoint: `/api/app-catalog` (cached from registry)
- Format: Signed YAML manifest + SHA256 verification
- Related files: `core/archipelago/src/marketplace.rs`, `app_catalog/`
---
*Integration audit: 2026-07-29*

176
.planning/codebase/STACK.md Normal file
View File

@ -0,0 +1,176 @@
# Technology Stack
**Analysis Date:** 2026-07-29
## Languages
**Primary:**
- Rust 2021 edition - Backend server (Archipelago daemon, container orchestrator)
- TypeScript 5.9 - Frontend (Vue components, application code)
- Vue 3 (TypeScript) - UI framework and reactive components
- Python 3.13 - Reticulum mesh daemon (RNS/LXMF protocols)
**Secondary:**
- JavaScript - Build scripts, mock backend (`neode-ui/mock-backend.js`), test utilities
- YAML - Configuration (docker-compose, app manifests)
- Shell - Build scripts, deployment helpers
## Runtime
**Environment:**
- Rust (1.70+) - Native compiled binaries
- Node.js 22 (Alpine-based containers) - Frontend dev/build, mock backend
- Python 3.13 - Reticulum daemon runtime
- Tokio async runtime (v1, full features) - Async server runtime
**Package Manager:**
- npm (v10+) - JavaScript dependencies
- Cargo (v1.70+) - Rust dependencies
- pip - Python dependencies (Reticulum stack)
- Lockfiles: `neode-ui/package-lock.json`, `core/Cargo.lock`
## Frameworks
**Core:**
- Tokio (v1) - Async Rust runtime, full features (networking, signals, sync primitives)
- Vue 3 (v3.5) - Progressive web framework with TypeScript support
- Hyper (v0.14) - HTTP/1 server framework with WebSocket support
- Reticulum (v1.3.5) - Mesh networking protocol stack
- LXMF (v1.0.1) - Lightweight message format protocol
**HTTP & WebSocket:**
- Hyper v0.14 (HTTP/1, full features) - Core HTTP server
- Hyper-util v0.1 - HTTP utilities
- Tower v0.5 - Middleware and service composing
- Tower-http v0.6 - CORS, tracing middleware
- Hyper-ws-listener v0.3 - WebSocket upgrade handler
- Tokio-tungstenite v0.20 - WebSocket client/server implementation
- Reqwest v0.11 - HTTP client (with rustls-tls, SOCKS proxy support, JSON)
**Frontend Build:**
- Vite v7.2 - Build tool and dev server
- Vue-tsc v3.1 - TypeScript compilation for Vue
- Tailwind CSS v3.4 - Utility-first CSS framework
- Autoprefixer v10.4 - CSS vendor prefixes
- PostCSS v8.5 - CSS processing
**Testing:**
- Vitest v3.1 - Unit test runner (Vite-native)
- Playwright v1.58 - E2E testing (browser automation)
- @vue/test-utils v2.4 - Vue component testing
- JSDOM v25 - DOM implementation for tests
- Tokio-test v0.4 - Async Rust test utilities
**PWA:**
- Vite-plugin-pwa v1.2 - Progressive Web App support
- Workbox integration - Service worker caching strategies
## Key Dependencies
**Critical:**
- bitcoin v0.32.5 - Bitcoin library (with rand-std feature for BIP-39/BIP-32)
- bip39 v2.1.0 - Mnemonic seed generation
- nostr-sdk v0.44 - Nostr protocol (NIP-04, NIP-44 encrypted messaging)
- mainline v2 - BitTorrent DHT (did:dht decentralized identity)
- reticulum v1.3.5 - Mesh networking protocol
- lxmf v1.0.1 - Lightweight message format
**Cryptography:**
- ed25519-dalek v2.2 - Ed25519 digital signatures (with rand_core)
- curve25519-dalek v4.1 - X25519 elliptic curve (key agreement)
- blake3 v1 - BLAKE3 hash function
- bcrypt v0.15 - Password hashing
- sha2 v0.10 - SHA-256 hashing
- hmac v0.12 - HMAC authentication
- argon2 v0.5 - Argon2 password hashing
- chacha20poly1305 v0.10 - AEAD encryption
- zeroize v1.8 - Secure memory wiping
**Authentication & Identity:**
- uuid v1.0 - UUID generation (v4)
- totp-rs v5.7 - TOTP 2FA (with otpauth, gen_secret)
- qrcode v0.14 - QR code generation (server-side)
**Data Serialization:**
- serde v1.0 - Serialization framework (with derive)
- serde_json v1.0 - JSON codec
- serde_yaml v0.9 - YAML codec
- ciborium v0.2 - CBOR encoding/decoding
- serde_bytes v0.11 - Efficient byte serialization
- toml v0.8 - TOML config parsing
**Networking & Mesh:**
- mdns-sd v0.18 - mDNS service discovery
- serial2-tokio v0.1 - Serial port communication (LoRa radios over USB)
- socket2 v0.5 - Low-level socket options (IPv6_V6ONLY for dual-stack)
- libc v0.2 - Process group signaling
**Compression & Archives:**
- tar v0.4 - TAR archive creation
- flate2 v1.0 - gzip compression
- zip v2.0 - ZIP archive handling (LoRa firmware flashing)
- reed-solomon-erasure v6.0 - Erasure coding (Phase 2 mesh transport)
- hkdf v0.12 - HKDF key derivation (Phase 3 encrypted mesh)
**Utilities:**
- anyhow v1.0 - Error handling
- thiserror v1.0 - Error types with derive macros
- tracing v0.1 - Structured logging
- tracing-subscriber v0.3 - Log filtering and formatting
- regex v1.10 - Pattern matching
- chrono v0.4 - Date/time handling
- hex v0.4 - Hex encoding/decoding
- bs58 v0.5 - Base58 encoding (Bitcoin addresses)
- base64 v0.21 - Base64 encoding
- zbase32 v0.1 - Z-base-32 encoding (DHT)
- data-encoding v2.6 - Multiple encoding schemes
- bytes v1 - Efficient byte buffer
- futures-util v0.3 - Async utilities
- http-body-util v0.1 - HTTP body utilities
- http-body v1.0 - HTTP body abstractions
- indexmap v2.0 - Ordered maps
- async-trait v0.1 - Async trait methods
- sd-notify v0.4 - Systemd watchdog notification
**Infrastructure (Optional):**
- iroh v1 (optional, feature-gated) - QUIC-based peer swarm engine (Phase 2)
- iroh-blobs v0.103 (optional, feature-gated) - Content addressable storage provider
## Configuration
**Environment:**
- Config file: `config/archipelago.toml` or `$DATA_DIR/config.yml`
- Runtime options: Docker/Podman selection, dev/prod modes, FIPS anchor selection, Nostr discovery
- Key env vars: `ARCHIPELAGO_DATA_DIR`, `ARCHIPELAGO_LOG_LEVEL`, `CONTAINER_RUNTIME`, `NOSTR_DISCOVERY_ENABLED`, `NOSTR_RELAYS`, `NOSTR_TOR_PROXY`
**Build:**
- Cargo workspace at `core/` (5 members: archipelago, container, openwrt, performance, security)
- Release profile: opt-level 3
- Dev profile: opt-level 0
- Test profile: opt-level 3
- Cross-compilation: aarch64-unknown-linux-gnu via `.cargo/config.toml`
**Frontend Build:**
- Vite config: `neode-ui/vite.config.ts` (development port 8100, production build to `../web/dist/neode-ui`)
- TypeScript config: `neode-ui/tsconfig.json`
- Module path alias: `@``src/`
## Platform Requirements
**Development:**
- Rust 1.70+ with Cargo
- Node.js 22+ with npm
- Python 3.13+ (for Reticulum daemon)
- Docker or Podman (for local app testing)
- rustup target: `aarch64-unknown-linux-gnu` (for ARM64 cross-compilation)
- gcc-aarch64-linux-gnu (for cross-compilation toolchain on Linux hosts)
**Production:**
- Deployment target: Debian/Alpine Linux (rootless Podman)
- Binary output: `/usr/local/bin/archipelago` (sideloaded or via Quadlet systemd units)
- Frontend served: nginx with Vite-built SPA (PWA manifest, service worker, CORS proxies)
- Database support: Optional (SQLx with SQLite driver available but commented out)
---
*Stack analysis: 2026-07-29*

View File

@ -0,0 +1,434 @@
# Codebase Structure
**Analysis Date:** 2026-07-29
## Directory Layout
```
archipelago-repo/
├── core/ # Rust workspace root (Cargo.toml at workspace level)
│ ├── archipelago/ # Main daemon binary (backend)
│ │ ├── src/
│ │ │ ├── main.rs # Entry point, startup, background tasks
│ │ │ ├── server.rs # HTTP server (Hyper), listener, connection multiplexing
│ │ │ ├── state.rs # StateManager + data_model.rs (central state)
│ │ │ ├── auth.rs # User auth, password hashing, session management
│ │ │ ├── identity.rs # Node identity, Ed25519 keys, Tor address
│ │ │ ├── config.rs # Config loading, data directory setup
│ │ │ ├── api/ # HTTP API layer
│ │ │ │ ├── handler/ # HTTP request dispatch, WebSocket, content proxy
│ │ │ │ └── rpc/ # JSON-RPC 2.0 methods (~40 domain modules)
│ │ │ │ ├── auth.rs # auth.login, auth.setup, etc.
│ │ │ │ ├── container.rs # container.install, .list, .start, etc.
│ │ │ │ ├── bitcoin.rs # bitcoin.status, bitcoin.send, etc.
│ │ │ │ ├── mesh.rs # mesh.* (peer discovery, LoRa, federation)
│ │ │ │ ├── wallet.rs # wallet.* (lightning, Bitcoin)
│ │ │ │ └── [20+ other domains]
│ │ │ ├── container/ # Container orchestration (Podman)
│ │ │ │ ├── prod_orchestrator.rs # Main Podman lifecycle (>250KB)
│ │ │ │ ├── boot_reconciler.rs # Periodic manifest sync loop
│ │ │ │ ├── docker_packages.rs # Image registry, image verification
│ │ │ │ ├── quadlet.rs # Systemd Quadlet generation
│ │ │ │ ├── secrets.rs # Secret injection (0600 files)
│ │ │ │ ├── lnd.rs # Lightning Network Daemon container setup
│ │ │ │ ├── app_catalog.rs # Signed app catalog, manifest overlay
│ │ │ │ └── [data managers, registry, image policy]
│ │ │ ├── bootstrap.rs # Post-startup tasks (systemd units, audio stack, gamepad)
│ │ │ ├── crash_recovery.rs # Container recovery after crash, PID marker
│ │ │ ├── health_monitor.rs # Periodic app health checks, restart
│ │ │ ├── mesh.rs # Mesh P2P listener, sender, LoRa radio control
│ │ │ ├── federation.rs # Federation (DNS-SD, HTTP API)
│ │ │ ├── fips/ # FIPS anchor (Tor bridge to peer)
│ │ │ ├── bitcoin_rpc.rs # Bitcoin Core RPC client calls
│ │ │ ├── bitcoin_status.rs # Bitcoin sync status polling
│ │ │ ├── content_server.rs # Content-addressed blob server (CAP tokens)
│ │ │ ├── blobs.rs # BlobStore (hash→file mapping, encryption)
│ │ │ ├── wallet.rs # Lightning + Bitcoin wallet logic
│ │ │ ├── identity_manager.rs # Seed derivation, key rotation
│ │ │ ├── marketplace.rs # Marketplace transaction logic
│ │ │ ├── transport.rs # Transport selection (Mesh/FIPS/Tor routing)
│ │ │ ├── update.rs # OTA update apply, verification, rollback
│ │ │ ├── session.rs # Session store (SQLite or in-memory)
│ │ │ ├── rate_limit.rs # Rate limiter (per-IP, per-endpoint, per-user)
│ │ │ ├── monitoring.rs # Metrics collection (app count, memory, etc.)
│ │ │ ├── data_model.rs # State struct tree (serde Serialize/Deserialize)
│ │ │ ├── constants.rs # Global constants (version, defaults)
│ │ │ ├── peer*.rs, webhook*.rs, nostr*.rs, vpn.rs, etc.
│ │ │ └── seed.rs # Seed storage, backup QR generation
│ │ ├── Cargo.toml # Dependencies
│ │ └── tests/ # Unit/integration tests
│ │
│ ├── container/ # Container management library (OCI types, Podman)
│ │ ├── src/
│ │ │ ├── manifest.rs # Manifest struct (YAML parsing)
│ │ │ ├── runtime.rs # Podman CLI calls (create, start, stop, logs)
│ │ │ ├── podman_client.rs # Podman socket API client
│ │ │ ├── image_verify.rs # Image signature verification (Cosign)
│ │ │ └── port_manager.rs # Port allocation, conflict detection
│ │ └── Cargo.toml
│ │
│ ├── security/ # Secrets management library
│ │ ├── src/
│ │ │ ├── secrets_manager.rs # Secret encryption/decryption (ChaCha20)
│ │ │ └── vault.rs # Vault storage, rotation
│ │ └── Cargo.toml
│ │
│ ├── performance/ # Performance monitoring library
│ ├── openwrt/ # OpenWrt device integration
│ ├── Cargo.toml # Workspace manifest (members: archipelago, container, security, etc.)
│ └── Cargo.lock # Locked dependency versions
├── neode-ui/ # Frontend (Vue 3, TypeScript)
│ ├── src/
│ │ ├── main.ts # Vue app entry point, Router setup, WebSocket init
│ │ ├── App.vue # Root component (layout, nav)
│ │ ├── router/
│ │ │ └── index.ts # Vue Router config (routes, guards)
│ │ ├── views/ # Page-level components (one per route)
│ │ │ ├── Home.vue # Dashboard
│ │ │ ├── Apps.vue # App browser + installer
│ │ │ ├── AppDetails.vue # Single app detail + logs
│ │ │ ├── AppSession.vue # Iframe container for app content
│ │ │ ├── Cloud.vue # File browser (WebDAV/DWN)
│ │ │ ├── Mesh.vue # Mesh map, contacts, messages
│ │ │ ├── Wallet.vue # Lightning + Bitcoin addresses/sends
│ │ │ ├── Marketplace.vue # Paid apps, content marketplace
│ │ │ ├── Server.vue # Node status, settings, restart
│ │ │ ├── Federation.vue # Federation peers, federation apps
│ │ │ ├── Onboarding*/ # Multi-step setup flow
│ │ │ └── [15+ other pages]
│ │ ├── components/ # Reusable UI components
│ │ │ ├── AppCard.vue # App listing card
│ │ │ ├── AppInstaller.vue # Install modal
│ │ │ ├── Modal.vue # Generic modal (teleported)
│ │ │ ├── Toast.vue # Notification toast
│ │ │ ├── MeshGraph.vue # Mesh topology graph (D3)
│ │ │ ├── MapView.vue # Mesh map (Leaflet)
│ │ │ ├── QrScanner.vue # QR code input
│ │ │ └── [30+ other components]
│ │ ├── composables/ # Logic hooks (Vue composition API)
│ │ │ ├── useAuth.ts # Login/logout logic
│ │ │ ├── useAppStore.ts # Access app Pinia store
│ │ │ ├── useRpc.ts # Make RPC calls
│ │ │ ├── useWebSocket.ts # WebSocket connection management
│ │ │ ├── useOnboarding.ts # Onboarding flow state
│ │ │ ├── useControllerNav.ts # Gamepad controller navigation
│ │ │ └── [20+ other composables]
│ │ ├── stores/ # Pinia state management (reactive stores)
│ │ │ ├── appStore.ts # Apps list, install state
│ │ │ ├── walletStore.ts # Lightning/Bitcoin addresses, balance
│ │ │ ├── meshStore.ts # Mesh peers, messages
│ │ │ ├── settingsStore.ts # User settings, theme, language
│ │ │ ├── userStore.ts # Current user identity
│ │ │ └── [other stores]
│ │ ├── api/
│ │ │ └── rpc-client.ts # RPC client library (request, WebSocket, reconnect)
│ │ ├── services/ # Business logic (not Vue-dependent)
│ │ │ ├── qrScanner.ts # QR scanner initialization
│ │ │ └── [other services]
│ │ ├── utils/ # Utility functions
│ │ │ ├── format.ts # Date, number, currency formatting
│ │ │ ├── validate.ts # Input validation (emails, addresses)
│ │ │ ├── crypto.ts # Client-side crypto (BIP39, etc.)
│ │ │ └── [helpers]
│ │ ├── types/ # TypeScript type definitions
│ │ │ ├── index.ts # Export all types
│ │ │ └── [domain-specific types]
│ │ ├── i18n.ts # Internationalization config
│ │ ├── locales/ # Translation files
│ │ │ ├── en.json # English
│ │ │ ├── es.json # Spanish
│ │ │ └── [other languages]
│ │ ├── assets/ # Static assets
│ │ │ └── icon/ # App icons, favicons
│ │ ├── style.css # Global CSS (Tailwind + custom)
│ │ ├── data/ # Static data (country lists, etc.)
│ │ └── e2e/ # Playwright E2E tests
│ │ ├── intro-experience.spec.ts
│ │ └── app-launch.spec.ts
│ │
│ ├── public/ # Static web root
│ │ ├── index.html # HTML entry point
│ │ ├── favicon.ico # Browser tab icon
│ │ ├── manifest.json # PWA manifest
│ │ └── catalog.json # App catalog (copied from app-catalog/catalog.json)
│ │
│ ├── package.json # Frontend dependencies + build scripts
│ ├── tsconfig.json # TypeScript config
│ ├── vite.config.ts # Vite build config
│ ├── vitest.config.ts # Vitest test runner config
│ ├── tailwind.config.js # Tailwind CSS config
│ ├── mock-backend.js # Dev mock server (for `npm run dev:mock`)
│ └── [other build configs]
├── apps/ # Containerized applications (app manifests + build scripts)
│ ├── bitcoin-core/ # Bitcoin Core container
│ │ ├── manifest.yml # Archipelago manifest (interface, ports, secrets, health)
│ │ ├── Dockerfile # OCI image definition
│ │ ├── bitcoin.conf.template # Config template (secrets injected at runtime)
│ │ └── start.sh # Container entrypoint
│ │
│ ├── lightning-stack/ # Lightning Network stack (LND)
│ ├── lnd/ # LND daemon
│ ├── immich/ # Photo backup app
│ ├── nextcloud/ # Cloud storage
│ ├── electrumx/ # Bitcoin block explorer index
│ ├── router/ # Mesh router app
│ ├── pine/ # Voice assistant (whisper + piper + nginx)
│ ├── vaultwarden/ # Password manager
│ ├── [40+ other apps]
│ ├── QUICKSTART.md # App development guide
│ ├── PORTS.md # Port allocation reference
│ └── build.sh # App build script (all apps)
├── web/ # Built frontend output
│ └── dist/
│ └── neode-ui/ # `npm run build` output (served by nginx)
│ ├── index.html
│ ├── [JS bundles]
│ └── [static assets]
├── tests/ # Test suite
│ ├── lifecycle/
│ │ ├── run-gate.sh # Single-node production gate (5 iterations)
│ │ └── TESTING.md # Test plan documentation
│ ├── e2e/ # End-to-end tests (Playwright)
│ └── [other test directories]
├── docs/ # Documentation (user & developer guides)
│ ├── PRODUCTION-MASTER-PLAN.md # North star: manifest-driven, registry-based, decentralized
│ ├── UNIFIED-TASK-TRACKER.md # Open tasks (fastest-first)
│ ├── APP-PACKAGING-MIGRATION-PLAN.md
│ ├── registry-manifest-design.md
│ ├── multinode-testing-plan.md
│ ├── release-workflow.md # OTA + ISO release process
│ ├── app-development.md # Guide for app developers
│ ├── api-rpc-reference.md # JSON-RPC 2.0 method documentation
│ └── [20+ other docs]
├── image-recipe/ # ISO/image build scripts
│ ├── build-debian-iso.sh # Builds bootable Debian ISO
│ ├── include/ # Root filesystem overlays
│ │ └── opt/archipelago/ # Pre-baked config, scripts, systemd units
│ └── [other image components]
├── scripts/ # Utility scripts
│ ├── deploy-to-target.sh # Sideload binary to test node (Tailscale SSH + rsync)
│ ├── resilience/ # Resilience test scripts
│ └── [other scripts]
├── demo/ # Demo deployment (pre-configured node)
│ ├── demo-deploy.yml # Docker Compose for vps2 demo
│ └── [demo-specific scripts]
├── docker/ # Docker/Podman config
│ └── [docker-compose fragments, Dockerfiles]
├── .planning/ # Codebase analysis documents (this is you)
│ └── codebase/
│ ├── ARCHITECTURE.md
│ ├── STRUCTURE.md
│ ├── CONVENTIONS.md
│ ├── TESTING.md
│ ├── STACK.md
│ ├── INTEGRATIONS.md
│ └── CONCERNS.md
├── .claude/ # Claude Code configuration
│ └── skills/ # GSD skills (if any project-specific)
├── .github/ # GitHub Actions CI/CD
├── .gitea/ # Gitea CI/CD (local Gitea runner)
├── .git/ # Git repository
├── .gitignore # Git ignore patterns
├── CLAUDE.md # Project instructions (commit rules, invariants, testing)
├── Cargo.lock # Locked Rust dependency versions
├── CHANGELOG.md # Release notes
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
└── README.md # Project overview
```
## Directory Purposes
**core/**
- Purpose: Rust backend source and dependencies
- Contains: Main daemon binary, container orchestration, RPC handlers, business logic
- Key files: `archipelago/src/main.rs` (entry point), `archipelago/Cargo.toml` (deps)
**neode-ui/**
- Purpose: Vue 3 frontend SPA source
- Contains: Views, components, stores, translations, tests, build config
- Key files: `src/main.ts` (entry point), `vite.config.ts` (build), `package.json` (deps)
**apps/**
- Purpose: Containerized application definitions
- Contains: Manifests (YAML), Dockerfiles, configs for ~50 apps (Bitcoin, LND, Immich, etc.)
- Key files: `*/manifest.yml` (app definition), `*/Dockerfile` (image build)
**web/dist/neode-ui/**
- Purpose: Production frontend output (built by `npm run build`)
- Contains: Bundled JS, HTML, static assets
- Key files: `index.html` (entry), JS chunks (hashed filenames)
- Note: Served by nginx at `/` on node
**tests/**
- Purpose: Test suite (unit, integration, E2E)
- Contains: Lifecycle gate (production exit criterion), E2E specs, test utilities
- Key files: `lifecycle/run-gate.sh` (the production gate), `e2e/*.spec.ts` (user flows)
**docs/**
- Purpose: User and developer documentation
- Contains: Architecture, design decisions, release process, task tracking
- Key files: `PRODUCTION-MASTER-PLAN.md` (north star), `UNIFIED-TASK-TRACKER.md` (open tasks)
**image-recipe/**
- Purpose: ISO/image build scripts
- Contains: Debian ISO recipe, root filesystem overlays, bootloader config
- Key files: `build-debian-iso.sh` (main build), `include/opt/archipelago/` (pre-baked system config)
**scripts/**
- Purpose: Utility and deployment scripts
- Contains: Sideload/deploy to test nodes, resilience testing, CI glue
- Key files: `deploy-to-target.sh` (ship binary to remote node)
## Key File Locations
**Entry Points:**
- Backend daemon: `core/archipelago/src/main.rs` (spawns HTTP server, tasks, orchestrator)
- Frontend app: `neode-ui/src/main.ts` (Vue app, router, WebSocket init)
- App manifest: `apps/*/manifest.yml` (Archipelago-specific; controls container, secrets, UI)
- Production gate: `tests/lifecycle/run-gate.sh` (exit criterion for releases)
**Configuration:**
- Backend config: `core/archipelago/src/config.rs` (data dir, bind port, dev mode flag)
- Frontend config: `neode-ui/vite.config.ts` (build settings, env vars)
- App registries: `/var/lib/archipelago/registries.json` (user-configured Gitea mirrors)
- Secrets location: `/var/lib/archipelago/secrets/` (encrypted ChaCha20 files)
**Core Logic:**
- Container orchestration: `core/archipelago/src/container/prod_orchestrator.rs` (300KB+ main logic)
- RPC dispatch: `core/archipelago/src/api/rpc/mod.rs` (method routing, ~200 RPCs)
- State management: `core/archipelago/src/state.rs` (StateManager) + `core/archipelago/src/data_model.rs` (struct def)
- Frontend stores: `neode-ui/src/stores/` (Pinia stores, reactive state)
**Testing:**
- Rust unit tests: Inline in `core/` modules (use `#[test]` and `#[tokio::test]`)
- Vitest unit tests: `neode-ui/src/**/__tests__/*.test.ts`
- E2E tests: `neode-ui/e2e/*.spec.ts` (Playwright)
- Integration tests: `tests/` (shell scripts, node command testing)
## Naming Conventions
**Files:**
- Rust modules: `snake_case.rs` (e.g., `health_monitor.rs`, `crash_recovery.rs`)
- Vue components: `PascalCase.vue` (e.g., `AppCard.vue`, `MeshGraph.vue`)
- Composables: `use[Name].ts` (e.g., `useAuth.ts`, `useRpc.ts`)
- Stores: `[domain]Store.ts` (e.g., `appStore.ts`, `walletStore.ts`)
- Tests: `[name].test.ts` or `[name].spec.ts` (e.g., `rpc-client.test.ts`, `app-launch.spec.ts`)
- Scripts: lowercase with hyphens (e.g., `deploy-to-target.sh`, `run-gate.sh`)
**Directories:**
- Rust workspace members: lowercase (e.g., `archipelago`, `container`, `security`)
- Feature directories: PascalCase or lowercase depending on context
- `neode-ui/src/views/` — page components (mostly PascalCase)
- `neode-ui/src/composables/` — logic hooks (lowercase files with `use` prefix)
- `core/archipelago/src/api/rpc/` — RPC modules by domain (lowercase: `bitcoin.rs`, `mesh.rs`)
**Functions/Methods:**
- Async functions: `async fn method_name() -> Result<T>` (no special suffix)
- Event handlers: `on[Event]` in Vue (e.g., `@click="onInstall"` calls `onInstall()`)
- Computed properties: `computed(() => ...)` (no special name)
- Public RPC methods: `pub async fn [domain]_[action](...)` (e.g., `container_install`, `bitcoin_send`)
**Variables & Constants:**
- Constants: `UPPER_SNAKE_CASE` (e.g., `RECONCILER_DEFAULT_INTERVAL`, `MAX_FILE_SIZE`)
- State variables: `camelCase` (e.g., `appList`, `isLoading`)
- Type aliases: `PascalCase` (e.g., `AppId`, `MeshPeer`)
**Exports & Modules:**
- Re-exports barrel files: `mod.rs` exporting `pub use child::*;`
- Private internals: `mod private;` (not `pub mod`)
- Path aliases (neode-ui): `@/` = `src/`, `@components/` = `src/components/`
## Where to Add New Code
**New RPC Method (Backend):**
1. Determine domain (auth, container, bitcoin, mesh, wallet, etc.)
2. Add async fn to `core/archipelago/src/api/rpc/[domain].rs`
3. Function signature: `pub async fn [action](handler: &RpcHandler, params: [ParamType]) -> Result<[ResponseType]>`
4. Register in `core/archipelago/src/api/rpc/mod.rs` dispatcher (line ~350+, search for `match method_name`)
5. Test: Unit test in same file with `#[tokio::test]`, or E2E in `tests/`
**New Frontend View (Page):**
1. Create `neode-ui/src/views/[ViewName].vue` (PascalCase)
2. Import Router in `neode-ui/src/router/index.ts`, add route
3. Add navigation link in `neode-ui/src/components/Nav.vue` (if public-facing)
4. State: Use or create Pinia store in `neode-ui/src/stores/`
5. Test: Add E2E test in `neode-ui/e2e/` if user-facing flow
**New Container App:**
1. Create directory `apps/[app-name]/`
2. Write `manifest.yml` (copy structure from existing app; define interfaces.main.ui, health check, secrets)
3. Write `Dockerfile` (base image, deps, entrypoint)
4. Add to `app-catalog/catalog.json` with entry (id, version, url to manifest)
5. Test: `archipelago container.install { manifest_url: "..." }` on dev node
**New Component (Frontend):**
1. Create `neode-ui/src/components/[ComponentName].vue`
2. If reusable logic, extract to `neode-ui/src/composables/use[Logic].ts`
3. If shared state, use Pinia store (don't create component-local state)
4. Example: `components/AppCard.vue` displays one app (reused in Apps.vue listing)
**New Utility Function:**
- Backend service logic: Add to `core/archipelago/src/[domain].rs` or new file if it's cross-cutting
- Frontend helper: Add to `neode-ui/src/utils/` (e.g., `format.ts`, `validate.ts`)
- Example: Lightning address validation → `neode-ui/src/utils/validate.ts:validateLightningAddress()`
**New Test:**
- Rust unit test: Inline in source file (`#[test]` or `#[tokio::test]`)
- Frontend unit test: `neode-ui/src/composables/__tests__/use[Logic].test.ts`
- E2E test: `neode-ui/e2e/[feature].spec.ts` (Playwright)
- Integration test: `tests/[feature].sh` (shell script running node commands)
## Special Directories
**core/target/**
- Purpose: Rust build artifacts (generated)
- Generated: Yes (by `cargo build`)
- Committed: No (in .gitignore)
**neode-ui/node_modules/**
- Purpose: npm dependencies
- Generated: Yes (by `npm install` or `pnpm install`)
- Committed: No (in .gitignore)
**web/dist/**
- Purpose: Built frontend output (generated)
- Generated: Yes (by `npm run build`)
- Committed: No (in .gitignore) — distributed via OTA/ISO
**/var/lib/archipelago/** (at runtime on node)
- Purpose: Data directory (user data, settings, secrets, backups)
- Generated: Yes (created by daemon on first boot)
- Committed: No (runtime data; contains user secrets)
- Subdirs:
- `identity/` — Node Ed25519 keys
- `secrets/` — Encrypted secret vaults (ChaCha20)
- `apps/` — App manifests (disk copies or registry overlays)
- `backups/` — Encrypted backup archives
- `registries.json` — User-configured Gitea mirrors
- etc.
**/opt/archipelago/** (at runtime on node)
- Purpose: System-level Archipelago files (read-only on ISO, writable post-install)
- Contains:
- `web-ui/` — Built frontend (nginx root)
- `bin/archipelago` — Daemon binary
- `docker/` — Docker Compose or Quadlet files (orchestration)
- `scripts/` — System maintenance scripts
- Note: OTA updates overwrite `web-ui/` + `bin/` atomically
---
*Structure analysis: 2026-07-29*

View File

@ -0,0 +1,449 @@
# Testing Patterns
**Analysis Date:** 2026-07-29
## Test Framework
**Frontend:**
- Runner: Vitest 3.1.1
- Config: `neode-ui/vitest.config.ts`
- Environment: jsdom (DOM testing in Node.js)
- Globals: enabled (`globals: true`) — `describe`, `it`, `expect` available without imports
- Assertion library: built-in Vitest assertions (compatible with Jest)
**Backend (Rust):**
- Framework: built-in `#[test]` attribute and `cargo test`
- Command: `cd core && cargo test --workspace --bins`
**E2E (Browser):**
- Framework: Playwright 1.58.2
- Config: implicit (tests in `neode-ui/e2e/` directory)
**Shell Integration Tests:**
- Framework: Bats (Bash Automated Testing System)
- Location: `tests/lifecycle/bats/`
- Config files: `tests/lifecycle/lib/rpc.bash` (RPC wrapper helpers)
**Run Commands:**
```bash
# Unit tests (Vitest)
npm run test # Run all tests once
npm run test:watch # Watch mode, re-run on file changes
# Rust tests
cd core && cargo test --workspace --bins
# Specific Vitest suite
npm run test -- src/composables/__tests__/useFileType.test.ts
# Shell lifecycle tests (from repo root)
ARCHY_PASSWORD=password123 tests/lifecycle/run.sh # Read-only tests
ARCHY_PASSWORD=password123 ARCHY_ALLOW_DESTRUCTIVE=1 tests/lifecycle/run.sh # Include destructive
# Release gate (5× iterations, must run ON the target node)
ARCHY_PASSWORD=password123 ARCHY_ALLOW_DESTRUCTIVE=1 ARCHY_ITERATIONS=5 \
tests/lifecycle/run-gate.sh
```
## Test File Organization
**Location:**
- Frontend: co-located with source in `__tests__/` subdirectories
- Example: `src/composables/useFileType.ts``src/composables/__tests__/useFileType.test.ts`
- Example: `src/api/rpc-client.ts``src/api/__tests__/rpc-client.test.ts`
- E2E: separate `e2e/` directory at root of frontend
- Shell: `tests/lifecycle/bats/` directory
**Naming:**
- Vitest: `*.test.ts` or `*.spec.ts` suffix (`.test.ts` preferred)
- Playwright: `*.spec.ts` suffix
- Bats: `*.bats` suffix
- Rust unit: same file with `#[test]` functions at the bottom or in submodules
**Structure:**
```
neode-ui/
├── src/
│ ├── composables/
│ │ ├── useFileType.ts
│ │ └── __tests__/
│ │ ├── useFileType.test.ts
│ │ ├── useNavSounds.test.ts
│ │ └── ... (other composable tests)
│ ├── api/
│ │ ├── rpc-client.ts
│ │ └── __tests__/
│ │ └── rpc-client.test.ts
│ └── stores/
│ ├── controller.ts
│ └── (no tests found for stores in exploration)
├── e2e/
│ ├── app-launch.spec.ts
│ ├── intro-experience.spec.ts
│ └── visual-regression.spec.ts
```
## Test Structure
**Vitest Suite Organization:**
```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { ref } from 'vue'
import { getFileCategory, useFileType, formatSize, formatDate } from '../useFileType'
describe('getFileCategory', () => {
it('returns folder for directories', () => {
expect(getFileCategory('', true)).toBe('folder')
expect(getFileCategory('jpg', true)).toBe('folder')
})
it('identifies image extensions', () => {
expect(getFileCategory('jpg', false)).toBe('image')
expect(getFileCategory('png', false)).toBe('image')
})
})
describe('useFileType', () => {
it('returns correct category and computed values for an image', () => {
const ext = ref('jpg')
const isDir = ref(false)
const result = useFileType(ext, isDir)
expect(result.category.value).toBe('image')
expect(result.isImage.value).toBe(true)
})
it('reacts to ref changes', () => {
const ext = ref('jpg')
const isDir = ref(false)
const result = useFileType(ext, isDir)
expect(result.category.value).toBe('image')
ext.value = 'mp3'
expect(result.category.value).toBe('audio')
})
})
```
**Patterns:**
- `describe()` blocks group related tests by function or component
- `it()` blocks test a single behavior (flat structure, no nesting of describe blocks observed)
- `beforeEach()` / `afterEach()` hooks for setup/teardown per test
- `beforeAll()` / `afterAll()` hooks for suite-level setup (e.g., login in bats tests)
- Assertions use `expect(actual).toBe(expected)` or `expect(actual).toEqual(object)`
**Playwright E2E Structure:**
```typescript
import { expect, test, type Page } from '@playwright/test'
async function login(page: Page) {
await page.goto('/login', { waitUntil: 'domcontentloaded' })
await page.evaluate(() => {
localStorage.setItem('neode_intro_seen', '1')
})
// ... fill form, submit
await page.waitForURL('**/dashboard**', { timeout: 20_000 })
}
test('installed app launch opens reachable app URL', async ({ page, context, baseURL }) => {
test.skip(!EXPECTED_URL, 'Set ARCHY_EXPECTED_LAUNCH_URL for launch qualification')
await login(page)
await page.goto('/dashboard/apps', { waitUntil: 'domcontentloaded' })
const appCard = page.locator('[data-controller-container]', {
has: page.getByRole('heading', { name: APP_CARD_TITLE, exact: true }),
}).first()
await appCard.waitFor({ timeout: 30_000 })
await expect(appCard.locator('button')).toBeVisible()
})
```
**Bats Shell Test Structure:**
```bash
#!/usr/bin/env bats
# tests/lifecycle/bats/bitcoin-knots.bats
load '../lib/rpc.bash'
setup_file() {
: "${ARCHY_PASSWORD:?Set ARCHY_PASSWORD env var to the UI password}"
export ARCHY_FORCE_LOGIN=1
rpc_login
unset ARCHY_FORCE_LOGIN
}
teardown_file() {
rpc_logout_local
}
@test "container-list includes bitcoin-knots" {
run rpc_result container-list
[ "$status" -eq 0 ]
echo "$output" | jq -e '.[] | select(.name == "bitcoin-knots")' >/dev/null
}
@test "container-status returns a valid status object" {
run rpc_call container-status '{"app_id":"bitcoin-knots"}'
[ "$status" -eq 0 ]
}
```
## Mocking
**Framework (Vitest):** `vi` from Vitest; global stub support
**Patterns:**
```typescript
const mockFetch = vi.fn()
vi.stubGlobal('fetch', mockFetch)
// Import after stubbing
const { rpcClient } = await import('../rpc-client')
// In tests:
mockFetch.mockResolvedValueOnce(jsonResponse({ result: { did: 'did:key:z123' } }))
mockFetch.mockRejectedValueOnce(new Error('fetch failed'))
// Assertions on mock calls:
expect(mockFetch).toHaveBeenCalledOnce()
const [url, init] = mockFetch.mock.calls[0]!
expect(url).toBe('/rpc/v1')
expect(init.method).toBe('POST')
```
**Vue Test Utils:**
- Component mounting: `mount(Component, { global: { mocks: { $ver: displayVersion } } })`
- Props tested by passing to mount options
- Events tested by listening to emitted events
**What to Mock:**
- External HTTP requests (fetch, axios)
- Timers (for timeout logic; `vi.useFakeTimers()`)
- Global objects (localStorage, console, window.location)
**What NOT to Mock:**
- Vue reactivity (ref, computed) — these are core to component behavior
- RPC client methods in component tests — prefer integration-style testing
- Built-in assertions (expect) — always available
- Pinia stores in unit tests of composables that use them — store directly if needed
## Fixtures and Factories
**Test Data:**
```typescript
function jsonResponse(body: unknown, status = 200, statusText = 'OK'): Response {
return {
ok: status >= 200 && status < 300,
status,
statusText,
json: () => Promise.resolve(body),
// ... other Response properties
}
}
// Usage:
mockFetch.mockResolvedValueOnce(jsonResponse({ result: { did: 'did:key:z123' } }))
mockFetch.mockResolvedValueOnce(jsonResponse(null, 502, 'Bad Gateway'))
```
**Location:**
- Fixtures (test data, helper functions) defined inline in test files or in small helper modules
- No central fixture factory observed; each test file is self-contained
- Shell test helpers in `tests/lifecycle/lib/rpc.bash` (RPC wrapper for bats)
## Coverage
**Requirements:**
- Frontend: 80% branch coverage (set in `vitest.config.ts` thresholds)
- Rust: no explicit threshold; pragmatic testing of public APIs
- Shell: coverage tracked per app in `tests/lifecycle/TESTING.md` (lifecycle matrix)
**View Coverage:**
```bash
# Generate coverage report
npm run test -- --coverage
# Output formats: text, text-summary, html
# Config in vitest.config.ts: reporter: ['text', 'text-summary']
```
**Coverage Scope (Frontend):**
- Included: `src/api/*.ts`, `src/stores/*.ts`, `src/composables/*.ts`, `src/utils/*.ts`, `src/services/*.ts`, `src/router/*.ts`
- Excluded: test files (`src/**/__tests__/**`), type definitions (`*.d.ts`), entry point (`src/main.ts`)
## Test Types
**Unit Tests (Vitest):**
- Scope: individual functions, composables, utility modules
- Approach: fast, isolated, mock external dependencies
- Example: `useFileType.test.ts` tests `getFileCategory`, `useFileType`, `formatSize`, `formatDate` independently
- Latency: ~5s for full suite; individual tests <1s
**Integration Tests (Vitest + RPCClient):**
- Scope: RPC client with mocked fetch, authentication flows, retry logic
- Approach: more complex setup, test interactions between layers
- Example: `rpc-client.test.ts` tests 70+ scenarios (login, TOTP, federation, package operations)
- Latency: ~30s for full suite
**E2E Tests (Playwright):**
- Scope: real browser, real app instance, user journeys (login → navigate → interact)
- Approach: full app stack running; no mocks of UI layer
- Example: `app-launch.spec.ts` tests app card discovery and launch via button click
- Latency: 30120s per test depending on app startup time
**Lifecycle Tests (Bats):**
- Scope: container operations (install, start, stop, restart, uninstall) on a live node
- Approach: RPC calls to backend, shell commands for verification, destructive operations tier-gated
- Tiers:
- L0 unit: Rust unit tests (cargo test)
- L1 RPC: JSON-RPC API responses (bats + rpc.bash)
- L2 UI: HTTP probe of app URLs (bats + ui-probes.bash)
- L3 lifecycle survival: container restart/reboot survival (bats, gated)
- Latency: 30120s per suite depending on tier and container startup
## Common Patterns
**Async Testing (Vitest):**
```typescript
it('makes a successful RPC call and returns the result', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse({ result: { did: 'did:key:z123' } }))
const result = await rpcClient.call<{ did: string }>({
method: 'node.did',
params: {},
})
expect(result).toEqual({ did: 'did:key:z123' })
expect(mockFetch).toHaveBeenCalledOnce()
})
```
- `async` keyword on test function
- `await` for async operations
- No explicit promise handling; expect called after `await` completes
- Timeouts set via test config or `{ timeout: N }` in individual tests
**Error Testing (Vitest):**
```typescript
it('throws after max retries on persistent 502', async () => {
mockFetch.mockResolvedValue(jsonResponse(null, 502, 'Bad Gateway'))
await expect(rpcClient.call({ method: 'test' })).rejects.toThrow('HTTP 502: Bad Gateway')
expect(mockFetch).toHaveBeenCalledTimes(3)
})
it('throws immediately on non-retryable HTTP errors', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse(null, 401, 'Unauthorized'))
await expect(rpcClient.call({ method: 'test' })).rejects.toThrow('Session expired')
expect(mockFetch).toHaveBeenCalledOnce()
})
```
- `expect(...).rejects.toThrow(message)` for expected rejections
- Mock returns set per-call (`mockResolvedValueOnce`, `mockResolvedValue`)
- Retry logic verified via call count assertions (`toHaveBeenCalledTimes`)
**Timer Mocking (Vitest):**
```typescript
beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true })
})
afterEach(() => {
vi.useRealTimers()
})
it('retries on 502 Bad Gateway and eventually succeeds', async () => {
mockFetch
.mockResolvedValueOnce(jsonResponse(null, 502, 'Bad Gateway'))
.mockResolvedValueOnce(jsonResponse({ result: 'ok' }))
const result = await rpcClient.call({ method: 'test' })
expect(result).toBe('ok')
expect(mockFetch).toHaveBeenCalledTimes(2)
})
```
- Fake timers enable testing of timeout/retry delays without blocking real time
- `shouldAdvanceTime: true` auto-advances clock for non-blocking tests
- Clean up with `vi.useRealTimers()` after each test
**Vue Component Testing (Vue Test Utils):**
```typescript
it('returns correct values for audio', () => {
const ext = ref('mp3')
const isDir = ref(false)
const result = useFileType(ext, isDir)
expect(result.category.value).toBe('audio')
expect(result.isAudio.value).toBe(true)
expect(result.isImage.value).toBe(false)
expect(result.iconColor.value).toBe('text-orange-400')
})
```
- Refs created with `ref()` passed as test inputs
- Computed values accessed via `.value`
- No mount overhead for pure composable logic
**Playwright Browser Testing:**
```typescript
test('installed app launch opens reachable app URL', async ({ page, context, baseURL }) => {
await login(page)
await page.goto('/dashboard/apps', { waitUntil: 'domcontentloaded' })
const appCard = page.locator('[data-controller-container]', {
has: page.getByRole('heading', { name: APP_CARD_TITLE, exact: true }),
}).first()
await appCard.waitFor({ timeout: 30_000 })
await expect(appCard.locator('button')).toBeVisible()
})
```
- Locators used to find elements (CSS selector, role, text)
- Wait timeouts on slow networks (30s for app startup)
- `waitUntil: 'domcontentloaded'` or `'networkidle'` for page load
- Screenshots and video recording available via config
## Test Configuration Details
**Vitest Config (`vitest.config.ts`):**
- Environment: jsdom
- Globals: enabled (no imports needed)
- Setup file: `vitest.setup.ts` (mocks global Vue config like `$ver`)
- Coverage provider: v8
- Coverage threshold: 80% branches
- Excluded from coverage: tests, types, main.ts
**Playwright Config (implicit, environment variables used):**
- Base URL: derived from `VITE_*` env vars in dev
- Timeouts: per-test overrides via `{ timeout: N }`
- Retry: 0 (no automatic retries; explicit in tests via polling)
- Config environment variables: `ARCHY_PASSWORD`, `ARCHY_APP_ID`, `ARCHY_EXPECTED_LAUNCH_URL`
**Shell Test Config (environment variables):**
- `ARCHY_PASSWORD`: login password (required)
- `ARCHY_ALLOW_DESTRUCTIVE`: enable stop/start/restart/uninstall tests
- `ARCHY_ALLOW_CASCADE_DESTRUCTIVE`: enable uninstall/reinstall on throwaway app
- `ARCHY_ITERATIONS`: loop count for release gate (5× for production readiness)
- `ARCHY_FORCE_LOGIN`: fresh RPC token per test file
---
*Testing analysis: 2026-07-29*