web: customs archive reads the generated types, annotations page follows nextLink through the typed builder, dead PaginatedResponse removed (audit package 5, stage 4a)

The five hand-written customs-archive response types in lib/api.ts duplicated CustomsArchiveRunListResponse,
CustomsArchiveEntryListResponse, CustomsArchiveRunSummary, CustomsArchiveEntrySummary and
TenantOffboardingCustomsArchiveEntry from the manifest; the two pages import those. The annotations page
built its next-page URL from API_BASE_URL + nextLink instead of the scoped builder plus extractContinuation.
PaginatedResponse<T> had no importer. types/diagnostics.ts states why it stays hand-written (no wire DTO
for GET diagnostics/paths).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-09-06 02:28:08 +02:00
parent 02187f0f3c
commit e71223507c
6 changed files with 19 additions and 72 deletions
@@ -3,17 +3,12 @@
import { Suspense, useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import {
api,
type CustomsArchiveEntrySummary,
type CustomsArchiveFullEntry,
type CustomsArchiveListEntriesResponse,
} from "@/lib/api";
import { api } from "@/lib/api";
import { apiErrorText, fetchJson, fetchOk } from "@/lib/apiClient";
import { useAdminConfig } from "../../AdminConfigContext";
import { AdminNotifications } from "../../AdminNotifications";
import { DeleteConfirmModal } from "../components/DeleteConfirmModal";
import type { CustomsArchiveEntryResponse } from "@/utils/wire-types.generated";
import type { CustomsArchiveEntryListResponse, CustomsArchiveEntryResponse, CustomsArchiveEntrySummary, TenantOffboardingCustomsArchiveEntry } from "@/utils/wire-types.generated";
export default function CustomsArchiveDetailPage() {
// useSearchParams() in CustomsArchiveDetailContent requires a Suspense boundary
@@ -33,7 +28,7 @@ function CustomsArchiveDetailContent() {
const { getAccessToken, setError, setSuccessMessage } = useAdminConfig();
const [entries, setEntries] = useState<CustomsArchiveEntrySummary[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [expanded, setExpanded] = useState<Record<string, CustomsArchiveFullEntry | "loading" | undefined>>({});
const [expanded, setExpanded] = useState<Record<string, TenantOffboardingCustomsArchiveEntry | "loading" | undefined>>({});
const [deletingRk, setDeletingRk] = useState<string | null>(null);
const [pendingDelete, setPendingDelete] = useState<CustomsArchiveEntrySummary | null>(null);
@@ -41,7 +36,7 @@ function CustomsArchiveDetailContent() {
try {
setLoading(true);
const url = api.customsArchive.listEntries(tenantId, historyRowKey);
const body = await fetchJson<CustomsArchiveListEntriesResponse>(url, getAccessToken);
const body = await fetchJson<CustomsArchiveEntryListResponse>(url, getAccessToken);
setEntries(body.entries ?? []);
} catch (err) {
setError(apiErrorText(err));
@@ -75,7 +70,7 @@ function CustomsArchiveDetailContent() {
api.customsArchive.getEntry(tenantId, historyRowKey, entry.rowKey),
getAccessToken,
);
setExpanded((prev) => ({ ...prev, [key]: body.entry as CustomsArchiveFullEntry }));
setExpanded((prev) => ({ ...prev, [key]: body.entry }));
} catch (err) {
setExpanded((prev) => {
const next = { ...prev };
@@ -153,7 +148,7 @@ function CustomsArchiveDetailContent() {
const detail = expanded[entry.rowKey];
const isOpen = !!detail;
const isLoadingFull = detail === "loading";
const fullEntry = typeof detail === "object" ? (detail as CustomsArchiveFullEntry) : null;
const fullEntry = typeof detail === "object" ? (detail as TenantOffboardingCustomsArchiveEntry) : null;
return (
<div
@@ -3,16 +3,12 @@
import { customsArchiveUrl } from "@/lib/routes";
import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import {
api,
type CustomsArchiveListRunsResponse,
type CustomsArchiveRunSummary,
} from "@/lib/api";
import { api } from "@/lib/api";
import { apiErrorText, fetchJson } from "@/lib/apiClient";
import { useAdminConfig } from "../AdminConfigContext";
import { AdminNotifications } from "../AdminNotifications";
import { DeleteConfirmModal } from "./components/DeleteConfirmModal";
import type { CustomsArchiveDeleteRunResponse } from "@/utils/wire-types.generated";
import type { CustomsArchiveDeleteRunResponse, CustomsArchiveRunListResponse, CustomsArchiveRunSummary } from "@/utils/wire-types.generated";
export default function CustomsArchivePage() {
const { getAccessToken, setError, setSuccessMessage } = useAdminConfig();
@@ -26,7 +22,7 @@ export default function CustomsArchivePage() {
try {
setLoading(true);
const url = api.customsArchive.listRuns({ tenantId: filter.trim() || undefined });
const body = await fetchJson<CustomsArchiveListRunsResponse>(url, getAccessToken);
const body = await fetchJson<CustomsArchiveRunListResponse>(url, getAccessToken);
setRuns(body.runs ?? []);
} catch (err) {
setError(apiErrorText(err));
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { sessionUrl } from "@/lib/routes";
import { scopedApi } from "@/lib/scopedApi";
import { API_BASE_URL } from "@/utils/config";
import { ProtectedRoute } from "@/components/ProtectedRoute";
import { useAuth } from "@/contexts/AuthContext";
import { useAdminMode, useAggregatedAdminScope } from "@/hooks";
@@ -23,6 +22,7 @@ import {
import { DocsLink } from "@/components/DocsLink";
import { DOCS_PATHS } from "@/lib/docsPaths";
import { fetchJson, nullOn404 } from "@/lib/apiClient";
import { extractContinuation } from "@/lib/paginationLink";
/**
* Annotations overview: every annotated session in one list, so a judged session can be
@@ -294,7 +294,13 @@ export default function AnnotationsPage() {
onClick={() => {
setLoading(true);
setLoadError(null);
fetchPage(`${API_BASE_URL}${nextLink}`, true);
fetchPage(
scopedApi.annotationsList(
{ routeGlobal, selectedTenantId, effectiveTenantId },
{ verdict: verdictFilter || undefined, lane: laneFilter || undefined, q: submittedQuery || undefined, continuation: extractContinuation(nextLink) ?? undefined },
),
true,
);
}}
className="px-4 py-1.5 text-sm font-medium bg-white border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
>
-42
View File
@@ -784,48 +784,6 @@ export const api = {
},
};
// ── Customs Archive response types ────────────────────────────────────────
// Shared by /admin/customs-archive (list runs) and
// /admin/customs-archive/[tenantId]/[historyRowKey] (list/inspect entries).
export interface CustomsArchiveRunSummary {
partitionKey: string;
tenantId: string;
historyRowKey: string;
archivedAt: string;
gatherRulesCount: number;
analyzeRulesCount: number;
imeLogPatternsCount: number;
}
export interface CustomsArchiveListRunsResponse {
success: boolean;
count: number;
runs: CustomsArchiveRunSummary[];
}
export interface CustomsArchiveEntrySummary {
partitionKey: string;
rowKey: string;
originalTable: string;
originalRowKey: string;
archivedAt: string;
entityJsonPreview: string;
}
export interface CustomsArchiveListEntriesResponse {
success: boolean;
count: number;
entries: CustomsArchiveEntrySummary[];
}
export interface CustomsArchiveFullEntry extends CustomsArchiveEntrySummary {
tenantId: string;
originalPartitionKey: string;
entityJson: string;
historyRowKey: string;
archivedBy: string;
}
// ── Backups response types (PR1 + PR2) ────────────────────────────────────
export interface BackupListResponse {
@@ -8,16 +8,6 @@
* url builder.
*/
export interface PaginatedResponse<T> {
count: number;
nextLink?: string | null;
/** legacy collection key — varies per endpoint (events / sessions / logs / reports) */
events?: T[];
sessions?: T[];
logs?: T[];
reports?: T[];
}
/**
* Extracts the `continuation` query param from a backend-supplied nextLink.
* Returns null when the input is empty or carries no continuation.
@@ -1,3 +1,5 @@
// GET /api/diagnostics/paths has no wire DTO in the shared manifest (the payload is assembled
// in GetDiagnosticsPathsFunction); this module is the client-owned shape of that response.
export interface DiagnosticsLogPath {
path: string;
description: string;