popilot 0.6.0 → 0.8.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-notification-server/package.json +18 -0
- package/scaffold/mcp-notification-server/src/index.ts +275 -0
- package/scaffold/mcp-notification-server/src/turso-client.ts +142 -0
- package/scaffold/mcp-notification-server/tsconfig.json +14 -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/001-memo-v2.sql +49 -0
- package/scaffold/pm-api/sql/002-notifications.sql +18 -0
- package/scaffold/pm-api/sql/003-content.sql +66 -0
- package/scaffold/pm-api/sql/004-agent-events.sql +21 -0
- package/scaffold/pm-api/sql/005-epic-sprint-decoupling.sql +6 -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/retro-link.ts +32 -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 +892 -0
- package/scaffold/spec-site/package.json +15 -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/AuthGate.vue +117 -0
- package/scaffold/spec-site/src/components/BurndownChart.vue +78 -0
- package/scaffold/spec-site/src/components/DocComments.vue +137 -0
- package/scaffold/spec-site/src/components/DocEditor.vue +118 -0
- package/scaffold/spec-site/src/components/DocExportBar.vue +110 -0
- package/scaffold/spec-site/src/components/DocsSidebar.vue +309 -0
- package/scaffold/spec-site/src/components/EmptyState.vue +30 -0
- package/scaffold/spec-site/src/components/ErrorBanner.vue +38 -0
- package/scaffold/spec-site/src/components/Icon.vue +58 -0
- package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
- package/scaffold/spec-site/src/components/MemoChecklist.vue +88 -0
- package/scaffold/spec-site/src/components/MemoGraph.vue +75 -0
- package/scaffold/spec-site/src/components/MemoItem.vue +353 -0
- package/scaffold/spec-site/src/components/MemoRelations.vue +101 -0
- package/scaffold/spec-site/src/components/MemoTimeline.vue +53 -0
- package/scaffold/spec-site/src/components/MentionInput.vue +174 -0
- package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
- package/scaffold/spec-site/src/components/PriorityBadge.vue +23 -0
- package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
- package/scaffold/spec-site/src/components/SlashCommand.ts +123 -0
- package/scaffold/spec-site/src/components/StateDisplay.vue +54 -0
- package/scaffold/spec-site/src/components/TreeNode.vue +82 -0
- package/scaffold/spec-site/src/components/UserAvatar.vue +24 -0
- package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
- package/scaffold/spec-site/src/composables/navTypes.ts +3 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
- package/scaffold/spec-site/src/composables/useBottomSheet.ts +103 -0
- 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/useMemo.ts +39 -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/useTurso.ts +17 -0
- package/scaffold/spec-site/src/composables/useUser.ts +19 -1
- package/scaffold/spec-site/src/composables/useViewport.ts +26 -0
- package/scaffold/spec-site/src/features.ts +108 -0
- package/scaffold/spec-site/src/mockup/ComponentPalette.vue +61 -0
- package/scaffold/spec-site/src/mockup/MockupCanvas.vue +459 -0
- package/scaffold/spec-site/src/mockup/PropertyPanel.vue +217 -0
- package/scaffold/spec-site/src/mockup/componentCatalog.ts +68 -0
- package/scaffold/spec-site/src/mockup/useScenarios.ts +67 -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/DocsEditor.vue +119 -0
- package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
- package/scaffold/spec-site/src/pages/DocsPage.vue +444 -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/MemosPage.vue +857 -0
- package/scaffold/spec-site/src/pages/MockupEditorPage.vue +611 -0
- package/scaffold/spec-site/src/pages/MockupListPage.vue +121 -0
- package/scaffold/spec-site/src/pages/MockupViewerPage.vue +199 -0
- package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
- package/scaffold/spec-site/src/pages/NotificationSettingsPage.vue +59 -0
- package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
- package/scaffold/spec-site/src/pages/SprintAdmin.vue +521 -0
- package/scaffold/spec-site/src/pages/SprintTimeline.vue +159 -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/KanbanBoard.vue +93 -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
- package/scaffold/spec-site/src/styles/buttons.css +124 -0
- package/scaffold/spec-site/src/utils/parseMentions.ts +56 -0
- package/scaffold/spec-site/src/utils/timezone.ts +18 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import { buildUpdateQuery } from '../utils/db.js'
|
|
3
|
+
import type { AppEnv } from '../types.js'
|
|
4
|
+
import { query, execute } from '../db/adapter.js'
|
|
5
|
+
import { queryOrThrow, executeOrThrow } from '../utils/db.js'
|
|
6
|
+
|
|
7
|
+
const app = new Hono<AppEnv>()
|
|
8
|
+
|
|
9
|
+
import { validateAssignee } from '../utils/assignee.js'
|
|
10
|
+
|
|
11
|
+
// Auto-sync task SP sum to story SP
|
|
12
|
+
async function syncStorySP(storyId: number) {
|
|
13
|
+
const { rows } = await queryOrThrow<{ total: number | null }>(
|
|
14
|
+
'SELECT SUM(story_points) as total FROM pm_tasks WHERE story_id = ?',
|
|
15
|
+
[storyId],
|
|
16
|
+
)
|
|
17
|
+
if (rows.length > 0) {
|
|
18
|
+
const total = rows[0].total ?? 0
|
|
19
|
+
await execute('UPDATE pm_stories SET story_points = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [total, storyId])
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// GET /epics
|
|
24
|
+
app.get('/epics', async (c) => {
|
|
25
|
+
const { rows } = await queryOrThrow('SELECT * FROM pm_epics ORDER BY title')
|
|
26
|
+
return c.json({ epics: rows })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// GET /data
|
|
30
|
+
app.get('/data', async (c) => {
|
|
31
|
+
const sprint = c.req.query('sprint')
|
|
32
|
+
|
|
33
|
+
if (sprint) {
|
|
34
|
+
// sprint=backlog -> stories where sprint IS NULL
|
|
35
|
+
const storiesResult = sprint === 'backlog'
|
|
36
|
+
? await query("SELECT * FROM pm_stories WHERE sprint IS NULL AND status NOT IN ('done', 'cancelled')")
|
|
37
|
+
: await query('SELECT * FROM pm_stories WHERE sprint = ?', [sprint])
|
|
38
|
+
if (storiesResult.error) return c.json({ error: storiesResult.error }, 500)
|
|
39
|
+
|
|
40
|
+
if (storiesResult.rows.length === 0) {
|
|
41
|
+
return c.json({ stories: [], tasks: [] })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const storyIds = (storiesResult.rows as Array<{ id: number }>).map(s => s.id)
|
|
45
|
+
const placeholders = storyIds.map(() => '?').join(', ')
|
|
46
|
+
const tasksResult = await query(
|
|
47
|
+
`SELECT * FROM pm_tasks WHERE story_id IN (${placeholders})`,
|
|
48
|
+
storyIds,
|
|
49
|
+
)
|
|
50
|
+
if (tasksResult.error) return c.json({ error: tasksResult.error }, 500)
|
|
51
|
+
return c.json({ stories: storiesResult.rows, tasks: tasksResult.rows })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const [storiesResult, tasksResult] = await Promise.all([
|
|
55
|
+
query('SELECT * FROM pm_stories'),
|
|
56
|
+
query('SELECT * FROM pm_tasks'),
|
|
57
|
+
])
|
|
58
|
+
if (storiesResult.error) return c.json({ error: storiesResult.error }, 500)
|
|
59
|
+
if (tasksResult.error) return c.json({ error: tasksResult.error }, 500)
|
|
60
|
+
return c.json({ stories: storiesResult.rows, tasks: tasksResult.rows })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// POST /epics
|
|
64
|
+
app.post('/epics', async (c) => {
|
|
65
|
+
const body = await c.req.json<{
|
|
66
|
+
title: string; description?: string; status?: string; owner?: string
|
|
67
|
+
sortOrder?: number; originSprint?: string
|
|
68
|
+
}>()
|
|
69
|
+
const { rowsAffected } = await executeOrThrow(
|
|
70
|
+
'INSERT INTO pm_epics (title, description, status, owner, sort_order, origin_sprint) VALUES (?, ?, ?, ?, ?, ?)',
|
|
71
|
+
[
|
|
72
|
+
body.title, body.description ?? null, body.status ?? 'active', body.owner ?? null,
|
|
73
|
+
body.sortOrder ?? 0, body.originSprint ?? null,
|
|
74
|
+
],
|
|
75
|
+
)
|
|
76
|
+
return c.json({ ok: true }, 201)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// PATCH /epics/:id
|
|
80
|
+
app.patch('/epics/:id', async (c) => {
|
|
81
|
+
const id = Number(c.req.param('id'))
|
|
82
|
+
const body = await c.req.json<Record<string, unknown>>()
|
|
83
|
+
|
|
84
|
+
const fieldMap: Record<string, string> = {
|
|
85
|
+
title: 'title', description: 'description', status: 'status', owner: 'owner',
|
|
86
|
+
sortOrder: 'sort_order', originSprint: 'origin_sprint', category: 'category', badge: 'badge',
|
|
87
|
+
}
|
|
88
|
+
const sets: string[] = []
|
|
89
|
+
const args: (string | number | null)[] = []
|
|
90
|
+
|
|
91
|
+
for (const [key, col] of Object.entries(fieldMap)) {
|
|
92
|
+
if (body[key] !== undefined) {
|
|
93
|
+
sets.push(`${col} = ?`)
|
|
94
|
+
args.push(body[key] as string | number | null)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (sets.length === 0) return c.json({ ok: true })
|
|
98
|
+
|
|
99
|
+
sets.push('updated_at = CURRENT_TIMESTAMP')
|
|
100
|
+
args.push(id)
|
|
101
|
+
const { rowsAffected } = await executeOrThrow(`UPDATE pm_epics SET ${sets.join(', ')} WHERE id = ?`, args)
|
|
102
|
+
return c.json({ ok: true })
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// DELETE /epics/:id
|
|
106
|
+
app.delete('/epics/:id', async (c) => {
|
|
107
|
+
const id = Number(c.req.param('id'))
|
|
108
|
+
const r1 = await execute('UPDATE pm_stories SET epic_id = NULL WHERE epic_id = ?', [id])
|
|
109
|
+
if (r1.error) return c.json({ error: r1.error }, 500)
|
|
110
|
+
const r2 = await execute('DELETE FROM pm_epics WHERE id = ?', [id])
|
|
111
|
+
if (r2.error) return c.json({ error: r2.error }, 500)
|
|
112
|
+
return c.json({ ok: true })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// POST /stories
|
|
116
|
+
app.post('/stories', async (c) => {
|
|
117
|
+
const body = await c.req.json<{
|
|
118
|
+
epicId?: number; sprint?: string | null; title: string; description?: string
|
|
119
|
+
acceptanceCriteria?: string; assignee?: string; status?: string
|
|
120
|
+
priority?: string; area?: string; storyPoints?: number; sortOrder?: number
|
|
121
|
+
}>()
|
|
122
|
+
|
|
123
|
+
const assigneeErr = await validateAssignee(body.assignee)
|
|
124
|
+
if (assigneeErr) return c.json({ error: assigneeErr }, 400)
|
|
125
|
+
|
|
126
|
+
const epicUid = body.epicId ? `pm:${body.epicId}` : `pm:0`
|
|
127
|
+
|
|
128
|
+
const { rowsAffected } = await executeOrThrow(
|
|
129
|
+
`INSERT INTO pm_stories (epic_id, epic_uid, sprint, title, description, acceptance_criteria, assignee, status, priority, area, story_points, sort_order, start_date, due_date)
|
|
130
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
131
|
+
[
|
|
132
|
+
body.epicId ?? null, epicUid, body.sprint ?? null, body.title,
|
|
133
|
+
body.description ?? null, body.acceptanceCriteria ?? null,
|
|
134
|
+
body.assignee ?? null, body.status ?? 'todo',
|
|
135
|
+
body.priority ?? null, body.area ?? null,
|
|
136
|
+
body.storyPoints ?? null, body.sortOrder ?? 0,
|
|
137
|
+
(body as any).startDate ?? null, (body as any).dueDate ?? null,
|
|
138
|
+
],
|
|
139
|
+
)
|
|
140
|
+
return c.json({ ok: true }, 201)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// PATCH /stories/:id
|
|
144
|
+
app.patch('/stories/:id', async (c) => {
|
|
145
|
+
const id = Number(c.req.param('id'))
|
|
146
|
+
const body = await c.req.json<Record<string, unknown>>()
|
|
147
|
+
|
|
148
|
+
if (body.assignee !== undefined) {
|
|
149
|
+
const assigneeErr = await validateAssignee(body.assignee as string | null)
|
|
150
|
+
if (assigneeErr) return c.json({ error: assigneeErr }, 400)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const fieldMap: Record<string, string> = {
|
|
154
|
+
epicId: 'epic_id', sprint: 'sprint', title: 'title',
|
|
155
|
+
description: 'description', acceptanceCriteria: 'acceptance_criteria',
|
|
156
|
+
assignee: 'assignee', status: 'status', priority: 'priority',
|
|
157
|
+
area: 'area', storyPoints: 'story_points', figmaUrl: 'figma_url', sortOrder: 'sort_order',
|
|
158
|
+
startDate: 'start_date', dueDate: 'due_date',
|
|
159
|
+
}
|
|
160
|
+
const sets: string[] = []
|
|
161
|
+
const args: (string | number | null)[] = []
|
|
162
|
+
|
|
163
|
+
for (const [key, col] of Object.entries(fieldMap)) {
|
|
164
|
+
if (body[key] !== undefined) {
|
|
165
|
+
sets.push(`${col} = ?`)
|
|
166
|
+
args.push(body[key] as string | number | null)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (sets.length === 0) return c.json({ ok: true })
|
|
170
|
+
|
|
171
|
+
sets.push('updated_at = CURRENT_TIMESTAMP')
|
|
172
|
+
args.push(id)
|
|
173
|
+
const { rowsAffected } = await executeOrThrow(`UPDATE pm_stories SET ${sets.join(', ')} WHERE id = ?`, args)
|
|
174
|
+
|
|
175
|
+
// Activity log
|
|
176
|
+
if (body.status !== undefined) {
|
|
177
|
+
const { logActivity } = await import('../utils/activity.js')
|
|
178
|
+
const userName = c.get('userName') || 'unknown'
|
|
179
|
+
await logActivity(userName, 'story_status_change', 'story', id, body.title as string || `SID:${id}`, { status: body.status })
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return c.json({ ok: true })
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// DELETE /stories/:id
|
|
186
|
+
app.delete('/stories/:id', async (c) => {
|
|
187
|
+
const id = Number(c.req.param('id'))
|
|
188
|
+
const r1 = await execute('DELETE FROM pm_tasks WHERE story_id = ?', [id])
|
|
189
|
+
if (r1.error) return c.json({ error: r1.error }, 500)
|
|
190
|
+
const r2 = await execute('DELETE FROM pm_stories WHERE id = ?', [id])
|
|
191
|
+
if (r2.error) return c.json({ error: r2.error }, 500)
|
|
192
|
+
return c.json({ ok: true })
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// POST /tasks
|
|
196
|
+
app.post('/tasks', async (c) => {
|
|
197
|
+
const body = await c.req.json<{
|
|
198
|
+
storyId: number; title: string; assignee?: string
|
|
199
|
+
description?: string; sortOrder?: number; storyPoints?: number
|
|
200
|
+
}>()
|
|
201
|
+
const assigneeErr = await validateAssignee(body.assignee)
|
|
202
|
+
if (assigneeErr) return c.json({ error: assigneeErr }, 400)
|
|
203
|
+
const { rowsAffected } = await executeOrThrow(
|
|
204
|
+
'INSERT INTO pm_tasks (story_id, title, assignee, description, sort_order, story_points, start_date, due_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
205
|
+
[body.storyId, body.title, body.assignee ?? null, body.description ?? null, body.sortOrder ?? 0, body.storyPoints ?? null, (body as any).startDate ?? null, (body as any).dueDate ?? null],
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
// Auto-sync story SP
|
|
209
|
+
if (body.storyPoints != null) {
|
|
210
|
+
await syncStorySP(body.storyId)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return c.json({ ok: true }, 201)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// PATCH /tasks/:id
|
|
217
|
+
app.patch('/tasks/:id', async (c) => {
|
|
218
|
+
const id = Number(c.req.param('id'))
|
|
219
|
+
const body = await c.req.json<Record<string, unknown>>()
|
|
220
|
+
|
|
221
|
+
if (body.assignee !== undefined) {
|
|
222
|
+
const assigneeErr = await validateAssignee(body.assignee as string | null)
|
|
223
|
+
if (assigneeErr) return c.json({ error: assigneeErr }, 400)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const fieldMap: Record<string, string> = {
|
|
227
|
+
title: 'title', assignee: 'assignee', status: 'status', description: 'description',
|
|
228
|
+
storyPoints: 'story_points', startDate: 'start_date', dueDate: 'due_date',
|
|
229
|
+
}
|
|
230
|
+
const sets: string[] = []
|
|
231
|
+
const args: (string | number | null)[] = []
|
|
232
|
+
|
|
233
|
+
for (const [key, col] of Object.entries(fieldMap)) {
|
|
234
|
+
if (body[key] !== undefined) {
|
|
235
|
+
sets.push(`${col} = ?`)
|
|
236
|
+
args.push(body[key] as string | number | null)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (sets.length === 0) return c.json({ ok: true })
|
|
240
|
+
|
|
241
|
+
sets.push('updated_at = CURRENT_TIMESTAMP')
|
|
242
|
+
args.push(id)
|
|
243
|
+
const { rowsAffected } = await executeOrThrow(`UPDATE pm_tasks SET ${sets.join(', ')} WHERE id = ?`, args)
|
|
244
|
+
|
|
245
|
+
// Auto-sync story SP on SP change
|
|
246
|
+
if (body.storyPoints !== undefined) {
|
|
247
|
+
const storyResult = await query<{ story_id: number }>('SELECT story_id FROM pm_tasks WHERE id = ?', [id])
|
|
248
|
+
if (!storyResult.error && storyResult.rows.length > 0) {
|
|
249
|
+
await syncStorySP(storyResult.rows[0].story_id)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return c.json({ ok: true })
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// DELETE /tasks/:id
|
|
257
|
+
app.delete('/tasks/:id', async (c) => {
|
|
258
|
+
const id = Number(c.req.param('id'))
|
|
259
|
+
const { rowsAffected } = await executeOrThrow('DELETE FROM pm_tasks WHERE id = ?', [id])
|
|
260
|
+
return c.json({ ok: true })
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// ── PR <-> Story link ──
|
|
264
|
+
|
|
265
|
+
type PrEntry = { prNumber: number; prUrl: string; prTitle: string; status: string }
|
|
266
|
+
|
|
267
|
+
async function linkPrToStory(
|
|
268
|
+
storyId: number,
|
|
269
|
+
pr: { prNumber: number; prUrl: string; prTitle: string; status?: string },
|
|
270
|
+
): Promise<{ ok: boolean; error?: string; relatedPrs: PrEntry[] }> {
|
|
271
|
+
const existing = await query('SELECT related_prs FROM pm_stories WHERE id = ?', [storyId])
|
|
272
|
+
if (!existing.rows?.length) return { ok: false, error: 'Story not found', relatedPrs: [] }
|
|
273
|
+
|
|
274
|
+
const current: PrEntry[] = JSON.parse((existing.rows[0] as any).related_prs || '[]')
|
|
275
|
+
const entry: PrEntry = { prNumber: pr.prNumber, prUrl: pr.prUrl, prTitle: pr.prTitle, status: pr.status || 'open' }
|
|
276
|
+
const idx = current.findIndex(p => p.prNumber === pr.prNumber)
|
|
277
|
+
if (idx >= 0) current[idx] = entry
|
|
278
|
+
else current.push(entry)
|
|
279
|
+
|
|
280
|
+
await execute('UPDATE pm_stories SET related_prs = ? WHERE id = ?', [JSON.stringify(current), storyId])
|
|
281
|
+
|
|
282
|
+
if (pr.status === 'merged') {
|
|
283
|
+
const storyResult = await query('SELECT status FROM pm_stories WHERE id = ?', [storyId])
|
|
284
|
+
const storyStatus = (storyResult.rows?.[0] as any)?.status
|
|
285
|
+
if (storyStatus === 'review') {
|
|
286
|
+
await execute("UPDATE pm_stories SET status = 'qa' WHERE id = ?", [storyId])
|
|
287
|
+
}
|
|
288
|
+
await execute(
|
|
289
|
+
"UPDATE memos_v2 SET status = 'resolved', resolved_by = 'system', resolved_at = CURRENT_TIMESTAMP WHERE status = 'open' AND content LIKE ?",
|
|
290
|
+
[`%SID:${storyId}%`],
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Activity log
|
|
295
|
+
if (pr.status === 'merged') {
|
|
296
|
+
const { logActivity } = await import('../utils/activity.js')
|
|
297
|
+
await logActivity('system', 'pr_merged', 'story', storyId, pr.prTitle)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { ok: true, relatedPrs: current }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// POST /stories/:id/link-pr
|
|
304
|
+
app.post('/stories/:id/link-pr', async (c) => {
|
|
305
|
+
const storyId = Number(c.req.param('id'))
|
|
306
|
+
const body = await c.req.json<{ prNumber: number; prUrl: string; prTitle: string; status?: string }>()
|
|
307
|
+
const result = await linkPrToStory(storyId, body)
|
|
308
|
+
if (!result.ok) return c.json({ error: result.error }, 404)
|
|
309
|
+
return c.json({ ok: true, relatedPrs: result.relatedPrs })
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
// GET /stories/:id/prs
|
|
313
|
+
app.get('/stories/:id/prs', async (c) => {
|
|
314
|
+
const storyId = Number(c.req.param('id'))
|
|
315
|
+
const existing = await query('SELECT related_prs FROM pm_stories WHERE id = ?', [storyId])
|
|
316
|
+
if (!existing.rows?.length) return c.json({ error: 'Story not found' }, 404)
|
|
317
|
+
return c.json({ prs: JSON.parse((existing.rows[0] as any).related_prs || '[]') })
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
// POST /link-pr-by-title — auto-parse SID:xxx from PR title
|
|
321
|
+
app.post('/link-pr-by-title', async (c) => {
|
|
322
|
+
const body = await c.req.json<{ prNumber: number; prUrl: string; prTitle: string; status?: string }>()
|
|
323
|
+
const match = body.prTitle.match(/SID[:\s]*(\d+)/i)
|
|
324
|
+
if (!match) return c.json({ ok: false, message: 'No SID pattern found' })
|
|
325
|
+
const storyId = Number(match[1])
|
|
326
|
+
const result = await linkPrToStory(storyId, body)
|
|
327
|
+
if (!result.ok) return c.json({ ok: false, message: result.error })
|
|
328
|
+
return c.json({ ok: true, storyId, relatedPrs: result.relatedPrs })
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// ── Story Comments ──
|
|
332
|
+
|
|
333
|
+
// GET /stories/:id/comments
|
|
334
|
+
app.get('/stories/:id/comments', async (c) => {
|
|
335
|
+
const storyId = Number(c.req.param('id'))
|
|
336
|
+
const { rows } = await queryOrThrow(
|
|
337
|
+
'SELECT * FROM story_comments WHERE story_id = ? ORDER BY created_at ASC',
|
|
338
|
+
[storyId],
|
|
339
|
+
)
|
|
340
|
+
return c.json({ comments: rows })
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
// POST /stories/:id/comments
|
|
344
|
+
app.post('/stories/:id/comments', async (c) => {
|
|
345
|
+
const storyId = Number(c.req.param('id'))
|
|
346
|
+
const body = await c.req.json<{ content: string }>()
|
|
347
|
+
const createdBy = c.get('userName') || 'unknown'
|
|
348
|
+
|
|
349
|
+
await executeOrThrow(
|
|
350
|
+
'INSERT INTO story_comments (story_id, content, created_by) VALUES (?, ?, ?)',
|
|
351
|
+
[storyId, body.content, createdBy],
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
// @mention webhook
|
|
355
|
+
const { rows: members } = await queryOrThrow<{ display_name: string }>(
|
|
356
|
+
"SELECT display_name FROM members WHERE is_active = 1",
|
|
357
|
+
)
|
|
358
|
+
const { notifyByName } = await import('../utils/agent-notify.js')
|
|
359
|
+
for (const m of members) {
|
|
360
|
+
if (body.content.includes(`@${m.display_name}`) && m.display_name !== createdBy) {
|
|
361
|
+
const preview = body.content.length > 50 ? body.content.slice(0, 50) + '...' : body.content
|
|
362
|
+
await notifyByName(m.display_name, `Comment: ${createdBy}`, `SID:${storyId} comment\n${preview}`)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return c.json({ ok: true }, 201)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
// DELETE /stories/:id/comments/:commentId
|
|
370
|
+
app.delete('/stories/:id/comments/:commentId', async (c) => {
|
|
371
|
+
const commentId = Number(c.req.param('commentId'))
|
|
372
|
+
const userName = c.get('userName')
|
|
373
|
+
await executeOrThrow(
|
|
374
|
+
'DELETE FROM story_comments WHERE id = ? AND created_by = ?',
|
|
375
|
+
[commentId, userName],
|
|
376
|
+
)
|
|
377
|
+
return c.json({ ok: true })
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
export default app
|
|
@@ -0,0 +1,58 @@
|
|
|
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 /:sprint/:epicId
|
|
9
|
+
// Supports both legacy string keys (E-01) and numeric pm_epics.id (18)
|
|
10
|
+
app.get('/:sprint/:epicId', async (c) => {
|
|
11
|
+
const sprint = c.req.param('sprint')
|
|
12
|
+
const epicId = c.req.param('epicId')
|
|
13
|
+
|
|
14
|
+
// 1. Try exact match first (works for both "E-01" and numeric "18")
|
|
15
|
+
const { rows } = await queryOrThrow<{ content: string }>(
|
|
16
|
+
'SELECT content FROM policy_documents WHERE sprint = ? AND epic_id = ?',
|
|
17
|
+
[sprint, epicId],
|
|
18
|
+
)
|
|
19
|
+
if (rows.length > 0) return c.json({ content: rows[0].content })
|
|
20
|
+
|
|
21
|
+
// 2. If epicId is numeric, resolve legacy E-XX key via story title pattern
|
|
22
|
+
if (/^\d+$/.test(epicId)) {
|
|
23
|
+
const storyResult = await query<{ title: string }>(
|
|
24
|
+
`SELECT title FROM pm_stories WHERE epic_id = ? AND sprint = ? LIMIT 1`,
|
|
25
|
+
[Number(epicId), sprint],
|
|
26
|
+
)
|
|
27
|
+
if (!storyResult.error && storyResult.rows.length > 0) {
|
|
28
|
+
const match = storyResult.rows[0].title.match(/^\[(E-\d+)/)
|
|
29
|
+
if (match) {
|
|
30
|
+
const legacyId = match[1]
|
|
31
|
+
const fallback = await query<{ content: string }>(
|
|
32
|
+
'SELECT content FROM policy_documents WHERE sprint = ? AND epic_id = ?',
|
|
33
|
+
[sprint, legacyId],
|
|
34
|
+
)
|
|
35
|
+
if (!fallback.error && fallback.rows.length > 0) {
|
|
36
|
+
return c.json({ content: fallback.rows[0].content })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return c.json({ error: 'Not found' }, 404)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// PUT /:sprint/:epicId — upsert policy document
|
|
46
|
+
app.put('/:sprint/:epicId', async (c) => {
|
|
47
|
+
const sprint = c.req.param('sprint')
|
|
48
|
+
const epicId = c.req.param('epicId')
|
|
49
|
+
const body = await c.req.json<{ content: string }>().catch(() => ({} as { content: string }))
|
|
50
|
+
if (!body.content) return c.json({ error: 'Missing content' }, 400)
|
|
51
|
+
const { rowsAffected } = await executeOrThrow(
|
|
52
|
+
'INSERT OR REPLACE INTO policy_documents (sprint, epic_id, content, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
|
|
53
|
+
[sprint, epicId, body.content],
|
|
54
|
+
)
|
|
55
|
+
return c.json({ ok: true })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export default app
|
|
@@ -0,0 +1,221 @@
|
|
|
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 /session
|
|
9
|
+
app.get('/session', async (c) => {
|
|
10
|
+
const sprint = c.req.query('sprint')
|
|
11
|
+
if (!sprint) return c.json({ error: 'sprint query param required' }, 400)
|
|
12
|
+
|
|
13
|
+
const { rows } = await queryOrThrow(
|
|
14
|
+
'SELECT * FROM retro_sessions WHERE sprint = ? LIMIT 1',
|
|
15
|
+
[sprint],
|
|
16
|
+
)
|
|
17
|
+
return c.json({ session: rows[0] ?? null })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// POST /session
|
|
21
|
+
app.post('/session', async (c) => {
|
|
22
|
+
const body = await c.req.json<{ sprint: string; title: string }>()
|
|
23
|
+
const insertResult = await executeOrThrow(
|
|
24
|
+
'INSERT INTO retro_sessions (sprint, title) VALUES (?, ?)',
|
|
25
|
+
[body.sprint, body.title],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const { rows } = await queryOrThrow(
|
|
29
|
+
'SELECT * FROM retro_sessions WHERE sprint = ? LIMIT 1',
|
|
30
|
+
[body.sprint],
|
|
31
|
+
)
|
|
32
|
+
return c.json({ session: rows[0] ?? null })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// PATCH /session/:id/phase
|
|
36
|
+
app.patch('/session/:id/phase', async (c) => {
|
|
37
|
+
const id = Number(c.req.param('id'))
|
|
38
|
+
const body = await c.req.json<{ phase: string }>()
|
|
39
|
+
const { rowsAffected } = await executeOrThrow(
|
|
40
|
+
'UPDATE retro_sessions SET phase = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
41
|
+
[body.phase, id],
|
|
42
|
+
)
|
|
43
|
+
return c.json({ ok: true })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// GET /items
|
|
47
|
+
app.get('/items', async (c) => {
|
|
48
|
+
const sessionId = c.req.query('sessionId')
|
|
49
|
+
const user = c.req.query('user')
|
|
50
|
+
if (!sessionId) return c.json({ error: 'sessionId query param required' }, 400)
|
|
51
|
+
|
|
52
|
+
const { rows } = await queryOrThrow(
|
|
53
|
+
`SELECT i.*, COUNT(v.item_id) as vote_count,
|
|
54
|
+
CASE WHEN SUM(CASE WHEN v.voter = ? THEN 1 ELSE 0 END) > 0 THEN 1 ELSE 0 END as has_voted
|
|
55
|
+
FROM retro_items i
|
|
56
|
+
LEFT JOIN retro_votes v ON v.item_id = i.id
|
|
57
|
+
WHERE i.session_id = ?
|
|
58
|
+
GROUP BY i.id`,
|
|
59
|
+
[user ?? '', Number(sessionId)],
|
|
60
|
+
)
|
|
61
|
+
const mapped = rows.map((r: Record<string, unknown>) => ({
|
|
62
|
+
...r,
|
|
63
|
+
voteCount: Number(r.vote_count ?? 0),
|
|
64
|
+
hasVoted: Number(r.has_voted ?? 0),
|
|
65
|
+
}))
|
|
66
|
+
return c.json({ items: mapped })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// POST /items
|
|
70
|
+
app.post('/items', async (c) => {
|
|
71
|
+
const body = await c.req.json<{
|
|
72
|
+
sessionId: number; category: string; content: string; author: string
|
|
73
|
+
}>()
|
|
74
|
+
const { rowsAffected } = await executeOrThrow(
|
|
75
|
+
'INSERT INTO retro_items (session_id, category, content, author) VALUES (?, ?, ?, ?)',
|
|
76
|
+
[body.sessionId, body.category, body.content, body.author],
|
|
77
|
+
)
|
|
78
|
+
return c.json({ ok: true })
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// DELETE /items/:id
|
|
82
|
+
app.delete('/items/:id', async (c) => {
|
|
83
|
+
const id = Number(c.req.param('id'))
|
|
84
|
+
const r1 = await executeOrThrow('DELETE FROM retro_votes WHERE item_id = ?', [id])
|
|
85
|
+
const r2 = await executeOrThrow('DELETE FROM retro_items WHERE id = ?', [id])
|
|
86
|
+
return c.json({ ok: true })
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// POST /items/:id/vote
|
|
90
|
+
app.post('/items/:id/vote', async (c) => {
|
|
91
|
+
const id = Number(c.req.param('id'))
|
|
92
|
+
const body = await c.req.json<{ voter: string; hasVoted: boolean }>()
|
|
93
|
+
|
|
94
|
+
if (body.hasVoted) {
|
|
95
|
+
const { rowsAffected } = await executeOrThrow(
|
|
96
|
+
'DELETE FROM retro_votes WHERE item_id = ? AND voter = ?',
|
|
97
|
+
[id, body.voter],
|
|
98
|
+
)
|
|
99
|
+
} else {
|
|
100
|
+
const { rowsAffected } = await executeOrThrow(
|
|
101
|
+
'INSERT OR IGNORE INTO retro_votes (item_id, voter) VALUES (?, ?)',
|
|
102
|
+
[id, body.voter],
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
return c.json({ ok: true })
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// GET /actions
|
|
109
|
+
app.get('/actions', async (c) => {
|
|
110
|
+
const sessionId = c.req.query('sessionId')
|
|
111
|
+
if (!sessionId) return c.json({ error: 'sessionId query param required' }, 400)
|
|
112
|
+
|
|
113
|
+
const { rows } = await queryOrThrow(
|
|
114
|
+
'SELECT * FROM retro_actions WHERE session_id = ? ORDER BY created_at ASC',
|
|
115
|
+
[Number(sessionId)],
|
|
116
|
+
)
|
|
117
|
+
return c.json({ actions: rows })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// POST /actions
|
|
121
|
+
app.post('/actions', async (c) => {
|
|
122
|
+
const body = await c.req.json<{
|
|
123
|
+
sessionId: number; content: string; assignee?: string
|
|
124
|
+
}>()
|
|
125
|
+
const { rowsAffected } = await executeOrThrow(
|
|
126
|
+
'INSERT INTO retro_actions (session_id, content, assignee) VALUES (?, ?, ?)',
|
|
127
|
+
[body.sessionId, body.content, body.assignee ?? null],
|
|
128
|
+
)
|
|
129
|
+
return c.json({ ok: true })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// PATCH /actions/:id/status
|
|
133
|
+
app.patch('/actions/:id/status', async (c) => {
|
|
134
|
+
const id = Number(c.req.param('id'))
|
|
135
|
+
const body = await c.req.json<{ status: string }>()
|
|
136
|
+
const { rowsAffected } = await executeOrThrow(
|
|
137
|
+
'UPDATE retro_actions SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
138
|
+
[body.status, id],
|
|
139
|
+
)
|
|
140
|
+
return c.json({ ok: true })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// DELETE /session/:id — cascade delete
|
|
144
|
+
app.delete('/session/:id', async (c) => {
|
|
145
|
+
const id = Number(c.req.param('id'))
|
|
146
|
+
|
|
147
|
+
// Delete votes via subquery
|
|
148
|
+
const r1 = await executeOrThrow(
|
|
149
|
+
'DELETE FROM retro_votes WHERE item_id IN (SELECT id FROM retro_items WHERE session_id = ?)',
|
|
150
|
+
[id],
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const r2 = await executeOrThrow('DELETE FROM retro_actions WHERE session_id = ?', [id])
|
|
154
|
+
|
|
155
|
+
const r3 = await executeOrThrow('DELETE FROM retro_items WHERE session_id = ?', [id])
|
|
156
|
+
|
|
157
|
+
const r4 = await executeOrThrow('DELETE FROM retro_sessions WHERE id = ?', [id])
|
|
158
|
+
|
|
159
|
+
return c.json({ ok: true })
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// POST /:sprint/complete — retro complete + action items -> backlog stories
|
|
163
|
+
app.post('/:sprint/complete', async (c) => {
|
|
164
|
+
const sprintId = c.req.param('sprint')
|
|
165
|
+
|
|
166
|
+
// 1. Check retro session
|
|
167
|
+
const { rows } = await queryOrThrow(
|
|
168
|
+
'SELECT id, phase FROM retro_sessions WHERE sprint = ? LIMIT 1',
|
|
169
|
+
[sprintId],
|
|
170
|
+
)
|
|
171
|
+
if (!rows.length) return c.json({ error: 'Retro session not found' }, 404)
|
|
172
|
+
const sessionId = (rows[0] as { id: number }).id
|
|
173
|
+
|
|
174
|
+
// 2. Get action items
|
|
175
|
+
const { rows: actionRows } = await queryOrThrow<{ id: number; content: string; assignee: string | null; status: string }>(
|
|
176
|
+
'SELECT id, content, assignee, status FROM retro_actions WHERE session_id = ?',
|
|
177
|
+
[sessionId],
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
// 3. Incomplete actions -> backlog stories
|
|
181
|
+
const { actionToStory, getPendingActions } = await import('../utils/retro-link.js')
|
|
182
|
+
const pending = getPendingActions(actionRows)
|
|
183
|
+
const created: Array<{ actionId: number; title: string }> = []
|
|
184
|
+
|
|
185
|
+
for (const action of pending) {
|
|
186
|
+
const story = actionToStory(action, sprintId)
|
|
187
|
+
const r = await executeOrThrow(
|
|
188
|
+
'INSERT INTO pm_stories (title, description, assignee, status, sprint, priority) VALUES (?, ?, ?, ?, ?, ?)',
|
|
189
|
+
[story.title, story.description, story.assignee, 'backlog', null, 'medium'],
|
|
190
|
+
)
|
|
191
|
+
created.push({ actionId: action.id, title: story.title })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 4. Set retro phase -> done
|
|
195
|
+
await executeOrThrow(
|
|
196
|
+
"UPDATE retro_sessions SET phase = 'done', updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
|
197
|
+
[sessionId],
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return c.json({
|
|
201
|
+
ok: true,
|
|
202
|
+
sprintId,
|
|
203
|
+
actionsProcessed: pending.length,
|
|
204
|
+
storiesCreated: created,
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// GET /:sprint/actions — retro action items by sprint
|
|
209
|
+
app.get('/:sprint/actions', async (c) => {
|
|
210
|
+
const sprint = c.req.param('sprint')
|
|
211
|
+
const { rows: sessions } = await queryOrThrow('SELECT id FROM retro_sessions WHERE sprint = ? LIMIT 1', [sprint])
|
|
212
|
+
if (!sessions.length) return c.json({ actions: [] })
|
|
213
|
+
const sessionId = (sessions[0] as any).id
|
|
214
|
+
const { rows } = await queryOrThrow(
|
|
215
|
+
'SELECT * FROM retro_actions WHERE session_id = ? ORDER BY created_at ASC',
|
|
216
|
+
[sessionId],
|
|
217
|
+
)
|
|
218
|
+
return c.json({ actions: rows })
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
export default app
|