Scrum4Me/scripts/realtime-mutate.ts
Madhura68 847fc84faf fix(M8): make SSE-stream survive Solo Paneel mutations
Symptoom op feat/ST-801-realtime-triggers initial implementation:
elke task-update sloot de open SSE-stream af en triggerde een
herverbinding met backoff. In de tussentijd gemiste events.

Oorzaak: Server Actions in App Router doen een impliciete
route-tree refresh die client components remount; daarmee killt
React de useEffect die de EventSource beheert.

Fix in twee delen:

1. Hef de realtime-hook op naar de (app)-layout via een nieuwe
   `SoloRealtimeBridge`-component. Layouts overleven Server-
   Action-refreshes beter dan pages, en de bridge leest het
   product-id uit de URL via usePathname. Connection-status
   (status, showConnectingIndicator) gaat naar de solo-store
   zodat SoloBoard 'm uit een gedeelde plek kan lezen.

2. Vervang updateTaskStatusAction en updateTaskPlanAction in de
   Solo-componenten door fetch naar de bestaande Route Handler
   `PATCH /api/tasks/[id]`. Route Handlers triggeren geen
   page-refresh, dus de SSE-stream blijft staan. lib/api-auth.ts
   accepteert nu naast Bearer-tokens ook iron-session cookies
   zodat browser-fetches zonder token werken.

Bijkomend: actions/tasks.ts laat /solo bewust niet meer
revalideren (wordt nu via realtime gedekt). Sprint/planning blijft
wel revalidaten — geen realtime daar.

Toegevoegd:
- components/solo/realtime-bridge.tsx — mount in (app) layout
- scripts/realtime-mutate.ts — handige test-helper voor externe
  mutaties (alsof MCP/REST schrijft) tijdens acceptance

Debug-logs in app/api/realtime/solo/route.ts staan nog aan voor
ST-806 acceptance; worden later gestript.

Bekend issue: Chrome op localhost (HTTP/1.1) cycle't EventSource
om de paar seconden vanwege de 6-connectie-limiet en retry-
heuristiek. Safari werkt stabiel. Productie op Vercel (HTTP/2
multiplexing) zou beide browsers stabiel moeten houden — Vercel
preview test is volgende stap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 04:35:12 +02:00

81 lines
3.1 KiB
TypeScript

// Test-helper voor M8 acceptatie. Muteert een task of story rechtstreeks
// in de DB om realtime-events te triggeren — alsof MCP of een andere
// schrijver het zou doen. Niet voor productiegebruik.
//
// Gebruik:
// tsx scripts/realtime-mutate.ts move <taskId> <status>
// tsx scripts/realtime-mutate.ts touch task <taskId>
// tsx scripts/realtime-mutate.ts touch story <storyId>
// tsx scripts/realtime-mutate.ts rename story <storyId> <new title>
// tsx scripts/realtime-mutate.ts list-tasks # toont id + status van assigned tasks
import * as dotenv from 'dotenv'
import * as path from 'path'
import { Pool } from 'pg'
const root = path.resolve(__dirname, '..')
dotenv.config({ path: path.join(root, '.env.local'), override: true })
dotenv.config({ path: path.join(root, '.env') })
async function main() {
const url = process.env.DATABASE_URL
if (!url) throw new Error('DATABASE_URL is not set')
const pool = new Pool({ connectionString: url })
const [, , cmd, ...rest] = process.argv
try {
if (cmd === 'move') {
const [taskId, status] = rest
if (!taskId || !status) throw new Error('move requires <taskId> <status>')
const r = await pool.query(
'UPDATE tasks SET status = $1::"TaskStatus", updated_at = NOW() WHERE id = $2 RETURNING id, status',
[status, taskId],
)
console.log('moved:', r.rows[0])
} else if (cmd === 'touch') {
const [entity, id] = rest
if (entity !== 'task' && entity !== 'story') throw new Error('touch entity must be task or story')
const table = entity === 'task' ? 'tasks' : 'stories'
const r = await pool.query(
`UPDATE ${table} SET updated_at = NOW() WHERE id = $1 RETURNING id`,
[id],
)
console.log('touched:', r.rows[0])
} else if (cmd === 'rename') {
const [entity, id, ...titleParts] = rest
const title = titleParts.join(' ')
if (entity !== 'story') throw new Error('rename only supported for story for now')
if (!id || !title) throw new Error('rename requires <id> <new title>')
const r = await pool.query(
'UPDATE stories SET title = $1, updated_at = NOW() WHERE id = $2 RETURNING id, title',
[title, id],
)
console.log('renamed:', r.rows[0])
} else if (cmd === 'list-tasks') {
const r = await pool.query(`
SELECT t.id, t.title, t.status, s.code AS story_code, s.title AS story_title
FROM tasks t
JOIN stories s ON t.story_id = s.id
WHERE s.assignee_id IS NOT NULL
ORDER BY s.sort_order, t.sort_order
LIMIT 20
`)
console.table(r.rows)
} else {
console.error('Usage:')
console.error(' tsx scripts/realtime-mutate.ts move <taskId> <TO_DO|IN_PROGRESS|REVIEW|DONE>')
console.error(' tsx scripts/realtime-mutate.ts touch task|story <id>')
console.error(' tsx scripts/realtime-mutate.ts rename story <id> <new title>')
console.error(' tsx scripts/realtime-mutate.ts list-tasks')
process.exit(1)
}
} finally {
await pool.end()
}
}
main().catch((err) => {
console.error(err.message)
process.exit(1)
})