feat: add Ollama proxy timeouts, SSH key migration, polish skills, and demo content

- Update all skill SSH commands from sshpass to key-based auth (~/.ssh/archipelago-deploy)
- Add proxy_connect_timeout 120s to nginx Ollama location blocks
- Add new polish/sweep skills for overnight automation
- Add demo content (documents, photos) for demo stack
- Add .ssh/ to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-03-08 08:06:52 +00:00
parent d3f0f1192e
commit e8a0e1af19
29 changed files with 1871 additions and 11 deletions

514
.claude/plans/plan.md Normal file
View File

@ -0,0 +1,514 @@
# Archipelago Production Polish Plan
**Duration**: 8 weeks (March 10 May 4, 2026)
**Goal**: Zero new features. Every existing feature polished to flawless production quality.
**Philosophy**: The iPhone moment — everything just works, feels inevitable, no rough edges.
## SSH Access
All remote commands use SSH key auth (password auth is disabled):
```bash
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228
```
Never use `sshpass`. The deploy script handles this automatically via `SSH_KEY`.
---
## Audit Summary
Full codebase audit completed March 8, 2026. Findings:
| Layer | Critical | High | Medium | Low |
|-------|----------|------|--------|-----|
| Frontend (Vue/TS) | 4 | 6 | 10 | 4 |
| Backend (Rust) | 6 | 6 | 6 | 7 |
| Infrastructure | 5 | 6 | 7 | 3 |
| UX Flows | 4 | 4 | 6 | 3 |
| **Total** | **19** | **22** | **29** | **17** |
---
## Skills Required
### Existing Skills (14)
`deploy`, `deploy-both`, `diagnose`, `check-server`, `frontend-dev`, `sync-configs`, `build-iso`, `server-logs`, `add-app`, `harden`, `test`, `lint`, `ux-review`, `refactor`
### New Skills (9)
| Skill | Purpose |
|-------|---------|
| `polish` | Main orchestrator — reads this plan, detects week, executes tasks |
| `polish-errors` | Fix silent error handling, add user-facing error states |
| `polish-loading` | Add skeleton loaders, loading indicators, empty states |
| `polish-forms` | Input validation, trimming, real-time feedback |
| `polish-backend` | Fix unwrap/expect, add timeouts, connection pooling |
| `polish-deploy` | Add rollback, health checks, pre-deploy validation |
| `polish-security` | Systemd hardening, nginx CSP, secrets management |
| `polish-websocket` | Reconnection UX, connection status indicator, heartbeat |
| `sweep` | Full automated quality sweep: lint + type-check + verify fixes |
---
## Week 1: Silent Failures & Error Handling (March 1016)
**Theme**: Nothing fails silently. Every error is visible, actionable, recoverable.
### Tasks
#### 1.1 Frontend: Kill all silent catch blocks
- **Files**: Settings.vue, Web5.vue, router/index.ts, Apps.vue, OnboardingIntro.vue
- **Action**: Replace 21+ `.catch(() => {})` patterns with proper error handling
- **Pattern**: Log to console in dev, show toast/inline error to user in prod
- **Acceptance**: Zero `.catch(() => {})` in codebase (grep confirms)
- **Skill**: `/polish-errors`
#### 1.2 Frontend: Remove all console.log from production
- **Files**: stores/app.ts (15+), api/websocket.ts (12+)
- **Action**: Replace with conditional dev-only logging or remove
- **Pattern**: `if (import.meta.env.DEV) console.log(...)` or remove entirely
- **Acceptance**: Zero `console.log` outside of dev guards (grep confirms)
- **Skill**: `/lint`
#### 1.3 Backend: Fix all unwrap/expect in handler.rs
- **Files**: core/archipelago/src/api/handler.rs (11 unwraps)
- **Action**: Replace `.unwrap()` on Response builders with `.map_err()` and `?`
- **Acceptance**: Zero `unwrap()` in handler.rs
- **Skill**: `/polish-backend`
#### 1.4 Backend: Fix unwrap/expect across all production paths
- **Files**: main.rs, identity.rs, totp.rs, rpc/mod.rs, image_verifier.rs
- **Action**: Audit all 32 `.unwrap()`/`.expect()` calls, replace with `?` or `.context()`
- **Acceptance**: Zero unwrap/expect outside of test modules
- **Skill**: `/polish-backend`
#### 1.5 Backend: Hardcoded Bitcoin RPC credentials
- **Files**: core/archipelago/src/api/rpc/bitcoin.rs:89
- **Action**: Move `archipelago/archipelago123` to env var or secrets manager
- **Pattern**: `std::env::var("ARCHIPELAGO_BITCOIN_RPC_USER").unwrap_or("archipelago".into())`
- **Acceptance**: No hardcoded credentials in Rust source
#### 1.6 Deploy & verify
- Run `/lint` to confirm zero violations
- Run `/deploy` to live server
- Run `/check-server` to verify health
- Manual spot-check: trigger errors in UI, confirm they're visible
---
## Week 2: Loading States & Visual Feedback (March 1723)
**Theme**: The user always knows what's happening. No blank screens, no mystery waits.
### Tasks
#### 2.1 Add skeleton loaders to all async views
- **Files**: Apps.vue, AppDetails.vue, Marketplace.vue, Cloud.vue, Server.vue, Settings.vue
- **Action**: Create `SkeletonLoader.vue` component, add to every view that fetches data
- **Pattern**: Show skeleton immediately, swap to real content on load
- **Acceptance**: Every view shows placeholder content during load
- **Skill**: `/polish-loading`
#### 2.2 Add timeout warnings to long operations
- **Files**: Login.vue (server startup), Marketplace.vue (app install)
- **Action**: After 15s show "Taking longer than expected...", after 30s show troubleshoot options
- **Acceptance**: No operation silently hangs
#### 2.3 Fix Start/Stop button state mismatch
- **Files**: Apps.vue, AppDetails.vue, ContainerApps.vue
- **Action**: Button reflects actual backend state, not a fixed 5s timer
- **Pattern**: Poll backend every 2s during state transition, update button immediately on response
- **Acceptance**: Button state always matches container state within 3s
#### 2.4 Connection status indicator
- **Files**: Create `ConnectionStatus.vue`, integrate into App.vue header
- **Action**: Show green/amber/red dot based on WebSocket connection state
- **Pattern**: Use `wsClient.isConnected()` — green=connected, amber=reconnecting, red=disconnected
- **Acceptance**: User always knows if they're connected
- **Skill**: `/polish-websocket`
#### 2.5 Fix OnlineStatusPill to use real data
- **Files**: components/OnlineStatusPill.vue
- **Action**: Connect to actual WebSocket state instead of hardcoded "Online"
- **Acceptance**: Pill reflects real connection state
#### 2.6 Empty states for all views
- **Files**: Apps.vue, Cloud.vue, ContainerApps.vue
- **Action**: When no data, show helpful message with CTA (e.g., "No apps installed — Browse Marketplace")
- **Acceptance**: Every view handles the zero-data case gracefully
#### 2.7 Deploy & verify
- `/deploy` then `/check-server`
- Test: disconnect network, observe status indicator
- Test: slow network (throttle), observe skeleton loaders
- Test: fresh account with no apps, observe empty states
---
## Week 3: Form Validation & Input Quality (March 2430)
**Theme**: Every input feels responsive, validated, impossible to misuse.
### Tasks
#### 3.1 Real-time password validation
- **Files**: Login.vue (password setup), Settings.vue (password change)
- **Action**: Show inline validation as user types: length check, match check, strength meter
- **Pattern**: Debounced validation on input, green checkmark / red X per rule
- **Acceptance**: User sees validation state before clicking submit
- **Skill**: `/polish-forms`
#### 3.2 TOTP input improvements
- **Files**: Login.vue (TOTP verify step)
- **Action**: Auto-submit on 6 digits, show session countdown timer, trim whitespace
- **Pattern**: `watch(code, () => { if (code.length === 6) submit() })`
- **Acceptance**: TOTP flow is fast and clear, session timeout is visible
#### 3.3 Input trimming on all forms
- **Files**: Login.vue, Settings.vue, any form input
- **Action**: `.trim()` all text inputs before submission
- **Acceptance**: Leading/trailing whitespace never causes failures
#### 3.4 Disable submit buttons during operations
- **Files**: Settings.vue (password change), Login.vue (login), Marketplace.vue (install)
- **Action**: Add `:disabled="isSubmitting"` to all action buttons
- **Pattern**: Button shows spinner + disabled state during async operation
- **Acceptance**: No button can be double-clicked during an operation
#### 3.5 Error message consistency
- **Files**: All views with error messages
- **Action**: Create `formatError()` utility that normalizes error messages
- **Pattern**: Network errors -> "Can't reach server", Auth errors -> "Session expired", Server errors -> "Something went wrong"
- **Acceptance**: Error messages are user-friendly, never show raw error strings
#### 3.6 Deploy & verify
- Test every form: login, password change, TOTP setup, app install
- Try invalid inputs, verify feedback is immediate and clear
---
## Week 4: Backend Robustness (March 31 April 6)
**Theme**: The backend never crashes, never hangs, handles every edge case.
### Tasks
#### 4.1 Add timeouts to all container operations
- **Files**: core/archipelago/src/container/dev_orchestrator.rs
- **Action**: Wrap all podman calls with `tokio::time::timeout(Duration::from_secs(30), ...)`
- **Acceptance**: No container operation can hang indefinitely
#### 4.2 Add timeouts to all external HTTP calls
- **Files**: bitcoin.rs, handler.rs (LND proxy)
- **Action**: Explicit `reqwest::Client` with timeout, not default
- **Pattern**: Reuse a single `Client` stored in `RpcHandler` state
- **Acceptance**: Every HTTP call has an explicit timeout
#### 4.3 Connection pooling for Bitcoin RPC
- **Files**: core/archipelago/src/api/rpc/bitcoin.rs
- **Action**: Store `reqwest::Client` in `RpcHandler`, reuse across requests
- **Acceptance**: One client instance, connection pooled
#### 4.4 Fix all clippy warnings
- **Action**: Run `cargo clippy --all-targets --all-features` on dev server, fix all 10 warnings
- **Warnings**: `should_implement_trait`, `get_first`, `assign_op_pattern`, `wildcard_in_or_patterns`, `redundant_field_names`, `unused_import`, `ptr_arg`, `very_complex_type`, `if_else_collapse`, `io::Error::other`
- **Acceptance**: `cargo clippy` returns zero warnings
- **Skill**: `/lint`
#### 4.5 Rate limiting on unauthenticated endpoints
- **Files**: core/archipelago/src/api/handler.rs
- **Action**: Add per-IP rate limiting to `/archipelago/node-message` and `/electrs-status`
- **Pattern**: In-memory rate limiter with 60 req/min per IP
- **Acceptance**: Endpoints return 429 when rate exceeded
#### 4.6 Consistent error codes and messages
- **Files**: All RPC endpoints
- **Action**: Define error code constants, consistent capitalization
- **Pattern**: `const ERR_AUTH: i32 = -1001;` etc.
- **Acceptance**: All error responses use defined constants
#### 4.7 Remove dead code
- **Files**: identity.rs (unused field, unused methods), auth.rs (dead_code allows)
- **Action**: Remove `identity_dir` field, remove unused `verify()` and `did_key()` methods, remove `#[allow(dead_code)]` and verify usage
- **Acceptance**: Zero `#[allow(dead_code)]` outside of generated code
#### 4.8 Replace println/eprintln with tracing
- **Files**: core/startos/src/* (23+ instances)
- **Action**: Replace `println!` -> `tracing::info!`, `eprintln!` -> `tracing::warn!`
- **Acceptance**: Zero `println!` / `eprintln!` in non-test code
#### 4.9 Deploy & verify
- `/deploy` then `/check-server` then `/diagnose`
- Test: kill Bitcoin container, verify backend doesn't crash
- Test: flood unauthenticated endpoint, verify rate limiting
- Test: restart backend, verify graceful startup
---
## Week 5: WebSocket & Real-Time Quality (April 713)
**Theme**: Real-time updates are bulletproof. Connection issues are transparent to the user.
### Tasks
#### 5.1 WebSocket reconnection UX
- **Files**: api/websocket.ts, App.vue
- **Action**: After max reconnect attempts, show persistent banner "Connection lost. Click to retry."
- **Pattern**: Don't silently give up after 10 attempts
- **Acceptance**: User always has a path to reconnect
- **Skill**: `/polish-websocket`
#### 5.2 WebSocket heartbeat improvement
- **Files**: api/websocket.ts
- **Action**: Send ping every 30s, expect pong within 5s, reconnect if missed
- **Acceptance**: Stale connections detected within 35s, not 60s
#### 5.3 RPC client session detection
- **Files**: api/rpc-client.ts
- **Action**: On 401/403 response, redirect to login page instead of showing generic error
- **Pattern**: `if (status === 401) { router.push('/login'); return; }`
- **Acceptance**: Expired sessions redirect to login immediately
#### 5.4 Message queuing during reconnection
- **Files**: api/rpc-client.ts, api/websocket.ts
- **Action**: If WebSocket is down, queue state-update subscriptions, replay on reconnect
- **Pattern**: Don't lose container state updates during brief disconnects
- **Acceptance**: State is consistent after reconnection without page refresh
#### 5.5 WebSocket race condition fix
- **Files**: stores/app.ts, api/websocket.ts
- **Action**: Fix duplicate listener issue on rapid reconnect (`isWsSubscribed` flag)
- **Pattern**: Use a Set of listener IDs, deduplicate on registration
- **Acceptance**: No duplicate event handlers after reconnect cycles
#### 5.6 Deploy & verify
- Test: kill backend, observe frontend reconnection behavior
- Test: toggle wifi, observe status indicator + reconnection
- Test: let session expire, verify redirect to login
---
## Week 6: Deployment & Infrastructure Hardening (April 1420)
**Theme**: Deployments are safe, reversible, and verified. Infrastructure is production-grade.
### Tasks
#### 6.1 Deploy script: add rollback capability
- **Files**: scripts/deploy-to-target.sh
- **Action**: Before overwriting binary/frontend, backup to `.backup` suffix
- **Pattern**: On health check failure after restart, restore from backup
- **Acceptance**: Failed deploy auto-restores previous working version
- **Skill**: `/polish-deploy`
#### 6.2 Deploy script: pre-deploy sanity checks
- **Files**: scripts/deploy-to-target.sh
- **Action**: Check disk space (2GB min), verify SSH key exists, verify target dir exists
- **Acceptance**: Deploy fails early with clear message if preconditions not met
#### 6.3 Deploy script: post-deploy health verification
- **Files**: scripts/deploy-to-target.sh
- **Action**: After restart, poll `/health` endpoint for 30s. If no 200, trigger rollback
- **Acceptance**: Every deploy is verified healthy before declaring success
#### 6.4 Deploy script: deployment locking
- **Files**: scripts/deploy-to-target.sh
- **Action**: Use flock to prevent concurrent deploys
- **Acceptance**: Second simultaneous deploy fails immediately with message
#### 6.5 First-boot script: add error handling
- **Files**: scripts/first-boot-containers.sh
- **Action**: Add `set -e`, verify each container starts before creating dependents
- **Acceptance**: If Bitcoin fails, Mempool is not attempted
#### 6.6 Systemd service hardening
- **Files**: image-recipe/configs/archipelago.service
- **Action**: Add `PrivateTmp=yes`, `NoNewPrivileges=true`, `ProtectSystem=strict`, `ProtectHome=yes`, `SystemCallFilter=@system-service`
- **Acceptance**: Service runs with minimal privileges
- **Skill**: `/harden`
#### 6.7 Nginx security headers
- **Files**: image-recipe/configs/nginx-archipelago.conf
- **Action**: Add HSTS, fix CSP (remove unsafe-inline), add rate limiting zones, custom log format that strips tokens
- **Acceptance**: Security headers pass Mozilla Observatory scan
#### 6.8 Nginx config: test before reload
- **Files**: scripts/deploy-to-target.sh
- **Action**: `nginx -t` failure should abort deploy and restore backup config
- **Acceptance**: Invalid nginx config never goes live
#### 6.9 Deploy & verify
- Test: deploy with intentionally broken binary, verify rollback
- Test: deploy with invalid nginx config, verify rollback
- Test: concurrent deploy attempt, verify lock
- Run `/diagnose` full check
---
## Week 7: Accessibility, Polish & Edge Cases (April 2127)
**Theme**: Every interaction is crisp. Keyboard users, slow networks, edge cases — all handled.
### Tasks
#### 7.1 ARIA labels on all interactive elements
- **Files**: All views and components
- **Action**: Add `aria-label` to buttons, links, form inputs that lack visible labels
- **Pattern**: `<button aria-label="Install Bitcoin Core" ...>`
- **Acceptance**: Every interactive element has accessible name
#### 7.2 Focus management in modals
- **Files**: Apps.vue (uninstall modal), Marketplace.vue (filter modal), Settings.vue
- **Action**: Trap focus inside modals, return focus on close, autofocus first interactive element
- **Pattern**: Use `useFocusTrap` composable
- **Acceptance**: Tab key never leaves modal; Escape closes; focus returns to trigger
#### 7.3 Keyboard navigation completeness
- **Files**: All views
- **Action**: Verify every action is reachable via keyboard (Tab/Enter/Escape)
- **Acceptance**: Full app usable without mouse
#### 7.4 Fix inline Tailwind violations
- **Files**: Web5.vue, AppDetails.vue, Cloud.vue, onboarding views
- **Action**: Extract inline classes to global classes in style.css
- **Pattern**: `px-3 py-1.5 rounded-lg bg-white/5` -> `.info-row` class
- **Acceptance**: Zero inline Tailwind utility classes in components
- **Skill**: `/ux-review`
#### 7.5 Touch feedback on mobile
- **Files**: style.css, app card components
- **Action**: Add `:active` states for mobile touch feedback
- **Pattern**: `.app-card:active { transform: scale(0.98); }`
- **Acceptance**: Every tappable element has tactile feedback
#### 7.6 Responsive edge cases
- **Files**: Marketplace.vue, Dashboard.vue, AppDetails.vue
- **Action**: Test at 320px, 375px, 768px, 1024px, 1440px widths
- **Fix**: Any overflow, text truncation, or broken layouts
- **Acceptance**: No horizontal scroll or broken layout at any standard width
#### 7.7 Fix template crash risks
- **Files**: ContainerApps.vue:76 (`app.image.split('/').pop()`)
- **Action**: Add null guards on all template expressions that chain methods
- **Pattern**: `app.image?.split('/').pop() ?? 'unknown'`
- **Acceptance**: No template expression can crash on null/undefined data
#### 7.8 Remove all TODO/FIXME from production code
- **Files**: Web5.vue, AppDetails.vue, backend TODO comments
- **Action**: Either implement the TODO or remove the dead code
- **Pattern**: If feature isn't ready, remove the UI element entirely
- **Acceptance**: Zero TODO/FIXME/HACK in committed code
- **Skill**: `/refactor`
#### 7.9 Deploy & verify
- Test: navigate entire app with keyboard only
- Test: resize browser through all breakpoints
- Test: screen reader (VoiceOver) basic navigation
- Run `/ux-review` on every view
---
## Week 8: Integration Testing, Final Sweep & ISO (April 28 May 4)
**Theme**: Everything works together. The final product is tested end-to-end and burned to ISO.
### Tasks
#### 8.1 Create critical path tests — Frontend
- **Files**: Create `neode-ui/src/__tests__/` directory
- **Tests to write**:
- Login flow: valid password, invalid password, TOTP, session timeout
- App lifecycle: install -> start -> launch -> stop -> uninstall
- Settings: password change, TOTP setup, TOTP disable
- WebSocket: connect, disconnect, reconnect
- **Framework**: Vitest + @vue/test-utils (already in package.json)
- **Acceptance**: 10+ critical path tests passing
- **Skill**: `/test`
#### 8.2 Create critical path tests — Backend
- **Tests to write**:
- RPC endpoint validation (good/bad input for each endpoint)
- Session management (create, validate, expire, invalidate)
- Container manifest parsing (valid, invalid, missing fields)
- Rate limiting (under limit, at limit, over limit)
- **Acceptance**: 10+ backend tests passing
- **Skill**: `/test`
#### 8.3 Create deployment verification test
- **Files**: scripts/verify-deploy.sh (new)
- **Action**: Script that hits every endpoint, checks every container, verifies every UI route
- **Pattern**: Automated smoke test run after every deploy
- **Acceptance**: Script exits 0 only if everything works
#### 8.4 Full quality sweep
- Run `/lint` — zero violations
- Run `/harden` — zero findings
- Run `/ux-review` — zero findings
- Run `/diagnose` — all green
- Run `/sweep` — clean bill of health
- **Acceptance**: All skills report zero issues
#### 8.5 Build final ISO
- Sync all configs: `/sync-configs`
- Build ISO: `/build-iso`
- Flash to USB, boot on clean hardware
- Verify first-boot experience end-to-end
- **Acceptance**: ISO boots, onboarding works, Bitcoin syncs, apps install
#### 8.6 Performance baseline
- Measure and document:
- Time to first meaningful paint (target: <2s)
- Login flow completion time (target: <3s)
- App install completion time (document actual)
- WebSocket reconnection time (target: <5s)
- Backend cold start time (target: <3s)
- **Acceptance**: All targets met or documented with explanation
#### 8.7 Final documentation pass
- Update `docs/current-state.md` to reflect production status
- Update `CHANGELOG.md` with all polish work
- Verify all CLAUDE.md instructions are still accurate
- **Acceptance**: Docs match reality
---
## Metrics & Definition of Done
### Per-Week Exit Criteria
Each week is "done" when:
1. All tasks for that week have acceptance criteria met
2. `/sweep` returns zero violations for that week's focus area
3. `/deploy` succeeds and `/check-server` is green
4. Manual spot-check of affected features passes
### Project Exit Criteria (Week 8)
The project is done when ALL of these are true:
- [ ] Zero `.catch(() => {})` in frontend
- [ ] Zero `console.log` outside dev guards
- [ ] Zero `unwrap()`/`expect()` in backend production paths
- [ ] Zero clippy warnings
- [ ] Zero inline Tailwind in components
- [ ] Zero TODO/FIXME in committed code
- [ ] Every view has: loading state, error state, empty state
- [ ] Every form has: real-time validation, disabled during submit
- [ ] Every button action has: loading feedback, error feedback
- [ ] WebSocket shows connection status to user
- [ ] Session timeout redirects to login
- [ ] Deploy has: rollback, health check, locking
- [ ] Systemd service is hardened
- [ ] Nginx has: HSTS, proper CSP, rate limiting, clean logs
- [ ] 10+ frontend tests passing
- [ ] 10+ backend tests passing
- [ ] ISO boots and onboards successfully
- [ ] All performance targets met
---
## Risk Register
| Risk | Mitigation |
|------|------------|
| Skeleton loaders change visual feel | Match exact glassmorphism style, use existing color tokens |
| Backend changes break existing functionality | Deploy to secondary server (198) first, test, then primary |
| Nginx CSP changes break app iframes | Test each framed app individually before deploying |
| Rate limiting blocks legitimate use | Set generous limits (60/min), monitor false positives |
| Test suite becomes maintenance burden | Only test critical paths, no unit tests for trivial code |
| ISO build captures incomplete state | Always build ISO from clean deploy, never mid-development |

View File

@ -16,13 +16,13 @@ Build a new Archipelago auto-installer ISO.
## Build (on target server — recommended)
```bash
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 'cd ~/archy/image-recipe && sudo ./build-auto-installer-iso.sh'
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 'cd ~/archy/image-recipe && sudo ./build-auto-installer-iso.sh'
```
## Copy ISO back to Mac
```bash
sshpass -p 'EwPDR8q45l0Upx@' scp -o StrictHostKeyChecking=no archipelago@192.168.1.228:~/archy/image-recipe/results/archipelago-auto-installer-*.iso .
scp -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228:~/archy/image-recipe/results/archipelago-auto-installer-*.iso .
```
**IMPORTANT**: Use `build-auto-installer-iso.sh` only. The deprecated `build-debian-iso.sh` causes boot-to-prompt issues.

View File

@ -18,6 +18,6 @@ Deploy all changes to BOTH servers (primary: 192.168.1.228, secondary: 192.168.1
3. Verify both servers respond:
```bash
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 'systemctl is-active archipelago'
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.198 'systemctl is-active archipelago'
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 'systemctl is-active archipelago'
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.198 'systemctl is-active archipelago'
```

View File

@ -18,7 +18,7 @@ Deploy all changes to the live server (192.168.1.228).
3. After deploy completes, verify the server is healthy:
```bash
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 'systemctl is-active archipelago nginx && sudo journalctl -u archipelago -n 10 --no-pager'
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 'systemctl is-active archipelago nginx && sudo journalctl -u archipelago -n 10 --no-pager'
```
4. Report whether the deploy succeeded and if any errors appeared in the logs.

View File

@ -4,7 +4,7 @@ description: Run a full diagnostic check on the Archipelago dev server
allowed-tools: Bash
---
SSH into the dev server and run a comprehensive diagnostic. Use `sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228` for all commands.
SSH into the dev server and run a comprehensive diagnostic. Use `ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228` for all commands.
## Checks to run

View File

@ -31,7 +31,7 @@ grep -rn 'console\.\(log\|warn\|error\)' src/ --include='*.ts' --include='*.vue'
## Backend Linting (on dev server)
```bash
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 \
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \
'source ~/.cargo/env && cd ~/archy/core && cargo clippy --all-targets --all-features 2>&1 && cargo fmt --all -- --check 2>&1'
```

View File

@ -0,0 +1,151 @@
# Skill: Polish Backend Quality
Fix Rust backend quality issues: eliminate panics, add timeouts, implement connection pooling, fix clippy warnings. The backend must never crash in production.
## Priority 1: Eliminate Panics
### Find all unwrap/expect in production code
```bash
ssh archipelago@192.168.1.228 "cd ~/archy && grep -rn 'unwrap()\|\.expect(' core/archipelago/src/ core/container/src/ core/security/src/ core/performance/src/ --include='*.rs' | grep -v test | grep -v '#\[test\]' | grep -v '_test.rs'"
```
### Fix patterns:
**Response builder unwraps** (handler.rs):
```rust
// BAD
Response::builder().body(body).unwrap()
// GOOD
Response::builder().body(body).map_err(|e| {
tracing::error!("Failed to build response: {}", e);
// Return a minimal 500 response
})?
```
**Socket address parsing** (main.rs):
```rust
// BAD
addr.parse().expect("Invalid bind address")
// GOOD
addr.parse().context("Invalid bind address")?
```
**TOTP secret creation** (totp.rs):
```rust
// BAD
TOTP::new(...).unwrap()
// GOOD
TOTP::new(...).map_err(|e| anyhow::anyhow!("Failed to create TOTP: {}", e))?
```
**Cosign URL parsing** (image_verifier.rs):
```rust
// BAD
sig_url.strip_prefix("cosign://").unwrap()
// GOOD
sig_url.strip_prefix("cosign://")
.ok_or_else(|| anyhow::anyhow!("Invalid cosign URL format: {}", sig_url))?
```
## Priority 2: Add Timeouts
Every external call must have an explicit timeout:
```rust
// Container operations
tokio::time::timeout(Duration::from_secs(30), podman_operation()).await
.context("Container operation timed out after 30s")??;
// HTTP calls (Bitcoin RPC, LND proxy)
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()?;
// Nostr operations
tokio::time::timeout(Duration::from_secs(15), nostr_publish()).await
.context("Nostr publish timed out")?;
```
## Priority 3: Connection Pooling
Store a reusable `reqwest::Client` in `RpcHandler`:
```rust
pub struct RpcHandler {
// ... existing fields
http_client: reqwest::Client,
}
impl RpcHandler {
pub fn new(...) -> Self {
let http_client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.pool_max_idle_per_host(5)
.build()
.expect("Failed to create HTTP client");
// ...
}
}
```
Use `self.http_client` everywhere instead of creating new clients per request.
## Priority 4: Fix Clippy Warnings
Run on dev server:
```bash
ssh archipelago@192.168.1.228 "cd ~/archy && cargo clippy --all-targets --all-features 2>&1"
```
Known warnings to fix:
- `should_implement_trait`: Implement `FromStr` for `AppManifest`
- `get_first``.first()`
- `assign_op_pattern` → use `+=`
- `wildcard_in_or_patterns` → remove redundant `_`
- `redundant_field_names` → shorthand
- `very_complex_type` → type alias
- `if_else_collapse` → simplify
## Priority 5: Replace println with tracing
```bash
ssh archipelago@192.168.1.228 "cd ~/archy && grep -rn 'println!\|eprintln!' core/ --include='*.rs' | grep -v test | grep -v target/"
```
Replace:
- `println!("...")``tracing::info!("...")`
- `eprintln!("...")``tracing::warn!("...")`
## Priority 6: Remove Dead Code
- Remove `#[allow(dead_code)]` annotations, verify if types are actually used
- Remove unused fields (e.g., `identity_dir` in NodeIdentity)
- Remove unused methods (e.g., `verify()`, `did_key()` in NodeIdentity)
## Verification
```bash
ssh archipelago@192.168.1.228 "cd ~/archy && cargo clippy --all-targets --all-features 2>&1 | grep -c 'warning'"
# Should be 0
ssh archipelago@192.168.1.228 "cd ~/archy && grep -rn 'unwrap()\|\.expect(' core/archipelago/src/ --include='*.rs' | grep -v test | grep -v '_test.rs' | wc -l"
# Should be 0 (or near-zero with justified exceptions)
ssh archipelago@192.168.1.228 "cd ~/archy && grep -rn 'println!\|eprintln!' core/ --include='*.rs' | grep -v test | grep -v target/ | wc -l"
# Should be 0
```
## Build & Deploy
All Rust changes MUST be built on the dev server, never macOS:
```bash
./scripts/deploy-to-target.sh --live
```
After deploy, verify:
```bash
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 "systemctl status archipelago && curl -s http://localhost:5678/health"
```

View File

@ -0,0 +1,176 @@
# Skill: Polish Deployment Pipeline
Harden deploy-to-target.sh with rollback capability, pre-deploy checks, post-deploy health verification, and deployment locking.
## 1. Pre-Deploy Checks
Add to the beginning of deploy-to-target.sh:
```bash
pre_deploy_checks() {
echo "Running pre-deploy checks..."
# SSH key exists
if [ ! -f "$SSH_KEY" ]; then
echo "ERROR: SSH key not found at $SSH_KEY"
exit 1
fi
# Target reachable
ssh $SSH_OPTS "$TARGET_HOST" "echo ok" >/dev/null 2>&1 || {
echo "ERROR: Cannot reach $TARGET_HOST"
exit 1
}
# Disk space (need 2GB free)
local free_kb=$(ssh $SSH_OPTS "$TARGET_HOST" "df /home | tail -1 | awk '{print \$4}'")
if [ "$free_kb" -lt 2097152 ]; then
echo "ERROR: Need 2GB free disk space, have $(( free_kb / 1024 ))MB"
exit 1
fi
echo "Pre-deploy checks passed"
}
```
## 2. Backup Before Deploy
Before overwriting binary or frontend:
```bash
backup_current() {
echo "Backing up current deployment..."
ssh $SSH_OPTS "$TARGET_HOST" "
# Backup binary
if [ -f /usr/local/bin/archipelago ]; then
sudo cp /usr/local/bin/archipelago /usr/local/bin/archipelago.backup
fi
# Backup frontend
if [ -d /opt/archipelago/web-ui ]; then
sudo cp -a /opt/archipelago/web-ui /opt/archipelago/web-ui.backup
fi
# Backup nginx config
if [ -f /etc/nginx/sites-available/archipelago ]; then
sudo cp /etc/nginx/sites-available/archipelago /etc/nginx/sites-available/archipelago.backup
fi
"
echo "Backup complete"
}
```
## 3. Post-Deploy Health Check
After restarting services:
```bash
health_check() {
echo "Running post-deploy health check..."
local max_attempts=15
local attempt=0
while [ $attempt -lt $max_attempts ]; do
attempt=$((attempt + 1))
local status=$(ssh $SSH_OPTS "$TARGET_HOST" "curl -s -o /dev/null -w '%{http_code}' http://localhost:5678/health" 2>/dev/null)
if [ "$status" = "200" ]; then
echo "Health check passed (attempt $attempt)"
return 0
fi
echo "Health check attempt $attempt/$max_attempts (status: $status)"
sleep 2
done
echo "ERROR: Health check failed after $max_attempts attempts"
return 1
}
```
## 4. Rollback on Failure
If health check fails:
```bash
rollback() {
echo "ROLLING BACK deployment..."
ssh $SSH_OPTS "$TARGET_HOST" "
# Restore binary
if [ -f /usr/local/bin/archipelago.backup ]; then
sudo cp /usr/local/bin/archipelago.backup /usr/local/bin/archipelago
fi
# Restore frontend
if [ -d /opt/archipelago/web-ui.backup ]; then
sudo rm -rf /opt/archipelago/web-ui
sudo mv /opt/archipelago/web-ui.backup /opt/archipelago/web-ui
fi
# Restore nginx
if [ -f /etc/nginx/sites-available/archipelago.backup ]; then
sudo cp /etc/nginx/sites-available/archipelago.backup /etc/nginx/sites-available/archipelago
sudo nginx -t && sudo systemctl reload nginx
fi
# Restart with old binary
sudo systemctl restart archipelago
"
echo "Rollback complete. Previous version restored."
}
```
## 5. Deployment Lock
Prevent concurrent deploys:
```bash
LOCK_FILE="/tmp/archipelago-deploy.lock"
acquire_lock() {
exec 9>"$LOCK_FILE"
flock -n 9 || {
echo "ERROR: Another deployment is in progress"
exit 1
}
trap "flock -u 9; rm -f $LOCK_FILE" EXIT
}
```
## 6. Nginx Config Validation
Before reloading nginx:
```bash
validate_nginx() {
ssh $SSH_OPTS "$TARGET_HOST" "sudo nginx -t" 2>&1 || {
echo "ERROR: Nginx config invalid. Restoring backup..."
ssh $SSH_OPTS "$TARGET_HOST" "
sudo cp /etc/nginx/sites-available/archipelago.backup /etc/nginx/sites-available/archipelago
sudo nginx -t && sudo systemctl reload nginx
"
return 1
}
}
```
## Integration
The deploy flow becomes:
1. `acquire_lock`
2. `pre_deploy_checks`
3. `backup_current`
4. Build + deploy (existing logic)
5. `validate_nginx`
6. Restart services
7. `health_check || rollback`
## Verification
Test the rollback:
1. Deploy a working version
2. Intentionally break the binary (e.g., truncate it)
3. Deploy the broken version
4. Verify rollback triggers and previous version is restored
5. Verify service is healthy after rollback
## Deploy
```bash
./scripts/deploy-to-target.sh --live
```
After modifying the deploy script itself, test with a known-good deploy first.

View File

@ -0,0 +1,82 @@
# Skill: Polish Error Handling
Fix silent error handling patterns across the entire codebase. Every async operation must have visible, actionable error feedback for the user.
## What to Fix
### Frontend (neode-ui/src/)
1. **Silent catch blocks**: Find and replace all `.catch(() => {})` patterns
- Search: `grep -rn "catch.*=>.*{}" --include="*.vue" --include="*.ts" src/`
- Replace with: proper error logging + user-visible feedback (toast, inline error, or modal)
- Pattern:
```typescript
.catch((err) => {
console.error('[ComponentName] operation failed:', err)
errorMessage.value = formatError(err)
})
```
2. **Unhandled router.push**: Find `router.push(...).catch(() => {})`
- Replace with: `router.push(...).catch(console.error)` minimum
- Or handle NavigationDuplicated gracefully
3. **Silent try/catch**: Find `try { ... } catch { /* empty */ }`
- Every catch block must either: log the error, show user feedback, or explicitly comment why it's safe to ignore
4. **Missing error states**: For each view, verify:
- `ref<string | null>` error variable exists
- Error is displayed in template (inline message, not just console)
- Error clears on retry or navigation
### Backend (core/)
5. **Silent error swallowing**: Find `unwrap_or_default()` on serialization
- Replace with proper error propagation or logging
- Pattern: `.map_err(|e| anyhow::anyhow!("Serialization failed: {}", e))?`
6. **Error response consistency**: All RPC errors should use:
- Consistent error codes (not random negative numbers)
- Human-readable messages
- Consistent JSON structure
## Verification
After fixes, run:
```bash
# Zero silent catches
grep -rn "catch.*=>.*{}\|catch\s*{" neode-ui/src/ --include="*.vue" --include="*.ts" | grep -v node_modules | grep -v "console\|error\|log\|warn"
# Zero empty catch blocks
grep -rn "catch.*{$" neode-ui/src/ --include="*.vue" --include="*.ts" -A1 | grep -P "^\d+-\s*\}"
```
Both should return zero results.
## Error Display Pattern
Use this consistent pattern for user-facing errors:
```typescript
const errorMessage = ref<string | null>(null)
async function doAction() {
errorMessage.value = null
try {
await rpcClient.someCall()
} catch (err) {
errorMessage.value = err instanceof Error ? err.message : 'Operation failed'
}
}
```
Template:
```vue
<p v-if="errorMessage" class="text-red-400 text-sm mt-2">{{ errorMessage }}</p>
```
## Deploy After Fixes
Always deploy and verify on live server after making changes:
```bash
./scripts/deploy-to-target.sh --live
```

View File

@ -0,0 +1,120 @@
# Skill: Polish Form Validation
Improve all form inputs to have real-time validation feedback, proper trimming, disabled states during submission, and consistent error messaging.
## Forms to Polish
### 1. Login.vue — Password Setup
- Real-time validation as user types (debounced 300ms):
- Length >= 8 chars (show checkmark/X)
- Passwords match (show match indicator)
- Trim input on submit
- Disable submit button while `isSubmitting`
- Clear error on new input
### 2. Login.vue — TOTP Verification
- `inputmode="numeric"` + `pattern="[0-9]*"`
- Auto-submit when 6 digits entered
- Show session timeout countdown if applicable
- Trim and strip non-numeric characters on paste
### 3. Settings.vue — Password Change
- Real-time strength validation:
- 12+ characters
- Has uppercase, lowercase, digit, special char
- New password matches confirmation
- Show strength meter (weak/medium/strong)
- Disable button during submission
- Show spinner in button during async operation
### 4. Any other form inputs found across views
## Validation Pattern
```typescript
const password = ref('')
const confirmPassword = ref('')
const isSubmitting = ref(false)
const passwordErrors = computed(() => {
const errors: string[] = []
if (password.value.length > 0 && password.value.length < 8)
errors.push('Must be at least 8 characters')
return errors
})
const passwordsMatch = computed(() =>
confirmPassword.value.length > 0 && password.value === confirmPassword.value
)
async function submit() {
if (isSubmitting.value) return
isSubmitting.value = true
try {
await rpcClient.call(...)
} catch (err) {
errorMessage.value = formatError(err)
} finally {
isSubmitting.value = false
}
}
```
## Template Pattern
```vue
<input v-model="password" type="password" class="glass-input" />
<ul v-if="passwordErrors.length" class="text-red-400 text-xs mt-1 space-y-0.5">
<li v-for="err in passwordErrors" :key="err">{{ err }}</li>
</ul>
<button
class="glass-button"
:disabled="isSubmitting || passwordErrors.length > 0"
@click="submit"
>
<span v-if="isSubmitting">Saving...</span>
<span v-else>Save</span>
</button>
```
## Input Trimming
All text inputs should be trimmed before submission:
```typescript
const trimmed = password.value.trim()
```
## Error Message Consistency
Create or use a `formatError` utility:
```typescript
function formatError(err: unknown): string {
if (err instanceof Error) {
if (err.message.includes('fetch') || err.message.includes('network'))
return 'Unable to reach server. Check your connection.'
if (err.message.includes('401') || err.message.includes('unauthorized'))
return 'Session expired. Please log in again.'
return err.message
}
return 'Something went wrong. Please try again.'
}
```
## Verification
For each form:
- [ ] Real-time validation shows feedback as user types
- [ ] Submit button disabled during operation
- [ ] Submit button disabled when validation fails
- [ ] Inputs trimmed before submission
- [ ] Error messages are user-friendly (no raw error strings)
- [ ] Success feedback shown after completion
## Deploy After Fixes
```bash
./scripts/deploy-to-target.sh --live
```
Test each form with: valid input, invalid input, empty input, whitespace-only input, rapid double-click on submit.

View File

@ -0,0 +1,83 @@
# Skill: Polish Loading States
Add skeleton loaders, loading indicators, timeout warnings, and empty states to all async views. No view should ever show a blank screen.
## Skeleton Loader Component
Create or use a `SkeletonLoader.vue` component with the glassmorphism style:
- Background: `bg-white/5` with shimmer animation
- Rounded corners matching the card it replaces
- Animate with CSS `@keyframes shimmer` (translate gradient left to right)
- Must use global classes from style.css, not inline Tailwind
## Views to Fix
For EACH view in `neode-ui/src/views/`, verify these states exist:
### 1. Loading State
- Show skeleton placeholders immediately on mount
- Pattern:
```vue
<template>
<div v-if="isLoading">
<!-- Skeleton matching the layout -->
</div>
<div v-else>
<!-- Real content -->
</div>
</template>
```
### 2. Empty State
- When data loads but is empty (zero items)
- Show helpful message with CTA
- Pattern:
```vue
<div v-if="!isLoading && items.length === 0" class="glass-card text-center py-12">
<p class="text-white/60">No apps installed yet</p>
<router-link to="/marketplace" class="glass-button mt-4">Browse Marketplace</router-link>
</div>
```
### 3. Timeout Warning
- After 15 seconds of loading, show "Taking longer than expected..."
- After 30 seconds, show troubleshooting options
- Pattern:
```typescript
const loadingTooLong = ref(false)
let timeout: ReturnType<typeof setTimeout>
onMounted(() => {
timeout = setTimeout(() => { loadingTooLong.value = true }, 15000)
})
watch(isLoading, (val) => { if (!val) clearTimeout(timeout) })
```
## Priority Views (must have all 3 states)
1. **Apps.vue** — app grid skeleton, "No apps installed" empty state
2. **AppDetails.vue** — detail card skeleton, loading indicator
3. **Marketplace.vue** — app card grid skeleton, "Loading apps..." with timeout
4. **Dashboard.vue** — metric card skeletons
5. **Cloud.vue** — file list skeleton, "No files" empty state
6. **Settings.vue** — settings section skeleton
7. **Server.vue** — server info skeleton
## Verification
For each view, confirm:
- [ ] `isLoading` ref exists and is set properly
- [ ] Template has `v-if="isLoading"` skeleton section
- [ ] Template has empty state for zero-data case
- [ ] Loading timeout warning after 15s
- [ ] Skeleton uses global classes, not inline Tailwind
## Deploy After Fixes
Always deploy and verify on live server:
```bash
./scripts/deploy-to-target.sh --live
```
Test by throttling network in browser DevTools to observe loading states.

View File

@ -0,0 +1,157 @@
# Skill: Polish Security
Security hardening pass for systemd, nginx, secrets management, and rate limiting.
## 1. Systemd Service Hardening
File: `image-recipe/configs/archipelago.service`
Add these directives to the `[Service]` section:
```ini
PrivateTmp=yes
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/archipelago
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
```
After editing, sync to server and verify:
```bash
ssh archipelago@192.168.1.228 "sudo systemd-analyze security archipelago"
```
## 2. Nginx Security Headers
File: `image-recipe/configs/nginx-archipelago.conf`
### Add HSTS (HTTPS block only):
```nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
```
### Fix CSP (remove unsafe-inline):
Replace:
```nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; frame-src 'self' http://localhost:* http://192.168.*:*;" always;
```
With CSP that uses nonces or hashes for inline scripts/styles. If inline scripts can't be removed yet, document which ones and plan their removal.
### Add rate limiting zones:
```nginx
# In http block:
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;
# On login/auth endpoints:
limit_req zone=auth burst=3 nodelay;
# On API endpoints:
limit_req zone=api burst=50 nodelay;
```
### Custom log format (strip tokens):
```nginx
log_format no_tokens '$remote_addr - $remote_user [$time_local] "$request_method $uri $server_protocol" $status $body_bytes_sent "$http_referer"';
access_log /var/log/nginx/archipelago_access.log no_tokens;
```
## 3. Secrets Management
### Remove hardcoded RPC credentials from scripts
File: `scripts/deploy-to-target.sh`
Replace:
```bash
-e CORE_RPC_USERNAME=archipelago -e CORE_RPC_PASSWORD=archipelago123
```
With:
```bash
-e CORE_RPC_USERNAME=archipelago -e CORE_RPC_PASSWORD=$(cat /var/lib/archipelago/secrets/bitcoin-rpc-pass)
```
### Generate secrets on first boot
File: `scripts/first-boot-containers.sh`
Add at the top:
```bash
SECRETS_DIR="/var/lib/archipelago/secrets"
mkdir -p "$SECRETS_DIR"
chmod 700 "$SECRETS_DIR"
# Generate Bitcoin RPC password if not exists
if [ ! -f "$SECRETS_DIR/bitcoin-rpc-pass" ]; then
openssl rand -base64 24 > "$SECRETS_DIR/bitcoin-rpc-pass"
chmod 600 "$SECRETS_DIR/bitcoin-rpc-pass"
fi
```
### Remove hardcoded credentials from Rust backend
File: `core/archipelago/src/api/rpc/bitcoin.rs`
Replace:
```rust
.basic_auth("archipelago", Some("archipelago123"))
```
With:
```rust
let rpc_user = std::env::var("ARCHIPELAGO_BITCOIN_RPC_USER").unwrap_or_else(|_| "archipelago".into());
let rpc_pass = std::env::var("ARCHIPELAGO_BITCOIN_RPC_PASS").unwrap_or_else(|_| "archipelago123".into());
.basic_auth(&rpc_user, Some(&rpc_pass))
```
## 4. Rate Limiting on Backend
File: `core/archipelago/src/api/handler.rs`
Add rate limiting to unauthenticated endpoints:
- `/archipelago/node-message` — 10 req/min per IP
- `/electrs-status` — 30 req/min per IP
Use an in-memory `HashMap<IpAddr, (Instant, u32)>` with cleanup on access.
## 5. SSH Hardening
File: `scripts/deploy-to-target.sh`
Replace:
```bash
SSH_OPTS="-o StrictHostKeyChecking=no"
```
With:
```bash
SSH_OPTS="-o StrictHostKeyChecking=accept-new"
```
And add SSH key validation:
```bash
if [ ! -f "$SSH_KEY" ]; then
echo "ERROR: SSH key not found at $SSH_KEY"
exit 1
fi
```
## Verification Checklist
- [ ] `systemd-analyze security archipelago` score < 5.0 (lower is better)
- [ ] Nginx headers pass: `curl -I http://192.168.1.228 | grep -i 'strict-transport\|content-security\|x-frame'`
- [ ] No hardcoded passwords in scripts: `grep -rn 'archipelago123' scripts/ core/`
- [ ] Rate limiting works: rapid-fire requests get 429
- [ ] SSH key required (no password fallback)
## Deploy
After changes, sync configs and deploy:
```bash
./scripts/deploy-to-target.sh --live
```
Then sync to ISO recipe:
```bash
# Run /sync-configs skill
```

View File

@ -0,0 +1,167 @@
# Skill: Polish WebSocket & Real-Time
Improve WebSocket reliability, reconnection UX, heartbeat, session timeout detection, and connection status indicators.
## 1. Connection Status Indicator
### Create or update connection status display
- **Location**: App.vue header or create ConnectionStatus.vue component
- **States**: Connected (green), Reconnecting (amber pulse), Disconnected (red)
- **Data source**: `wsClient.isConnected()` from websocket.ts
- **Style**: Use existing design tokens, small dot + text in header area
```vue
<div class="flex items-center gap-1.5">
<div :class="[
'w-2 h-2 rounded-full',
isConnected ? 'bg-green-400' : isReconnecting ? 'bg-amber-400 animate-pulse' : 'bg-red-400'
]" />
<span class="text-xs text-white/40">
{{ isConnected ? '' : isReconnecting ? 'Reconnecting...' : 'Offline' }}
</span>
</div>
```
### Fix OnlineStatusPill.vue
- Connect to actual WebSocket state instead of hardcoded "Online"
- Use the app store's connection state
## 2. Reconnection UX
### Don't silently give up
File: `api/websocket.ts`
After max reconnect attempts (currently 10), instead of silently stopping:
- Set a `permanentlyDisconnected` flag
- Emit event that App.vue listens to
- Show persistent banner: "Connection lost. Click to retry." or "Refresh page to reconnect."
```typescript
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.shouldReconnect = false
this.notifyConnectionState(false)
// Emit permanent disconnect event
this.onPermanentDisconnect?.()
}
```
### Allow manual reconnect
Add a `forceReconnect()` method that resets attempt counter and tries again:
```typescript
forceReconnect() {
this.reconnectAttempts = 0
this.shouldReconnect = true
this.connect()
}
```
## 3. Heartbeat Improvement
File: `api/websocket.ts`
Current: 60-second stale detection (passive).
Target: 30-second active ping with 5-second pong timeout.
```typescript
private startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }))
this.pongTimeout = setTimeout(() => {
// No pong received — connection is dead
this.ws?.close()
this.handleReconnect()
}, 5000)
}
}, 30000)
}
// In message handler:
if (data.type === 'pong') {
clearTimeout(this.pongTimeout)
return
}
```
Note: Backend must respond to `ping` with `pong`. Check handler.rs WebSocket handler.
## 4. Session Timeout Detection
File: `api/rpc-client.ts`
When RPC returns 401 or 403:
```typescript
if (response.status === 401 || response.status === 403) {
// Session expired — redirect to login
window.location.href = '/login'
return
}
```
This should be in the base `call()` method so it applies to all RPC calls.
## 5. Fix Race Condition on Reconnect
File: `stores/app.ts` or `api/websocket.ts`
Problem: `isWsSubscribed` flag doesn't prevent duplicate listeners on rapid reconnect.
Fix: Use listener deduplication:
```typescript
private listeners = new Map<string, Set<Function>>()
subscribe(event: string, callback: Function) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set())
}
this.listeners.get(event)!.add(callback)
}
```
Or simpler: remove all listeners before reconnect, then re-add:
```typescript
onReconnect() {
// Clear old subscriptions
this.removeAllListeners()
// Re-subscribe
this.setupSubscriptions()
}
```
## 6. Message Queuing During Disconnect
When WebSocket is down, queue subscription requests:
```typescript
private pendingSubscriptions: Array<() => void> = []
subscribe(event: string, callback: Function) {
if (this.ws?.readyState !== WebSocket.OPEN) {
this.pendingSubscriptions.push(() => this.subscribe(event, callback))
return
}
// Normal subscribe logic
}
onReconnected() {
// Replay pending subscriptions
const pending = [...this.pendingSubscriptions]
this.pendingSubscriptions = []
pending.forEach(fn => fn())
}
```
## Verification
1. **Kill backend** → frontend shows "Disconnected" → restart backend → frontend reconnects and shows "Connected"
2. **Toggle wifi** → status indicator updates → wifi back → auto-reconnect
3. **Wait for session timeout** → next RPC call redirects to login
4. **Rapid reconnect** → no duplicate event handlers (check with DevTools)
5. **Leave tab in background** → come back → status is accurate
## Deploy
```bash
./scripts/deploy-to-target.sh --live
```
Test with browser DevTools Network tab to observe WebSocket frames.

View File

@ -0,0 +1,104 @@
# Skill: Production Polish (Overnight Orchestrator)
Main entry point for the Archipelago production polish plan. Reads `plan.md` at the project root, determines the current week based on today's date, and executes the tasks for that week.
## How It Works
1. Read `plan.md` from the project root
2. Determine the current week from the schedule:
- Week 1: March 1016 — Silent Failures & Error Handling
- Week 2: March 1723 — Loading States & Visual Feedback
- Week 3: March 2430 — Form Validation & Input Quality
- Week 4: March 31 April 6 — Backend Robustness
- Week 5: April 713 — WebSocket & Real-Time Quality
- Week 6: April 1420 — Deployment & Infrastructure Hardening
- Week 7: April 2127 — Accessibility, Polish & Edge Cases
- Week 8: April 28 May 4 — Integration Testing, Final Sweep & ISO
3. Execute tasks for the current week, in order
4. After completing tasks, run `/sweep` to verify
5. Deploy and verify with `/deploy` then `/check-server`
## Execution Flow
### Step 1: Read the plan
```
Read plan.md and find the current week's section
```
### Step 2: Check what's already done
Run the verification checks for the current week's tasks. For example in Week 1:
- Count remaining `.catch(() => {})` patterns
- Count remaining `console.log` outside dev guards
- Count remaining `unwrap()` in backend production code
- Check if hardcoded credentials still exist
### Step 3: Work on the next incomplete task
Pick the first task in the current week that still has violations (hasn't met its acceptance criteria). Fix violations one file at a time:
1. Read the file
2. Apply the fix following the pattern described in the task
3. Verify the fix compiles/type-checks
4. Move to the next violation
### Step 4: Verify after each batch of fixes
After fixing all violations for a task:
- Frontend: `cd neode-ui && npx vue-tsc --noEmit`
- Backend: `ssh archipelago@192.168.1.228 "cd ~/archy && cargo check"`
- Run the task's specific acceptance grep/check
### Step 5: Deploy when a task is complete
When all violations for a task are fixed and verified:
```bash
./scripts/deploy-to-target.sh --live
```
Then verify:
```bash
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 "systemctl is-active archipelago && curl -s http://localhost:5678/health"
```
### Step 6: Move to the next task
Repeat Steps 3-5 for the next incomplete task in the current week.
### Step 7: When all tasks are done
Run `/sweep` for a full quality report. If clean, the week is complete.
## Rules
- **Never change functionality** — only improve quality of existing code
- **Never change the design** — use existing glassmorphism classes, color tokens, and layout patterns
- **Always deploy after changes** — don't leave undeployed code
- **Always verify after deploy** — check server health
- **Build Rust on the dev server** — never compile Rust on macOS
- **Commit after each completed task** — atomic commits with `fix:` or `refactor:` prefix
- **If something breaks, revert** — don't push forward with broken code
## Arguments
If `$ARGUMENTS` is provided:
- `week N` — Force execution of week N regardless of date
- `task N.M` — Execute only task N.M (e.g., `task 1.3`)
- `status` — Show completion status for all weeks without executing
- `sweep` — Run sweep only, no fixes
## Example Usage
```
/polish # Auto-detect week, work on next incomplete task
/polish week 1 # Force Week 1 tasks
/polish task 1.3 # Work on just task 1.3
/polish status # Show what's done and what's left
/polish sweep # Just run the quality sweep
```
## For Overnight TUI
Launch with:
```
/loop 30m /polish
```
Each 30-minute cycle:
1. Checks current week
2. Finds next incomplete task
3. Fixes as many violations as possible in the time available
4. Deploys and verifies
5. Reports progress

View File

@ -5,7 +5,7 @@ allowed-tools: Bash
argument-hint: "[backend|nginx|container-name]"
---
View logs from the Archipelago server (192.168.1.228). Use `sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228` for all commands.
View logs from the Archipelago server (192.168.1.228). Use `ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228` for all commands.
If $ARGUMENTS is provided, show logs for that specific service. Otherwise, show backend logs by default.

View File

@ -0,0 +1,105 @@
# Skill: Quality Sweep
Full automated quality sweep across the entire codebase. Detects regressions, violations, and quality issues. This is the overnight watchdog.
Run all checks below sequentially. For each check, use the Grep tool (not bash grep) for local file scanning, and Bash for remote/build commands. Report a summary at the end.
## Checks
### 1. TypeScript Type Check
Run in bash:
```bash
cd /Users/dorian/Projects/archy/neode-ui && npx vue-tsc --noEmit 2>&1 | tail -20
```
PASS = zero errors. Count any errors found.
### 2. Frontend Violations
Use the Grep tool to scan `neode-ui/src/` for each pattern. Count matches for each:
**Silent catch blocks** — pattern: `catch\s*\(\s*\)\s*=>?\s*\{\s*\}` or `\.catch\(\(\)\s*=>\s*\{\}` in `*.vue` and `*.ts` files
**console.log in prod** — pattern: `console\.(log|warn|error)` in `*.vue` and `*.ts` files. Exclude lines containing `import.meta.env.DEV` or `// dev-only`
**any type usage** — pattern: `:\s*any[^a-zA-Z]|as\s+any[^a-zA-Z]` in `*.vue` and `*.ts` files. Exclude `.d.ts` files
**TODO/FIXME/HACK** — pattern: `TODO|FIXME|HACK|XXX` in `*.vue` and `*.ts` files
**Banned CSS classes** — pattern: `gradient-button|gradient-card` in `*.vue` files
### 3. Backend Violations (via SSH)
Run in bash:
```bash
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 "
echo '--- unwrap/expect ---'
grep -rn 'unwrap()\|\.expect(' ~/archy/core/archipelago/src/ ~/archy/core/container/src/ ~/archy/core/security/src/ --include='*.rs' | grep -v test | grep -v '_test.rs' | grep -v target/ | wc -l
echo '--- println/eprintln ---'
grep -rn 'println!\|eprintln!' ~/archy/core/ --include='*.rs' | grep -v test | grep -v target/ | wc -l
echo '--- TODO/FIXME ---'
grep -rn 'TODO\|FIXME\|HACK' ~/archy/core/ --include='*.rs' | grep -v target/ | wc -l
"
```
### 4. Hardcoded Credentials
Use Grep tool locally — pattern: `archipelago123|password123` in `core/` and `scripts/` directories, excluding `target/`, `node_modules/`, and `deploy-config.sh`
### 5. Server Health
Run in bash:
```bash
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 "
echo 'service:' \$(systemctl is-active archipelago)
echo 'health:' \$(curl -s -o /dev/null -w '%{http_code}' http://localhost:5678/health)
echo 'containers:' \$(podman ps -q 2>/dev/null | wc -l || docker ps -q | wc -l)
echo 'errors:' \$(journalctl -u archipelago --since '1 hour ago' --no-pager -p err 2>/dev/null | wc -l)
echo 'disk:' \$(df -h / | tail -1 | awk '{print \$5}')
"
```
### 6. Frontend Build
Run in bash:
```bash
cd /Users/dorian/Projects/archy/neode-ui && npm run build 2>&1 | tail -5
```
PASS = exit code 0.
## Report Format
After all checks, output a summary exactly like this:
```
=== SWEEP REPORT ===
TypeScript: PASS/FAIL (N errors)
Silent catches: PASS/FAIL (N)
Console.log: PASS/FAIL (N)
Any types: PASS/FAIL (N)
TODOs: PASS/FAIL (N)
Banned classes: PASS/FAIL (N)
Backend unwrap: PASS/FAIL (N)
Backend println: PASS/FAIL (N)
Hardcoded creds: PASS/FAIL (N)
Server health: PASS/FAIL
Frontend build: PASS/FAIL
Total violations: N
```
PASS = zero violations for that check. FAIL = one or more.
## Auto-Fix Rules
Safe to auto-fix without asking:
- `cargo fmt --all` on dev server (formatting only)
- Trailing whitespace removal
- Import ordering
Do NOT auto-fix (flag for review):
- Error handling changes
- Logic or behavior changes
- Anything in core/ Rust files beyond formatting
## Reference
Full plan with weekly task breakdown: `plan.md` (project root)
Current week's focus determines which violations are highest priority.

View File

@ -11,12 +11,12 @@ Sync system configuration files from the live server back to the repo for ISO bu
1. **Capture systemd service**:
```bash
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 'sudo cat /etc/systemd/system/archipelago.service' > image-recipe/configs/archipelago.service
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 'sudo cat /etc/systemd/system/archipelago.service' > image-recipe/configs/archipelago.service
```
2. **Capture nginx config**:
```bash
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 'sudo cat /etc/nginx/sites-available/archipelago' > image-recipe/configs/nginx-archipelago.conf
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 'sudo cat /etc/nginx/sites-available/archipelago' > image-recipe/configs/nginx-archipelago.conf
```
3. **Capture any custom scripts** in `/opt/archipelago/scripts/` if they've changed.

View File

@ -13,7 +13,7 @@ Run or create tests for $ARGUMENTS.
### Run existing tests
```bash
# On dev server (never build Rust on macOS)
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 \
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \
'source ~/.cargo/env && cd ~/archy/core && cargo test --all-features 2>&1'
```

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
# SSH keys (sandbox copies)
.ssh/
# Rust build output
target/
**/target/

View File

@ -0,0 +1,39 @@
{
"backups": [
{
"id": "bkp-2025-03-01",
"timestamp": "2025-03-01T02:00:00Z",
"type": "full",
"apps": ["bitcoin-knots", "lnd", "mempool", "nextcloud"],
"size_mb": 2340,
"status": "success",
"encrypted": true,
"destination": "local-usb"
},
{
"id": "bkp-2025-02-15",
"timestamp": "2025-02-15T02:00:00Z",
"type": "incremental",
"apps": ["bitcoin-knots", "lnd"],
"size_mb": 156,
"status": "success",
"encrypted": true,
"destination": "local-usb"
},
{
"id": "bkp-2025-02-01",
"timestamp": "2025-02-01T02:00:00Z",
"type": "full",
"apps": ["bitcoin-knots", "lnd", "mempool", "nextcloud", "vaultwarden"],
"size_mb": 2890,
"status": "success",
"encrypted": true,
"destination": "local-usb"
}
],
"schedule": {
"full": "1st of month, 02:00 UTC",
"incremental": "15th of month, 02:00 UTC"
},
"retention": "6 months"
}

View File

@ -0,0 +1,28 @@
# Bitcoin Whitepaper Notes
## Key Concepts
### Peer-to-Peer Electronic Cash
- No trusted third party needed
- Double-spending solved via proof-of-work
- Longest chain = truth
### Proof of Work
- SHA-256 based hashing
- Difficulty adjusts every 2016 blocks (~2 weeks)
- Incentive: block reward + transaction fees
### Network
- Nodes broadcast transactions
- Miners collect into blocks
- Blocks are chained via hash references
## My Thoughts
- The 21M supply cap is genius - digital scarcity
- Lightning Network solves the scaling concern
- Self-custody is the whole point
## Reading List
- [ ] Mastering Bitcoin - Andreas Antonopoulos
- [ ] The Bitcoin Standard - Saifedean Ammous
- [ ] Programming Bitcoin - Jimmy Song

View File

@ -0,0 +1,8 @@
channel_id,peer_alias,capacity_sats,local_balance,remote_balance,status,opened_date
ch_001,ACINQ,5000000,2450000,2550000,active,2024-11-15
ch_002,WalletOfSatoshi,2000000,1200000,800000,active,2024-12-01
ch_003,Voltage,10000000,4500000,5500000,active,2024-12-20
ch_004,Kraken,3000000,1800000,1200000,active,2025-01-05
ch_005,BitcoinBeach,1000000,500000,500000,active,2025-01-22
ch_006,LNBig,8000000,3200000,4800000,active,2025-02-10
ch_007,Breez,1500000,900000,600000,inactive,2025-02-28
1 channel_id peer_alias capacity_sats local_balance remote_balance status opened_date
2 ch_001 ACINQ 5000000 2450000 2550000 active 2024-11-15
3 ch_002 WalletOfSatoshi 2000000 1200000 800000 active 2024-12-01
4 ch_003 Voltage 10000000 4500000 5500000 active 2024-12-20
5 ch_004 Kraken 3000000 1800000 1200000 active 2025-01-05
6 ch_005 BitcoinBeach 1000000 500000 500000 active 2025-01-22
7 ch_006 LNBig 8000000 3200000 4800000 active 2025-02-10
8 ch_007 Breez 1500000 900000 600000 inactive 2025-02-28

View File

@ -0,0 +1,31 @@
# Archipelago Node Setup Checklist
## Hardware
- [x] Intel NUC / Mini PC (16GB RAM minimum)
- [x] 2TB NVMe SSD (for Bitcoin blockchain)
- [x] USB drive for Archipelago installer
- [x] Ethernet cable (recommended over WiFi)
## Initial Setup
- [x] Flash Archipelago ISO to USB
- [x] Boot from USB, follow installer
- [x] Set admin password
- [x] Connect to local network
## Core Apps
- [x] Bitcoin Knots - Full node validation
- [x] LND - Lightning Network
- [x] Mempool Explorer - Block/tx visualizer
- [ ] BTCPay Server - Payment processing
- [ ] Fedimint - Federated mint
## Security
- [x] Enable 2FA (TOTP)
- [x] Set up Tor hidden services
- [ ] Configure Tailscale VPN for remote access
- [ ] Back up node identity (DID)
## Monitoring
- [ ] Set up Grafana dashboards
- [ ] Configure Uptime Kuma alerts
- [ ] Review system logs weekly

View File

@ -0,0 +1,37 @@
THE SOVEREIGNTY MANIFESTO
=========================
We hold these truths to be self-evident:
1. Your data belongs to you.
Not to corporations. Not to governments. Not to platforms.
To you.
2. Your money should be uncensorable.
No one should have the power to freeze your funds,
reverse your transactions, or inflate away your savings.
3. Your communications should be private.
End-to-end encryption is a right, not a feature.
Metadata collection is surveillance.
4. Your compute should be sovereign.
Running your own server is not paranoia.
It's digital self-reliance.
5. Your identity should be self-issued.
You don't need permission from a corporation
to prove who you are.
---
The tools exist. Bitcoin. Lightning. Tor. Nostr.
Self-hosted infrastructure. Open source software.
The question isn't whether it's possible.
The question is whether you'll do it.
Run your own node.
Hold your own keys.
Own your own data.
Be sovereign.

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1a1a2e;stop-opacity:1" />
<stop offset="100%" style="stop-color:#16213e;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="1200" height="800" fill="url(#bg)"/>
<text x="600" y="380" text-anchor="middle" font-family="Arial" font-size="32" fill="rgba(255,255,255,0.6)">bitcoin-conference-2024</text>
<text x="600" y="430" text-anchor="middle" font-family="Arial" font-size="16" fill="rgba(255,255,255,0.3)">Demo Photo Placeholder</text>
</svg>

After

Width:  |  Height:  |  Size: 674 B

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1a1a2e;stop-opacity:1" />
<stop offset="100%" style="stop-color:#16213e;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="1200" height="800" fill="url(#bg)"/>
<text x="600" y="380" text-anchor="middle" font-family="Arial" font-size="32" fill="rgba(255,255,255,0.6)">home-server-build</text>
<text x="600" y="430" text-anchor="middle" font-family="Arial" font-size="16" fill="rgba(255,255,255,0.3)">Demo Photo Placeholder</text>
</svg>

After

Width:  |  Height:  |  Size: 668 B

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1a1a2e;stop-opacity:1" />
<stop offset="100%" style="stop-color:#16213e;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="1200" height="800" fill="url(#bg)"/>
<text x="600" y="380" text-anchor="middle" font-family="Arial" font-size="32" fill="rgba(255,255,255,0.6)">lightning-network-visualization</text>
<text x="600" y="430" text-anchor="middle" font-family="Arial" font-size="16" fill="rgba(255,255,255,0.3)">Demo Photo Placeholder</text>
</svg>

After

Width:  |  Height:  |  Size: 682 B

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1a1a2e;stop-opacity:1" />
<stop offset="100%" style="stop-color:#16213e;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="1200" height="800" fill="url(#bg)"/>
<text x="600" y="380" text-anchor="middle" font-family="Arial" font-size="32" fill="rgba(255,255,255,0.6)">node-rack-setup</text>
<text x="600" y="430" text-anchor="middle" font-family="Arial" font-size="16" fill="rgba(255,255,255,0.3)">Demo Photo Placeholder</text>
</svg>

After

Width:  |  Height:  |  Size: 666 B

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1a1a2e;stop-opacity:1" />
<stop offset="100%" style="stop-color:#16213e;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="1200" height="800" fill="url(#bg)"/>
<text x="600" y="380" text-anchor="middle" font-family="Arial" font-size="32" fill="rgba(255,255,255,0.6)">sunset-from-balcony</text>
<text x="600" y="430" text-anchor="middle" font-family="Arial" font-size="16" fill="rgba(255,255,255,0.3)">Demo Photo Placeholder</text>
</svg>

After

Width:  |  Height:  |  Size: 670 B