@tonyclaw/agent-inspector 2.0.29 → 2.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-DcqxlgSQ.js → CompareDrawer-tIUf2EJm.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-BxRaXhKR.js +115 -0
- package/.output/public/assets/{ReplayDialog-CBcnPgx1.js → ReplayDialog-D6v1xcuC.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-DSgSqCYt.js → RequestAnatomy-D6PniTlx.js} +1 -1
- package/.output/public/assets/{ResponseView-CjqzBSoF.js → ResponseView-Sx8PjuvU.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-DdBBKolI.js → StreamingChunkSequence-D-Rbfnxh.js} +1 -1
- package/.output/public/assets/_sessionId-BhNTuZ31.js +1 -0
- package/.output/public/assets/index-Byk60-jA.css +1 -0
- package/.output/public/assets/index-Kxptlkpg.js +1 -0
- package/.output/public/assets/{main-B6OLZCp9.js → main-B1kdhY98.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +159 -134
- package/.output/server/{_sessionId-CCfKJJ19.mjs → _sessionId-BceHFKf4.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-uaPI5R6e.mjs → CompareDrawer-uSxK5Ei5.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-BV2nIxXy.mjs → ProxyViewerContainer-_npQ_f2i.mjs} +566 -17
- package/.output/server/_ssr/{ReplayDialog-eE2Hy2Nl.mjs → ReplayDialog-BHAzSLCT.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-B7vwWo57.mjs → RequestAnatomy-DjlZLXBd.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-Bm6Qp8mo.mjs → ResponseView-BuDEJ3fl.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-CbAJKJQx.mjs → StreamingChunkSequence-DFcu0bD8.mjs} +3 -3
- package/.output/server/_ssr/{index-u14_Aj60.mjs → index-Bw6Fw33c.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-qgeGUp1k.mjs → router-D90tMCb3.mjs} +1236 -143
- package/.output/server/_tanstack-start-manifest_v-BVM4AUlx.mjs +4 -0
- package/.output/server/index.mjs +60 -60
- package/README.md +24 -1
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +9 -2
- package/src/components/groups/GroupsDialog.tsx +732 -0
- package/src/lib/groupContract.ts +148 -0
- package/src/lib/runContract.ts +2 -0
- package/src/lib/useGroupEvidence.ts +33 -0
- package/src/lib/useGroups.ts +28 -0
- package/src/mcp/server.ts +282 -1
- package/src/mcp/toolHandlers.ts +123 -0
- package/src/proxy/groupEvidenceExporter.ts +354 -0
- package/src/proxy/groupStore.ts +259 -0
- package/src/routes/api/groups.$groupId.evidence.ts +74 -0
- package/src/routes/api/groups.$groupId.sessions.ts +50 -0
- package/src/routes/api/groups.$groupId.ts +50 -0
- package/src/routes/api/groups.ts +39 -0
- package/.output/public/assets/ProxyViewerContainer-D650kQlv.js +0 -115
- package/.output/public/assets/_sessionId-wctOoGh_.js +0 -1
- package/.output/public/assets/index-BhQGSdhG.js +0 -1
- package/.output/public/assets/index-CwlHPmgL.css +0 -1
- package/.output/server/_tanstack-start-manifest_v-BrfUXnre.mjs +0 -4
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import {
|
|
6
|
+
AddInspectorGroupSessionInputSchema,
|
|
7
|
+
CreateInspectorGroupInputSchema,
|
|
8
|
+
InspectorGroupSchema,
|
|
9
|
+
UpdateInspectorGroupInputSchema,
|
|
10
|
+
type AddInspectorGroupSessionInput,
|
|
11
|
+
type CreateInspectorGroupInput,
|
|
12
|
+
type InspectorGroup,
|
|
13
|
+
type InspectorGroupEvidence,
|
|
14
|
+
type InspectorGroupMember,
|
|
15
|
+
type UpdateInspectorGroupInput,
|
|
16
|
+
} from "../lib/groupContract";
|
|
17
|
+
import { getDataDir } from "./dataDir";
|
|
18
|
+
|
|
19
|
+
const GroupsFileSchema = z.object({
|
|
20
|
+
groups: z.array(InspectorGroupSchema),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function groupsFilePath(): string {
|
|
24
|
+
return join(getDataDir(), "groups.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function nowIso(): string {
|
|
28
|
+
return new Date().toISOString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function stableId(input: string): string {
|
|
32
|
+
return input
|
|
33
|
+
.trim()
|
|
34
|
+
.replace(/[^A-Za-z0-9_.-]+/g, "-")
|
|
35
|
+
.replace(/^-+|-+$/g, "")
|
|
36
|
+
.slice(0, 80);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createGroupId(): string {
|
|
40
|
+
return `grp_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createMemberId(): string {
|
|
44
|
+
return `mem_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeGroupId(input: string | undefined): string {
|
|
48
|
+
if (input === undefined) return createGroupId();
|
|
49
|
+
const normalized = stableId(input);
|
|
50
|
+
return normalized.length > 0 ? normalized : createGroupId();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeMemberId(input: string | undefined, sessionId: string): string {
|
|
54
|
+
if (input === undefined) {
|
|
55
|
+
const normalizedSession = stableId(sessionId);
|
|
56
|
+
return normalizedSession.length > 0 ? `mem_${normalizedSession}` : createMemberId();
|
|
57
|
+
}
|
|
58
|
+
const normalized = stableId(input);
|
|
59
|
+
return normalized.length > 0 ? normalized : createMemberId();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function nullableText(value: string | null | undefined): string | null {
|
|
63
|
+
if (value === undefined || value === null) return null;
|
|
64
|
+
const trimmed = value.trim();
|
|
65
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readGroups(): InspectorGroup[] {
|
|
69
|
+
const filePath = groupsFilePath();
|
|
70
|
+
if (!existsSync(filePath)) return [];
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const parsed: unknown = JSON.parse(readFileSync(filePath, "utf8"));
|
|
74
|
+
const result = GroupsFileSchema.safeParse(parsed);
|
|
75
|
+
return result.success ? result.data.groups : [];
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function writeGroups(groups: readonly InspectorGroup[]): void {
|
|
82
|
+
const filePath = groupsFilePath();
|
|
83
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
84
|
+
writeFileSync(filePath, `${JSON.stringify({ groups }, null, 2)}\n`, "utf8");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function sortedGroups(groups: readonly InspectorGroup[]): InspectorGroup[] {
|
|
88
|
+
return [...groups].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function listGroups(): InspectorGroup[] {
|
|
92
|
+
return sortedGroups(readGroups());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getGroup(groupId: string): InspectorGroup | null {
|
|
96
|
+
return readGroups().find((group) => group.id === groupId) ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createGroup(input: CreateInspectorGroupInput): InspectorGroup {
|
|
100
|
+
const parsed = CreateInspectorGroupInputSchema.parse(input);
|
|
101
|
+
const groupId = normalizeGroupId(parsed?.groupId);
|
|
102
|
+
const existing = getGroup(groupId);
|
|
103
|
+
if (existing !== null) return existing;
|
|
104
|
+
|
|
105
|
+
const timestamp = nowIso();
|
|
106
|
+
const group: InspectorGroup = {
|
|
107
|
+
id: groupId,
|
|
108
|
+
title: parsed?.title ?? `Inspector Group ${groupId}`,
|
|
109
|
+
kind: parsed?.kind ?? "evaluation",
|
|
110
|
+
task: parsed?.task ?? null,
|
|
111
|
+
project: parsed?.project ?? null,
|
|
112
|
+
status: parsed?.status ?? "created",
|
|
113
|
+
tags: parsed?.tags ?? [],
|
|
114
|
+
metadata: parsed?.metadata ?? {},
|
|
115
|
+
evidence: null,
|
|
116
|
+
members: [],
|
|
117
|
+
createdAt: timestamp,
|
|
118
|
+
updatedAt: timestamp,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
writeGroups([...readGroups(), group]);
|
|
122
|
+
return group;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function updateGroup(
|
|
126
|
+
groupId: string,
|
|
127
|
+
input: UpdateInspectorGroupInput,
|
|
128
|
+
): InspectorGroup | null {
|
|
129
|
+
const parsed = UpdateInspectorGroupInputSchema.parse(input);
|
|
130
|
+
const groups = readGroups();
|
|
131
|
+
let updatedGroup: InspectorGroup | null = null;
|
|
132
|
+
const updatedAt = nowIso();
|
|
133
|
+
|
|
134
|
+
const updatedGroups = groups.map((group) => {
|
|
135
|
+
if (group.id !== groupId) return group;
|
|
136
|
+
updatedGroup = {
|
|
137
|
+
...group,
|
|
138
|
+
title: parsed.title ?? group.title,
|
|
139
|
+
kind: parsed.kind ?? group.kind,
|
|
140
|
+
task: parsed.task === undefined ? group.task : parsed.task,
|
|
141
|
+
project: parsed.project === undefined ? group.project : parsed.project,
|
|
142
|
+
status: parsed.status ?? group.status,
|
|
143
|
+
tags: parsed.tags ?? group.tags,
|
|
144
|
+
metadata:
|
|
145
|
+
parsed.metadata === undefined ? group.metadata : { ...group.metadata, ...parsed.metadata },
|
|
146
|
+
updatedAt,
|
|
147
|
+
};
|
|
148
|
+
return updatedGroup;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (updatedGroup === null) return null;
|
|
152
|
+
writeGroups(updatedGroups);
|
|
153
|
+
return updatedGroup;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function matchesExistingMember(
|
|
157
|
+
member: InspectorGroupMember,
|
|
158
|
+
memberId: string,
|
|
159
|
+
sessionId: string,
|
|
160
|
+
runId: string | null,
|
|
161
|
+
): boolean {
|
|
162
|
+
if (member.id === memberId) return true;
|
|
163
|
+
if (member.sessionId !== sessionId) return false;
|
|
164
|
+
return member.runId === runId;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function upsertMember(
|
|
168
|
+
group: InspectorGroup,
|
|
169
|
+
input: AddInspectorGroupSessionInput,
|
|
170
|
+
updatedAt: string,
|
|
171
|
+
): InspectorGroup {
|
|
172
|
+
const parsed = AddInspectorGroupSessionInputSchema.parse(input);
|
|
173
|
+
const sessionId = parsed.sessionId.trim();
|
|
174
|
+
const runId = nullableText(parsed.runId);
|
|
175
|
+
const memberId = normalizeMemberId(parsed.memberId, sessionId);
|
|
176
|
+
let matched = false;
|
|
177
|
+
|
|
178
|
+
const members = group.members.map((member) => {
|
|
179
|
+
if (!matchesExistingMember(member, memberId, sessionId, runId)) return member;
|
|
180
|
+
matched = true;
|
|
181
|
+
return {
|
|
182
|
+
...member,
|
|
183
|
+
sessionId,
|
|
184
|
+
runId,
|
|
185
|
+
label: parsed.label === undefined ? member.label : nullableText(parsed.label),
|
|
186
|
+
agent: parsed.agent === undefined ? member.agent : nullableText(parsed.agent),
|
|
187
|
+
provider: parsed.provider === undefined ? member.provider : nullableText(parsed.provider),
|
|
188
|
+
model: parsed.model === undefined ? member.model : nullableText(parsed.model),
|
|
189
|
+
status: parsed.status === undefined ? member.status : parsed.status,
|
|
190
|
+
metadata:
|
|
191
|
+
parsed.metadata === undefined
|
|
192
|
+
? member.metadata
|
|
193
|
+
: { ...member.metadata, ...parsed.metadata },
|
|
194
|
+
updatedAt,
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (!matched) {
|
|
199
|
+
members.push({
|
|
200
|
+
id: memberId,
|
|
201
|
+
groupId: group.id,
|
|
202
|
+
sessionId,
|
|
203
|
+
runId,
|
|
204
|
+
label: nullableText(parsed.label),
|
|
205
|
+
agent: nullableText(parsed.agent),
|
|
206
|
+
provider: nullableText(parsed.provider),
|
|
207
|
+
model: nullableText(parsed.model),
|
|
208
|
+
status: parsed.status ?? null,
|
|
209
|
+
metadata: parsed.metadata ?? {},
|
|
210
|
+
addedAt: updatedAt,
|
|
211
|
+
updatedAt,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { ...group, members, updatedAt };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function addGroupSession(
|
|
219
|
+
groupId: string,
|
|
220
|
+
input: AddInspectorGroupSessionInput,
|
|
221
|
+
): InspectorGroup | null {
|
|
222
|
+
const groups = readGroups();
|
|
223
|
+
let updatedGroup: InspectorGroup | null = null;
|
|
224
|
+
const updatedAt = nowIso();
|
|
225
|
+
|
|
226
|
+
const updatedGroups = groups.map((group) => {
|
|
227
|
+
if (group.id !== groupId) return group;
|
|
228
|
+
updatedGroup = upsertMember(group, input, updatedAt);
|
|
229
|
+
return updatedGroup;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (updatedGroup === null) return null;
|
|
233
|
+
writeGroups(updatedGroups);
|
|
234
|
+
return updatedGroup;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function updateGroupEvidence(
|
|
238
|
+
groupId: string,
|
|
239
|
+
evidence: InspectorGroupEvidence,
|
|
240
|
+
): InspectorGroup | null {
|
|
241
|
+
const groups = readGroups();
|
|
242
|
+
let updatedGroup: InspectorGroup | null = null;
|
|
243
|
+
const updatedGroups = groups.map((group) => {
|
|
244
|
+
if (group.id !== groupId) return group;
|
|
245
|
+
updatedGroup = {
|
|
246
|
+
...group,
|
|
247
|
+
evidence,
|
|
248
|
+
updatedAt: evidence.exportedAt,
|
|
249
|
+
};
|
|
250
|
+
return updatedGroup;
|
|
251
|
+
});
|
|
252
|
+
if (updatedGroup === null) return null;
|
|
253
|
+
writeGroups(updatedGroups);
|
|
254
|
+
return updatedGroup;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function _resetGroupsForTests(): void {
|
|
258
|
+
writeGroups([]);
|
|
259
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { GroupEvidenceExportOptionsSchema } from "../../lib/groupContract";
|
|
3
|
+
import {
|
|
4
|
+
exportGroupEvidence,
|
|
5
|
+
readGroupEvidenceDocument,
|
|
6
|
+
readGroupEvidenceMarkdown,
|
|
7
|
+
} from "../../proxy/groupEvidenceExporter";
|
|
8
|
+
import { getGroup } from "../../proxy/groupStore";
|
|
9
|
+
|
|
10
|
+
type JsonBodyResult = { ok: true; value: unknown } | { ok: false };
|
|
11
|
+
|
|
12
|
+
async function readJsonBody(request: Request): Promise<JsonBodyResult> {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await request.text();
|
|
15
|
+
if (raw.trim().length === 0) return { ok: true, value: undefined };
|
|
16
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
17
|
+
} catch {
|
|
18
|
+
return { ok: false };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const Route = createFileRoute("/api/groups/$groupId/evidence")({
|
|
23
|
+
server: {
|
|
24
|
+
handlers: {
|
|
25
|
+
GET: ({ params }: { params: { groupId: string } }) => {
|
|
26
|
+
const group = getGroup(params.groupId);
|
|
27
|
+
if (group === null) {
|
|
28
|
+
return Response.json({ error: "Group not found" }, { status: 404 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const markdown = readGroupEvidenceMarkdown(group);
|
|
32
|
+
if (group.evidence === null || markdown === null) {
|
|
33
|
+
return Response.json(
|
|
34
|
+
{ error: "Evidence has not been exported for this group" },
|
|
35
|
+
{ status: 404 },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const document = readGroupEvidenceDocument(group);
|
|
40
|
+
|
|
41
|
+
return Response.json({
|
|
42
|
+
group,
|
|
43
|
+
evidence: group.evidence,
|
|
44
|
+
markdown,
|
|
45
|
+
summary: document?.summary ?? null,
|
|
46
|
+
report: document?.jenkinsReport ?? null,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
POST: async ({ params, request }: { params: { groupId: string }; request: Request }) => {
|
|
50
|
+
const group = getGroup(params.groupId);
|
|
51
|
+
if (group === null) {
|
|
52
|
+
return Response.json({ error: "Group not found" }, { status: 404 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const body = await readJsonBody(request);
|
|
56
|
+
if (!body.ok) {
|
|
57
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const parsed = GroupEvidenceExportOptionsSchema.safeParse(body.value);
|
|
61
|
+
if (!parsed.success) {
|
|
62
|
+
return Response.json(
|
|
63
|
+
{ error: "Invalid request body", details: parsed.error.format() },
|
|
64
|
+
{ status: 400 },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return Response.json(
|
|
69
|
+
await exportGroupEvidence(group, parsed.data, new URL(request.url).origin),
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { AddInspectorGroupSessionInputSchema } from "../../lib/groupContract";
|
|
3
|
+
import { addGroupSession, getGroup } from "../../proxy/groupStore";
|
|
4
|
+
|
|
5
|
+
type JsonBodyResult = { ok: true; value: unknown } | { ok: false };
|
|
6
|
+
|
|
7
|
+
async function readJsonBody(request: Request): Promise<JsonBodyResult> {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await request.text();
|
|
10
|
+
if (raw.trim().length === 0) return { ok: true, value: {} };
|
|
11
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
12
|
+
} catch {
|
|
13
|
+
return { ok: false };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Route = createFileRoute("/api/groups/$groupId/sessions")({
|
|
18
|
+
server: {
|
|
19
|
+
handlers: {
|
|
20
|
+
GET: ({ params }: { params: { groupId: string } }) => {
|
|
21
|
+
const group = getGroup(params.groupId);
|
|
22
|
+
if (group === null) {
|
|
23
|
+
return Response.json({ error: "Group not found" }, { status: 404 });
|
|
24
|
+
}
|
|
25
|
+
return Response.json({ groupId: group.id, members: group.members });
|
|
26
|
+
},
|
|
27
|
+
POST: async ({ params, request }: { params: { groupId: string }; request: Request }) => {
|
|
28
|
+
const body = await readJsonBody(request);
|
|
29
|
+
if (!body.ok) {
|
|
30
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parsed = AddInspectorGroupSessionInputSchema.safeParse(body.value);
|
|
34
|
+
if (!parsed.success) {
|
|
35
|
+
return Response.json(
|
|
36
|
+
{ error: "Invalid request body", details: parsed.error.format() },
|
|
37
|
+
{ status: 400 },
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const updated = addGroupSession(params.groupId, parsed.data);
|
|
42
|
+
if (updated === null) {
|
|
43
|
+
return Response.json({ error: "Group not found" }, { status: 404 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Response.json(updated);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { UpdateInspectorGroupInputSchema } from "../../lib/groupContract";
|
|
3
|
+
import { getGroup, updateGroup } from "../../proxy/groupStore";
|
|
4
|
+
|
|
5
|
+
type JsonBodyResult = { ok: true; value: unknown } | { ok: false };
|
|
6
|
+
|
|
7
|
+
async function readJsonBody(request: Request): Promise<JsonBodyResult> {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await request.text();
|
|
10
|
+
if (raw.trim().length === 0) return { ok: true, value: {} };
|
|
11
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
12
|
+
} catch {
|
|
13
|
+
return { ok: false };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Route = createFileRoute("/api/groups/$groupId")({
|
|
18
|
+
server: {
|
|
19
|
+
handlers: {
|
|
20
|
+
GET: ({ params }: { params: { groupId: string } }) => {
|
|
21
|
+
const group = getGroup(params.groupId);
|
|
22
|
+
if (group === null) {
|
|
23
|
+
return Response.json({ error: "Group not found" }, { status: 404 });
|
|
24
|
+
}
|
|
25
|
+
return Response.json(group);
|
|
26
|
+
},
|
|
27
|
+
PATCH: async ({ params, request }: { params: { groupId: string }; request: Request }) => {
|
|
28
|
+
const body = await readJsonBody(request);
|
|
29
|
+
if (!body.ok) {
|
|
30
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parsed = UpdateInspectorGroupInputSchema.safeParse(body.value);
|
|
34
|
+
if (!parsed.success) {
|
|
35
|
+
return Response.json(
|
|
36
|
+
{ error: "Invalid request body", details: parsed.error.format() },
|
|
37
|
+
{ status: 400 },
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const updated = updateGroup(params.groupId, parsed.data);
|
|
42
|
+
if (updated === null) {
|
|
43
|
+
return Response.json({ error: "Group not found" }, { status: 404 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Response.json(updated);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { CreateInspectorGroupInputSchema } from "../../lib/groupContract";
|
|
3
|
+
import { createGroup, listGroups } from "../../proxy/groupStore";
|
|
4
|
+
|
|
5
|
+
type JsonBodyResult = { ok: true; value: unknown } | { ok: false };
|
|
6
|
+
|
|
7
|
+
async function readJsonBody(request: Request): Promise<JsonBodyResult> {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await request.text();
|
|
10
|
+
if (raw.trim().length === 0) return { ok: true, value: undefined };
|
|
11
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
12
|
+
} catch {
|
|
13
|
+
return { ok: false };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Route = createFileRoute("/api/groups")({
|
|
18
|
+
server: {
|
|
19
|
+
handlers: {
|
|
20
|
+
GET: () => Response.json({ groups: listGroups() }),
|
|
21
|
+
POST: async ({ request }: { request: Request }) => {
|
|
22
|
+
const body = await readJsonBody(request);
|
|
23
|
+
if (!body.ok) {
|
|
24
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const parsed = CreateInspectorGroupInputSchema.safeParse(body.value);
|
|
28
|
+
if (!parsed.success) {
|
|
29
|
+
return Response.json(
|
|
30
|
+
{ error: "Invalid request body", details: parsed.error.format() },
|
|
31
|
+
{ status: 400 },
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Response.json(createGroup(parsed.data), { status: 201 });
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|