popilot 0.3.0 → 0.5.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 (81) hide show
  1. package/lib/hydrate.mjs +6 -1
  2. package/lib/setup-wizard.mjs +29 -3
  3. package/package.json +1 -1
  4. package/scaffold/.claude/commands/_domain.md.hbs +33 -0
  5. package/scaffold/.claude/commands/analytics.md.hbs +55 -0
  6. package/scaffold/.claude/commands/daily.md.hbs +301 -0
  7. package/scaffold/.claude/commands/dev.md.hbs +62 -0
  8. package/scaffold/.claude/commands/gtm.md +82 -0
  9. package/scaffold/.claude/commands/handoff.md +259 -0
  10. package/scaffold/.claude/commands/market.md +120 -0
  11. package/scaffold/.claude/commands/metrics.md +123 -0
  12. package/scaffold/.claude/commands/oscar-loop.md +436 -0
  13. package/scaffold/.claude/commands/party.md +85 -0
  14. package/scaffold/.claude/commands/plan.md +43 -0
  15. package/scaffold/.claude/commands/poc.md +69 -0
  16. package/scaffold/.claude/commands/research.md +203 -0
  17. package/scaffold/.claude/commands/retro.md +68 -0
  18. package/scaffold/.claude/commands/save.md +440 -0
  19. package/scaffold/.claude/commands/sessions.md +139 -0
  20. package/scaffold/.claude/commands/sprint.md +106 -0
  21. package/scaffold/.claude/commands/start.md +396 -0
  22. package/scaffold/.claude/commands/strategy.md +41 -0
  23. package/scaffold/.claude/commands/task.md +220 -0
  24. package/scaffold/.claude/commands/tracking.md +116 -0
  25. package/scaffold/.claude/commands/validate.md +58 -0
  26. package/scaffold/.context/WORKFLOW.md.hbs +58 -26
  27. package/scaffold/.context/agents/planner.md.hbs +35 -7
  28. package/scaffold/.context/integrations/_registry.yaml +6 -0
  29. package/scaffold/.context/integrations/providers/sqlite_lambda.yaml +24 -0
  30. package/scaffold/.context/integrations/providers/supabase.yaml +34 -0
  31. package/scaffold/.context/integrations/providers/turso_cf.yaml +34 -0
  32. package/scaffold/.context/poc/_skills/build.md +79 -0
  33. package/scaffold/.context/poc/_skills/scope.md +50 -0
  34. package/scaffold/.context/poc/_skills/spec.md +80 -0
  35. package/scaffold/.context/poc/_skills/verify.md +60 -0
  36. package/scaffold/CLAUDE.md.hbs +210 -0
  37. package/scaffold/spec-site/.env.example +11 -0
  38. package/scaffold/spec-site/index.html +2 -2
  39. package/scaffold/spec-site/sql/schema.sql +224 -0
  40. package/scaffold/spec-site/src/api/client.ts +131 -0
  41. package/scaffold/spec-site/src/api/types.ts +177 -0
  42. package/scaffold/spec-site/src/components/Accordion.vue +1 -1
  43. package/scaffold/spec-site/src/components/AppHeader.vue +5 -4
  44. package/scaffold/spec-site/src/components/CoachingCard.vue +1 -1
  45. package/scaffold/spec-site/src/components/ScenarioSwitcher.vue +1 -1
  46. package/scaffold/spec-site/src/composables/navTypes.ts +39 -0
  47. package/scaffold/spec-site/src/composables/pmTypes.ts +134 -0
  48. package/scaffold/spec-site/src/composables/useAuth.ts +139 -0
  49. package/scaffold/spec-site/src/composables/useMemo.ts +51 -40
  50. package/scaffold/spec-site/src/composables/useNavStore.ts +202 -0
  51. package/scaffold/spec-site/src/composables/usePageContent.ts +208 -0
  52. package/scaffold/spec-site/src/composables/usePmStore.ts +224 -0
  53. package/scaffold/spec-site/src/composables/useRetro.ts +181 -95
  54. package/scaffold/spec-site/src/composables/useScenarioStore.ts +74 -30
  55. package/scaffold/spec-site/src/composables/useUser.ts +12 -6
  56. package/scaffold/spec-site/src/data/navigation.ts +7 -42
  57. package/scaffold/spec-site/src/data/types.ts +13 -43
  58. package/scaffold/spec-site/src/main.ts +7 -0
  59. package/scaffold/spec-site/src/pages/PolicyDetail.vue +30 -11
  60. package/scaffold/spec-site/src/pages/PolicyIndex.vue +22 -7
  61. package/scaffold/spec-site/src/pages/retro/RetroActions.vue +3 -3
  62. package/scaffold/spec-site/src/pages/retro/RetroBoard.vue +2 -2
  63. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +5 -7
  64. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +2 -2
  65. package/scaffold/spec-site/src/pages/shared/NoContentPlaceholder.vue +2 -2
  66. package/scaffold/spec-site/src/pages/shared/PolicyFallback.vue +25 -13
  67. package/scaffold/spec-site/src/router.ts +11 -7
  68. package/scaffold/spec-site/src/styles/base.css +2 -2
  69. package/scaffold/spec-site/src/styles/split-pane.css +1 -1
  70. package/scaffold/spec-site/src/styles/variables.css +7 -7
  71. package/scaffold/spec-site/src/assets/icons/menu/ic_ads.svg +0 -10
  72. package/scaffold/spec-site/src/assets/icons/menu/ic_ads_on.svg +0 -10
  73. package/scaffold/spec-site/src/assets/icons/menu/ic_board.svg +0 -14
  74. package/scaffold/spec-site/src/assets/icons/menu/ic_board_on.svg +0 -14
  75. package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard.svg +0 -21
  76. package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard_on.svg +0 -21
  77. package/scaffold/spec-site/src/assets/icons/menu/ic_pricing.svg +0 -20
  78. package/scaffold/spec-site/src/assets/icons/menu/ic_pricing_on.svg +0 -20
  79. package/scaffold/spec-site/src/assets/icons/menu/ic_store.svg +0 -11
  80. package/scaffold/spec-site/src/assets/icons/menu/ic_store_on.svg +0 -11
  81. package/scaffold/spec-site/src/composables/useTurso.ts +0 -160
@@ -1,5 +1,12 @@
1
+ /**
2
+ * Retro composable — Sprint retrospective board
3
+ *
4
+ * In API mode: full CRUD via REST endpoints
5
+ * In static mode: localStorage-only (offline retro)
6
+ */
7
+
1
8
  import { ref, computed, onUnmounted } from 'vue'
2
- import { query, execute } from './useTurso'
9
+ import { apiGet, apiPost, apiPatch, apiDelete, isStaticMode } from '@/api/client'
3
10
 
4
11
  export type RetroPhase = 'write' | 'vote' | 'discuss' | 'done'
5
12
  export type RetroCategory = 'keep' | 'problem' | 'try'
@@ -36,6 +43,27 @@ export interface RetroAction {
36
43
  export const VOTES_PER_PERSON = 5
37
44
  const POLL_INTERVAL_MS = 4000
38
45
 
46
+ // ── localStorage fallback for static mode ──
47
+
48
+ function localKey(sprint: string) { return `retro_${sprint}` }
49
+
50
+ interface LocalRetroData {
51
+ session: RetroSession
52
+ items: RetroItem[]
53
+ actions: RetroAction[]
54
+ }
55
+
56
+ function loadLocal(sprint: string): LocalRetroData | null {
57
+ try {
58
+ const raw = localStorage.getItem(localKey(sprint))
59
+ return raw ? JSON.parse(raw) : null
60
+ } catch { return null }
61
+ }
62
+
63
+ function saveLocal(sprint: string, data: LocalRetroData) {
64
+ localStorage.setItem(localKey(sprint), JSON.stringify(data))
65
+ }
66
+
39
67
  export function useRetro(sprintId: string) {
40
68
  const session = ref<RetroSession | null>(null)
41
69
  const items = ref<RetroItem[]>([])
@@ -53,33 +81,45 @@ export function useRetro(sprintId: string) {
53
81
  loading.value = true
54
82
  error.value = null
55
83
 
56
- const r = await query<RetroSession>(
57
- 'SELECT * FROM retro_sessions WHERE sprint = ? LIMIT 1',
58
- [sprintId],
59
- )
60
- if (r.error) {
61
- error.value = r.error
84
+ if (isStaticMode()) {
85
+ const local = loadLocal(sprintId)
86
+ if (local) {
87
+ session.value = local.session
88
+ items.value = local.items
89
+ actions.value = local.actions
90
+ } else {
91
+ const now = new Date().toISOString()
92
+ session.value = {
93
+ id: Date.now(),
94
+ sprint: sprintId,
95
+ title: `${sprintId.toUpperCase()} Retro`,
96
+ phase: 'write',
97
+ created_at: now,
98
+ updated_at: now,
99
+ }
100
+ items.value = []
101
+ actions.value = []
102
+ saveLocal(sprintId, { session: session.value, items: [], actions: [] })
103
+ }
62
104
  loading.value = false
63
105
  return
64
106
  }
65
107
 
66
- if (r.rows.length > 0) {
67
- session.value = r.rows[0]
68
- } else {
69
- const ins = await execute(
70
- 'INSERT INTO retro_sessions (sprint, title) VALUES (?, ?)',
71
- [sprintId, `${sprintId.toUpperCase()} Retro`],
72
- )
73
- if (ins.error) {
74
- error.value = ins.error
108
+ const { data, error: apiError } = await apiGet<{ session: RetroSession }>(`/api/v2/retro/session`, { sprint: sprintId })
109
+ if (apiError || !data) {
110
+ // Try to create session
111
+ const { data: created, error: createError } = await apiPost<{ session: RetroSession }>('/api/v2/retro/session', {
112
+ sprint: sprintId,
113
+ title: `${sprintId.toUpperCase()} Retro`,
114
+ })
115
+ if (createError || !created) {
116
+ error.value = createError ?? 'Failed to create session'
75
117
  loading.value = false
76
118
  return
77
119
  }
78
- const r2 = await query<RetroSession>(
79
- 'SELECT * FROM retro_sessions WHERE sprint = ? LIMIT 1',
80
- [sprintId],
81
- )
82
- session.value = r2.rows[0] ?? null
120
+ session.value = created.session
121
+ } else {
122
+ session.value = data.session
83
123
  }
84
124
 
85
125
  await refresh()
@@ -88,55 +128,68 @@ export function useRetro(sprintId: string) {
88
128
 
89
129
  async function setPhase(phase: RetroPhase) {
90
130
  if (!session.value) return
91
- await execute(
92
- "UPDATE retro_sessions SET phase = ?, updated_at = datetime('now') WHERE id = ?",
93
- [phase, session.value.id],
94
- )
131
+
132
+ if (isStaticMode()) {
133
+ session.value = { ...session.value, phase }
134
+ saveLocal(sprintId, { session: session.value, items: items.value, actions: actions.value })
135
+ return
136
+ }
137
+
138
+ await apiPatch(`/api/v2/retro/session/${session.value.id}/phase`, { phase })
95
139
  session.value = { ...session.value, phase }
96
140
  }
97
141
 
98
142
  // -- Items --
99
143
  async function loadItems(currentUser: string) {
100
144
  if (!session.value) return
101
- const r = await query<Record<string, unknown>>(
102
- `SELECT i.*,
103
- COUNT(v.voter) as voteCount,
104
- MAX(CASE WHEN v.voter = ? THEN 1 ELSE 0 END) as hasVoted
105
- FROM retro_items i
106
- LEFT JOIN retro_votes v ON v.item_id = i.id
107
- WHERE i.session_id = ?
108
- GROUP BY i.id
109
- ORDER BY i.created_at ASC`,
110
- [currentUser, session.value.id],
111
- )
112
- if (!r.error) {
113
- items.value = r.rows.map((row) => ({
114
- id: Number(row.id),
115
- session_id: Number(row.session_id),
116
- category: row.category as RetroCategory,
117
- content: row.content as string,
118
- author: row.author as string,
119
- created_at: row.created_at as string,
120
- voteCount: Number(row.voteCount),
121
- hasVoted: Boolean(Number(row.hasVoted)),
122
- }))
123
- }
145
+
146
+ if (isStaticMode()) return // items already in memory
147
+
148
+ const { data } = await apiGet<{ items: RetroItem[] }>('/api/v2/retro/items', {
149
+ sessionId: String(session.value.id),
150
+ voter: currentUser,
151
+ })
152
+ if (data) items.value = data.items
124
153
  }
125
154
 
126
155
  async function addItem(category: RetroCategory, content: string, author: string) {
127
156
  if (!session.value) return
128
157
  const trimmed = content.trim()
129
158
  if (!trimmed) return
130
- await execute(
131
- 'INSERT INTO retro_items (session_id, category, content, author) VALUES (?, ?, ?, ?)',
132
- [session.value.id, category, trimmed, author],
133
- )
159
+
160
+ if (isStaticMode()) {
161
+ const newItem: RetroItem = {
162
+ id: Date.now(),
163
+ session_id: session.value.id,
164
+ category,
165
+ content: trimmed,
166
+ author,
167
+ created_at: new Date().toISOString(),
168
+ voteCount: 0,
169
+ hasVoted: false,
170
+ }
171
+ items.value.push(newItem)
172
+ saveLocal(sprintId, { session: session.value, items: items.value, actions: actions.value })
173
+ return
174
+ }
175
+
176
+ await apiPost('/api/v2/retro/items', {
177
+ sessionId: session.value.id,
178
+ category,
179
+ content: trimmed,
180
+ author,
181
+ })
134
182
  await loadItems(author)
135
183
  }
136
184
 
137
185
  async function deleteItem(itemId: number, currentUser: string) {
138
- await execute('DELETE FROM retro_votes WHERE item_id = ?', [itemId])
139
- await execute('DELETE FROM retro_items WHERE id = ?', [itemId])
186
+ if (isStaticMode()) {
187
+ items.value = items.value.filter(i => i.id !== itemId)
188
+ saveLocal(sprintId, { session: session.value!, items: items.value, actions: actions.value })
189
+ return
190
+ }
191
+
192
+ await apiDelete(`/api/v2/retro/items/${itemId}`)
140
193
  await loadItems(currentUser)
141
194
  }
142
195
 
@@ -146,56 +199,87 @@ export function useRetro(sprintId: string) {
146
199
 
147
200
  async function toggleVote(itemId: number, currentUser: string, hasVoted: boolean) {
148
201
  if (!hasVoted && votesRemaining.value <= 0) return
149
- if (hasVoted) {
150
- await execute('DELETE FROM retro_votes WHERE item_id = ? AND voter = ?', [
151
- itemId,
152
- currentUser,
153
- ])
154
- } else {
155
- await execute('INSERT OR IGNORE INTO retro_votes (item_id, voter) VALUES (?, ?)', [
156
- itemId,
157
- currentUser,
158
- ])
202
+
203
+ if (isStaticMode()) {
204
+ items.value = items.value.map(i => {
205
+ if (i.id !== itemId) return i
206
+ return {
207
+ ...i,
208
+ hasVoted: !hasVoted,
209
+ voteCount: hasVoted ? i.voteCount - 1 : i.voteCount + 1,
210
+ }
211
+ })
212
+ saveLocal(sprintId, { session: session.value!, items: items.value, actions: actions.value })
213
+ return
159
214
  }
215
+
216
+ await apiPost(`/api/v2/retro/items/${itemId}/vote`, { voter: currentUser, remove: hasVoted })
160
217
  await loadItems(currentUser)
161
218
  }
162
219
 
163
220
  // -- Actions --
164
221
  async function loadActions() {
165
- if (!session.value) return
166
- const r = await query<RetroAction>(
167
- 'SELECT * FROM retro_actions WHERE session_id = ? ORDER BY created_at ASC',
168
- [session.value.id],
169
- )
170
- if (!r.error) actions.value = r.rows
222
+ if (!session.value || isStaticMode()) return
223
+
224
+ const { data } = await apiGet<{ actions: RetroAction[] }>('/api/v2/retro/actions', {
225
+ sessionId: String(session.value.id),
226
+ })
227
+ if (data) actions.value = data.actions
171
228
  }
172
229
 
173
230
  async function addAction(content: string, assignee: string | null) {
174
231
  if (!session.value) return
175
- await execute(
176
- 'INSERT INTO retro_actions (session_id, content, assignee) VALUES (?, ?, ?)',
177
- [session.value.id, content.trim(), assignee],
178
- )
232
+
233
+ if (isStaticMode()) {
234
+ actions.value.push({
235
+ id: Date.now(),
236
+ session_id: session.value.id,
237
+ content: content.trim(),
238
+ assignee,
239
+ status: 'pending',
240
+ created_at: new Date().toISOString(),
241
+ })
242
+ saveLocal(sprintId, { session: session.value, items: items.value, actions: actions.value })
243
+ return
244
+ }
245
+
246
+ await apiPost('/api/v2/retro/actions', {
247
+ sessionId: session.value.id,
248
+ content: content.trim(),
249
+ assignee,
250
+ })
179
251
  await loadActions()
180
252
  }
181
253
 
182
254
  async function toggleActionStatus(actionId: number, currentStatus: 'pending' | 'done') {
183
255
  const next = currentStatus === 'pending' ? 'done' : 'pending'
184
- await execute('UPDATE retro_actions SET status = ? WHERE id = ?', [next, actionId])
256
+
257
+ if (isStaticMode()) {
258
+ actions.value = actions.value.map(a =>
259
+ a.id === actionId ? { ...a, status: next } : a
260
+ )
261
+ saveLocal(sprintId, { session: session.value!, items: items.value, actions: actions.value })
262
+ return
263
+ }
264
+
265
+ await apiPatch(`/api/v2/retro/actions/${actionId}/status`, { status: next })
185
266
  await loadActions()
186
267
  }
187
268
 
188
269
  // -- Reset --
189
270
  async function resetSession() {
190
271
  if (!session.value) return
191
- const sid = session.value.id
192
- await execute(
193
- 'DELETE FROM retro_votes WHERE item_id IN (SELECT id FROM retro_items WHERE session_id = ?)',
194
- [sid],
195
- )
196
- await execute('DELETE FROM retro_actions WHERE session_id = ?', [sid])
197
- await execute('DELETE FROM retro_items WHERE session_id = ?', [sid])
198
- await execute('DELETE FROM retro_sessions WHERE id = ?', [sid])
272
+
273
+ if (isStaticMode()) {
274
+ localStorage.removeItem(localKey(sprintId))
275
+ session.value = null
276
+ items.value = []
277
+ actions.value = []
278
+ await loadOrCreateSession()
279
+ return
280
+ }
281
+
282
+ await apiDelete(`/api/v2/retro/session/${session.value.id}`)
199
283
  session.value = null
200
284
  items.value = []
201
285
  actions.value = []
@@ -228,7 +312,7 @@ export function useRetro(sprintId: string) {
228
312
  lines.push('- (none)')
229
313
  } else {
230
314
  for (const item of catItems) {
231
- const votes = item.voteCount > 0 ? ` (👍 ${item.voteCount})` : ''
315
+ const votes = item.voteCount > 0 ? ` (${item.voteCount} votes)` : ''
232
316
  lines.push(`- ${item.content}${votes} — _${item.author}_`)
233
317
  }
234
318
  }
@@ -236,10 +320,10 @@ export function useRetro(sprintId: string) {
236
320
  }
237
321
 
238
322
  if (actions.value.length > 0) {
239
- lines.push('## 📋 Action Items')
323
+ lines.push('## Action Items')
240
324
  lines.push('')
241
325
  for (const a of actions.value) {
242
- const check = a.status === 'done' ? '' : ''
326
+ const check = a.status === 'done' ? '[x]' : '[ ]'
243
327
  const assignee = a.assignee ? ` @${a.assignee}` : ''
244
328
  lines.push(`- ${check} ${a.content}${assignee}`)
245
329
  }
@@ -252,17 +336,17 @@ export function useRetro(sprintId: string) {
252
336
  return lines.join('\n')
253
337
  }
254
338
 
255
- // -- Refresh (items + actions + session phase) --
339
+ // -- Refresh --
256
340
  let _currentUser = ''
257
341
 
258
342
  async function refresh() {
259
- if (!session.value) return
260
- const r = await query<{ phase: string }>(
261
- 'SELECT phase FROM retro_sessions WHERE id = ?',
262
- [session.value.id],
263
- )
264
- if (r.rows[0] && r.rows[0].phase !== session.value.phase) {
265
- session.value = { ...session.value, phase: r.rows[0].phase as RetroPhase }
343
+ if (!session.value || isStaticMode()) return
344
+
345
+ const { data } = await apiGet<{ session: { phase: string } }>(`/api/v2/retro/session`, {
346
+ sprint: sprintId,
347
+ })
348
+ if (data?.session && data.session.phase !== session.value.phase) {
349
+ session.value = { ...session.value, phase: data.session.phase as RetroPhase }
266
350
  }
267
351
  await loadItems(_currentUser)
268
352
  await loadActions()
@@ -274,7 +358,9 @@ export function useRetro(sprintId: string) {
274
358
  function startPolling(currentUser: string) {
275
359
  _currentUser = currentUser
276
360
  stopPolling()
277
- pollTimer = setInterval(refresh, POLL_INTERVAL_MS)
361
+ if (!isStaticMode()) {
362
+ pollTimer = setInterval(refresh, POLL_INTERVAL_MS)
363
+ }
278
364
  }
279
365
 
280
366
  function stopPolling() {
@@ -1,7 +1,26 @@
1
+ /**
2
+ * Scenario store — CRUD for custom scenarios (API-backed or localStorage)
3
+ */
4
+
1
5
  import { ref } from 'vue'
2
- import { query, execute } from './useTurso'
6
+ import { apiGet, apiPost, apiDelete, isStaticMode } from '@/api/client'
3
7
  import type { Scenario } from '@/data/types'
4
8
 
9
+ function localKey(pageId: string, sprint: string) {
10
+ return `scenarios_${pageId}_${sprint}`
11
+ }
12
+
13
+ function loadLocal(pageId: string, sprint: string): Scenario<any>[] {
14
+ try {
15
+ const raw = localStorage.getItem(localKey(pageId, sprint))
16
+ return raw ? JSON.parse(raw) : []
17
+ } catch { return [] }
18
+ }
19
+
20
+ function saveLocal(pageId: string, sprint: string, list: Scenario<any>[]) {
21
+ localStorage.setItem(localKey(pageId, sprint), JSON.stringify(list))
22
+ }
23
+
5
24
  export function useScenarioStore(pageId: string, sprint: string) {
6
25
  const customScenarios = ref<Scenario<any>[]>([])
7
26
  const loading = ref(false)
@@ -10,21 +29,26 @@ export function useScenarioStore(pageId: string, sprint: string) {
10
29
  async function loadCustomScenarios(): Promise<Scenario<any>[]> {
11
30
  loading.value = true
12
31
  error.value = null
13
- const r = await query<{
14
- scenario_id: string
15
- label: string
16
- data_json: string
17
- }>(
18
- 'SELECT scenario_id, label, data_json FROM scenario_data WHERE page_id = ? AND sprint = ? ORDER BY created_at ASC',
19
- [pageId, sprint],
20
- )
32
+
33
+ if (isStaticMode()) {
34
+ customScenarios.value = loadLocal(pageId, sprint)
35
+ loading.value = false
36
+ return customScenarios.value
37
+ }
38
+
39
+ const { data, error: apiError } = await apiGet<{
40
+ scenarios: Array<{ scenario_id: string; label: string; data_json: string }>
41
+ }>(`/api/v2/scenarios`, { pageId, sprint })
42
+
21
43
  loading.value = false
22
- if (r.error) {
23
- error.value = r.error
24
- customScenarios.value = []
25
- return []
44
+
45
+ if (apiError || !data) {
46
+ error.value = apiError ?? 'Unknown error'
47
+ customScenarios.value = loadLocal(pageId, sprint) // fallback
48
+ return customScenarios.value
26
49
  }
27
- const list = r.rows
50
+
51
+ const list = data.scenarios
28
52
  .map((row) => {
29
53
  try {
30
54
  return {
@@ -37,22 +61,35 @@ export function useScenarioStore(pageId: string, sprint: string) {
37
61
  }
38
62
  })
39
63
  .filter((s): s is Scenario<any> => s !== null)
64
+
40
65
  customScenarios.value = list
41
66
  return list
42
67
  }
43
68
 
44
69
  async function saveScenario(scenario: Scenario<any>, author: string): Promise<boolean> {
45
70
  error.value = null
46
- const dataJson = JSON.stringify(scenario.data)
47
- const res = await execute(
48
- `INSERT INTO scenario_data (page_id, sprint, scenario_id, label, data_json, author)
49
- VALUES (?, ?, ?, ?, ?, ?)
50
- ON CONFLICT(page_id, sprint, scenario_id)
51
- DO UPDATE SET label = ?, data_json = ?, author = ?, updated_at = datetime('now')`,
52
- [pageId, sprint, scenario.id, scenario.label, dataJson, author, scenario.label, dataJson, author],
53
- )
54
- if (res.error) {
55
- error.value = res.error
71
+
72
+ if (isStaticMode()) {
73
+ const existing = customScenarios.value.findIndex(s => s.id === scenario.id)
74
+ if (existing >= 0) {
75
+ customScenarios.value[existing] = scenario
76
+ } else {
77
+ customScenarios.value.push(scenario)
78
+ }
79
+ saveLocal(pageId, sprint, customScenarios.value)
80
+ return true
81
+ }
82
+
83
+ const { error: apiError } = await apiPost('/api/v2/scenarios', {
84
+ pageId, sprint,
85
+ scenarioId: scenario.id,
86
+ label: scenario.label,
87
+ dataJson: JSON.stringify(scenario.data),
88
+ author,
89
+ })
90
+
91
+ if (apiError) {
92
+ error.value = apiError
56
93
  return false
57
94
  }
58
95
  await loadCustomScenarios()
@@ -61,12 +98,19 @@ export function useScenarioStore(pageId: string, sprint: string) {
61
98
 
62
99
  async function deleteScenario(scenarioId: string): Promise<boolean> {
63
100
  error.value = null
64
- const res = await execute(
65
- 'DELETE FROM scenario_data WHERE page_id = ? AND sprint = ? AND scenario_id = ?',
66
- [pageId, sprint, scenarioId],
67
- )
68
- if (res.error) {
69
- error.value = res.error
101
+
102
+ if (isStaticMode()) {
103
+ customScenarios.value = customScenarios.value.filter(s => s.id !== scenarioId)
104
+ saveLocal(pageId, sprint, customScenarios.value)
105
+ return true
106
+ }
107
+
108
+ const { error: apiError } = await apiDelete(`/api/v2/scenarios/${scenarioId}`, {
109
+ pageId, sprint,
110
+ })
111
+
112
+ if (apiError) {
113
+ error.value = apiError
70
114
  return false
71
115
  }
72
116
  await loadCustomScenarios()
@@ -1,17 +1,23 @@
1
+ /**
2
+ * User composable — Team member selection
3
+ *
4
+ * Stores current user in localStorage for retro/memo features.
5
+ * Team members list is configurable per project.
6
+ */
7
+
1
8
  import { ref } from 'vue'
2
9
 
3
- // TODO: Replace with your team members
4
- export const TEAM_MEMBERS = ['Member1', 'Member2', 'Member3'] as const
5
- export type TeamMember = (typeof TEAM_MEMBERS)[number]
10
+ // TODO: Replace with your team members or load dynamically from API
11
+ export const TEAM_MEMBERS: string[] = []
6
12
 
7
13
  const STORAGE_KEY = 'retro-user-name'
8
14
 
9
- const currentUser = ref<TeamMember | null>(
10
- (localStorage.getItem(STORAGE_KEY) as TeamMember | null) ?? null,
15
+ const currentUser = ref<string | null>(
16
+ localStorage.getItem(STORAGE_KEY) ?? null,
11
17
  )
12
18
 
13
19
  export function useUser() {
14
- function setUser(name: TeamMember) {
20
+ function setUser(name: string) {
15
21
  currentUser.value = name
16
22
  localStorage.setItem(STORAGE_KEY, name)
17
23
  }
@@ -1,11 +1,10 @@
1
- import type { PageVersion } from './types'
2
-
3
- export interface SprintConfig {
4
- id: string
5
- label: string
6
- theme: string
7
- active: boolean
8
- }
1
+ /**
2
+ * Navigation — Feature page definitions for static mode.
3
+ *
4
+ * In API mode, sprints/epics come from useNavStore.
5
+ * This file defines feature pages (wireframe pages) which are
6
+ * always statically configured regardless of mode.
7
+ */
9
8
 
10
9
  export interface FeaturePage {
11
10
  id: string
@@ -14,22 +13,6 @@ export interface FeaturePage {
14
13
  epicMap: Record<string, string> // sprint -> epicId (policy fallback)
15
14
  }
16
15
 
17
- export interface PageConfig {
18
- id: string
19
- label: string
20
- badge?: string
21
- category: string
22
- sprint: string
23
- description?: string
24
- /** Relative path from spec-site/src/ to the markdown file */
25
- mdPath?: string
26
- }
27
-
28
- /** Sprint definitions -- TODO: Replace with your sprints */
29
- export const sprints: SprintConfig[] = [
30
- { id: 's1', label: 'S1', theme: '', active: true },
31
- ]
32
-
33
16
  /** Feature pages for the sidebar navigation -- TODO: Add your feature pages */
34
17
  export const featurePages: FeaturePage[] = []
35
18
 
@@ -37,23 +20,5 @@ export function isValidFeaturePage(id: string): boolean {
37
20
  return featurePages.some(p => p.id === id)
38
21
  }
39
22
 
40
- // -- Policy pages (epic specs per sprint) -- TODO: Add your policy pages
41
- export const pages: PageConfig[] = []
42
-
43
- export function getPagesByCategory(sprint: string, category: string): PageConfig[] {
44
- return pages.filter(p => p.sprint === sprint && p.category === category)
45
- }
46
-
47
- export function getActiveSprint(): SprintConfig {
48
- return sprints.find(s => s.active) ?? sprints[0]
49
- }
50
-
51
23
  /** All navigable pages (features + extras like retro) */
52
24
  export const allPages = [...featurePages]
53
-
54
- /** Epic spec file mapping: sprint -> epic files -- TODO: Add your epic spec files */
55
- const epicSpecFiles: Record<string, Record<string, string>> = {}
56
-
57
- export function getEpicSpecFileName(sprint: string, epicId: string): string | null {
58
- return epicSpecFiles[sprint]?.[epicId] ?? null
59
- }