Shoraky commited on
Commit
997abf7
·
1 Parent(s): c553d42

Fix feedback author attribution and stage one-time repair

Browse files
Files changed (1) hide show
  1. src/admin.ts +68 -13
src/admin.ts CHANGED
@@ -1,5 +1,6 @@
1
  import { readBatches } from "./read-batches.js";
2
  import { readPages } from "./read-pages.js";
 
3
  import type { Express, Request, Response, NextFunction } from "express";
4
  import { z } from "zod";
5
  import { env } from "./config.js";
@@ -71,6 +72,56 @@ async function loadRelatedCounts(table: "players" | "sessions" | "actions", key:
71
  }
72
 
73
  export function registerAdminRoutes(app: Express) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  app.get("/api/admin/users", asyncRoute(async (req, res) => {
75
  if (!await admin(req, res)) return;
76
  const search = String(req.query.search ?? "").replace(/[^\p{L}\p{N}@._ -]/gu, "").trim().slice(0, 100);
@@ -335,22 +386,26 @@ export function registerAdminRoutes(app: Express) {
335
  });
336
  const remoteResult = await remote.json().catch(() => ({}));
337
  if (!remote.ok) return fail(res, remote.status, (remoteResult as { detail?: string }).detail ?? "Unable to update action data.");
338
- const { data: existing, error: readError } = await supabaseAdmin.from("annotations").select("user_id").eq("action_id", actionId.data).maybeSingle();
 
339
  if (readError) throw new Error(`Unable to load action review: ${readError.message}`);
340
- if (input.data.score !== null || notes.length) {
341
- const { error } = await supabaseAdmin.from("annotations").upsert({ action_id: actionId.data, user_id: existing?.user_id ?? current.user.id, score: input.data.score }, { onConflict: "action_id" });
342
- if (error) throw new Error(`Unable to save action review: ${error.message}`);
343
- } else {
344
- const { error } = await supabaseAdmin.from("annotations").delete().eq("action_id", actionId.data);
345
- if (error) throw new Error(`Unable to remove action review: ${error.message}`);
346
  return res.json({ ok: true });
347
  }
348
- const { error: clearError } = await supabaseAdmin.from("annotations_notes").delete().eq("action_id", actionId.data);
349
- if (clearError) throw new Error(`Unable to replace action notes: ${clearError.message}`);
350
- if (notes.length) {
351
- const { error } = await supabaseAdmin.from("annotations_notes").upsert(notes.map((note) => ({ action_id: actionId.data, note })), { onConflict: "action_id,note", ignoreDuplicates: true });
352
- if (error) throw new Error(`Unable to save action notes: ${error.message}`);
353
- }
 
 
 
 
354
  return res.json({ ok: true });
355
  }));
356
 
 
1
  import { readBatches } from "./read-batches.js";
2
  import { readPages } from "./read-pages.js";
3
+ import { createHash } from "node:crypto";
4
  import type { Express, Request, Response, NextFunction } from "express";
5
  import { z } from "zod";
6
  import { env } from "./config.js";
 
72
  }
73
 
74
  export function registerAdminRoutes(app: Express) {
75
+ // Temporary one-time repair endpoint. Remove after the production attribution repair.
76
+ const feedbackAuthorId = "474bbc30-5535-4c06-8010-6d59d01949c0";
77
+ async function feedbackAuthorSnapshot() {
78
+ const notes = await readPages(async (from, to) => {
79
+ const { data, error } = await supabaseAdmin.from("annotations_notes").select("action_id")
80
+ .order("action_id").order("note").range(from, to);
81
+ if (error) throw new Error(`Unable to list feedback notes: ${error.message}`);
82
+ return data ?? [];
83
+ });
84
+ const ids = [...new Set(notes.map((note) => note.action_id))];
85
+ const rows: { action_id: string; user_id: string | null }[] = [];
86
+ for (let index = 0; index < ids.length; index += 100) {
87
+ const { data, error } = await supabaseAdmin.from("annotations").select("action_id,user_id")
88
+ .in("action_id", ids.slice(index, index + 100));
89
+ if (error) throw new Error(`Unable to list feedback authors: ${error.message}`);
90
+ rows.push(...(data ?? []));
91
+ }
92
+ if (rows.length !== ids.length) throw new Error("Feedback author snapshot is incomplete.");
93
+ rows.sort((a, b) => a.action_id.localeCompare(b.action_id));
94
+ const fingerprint = createHash("sha256").update(JSON.stringify(rows)).digest("hex");
95
+ return { rows, noteCount: notes.length, fingerprint };
96
+ }
97
+ app.get("/api/admin/repair-feedback-authors", asyncRoute(async (req, res) => {
98
+ if (!await admin(req, res)) return;
99
+ const { data: author, error } = await supabaseAdmin.from("users").select("fname,lname")
100
+ .eq("user_id", feedbackAuthorId).maybeSingle();
101
+ if (error || author?.fname !== "Ahmed" || author?.lname !== "Gad") throw new Error("Target feedback author could not be verified.");
102
+ const snapshot = await feedbackAuthorSnapshot();
103
+ return res.json({ ...snapshot, targetUserId: feedbackAuthorId });
104
+ }));
105
+ app.post("/api/admin/repair-feedback-authors", asyncRoute(async (req, res) => {
106
+ if (!await admin(req, res)) return;
107
+ const input = z.object({ fingerprint: z.string().regex(/^[a-f0-9]{64}$/) }).strict().safeParse(req.body);
108
+ if (!input.success) return fail(res, 400, "A current feedback snapshot is required.");
109
+ const { data: author, error } = await supabaseAdmin.from("users").select("fname,lname")
110
+ .eq("user_id", feedbackAuthorId).maybeSingle();
111
+ if (error || author?.fname !== "Ahmed" || author?.lname !== "Gad") throw new Error("Target feedback author could not be verified.");
112
+ const snapshot = await feedbackAuthorSnapshot();
113
+ if (snapshot.fingerprint !== input.data.fingerprint) return fail(res, 409, "Feedback authors changed. Refresh the snapshot.");
114
+ const ids = snapshot.rows.filter((row) => row.user_id !== feedbackAuthorId).map((row) => row.action_id);
115
+ for (let index = 0; index < ids.length; index += 100) {
116
+ const { error: updateError } = await supabaseAdmin.from("annotations")
117
+ .update({ user_id: feedbackAuthorId }).in("action_id", ids.slice(index, index + 100));
118
+ if (updateError) throw new Error(`Unable to update feedback authors: ${updateError.message}`);
119
+ }
120
+ const verified = await feedbackAuthorSnapshot();
121
+ if (verified.rows.some((row) => row.user_id !== feedbackAuthorId)) throw new Error("Feedback author verification failed.");
122
+ return res.json({ updatedAnnotations: ids.length, totalAnnotations: verified.rows.length,
123
+ noteCount: verified.noteCount, targetUserId: feedbackAuthorId, fingerprint: verified.fingerprint });
124
+ }));
125
  app.get("/api/admin/users", asyncRoute(async (req, res) => {
126
  if (!await admin(req, res)) return;
127
  const search = String(req.query.search ?? "").replace(/[^\p{L}\p{N}@._ -]/gu, "").trim().slice(0, 100);
 
386
  });
387
  const remoteResult = await remote.json().catch(() => ({}));
388
  if (!remote.ok) return fail(res, remote.status, (remoteResult as { detail?: string }).detail ?? "Unable to update action data.");
389
+ const { data: existing, error: readError } = await supabaseAdmin.from("annotations")
390
+ .select("score,annotations_notes(note)").eq("action_id", actionId.data).maybeSingle();
391
  if (readError) throw new Error(`Unable to load action review: ${readError.message}`);
392
+ if (input.data.score === null && notes.length === 0) {
393
+ if (existing) {
394
+ const { error } = await supabaseAdmin.from("annotations").delete().eq("action_id", actionId.data);
395
+ if (error) throw new Error(`Unable to remove action review: ${error.message}`);
396
+ }
 
397
  return res.json({ ok: true });
398
  }
399
+ const previousNotes = (existing?.annotations_notes ?? []).map((row) => row.note);
400
+ const previousScore = existing?.score == null ? null : Number(existing.score);
401
+ const reviewUnchanged = existing && previousScore === input.data.score
402
+ && previousNotes.length === notes.length && previousNotes.every((note) => notes.includes(note));
403
+ if (reviewUnchanged) return res.json({ ok: true });
404
+ const { error } = await supabaseAdmin.rpc("save_action_review", {
405
+ p_session_id: sessionId.data, p_action_id: actionId.data,
406
+ p_user_id: current.user.id, p_score: input.data.score, p_notes: notes,
407
+ });
408
+ if (error) throw new Error(`Unable to save action review: ${error.message}`);
409
  return res.json({ ok: true });
410
  }));
411