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:
parent
d3f0f1192e
commit
e8a0e1af19
514
.claude/plans/plan.md
Normal file
514
.claude/plans/plan.md
Normal 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 10–16)
|
||||||
|
|
||||||
|
**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 17–23)
|
||||||
|
|
||||||
|
**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 24–30)
|
||||||
|
|
||||||
|
**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 7–13)
|
||||||
|
|
||||||
|
**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 14–20)
|
||||||
|
|
||||||
|
**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 21–27)
|
||||||
|
|
||||||
|
**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 |
|
||||||
@ -16,13 +16,13 @@ Build a new Archipelago auto-installer ISO.
|
|||||||
## Build (on target server — recommended)
|
## Build (on target server — recommended)
|
||||||
|
|
||||||
```bash
|
```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
|
## Copy ISO back to Mac
|
||||||
|
|
||||||
```bash
|
```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.
|
**IMPORTANT**: Use `build-auto-installer-iso.sh` only. The deprecated `build-debian-iso.sh` causes boot-to-prompt issues.
|
||||||
|
|||||||
@ -18,6 +18,6 @@ Deploy all changes to BOTH servers (primary: 192.168.1.228, secondary: 192.168.1
|
|||||||
|
|
||||||
3. Verify both servers respond:
|
3. Verify both servers respond:
|
||||||
```bash
|
```bash
|
||||||
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 'systemctl is-active archipelago'
|
ssh -i ~/.ssh/archipelago-deploy 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.198 'systemctl is-active archipelago'
|
||||||
```
|
```
|
||||||
|
|||||||
@ -18,7 +18,7 @@ Deploy all changes to the live server (192.168.1.228).
|
|||||||
|
|
||||||
3. After deploy completes, verify the server is healthy:
|
3. After deploy completes, verify the server is healthy:
|
||||||
```bash
|
```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.
|
4. Report whether the deploy succeeded and if any errors appeared in the logs.
|
||||||
|
|||||||
@ -4,7 +4,7 @@ description: Run a full diagnostic check on the Archipelago dev server
|
|||||||
allowed-tools: Bash
|
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
|
## Checks to run
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@ grep -rn 'console\.\(log\|warn\|error\)' src/ --include='*.ts' --include='*.vue'
|
|||||||
## Backend Linting (on dev server)
|
## Backend Linting (on dev server)
|
||||||
|
|
||||||
```bash
|
```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'
|
'source ~/.cargo/env && cd ~/archy/core && cargo clippy --all-targets --all-features 2>&1 && cargo fmt --all -- --check 2>&1'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
151
.claude/skills/polish-backend/SKILL.md
Normal file
151
.claude/skills/polish-backend/SKILL.md
Normal 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"
|
||||||
|
```
|
||||||
176
.claude/skills/polish-deploy/SKILL.md
Normal file
176
.claude/skills/polish-deploy/SKILL.md
Normal 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.
|
||||||
82
.claude/skills/polish-errors/SKILL.md
Normal file
82
.claude/skills/polish-errors/SKILL.md
Normal 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
|
||||||
|
```
|
||||||
120
.claude/skills/polish-forms/SKILL.md
Normal file
120
.claude/skills/polish-forms/SKILL.md
Normal 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.
|
||||||
83
.claude/skills/polish-loading/SKILL.md
Normal file
83
.claude/skills/polish-loading/SKILL.md
Normal 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.
|
||||||
157
.claude/skills/polish-security/SKILL.md
Normal file
157
.claude/skills/polish-security/SKILL.md
Normal 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
|
||||||
|
```
|
||||||
167
.claude/skills/polish-websocket/SKILL.md
Normal file
167
.claude/skills/polish-websocket/SKILL.md
Normal 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.
|
||||||
104
.claude/skills/polish/SKILL.md
Normal file
104
.claude/skills/polish/SKILL.md
Normal 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 10–16 — Silent Failures & Error Handling
|
||||||
|
- Week 2: March 17–23 — Loading States & Visual Feedback
|
||||||
|
- Week 3: March 24–30 — Form Validation & Input Quality
|
||||||
|
- Week 4: March 31 – April 6 — Backend Robustness
|
||||||
|
- Week 5: April 7–13 — WebSocket & Real-Time Quality
|
||||||
|
- Week 6: April 14–20 — Deployment & Infrastructure Hardening
|
||||||
|
- Week 7: April 21–27 — 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
|
||||||
@ -5,7 +5,7 @@ allowed-tools: Bash
|
|||||||
argument-hint: "[backend|nginx|container-name]"
|
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.
|
If $ARGUMENTS is provided, show logs for that specific service. Otherwise, show backend logs by default.
|
||||||
|
|
||||||
|
|||||||
105
.claude/skills/sweep/SKILL.md
Normal file
105
.claude/skills/sweep/SKILL.md
Normal 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.
|
||||||
@ -11,12 +11,12 @@ Sync system configuration files from the live server back to the repo for ISO bu
|
|||||||
|
|
||||||
1. **Capture systemd service**:
|
1. **Capture systemd service**:
|
||||||
```bash
|
```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**:
|
2. **Capture nginx config**:
|
||||||
```bash
|
```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.
|
3. **Capture any custom scripts** in `/opt/archipelago/scripts/` if they've changed.
|
||||||
|
|||||||
@ -13,7 +13,7 @@ Run or create tests for $ARGUMENTS.
|
|||||||
### Run existing tests
|
### Run existing tests
|
||||||
```bash
|
```bash
|
||||||
# On dev server (never build Rust on macOS)
|
# 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'
|
'source ~/.cargo/env && cd ~/archy/core && cargo test --all-features 2>&1'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
|||||||
|
# SSH keys (sandbox copies)
|
||||||
|
.ssh/
|
||||||
|
|
||||||
# Rust build output
|
# Rust build output
|
||||||
target/
|
target/
|
||||||
**/target/
|
**/target/
|
||||||
|
|||||||
39
demo/content/documents/backup-log.json
Normal file
39
demo/content/documents/backup-log.json
Normal 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"
|
||||||
|
}
|
||||||
28
demo/content/documents/bitcoin-whitepaper-notes.md
Normal file
28
demo/content/documents/bitcoin-whitepaper-notes.md
Normal 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
|
||||||
8
demo/content/documents/lightning-channels.csv
Normal file
8
demo/content/documents/lightning-channels.csv
Normal 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
|
||||||
|
31
demo/content/documents/node-setup-checklist.md
Normal file
31
demo/content/documents/node-setup-checklist.md
Normal 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
|
||||||
37
demo/content/documents/sovereignty-manifesto.txt
Normal file
37
demo/content/documents/sovereignty-manifesto.txt
Normal 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.
|
||||||
11
demo/content/photos/bitcoin-conference-2024.svg
Normal file
11
demo/content/photos/bitcoin-conference-2024.svg
Normal 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 |
11
demo/content/photos/home-server-build.svg
Normal file
11
demo/content/photos/home-server-build.svg
Normal 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 |
11
demo/content/photos/lightning-network-visualization.svg
Normal file
11
demo/content/photos/lightning-network-visualization.svg
Normal 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 |
11
demo/content/photos/node-rack-setup.svg
Normal file
11
demo/content/photos/node-rack-setup.svg
Normal 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 |
11
demo/content/photos/sunset-from-balcony.svg
Normal file
11
demo/content/photos/sunset-from-balcony.svg
Normal 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 |
Loading…
x
Reference in New Issue
Block a user