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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
// ── Domain types ──
|
|
6
6
|
|
|
7
|
-
export type StoryStatus = 'draft' | 'backlog' | 'ready' | 'in-progress' | 'review' | 'done'
|
|
7
|
+
export type StoryStatus = 'draft' | 'backlog' | 'ready' | 'in-progress' | 'review' | 'qa' | 'done'
|
|
8
8
|
export type TaskStatus = 'todo' | 'in-progress' | 'done'
|
|
9
9
|
export type Priority = 'low' | 'medium' | 'high' | 'critical'
|
|
10
10
|
export type EpicStatus = 'active' | 'completed' | 'archived'
|
|
@@ -31,6 +31,10 @@ export interface PmStory {
|
|
|
31
31
|
priority: Priority
|
|
32
32
|
area: string
|
|
33
33
|
storyPoints: number | null
|
|
34
|
+
startDate: string | null
|
|
35
|
+
dueDate: string | null
|
|
36
|
+
figmaUrl: string | null
|
|
37
|
+
relatedPrs: Array<{ prNumber: number; prUrl: string; prTitle: string; status: string }>
|
|
34
38
|
sortOrder: number
|
|
35
39
|
createdAt: string
|
|
36
40
|
updatedAt: string
|
|
@@ -43,6 +47,8 @@ export interface PmTask {
|
|
|
43
47
|
assignee: string | null
|
|
44
48
|
status: TaskStatus
|
|
45
49
|
description: string | null
|
|
50
|
+
storyPoints: number | null
|
|
51
|
+
dueDate: string | null
|
|
46
52
|
sortOrder: number
|
|
47
53
|
createdAt: string
|
|
48
54
|
updatedAt: string
|
|
@@ -77,6 +83,10 @@ export function mapStory(r: PmStoryRow): PmStory {
|
|
|
77
83
|
priority: (r.priority ?? 'medium') as Priority,
|
|
78
84
|
area: r.area ?? 'FE',
|
|
79
85
|
storyPoints: r.story_points,
|
|
86
|
+
startDate: r.start_date ?? null,
|
|
87
|
+
dueDate: r.due_date ?? null,
|
|
88
|
+
figmaUrl: r.figma_url ?? null,
|
|
89
|
+
relatedPrs: r.related_prs ? JSON.parse(r.related_prs) : [],
|
|
80
90
|
sortOrder: r.sort_order,
|
|
81
91
|
createdAt: r.created_at,
|
|
82
92
|
updatedAt: r.updated_at,
|
|
@@ -91,6 +101,8 @@ export function mapTask(r: PmTaskRow): PmTask {
|
|
|
91
101
|
assignee: r.assignee,
|
|
92
102
|
status: r.status as TaskStatus,
|
|
93
103
|
description: r.description,
|
|
104
|
+
storyPoints: r.story_points ?? null,
|
|
105
|
+
dueDate: r.due_date ?? null,
|
|
94
106
|
sortOrder: r.sort_order,
|
|
95
107
|
createdAt: r.created_at,
|
|
96
108
|
updatedAt: r.updated_at,
|
|
@@ -99,7 +111,7 @@ export function mapTask(r: PmTaskRow): PmTask {
|
|
|
99
111
|
|
|
100
112
|
// ── Status constants ──
|
|
101
113
|
|
|
102
|
-
export const STORY_STATUSES: StoryStatus[] = ['draft', 'backlog', 'ready', 'in-progress', 'review', 'done']
|
|
114
|
+
export const STORY_STATUSES: StoryStatus[] = ['draft', 'backlog', 'ready', 'in-progress', 'review', 'qa', 'done']
|
|
103
115
|
export const TASK_STATUSES: TaskStatus[] = ['todo', 'in-progress', 'done']
|
|
104
116
|
export const PRIORITIES: Priority[] = ['low', 'medium', 'high', 'critical']
|
|
105
117
|
export const AREAS = ['FE', 'BE', 'Design', 'Data', 'Infra', 'PO'] as const
|
|
@@ -111,6 +123,7 @@ export const STORY_STATUS_LABELS: Record<StoryStatus, string> = {
|
|
|
111
123
|
'ready': 'Ready',
|
|
112
124
|
'in-progress': 'In Progress',
|
|
113
125
|
'review': 'Review',
|
|
126
|
+
'qa': 'QA',
|
|
114
127
|
'done': 'Done',
|
|
115
128
|
}
|
|
116
129
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ref, computed, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type SheetState = 'peek' | 'half' | 'full'
|
|
4
|
+
|
|
5
|
+
const PEEK_HEIGHT = 48
|
|
6
|
+
const HALF_RATIO = 0.5
|
|
7
|
+
const FULL_RATIO = 0.85
|
|
8
|
+
const VELOCITY_THRESHOLD = 0.5 // px/ms
|
|
9
|
+
|
|
10
|
+
export function useBottomSheet() {
|
|
11
|
+
const state: Ref<SheetState> = ref('peek')
|
|
12
|
+
const dragging = ref(false)
|
|
13
|
+
const dragOffset = ref(0)
|
|
14
|
+
|
|
15
|
+
let startY = 0
|
|
16
|
+
let startTime = 0
|
|
17
|
+
let startTranslateY = 0
|
|
18
|
+
|
|
19
|
+
function getTranslateY(s: SheetState): number {
|
|
20
|
+
const vh = window.innerHeight
|
|
21
|
+
const sheetHeight = vh * FULL_RATIO
|
|
22
|
+
switch (s) {
|
|
23
|
+
case 'peek': return sheetHeight - PEEK_HEIGHT
|
|
24
|
+
case 'half': return sheetHeight - vh * HALF_RATIO
|
|
25
|
+
case 'full': return 0
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const translateY = computed(() => {
|
|
30
|
+
const base = getTranslateY(state.value)
|
|
31
|
+
return dragging.value ? base + dragOffset.value : base
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function tap() {
|
|
35
|
+
const order: SheetState[] = ['peek', 'half', 'full']
|
|
36
|
+
const idx = order.indexOf(state.value)
|
|
37
|
+
state.value = order[(idx + 1) % order.length]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function onPointerDown(e: PointerEvent) {
|
|
41
|
+
dragging.value = true
|
|
42
|
+
dragOffset.value = 0
|
|
43
|
+
startY = e.clientY
|
|
44
|
+
startTime = Date.now()
|
|
45
|
+
startTranslateY = getTranslateY(state.value)
|
|
46
|
+
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function onPointerMove(e: PointerEvent) {
|
|
50
|
+
if (!dragging.value) return
|
|
51
|
+
const dy = e.clientY - startY
|
|
52
|
+
const sheetHeight = window.innerHeight * FULL_RATIO
|
|
53
|
+
dragOffset.value = Math.max(-startTranslateY, Math.min(dy, sheetHeight - PEEK_HEIGHT - startTranslateY))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function onPointerUp(e: PointerEvent) {
|
|
57
|
+
if (!dragging.value) return
|
|
58
|
+
dragging.value = false
|
|
59
|
+
|
|
60
|
+
const dy = e.clientY - startY
|
|
61
|
+
const dt = Date.now() - startTime
|
|
62
|
+
const velocity = dy / Math.max(dt, 1)
|
|
63
|
+
|
|
64
|
+
if (Math.abs(velocity) > VELOCITY_THRESHOLD) {
|
|
65
|
+
if (velocity > 0) {
|
|
66
|
+
state.value = state.value === 'full' ? 'half' : 'peek'
|
|
67
|
+
} else {
|
|
68
|
+
state.value = state.value === 'peek' ? 'half' : 'full'
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
const currentTranslate = startTranslateY + dragOffset.value
|
|
72
|
+
const sheetHeight = window.innerHeight * FULL_RATIO
|
|
73
|
+
const peekT = sheetHeight - PEEK_HEIGHT
|
|
74
|
+
const halfT = sheetHeight - window.innerHeight * HALF_RATIO
|
|
75
|
+
const fullT = 0
|
|
76
|
+
|
|
77
|
+
const distances = [
|
|
78
|
+
{ state: 'peek' as SheetState, d: Math.abs(currentTranslate - peekT) },
|
|
79
|
+
{ state: 'half' as SheetState, d: Math.abs(currentTranslate - halfT) },
|
|
80
|
+
{ state: 'full' as SheetState, d: Math.abs(currentTranslate - fullT) },
|
|
81
|
+
]
|
|
82
|
+
distances.sort((a, b) => a.d - b.d)
|
|
83
|
+
state.value = distances[0].state
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
dragOffset.value = 0
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setState(s: SheetState) {
|
|
90
|
+
state.value = s
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
state,
|
|
95
|
+
dragging,
|
|
96
|
+
translateY,
|
|
97
|
+
tap,
|
|
98
|
+
setState,
|
|
99
|
+
onPointerDown,
|
|
100
|
+
onPointerMove,
|
|
101
|
+
onPointerUp,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard composable — aggregates data from multiple dashboard APIs.
|
|
3
|
+
*
|
|
4
|
+
* In static mode, returns empty state gracefully.
|
|
5
|
+
*/
|
|
6
|
+
import { ref } from 'vue'
|
|
7
|
+
import { apiGet, isStaticMode } from '@/api/client'
|
|
8
|
+
|
|
9
|
+
export interface UnreadMemo {
|
|
10
|
+
id: number
|
|
11
|
+
content: string
|
|
12
|
+
memoType: string
|
|
13
|
+
createdBy: string
|
|
14
|
+
createdAt: string
|
|
15
|
+
reviewRequired: number
|
|
16
|
+
pageId: string
|
|
17
|
+
replyCount: number
|
|
18
|
+
title: string | null
|
|
19
|
+
supersedesId: number | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SprintProgress {
|
|
23
|
+
sprint: string
|
|
24
|
+
total: number
|
|
25
|
+
done: number
|
|
26
|
+
progressPercent: number
|
|
27
|
+
byStatus: Record<string, number>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StandupStatus {
|
|
31
|
+
date: string
|
|
32
|
+
written: string[]
|
|
33
|
+
count: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MyRequest {
|
|
37
|
+
id: number
|
|
38
|
+
title: string | null
|
|
39
|
+
content: string
|
|
40
|
+
memoType: string
|
|
41
|
+
assignedTo: string | null
|
|
42
|
+
status: string
|
|
43
|
+
createdAt: string
|
|
44
|
+
supersedesId: number | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Decision {
|
|
48
|
+
id: number
|
|
49
|
+
title: string | null
|
|
50
|
+
content: string
|
|
51
|
+
createdBy: string
|
|
52
|
+
assignedTo: string | null
|
|
53
|
+
createdAt: string
|
|
54
|
+
supersedesId: number | null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface NudgeLogItem {
|
|
58
|
+
id: number
|
|
59
|
+
ruleId: string
|
|
60
|
+
title: string
|
|
61
|
+
body: string
|
|
62
|
+
createdAt: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface TeamInitiative {
|
|
66
|
+
id: number
|
|
67
|
+
title: string | null
|
|
68
|
+
content: string
|
|
69
|
+
memoType: string
|
|
70
|
+
createdBy: string
|
|
71
|
+
createdAt: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function useDashboard() {
|
|
75
|
+
const unreadMemos = ref<UnreadMemo[]>([])
|
|
76
|
+
const pendingReviews = ref<UnreadMemo[]>([])
|
|
77
|
+
const myRequests = ref<MyRequest[]>([])
|
|
78
|
+
const activeDecisions = ref<Decision[]>([])
|
|
79
|
+
const sprintProgress = ref<SprintProgress | null>(null)
|
|
80
|
+
const mySprintProgress = ref<SprintProgress | null>(null)
|
|
81
|
+
const standupStatus = ref<StandupStatus | null>(null)
|
|
82
|
+
const loading = ref(false)
|
|
83
|
+
const errors = ref<string[]>([])
|
|
84
|
+
const nudgeLog = ref<NudgeLogItem[]>([])
|
|
85
|
+
const teamInitiatives = ref<TeamInitiative[]>([])
|
|
86
|
+
|
|
87
|
+
function todayStr(): string {
|
|
88
|
+
return new Date().toISOString().split('T')[0]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function loadAll(sprint: string, userName?: string) {
|
|
92
|
+
if (isStaticMode()) { loading.value = false; return }
|
|
93
|
+
loading.value = true
|
|
94
|
+
errors.value = []
|
|
95
|
+
|
|
96
|
+
const fetches = [
|
|
97
|
+
apiGet<{ unreadMemos: Array<Record<string, unknown>> }>('/api/v2/dashboard/unread-memos'),
|
|
98
|
+
apiGet<{ unreadMemos: Array<Record<string, unknown>> }>('/api/v2/dashboard/unread-memos', { review_required: '1' }),
|
|
99
|
+
apiGet<{ sprint: string; total: number; done: number; progressPercent: number; byStatus: Record<string, number> }>('/api/v2/dashboard/sprint-progress', { sprint }),
|
|
100
|
+
apiGet<{ date: string; written: string[]; count: number }>('/api/v2/dashboard/standup-status', { sprint, date: todayStr() }),
|
|
101
|
+
apiGet<{ myRequests: Array<Record<string, unknown>> }>('/api/v2/dashboard/my-requests'),
|
|
102
|
+
apiGet<{ decisions: Array<Record<string, unknown>> }>('/api/v2/dashboard/active-decisions'),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
if (userName) {
|
|
106
|
+
fetches.push(
|
|
107
|
+
apiGet<SprintProgress>('/api/v2/dashboard/sprint-progress', { sprint, user: userName }),
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const results = await Promise.all(fetches)
|
|
112
|
+
|
|
113
|
+
if (results[0].error) errors.value.push(results[0].error)
|
|
114
|
+
else if (results[0].data) unreadMemos.value = ((results[0].data as any).unreadMemos ?? []).map(mapMemo)
|
|
115
|
+
|
|
116
|
+
if (results[1].error) errors.value.push(results[1].error)
|
|
117
|
+
else if (results[1].data) pendingReviews.value = ((results[1].data as any).unreadMemos ?? []).map(mapMemo)
|
|
118
|
+
|
|
119
|
+
if (results[2].error) errors.value.push(results[2].error)
|
|
120
|
+
else if (results[2].data) sprintProgress.value = results[2].data as SprintProgress
|
|
121
|
+
|
|
122
|
+
if (results[3].error) errors.value.push(results[3].error)
|
|
123
|
+
else if (results[3].data) standupStatus.value = results[3].data as StandupStatus
|
|
124
|
+
|
|
125
|
+
if (results[4].error) errors.value.push(results[4].error)
|
|
126
|
+
else if (results[4].data) myRequests.value = ((results[4].data as any).myRequests ?? []).map(mapRequest)
|
|
127
|
+
|
|
128
|
+
if (results[5].error) errors.value.push(results[5].error)
|
|
129
|
+
else if (results[5].data) activeDecisions.value = ((results[5].data as any).decisions ?? []).map(mapDecision)
|
|
130
|
+
|
|
131
|
+
if (userName && results[6]) {
|
|
132
|
+
if (results[6].error) errors.value.push(results[6].error)
|
|
133
|
+
else if (results[6].data) mySprintProgress.value = results[6].data as SprintProgress
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
loading.value = false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function loadNudgeLog() {
|
|
140
|
+
if (isStaticMode()) return
|
|
141
|
+
const { data } = await apiGet<{ nudges: Array<Record<string, unknown>> }>(
|
|
142
|
+
'/api/v2/dashboard/nudge-log', { limit: '10' },
|
|
143
|
+
)
|
|
144
|
+
if (data?.nudges) {
|
|
145
|
+
nudgeLog.value = (data.nudges as Array<Record<string, unknown>>).map(r => ({
|
|
146
|
+
id: r.id as number,
|
|
147
|
+
ruleId: (r.rule_id as string) ?? '',
|
|
148
|
+
title: (r.title as string) ?? '',
|
|
149
|
+
body: (r.body as string) ?? '',
|
|
150
|
+
createdAt: (r.created_at as string) ?? '',
|
|
151
|
+
}))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function loadTeamInitiatives() {
|
|
156
|
+
if (isStaticMode()) return
|
|
157
|
+
const { data } = await apiGet<{ initiatives: Array<Record<string, unknown>> }>(
|
|
158
|
+
'/api/v2/initiatives', { limit: '20' },
|
|
159
|
+
)
|
|
160
|
+
if (data?.initiatives) {
|
|
161
|
+
teamInitiatives.value = (data.initiatives as Array<Record<string, unknown>>).map(r => ({
|
|
162
|
+
id: r.id as number,
|
|
163
|
+
title: (r.title as string) ?? null,
|
|
164
|
+
content: (r.content as string) ?? '',
|
|
165
|
+
memoType: (r.status as string) ?? 'pending',
|
|
166
|
+
createdBy: (r.author as string) ?? '',
|
|
167
|
+
createdAt: (r.created_at as string) ?? '',
|
|
168
|
+
}))
|
|
169
|
+
} else {
|
|
170
|
+
// Fallback: memo-based (when initiatives table is unavailable)
|
|
171
|
+
const { data: memoData } = await apiGet<{ memos: Array<Record<string, unknown>> }>(
|
|
172
|
+
'/api/v2/memos/all', { limit: '10', status: 'open' },
|
|
173
|
+
)
|
|
174
|
+
if (memoData?.memos) {
|
|
175
|
+
teamInitiatives.value = (memoData.memos as Array<Record<string, unknown>>)
|
|
176
|
+
.filter(r => r.memo_type === 'feature_request')
|
|
177
|
+
.map(r => ({
|
|
178
|
+
id: r.id as number,
|
|
179
|
+
title: (r.title as string) ?? null,
|
|
180
|
+
content: (r.content as string) ?? '',
|
|
181
|
+
memoType: (r.memo_type as string) ?? '',
|
|
182
|
+
createdBy: (r.created_by as string) ?? '',
|
|
183
|
+
createdAt: (r.created_at as string) ?? '',
|
|
184
|
+
}))
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
unreadMemos, pendingReviews, myRequests, activeDecisions,
|
|
191
|
+
sprintProgress, mySprintProgress, standupStatus, nudgeLog, teamInitiatives,
|
|
192
|
+
loading, errors, loadAll, loadNudgeLog, loadTeamInitiatives,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function mapMemo(r: Record<string, unknown>): UnreadMemo {
|
|
197
|
+
return {
|
|
198
|
+
id: r.id as number, content: (r.content as string) ?? '', memoType: (r.memo_type as string) ?? 'memo',
|
|
199
|
+
createdBy: (r.created_by as string) ?? '', createdAt: (r.created_at as string) ?? '',
|
|
200
|
+
reviewRequired: (r.review_required as number) ?? 0, pageId: (r.page_id as string) ?? '',
|
|
201
|
+
replyCount: (r.reply_count as number) ?? 0, title: (r.title as string) ?? null,
|
|
202
|
+
supersedesId: (r.supersedes_id as number) ?? null,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function mapRequest(r: Record<string, unknown>): MyRequest {
|
|
207
|
+
return {
|
|
208
|
+
id: r.id as number, title: (r.title as string) ?? null, content: (r.content as string) ?? '',
|
|
209
|
+
memoType: (r.memo_type as string) ?? '', assignedTo: (r.assigned_to as string) ?? null,
|
|
210
|
+
status: (r.status as string) ?? 'open', createdAt: (r.created_at as string) ?? '',
|
|
211
|
+
supersedesId: (r.supersedes_id as number) ?? null,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function mapDecision(r: Record<string, unknown>): Decision {
|
|
216
|
+
return {
|
|
217
|
+
id: r.id as number, title: (r.title as string) ?? null, content: (r.content as string) ?? '',
|
|
218
|
+
createdBy: (r.created_by as string) ?? '', assignedTo: (r.assigned_to as string) ?? null,
|
|
219
|
+
createdAt: (r.created_at as string) ?? '', supersedesId: (r.supersedes_id as number) ?? null,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media query composable — generic reactive media query wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Usage: const isMobile = useMediaQuery('(max-width: 767px)')
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ref, onMounted, onUnmounted, type Ref } from 'vue'
|
|
8
|
+
|
|
9
|
+
export function useMediaQuery(query: string): Ref<boolean> {
|
|
10
|
+
const matches = ref(false)
|
|
11
|
+
let mql: MediaQueryList | null = null
|
|
12
|
+
|
|
13
|
+
function update(e: MediaQueryListEvent | MediaQueryList) {
|
|
14
|
+
matches.value = e.matches
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onMounted(() => {
|
|
18
|
+
mql = window.matchMedia(query)
|
|
19
|
+
matches.value = mql.matches
|
|
20
|
+
mql.addEventListener('change', update)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
onUnmounted(() => {
|
|
24
|
+
mql?.removeEventListener('change', update)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return matches
|
|
28
|
+
}
|
|
@@ -8,11 +8,50 @@
|
|
|
8
8
|
import { ref, computed } from 'vue'
|
|
9
9
|
import { isStaticMode } from '@/api/client'
|
|
10
10
|
|
|
11
|
+
// ── Tier 1 (simple, localStorage-backed) ──
|
|
11
12
|
export interface MemoItem {
|
|
12
13
|
id: number
|
|
13
14
|
text: string
|
|
14
15
|
author: string
|
|
15
16
|
ts: number
|
|
17
|
+
// Tier 2 extended fields (API-backed)
|
|
18
|
+
page_id?: string
|
|
19
|
+
content?: string
|
|
20
|
+
memo_type?: MemoType
|
|
21
|
+
status?: 'open' | 'resolved'
|
|
22
|
+
created_by?: string
|
|
23
|
+
assigned_to?: string | null
|
|
24
|
+
review_required?: number
|
|
25
|
+
title?: string | null
|
|
26
|
+
channel?: string
|
|
27
|
+
resolved_by?: string | null
|
|
28
|
+
resolved_at?: string | null
|
|
29
|
+
created_at?: string
|
|
30
|
+
updated_at?: string
|
|
31
|
+
checklist?: string | null
|
|
32
|
+
reply_count?: number
|
|
33
|
+
has_relations?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type MemoType = 'memo' | 'decision' | 'request' | 'backlog' | 'blocker' | 'question' | 'announcement'
|
|
37
|
+
|
|
38
|
+
export const MEMO_TYPES: { value: MemoType; label: string; icon: string; color: string }[] = [
|
|
39
|
+
{ value: 'memo', label: 'Memo', icon: '📝', color: '#3b82f6' },
|
|
40
|
+
{ value: 'decision', label: 'Decision', icon: '⚡', color: '#8b5cf6' },
|
|
41
|
+
{ value: 'request', label: 'Request', icon: '📋', color: '#f59e0b' },
|
|
42
|
+
{ value: 'backlog', label: 'Backlog', icon: '💡', color: '#22c55e' },
|
|
43
|
+
{ value: 'blocker', label: 'Blocker', icon: '🚧', color: '#ef4444' },
|
|
44
|
+
{ value: 'question', label: 'Question', icon: '❓', color: '#06b6d4' },
|
|
45
|
+
{ value: 'announcement', label: 'Announcement', icon: '📢', color: '#6366f1' },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
export interface ReplyItem {
|
|
49
|
+
id: number
|
|
50
|
+
memo_id: number
|
|
51
|
+
content: string
|
|
52
|
+
created_by: string
|
|
53
|
+
review_type?: string
|
|
54
|
+
created_at: string
|
|
16
55
|
}
|
|
17
56
|
|
|
18
57
|
export function useMemo(pageId: string) {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification composable — fetch, poll, and manage notifications.
|
|
3
|
+
*
|
|
4
|
+
* Singleton pattern: refs are module-level, shared across all consumers.
|
|
5
|
+
* In static mode, returns empty state gracefully.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ref, computed } from 'vue'
|
|
9
|
+
import { apiGet, apiPost, apiPatch, apiDelete, isStaticMode } from '@/api/client'
|
|
10
|
+
|
|
11
|
+
export interface NotificationItem {
|
|
12
|
+
id: number
|
|
13
|
+
type: string
|
|
14
|
+
title: string
|
|
15
|
+
body: string | null
|
|
16
|
+
sourceType: string
|
|
17
|
+
sourceId: number
|
|
18
|
+
pageId: string
|
|
19
|
+
actor: string
|
|
20
|
+
isRead: boolean
|
|
21
|
+
createdAt: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Module-level singletons
|
|
25
|
+
const notifications = ref<NotificationItem[]>([])
|
|
26
|
+
const _userName = ref<string | null>(null)
|
|
27
|
+
let _pollTimer: ReturnType<typeof setInterval> | null = null
|
|
28
|
+
|
|
29
|
+
// Cross-component communication: notification click -> open memo sidebar
|
|
30
|
+
export const shouldOpenMemoSidebar = ref(false)
|
|
31
|
+
export const pendingNotificationPageId = ref<string | null>(null)
|
|
32
|
+
|
|
33
|
+
export function useNotification() {
|
|
34
|
+
const unreadCount = computed(() => notifications.value.filter(n => !n.isRead).length)
|
|
35
|
+
|
|
36
|
+
function setUser(name: string | null) {
|
|
37
|
+
_userName.value = name
|
|
38
|
+
if (name) {
|
|
39
|
+
fetchNotifications()
|
|
40
|
+
} else {
|
|
41
|
+
notifications.value = []
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function fetchNotifications(): Promise<void> {
|
|
46
|
+
if (isStaticMode() || !_userName.value) return
|
|
47
|
+
try {
|
|
48
|
+
const { data } = await apiGet<{
|
|
49
|
+
notifications: Array<{
|
|
50
|
+
id: number; type: string; title: string; body: string | null
|
|
51
|
+
source_type: string; source_id: number; page_id: string
|
|
52
|
+
actor: string; is_read: number; created_at: string
|
|
53
|
+
}>
|
|
54
|
+
}>('/api/v2/notifications', { user: _userName.value })
|
|
55
|
+
if (data) {
|
|
56
|
+
notifications.value = data.notifications.map(row => ({
|
|
57
|
+
id: Number(row.id),
|
|
58
|
+
type: String(row.type),
|
|
59
|
+
title: String(row.title),
|
|
60
|
+
body: row.body ? String(row.body) : null,
|
|
61
|
+
sourceType: String(row.source_type),
|
|
62
|
+
sourceId: Number(row.source_id),
|
|
63
|
+
pageId: String(row.page_id),
|
|
64
|
+
actor: String(row.actor),
|
|
65
|
+
isRead: Number(row.is_read) === 1,
|
|
66
|
+
createdAt: new Date(row.created_at + 'Z').getTime(),
|
|
67
|
+
}))
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// offline -- keep existing
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function markAsRead(id: number): Promise<void> {
|
|
75
|
+
if (isStaticMode()) return
|
|
76
|
+
try {
|
|
77
|
+
const { error } = await apiPatch(`/api/v2/notifications/${id}/read`, {})
|
|
78
|
+
if (!error) {
|
|
79
|
+
const item = notifications.value.find(n => n.id === id)
|
|
80
|
+
if (item) item.isRead = true
|
|
81
|
+
}
|
|
82
|
+
} catch { /* ignore */ }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function markAllAsRead(): Promise<void> {
|
|
86
|
+
if (isStaticMode() || !_userName.value) return
|
|
87
|
+
try {
|
|
88
|
+
const { error } = await apiPost('/api/v2/notifications/mark-all-read', { user: _userName.value })
|
|
89
|
+
if (!error) {
|
|
90
|
+
notifications.value.forEach(n => { n.isRead = true })
|
|
91
|
+
} else {
|
|
92
|
+
await fetchNotifications()
|
|
93
|
+
}
|
|
94
|
+
} catch { /* ignore */ }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function startPolling() {
|
|
98
|
+
stopPolling()
|
|
99
|
+
_pollTimer = setInterval(() => fetchNotifications(), 30_000)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function stopPolling() {
|
|
103
|
+
if (_pollTimer) {
|
|
104
|
+
clearInterval(_pollTimer)
|
|
105
|
+
_pollTimer = null
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Notification creation (called from useMemo)
|
|
110
|
+
|
|
111
|
+
async function createMemoNotifications(
|
|
112
|
+
memoId: number,
|
|
113
|
+
pageId: string,
|
|
114
|
+
author: string,
|
|
115
|
+
assignedTo: string | null,
|
|
116
|
+
content: string,
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
if (isStaticMode()) return
|
|
119
|
+
const preview = content.length > 60 ? content.slice(0, 60) + '...' : content
|
|
120
|
+
|
|
121
|
+
if (assignedTo) {
|
|
122
|
+
await _insertNotification(assignedTo, 'memo_assigned', `${author} left a memo`, preview, 'memo', memoId, pageId, author)
|
|
123
|
+
} else {
|
|
124
|
+
const members = await _getActiveMembers()
|
|
125
|
+
for (const m of members) {
|
|
126
|
+
if (m === author) continue
|
|
127
|
+
await _insertNotification(m, 'memo_mention_all', `${author} left a team memo`, preview, 'memo', memoId, pageId, author)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function createReplyNotification(
|
|
133
|
+
memoId: number,
|
|
134
|
+
pageId: string,
|
|
135
|
+
replier: string,
|
|
136
|
+
memoAuthor: string,
|
|
137
|
+
content: string,
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
if (isStaticMode()) return
|
|
140
|
+
if (replier === memoAuthor) return
|
|
141
|
+
const preview = content.length > 60 ? content.slice(0, 60) + '...' : content
|
|
142
|
+
await _insertNotification(memoAuthor, 'reply_received', `${replier} replied`, preview, 'memo', memoId, pageId, replier)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function deleteNotificationsForMemo(memoId: number): Promise<void> {
|
|
146
|
+
if (isStaticMode()) return
|
|
147
|
+
try {
|
|
148
|
+
await apiDelete('/api/v2/notifications/by-source', { sourceType: 'memo', sourceId: memoId })
|
|
149
|
+
} catch { /* ignore */ }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Internal helpers
|
|
153
|
+
|
|
154
|
+
async function _insertNotification(
|
|
155
|
+
userName: string, type: string, title: string, body: string | null,
|
|
156
|
+
sourceType: string, sourceId: number, pageId: string, actor: string,
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
try {
|
|
159
|
+
await apiPost('/api/v2/notifications', {
|
|
160
|
+
userName, type, title, body, sourceType, sourceId, pageId, actor,
|
|
161
|
+
})
|
|
162
|
+
} catch { /* ignore */ }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function _getActiveMembers(): Promise<string[]> {
|
|
166
|
+
try {
|
|
167
|
+
const { data } = await apiGet<{ users: string[] }>('/api/v2/notifications/active-users')
|
|
168
|
+
if (data) return data.users
|
|
169
|
+
} catch { /* ignore */ }
|
|
170
|
+
return []
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatTimeAgo(ts: number): string {
|
|
174
|
+
const diff = Date.now() - ts
|
|
175
|
+
const mins = Math.floor(diff / 60_000)
|
|
176
|
+
if (mins < 1) return 'just now'
|
|
177
|
+
if (mins < 60) return `${mins}min ago`
|
|
178
|
+
const hours = Math.floor(mins / 60)
|
|
179
|
+
if (hours < 24) return `${hours}hr ago`
|
|
180
|
+
const days = Math.floor(hours / 24)
|
|
181
|
+
return `${days}d ago`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
notifications,
|
|
186
|
+
unreadCount,
|
|
187
|
+
setUser,
|
|
188
|
+
fetchNotifications,
|
|
189
|
+
markAsRead,
|
|
190
|
+
markAllAsRead,
|
|
191
|
+
startPolling,
|
|
192
|
+
stopPolling,
|
|
193
|
+
createMemoNotifications,
|
|
194
|
+
createReplyNotification,
|
|
195
|
+
deleteNotificationsForMemo,
|
|
196
|
+
formatTimeAgo,
|
|
197
|
+
shouldOpenMemoSidebar,
|
|
198
|
+
pendingNotificationPageId,
|
|
199
|
+
}
|
|
200
|
+
}
|