# 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.
