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>
81 lines
3.1 KiB
TypeScript
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)
|
|
})
|