popilot 0.6.0 → 0.7.0

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.
Files changed (112) hide show
  1. package/bin/cli.mjs +204 -2
  2. package/lib/doctor.mjs +38 -1
  3. package/lib/hydrate.mjs +15 -0
  4. package/lib/scaffold.mjs +5 -0
  5. package/lib/setup-wizard.mjs +35 -2
  6. package/package.json +1 -1
  7. package/scaffold/.context/project.yaml.example +19 -0
  8. package/scaffold/mcp-pm/package.json +19 -0
  9. package/scaffold/mcp-pm/src/api-client.ts +69 -0
  10. package/scaffold/mcp-pm/src/index.ts +660 -0
  11. package/scaffold/mcp-pm/tsconfig.json +14 -0
  12. package/scaffold/pm-api/package.json +21 -0
  13. package/scaffold/pm-api/sql/schema-core.sql +331 -0
  14. package/scaffold/pm-api/sql/schema-docs.sql +25 -0
  15. package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
  16. package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
  17. package/scaffold/pm-api/src/auth.ts +28 -0
  18. package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
  19. package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
  20. package/scaffold/pm-api/src/db/adapter.ts +36 -0
  21. package/scaffold/pm-api/src/db/turso.ts +147 -0
  22. package/scaffold/pm-api/src/index.ts +114 -0
  23. package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
  24. package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
  25. package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
  26. package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
  27. package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
  28. package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
  29. package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
  30. package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
  31. package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
  32. package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
  33. package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
  34. package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
  35. package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
  36. package/scaffold/pm-api/src/mcp.ts +871 -0
  37. package/scaffold/pm-api/src/nudge.ts +283 -0
  38. package/scaffold/pm-api/src/routes/auth.ts +32 -0
  39. package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
  40. package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
  41. package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
  42. package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
  43. package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
  44. package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
  45. package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
  46. package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
  47. package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
  48. package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
  49. package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
  50. package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
  51. package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
  52. package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
  53. package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
  54. package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
  55. package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
  56. package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
  57. package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
  58. package/scaffold/pm-api/src/types.ts +11 -0
  59. package/scaffold/pm-api/src/utils/activity.ts +22 -0
  60. package/scaffold/pm-api/src/utils/admin.ts +9 -0
  61. package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
  62. package/scaffold/pm-api/src/utils/assignee.ts +69 -0
  63. package/scaffold/pm-api/src/utils/db.ts +45 -0
  64. package/scaffold/pm-api/src/utils/initiative.ts +23 -0
  65. package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
  66. package/scaffold/pm-api/tsconfig.json +15 -0
  67. package/scaffold/pm-api/wrangler.toml.hbs +11 -0
  68. package/scaffold/spec-site/package-lock.json +40 -0
  69. package/scaffold/spec-site/package.json +4 -1
  70. package/scaffold/spec-site/src/api/types.ts +6 -0
  71. package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
  72. package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
  73. package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
  74. package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
  75. package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
  76. package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
  77. package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
  78. package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
  79. package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
  80. package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
  81. package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
  82. package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
  83. package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
  84. package/scaffold/spec-site/src/composables/useUser.ts +19 -1
  85. package/scaffold/spec-site/src/features.ts +108 -0
  86. package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
  87. package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
  88. package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
  89. package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
  90. package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
  91. package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
  92. package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
  93. package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
  94. package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
  95. package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
  96. package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
  97. package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
  98. package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
  99. package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
  100. package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
  101. package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
  102. package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
  103. package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
  104. package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
  105. package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
  106. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
  107. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
  108. package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
  109. package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
  110. package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
  111. package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
  112. package/scaffold/spec-site/src/router.ts +141 -0
@@ -0,0 +1,260 @@
1
+ import { Hono } from 'hono'
2
+ import type { AppEnv } from '../types.js'
3
+ import { query, execute } from '../db/adapter.js'
4
+ import { queryOrThrow, executeOrThrow } from '../utils/db.js'
5
+
6
+ const app = new Hono<AppEnv>()
7
+
8
+ // GET / — load all sprints + epics (pm_epics = SSOT)
9
+ app.get('/', async (c) => {
10
+ const [sprints, epics] = await Promise.all([
11
+ query('SELECT * FROM nav_sprints ORDER BY sort_order'),
12
+ query('SELECT * FROM pm_epics ORDER BY sort_order, id'),
13
+ ])
14
+ if (sprints.error) return c.json({ error: sprints.error }, 500)
15
+ if (epics.error) return c.json({ error: epics.error }, 500)
16
+ return c.json({ sprints: sprints.rows, epics: epics.rows })
17
+ })
18
+
19
+ // POST /sprints
20
+ app.post('/sprints', async (c) => {
21
+ const body = await c.req.json<{
22
+ id: string; label: string; theme: string
23
+ startDate?: string; endDate?: string; sortOrder: number
24
+ status?: string
25
+ }>()
26
+ const status = body.status ?? 'planning'
27
+ const { rowsAffected } = await executeOrThrow(
28
+ 'INSERT INTO nav_sprints (id, label, theme, start_date, end_date, sort_order, status) VALUES (?, ?, ?, ?, ?, ?, ?)',
29
+ [body.id, body.label, body.theme, body.startDate ?? null, body.endDate ?? null, body.sortOrder, status],
30
+ )
31
+ return c.json({ ok: true })
32
+ })
33
+
34
+ // GET /sprints/velocity — velocity based on past sprint results
35
+ app.get('/sprints/velocity', async (c) => {
36
+ const { rows } = await queryOrThrow(
37
+ `SELECT s.sprint,
38
+ SUM(CASE WHEN s.status = 'done' THEN COALESCE(s.story_points, 0) ELSE 0 END) as done_sp,
39
+ SUM(COALESCE(s.story_points, 0)) as total_sp,
40
+ COUNT(*) as story_count
41
+ FROM pm_stories s
42
+ JOIN nav_sprints ns ON s.sprint = ns.id
43
+ WHERE ns.status = 'closed' AND s.sprint IS NOT NULL
44
+ GROUP BY s.sprint
45
+ ORDER BY ns.sort_order`,
46
+ )
47
+
48
+ const sprints = (rows as Array<{ sprint: string; done_sp: number; total_sp: number; story_count: number }>)
49
+ const doneSPs = sprints.map(s => s.done_sp)
50
+ const avgVelocity = doneSPs.length ? Math.round(doneSPs.reduce((a, b) => a + b, 0) / doneSPs.length) : 0
51
+ const lastThree = doneSPs.slice(-3)
52
+ const recentAvg = lastThree.length ? Math.round(lastThree.reduce((a, b) => a + b, 0) / lastThree.length) : 0
53
+
54
+ return c.json({
55
+ sprints,
56
+ avgVelocity,
57
+ recentAvgVelocity: recentAvg,
58
+ sprintCount: sprints.length,
59
+ })
60
+ })
61
+
62
+ // GET /sprints/timeline — full sprint timeline
63
+ app.get('/sprints/timeline', async (c) => {
64
+ const sprints = await query(
65
+ 'SELECT id, label, theme, status, start_date, end_date, velocity, team_size FROM nav_sprints ORDER BY sort_order',
66
+ )
67
+ if (sprints.error) return c.json({ error: sprints.error }, 500)
68
+
69
+ const timeline = []
70
+ for (const s of sprints.rows as Array<Record<string, unknown>>) {
71
+ const stories = await query(
72
+ `SELECT status, story_points FROM pm_stories WHERE sprint = ?`,
73
+ [s.id as string],
74
+ )
75
+ const storyRows = (stories.rows ?? []) as Array<{ status: string; story_points: number | null }>
76
+ const total = storyRows.length
77
+ const done = storyRows.filter(r => r.status === 'done').length
78
+ const totalSP = storyRows.reduce((sum, r) => sum + (r.story_points ?? 0), 0)
79
+ const doneSP = storyRows.filter(r => r.status === 'done').reduce((sum, r) => sum + (r.story_points ?? 0), 0)
80
+
81
+ timeline.push({
82
+ id: s.id,
83
+ label: s.label,
84
+ theme: s.theme,
85
+ status: s.status,
86
+ startDate: s.start_date,
87
+ endDate: s.end_date,
88
+ velocity: s.velocity,
89
+ teamSize: s.team_size,
90
+ storyCount: total,
91
+ doneCount: done,
92
+ totalSP,
93
+ doneSP,
94
+ completionRate: totalSP > 0 ? Math.round((doneSP / totalSP) * 100) : 0,
95
+ })
96
+ }
97
+
98
+ return c.json({ timeline })
99
+ })
100
+
101
+ // PATCH /sprints/:id
102
+ app.patch('/sprints/:id', async (c) => {
103
+ const id = c.req.param('id')
104
+ const body = await c.req.json<{
105
+ label?: string; theme?: string; startDate?: string; endDate?: string; status?: string
106
+ }>()
107
+ const sets: string[] = []
108
+ const args: (string | number | null)[] = []
109
+
110
+ if (body.label !== undefined) { sets.push('label = ?'); args.push(body.label) }
111
+ if (body.theme !== undefined) { sets.push('theme = ?'); args.push(body.theme) }
112
+ if (body.startDate !== undefined) { sets.push('start_date = ?'); args.push(body.startDate) }
113
+ if (body.endDate !== undefined) { sets.push('end_date = ?'); args.push(body.endDate) }
114
+ if (body.status !== undefined) { sets.push('status = ?'); args.push(body.status) }
115
+
116
+ if (sets.length === 0) return c.json({ ok: true })
117
+
118
+ sets.push('updated_at = CURRENT_TIMESTAMP')
119
+ args.push(id)
120
+ const { rowsAffected } = await executeOrThrow(`UPDATE nav_sprints SET ${sets.join(', ')} WHERE id = ?`, args)
121
+ return c.json({ ok: true })
122
+ })
123
+
124
+ // POST /sprints/:id/status — transition sprint status (planning → active → closed)
125
+ app.post('/sprints/:id/status', async (c) => {
126
+ const id = c.req.param('id')
127
+ const body = await c.req.json<{ status: string }>()
128
+ const validStatuses = ['planning', 'active', 'closed']
129
+ if (!validStatuses.includes(body.status)) {
130
+ return c.json({ error: `Invalid status. Must be one of: ${validStatuses.join(', ')}` }, 400)
131
+ }
132
+
133
+ // If activating, also set active=1 and deactivate others
134
+ if (body.status === 'active') {
135
+ const r1 = await execute('UPDATE nav_sprints SET active = 0, updated_at = CURRENT_TIMESTAMP', [])
136
+ if (r1.error) return c.json({ error: r1.error }, 500)
137
+ const r2 = await execute(
138
+ 'UPDATE nav_sprints SET status = ?, active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
139
+ [body.status, id],
140
+ )
141
+ if (r2.error) return c.json({ error: r2.error }, 500)
142
+ if (r2.rowsAffected === 0) return c.json({ error: `Sprint '${id}' not found` }, 404)
143
+ return c.json({ ok: true })
144
+ }
145
+
146
+ // If closing, also set active=0
147
+ const active = body.status === 'planning' ? null : 0
148
+ const sets = ['status = ?', 'updated_at = CURRENT_TIMESTAMP']
149
+ const args: (string | number | null)[] = [body.status]
150
+ if (active !== null) { sets.push('active = ?'); args.push(active) }
151
+ args.push(id)
152
+
153
+ const { rowsAffected } = await executeOrThrow(`UPDATE nav_sprints SET ${sets.join(', ')} WHERE id = ?`, args)
154
+ if (rowsAffected === 0) return c.json({ error: `Sprint '${id}' not found` }, 404)
155
+ return c.json({ ok: true })
156
+ })
157
+
158
+ // DELETE /sprints/:id
159
+ app.delete('/sprints/:id', async (c) => {
160
+ const id = c.req.param('id')
161
+ const r = await execute('DELETE FROM nav_sprints WHERE id = ?', [id])
162
+ if (r.error) return c.json({ error: r.error }, 500)
163
+ return c.json({ ok: true })
164
+ })
165
+
166
+ // POST /sprints/:id/kickoff — sprint kickoff
167
+ app.post('/sprints/:id/kickoff', async (c) => {
168
+ const sprintId = c.req.param('id')
169
+ const body = await c.req.json<{
170
+ storyIds: number[]
171
+ teamMembers?: string[]
172
+ velocity?: number
173
+ }>()
174
+
175
+ if (!body.storyIds?.length) {
176
+ return c.json({ error: 'storyIds required (stories selected from backlog)' }, 400)
177
+ }
178
+
179
+ // 1. Check sprint status — only planning can kickoff
180
+ const sprint = await query('SELECT id, status FROM nav_sprints WHERE id = ?', [sprintId])
181
+ if (sprint.error || !sprint.rows.length) return c.json({ error: 'Sprint not found' }, 404)
182
+ const currentStatus = (sprint.rows[0] as { status: string }).status
183
+ if (currentStatus !== 'planning') {
184
+ return c.json({ error: `Kickoff only available in planning state (current: ${currentStatus})` }, 400)
185
+ }
186
+
187
+ // 2. SP total vs velocity check (warning only, does not block)
188
+ const storyPlaceholders = body.storyIds.map(() => '?').join(', ')
189
+ const stories = await query(
190
+ `SELECT id, title, story_points FROM pm_stories WHERE id IN (${storyPlaceholders})`,
191
+ body.storyIds,
192
+ )
193
+ if (stories.error) return c.json({ error: stories.error }, 500)
194
+ const totalSP = (stories.rows as Array<{ story_points: number | null }>)
195
+ .reduce((sum, s) => sum + (s.story_points ?? 0), 0)
196
+ const velocityWarning = body.velocity && totalSP > body.velocity
197
+ ? `Warning: SP total (${totalSP}) exceeds velocity (${body.velocity})` : null
198
+
199
+ // 3. Assign selected stories to this sprint
200
+ const assignResult = await execute(
201
+ `UPDATE pm_stories SET sprint = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (${storyPlaceholders})`,
202
+ [sprintId, ...body.storyIds],
203
+ )
204
+ if (assignResult.error) return c.json({ error: assignResult.error }, 500)
205
+
206
+ // 4. Deactivate existing active sprint + activate this one
207
+ await execute('UPDATE nav_sprints SET active = 0, status = \'closed\', updated_at = CURRENT_TIMESTAMP WHERE active = 1 AND id != ?', [sprintId])
208
+ await execute(
209
+ 'UPDATE nav_sprints SET status = \'active\', active = 1, velocity = ?, team_size = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
210
+ [body.velocity ?? null, body.teamMembers?.length ?? null, sprintId],
211
+ )
212
+
213
+ return c.json({
214
+ ok: true,
215
+ sprint: sprintId,
216
+ storiesAssigned: body.storyIds.length,
217
+ totalSP,
218
+ velocity: body.velocity ?? null,
219
+ velocityWarning,
220
+ teamMembers: body.teamMembers ?? [],
221
+ })
222
+ })
223
+
224
+ // POST /sprints/:id/activate
225
+ app.post('/sprints/:id/activate', async (c) => {
226
+ const id = c.req.param('id')
227
+ const r1 = await execute('UPDATE nav_sprints SET active = 0, updated_at = CURRENT_TIMESTAMP', [])
228
+ if (r1.error) return c.json({ error: r1.error }, 500)
229
+ const r2 = await execute('UPDATE nav_sprints SET active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [id])
230
+ if (r2.error) return c.json({ error: r2.error }, 500)
231
+ return c.json({ ok: true })
232
+ })
233
+
234
+ // ── Story-level carry-over (epics are global, stories move between sprints) ──
235
+
236
+ // POST /stories/carry-over
237
+ app.post('/stories/carry-over', async (c) => {
238
+ const body = await c.req.json<{
239
+ storyIds: number[]
240
+ targetSprint: string
241
+ }>()
242
+
243
+ if (!body.storyIds?.length || !body.targetSprint) {
244
+ return c.json({ error: 'storyIds and targetSprint required' }, 400)
245
+ }
246
+
247
+ const placeholders = body.storyIds.map(() => '?').join(', ')
248
+ const { rowsAffected } = await executeOrThrow(
249
+ `UPDATE pm_stories SET sprint = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (${placeholders})`,
250
+ [body.targetSprint, ...body.storyIds],
251
+ )
252
+ return c.json({ ok: true, moved: body.storyIds.length })
253
+ })
254
+
255
+ // ── Legacy carry-over (deprecated, backward compat) ──
256
+ app.post('/epics/carry-over', async (c) => {
257
+ return c.json({ error: 'Deprecated. Use POST /stories/carry-over instead.' }, 410)
258
+ })
259
+
260
+ export default app
@@ -0,0 +1,79 @@
1
+ import { Hono } from 'hono'
2
+ import type { AppEnv } from '../types.js'
3
+ import { queryOrThrow, executeOrThrow } from '../utils/db.js'
4
+
5
+ const app = new Hono<AppEnv>()
6
+
7
+ // GET /
8
+ app.get('/', async (c) => {
9
+ const user = c.req.query('user')
10
+ if (!user) return c.json({ error: 'user query param required' }, 400)
11
+ const { rows } = await queryOrThrow(
12
+ 'SELECT * FROM notifications WHERE user_name = ? ORDER BY created_at DESC LIMIT 50',
13
+ [user],
14
+ )
15
+ return c.json({ notifications: rows })
16
+ })
17
+
18
+ // GET /unread-count
19
+ app.get('/unread-count', async (c) => {
20
+ const user = c.req.query('user')
21
+ if (!user) return c.json({ error: 'user query param required' }, 400)
22
+ const { rows } = await queryOrThrow<{ count: number }>(
23
+ 'SELECT COUNT(*) as count FROM notifications WHERE user_name = ? AND is_read = 0',
24
+ [user],
25
+ )
26
+ return c.json({ count: rows[0]?.count ?? 0 })
27
+ })
28
+
29
+ // PATCH /:id/read
30
+ app.patch('/:id/read', async (c) => {
31
+ const id = Number(c.req.param('id'))
32
+ await executeOrThrow('UPDATE notifications SET is_read = 1 WHERE id = ?', [id])
33
+ return c.json({ ok: true })
34
+ })
35
+
36
+ // POST /mark-all-read
37
+ app.post('/mark-all-read', async (c) => {
38
+ const body = await c.req.json<{ user: string }>()
39
+ await executeOrThrow(
40
+ 'UPDATE notifications SET is_read = 1 WHERE user_name = ? AND is_read = 0',
41
+ [body.user],
42
+ )
43
+ return c.json({ ok: true })
44
+ })
45
+
46
+ // POST /
47
+ app.post('/', async (c) => {
48
+ const body = await c.req.json<{
49
+ userName: string; type: string; title: string; body?: string
50
+ sourceType: string; sourceId: string; pageId: string; actor: string
51
+ }>()
52
+ await executeOrThrow(
53
+ 'INSERT INTO notifications (user_name, type, title, body, source_type, source_id, page_id, actor) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
54
+ [body.userName, body.type, body.title, body.body ?? null, body.sourceType, body.sourceId, body.pageId, body.actor],
55
+ )
56
+ return c.json({ ok: true })
57
+ })
58
+
59
+ // DELETE /by-source
60
+ app.delete('/by-source', async (c) => {
61
+ const sourceType = c.req.query('sourceType')
62
+ const sourceId = c.req.query('sourceId')
63
+ if (!sourceType || !sourceId) return c.json({ error: 'sourceType and sourceId query params required' }, 400)
64
+ await executeOrThrow(
65
+ 'DELETE FROM notifications WHERE source_type = ? AND source_id = ?',
66
+ [sourceType, sourceId],
67
+ )
68
+ return c.json({ ok: true })
69
+ })
70
+
71
+ // GET /active-users
72
+ app.get('/active-users', async (c) => {
73
+ const { rows } = await queryOrThrow<{ user_name: string }>(
74
+ 'SELECT user_name FROM auth_tokens WHERE is_active = 1',
75
+ )
76
+ return c.json({ users: rows.map(r => r.user_name) })
77
+ })
78
+
79
+ export default app
@@ -0,0 +1,35 @@
1
+ import { Hono } from 'hono'
2
+ import type { AppEnv } from '../types.js'
3
+ import { query } from '../db/adapter.js'
4
+
5
+ const app = new Hono<AppEnv>()
6
+
7
+ // GET /:pageId/:sprint
8
+ app.get('/:pageId/:sprint', async (c) => {
9
+ const pageId = c.req.param('pageId')
10
+ const sprint = c.req.param('sprint')
11
+
12
+ const [rules, scenarios, areas, versions, meta] = await Promise.all([
13
+ query('SELECT * FROM spec_rules WHERE page_id = ? ORDER BY rule_group, sort_order', [pageId]),
14
+ query('SELECT scenario_id, label, data_json, is_default, sort_order FROM spec_scenarios WHERE page_id = ? ORDER BY sort_order', [pageId]),
15
+ query('SELECT area_id, label, short_label, rule_count, sort_order FROM spec_areas WHERE page_id = ? ORDER BY sort_order', [pageId]),
16
+ query('SELECT * FROM spec_versions WHERE page_id = ?', [pageId]),
17
+ query('SELECT default_scenario_id, spec_title, route_title FROM spec_wireframe_meta WHERE page_id = ? AND sprint = ?', [pageId, sprint]),
18
+ ])
19
+
20
+ if (rules.error) return c.json({ error: rules.error }, 500)
21
+ if (scenarios.error) return c.json({ error: scenarios.error }, 500)
22
+ if (areas.error) return c.json({ error: areas.error }, 500)
23
+ if (versions.error) return c.json({ error: versions.error }, 500)
24
+ if (meta.error) return c.json({ error: meta.error }, 500)
25
+
26
+ return c.json({
27
+ rules: rules.rows,
28
+ scenarios: scenarios.rows,
29
+ areas: areas.rows,
30
+ versions: versions.rows,
31
+ meta: meta.rows[0] ?? null,
32
+ })
33
+ })
34
+
35
+ export default app