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,299 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted, computed } from 'vue'
3
+ import { apiGet, apiPost, apiPatch, apiDelete, apiPut } from '@/api/client'
4
+
5
+ interface MemberRow {
6
+ id: number
7
+ display_name: string
8
+ email: string | null
9
+ role: string
10
+ is_active: number
11
+ created_at: string
12
+ updated_at: string
13
+ }
14
+
15
+ const members = ref<MemberRow[]>([])
16
+ const loading = ref(true)
17
+ const error = ref('')
18
+
19
+ // New member form
20
+ const newName = ref('')
21
+ const newEmail = ref('')
22
+ const newTtlDays = ref<number | null>(null)
23
+
24
+ // Status message
25
+ const statusMsg = ref('')
26
+
27
+ // LLM settings
28
+ const llmApiKey = ref('')
29
+ const llmProvider = ref('openai')
30
+ const llmModel = ref('')
31
+ const settingsSaved = ref(false)
32
+
33
+ async function loadSettings() {
34
+ const { data } = await apiGet<{ settings: Record<string, string> }>('/api/v2/admin/settings')
35
+ if (data?.settings) {
36
+ llmApiKey.value = data.settings.llm_api_key ?? ''
37
+ llmProvider.value = data.settings.llm_provider ?? 'openai'
38
+ llmModel.value = data.settings.llm_model ?? ''
39
+ }
40
+ }
41
+
42
+ async function saveSettings() {
43
+ settingsSaved.value = false
44
+ await apiPut('/api/v2/admin/settings/llm_api_key', { value: llmApiKey.value || null })
45
+ await apiPut('/api/v2/admin/settings/llm_provider', { value: llmProvider.value || null })
46
+ await apiPut('/api/v2/admin/settings/llm_model', { value: llmModel.value || null })
47
+ settingsSaved.value = true
48
+ setTimeout(() => { settingsSaved.value = false }, 3000)
49
+ }
50
+
51
+ async function clearApiKey() {
52
+ await apiPut('/api/v2/admin/settings/llm_api_key', { value: null })
53
+ llmApiKey.value = ''
54
+ settingsSaved.value = true
55
+ setTimeout(() => { settingsSaved.value = false }, 3000)
56
+ }
57
+
58
+ loadSettings()
59
+
60
+ async function loadMembers() {
61
+ loading.value = true
62
+ const { data, error: apiError } = await apiGet<{ members: MemberRow[] }>('/api/v2/admin/members')
63
+ if (apiError) {
64
+ error.value = apiError
65
+ } else if (data) {
66
+ members.value = data.members
67
+ }
68
+ loading.value = false
69
+ }
70
+
71
+ function generateToken(): string {
72
+ return crypto.randomUUID()
73
+ }
74
+
75
+ async function addMember() {
76
+ const name = newName.value.trim()
77
+ if (!name) return
78
+ const token = generateToken()
79
+ const email = newEmail.value.trim() || null
80
+ const ttl = newTtlDays.value
81
+ const body: Record<string, unknown> = { token, userName: name, userEmail: email }
82
+ if (ttl && Number.isInteger(ttl) && ttl > 0 && ttl <= 3650) body.ttlDays = ttl
83
+ const { error: apiError } = await apiPost('/api/v2/admin/members', body)
84
+ if (apiError) {
85
+ statusMsg.value = `Error: ${apiError}`
86
+ } else {
87
+ statusMsg.value = `${name} added`
88
+ newName.value = ''
89
+ newEmail.value = ''
90
+ newTtlDays.value = null
91
+ await loadMembers()
92
+ }
93
+ clearStatus()
94
+ }
95
+
96
+ async function revokeToken(id: string, name: string) {
97
+ if (!confirm(`Revoke token for ${name}?`)) return
98
+ const { error: apiError } = await apiPatch(`/api/v2/admin/members/${id}/revoke`, {})
99
+ if (!apiError) { statusMsg.value = `${name} token revoked`; await loadMembers() }
100
+ clearStatus()
101
+ }
102
+
103
+ async function reactivateToken(id: string, name: string) {
104
+ const { error: apiError } = await apiPatch(`/api/v2/admin/members/${id}/activate`, {})
105
+ if (!apiError) { statusMsg.value = `${name} token reactivated`; await loadMembers() }
106
+ clearStatus()
107
+ }
108
+
109
+ async function regenerateToken(oldToken: string, name: string) {
110
+ if (!confirm(`Regenerate token for ${name}? The old token will be invalidated.`)) return
111
+ const newToken = generateToken()
112
+ const { error: apiError } = await apiPost(`/api/v2/admin/members/${oldToken}/regenerate`, { newToken })
113
+ if (!apiError) { statusMsg.value = `${name} token regenerated`; await loadMembers() }
114
+ clearStatus()
115
+ }
116
+
117
+ async function deleteMember(id: string, name: string) {
118
+ if (!confirm(`Permanently delete ${name}? This cannot be undone.`)) return
119
+ const { error: apiError } = await apiDelete(`/api/v2/admin/members/${id}`)
120
+ if (!apiError) { statusMsg.value = `${name} deleted`; await loadMembers() }
121
+ clearStatus()
122
+ }
123
+
124
+ function clearStatus() {
125
+ setTimeout(() => { statusMsg.value = '' }, 3000)
126
+ }
127
+
128
+ function formatDate(d: string | null): string {
129
+ if (!d) return '-'
130
+ return d.replace('T', ' ').substring(0, 16)
131
+ }
132
+
133
+ const activeCount = computed(() => members.value.filter(m => m.is_active).length)
134
+ const totalCount = computed(() => members.value.length)
135
+
136
+ onMounted(loadMembers)
137
+ </script>
138
+
139
+ <template>
140
+ <div class="admin">
141
+ <div class="admin-header">
142
+ <div class="admin-header-row">
143
+ <div>
144
+ <h1>Team Token Management</h1>
145
+ <p class="admin-subtitle">This page is accessible only to administrators</p>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Status -->
151
+ <Transition name="fade">
152
+ <div v-if="statusMsg" class="admin-status">{{ statusMsg }}</div>
153
+ </Transition>
154
+
155
+ <!-- Stats -->
156
+ <div class="admin-stats">
157
+ <div class="stat">
158
+ <span class="stat-num">{{ activeCount }}</span>
159
+ <span class="stat-label">Active</span>
160
+ </div>
161
+ <div class="stat">
162
+ <span class="stat-num">{{ totalCount }}</span>
163
+ <span class="stat-label">Total</span>
164
+ </div>
165
+ </div>
166
+
167
+ <!-- Add Member -->
168
+ <div class="admin-card">
169
+ <h2>Add Member</h2>
170
+ <div class="add-form">
171
+ <input v-model="newName" class="input" placeholder="Name" />
172
+ <input v-model="newEmail" class="input" placeholder="Email (optional)" />
173
+ <input v-model.number="newTtlDays" class="input input--sm" type="number" placeholder="TTL (days)" min="1" />
174
+ <button class="btn btn--primary" @click="addMember" :disabled="!newName.trim()">Add</button>
175
+ </div>
176
+ <p class="add-hint">Leave TTL empty for unlimited</p>
177
+ </div>
178
+
179
+ <!-- Members Table -->
180
+ <div class="admin-card">
181
+ <h2>Members</h2>
182
+ <div v-if="loading" class="admin-loading">Loading...</div>
183
+ <div v-else-if="error" class="admin-error">{{ error }}</div>
184
+ <div v-else class="table-wrap">
185
+ <table>
186
+ <thead>
187
+ <tr>
188
+ <th>Status</th>
189
+ <th>Name</th>
190
+ <th>Email</th>
191
+ <th>Role</th>
192
+ <th>Created</th>
193
+ <th>Actions</th>
194
+ </tr>
195
+ </thead>
196
+ <tbody>
197
+ <tr v-for="m in members" :key="String(m.id)" :class="{ inactive: !m.is_active }">
198
+ <td>
199
+ <span class="badge" :class="m.is_active ? 'badge--active' : 'badge--revoked'">
200
+ {{ m.is_active ? 'active' : 'revoked' }}
201
+ </span>
202
+ </td>
203
+ <td class="td-name">{{ m.display_name }}</td>
204
+ <td class="td-email">{{ m.email || '-' }}</td>
205
+ <td>{{ m.role }}</td>
206
+ <td class="td-date">{{ formatDate(m.created_at) }}</td>
207
+ <td class="td-actions">
208
+ <button v-if="m.is_active" class="btn btn--sm btn--warn" @click="revokeToken(String(m.id), m.display_name)">Revoke</button>
209
+ <button v-else class="btn btn--sm btn--ok" @click="reactivateToken(String(m.id), m.display_name)">Activate</button>
210
+ <button class="btn btn--sm" @click="regenerateToken(String(m.id), m.display_name)">Regen</button>
211
+ <button class="btn btn--sm btn--danger" @click="deleteMember(String(m.id), m.display_name)">Delete</button>
212
+ </td>
213
+ </tr>
214
+ </tbody>
215
+ </table>
216
+ </div>
217
+ </div>
218
+
219
+ <!-- AI Settings -->
220
+ <div class="admin-section">
221
+ <h2>AI Settings (BYOM)</h2>
222
+ <div class="setting-row">
223
+ <label>API Key</label>
224
+ <input v-model="llmApiKey" type="password" class="setting-input" placeholder="sk-..." />
225
+ </div>
226
+ <div class="setting-row">
227
+ <label>Provider</label>
228
+ <select v-model="llmProvider" class="setting-input">
229
+ <option value="openai">OpenAI</option>
230
+ <option value="anthropic">Anthropic</option>
231
+ <option value="gemini">Gemini</option>
232
+ </select>
233
+ </div>
234
+ <div class="setting-row">
235
+ <label>Model</label>
236
+ <input v-model="llmModel" class="setting-input" placeholder="gpt-4o-mini" />
237
+ </div>
238
+ <div class="setting-actions">
239
+ <button class="btn btn--primary" @click="saveSettings">Save</button>
240
+ <button class="btn btn--danger" @click="clearApiKey">Clear API Key</button>
241
+ <span v-if="settingsSaved" class="save-ok">Saved</span>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </template>
246
+
247
+ <style scoped>
248
+ .admin { max-width: 1000px; margin: 0 auto; padding: 32px 24px; }
249
+ .admin-header { margin-bottom: 24px; }
250
+ .admin-header-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
251
+ .admin-header h1 { font-size: 24px; font-weight: 700; color: #1e293b; margin-bottom: 4px; }
252
+ .admin-subtitle { font-size: 13px; color: #94a3b8; }
253
+ .admin-status { background: #ecfdf5; border: 1px solid #a7f3d0; color: #065f46; padding: 10px 16px; border-radius: 8px; font-size: 13px; font-weight: 500; margin-bottom: 16px; }
254
+ .admin-stats { display: flex; gap: 16px; margin-bottom: 24px; }
255
+ .stat { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; padding: 16px 24px; display: flex; flex-direction: column; align-items: center; min-width: 80px; }
256
+ .stat-num { font-size: 28px; font-weight: 700; color: #1e293b; }
257
+ .stat-label { font-size: 12px; color: #94a3b8; margin-top: 2px; }
258
+ .admin-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px 24px; margin-bottom: 20px; }
259
+ .admin-card h2 { font-size: 16px; font-weight: 600; color: #1e293b; margin-bottom: 16px; }
260
+ .add-form { display: flex; gap: 8px; flex-wrap: wrap; }
261
+ .input { padding: 8px 12px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; flex: 1; min-width: 120px; }
262
+ .input--sm { max-width: 100px; flex: none; }
263
+ .input:focus { outline: none; border-color: #3b82f6; }
264
+ .add-hint { font-size: 11px; color: #94a3b8; margin-top: 8px; }
265
+ .btn { padding: 8px 16px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; background: #fff; color: #475569; white-space: nowrap; transition: all 0.15s; }
266
+ .btn:hover { background: #f1f5f9; }
267
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
268
+ .btn--primary { background: #1e293b; color: #fff; border-color: #1e293b; }
269
+ .btn--primary:hover { background: #334155; }
270
+ .btn--sm { padding: 4px 10px; font-size: 11px; }
271
+ .btn--warn { color: #f59e0b; border-color: #fcd34d; }
272
+ .btn--ok { color: #22c55e; border-color: #86efac; }
273
+ .btn--danger { color: #ef4444; border-color: #fca5a5; }
274
+ .table-wrap { overflow-x: auto; }
275
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
276
+ th { text-align: left; padding: 8px 10px; font-size: 11px; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid #e2e8f0; }
277
+ td { padding: 10px; border-bottom: 1px solid #f1f5f9; color: #475569; vertical-align: middle; }
278
+ tr.inactive td { opacity: 0.5; }
279
+ tr:hover td { background: #f8fafc; }
280
+ .td-name { font-weight: 600; color: #1e293b; }
281
+ .td-email { font-size: 12px; }
282
+ .td-date { font-size: 11px; color: #94a3b8; white-space: nowrap; }
283
+ .td-actions { display: flex; gap: 4px; flex-wrap: nowrap; }
284
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; text-transform: uppercase; }
285
+ .badge--active { background: #ecfdf5; color: #059669; }
286
+ .badge--revoked { background: #fef2f2; color: #dc2626; }
287
+ .admin-loading, .admin-error { padding: 20px; text-align: center; color: #94a3b8; font-size: 14px; }
288
+ .admin-error { color: #ef4444; }
289
+ .admin-section { margin-top: 32px; padding: 20px; background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; }
290
+ .admin-section h2 { font-size: 16px; font-weight: 600; margin-bottom: 16px; }
291
+ .setting-row { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
292
+ .setting-row label { width: 100px; font-size: 13px; font-weight: 500; color: #64748b; flex-shrink: 0; }
293
+ .setting-input { flex: 1; padding: 8px 12px; border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; font-size: 13px; }
294
+ .setting-actions { display: flex; gap: 8px; align-items: center; }
295
+ .save-ok { font-size: 12px; color: #16a34a; }
296
+ .fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
297
+ .fade-enter-from, .fade-leave-to { opacity: 0; }
298
+ @media (max-width: 767px) { .admin { padding: 16px; } .add-form { flex-direction: column; } .td-actions { flex-wrap: wrap; } }
299
+ </style>