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:
co-authored by
Claude Opus 4.6
parent
d3f0f1192e
commit
e8a0e1af19
@@ -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.
|
||||
Reference in New Issue
Block a user