P4-B: notifications Web Push (VAPID) sur passage en waiting

- db: migration id=4 `push_subscriptions` (liées au token d'auth) ; clés VAPID en settings
- PushService: bootstrap idempotent des clés VAPID, subscribe/unsubscribe/count,
  notify() best-effort (purge des abonnements 410/404 Gone) ; sender injectable pour les tests
- routes/push.ts: GET vapid-public-key, POST subscribe/unsubscribe/test (toutes sous auth globale)
- pty-manager: déclencheur push sur FRONT MONTANT vers waiting, débouncé 1500ms et annulable
  (faux positif ignoré) ; câblage app/index/config (--vapid-contact)
- shared/api.ts: types VapidKeyResponse / PushSubscribeRequest / PushUnsubscribeRequest
- deps: web-push (+ @types/web-push) côté serveur
- tests: push-service (idempotence, UPSERT, 410-purge, payload) + trigger pty-manager (169 verts)
This commit is contained in:
2026-06-15 12:13:56 +02:00
parent 25d6d09d24
commit 664e2b583c
13 changed files with 493 additions and 11 deletions

View File

@@ -11,10 +11,12 @@ import { AuthService, LoginRateLimiter, type AuthContext } from './auth/service.
import { PtyManager } from './core/pty-manager.js';
import { DiscoveryService } from './core/discovery-service.js';
import { WorktreeManager } from './core/worktree-manager.js';
import { PushService } from './core/push-service.js';
import { registerAuthRoutes } from './routes/auth.js';
import { registerSessionRoutes } from './routes/sessions.js';
import { registerRepoRoutes } from './routes/repos.js';
import { registerWorktreeRoutes } from './routes/worktrees.js';
import { registerPushRoutes } from './routes/push.js';
import { registerWsGateway } from './ws/gateway.js';
declare module 'fastify' {
@@ -32,13 +34,15 @@ export interface AppBundle {
manager: PtyManager;
discovery: DiscoveryService;
worktrees: WorktreeManager;
push: PushService;
}
export function buildApp(config: Config, db: Db, serverVersion: string): AppBundle {
const app = Fastify({ logger: { level: process.env.ARBORETUM_LOG ?? 'info' } });
const auth = new AuthService(db);
const limiter = new LoginRateLimiter();
const manager = new PtyManager(db, config.claudeSessionsDir);
const push = new PushService(db, config.vapidContact);
const manager = new PtyManager(db, config.claudeSessionsDir, push);
const discovery = new DiscoveryService({
ptyManager: manager,
projectsDir: config.claudeProjectsDir,
@@ -87,6 +91,7 @@ export function buildApp(config: Config, db: Db, serverVersion: string): AppBund
registerSessionRoutes(app, manager, discovery);
registerRepoRoutes(app, worktrees);
registerWorktreeRoutes(app, worktrees);
registerPushRoutes(app, push);
// La route websocket doit être déclarée APRÈS le chargement du plugin (contexte
// encapsulé) — sinon le handler reçoit la signature REST (request, reply).
void app.register(async (scoped) => {
@@ -105,5 +110,5 @@ export function buildApp(config: Config, db: Db, serverVersion: string): AppBund
});
}
return { app, auth, manager, discovery, worktrees };
return { app, auth, manager, discovery, worktrees, push };
}