feat(rectification): anonymous aggregate telemetry + admin summary page
One row per rectification Case, written once when the range card is first delivered (GET /api/rectification/cases/[caseId], fire-and-forget after the response is built). Numbers and closed enums only: no user / case / session id, birth data, names, text or timestamps finer than the ISO week. Dedupe via a separate case_id ledger that cascades with the Case (and account deletion). Migration 20260926010000 is additive: two RLS tables with no runtime table grants, SECURITY DEFINER write (service_role), purge (service_role) and aggregate-only summary (admin_runtime) functions; 180-day retention. Admin: 「校正统计」 page + GET /api/admin/rectification-telemetry (admin.customers.read), aggregates only, no per-row view or export. TASK-rectification-telemetry-20260926. test:db not run locally (no Docker). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f74825a27c
commit
d0bfc1fc3d
@@ -0,0 +1,33 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { requirePermission } from "@/lib/admin/auth";
|
||||
import { queryAdminRows } from "@/lib/admin/database";
|
||||
import { adminErrorResponse } from "@/lib/admin/http";
|
||||
import {
|
||||
parseRectificationTelemetrySummary,
|
||||
parseTelemetrySummaryWeeks,
|
||||
} from "@/lib/admin/rectification-telemetry-summary";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
/**
|
||||
* GET /api/admin/rectification-telemetry?weeks=4|12|26
|
||||
*
|
||||
* Aggregates only (TASK-rectification-telemetry-20260926, D3). The admin role
|
||||
* has no table privilege on `rectification_telemetry`; the only read path is
|
||||
* the SECURITY DEFINER summary function, which returns medians, distributions
|
||||
* and a weekly trend. There is no per-row list and no export.
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
await requirePermission("admin.customers.read");
|
||||
const weeks = parseTelemetrySummaryWeeks(new URL(request.url).searchParams.get("weeks"));
|
||||
const rows = await queryAdminRows<{ summary: unknown }>(
|
||||
"select public.rectification_telemetry_summary($1::integer) as summary",
|
||||
[weeks],
|
||||
);
|
||||
return NextResponse.json({ data: parseRectificationTelemetrySummary(rows[0]?.summary ?? null) });
|
||||
} catch (error) {
|
||||
return adminErrorResponse(error);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
safeToolErrorCode,
|
||||
} from "@/lib/rectification-agentic/v9/tool-service";
|
||||
import { dossierResponseWithIdentity } from "@/lib/rectification-agentic/v9/case-dossier-response";
|
||||
import { scheduleRectificationTelemetry } from "@/lib/rectification-agentic/v9/telemetry";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
@@ -96,7 +97,22 @@ export async function GET(request: Request, context: RouteContext) {
|
||||
}),
|
||||
);
|
||||
const listed = await listV10ConversationFocuses(accounting, user.id, caseId);
|
||||
return NextResponse.json(await dossierResponseWithIdentity(dossier, receipts, skillIdentity, { fullDetail, listed }));
|
||||
return NextResponse.json(await dossierResponseWithIdentity(dossier, receipts, skillIdentity, {
|
||||
fullDetail,
|
||||
listed,
|
||||
// Anonymous aggregate telemetry: fire-and-forget, never awaited here.
|
||||
onProjected: ({ decision, rangeDelivery, readOnly }) => {
|
||||
void scheduleRectificationTelemetry({
|
||||
accounting,
|
||||
userId: user.id,
|
||||
caseId,
|
||||
readOnly,
|
||||
build: () => listed.available
|
||||
? { dossier, focuses: listed.focuses, decision, rangeDelivery, now: Date.now() }
|
||||
: null,
|
||||
});
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error instanceof RectificationToolServiceError) {
|
||||
const message = error.message;
|
||||
|
||||
Reference in New Issue
Block a user