codetrap 0.1.6 → 0.1.7
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/README.md +10 -1
- package/package.json +1 -1
- package/src/commands/workflow.ts +49 -7
- package/src/db/connection.ts +6 -6
- package/src/index.ts +4 -4
- package/src/lib/command-requests.ts +22 -0
- package/src/lib/doctor.ts +62 -2
- package/src/lib/embed-output.ts +26 -0
- package/src/lib/scope-context.ts +7 -7
- package/src/lib/scope.ts +4 -4
- package/src/lib/search-eval.ts +100 -7
- package/src/lib/session-operations.ts +37 -0
- package/src/lib/session-store.ts +108 -0
- package/src/lib/store.ts +7 -3
- package/src/web/client-script.ts +1168 -0
- package/src/web/client-text.ts +335 -0
- package/src/web/server.ts +70 -11
- package/src/web/static.ts +224 -472
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
export const WEB_TEXT = {
|
|
2
|
+
en: {
|
|
3
|
+
"app.subtitle": "review console",
|
|
4
|
+
"nav.review": "Review",
|
|
5
|
+
"nav.library": "Library",
|
|
6
|
+
"nav.insights": "Insights",
|
|
7
|
+
"action.refresh": "Refresh",
|
|
8
|
+
"action.add": "Add",
|
|
9
|
+
"action.deleteSession": "Delete",
|
|
10
|
+
"action.cleanDeletedCandidates": "Clear deleted candidates",
|
|
11
|
+
"section.sessions": "sessions",
|
|
12
|
+
"placeholder.projectPath": "/path/to/project",
|
|
13
|
+
"title.candidateInbox": "candidate inbox",
|
|
14
|
+
"title.candidateDetail": "candidate detail",
|
|
15
|
+
"title.trapLibrary": "trap library",
|
|
16
|
+
"title.trapDetail": "trap detail",
|
|
17
|
+
"title.growthInsights": "growth insights",
|
|
18
|
+
"title.insightDetail": "insight detail",
|
|
19
|
+
"title.recentTraps": "recent traps",
|
|
20
|
+
"title.mostViewed": "most viewed",
|
|
21
|
+
"title.recentHighSeverity": "recent high severity",
|
|
22
|
+
"title.evidence": "evidence",
|
|
23
|
+
"title.possibleConflicts": "possible conflicts",
|
|
24
|
+
"title.before": "Before",
|
|
25
|
+
"title.after": "After",
|
|
26
|
+
"tab.inbox": "Inbox {count}",
|
|
27
|
+
"tab.reviewed": "Reviewed {count}",
|
|
28
|
+
"meta.noProject": "no project selected",
|
|
29
|
+
"meta.noSession": "no session selected",
|
|
30
|
+
"meta.sessionCounts": "{goal} / {pending} pending, {reviewed} reviewed",
|
|
31
|
+
"meta.libraryCounts": "{shown} shown / {loaded} loaded / {sort}",
|
|
32
|
+
"meta.insightCounts": "{count} traps / {status} status",
|
|
33
|
+
"meta.selectCandidate": "select a candidate",
|
|
34
|
+
"meta.selectTrap": "select a trap",
|
|
35
|
+
"meta.selectProject": "select a project",
|
|
36
|
+
"empty.noProjects": "No projects",
|
|
37
|
+
"empty.noSessions": "No sessions",
|
|
38
|
+
"empty.noPending": "No pending candidates",
|
|
39
|
+
"empty.noReviewed": "No reviewed candidates",
|
|
40
|
+
"empty.noTrapMatches": "No traps match this view",
|
|
41
|
+
"empty.noTrapSelected": "No trap selected",
|
|
42
|
+
"empty.loadingTrapDetails": "Loading trap details",
|
|
43
|
+
"empty.noCandidateSelected": "No candidate selected",
|
|
44
|
+
"empty.noEvidence": "No evidence",
|
|
45
|
+
"empty.noData": "No data",
|
|
46
|
+
"empty.noTraps": "No traps",
|
|
47
|
+
"action.viewTrap": "View trap",
|
|
48
|
+
"action.clearFilters": "Clear filters",
|
|
49
|
+
"action.save": "Save",
|
|
50
|
+
"action.accept": "Accept",
|
|
51
|
+
"action.reject": "Reject",
|
|
52
|
+
"action.acceptAnyway": "Accept anyway",
|
|
53
|
+
"action.supersede": "Supersede",
|
|
54
|
+
"placeholder.searchTraps": "Search title, context, mistake, fix, tags",
|
|
55
|
+
"placeholder.anyModule": "any module",
|
|
56
|
+
"placeholder.anyOwner": "any owner",
|
|
57
|
+
"placeholder.supersedesId": "supersedes id",
|
|
58
|
+
"label.scope": "Scope",
|
|
59
|
+
"label.status": "Status",
|
|
60
|
+
"label.category": "Category",
|
|
61
|
+
"label.sort": "Sort",
|
|
62
|
+
"label.module": "Module",
|
|
63
|
+
"label.owner": "Owner",
|
|
64
|
+
"label.title": "Title",
|
|
65
|
+
"label.severity": "Severity",
|
|
66
|
+
"label.tags": "Tags",
|
|
67
|
+
"label.pathGlobs": "Path globs",
|
|
68
|
+
"label.context": "Context",
|
|
69
|
+
"label.mistake": "Mistake",
|
|
70
|
+
"label.fix": "Fix",
|
|
71
|
+
"label.created": "Created",
|
|
72
|
+
"label.updated": "Updated",
|
|
73
|
+
"label.stateKey": "State key",
|
|
74
|
+
"label.supersedes": "Supersedes",
|
|
75
|
+
"label.validFrom": "Valid from",
|
|
76
|
+
"label.validUntil": "Valid until",
|
|
77
|
+
"metric.loadedTraps": "Loaded traps",
|
|
78
|
+
"metric.confirmedTraps": "Confirmed traps",
|
|
79
|
+
"metric.highSeverity": "High severity",
|
|
80
|
+
"metric.topCategory": "Top category",
|
|
81
|
+
"metric.focusArea": "Focus area",
|
|
82
|
+
"metric.mostViewed": "Most viewed",
|
|
83
|
+
"metric.currentFilters": "current filters",
|
|
84
|
+
"metric.selectedScope": "selected scope",
|
|
85
|
+
"metric.errorCritical": "error + critical",
|
|
86
|
+
"metric.repeatedPattern": "repeated pattern",
|
|
87
|
+
"metric.largestPattern": "largest pattern",
|
|
88
|
+
"metric.module": "module",
|
|
89
|
+
"metric.tag": "tag",
|
|
90
|
+
"metric.noHits": "no hits yet",
|
|
91
|
+
"insight.categories": "categories",
|
|
92
|
+
"insight.modules": "modules",
|
|
93
|
+
"insight.tags": "tags",
|
|
94
|
+
"insight.severityMix": "severity mix",
|
|
95
|
+
"option.projectGlobal": "project + global",
|
|
96
|
+
"option.allCategories": "all categories",
|
|
97
|
+
"sort.updated": "recently updated",
|
|
98
|
+
"sort.severity": "severity",
|
|
99
|
+
"sort.hits": "hit count",
|
|
100
|
+
"sort.category": "category",
|
|
101
|
+
"sort.title": "title",
|
|
102
|
+
"sortLabel.updated": "recent first",
|
|
103
|
+
"sortLabel.severity": "severity first",
|
|
104
|
+
"sortLabel.hits": "hits first",
|
|
105
|
+
"sortLabel.category": "category sort",
|
|
106
|
+
"sortLabel.title": "title sort",
|
|
107
|
+
"pill.hits": "{count} hits",
|
|
108
|
+
"pill.candidates": "{count} candidates",
|
|
109
|
+
"pill.accepted": "{count} accepted",
|
|
110
|
+
"pill.warnings": "{count} warnings",
|
|
111
|
+
"pill.quality": "quality {score}",
|
|
112
|
+
"pill.conflict": "conflict {status}",
|
|
113
|
+
"pill.action": "action {action}",
|
|
114
|
+
"review.pending": "pending review",
|
|
115
|
+
"review.rejected": "rejected",
|
|
116
|
+
"review.accepted": "accepted -> trap #{id}",
|
|
117
|
+
"review.acceptedDeleted": "accepted -> trap #{id} deleted",
|
|
118
|
+
"review.acceptedLinkMissing": "accepted -> trap link missing",
|
|
119
|
+
"status.refreshed": "Refreshed",
|
|
120
|
+
"status.candidateSaved": "Candidate saved",
|
|
121
|
+
"status.candidateRejected": "Candidate rejected",
|
|
122
|
+
"status.candidateAccepted": "Candidate accepted",
|
|
123
|
+
"status.possibleConflict": "Possible conflict found",
|
|
124
|
+
"status.supersedesRequired": "Supersedes id is required",
|
|
125
|
+
"status.openedTrap": "Opened trap #{id}",
|
|
126
|
+
"status.trapNotInLibrary": "Trap #{id} is not in the current library",
|
|
127
|
+
"status.sessionDeleted": "Session deleted",
|
|
128
|
+
"status.deletedCandidatesCleaned": "Deleted candidate links cleared",
|
|
129
|
+
"prompt.rejectReason": "Reject reason",
|
|
130
|
+
"prompt.deleteSession": "Delete session {id}?",
|
|
131
|
+
"value.project": "project",
|
|
132
|
+
"value.global": "global",
|
|
133
|
+
"value.active": "active",
|
|
134
|
+
"value.all": "all",
|
|
135
|
+
"value.archived": "archived",
|
|
136
|
+
"value.superseded": "superseded",
|
|
137
|
+
"value.proposed": "proposed",
|
|
138
|
+
"value.accepted": "accepted",
|
|
139
|
+
"value.rejected": "rejected",
|
|
140
|
+
"value.accepted_missing": "accepted missing",
|
|
141
|
+
"value.warning": "warning",
|
|
142
|
+
"value.error": "error",
|
|
143
|
+
"value.critical": "critical",
|
|
144
|
+
"value.api": "api",
|
|
145
|
+
"value.database": "database",
|
|
146
|
+
"value.auth": "auth",
|
|
147
|
+
"value.convention": "convention",
|
|
148
|
+
"value.security": "security",
|
|
149
|
+
"value.performance": "performance",
|
|
150
|
+
"value.bug": "bug",
|
|
151
|
+
"value.other": "other",
|
|
152
|
+
"value.none": "none",
|
|
153
|
+
"value.possible": "possible",
|
|
154
|
+
"value.confirmed": "confirmed",
|
|
155
|
+
"value.accept": "accept",
|
|
156
|
+
"value.edit": "edit",
|
|
157
|
+
"value.supersede": "supersede",
|
|
158
|
+
"value.archive_old": "archive old",
|
|
159
|
+
"value.manual": "manual",
|
|
160
|
+
"value.conversation": "conversation",
|
|
161
|
+
"value.commit": "commit",
|
|
162
|
+
"value.issue": "issue",
|
|
163
|
+
"value.test_failure": "test failure",
|
|
164
|
+
"value.article": "article",
|
|
165
|
+
},
|
|
166
|
+
zh: {
|
|
167
|
+
"app.subtitle": "复盘控制台",
|
|
168
|
+
"nav.review": "审核",
|
|
169
|
+
"nav.library": "库",
|
|
170
|
+
"nav.insights": "洞察",
|
|
171
|
+
"action.refresh": "刷新",
|
|
172
|
+
"action.add": "添加",
|
|
173
|
+
"action.deleteSession": "删除",
|
|
174
|
+
"action.cleanDeletedCandidates": "清除已删除候选",
|
|
175
|
+
"section.sessions": "会话",
|
|
176
|
+
"placeholder.projectPath": "/项目/路径",
|
|
177
|
+
"title.candidateInbox": "候选收件箱",
|
|
178
|
+
"title.candidateDetail": "候选详情",
|
|
179
|
+
"title.trapLibrary": "陷阱库",
|
|
180
|
+
"title.trapDetail": "陷阱详情",
|
|
181
|
+
"title.growthInsights": "成长洞察",
|
|
182
|
+
"title.insightDetail": "洞察详情",
|
|
183
|
+
"title.recentTraps": "最近陷阱",
|
|
184
|
+
"title.mostViewed": "查看最多",
|
|
185
|
+
"title.recentHighSeverity": "最近高严重度",
|
|
186
|
+
"title.evidence": "证据",
|
|
187
|
+
"title.possibleConflicts": "可能冲突",
|
|
188
|
+
"title.before": "修改前",
|
|
189
|
+
"title.after": "修改后",
|
|
190
|
+
"tab.inbox": "待审 {count}",
|
|
191
|
+
"tab.reviewed": "已审 {count}",
|
|
192
|
+
"meta.noProject": "未选择项目",
|
|
193
|
+
"meta.noSession": "未选择会话",
|
|
194
|
+
"meta.sessionCounts": "{goal} / {pending} 个待审,{reviewed} 个已审",
|
|
195
|
+
"meta.libraryCounts": "显示 {shown} / 已加载 {loaded} / {sort}",
|
|
196
|
+
"meta.insightCounts": "{count} 条陷阱 / 状态 {status}",
|
|
197
|
+
"meta.selectCandidate": "选择一个候选",
|
|
198
|
+
"meta.selectTrap": "选择一个陷阱",
|
|
199
|
+
"meta.selectProject": "选择一个项目",
|
|
200
|
+
"empty.noProjects": "没有项目",
|
|
201
|
+
"empty.noSessions": "没有会话",
|
|
202
|
+
"empty.noPending": "没有待审候选",
|
|
203
|
+
"empty.noReviewed": "没有已审候选",
|
|
204
|
+
"empty.noTrapMatches": "没有匹配的陷阱",
|
|
205
|
+
"empty.noTrapSelected": "未选择陷阱",
|
|
206
|
+
"empty.loadingTrapDetails": "正在加载陷阱详情",
|
|
207
|
+
"empty.noCandidateSelected": "未选择候选",
|
|
208
|
+
"empty.noEvidence": "没有证据",
|
|
209
|
+
"empty.noData": "没有数据",
|
|
210
|
+
"empty.noTraps": "没有陷阱",
|
|
211
|
+
"action.viewTrap": "查看陷阱",
|
|
212
|
+
"action.clearFilters": "清除筛选",
|
|
213
|
+
"action.save": "保存",
|
|
214
|
+
"action.accept": "接受",
|
|
215
|
+
"action.reject": "拒绝",
|
|
216
|
+
"action.acceptAnyway": "仍然接受",
|
|
217
|
+
"action.supersede": "标记取代",
|
|
218
|
+
"placeholder.searchTraps": "搜索标题、上下文、错误、修复、标签",
|
|
219
|
+
"placeholder.anyModule": "任意模块",
|
|
220
|
+
"placeholder.anyOwner": "任意负责人",
|
|
221
|
+
"placeholder.supersedesId": "被取代的 id",
|
|
222
|
+
"label.scope": "范围",
|
|
223
|
+
"label.status": "状态",
|
|
224
|
+
"label.category": "分类",
|
|
225
|
+
"label.sort": "排序",
|
|
226
|
+
"label.module": "模块",
|
|
227
|
+
"label.owner": "负责人",
|
|
228
|
+
"label.title": "标题",
|
|
229
|
+
"label.severity": "严重度",
|
|
230
|
+
"label.tags": "标签",
|
|
231
|
+
"label.pathGlobs": "路径规则",
|
|
232
|
+
"label.context": "上下文",
|
|
233
|
+
"label.mistake": "错误",
|
|
234
|
+
"label.fix": "修复",
|
|
235
|
+
"label.created": "创建时间",
|
|
236
|
+
"label.updated": "更新时间",
|
|
237
|
+
"label.stateKey": "状态键",
|
|
238
|
+
"label.supersedes": "取代",
|
|
239
|
+
"label.validFrom": "生效开始",
|
|
240
|
+
"label.validUntil": "生效结束",
|
|
241
|
+
"metric.loadedTraps": "已加载陷阱",
|
|
242
|
+
"metric.confirmedTraps": "确认陷阱",
|
|
243
|
+
"metric.highSeverity": "高严重度",
|
|
244
|
+
"metric.topCategory": "最高分类",
|
|
245
|
+
"metric.focusArea": "关注区域",
|
|
246
|
+
"metric.mostViewed": "查看最多",
|
|
247
|
+
"metric.currentFilters": "当前筛选",
|
|
248
|
+
"metric.selectedScope": "选中范围",
|
|
249
|
+
"metric.errorCritical": "error + critical",
|
|
250
|
+
"metric.repeatedPattern": "重复模式",
|
|
251
|
+
"metric.largestPattern": "最大模式",
|
|
252
|
+
"metric.module": "模块",
|
|
253
|
+
"metric.tag": "标签",
|
|
254
|
+
"metric.noHits": "还没有查看记录",
|
|
255
|
+
"insight.categories": "分类",
|
|
256
|
+
"insight.modules": "模块",
|
|
257
|
+
"insight.tags": "标签",
|
|
258
|
+
"insight.severityMix": "严重度分布",
|
|
259
|
+
"option.projectGlobal": "项目 + 全局",
|
|
260
|
+
"option.allCategories": "全部分类",
|
|
261
|
+
"sort.updated": "最近更新",
|
|
262
|
+
"sort.severity": "严重度",
|
|
263
|
+
"sort.hits": "查看次数",
|
|
264
|
+
"sort.category": "分类",
|
|
265
|
+
"sort.title": "标题",
|
|
266
|
+
"sortLabel.updated": "最近优先",
|
|
267
|
+
"sortLabel.severity": "严重度优先",
|
|
268
|
+
"sortLabel.hits": "查看次数优先",
|
|
269
|
+
"sortLabel.category": "按分类排序",
|
|
270
|
+
"sortLabel.title": "按标题排序",
|
|
271
|
+
"pill.hits": "{count} 次查看",
|
|
272
|
+
"pill.candidates": "{count} 个候选",
|
|
273
|
+
"pill.accepted": "{count} 个已接受",
|
|
274
|
+
"pill.warnings": "{count} 个警告",
|
|
275
|
+
"pill.quality": "质量 {score}",
|
|
276
|
+
"pill.conflict": "冲突 {status}",
|
|
277
|
+
"pill.action": "建议 {action}",
|
|
278
|
+
"review.pending": "待审核",
|
|
279
|
+
"review.rejected": "已拒绝",
|
|
280
|
+
"review.accepted": "已接受 -> 陷阱 #{id}",
|
|
281
|
+
"review.acceptedDeleted": "已接受 -> 陷阱 #{id} 已删除",
|
|
282
|
+
"review.acceptedLinkMissing": "已接受 -> 缺少陷阱链接",
|
|
283
|
+
"status.refreshed": "已刷新",
|
|
284
|
+
"status.candidateSaved": "候选已保存",
|
|
285
|
+
"status.candidateRejected": "候选已拒绝",
|
|
286
|
+
"status.candidateAccepted": "候选已接受",
|
|
287
|
+
"status.possibleConflict": "发现可能冲突",
|
|
288
|
+
"status.supersedesRequired": "需要填写被取代的 id",
|
|
289
|
+
"status.openedTrap": "已打开陷阱 #{id}",
|
|
290
|
+
"status.trapNotInLibrary": "当前陷阱库里没有陷阱 #{id}",
|
|
291
|
+
"status.sessionDeleted": "会话已删除",
|
|
292
|
+
"status.deletedCandidatesCleaned": "已清除删除候选链接",
|
|
293
|
+
"prompt.rejectReason": "拒绝原因",
|
|
294
|
+
"prompt.deleteSession": "删除会话 {id}?",
|
|
295
|
+
"value.project": "项目",
|
|
296
|
+
"value.global": "全局",
|
|
297
|
+
"value.active": "有效",
|
|
298
|
+
"value.all": "全部",
|
|
299
|
+
"value.archived": "已归档",
|
|
300
|
+
"value.superseded": "已取代",
|
|
301
|
+
"value.proposed": "待提议",
|
|
302
|
+
"value.accepted": "已接受",
|
|
303
|
+
"value.rejected": "已拒绝",
|
|
304
|
+
"value.accepted_missing": "接受记录缺失",
|
|
305
|
+
"value.warning": "警告",
|
|
306
|
+
"value.error": "错误",
|
|
307
|
+
"value.critical": "严重",
|
|
308
|
+
"value.api": "API",
|
|
309
|
+
"value.database": "数据库",
|
|
310
|
+
"value.auth": "认证",
|
|
311
|
+
"value.convention": "约定",
|
|
312
|
+
"value.security": "安全",
|
|
313
|
+
"value.performance": "性能",
|
|
314
|
+
"value.bug": "缺陷",
|
|
315
|
+
"value.other": "其他",
|
|
316
|
+
"value.none": "无",
|
|
317
|
+
"value.possible": "可能",
|
|
318
|
+
"value.confirmed": "确认",
|
|
319
|
+
"value.accept": "接受",
|
|
320
|
+
"value.edit": "编辑",
|
|
321
|
+
"value.supersede": "取代",
|
|
322
|
+
"value.archive_old": "归档旧项",
|
|
323
|
+
"value.manual": "手动",
|
|
324
|
+
"value.conversation": "对话",
|
|
325
|
+
"value.commit": "提交",
|
|
326
|
+
"value.issue": "Issue",
|
|
327
|
+
"value.test_failure": "测试失败",
|
|
328
|
+
"value.article": "文章",
|
|
329
|
+
}
|
|
330
|
+
} as const;
|
|
331
|
+
|
|
332
|
+
export type WebLocale = keyof typeof WEB_TEXT;
|
|
333
|
+
export type WebTextKey = keyof typeof WEB_TEXT["en"];
|
|
334
|
+
|
|
335
|
+
export const WEB_TEXT_JSON = JSON.stringify(WEB_TEXT);
|
package/src/web/server.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { TrapStore } from "../lib/store";
|
|
|
5
5
|
import { TrapOperations } from "../lib/trap-operations";
|
|
6
6
|
import { SessionOperations, type SessionAcceptResult } from "../lib/session-operations";
|
|
7
7
|
import { SessionStore } from "../lib/session-store";
|
|
8
|
+
import { toListJson, toTrapDetailsJson } from "../lib/output-json";
|
|
8
9
|
import { WEB_INDEX_HTML } from "./static";
|
|
9
10
|
import {
|
|
10
11
|
addWebProject,
|
|
@@ -89,6 +90,9 @@ export function createWebHandler(context: WebContext): (request: Request) => Pro
|
|
|
89
90
|
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
90
91
|
return htmlResponse(WEB_INDEX_HTML);
|
|
91
92
|
}
|
|
93
|
+
if (url.pathname === "/favicon.ico") {
|
|
94
|
+
return new Response(null, { status: 204 });
|
|
95
|
+
}
|
|
92
96
|
return jsonResponse({ error: "Not found" }, 404);
|
|
93
97
|
} catch (error) {
|
|
94
98
|
const status = error instanceof WebHttpError || error instanceof WebPayloadError ? error.status : 500;
|
|
@@ -141,14 +145,14 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
|
|
|
141
145
|
|
|
142
146
|
if (request.method === "GET" && url.pathname === "/api/sessions") {
|
|
143
147
|
const projectRoot = projectRootFromQuery(url, context);
|
|
144
|
-
const sessions = sessionOperations(projectRoot).sessions.listSessions({ status: "all", limit: 100 });
|
|
148
|
+
const sessions = sessionOperations(projectRoot, context.home).sessions.listSessions({ status: "all", limit: 100 });
|
|
145
149
|
return jsonResponse({ project_root: projectRoot, sessions });
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
if (request.method === "GET" && url.pathname === "/api/candidates") {
|
|
149
153
|
const projectRoot = projectRootFromQuery(url, context);
|
|
150
154
|
const sessionId = requiredQuery(url, "session");
|
|
151
|
-
const ops = sessionOperations(projectRoot);
|
|
155
|
+
const ops = sessionOperations(projectRoot, context.home);
|
|
152
156
|
const session = ops.sessions.showSession(sessionId).session;
|
|
153
157
|
const document = ops.sessions.candidateDocument(sessionId);
|
|
154
158
|
return jsonResponse({
|
|
@@ -158,19 +162,36 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
|
|
|
158
162
|
});
|
|
159
163
|
}
|
|
160
164
|
|
|
165
|
+
if (request.method === "GET" && url.pathname === "/api/traps") {
|
|
166
|
+
const projectRoot = projectRootFromQuery(url, context);
|
|
167
|
+
const groups = trapOperations(projectRoot, context.home).listTraps({
|
|
168
|
+
category: optionalQuery(url, "category"),
|
|
169
|
+
scope: optionalQuery(url, "scope"),
|
|
170
|
+
status: optionalQuery(url, "status"),
|
|
171
|
+
module: optionalQuery(url, "module"),
|
|
172
|
+
owner: optionalQuery(url, "owner"),
|
|
173
|
+
limit: optionalNumberQuery(url, "limit"),
|
|
174
|
+
offset: optionalNumberQuery(url, "offset"),
|
|
175
|
+
});
|
|
176
|
+
return jsonResponse({
|
|
177
|
+
project_root: projectRoot,
|
|
178
|
+
traps: toListJson(groups),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
161
182
|
if (request.method === "GET" && url.pathname === "/api/trap") {
|
|
162
183
|
const projectRoot = projectRootFromQuery(url, context);
|
|
163
184
|
const id = numberQuery(url, "id");
|
|
164
185
|
const scope = url.searchParams.get("scope") ?? undefined;
|
|
165
|
-
const details = trapOperations(projectRoot).getTrapDetails(id, scope);
|
|
186
|
+
const details = trapOperations(projectRoot, context.home).getTrapDetails(id, scope);
|
|
166
187
|
if (!details) throw new WebHttpError(404, `Trap #${id} not found.`);
|
|
167
|
-
return jsonResponse(details);
|
|
188
|
+
return jsonResponse(toTrapDetailsJson(details));
|
|
168
189
|
}
|
|
169
190
|
|
|
170
191
|
if (request.method === "POST" && url.pathname === "/api/candidate/save") {
|
|
171
192
|
const body = await readJsonBody(request);
|
|
172
193
|
const projectRoot = projectRootFromBody(body, context);
|
|
173
|
-
const result = sessionOperations(projectRoot).sessions.saveCandidate({
|
|
194
|
+
const result = sessionOperations(projectRoot, context.home).sessions.saveCandidate({
|
|
174
195
|
candidateId: stringBodyField(body, "candidateId"),
|
|
175
196
|
sessionId: optionalStringBodyField(body, "sessionId"),
|
|
176
197
|
edit: recordBodyField(body, "trap"),
|
|
@@ -181,7 +202,7 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
|
|
|
181
202
|
if (request.method === "POST" && url.pathname === "/api/candidate/accept") {
|
|
182
203
|
const body = await readJsonBody(request);
|
|
183
204
|
const projectRoot = projectRootFromBody(body, context);
|
|
184
|
-
const result = await sessionOperations(projectRoot).sessions.acceptCandidate({
|
|
205
|
+
const result = await sessionOperations(projectRoot, context.home).sessions.acceptCandidate({
|
|
185
206
|
candidateId: stringBodyField(body, "candidateId"),
|
|
186
207
|
sessionId: optionalStringBodyField(body, "sessionId"),
|
|
187
208
|
acceptAnyway: booleanBodyField(body, "acceptAnyway"),
|
|
@@ -204,7 +225,7 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
|
|
|
204
225
|
if (request.method === "POST" && url.pathname === "/api/candidate/reject") {
|
|
205
226
|
const body = await readJsonBody(request);
|
|
206
227
|
const projectRoot = projectRootFromBody(body, context);
|
|
207
|
-
const result = sessionOperations(projectRoot).sessions.rejectCandidate({
|
|
228
|
+
const result = sessionOperations(projectRoot, context.home).sessions.rejectCandidate({
|
|
208
229
|
candidateId: stringBodyField(body, "candidateId"),
|
|
209
230
|
sessionId: optionalStringBodyField(body, "sessionId"),
|
|
210
231
|
reason: optionalStringBodyField(body, "reason"),
|
|
@@ -212,19 +233,44 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
|
|
|
212
233
|
return jsonResponse({ success: true, session: result.session, candidate: result.candidate });
|
|
213
234
|
}
|
|
214
235
|
|
|
236
|
+
if (request.method === "POST" && url.pathname === "/api/session/delete") {
|
|
237
|
+
const body = await readJsonBody(request);
|
|
238
|
+
const projectRoot = projectRootFromBody(body, context);
|
|
239
|
+
const result = sessionOperations(projectRoot, context.home).sessions.deleteSession(
|
|
240
|
+
stringBodyField(body, "sessionId")
|
|
241
|
+
);
|
|
242
|
+
return jsonResponse({ success: true, ...result });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (request.method === "POST" && url.pathname === "/api/session/cleanup") {
|
|
246
|
+
const body = await readJsonBody(request);
|
|
247
|
+
const projectRoot = projectRootFromBody(body, context);
|
|
248
|
+
const ops = sessionOperations(projectRoot, context.home);
|
|
249
|
+
const result = ops.sessions.cleanupDeletedTrapCandidates(
|
|
250
|
+
optionalStringBodyField(body, "sessionId")
|
|
251
|
+
);
|
|
252
|
+
return jsonResponse({
|
|
253
|
+
success: true,
|
|
254
|
+
session: result.session,
|
|
255
|
+
removed_count: result.removed_count,
|
|
256
|
+
removed_candidate_ids: result.removed_candidate_ids,
|
|
257
|
+
candidates: webCandidates(result.candidates, ops.traps),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
215
261
|
throw new WebHttpError(404, "Not found");
|
|
216
262
|
}
|
|
217
263
|
|
|
218
|
-
function sessionOperations(projectRoot: string): { traps: TrapOperations; sessions: SessionOperations } {
|
|
219
|
-
const traps = trapOperations(projectRoot);
|
|
264
|
+
function sessionOperations(projectRoot: string, home?: string): { traps: TrapOperations; sessions: SessionOperations } {
|
|
265
|
+
const traps = trapOperations(projectRoot, home);
|
|
220
266
|
return {
|
|
221
267
|
traps,
|
|
222
268
|
sessions: new SessionOperations(new SessionStore(projectRoot), traps),
|
|
223
269
|
};
|
|
224
270
|
}
|
|
225
271
|
|
|
226
|
-
function trapOperations(projectRoot: string): TrapOperations {
|
|
227
|
-
return new TrapOperations(new TrapStore(projectRoot));
|
|
272
|
+
function trapOperations(projectRoot: string, home?: string): TrapOperations {
|
|
273
|
+
return new TrapOperations(new TrapStore(projectRoot, undefined, home));
|
|
228
274
|
}
|
|
229
275
|
|
|
230
276
|
function webCandidates(candidates: CandidateTrap[], traps: TrapOperations): WebCandidate[] {
|
|
@@ -353,6 +399,19 @@ function numberQuery(url: URL, key: string): number {
|
|
|
353
399
|
return value;
|
|
354
400
|
}
|
|
355
401
|
|
|
402
|
+
function optionalQuery(url: URL, key: string): string | undefined {
|
|
403
|
+
const value = url.searchParams.get(key)?.trim();
|
|
404
|
+
return value || undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function optionalNumberQuery(url: URL, key: string): number | undefined {
|
|
408
|
+
const value = optionalQuery(url, key);
|
|
409
|
+
if (!value) return undefined;
|
|
410
|
+
const parsed = Number.parseInt(value, 10);
|
|
411
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new WebHttpError(400, `${key} must be a positive integer.`);
|
|
412
|
+
return parsed;
|
|
413
|
+
}
|
|
414
|
+
|
|
356
415
|
function stringBodyField(body: Record<string, unknown>, key: string): string {
|
|
357
416
|
const value = optionalStringBodyField(body, key);
|
|
358
417
|
if (!value) throw new WebHttpError(400, `${key} is required.`);
|