Shoraky commited on
Commit
e1e1b5f
·
1 Parent(s): 2ebe600

Persist action coach reviews

Browse files
Files changed (1) hide show
  1. src/server.ts +72 -0
src/server.ts CHANGED
@@ -58,6 +58,10 @@ const profileSchema = z.object({
58
  role: z.enum(profileRoles).nullable(),
59
  });
60
  const passwordSchema = z.object({ currentPassword: z.string().max(128).optional(), newPassword: z.string().min(8).max(128) });
 
 
 
 
61
 
62
 
63
  async function requireActiveSubscription(req: Request, res: Response) {
@@ -274,6 +278,74 @@ app.get("/api/sessions-overview", async (req, res, next) => {
274
  } catch (error) { next(error); }
275
  });
276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  // Session deletion is handled by the AI service through the proxy below so
278
  // the session folder and database records are deleted together.
279
 
 
58
  role: z.enum(profileRoles).nullable(),
59
  });
60
  const passwordSchema = z.object({ currentPassword: z.string().max(128).optional(), newPassword: z.string().min(8).max(128) });
61
+ const actionReviewSchema = z.object({
62
+ coachScore: z.number().int().min(0).max(10).nullable(),
63
+ feedback: z.array(z.string().trim().min(1).max(2000)).max(30),
64
+ }).strict();
65
 
66
 
67
  async function requireActiveSubscription(req: Request, res: Response) {
 
278
  } catch (error) { next(error); }
279
  });
280
 
281
+ app.get("/api/sessions/:sessionId/reviews", async (req, res, next) => {
282
+ try {
283
+ const current = await requireActiveSubscription(req, res);
284
+ if (!current) return;
285
+ const sessionId = z.uuid().safeParse(req.params.sessionId);
286
+ if (!sessionId.success) return res.sendStatus(404);
287
+
288
+ const { data: session, error: sessionError } = await supabaseAdmin.from("sessions")
289
+ .select("session_id").eq("session_id", sessionId.data).eq("user_id", current.user.id).maybeSingle();
290
+ if (sessionError) throw new Error(`Unable to verify review session: ${sessionError.message}`);
291
+ if (!session) return res.sendStatus(404);
292
+
293
+ const { data: actions, error: actionsError } = await supabaseAdmin.from("actions")
294
+ .select("action_id").eq("session_id", sessionId.data);
295
+ if (actionsError) throw new Error(`Unable to load review actions: ${actionsError.message}`);
296
+ const actionIds = (actions ?? []).map((action) => action.action_id);
297
+ if (!actionIds.length) return res.json({ reviews: {} });
298
+
299
+ const [{ data: annotations, error: annotationsError }, { data: notes, error: notesError }] = await Promise.all([
300
+ supabaseAdmin.from("annotations").select("action_id,score").in("action_id", actionIds),
301
+ supabaseAdmin.from("annotations_notes").select("action_id,note").in("action_id", actionIds),
302
+ ]);
303
+ if (annotationsError) throw new Error(`Unable to load action reviews: ${annotationsError.message}`);
304
+ if (notesError) throw new Error(`Unable to load action notes: ${notesError.message}`);
305
+
306
+ const reviews: Record<string, { coachScore: number | null; feedback: string[] }> = Object.fromEntries(
307
+ actionIds.map((actionId) => [actionId, { coachScore: null, feedback: [] }]),
308
+ );
309
+ for (const annotation of annotations ?? []) {
310
+ if (reviews[annotation.action_id]) reviews[annotation.action_id].coachScore = annotation.score == null ? null : Number(annotation.score);
311
+ }
312
+ for (const note of notes ?? []) reviews[note.action_id]?.feedback.push(note.note);
313
+ return res.json({ reviews });
314
+ } catch (error) { next(error); }
315
+ });
316
+
317
+ app.put("/api/sessions/:sessionId/actions/:actionId/review", async (req, res, next) => {
318
+ try {
319
+ const current = await requireActiveSubscription(req, res);
320
+ if (!current) return;
321
+ const sessionId = z.uuid().safeParse(req.params.sessionId);
322
+ const actionId = z.uuid().safeParse(req.params.actionId);
323
+ const input = actionReviewSchema.safeParse(req.body);
324
+ if (!sessionId.success || !actionId.success || !input.success) {
325
+ return res.status(400).json({ error: "Check the action score and feedback points." });
326
+ }
327
+
328
+ const { data: session, error: sessionError } = await supabaseAdmin.from("sessions")
329
+ .select("session_id").eq("session_id", sessionId.data).eq("user_id", current.user.id).maybeSingle();
330
+ if (sessionError) throw new Error(`Unable to verify review session: ${sessionError.message}`);
331
+ if (!session) return res.sendStatus(404);
332
+ const { data: action, error: actionError } = await supabaseAdmin.from("actions")
333
+ .select("action_id").eq("action_id", actionId.data).eq("session_id", sessionId.data).maybeSingle();
334
+ if (actionError) throw new Error(`Unable to verify review action: ${actionError.message}`);
335
+ if (!action) return res.sendStatus(404);
336
+
337
+ const { error } = await supabaseAdmin.rpc("save_action_review", {
338
+ p_session_id: sessionId.data,
339
+ p_action_id: actionId.data,
340
+ p_user_id: current.user.id,
341
+ p_score: input.data.coachScore,
342
+ p_notes: [...new Set(input.data.feedback)],
343
+ });
344
+ if (error) throw new Error(`Unable to save action review: ${error.message}`);
345
+ return res.json({ ok: true });
346
+ } catch (error) { next(error); }
347
+ });
348
+
349
  // Session deletion is handled by the AI service through the proxy below so
350
  // the session folder and database records are deleted together.
351