- Added YAML frontmatter to all 8 polish-* skills and sweep skill so Claude can auto-invoke them - New bitcoin-conventions skill with PROUX UX methodology, sats display, address validation, Tor preferences, Lightning patterns - Path-specific rules for containers (security hardening) and frontend (Vue/glassmorphism conventions) - Gitea Actions: nightly security review and weekly dependency audit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4.8 KiB
name, description
| name | description |
|---|---|
| polish-websocket | Improve Archipelago WebSocket reliability, reconnection UX, heartbeat monitoring, session timeout detection, and connection status indicators. Use when user says "polish websocket", "fix reconnection", "websocket reliability", or "connection status". |
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
<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
permanentlyDisconnectedflag - Emit event that App.vue listens to
- Show persistent banner: "Connection lost. Click to retry." or "Refresh page to reconnect."
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:
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.
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:
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:
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:
onReconnect() {
// Clear old subscriptions
this.removeAllListeners()
// Re-subscribe
this.setupSubscriptions()
}
6. Message Queuing During Disconnect
When WebSocket is down, queue subscription requests:
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
- Kill backend → frontend shows "Disconnected" → restart backend → frontend reconnects and shows "Connected"
- Toggle wifi → status indicator updates → wifi back → auto-reconnect
- Wait for session timeout → next RPC call redirects to login
- Rapid reconnect → no duplicate event handlers (check with DevTools)
- Leave tab in background → come back → status is accurate
Deploy
./scripts/deploy-to-target.sh --live
Test with browser DevTools Network tab to observe WebSocket frames.