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>
This commit is contained in:
parent
f12e50d8cb
commit
847fc84faf
10 changed files with 254 additions and 74 deletions
81
scripts/realtime-mutate.ts
Normal file
81
scripts/realtime-mutate.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// 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)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue