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.
- package/bin/cli.mjs +204 -2
- package/lib/doctor.mjs +38 -1
- package/lib/hydrate.mjs +15 -0
- package/lib/scaffold.mjs +5 -0
- package/lib/setup-wizard.mjs +35 -2
- package/package.json +1 -1
- package/scaffold/.context/project.yaml.example +19 -0
- package/scaffold/mcp-pm/package.json +19 -0
- package/scaffold/mcp-pm/src/api-client.ts +69 -0
- package/scaffold/mcp-pm/src/index.ts +660 -0
- package/scaffold/mcp-pm/tsconfig.json +14 -0
- package/scaffold/pm-api/package.json +21 -0
- package/scaffold/pm-api/sql/schema-core.sql +331 -0
- package/scaffold/pm-api/sql/schema-docs.sql +25 -0
- package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
- package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
- package/scaffold/pm-api/src/auth.ts +28 -0
- package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
- package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
- package/scaffold/pm-api/src/db/adapter.ts +36 -0
- package/scaffold/pm-api/src/db/turso.ts +147 -0
- package/scaffold/pm-api/src/index.ts +114 -0
- package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
- package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
- package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
- package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
- package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
- package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
- package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
- package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
- package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
- package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
- package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
- package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
- package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
- package/scaffold/pm-api/src/mcp.ts +871 -0
- package/scaffold/pm-api/src/nudge.ts +283 -0
- package/scaffold/pm-api/src/routes/auth.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
- package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
- package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
- package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
- package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
- package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
- package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
- package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
- package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
- package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
- package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
- package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
- package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
- package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
- package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
- package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
- package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
- package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
- package/scaffold/pm-api/src/types.ts +11 -0
- package/scaffold/pm-api/src/utils/activity.ts +22 -0
- package/scaffold/pm-api/src/utils/admin.ts +9 -0
- package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
- package/scaffold/pm-api/src/utils/assignee.ts +69 -0
- package/scaffold/pm-api/src/utils/db.ts +45 -0
- package/scaffold/pm-api/src/utils/initiative.ts +23 -0
- package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
- package/scaffold/pm-api/tsconfig.json +15 -0
- package/scaffold/pm-api/wrangler.toml.hbs +11 -0
- package/scaffold/spec-site/package-lock.json +40 -0
- package/scaffold/spec-site/package.json +4 -1
- package/scaffold/spec-site/src/api/types.ts +6 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
- package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
- package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
- package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
- package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
- package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
- package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
- package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
- package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
- package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
- package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
- package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
- package/scaffold/spec-site/src/composables/useUser.ts +19 -1
- package/scaffold/spec-site/src/features.ts +108 -0
- package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
- package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
- package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
- package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
- package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
- package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
- package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
- package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
- package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
- package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
- package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
- package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
- package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
- package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
- package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
- package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
- package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
- package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
- package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
- package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
- package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
- package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
- package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
- 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
|