- 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>
173 lines
4.8 KiB
Markdown
173 lines
4.8 KiB
Markdown
---
|
|
name: polish-websocket
|
|
description: 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
|
|
|
|
```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.
|