Files
LogWhispererAI/.opencode/skills/examples/ws-integration.md
Luca Sacchi Ricciardi aa489c7eb8 docs: add comprehensive frontend landing page plan and download design skills
Add detailed landing page development plan in docs/frontend_landing_plan.md:
- Complete landing page structure (Hero, Problem/Solution, Features, Demo, CTA)
- Design guidelines from downloaded skills (typography, color, motion, composition)
- Security considerations (XSS prevention, input sanitization, CSP)
- Performance targets (LCP <2.5s, bundle <150KB, Lighthouse >90)
- Responsiveness and accessibility requirements (WCAG 2.1 AA)
- Success KPIs and monitoring setup
- 3-week development timeline with daily tasks
- Definition of Done checklist

Download 10+ frontend/UI/UX skills via universal-skills-manager:
- frontend-ui-ux: UI/UX design without mockups
- frontend-design-guidelines: Production-grade interface guidelines
- frontend-developer: React best practices (40+ rules)
- frontend-engineer: Next.js 14 App Router patterns
- ui-ux-master: Comprehensive design systems and accessibility
- ui-ux-systems-designer: Information architecture and interaction
- ui-ux-design-user-experience: Platform-specific guidelines
- Plus additional reference materials and validation scripts

Configure universal-skills MCP with SkillsMP API key for curated skill access.

Safety first: All skills validated before installation, no project code modified.

Refs: Universal Skills Manager (github:jacob-bd/universal-skills-manager)
Next: Begin Sprint 3 landing page development
2026-04-03 13:13:59 +02:00

9.3 KiB

WebSocket Integration Example

Реальный пример из Family Budget: интеграция WebSocket для real-time обновлений.

Files

  • WebSocket Client: frontend/web/static/js/budget/budgetWSClient.js
  • Backend Manager: backend/app/api/v1/endpoints/budget_sse.py
  • Usage: frontend/web/templates/base.html

Architecture

┌──────────────────────────────────────────────────────────┐
│ Frontend: budgetWSClient.js                              │
│                                                           │
│  new BudgetWSClient() → WebSocket("/api/v1/budget/ws")  │
│         ↓                                                 │
│  Event handlers:                                          │
│  - budget_fact_created                                    │
│  - budget_fact_updated                                    │
│  - budget_fact_deleted                                    │
│  - article_updated                                        │
└──────────────────────────────────────────────────────────┘
                      ↓ (WebSocket)
┌──────────────────────────────────────────────────────────┐
│ Backend: BudgetConnectionManager (in-memory)             │
│                                                           │
│  - Active connections: Map<connection_id, WebSocket>     │
│  - Broadcast events to all connected clients             │
│  - WORKERS=1 constraint (no Redis Pub/Sub)              │
└──────────────────────────────────────────────────────────┘
                      ↑ (Broadcast)
┌──────────────────────────────────────────────────────────┐
│ API Endpoints: /api/v1/facts                             │
│                                                           │
│  POST /facts → ws_manager.broadcast("budget_fact_created", data) │
│  PUT /facts/{id} → ws_manager.broadcast("budget_fact_updated", data) │
│  DELETE /facts/{id} → ws_manager.broadcast("budget_fact_deleted", data) │
└──────────────────────────────────────────────────────────┘

Frontend Implementation

1. Initialize WebSocket Client

// In base.html (global scope)
<script type="module">
    import { BudgetWSClient } from '/static/js/budget/budgetWSClient.js';
    window.budgetWS = new BudgetWSClient();

    // Register event handlers
    budgetWS.on('budget_fact_created', (data) => {
        console.log('New fact created:', data);
        // Refresh HTMX content
        htmx.trigger('#recent-transactions', 'refresh');
        htmx.trigger('#quick-stats', 'refresh');
    });

    budgetWS.on('budget_fact_updated', (data) => {
        console.log('Fact updated:', data);
        htmx.trigger('#recent-transactions', 'refresh');
    });

    budgetWS.on('budget_fact_deleted', (data) => {
        console.log('Fact deleted:', data.id);
        htmx.trigger('#recent-transactions', 'refresh');
    });

    // Connect
    budgetWS.connect();
</script>

2. WebSocket Client (Simplified)

export class BudgetWSClient {
    constructor() {
        this.ws = null;
        this.isConnected = false;
        this.handlers = {};
    }

    connect() {
        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
        const wsUrl = `${protocol}//${window.location.host}/api/v1/budget/ws`;

        this.ws = new WebSocket(wsUrl);

        this.ws.onopen = () => {
            console.log('[BudgetWS] Connected');
            this.isConnected = true;
        };

        this.ws.onmessage = (event) => {
            const message = JSON.parse(event.data);
            const { event: eventType, data } = message;

            // Trigger registered handlers
            if (this.handlers[eventType]) {
                this.handlers[eventType].forEach(handler => handler(data));
            }
        };

        this.ws.onclose = () => {
            console.log('[BudgetWS] Disconnected');
            this.isConnected = false;
            // Reconnect after 3 seconds
            setTimeout(() => this.connect(), 3000);
        };

        this.ws.onerror = (error) => {
            console.error('[BudgetWS] Error:', error);
        };
    }

    on(event, handler) {
        if (!this.handlers[event]) {
            this.handlers[event] = [];
        }
        this.handlers[event].push(handler);
    }

    disconnect() {
        if (this.ws) {
            this.ws.close();
        }
    }
}

Backend Implementation

1. WebSocket Manager

# backend/app/api/v1/endpoints/budget_sse.py

from fastapi import WebSocket, WebSocketDisconnect
from typing import Dict
import logging

logger = logging.getLogger(__name__)

class BudgetConnectionManager:
    """
    In-memory WebSocket connection manager

    CRITICAL: Works only with WORKERS=1
    Multiple workers = separate managers = lost events
    """

    def __init__(self):
        self.active_connections: Dict[str, WebSocket] = {}

    async def connect(self, websocket: WebSocket, connection_id: str):
        await websocket.accept()
        self.active_connections[connection_id] = websocket
        logger.info(f"WebSocket connected: {connection_id}")

    def disconnect(self, connection_id: str):
        if connection_id in self.active_connections:
            del self.active_connections[connection_id]
            logger.info(f"WebSocket disconnected: {connection_id}")

    async def broadcast(self, event: str, data: dict):
        """Broadcast event to all connected clients"""
        message = {"event": event, "data": data}

        disconnected = []
        for conn_id, websocket in self.active_connections.items():
            try:
                await websocket.send_json(message)
            except Exception as e:
                logger.error(f"Broadcast error to {conn_id}: {e}")
                disconnected.append(conn_id)

        # Remove disconnected clients
        for conn_id in disconnected:
            self.disconnect(conn_id)

# Global instance
ws_manager = BudgetConnectionManager()

2. WebSocket Endpoint

from fastapi import APIRouter, WebSocket, WebSocketDisconnect
import uuid

router = APIRouter()

@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    connection_id = str(uuid.uuid4())

    await ws_manager.connect(websocket, connection_id)

    try:
        while True:
            # Keep connection alive (receive pings)
            data = await websocket.receive_text()
            # Optional: handle client messages
    except WebSocketDisconnect:
        ws_manager.disconnect(connection_id)

3. Broadcast from API Endpoints

# backend/app/api/v1/endpoints/facts.py

@router.post("/", response_model=FactResponse)
async def create_fact(
    fact_data: FactCreate,
    session: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    # Create fact
    fact = await fact_service.create_fact(session, fact_data, current_user)

    # Broadcast WebSocket event
    await ws_manager.broadcast(
        "budget_fact_created",
        {
            "id": fact.id,
            "amount": fact.amount,
            "article_id": fact.article_id,
            "financial_center_id": fact.financial_center_id,
        }
    )

    return fact

Key Features

1. Auto-Reconnect

this.ws.onclose = () => {
    setTimeout(() => this.connect(), 3000); // Reconnect after 3 sec
};

2. Event-Driven Architecture

// Register handler
budgetWS.on('budget_fact_created', (data) => {
    // Handle event
});

// Broadcast from backend
await ws_manager.broadcast("budget_fact_created", data);

3. HTMX Integration

// Refresh HTMX content on WebSocket event
htmx.trigger('#recent-transactions', 'refresh');

4. Single Worker Constraint

# docker-compose.yml
services:
  backend:
    command: uvicorn app.main:app --workers 1  # CRITICAL!

Why WORKERS=1?

  • BudgetConnectionManager is in-memory
  • Each worker has separate instance
  • Events don't propagate between workers
  • User A (worker 1) creates fact → only worker 1 clients receive event
  • User B (worker 2) doesn't see update

Future: Redis Pub/Sub

  • Use Redis for event broadcasting
  • Workers subscribe to Redis channel
  • Events propagate across all workers
  • Can scale to multiple workers

Performance

  • Connection time: ~100ms
  • Event latency: <50ms (same worker)
  • Reconnect time: 3 seconds
  • Memory: ~5KB per connection

References