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
co-authored by Claude Opus 4.6
parent d3f0f1192e
commit e8a0e1af19
29 changed files with 1871 additions and 11 deletions
+2 -2
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.
+2 -2
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'
```
+1 -1
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.
+1 -1
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
+1 -1
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'
```
+151
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"
```
+176
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.
+82
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
```
+120
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.
+83
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.
+157
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
```
+167
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.
+104
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
+1 -1
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.
+105
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.
+2 -2
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.
+1 -1
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'
```