Primo commit - Prompt 02 — WebSocket endpoints /agent e /client + parsing robusto

This commit is contained in:
Michele Proietto
2026-01-08 17:10:47 +01:00
parent c9391dda7b
commit bb69e51ef1
19 changed files with 1991 additions and 0 deletions

321
tasklist_fase1.txt Normal file
View File

@@ -0,0 +1,321 @@
Prompt 00 — Monorepo + shared protocol (TypeScript ovunque)
Crea una PR: “Monorepo bootstrap + shared protocol v1”.
Obiettivo:
- Impostare un monorepo con TypeScript ovunque e un pacchetto condiviso `shared/protocol` con tipi e helper per il protocollo WebSocket v1.
Struttura richiesta:
- /server (Node.js + TS)
- /windows (Electron + TS)
- /shared/protocol (TS, esporta tipi e validator base)
- /android NON in TS (verrà dopo), ma lascia spazio /android
Requisiti tecnici:
- Usa pnpm workspaces (preferibile) oppure yarn workspaces.
- In `shared/protocol` definisci:
- `ProtocolVersion = 1`
- tipi per tutti i messaggi Fase 1:
- agent_register, agent_registered, agent_heartbeat
- server_connect_request
- agent_connect_response
- client_login, client_login_result
- client_list_devices, client_device_list
- client_connect_request
- client_connect_status, client_connect_result
- error
- un `type AnyMessage = ...` union
- helper `isMessage(obj): obj is AnyMessage` con validazione minimale (no librerie pesanti; ok zod se vuoi ma tienilo semplice)
- helper `makeRequestId()` (uuid v4) e `nowTs()`
Output:
- Workspace funzionante con `pnpm i` e build TS.
- README root con comandi base.
Acceptance:
- `pnpm -r build` passa.
- Import da `shared/protocol` funziona da server e windows (anche se windows non è ancora implementato).
- Niente codice Android in questa PR.
Prompt 01 — Server skeleton + healthcheck + Docker
Crea una PR: “Server signaling TS skeleton + /health + Docker”.
Contesto:
- Monorepo esiste con `shared/protocol`.
Obiettivo:
- Creare `server` Node.js TypeScript che espone:
- HTTP GET `/health` => 200 “ok”
- WebSocket upgrade pronto (endpoints verranno dopo)
Requisiti:
- Framework HTTP: Fastify o Express (scegli uno e mantienilo semplice).
- Logger: pino (consigliato) o console strutturata.
- Config `.env` (usa dotenv).
- Dockerfile per server.
- Script: `pnpm --filter server dev` e `build` e `start`.
Acceptance:
- Avvio locale: `pnpm --filter server dev`
- `curl localhost:<port>/health` => 200
- `docker build` e `docker run` funzionano.
- Nessun endpoint WS richiesto ancora (solo scaffolding).
Prompt 02 — WebSocket endpoints /agent e /client + parsing robusto
Crea una PR: “WS endpoints /agent e /client + parsing robusto”.
Obiettivo:
- Implementare WebSocket su server con due path:
- ws://.../agent
- ws://.../client
Requisiti:
- Usa libreria `ws` (preferita) integrata con il server HTTP scelto.
- Per ogni connessione, identifica “kind” (agent/client) in base al path.
- Parsing JSON robusto: se arriva JSON invalido o messaggio non conforme, rispondi con:
{ v:1, type:"error", requestId: <se presente>, code:"BAD_REQUEST", message:"..." }
- Non deve crashare.
Acceptance:
- Connessione WS a /agent e /client possibile.
- Invio messaggio non JSON => ricevo error e connessione resta (o chiude con motivo, ok entrambe purché non crashi).
- Logga connect/disconnect con ip e path.
Prompt 03 — Auth Fase 1 (client login + agent pairingKey)
Crea una PR: “Auth F1: client_login + agent_register pairingKey”.
Obiettivo:
- Implementare autenticazione minimale:
- Client: `client_login` con username/password contro seed in env
- Agent: `agent_register` con pairingKey contro seed in env, associato allo stesso userId del seed
Env richieste (server):
- SEED_USERNAME
- SEED_PASSWORD (plaintext per ora, MVP)
- SEED_USER_ID (es. "user-1")
- SEED_PAIRING_KEY (string)
Comportamento:
- client_login ok => memorizza sessione in-memory (map ws -> userId).
- richieste client senza login => error UNAUTHORIZED.
- agent_register con pairingKey errata => error UNAUTHORIZED.
Usa i tipi di `shared/protocol`.
Acceptance:
- client senza login che fa list => UNAUTHORIZED
- login corretto => ok:true
- agent_register con pairingKey corretto => agent_registered ok:true
- agent_register con pairingKey errata => UNAUTHORIZED
Prompt 04 — Presence registry + heartbeat + lastSeen
Crea una PR: “Presence registry: devices online/offline + heartbeat”.
Obiettivo:
- Mantenere registry in memoria:
devicesById: deviceId -> { deviceId, deviceName, userId, online, lastSeenIso, wsRef? }
Regole:
- Su agent_register: crea/aggiorna entry, set online=true, lastSeen=now, salva wsRef.
- Su agent_heartbeat: aggiorna lastSeen=now (se deviceId noto), altrimenti error BAD_REQUEST.
- Su WS disconnect agent: online=false, lastSeen=now, wsRef=null.
Extra:
- Aggiungi un cleanup interval che marca offline se non riceve heartbeat entro 90s (anche se la socket resta “mezzo morta”).
Acceptance:
- Simulando heartbeat si aggiorna lastSeen.
- Chiudendo connessione agent => online=false.
- Se heartbeat manca >90s => online=false.
Prompt 05 — client_list_devices + filtro per userId
Crea una PR: “client_list_devices + device list filtrata”.
Obiettivo:
- Implementare:
- client_list_devices => server risponde client_device_list
Dettagli:
- Deve essere richiesto solo da client autenticato.
- Ritorna SOLO device associati al userId del client.
- Ogni device include: deviceId, deviceName, online, lastSeenIso.
Acceptance:
- Un client loggato vede la lista.
- Un client non loggato riceve UNAUTHORIZED.
- Se ci sono più device (simulati) appartenenti a userId diversi, ne vede solo i suoi.
Prompt 06 — Connect request routing + session state + timeout expired
Crea una PR: “Connect request routing + session state + timeout”.
Obiettivo:
- Implementare client_connect_request:
- verifica auth client
- verifica ownership (device.userId == client.userId)
- se device offline => error DEVICE_OFFLINE
- se device online => crea sessionId e status=pending
- inoltra a agent `server_connect_request` con {sessionId, fromUser: username}
- al client risponde `client_connect_status` status=pending
Session store in memory:
- sessionsById: sessionId -> { sessionId, userId, deviceId, status, createdAt, clientWsRef }
Timeout:
- dopo 60s se ancora pending => status=expired e invia al client `client_connect_result` status=expired
Acceptance:
- Richiesta verso device offline => DEVICE_OFFLINE
- Richiesta verso device online => agent riceve server_connect_request e client riceve pending
- Se agent non risponde entro 60s => client riceve expired
Prompt 07 — Agent accept/deny -> client result
Crea una PR: “agent_connect_response accept/deny -> client_connect_result”.
Obiettivo:
- Implementare agent_connect_response:
- valida sessionId esistente
- valida che session.deviceId == msg.deviceId
- valida che session.status == pending
- se decision accept => status=accepted
- se decision deny => status=denied
- invia a client `client_connect_result` con status accepted/denied
Edge cases:
- sessionId inesistente => error NOT_FOUND
- session non pending => error CONFLICT
- deviceId mismatch => error FORBIDDEN
Acceptance:
- Agent accept => client riceve accepted
- Agent deny => client riceve denied
- Risposte duplicate => CONFLICT
Prompt 08 — docker-compose dev (server + nginx reverse proxy WS)
Crea una PR: “Dev compose: nginx reverse proxy -> server (WS upgrade ok)”.
Obiettivo:
- Aggiungere `infra/docker-compose.yml` con:
- server
- nginx reverse proxy davanti
Requisiti:
- Nginx deve supportare WebSocket upgrade per /agent e /client.
- In dev, niente TLS (http + ws).
- Documenta in README come testare:
- ws://localhost/agent
- ws://localhost/client
(oppure con porte diverse, ma deve essere chiaro)
Acceptance:
- Connessione WS tramite nginx funziona (upgrade ok).
- /health raggiungibile via nginx.
Prompt Windows (Electron) — Fase 1
Prompt W1 — Electron skeleton + pagine login/devices
Crea una PR: “Electron TS skeleton: login page + devices page”.
Obiettivo:
- Creare app Electron in /windows con TypeScript.
- UI minimale con due schermate:
- Login (username/password, serverUrl)
- Devices (placeholder lista)
Requisiti:
- Puoi usare React+Vite oppure Electron Forge. Scegli stack semplice e moderno.
- Config serverUrl persistente (localStorage ok).
Acceptance:
- `pnpm --filter windows dev` avvia lapp.
- Navigazione login -> devices (anche finta).
Prompt W2 — WS client + client_login
Crea una PR: “Windows: WS client + client_login”.
Obiettivo:
- Connettere Electron a ws://<serverUrl>/client
- Implementare invio client_login e gestione client_login_result usando `shared/protocol`.
Requisiti:
- Gestisci reconnect base (retry ogni 2s fino a successo) o almeno errore UI.
- Dopo login ok => vai a schermata Devices.
Acceptance:
- Login corretto => ok e vai a Devices.
- Login errato => mostra errore.
Prompt W3 — Lista device (client_list_devices + refresh)
Crea una PR: “Windows: device list + refresh/poll”.
Obiettivo:
- Implementare:
- client_list_devices allapertura della schermata
- poll ogni 5s (o pulsante refresh) per aggiornare
- Render tabella: deviceName, deviceId, online, lastSeenIso.
Acceptance:
- Vedo device comparire e cambiare stato online/offline.
Prompt W4 — Connect request UI + stato pending/accepted/denied/expired
Crea una PR: “Windows: connect request + status UI”.
Obiettivo:
- Aggiungere un bottone “Connect” per ogni device.
- Click => invia client_connect_request.
- Mostra stato:
- pending (spinner o testo)
- accepted / denied / expired
Requisiti:
- Gestisci più richieste (almeno una alla volta; se già pending, disabilita altri connect).
Acceptance:
- Pending appare subito.
- Alla risposta dellagent si aggiorna la UI.
Prompt Android (Kotlin) — Fase 1
Prompt A1 — Foreground service + WS + deviceId persistente
Crea una PR Android: “Agent: ForegroundService + WS client + deviceId persistente”.
Obiettivo:
- App Android Kotlin con:
- ForegroundService “Remote Agent”
- WebSocket OkHttp
- deviceId UUID persistente in SharedPreferences
- schermata settings: serverUrl, pairingKey, deviceName (persistenti)
Comportamento:
- Avvio app -> start service -> connect ws://serverUrl/agent
- Log utile.
Acceptance:
- Service parte e resta in foreground con notifica.
- deviceId resta uguale tra riavvii app.
Prompt A2 — agent_register + heartbeat + reconnect
Crea una PR Android: “Agent: agent_register + heartbeat + reconnect”.
Obiettivo:
- Dopo WS open => invia agent_register {deviceId, deviceName, pairingKey}
- Heartbeat ogni 30s: agent_heartbeat
- Reconnect automatico se WS cade (retry con backoff semplice).
Acceptance:
- Server mostra device online.
- Stacco/riattacco rete => torna online senza riaprire app.
Prompt A3 — Notifica connect_request con azioni + agent_connect_response
Crea una PR Android: “Agent: notifica connect request + accept/deny”.
Obiettivo:
- Ricevere server_connect_request dal WS
- Mostrare notifica con azioni:
- ACCETTA
- RIFIUTA
- Click azione => invia agent_connect_response con {sessionId, deviceId, decision}
Requisiti:
- Deve funzionare anche con app in background.
- Usa PendingIntent + BroadcastReceiver per gestire azioni.
Acceptance:
- Notifica appare quando il client richiede connessione.
- Accept/deny inviano risposta e il client riceve lesito.