feat: reject with feedback, email alerts, SSE real-time updates
Reject modal now requires a reason — stored as an auto-comment on the version so context stays in the thread. Email alert fires on first play of a shared link (fire-and-forget, no-op without RESEND_API_KEY). SSE endpoint per track broadcasts version:new, version:status and comment:new events; track page subscribes and reloads data live. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
49
apps/api/src/routes/sse.ts
Normal file
49
apps/api/src/routes/sse.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Hono } from 'hono';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { tracks, projectMembers } from '@music-hub/db';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { subscribe } from '../services/sse.js';
|
||||
import type { AppEnv } from '../types.js';
|
||||
|
||||
export const sseRoutes = new Hono<AppEnv>()
|
||||
.use('*', requireAuth)
|
||||
|
||||
.get('/track/:trackId', async (c) => {
|
||||
const db = c.get('db');
|
||||
const userId = c.get('userId');
|
||||
const trackId = c.req.param('trackId');
|
||||
|
||||
const [track] = await db.select().from(tracks).where(eq(tracks.id, trackId)).limit(1);
|
||||
if (!track) return c.json({ error: 'Not found' }, 404);
|
||||
|
||||
const [membership] = await db
|
||||
.select()
|
||||
.from(projectMembers)
|
||||
.where(and(eq(projectMembers.projectId, track.projectId), eq(projectMembers.userId, userId)))
|
||||
.limit(1);
|
||||
if (!membership) return c.json({ error: 'Forbidden' }, 403);
|
||||
|
||||
const { readable, writable } = new TransformStream();
|
||||
const writer = writable.getWriter();
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const send = (data: string) => writer.write(encoder.encode(data)).catch(() => {});
|
||||
const unsubscribe = subscribe(trackId, send);
|
||||
|
||||
// Initial ping
|
||||
send(': connected\n\n');
|
||||
|
||||
c.req.raw.signal.addEventListener('abort', () => {
|
||||
unsubscribe();
|
||||
writer.close().catch(() => {});
|
||||
});
|
||||
|
||||
return new Response(readable, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no',
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user