Dorian 4e54b8bd4d feat: add YAML frontmatter, bitcoin-conventions skill, path rules, and Gitea CI
- 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>
2026-03-15 12:35:17 +00:00

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 permanentlyDisconnected flag
  • 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

  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

./scripts/deploy-to-target.sh --live

Test with browser DevTools Network tab to observe WebSocket frames.