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.
- package/lib/hydrate.mjs +6 -1
- package/lib/setup-wizard.mjs +29 -3
- package/package.json +1 -1
- package/scaffold/.claude/commands/_domain.md.hbs +33 -0
- package/scaffold/.claude/commands/analytics.md.hbs +55 -0
- package/scaffold/.claude/commands/daily.md.hbs +301 -0
- package/scaffold/.claude/commands/dev.md.hbs +62 -0
- package/scaffold/.claude/commands/gtm.md +82 -0
- package/scaffold/.claude/commands/handoff.md +259 -0
- package/scaffold/.claude/commands/market.md +120 -0
- package/scaffold/.claude/commands/metrics.md +123 -0
- package/scaffold/.claude/commands/oscar-loop.md +436 -0
- package/scaffold/.claude/commands/party.md +85 -0
- package/scaffold/.claude/commands/plan.md +43 -0
- package/scaffold/.claude/commands/poc.md +69 -0
- package/scaffold/.claude/commands/research.md +203 -0
- package/scaffold/.claude/commands/retro.md +68 -0
- package/scaffold/.claude/commands/save.md +440 -0
- package/scaffold/.claude/commands/sessions.md +139 -0
- package/scaffold/.claude/commands/sprint.md +106 -0
- package/scaffold/.claude/commands/start.md +396 -0
- package/scaffold/.claude/commands/strategy.md +41 -0
- package/scaffold/.claude/commands/task.md +220 -0
- package/scaffold/.claude/commands/tracking.md +116 -0
- package/scaffold/.claude/commands/validate.md +58 -0
- package/scaffold/.context/WORKFLOW.md.hbs +58 -26
- package/scaffold/.context/agents/planner.md.hbs +35 -7
- package/scaffold/.context/integrations/_registry.yaml +6 -0
- package/scaffold/.context/integrations/providers/sqlite_lambda.yaml +24 -0
- package/scaffold/.context/integrations/providers/supabase.yaml +34 -0
- package/scaffold/.context/integrations/providers/turso_cf.yaml +34 -0
- package/scaffold/.context/poc/_skills/build.md +79 -0
- package/scaffold/.context/poc/_skills/scope.md +50 -0
- package/scaffold/.context/poc/_skills/spec.md +80 -0
- package/scaffold/.context/poc/_skills/verify.md +60 -0
- package/scaffold/CLAUDE.md.hbs +210 -0
- package/scaffold/spec-site/.env.example +11 -0
- package/scaffold/spec-site/index.html +2 -2
- package/scaffold/spec-site/sql/schema.sql +224 -0
- package/scaffold/spec-site/src/api/client.ts +131 -0
- package/scaffold/spec-site/src/api/types.ts +177 -0
- package/scaffold/spec-site/src/components/Accordion.vue +1 -1
- package/scaffold/spec-site/src/components/AppHeader.vue +5 -4
- package/scaffold/spec-site/src/components/CoachingCard.vue +1 -1
- package/scaffold/spec-site/src/components/ScenarioSwitcher.vue +1 -1
- package/scaffold/spec-site/src/composables/navTypes.ts +39 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +134 -0
- package/scaffold/spec-site/src/composables/useAuth.ts +139 -0
- package/scaffold/spec-site/src/composables/useMemo.ts +51 -40
- package/scaffold/spec-site/src/composables/useNavStore.ts +202 -0
- package/scaffold/spec-site/src/composables/usePageContent.ts +208 -0
- package/scaffold/spec-site/src/composables/usePmStore.ts +224 -0
- package/scaffold/spec-site/src/composables/useRetro.ts +181 -95
- package/scaffold/spec-site/src/composables/useScenarioStore.ts +74 -30
- package/scaffold/spec-site/src/composables/useUser.ts +12 -6
- package/scaffold/spec-site/src/data/navigation.ts +7 -42
- package/scaffold/spec-site/src/data/types.ts +13 -43
- package/scaffold/spec-site/src/main.ts +7 -0
- package/scaffold/spec-site/src/pages/PolicyDetail.vue +30 -11
- package/scaffold/spec-site/src/pages/PolicyIndex.vue +22 -7
- package/scaffold/spec-site/src/pages/retro/RetroActions.vue +3 -3
- package/scaffold/spec-site/src/pages/retro/RetroBoard.vue +2 -2
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +5 -7
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +2 -2
- package/scaffold/spec-site/src/pages/shared/NoContentPlaceholder.vue +2 -2
- package/scaffold/spec-site/src/pages/shared/PolicyFallback.vue +25 -13
- package/scaffold/spec-site/src/router.ts +11 -7
- package/scaffold/spec-site/src/styles/base.css +2 -2
- package/scaffold/spec-site/src/styles/split-pane.css +1 -1
- package/scaffold/spec-site/src/styles/variables.css +7 -7
- package/scaffold/spec-site/src/assets/icons/menu/ic_ads.svg +0 -10
- package/scaffold/spec-site/src/assets/icons/menu/ic_ads_on.svg +0 -10
- package/scaffold/spec-site/src/assets/icons/menu/ic_board.svg +0 -14
- package/scaffold/spec-site/src/assets/icons/menu/ic_board_on.svg +0 -14
- package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard.svg +0 -21
- package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard_on.svg +0 -21
- package/scaffold/spec-site/src/assets/icons/menu/ic_pricing.svg +0 -20
- package/scaffold/spec-site/src/assets/icons/menu/ic_pricing_on.svg +0 -20
- package/scaffold/spec-site/src/assets/icons/menu/ic_store.svg +0 -11
- package/scaffold/spec-site/src/assets/icons/menu/ic_store_on.svg +0 -11
- 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 {
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
)
|
|
73
|
-
if (
|
|
74
|
-
error.value =
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)
|
|
170
|
-
if (
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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 ? ` (
|
|
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('##
|
|
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
|
|
339
|
+
// -- Refresh --
|
|
256
340
|
let _currentUser = ''
|
|
257
341
|
|
|
258
342
|
async function refresh() {
|
|
259
|
-
if (!session.value) return
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
264
|
-
if (
|
|
265
|
-
session.value = { ...session.value, phase:
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 = [
|
|
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<
|
|
10
|
-
|
|
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:
|
|
20
|
+
function setUser(name: string) {
|
|
15
21
|
currentUser.value = name
|
|
16
22
|
localStorage.setItem(STORAGE_KEY, name)
|
|
17
23
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
}
|