popilot 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/bin/cli.mjs +204 -2
  2. package/lib/doctor.mjs +38 -1
  3. package/lib/hydrate.mjs +15 -0
  4. package/lib/scaffold.mjs +5 -0
  5. package/lib/setup-wizard.mjs +35 -2
  6. package/package.json +1 -1
  7. package/scaffold/.context/project.yaml.example +19 -0
  8. package/scaffold/mcp-pm/package.json +19 -0
  9. package/scaffold/mcp-pm/src/api-client.ts +69 -0
  10. package/scaffold/mcp-pm/src/index.ts +660 -0
  11. package/scaffold/mcp-pm/tsconfig.json +14 -0
  12. package/scaffold/pm-api/package.json +21 -0
  13. package/scaffold/pm-api/sql/schema-core.sql +331 -0
  14. package/scaffold/pm-api/sql/schema-docs.sql +25 -0
  15. package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
  16. package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
  17. package/scaffold/pm-api/src/auth.ts +28 -0
  18. package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
  19. package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
  20. package/scaffold/pm-api/src/db/adapter.ts +36 -0
  21. package/scaffold/pm-api/src/db/turso.ts +147 -0
  22. package/scaffold/pm-api/src/index.ts +114 -0
  23. package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
  24. package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
  25. package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
  26. package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
  27. package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
  28. package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
  29. package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
  30. package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
  31. package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
  32. package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
  33. package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
  34. package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
  35. package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
  36. package/scaffold/pm-api/src/mcp.ts +871 -0
  37. package/scaffold/pm-api/src/nudge.ts +283 -0
  38. package/scaffold/pm-api/src/routes/auth.ts +32 -0
  39. package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
  40. package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
  41. package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
  42. package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
  43. package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
  44. package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
  45. package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
  46. package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
  47. package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
  48. package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
  49. package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
  50. package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
  51. package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
  52. package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
  53. package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
  54. package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
  55. package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
  56. package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
  57. package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
  58. package/scaffold/pm-api/src/types.ts +11 -0
  59. package/scaffold/pm-api/src/utils/activity.ts +22 -0
  60. package/scaffold/pm-api/src/utils/admin.ts +9 -0
  61. package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
  62. package/scaffold/pm-api/src/utils/assignee.ts +69 -0
  63. package/scaffold/pm-api/src/utils/db.ts +45 -0
  64. package/scaffold/pm-api/src/utils/initiative.ts +23 -0
  65. package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
  66. package/scaffold/pm-api/tsconfig.json +15 -0
  67. package/scaffold/pm-api/wrangler.toml.hbs +11 -0
  68. package/scaffold/spec-site/package-lock.json +40 -0
  69. package/scaffold/spec-site/package.json +4 -1
  70. package/scaffold/spec-site/src/api/types.ts +6 -0
  71. package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
  72. package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
  73. package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
  74. package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
  75. package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
  76. package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
  77. package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
  78. package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
  79. package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
  80. package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
  81. package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
  82. package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
  83. package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
  84. package/scaffold/spec-site/src/composables/useUser.ts +19 -1
  85. package/scaffold/spec-site/src/features.ts +108 -0
  86. package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
  87. package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
  88. package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
  89. package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
  90. package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
  91. package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
  92. package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
  93. package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
  94. package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
  95. package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
  96. package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
  97. package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
  98. package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
  99. package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
  100. package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
  101. package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
  102. package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
  103. package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
  104. package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
  105. package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
  106. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
  107. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
  108. package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
  109. package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
  110. package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
  111. package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
  112. package/scaffold/spec-site/src/router.ts +141 -0
@@ -0,0 +1,650 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, computed, ref } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { useDashboard } from '@/composables/useDashboard'
5
+ import { apiPatch, apiPost, apiGet } from '@/api/client'
6
+ import { getActiveSprint } from '@/composables/useNavStore'
7
+ import { useUser, TEAM_MEMBERS } from '@/composables/useUser'
8
+ import { Bell, Rocket, BarChart3, Sun, Zap, FileText, ClipboardList, ScrollText } from 'lucide-vue-next'
9
+
10
+ const memoTypeComponentMap: Record<string, any> = {
11
+ decision: Zap,
12
+ feature_request: Rocket,
13
+ policy_request: ScrollText,
14
+ request: ClipboardList,
15
+ memo: FileText
16
+ }
17
+
18
+ const router = useRouter()
19
+ const { currentUser, dynamicMembers, loadMembers } = useUser()
20
+ const dashboard = useDashboard()
21
+
22
+ const sprint = computed(() => getActiveSprint().id)
23
+
24
+ const members = computed(() => {
25
+ return dynamicMembers.value.length > 0 ? dynamicMembers.value : [...TEAM_MEMBERS]
26
+ })
27
+
28
+ const standupNotWritten = computed(() => {
29
+ if (!dashboard.standupStatus.value) return []
30
+ const written = new Set(dashboard.standupStatus.value.written)
31
+ return members.value.filter((m: string) => !written.has(m))
32
+ })
33
+
34
+ function formatTime(ts: string): string {
35
+ if (!ts) return ''
36
+ const d = new Date(ts + (ts.includes('Z') ? '' : 'Z'))
37
+ const h = d.getHours().toString().padStart(2, '0')
38
+ const m = d.getMinutes().toString().padStart(2, '0')
39
+ return `${h}:${m}`
40
+ }
41
+
42
+ function truncate(text: string, max: number): string {
43
+ return text.length > max ? text.slice(0, max) + '...' : text
44
+ }
45
+
46
+ function nudgeRuleLabel(ruleId: string): string {
47
+ const labels: Record<string, string> = {
48
+ review_overdue: 'Review Overdue',
49
+ sprint_deadline: 'Sprint Deadline',
50
+ standup_missing: 'Standup Missing',
51
+ task_stale: 'Task Stale',
52
+ blocker_unresolved: 'Blocker Unresolved',
53
+ daily_report: 'Daily Report',
54
+ }
55
+ return labels[ruleId] ?? ruleId
56
+ }
57
+
58
+ function formatDate(ts: string): string {
59
+ if (!ts) return ''
60
+ const d = new Date(ts + (ts.includes('Z') ? '' : 'Z'))
61
+ return `${(d.getMonth() + 1).toString().padStart(2, '0')}/${d.getDate().toString().padStart(2, '0')}`
62
+ }
63
+
64
+ function initiativeStatusLabel(status: string): string {
65
+ return { pending: 'Pending', approved: 'Approved', rejected: 'Rejected', deferred: 'Deferred' }[status] ?? status
66
+ }
67
+
68
+ async function handleInitiative(id: number, status: 'approved' | 'rejected') {
69
+ await apiPatch(`/api/v2/initiatives/${id}/status`, { status })
70
+ await dashboard.loadTeamInitiatives()
71
+ }
72
+
73
+ async function convertToEpic(item: any) {
74
+ const title = item.title || item.content?.split('\n')[0]?.slice(0, 100) || 'New Epic'
75
+ if (!confirm(`Create epic "${title}"?`)) return
76
+ const { error } = await apiPost('/api/v2/pm/epics', { title, description: item.content })
77
+ if (error) { alert(error); return }
78
+ await handleInitiative(item.id, 'approved' as any)
79
+ alert('Epic created')
80
+ }
81
+
82
+ async function convertToStory(item: any) {
83
+ const title = item.title || item.content?.split('\n')[0]?.slice(0, 100) || 'New Story'
84
+ if (!confirm(`Create story "${title}"?`)) return
85
+ const { error } = await apiPost('/api/v2/pm/stories', {
86
+ title,
87
+ description: item.content,
88
+ status: 'backlog',
89
+ })
90
+ if (error) { alert(error); return }
91
+ await handleInitiative(item.id, 'approved' as any)
92
+ alert('Story created')
93
+ }
94
+
95
+ async function resolveFromNudge(nudge: { title: string; body: string }) {
96
+ await dashboard.loadAll(sprint.value, currentUser.value ?? undefined)
97
+ await dashboard.loadNudgeLog()
98
+ }
99
+
100
+ async function handleReview(memoId: number, action: 'approve' | 'reject') {
101
+ const endpoint = action === 'approve' ? 'resolve' : 'reopen'
102
+ await apiPatch(`/api/v2/memos/${memoId}/${endpoint}`, { userName: currentUser.value ?? '' })
103
+ await dashboard.loadAll(sprint.value, currentUser.value ?? undefined)
104
+ }
105
+
106
+ const mySummary = ref<any>(null)
107
+ const activities = ref<any[]>([])
108
+
109
+ async function loadActivities() {
110
+ const { data } = await apiGet('/api/v2/activity?limit=20')
111
+ if (data?.activities) activities.value = data.activities as any[]
112
+ }
113
+
114
+ function formatActivityTime(d: string) {
115
+ if (!d) return ''
116
+ const date = new Date(d.endsWith('Z') ? d : d + 'Z')
117
+ return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
118
+ }
119
+
120
+ function activityIcon(type: string) {
121
+ const map: Record<string, string> = {
122
+ story_status_change: '🔄', pr_merged: '🔀', memo_created: '📝',
123
+ memo_reply: '💬', initiative_created: '💡', standup_saved: '📋',
124
+ }
125
+ return map[type] || '📌'
126
+ }
127
+
128
+ function activityDesc(a: any) {
129
+ const meta = a.metadata ? JSON.parse(a.metadata) : {}
130
+ if (a.action_type === 'story_status_change') return `${a.target_title} → ${meta.status || ''}`
131
+ if (a.action_type === 'pr_merged') return `PR merged: ${a.target_title}`
132
+ if (a.action_type === 'memo_created') return `Memo: ${a.target_title?.slice(0, 40)}`
133
+ return a.target_title || a.action_type
134
+ }
135
+
136
+ async function loadMySummary() {
137
+ const user = currentUser.value
138
+ if (!user) return
139
+ const { data } = await apiGet(`/api/v2/dashboard/my-summary?user=${encodeURIComponent(user)}`)
140
+ if (data) mySummary.value = data
141
+ }
142
+
143
+ const sprintSummary = ref<{ completedCount: number; totalStories: number; doneSP: number; totalSP: number; completionRate: number } | null>(null)
144
+ const velocityData = ref<Array<{ assignee: string; doneSP: number; totalSP: number }>>([])
145
+
146
+ const sprintHistory = ref<Array<{ id: string; label: string; doneSP: number; isActive?: boolean }>>([])
147
+ const maxHistorySP = computed(() => Math.max(...sprintHistory.value.map(s => s.doneSP), 1))
148
+
149
+ async function loadSprintHistory() {
150
+ const { data: navData } = await apiGet('/api/v2/nav')
151
+ if (!navData?.sprints) return
152
+ const allSprints = (navData.sprints as any[]).sort((a: any, b: any) => (a.start_date ?? '').localeCompare(b.start_date ?? ''))
153
+ const sprints = allSprints.slice(-5)
154
+ const history: typeof sprintHistory.value = []
155
+ for (const sp of sprints) {
156
+ const { data: preview } = await apiGet(`/api/v2/kickoff/${sp.id}/close-preview`)
157
+ history.push({
158
+ id: sp.id,
159
+ label: sp.label || sp.id,
160
+ doneSP: (preview?.summary as any)?.doneSP ?? 0,
161
+ isActive: sp.status === 'active' || sp.active === 1,
162
+ })
163
+ }
164
+ sprintHistory.value = history
165
+ }
166
+
167
+ async function loadSprintStats() {
168
+ const { data } = await apiGet(`/api/v2/kickoff/${sprint.value}/close-preview`)
169
+ if (data?.summary) sprintSummary.value = data.summary as any
170
+ if (data?.velocity) velocityData.value = data.velocity as any
171
+ }
172
+
173
+ onMounted(async () => {
174
+ await loadMembers()
175
+ await Promise.all([
176
+ dashboard.loadAll(sprint.value, currentUser.value ?? undefined),
177
+ dashboard.loadNudgeLog(),
178
+ dashboard.loadTeamInitiatives(),
179
+ loadSprintStats(),
180
+ loadSprintHistory(),
181
+ loadMySummary(),
182
+ loadActivities(),
183
+ ])
184
+ })
185
+ </script>
186
+
187
+ <template>
188
+ <div class="dashboard-wrapper">
189
+ <div class="dashboard">
190
+ <div class="dashboard-header">
191
+ <div class="header-top">
192
+ <h1>Team Flow Monitor</h1>
193
+ <span class="sprint-badge">{{ sprint }}</span>
194
+ </div>
195
+ <p class="header-subtitle">Team progress and key initiatives at a glance</p>
196
+ </div>
197
+
198
+ <div v-if="dashboard.loading.value" class="loading">Loading...</div>
199
+
200
+ <!-- My Work -->
201
+ <section v-if="mySummary" class="my-section">
202
+ <h2>My Work</h2>
203
+ <div class="my-cards">
204
+ <div class="my-card" :class="{ warn: (mySummary.myStories || []).some((s: any) => s.daysInProgress >= 3) }" @click="$router.push('/board')">
205
+ <div class="my-card-title">In Progress Stories</div>
206
+ <div class="my-card-count">{{ (mySummary.myStories || []).length }}</div>
207
+ <div v-for="s in (mySummary.myStories || []).slice(0, 3)" :key="s.id" class="my-story-item">
208
+ <span>{{ s.title?.slice(0, 30) }}</span>
209
+ <span v-if="s.daysInProgress >= 3" class="days-warn">{{ s.daysInProgress }}d</span>
210
+ </div>
211
+ </div>
212
+ <div class="my-card" @click="$router.push('/board')">
213
+ <div class="my-card-title">Pending Reviews</div>
214
+ <div class="my-card-count">{{ (mySummary.myReviews || []).length }}</div>
215
+ </div>
216
+ <div class="my-card" @click="$router.push('/inbox')">
217
+ <div class="my-card-title">Unread Mentions</div>
218
+ <div class="my-card-count">{{ mySummary.unreadMentions || 0 }}</div>
219
+ </div>
220
+ <div class="my-card" @click="$router.push('/memos')">
221
+ <div class="my-card-title">Unanswered Memos</div>
222
+ <div class="my-card-count">{{ mySummary.unansweredMemos || 0 }}</div>
223
+ </div>
224
+ </div>
225
+ </section>
226
+
227
+ <!-- Sprint Progress -->
228
+ <section v-if="sprintSummary" class="sprint-progress-card">
229
+ <h2>Sprint Progress</h2>
230
+ <div class="progress-stats">
231
+ <div class="progress-stat">
232
+ <span class="stat-value">{{ sprintSummary.completedCount }} / {{ sprintSummary.totalStories }}</span>
233
+ <span class="stat-label">Stories</span>
234
+ </div>
235
+ <div class="progress-stat">
236
+ <span class="stat-value">{{ sprintSummary.doneSP }} / {{ sprintSummary.totalSP }}</span>
237
+ <span class="stat-label">SP</span>
238
+ </div>
239
+ <div class="progress-stat">
240
+ <span class="stat-value">{{ sprintSummary.completionRate }}%</span>
241
+ <span class="stat-label">Completion</span>
242
+ </div>
243
+ </div>
244
+ <div class="progress-bar-wrap">
245
+ <div class="progress-bar-fill" :style="{ width: sprintSummary.completionRate + '%' }" />
246
+ </div>
247
+
248
+ <div v-if="velocityData.length" class="velocity-chart">
249
+ <h3>Team Velocity</h3>
250
+ <div v-for="v in velocityData" :key="v.assignee" class="velocity-bar-row">
251
+ <span class="velocity-label">{{ v.assignee }}</span>
252
+ <div class="velocity-bar-bg">
253
+ <div class="velocity-bar-done" :style="{ width: v.totalSP > 0 ? (v.doneSP / v.totalSP * 100) + '%' : '0%' }" />
254
+ </div>
255
+ <span class="velocity-sp">{{ v.doneSP }}/{{ v.totalSP }}</span>
256
+ </div>
257
+ </div>
258
+
259
+ <div class="burndown-chart">
260
+ <h3>Burndown Chart</h3>
261
+ <svg viewBox="0 0 400 200" class="burndown-svg">
262
+ <line x1="40" y1="20" x2="380" y2="180" stroke="#d1d5db" stroke-width="2" stroke-dasharray="6,4" />
263
+ <text x="350" y="170" font-size="11" fill="#9ca3af" font-weight="600">Ideal</text>
264
+ <line x1="40" y1="180" x2="380" y2="180" stroke="#9ca3af" stroke-width="1" />
265
+ <line x1="40" y1="20" x2="40" y2="180" stroke="#9ca3af" stroke-width="1" />
266
+ <circle
267
+ :cx="40 + (sprintSummary.completionRate / 100) * 340"
268
+ :cy="20 + ((1 - ((sprintSummary.totalSP - sprintSummary.doneSP) / (sprintSummary.totalSP || 1))) * 160)"
269
+ r="6" fill="#3b82f6"
270
+ />
271
+ <line x1="40" y1="20"
272
+ :x2="40 + (sprintSummary.completionRate / 100) * 340"
273
+ :y2="20 + ((1 - ((sprintSummary.totalSP - sprintSummary.doneSP) / (sprintSummary.totalSP || 1))) * 160)"
274
+ stroke="#3b82f6" stroke-width="2"
275
+ />
276
+ <text x="40" y="195" font-size="10" fill="#888">Start</text>
277
+ <text x="360" y="195" font-size="10" fill="#888">End</text>
278
+ <text x="5" y="25" font-size="10" fill="#888">{{ sprintSummary.totalSP }}SP</text>
279
+ <text x="5" y="185" font-size="10" fill="#888">0</text>
280
+ </svg>
281
+ <p class="burndown-note">Remaining SP: {{ (sprintSummary.totalSP ?? 0) - (sprintSummary.doneSP ?? 0) }} / {{ sprintSummary.totalSP ?? 0 }}</p>
282
+ </div>
283
+
284
+ <div v-if="sprintHistory.length > 1" class="sprint-history-chart">
285
+ <h3>Sprint Velocity Trend</h3>
286
+ <div class="history-bars">
287
+ <div v-for="sh in sprintHistory" :key="sh.id" class="history-bar-col">
288
+ <div class="history-bar-bg">
289
+ <div class="history-bar-fill" :style="{ height: maxHistorySP > 0 ? (sh.doneSP / maxHistorySP * 100) + '%' : '0%' }" />
290
+ </div>
291
+ <span class="history-bar-label">{{ sh.label }}</span>
292
+ <span class="history-bar-sp">{{ sh.doneSP }}{{ sh.isActive ? ' (active)' : '' }}</span>
293
+ </div>
294
+ </div>
295
+ </div>
296
+ </section>
297
+
298
+ <!-- Activity Feed -->
299
+ <section v-if="activities.length" class="activity-section">
300
+ <h2>Recent Activity</h2>
301
+ <div class="activity-list">
302
+ <div v-for="a in activities.slice(0, 10)" :key="a.id" class="activity-item">
303
+ <span class="activity-time">{{ formatActivityTime(a.created_at) }}</span>
304
+ <span class="activity-icon">{{ activityIcon(a.action_type) }}</span>
305
+ <span class="activity-actor">{{ a.actor }}</span>
306
+ <span class="activity-desc">{{ activityDesc(a) }}</span>
307
+ </div>
308
+ </div>
309
+ </section>
310
+
311
+ <div v-if="!dashboard.loading.value" class="dashboard-grid">
312
+ <div class="section-divider"><span>Team Flow</span></div>
313
+
314
+ <!-- Nudge Log -->
315
+ <section class="card card--nudge">
316
+ <div class="card-title">
317
+ <Bell :size="16" class="card-icon" />
318
+ Recent Alerts (Nudge)
319
+ <span v-if="dashboard.nudgeLog.value.length" class="count-badge count--amber">{{ dashboard.nudgeLog.value.length }}</span>
320
+ </div>
321
+ <div v-if="dashboard.nudgeLog.value.length === 0" class="card-empty">No alerts sent</div>
322
+ <div v-else class="card-list">
323
+ <div v-for="n in dashboard.nudgeLog.value" :key="n.id" class="nudge-row clickable" :class="'nudge--' + n.ruleId" @click="router.push(`/board/${sprint}`)">
324
+ <span class="nudge-rule-badge" :class="'rule--' + n.ruleId">{{ nudgeRuleLabel(n.ruleId) }}</span>
325
+ <div class="nudge-text">
326
+ <span class="nudge-title">{{ n.title }}</span>
327
+ <span v-if="n.body" class="nudge-body">{{ truncate(n.body, 80) }}</span>
328
+ </div>
329
+ <span class="memo-time">{{ formatDate(n.createdAt) }} {{ formatTime(n.createdAt) }}</span>
330
+ <button v-if="n.ruleId === 'review_overdue'" class="btn-action btn--approve" @click.stop="resolveFromNudge(n)" title="Approve">✓</button>
331
+ </div>
332
+ </div>
333
+ </section>
334
+
335
+ <!-- Team Initiatives -->
336
+ <section class="card card--initiatives">
337
+ <div class="card-title">
338
+ <Rocket :size="16" class="card-icon" />
339
+ Team Initiatives
340
+ <span v-if="dashboard.teamInitiatives.value.length" class="count-badge count--blue">{{ dashboard.teamInitiatives.value.length }}</span>
341
+ </div>
342
+ <div v-if="dashboard.teamInitiatives.value.length === 0" class="card-empty">No active initiatives</div>
343
+ <div v-else class="card-list">
344
+ <div v-for="item in dashboard.teamInitiatives.value" :key="item.id" class="memo-row initiative-row">
345
+ <span class="initiative-status-badge" :class="'ist--' + item.memoType">{{ initiativeStatusLabel(item.memoType) }}</span>
346
+ <span class="memo-content">{{ item.title || truncate(item.content, 50) }}</span>
347
+ <span class="memo-author">{{ item.createdBy }}</span>
348
+ <span class="memo-time">{{ formatDate(item.createdAt) }}</span>
349
+ <div v-if="item.memoType === 'pending'" class="initiative-actions">
350
+ <button class="btn-action btn--approve" @click.stop="handleInitiative(item.id, 'approved')" title="Approve">✓</button>
351
+ <button class="btn-action btn--reject" @click.stop="handleInitiative(item.id, 'rejected')" title="Reject">✗</button>
352
+ </div>
353
+ <div v-if="item.memoType === 'approved'" class="initiative-actions">
354
+ <button class="btn-action btn--convert-epic" @click.stop="convertToEpic(item)" title="Convert to Epic">Epic</button>
355
+ <button class="btn-action btn--convert-story" @click.stop="convertToStory(item)" title="Convert to Story">Story</button>
356
+ </div>
357
+ </div>
358
+ </div>
359
+ </section>
360
+
361
+ <!-- Sprint Progress + Standup -->
362
+ <section class="card card--progress">
363
+ <div class="card-title">
364
+ <BarChart3 :size="16" class="card-icon" />
365
+ Sprint Progress
366
+ </div>
367
+ <div v-if="dashboard.sprintProgress.value" class="progress-content clickable" @click="router.push(`/board/${sprint}`)">
368
+ <div class="progress-bar-container">
369
+ <div class="progress-bar-fill" :style="{ width: dashboard.sprintProgress.value.progressPercent + '%' }" />
370
+ </div>
371
+ <div class="progress-stats">
372
+ <span class="progress-percent">{{ dashboard.sprintProgress.value.progressPercent }}%</span>
373
+ <span class="progress-detail">{{ dashboard.sprintProgress.value.done }} / {{ dashboard.sprintProgress.value.total }} stories</span>
374
+ </div>
375
+ <div class="status-breakdown">
376
+ <span v-for="(cnt, status) in dashboard.sprintProgress.value.byStatus" :key="status" class="status-chip" :class="'status--' + status">
377
+ {{ status }}: {{ cnt }}
378
+ </span>
379
+ </div>
380
+ </div>
381
+ <div v-else class="card-empty">No data</div>
382
+
383
+ <div v-if="dashboard.mySprintProgress.value" class="my-progress">
384
+ <div class="my-progress-label">My Stories</div>
385
+ <div class="progress-bar-container" style="height: 6px;">
386
+ <div class="progress-bar-fill" :style="{ width: dashboard.mySprintProgress.value.progressPercent + '%' }" />
387
+ </div>
388
+ <span class="progress-detail">{{ dashboard.mySprintProgress.value.done }} / {{ dashboard.mySprintProgress.value.total }}</span>
389
+ </div>
390
+ </section>
391
+
392
+ <section class="card card--standup">
393
+ <div class="card-title">
394
+ <Sun :size="16" class="card-icon" />
395
+ Today's Standup
396
+ </div>
397
+ <div v-if="dashboard.standupStatus.value" class="standup-content">
398
+ <div class="standup-stat">
399
+ <span class="standup-count">{{ dashboard.standupStatus.value.count }}</span>
400
+ <span class="standup-label">/ {{ members.length }} written</span>
401
+ </div>
402
+ <div v-if="standupNotWritten.length > 0" class="standup-missing">
403
+ <span class="missing-label">Not written:</span>
404
+ <span v-for="name in standupNotWritten" :key="name" class="missing-name">{{ name }}</span>
405
+ </div>
406
+ <div v-else class="card-empty">All members completed</div>
407
+ <button class="btn btn--sm" @click="router.push(`/standup/${sprint}`)">View Standup →</button>
408
+ </div>
409
+ <div v-else class="card-empty">No data</div>
410
+ </section>
411
+
412
+ <div class="section-divider"><span>My Work</span></div>
413
+
414
+ <!-- Pending Approvals -->
415
+ <section class="card card--pending">
416
+ <div class="card-title">
417
+ <Bell :size="16" class="card-icon" />
418
+ Pending Approvals
419
+ <span v-if="dashboard.pendingReviews.value.length" class="count-badge">{{ dashboard.pendingReviews.value.length }}</span>
420
+ </div>
421
+ <div v-if="dashboard.pendingReviews.value.length === 0" class="card-empty">None</div>
422
+ <div v-else class="card-list">
423
+ <div v-for="memo in dashboard.pendingReviews.value" :key="memo.id" class="memo-row review-row">
424
+ <span class="memo-author">{{ memo.createdBy }}</span>
425
+ <span class="memo-content">{{ truncate(memo.content, 60) }}</span>
426
+ <span class="memo-time">{{ formatTime(memo.createdAt) }}</span>
427
+ <div class="review-actions">
428
+ <button class="btn-action btn--approve" @click.stop="handleReview(memo.id, 'approve')">✓</button>
429
+ <button class="btn-action btn--reject" @click.stop="handleReview(memo.id, 'reject')">✗</button>
430
+ </div>
431
+ </div>
432
+ </div>
433
+ </section>
434
+
435
+ <!-- Unread Memos -->
436
+ <section class="card card--memos">
437
+ <div class="card-title">
438
+ <FileText :size="16" class="card-icon" />
439
+ Unread Memos
440
+ <span v-if="dashboard.unreadMemos.value.length" class="count-badge">{{ dashboard.unreadMemos.value.length }}</span>
441
+ </div>
442
+ <div v-if="dashboard.unreadMemos.value.length === 0" class="card-empty">None</div>
443
+ <div v-else class="card-list">
444
+ <div v-for="memo in dashboard.unreadMemos.value" :key="memo.id" class="memo-row clickable" :class="{ 'review-required': memo.reviewRequired }" @click="router.push(`/board/${sprint}`)">
445
+ <span class="memo-author">{{ memo.createdBy }}</span>
446
+ <span class="memo-content">{{ truncate(memo.content, 60) }}</span>
447
+ <span v-if="memo.reviewRequired" class="review-badge">Approval Required</span>
448
+ <span class="memo-time">{{ formatTime(memo.createdAt) }}</span>
449
+ </div>
450
+ </div>
451
+ </section>
452
+
453
+ <!-- My Requests -->
454
+ <section class="card card--requests">
455
+ <div class="card-title">
456
+ <Rocket :size="16" class="card-icon" />
457
+ My Requests
458
+ <span v-if="dashboard.myRequests.value.length" class="count-badge count--blue">{{ dashboard.myRequests.value.length }}</span>
459
+ </div>
460
+ <div v-if="dashboard.myRequests.value.length === 0" class="card-empty">None</div>
461
+ <div v-else class="card-list">
462
+ <div v-for="req in dashboard.myRequests.value" :key="req.id" class="memo-row">
463
+ <component :is="memoTypeComponentMap[req.memoType] || FileText" :size="14" class="memo-type-icon" />
464
+ <span class="memo-content">{{ req.title || truncate(req.content, 50) }}</span>
465
+ <span class="status-chip" :class="'status--' + req.status">{{ req.status }}</span>
466
+ </div>
467
+ </div>
468
+ </section>
469
+
470
+ <!-- Active Decisions -->
471
+ <section class="card card--decisions">
472
+ <div class="card-title">
473
+ <Zap :size="16" class="card-icon" />
474
+ Active Decisions
475
+ <span v-if="dashboard.activeDecisions.value.length" class="count-badge count--purple">{{ dashboard.activeDecisions.value.length }}</span>
476
+ </div>
477
+ <div v-if="dashboard.activeDecisions.value.length === 0" class="card-empty">None</div>
478
+ <div v-else class="card-list">
479
+ <div v-for="dec in dashboard.activeDecisions.value" :key="dec.id" class="memo-row decision-row">
480
+ <span class="memo-content">{{ dec.title || truncate(dec.content, 50) }}</span>
481
+ <span class="memo-author">{{ dec.createdBy }}</span>
482
+ <span class="memo-time">{{ formatTime(dec.createdAt) }}</span>
483
+ </div>
484
+ </div>
485
+ </section>
486
+ </div>
487
+
488
+ <div v-if="dashboard.errors.value.length" class="error-list">
489
+ <div v-for="(err, i) in dashboard.errors.value" :key="i" class="error-msg">{{ err }}</div>
490
+ </div>
491
+ </div>
492
+ </div>
493
+ </template>
494
+
495
+ <style scoped>
496
+ .dashboard { max-width: 1100px; margin: 0 auto; padding: 24px; min-height: 100vh; position: relative; }
497
+ .dashboard-wrapper { background: transparent; min-height: 100vh; position: relative; }
498
+ .dashboard > * { position: relative; z-index: 1; }
499
+ .dashboard-header { margin-bottom: 24px; }
500
+ .header-top { display: flex; align-items: center; gap: 10px; }
501
+ .dashboard-header h1 { font-size: 26px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.02em; }
502
+ .header-subtitle { font-size: 14px; color: var(--text-secondary); margin: 4px 0 0; }
503
+ .sprint-badge { font-size: 12px; font-weight: 600; color: var(--text-primary); background: rgba(0,0,0,0.06); backdrop-filter: blur(10px); padding: 4px 10px; border-radius: 20px; text-transform: uppercase; }
504
+ .dashboard-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; overflow: hidden; }
505
+ .card { background: rgba(255,255,255,0.25); backdrop-filter: blur(40px) saturate(1.8); -webkit-backdrop-filter: blur(40px) saturate(1.8); border: 1px solid rgba(255,255,255,0.45); border-radius: 24px; padding: 22px; box-shadow: 0 2px 12px rgba(0,0,0,0.03), inset 0 1px 0 rgba(255,255,255,0.7), inset 0 -1px 0 rgba(0,0,0,0.03); transition: all 0.3s cubic-bezier(0.25,0.1,0.25,1); overflow: visible; min-width: 0; position: relative; isolation: isolate; }
506
+ .card::before { content: ''; position: absolute; inset: -20%; background: radial-gradient(ellipse at 20% 50%, rgba(0,220,200,0.08) 0%, transparent 50%), radial-gradient(ellipse at 80% 30%, rgba(160,100,255,0.06) 0%, transparent 50%), radial-gradient(ellipse at 50% 80%, rgba(255,150,200,0.05) 0%, transparent 50%); filter: blur(30px); pointer-events: none; z-index: -1; border-radius: inherit; }
507
+ .card:hover { background: rgba(255,255,255,0.32); box-shadow: 0 4px 16px rgba(0,0,0,0.04), inset 0 1px 0 rgba(255,255,255,0.8), inset 0 -1px 0 rgba(0,0,0,0.04); transform: translateY(-2px); }
508
+ .card-title { font-size: 14px; font-weight: 700; color: var(--text-primary); margin-bottom: 12px; display: flex; align-items: center; gap: 6px; }
509
+ .card-icon { flex-shrink: 0; color: var(--text-secondary); }
510
+ .count-badge { font-size: 11px; font-weight: 700; color: #fff; background: #ef4444; padding: 1px 6px; border-radius: 10px; min-width: 18px; text-align: center; }
511
+ .card-empty { text-align: center; padding: 8px; color: var(--text-muted); font-size: 12px; }
512
+ .card-list { display: flex; flex-direction: column; gap: 6px; overflow: hidden; }
513
+ .memo-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; background: var(--inner-card-bg); font-size: 12px; overflow: hidden; min-width: 0; }
514
+ .memo-row.review-required { background: rgba(255,149,0,0.10); border-left: 3px solid #FF9500; }
515
+ .memo-row.clickable { cursor: pointer; transition: background 0.15s; }
516
+ .memo-row.clickable:hover { background: rgba(255,255,255,0.08); }
517
+ .review-row { flex-wrap: wrap; }
518
+ .review-actions { display: flex; gap: 4px; margin-left: auto; }
519
+ .btn-action { border: none; border-radius: 4px; width: 28px; height: 28px; cursor: pointer; font-size: 14px; font-weight: 700; }
520
+ .btn--approve { background: rgba(34,197,94,0.15); color: #16a34a; }
521
+ .btn--approve:hover { background: #bbf7d0; }
522
+ .btn--reject { background: rgba(255,59,48,0.15); color: #dc2626; }
523
+ .btn--reject:hover { background: #fecaca; }
524
+ .clickable { cursor: pointer; }
525
+ .memo-author { font-weight: 600; color: #334155; flex-shrink: 0; }
526
+ .memo-content { flex: 1; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
527
+ .memo-time { flex-shrink: 0; color: var(--text-muted); font-size: 11px; }
528
+ .review-badge { font-size: 9px; font-weight: 600; padding: 1px 5px; border-radius: 3px; background: rgba(255,149,0,0.12); color: #d97706; flex-shrink: 0; }
529
+ .progress-content { display: flex; flex-direction: column; gap: 8px; }
530
+ .progress-bar-container { width: 100%; height: 8px; background: rgba(0,0,0,0.06); border-radius: 4px; overflow: hidden; }
531
+ .progress-bar-fill { height: 100%; background: linear-gradient(90deg, #3B82F6, #60A5FA); border-radius: 4px; transition: width 0.5s; }
532
+ .progress-stats { display: flex; align-items: baseline; gap: 8px; }
533
+ .progress-percent { font-size: 24px; font-weight: 700; color: var(--text-primary); }
534
+ .progress-detail { font-size: 12px; color: var(--text-secondary); }
535
+ .status-breakdown { display: flex; gap: 6px; flex-wrap: wrap; }
536
+ .status-chip { font-size: 10px; padding: 2px 8px; border-radius: 4px; background: rgba(0,0,0,0.04); color: var(--text-secondary); font-weight: 600; border: 1px solid rgba(0,0,0,0.06); }
537
+ .status--done { background: rgba(34,197,94,0.22); color: #16a34a; border-color: rgba(34,197,94,0.20); }
538
+ .status--in-progress { background: rgba(59,130,246,0.12); color: #2563EB; border-color: rgba(59,130,246,0.20); }
539
+ .status--review { background: rgba(255,149,0,0.12); color: #d97706; border-color: rgba(255,149,0,0.20); }
540
+ .status--blocked { background: rgba(255,59,48,0.12); color: #dc2626; border-color: rgba(255,59,48,0.20); }
541
+ .status--backlog { background: rgba(100,116,139,0.10); color: #64748b; border-color: rgba(100,116,139,0.15); }
542
+ .status--open { background: rgba(59,130,246,0.15); color: #2563EB; border-color: rgba(59,130,246,0.25); }
543
+ .status--resolved { background: rgba(34,197,94,0.15); color: #16a34a; border-color: rgba(34,197,94,0.25); }
544
+ .standup-content { display: flex; flex-direction: column; gap: 8px; }
545
+ .standup-stat { display: flex; align-items: baseline; gap: 4px; }
546
+ .standup-count { font-size: 28px; font-weight: 700; color: var(--text-primary); }
547
+ .standup-label { font-size: 13px; color: var(--text-secondary); }
548
+ .standup-missing { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
549
+ .missing-label { font-size: 11px; color: #ef4444; font-weight: 600; }
550
+ .missing-name { font-size: 11px; padding: 1px 6px; border-radius: 3px; background: rgba(255,59,48,0.12); color: #dc2626; }
551
+ .btn { padding: 6px 14px; border: 1px solid rgba(255,255,255,0.08); border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; background: var(--inner-card-bg); color: var(--text-secondary); transition: all 0.15s; align-self: flex-start; }
552
+ .btn:hover { background: #f1f5f9; }
553
+ .btn--sm { padding: 4px 10px; font-size: 11px; }
554
+ .count--blue { background: #3b82f6; }
555
+ .count--purple { background: #8b5cf6; }
556
+ .count--amber { background: #f59e0b; }
557
+ .memo-type-icon { flex-shrink: 0; color: var(--text-secondary); }
558
+ .decision-row { border-left: 3px solid #8b5cf6; }
559
+ .my-progress { margin-top: 10px; padding-top: 8px; border-top: 1px dashed rgba(0,0,0,0.08); display: flex; align-items: center; gap: 8px; }
560
+ .my-progress-label { font-size: 11px; font-weight: 600; color: var(--text-secondary); flex-shrink: 0; }
561
+ .my-progress .progress-bar-container { flex: 1; }
562
+ .section-divider { grid-column: 1 / -1; display: flex; align-items: center; gap: 12px; margin: 8px 0 0; }
563
+ .section-divider::after { content: ''; flex: 1; height: 1px; background: rgba(255,255,255,0.08); }
564
+ .section-divider span { font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
565
+ .card--nudge { grid-column: 1 / -1; }
566
+ .nudge-row { display: flex; align-items: flex-start; gap: 8px; padding: 8px 10px; border-radius: 6px; background: var(--inner-card-bg); font-size: 12px; overflow: hidden; min-width: 0; }
567
+ .nudge--review_overdue { border-left: 3px solid #f59e0b; }
568
+ .nudge--sprint_deadline { border-left: 3px solid #ef4444; }
569
+ .nudge--standup_missing { border-left: 3px solid #3b82f6; }
570
+ .nudge--task_stale { border-left: 3px solid #8b5cf6; }
571
+ .nudge--blocker_unresolved { border-left: 3px solid #dc2626; }
572
+ .nudge--daily_report { border-left: 3px solid #22c55e; }
573
+ .nudge-rule-badge { font-size: 9px; font-weight: 700; padding: 2px 6px; border-radius: 3px; flex-shrink: 0; background: #f1f5f9; color: var(--text-secondary); white-space: nowrap; }
574
+ .rule--review_overdue { background: rgba(255,149,0,0.15); color: #FF9500; }
575
+ .rule--sprint_deadline { background: rgba(255,59,48,0.15); color: #dc2626; }
576
+ .rule--standup_missing { background: #dbeafe; color: #2563EB; }
577
+ .rule--task_stale { background: #ede9fe; color: #7c3aed; }
578
+ .rule--blocker_unresolved { background: rgba(255,59,48,0.15); color: #dc2626; }
579
+ .rule--daily_report { background: rgba(34,197,94,0.15); color: #16a34a; }
580
+ .nudge-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
581
+ .nudge-title { color: #334155; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
582
+ .nudge-body { color: var(--text-muted); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
583
+ .card--initiatives { grid-column: 1 / -1; }
584
+ .initiative-row { border-left: 3px solid #3b82f6; flex-wrap: wrap; }
585
+ .initiative-status-badge { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; }
586
+ .btn--convert-epic { background: #dbeafe; color: #1d4ed8; border: 1px solid #bfdbfe; border-radius: 4px; padding: 2px 8px; font-size: 11px; cursor: pointer; }
587
+ .btn--convert-story { background: #fef3c7; color: #d97706; border: 1px solid #fde68a; border-radius: 4px; padding: 2px 8px; font-size: 11px; cursor: pointer; }
588
+ .ist--pending { background: rgba(59,130,246,0.12); color: #2563EB; }
589
+ .ist--approved { background: rgba(34,197,94,0.12); color: #16a34a; }
590
+ .ist--rejected { background: rgba(239,68,68,0.12); color: #dc2626; }
591
+ .ist--deferred { background: rgba(148,163,184,0.12); color: #64748b; }
592
+ .initiative-actions { display: flex; gap: 4px; margin-left: auto; }
593
+ .loading { text-align: center; padding: 40px; color: var(--text-muted); font-size: 14px; }
594
+ .error-list { margin-top: 16px; }
595
+ .error-msg { font-size: 12px; color: #ef4444; padding: 4px 0; }
596
+ .activity-section { margin-bottom: 24px; }
597
+ .activity-section h2 { font-size: 16px; font-weight: 700; margin-bottom: 12px; }
598
+ .activity-list { background: #fff; border-radius: 12px; padding: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
599
+ .activity-item { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid rgba(0,0,0,0.04); font-size: 13px; }
600
+ .activity-item:last-child { border-bottom: none; }
601
+ .activity-time { font-size: 11px; color: #9ca3af; min-width: 40px; }
602
+ .activity-icon { font-size: 14px; }
603
+ .activity-actor { font-weight: 600; color: #333; }
604
+ .activity-desc { color: #6b7280; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
605
+ .my-section { margin-bottom: 24px; }
606
+ .my-section h2 { font-size: 16px; font-weight: 700; margin-bottom: 12px; }
607
+ .my-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
608
+ .my-card { background: #fff; border-radius: 12px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); cursor: pointer; transition: all 0.2s; }
609
+ .my-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
610
+ .my-card.warn { background: #fef2f2; border-left: 3px solid #ef4444; }
611
+ .my-card-title { font-size: 12px; color: #6b7280; margin-bottom: 4px; }
612
+ .my-card-count { font-size: 24px; font-weight: 700; color: #111; }
613
+ .my-story-item { font-size: 11px; color: #6b7280; margin-top: 4px; display: flex; justify-content: space-between; }
614
+ .days-warn { color: #ef4444; font-weight: 600; }
615
+ @media (max-width: 768px) { .my-cards { grid-template-columns: 1fr; } }
616
+ .sprint-progress-card { background: rgba(255,255,255,0.25); backdrop-filter: blur(40px) saturate(1.8); border: 1px solid rgba(255,255,255,0.45); border-radius: 24px; padding: 24px; margin-bottom: 24px; }
617
+ .sprint-progress-card h2 { font-size: 16px; margin-bottom: 16px; }
618
+ .sprint-progress-card .progress-stats { display: flex; gap: 24px; margin-bottom: 16px; }
619
+ .progress-stat { text-align: center; }
620
+ .stat-value { font-size: 20px; font-weight: 700; display: block; }
621
+ .stat-label { font-size: 12px; color: var(--text-muted, #888); }
622
+ .progress-bar-wrap { height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; margin-bottom: 24px; }
623
+ .velocity-chart h3 { font-size: 14px; margin-bottom: 12px; }
624
+ .velocity-bar-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
625
+ .velocity-label { width: 80px; font-size: 12px; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
626
+ .velocity-bar-bg { flex: 1; height: 16px; background: #e5e7eb; border-radius: 4px; overflow: hidden; }
627
+ .velocity-bar-done { height: 100%; background: #3b82f6; border-radius: 4px; transition: width 0.3s; }
628
+ .velocity-sp { font-size: 11px; color: var(--text-muted, #888); min-width: 50px; }
629
+ .burndown-chart { margin-top: 24px; }
630
+ .burndown-chart h3 { font-size: 14px; margin-bottom: 8px; }
631
+ .burndown-svg { width: 100%; max-height: 200px; }
632
+ .burndown-note { font-size: 12px; color: var(--text-muted, #888); margin-top: 4px; }
633
+ .sprint-history-chart { margin-top: 24px; }
634
+ .sprint-history-chart h3 { font-size: 14px; margin-bottom: 12px; }
635
+ .history-bars { display: flex; gap: 12px; align-items: flex-end; height: 120px; }
636
+ .history-bar-col { display: flex; flex-direction: column; align-items: center; flex: 1; height: 100%; }
637
+ .history-bar-bg { flex: 1; width: 100%; max-width: 40px; background: #e5e7eb; border-radius: 4px; display: flex; flex-direction: column; justify-content: flex-end; overflow: hidden; }
638
+ .history-bar-fill { background: #3b82f6; border-radius: 4px; transition: height 0.3s; }
639
+ .history-bar-label { font-size: 10px; margin-top: 4px; color: var(--text-muted, #888); }
640
+ .history-bar-sp { font-size: 11px; font-weight: 600; }
641
+ @media (max-width: 767px) {
642
+ .dashboard { padding: 10px; }
643
+ .dashboard-header { margin-bottom: 12px; }
644
+ .dashboard-header h1 { font-size: 17px; }
645
+ .dashboard-grid { grid-template-columns: 1fr; gap: 8px; }
646
+ .card { padding: 10px; border-radius: 8px; }
647
+ .card-title { font-size: 12px; margin-bottom: 6px; }
648
+ .card--nudge, .card--initiatives, .section-divider { grid-column: 1 !important; }
649
+ }
650
+ </style>