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,343 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+ import { apiGet, apiPatch, isStaticMode } from '@/api/client'
4
+ import { useAuth } from '@/composables/useAuth'
5
+
6
+ const { authUser, isAuthenticated } = useAuth()
7
+
8
+ const token = computed(() => localStorage.getItem('spec-auth-token') || '')
9
+ const maskedToken = computed(() => token.value ? `${token.value.substring(0, 8)}...${token.value.substring(token.value.length - 4)}` : '')
10
+ const showFullToken = ref(false)
11
+
12
+ const copiedId = ref<string | null>(null)
13
+
14
+ // Webhook URL
15
+ const webhookUrl = ref('')
16
+ const savingWebhook = ref(false)
17
+ const webhookSaved = ref(false)
18
+
19
+ async function loadWebhookUrl() {
20
+ if (isStaticMode()) return
21
+ try {
22
+ const { data } = await apiGet<{ members: Array<{ webhook_url: string | null; display_name: string }> }>('/api/v2/admin/members')
23
+ if (data?.members) {
24
+ const me = data.members.find(m => m.display_name === authUser.value)
25
+ if (me?.webhook_url) webhookUrl.value = me.webhook_url
26
+ }
27
+ } catch (_) { /* ignore */ }
28
+ }
29
+
30
+ async function saveWebhookUrl() {
31
+ savingWebhook.value = true
32
+ webhookSaved.value = false
33
+ try {
34
+ const { data } = await apiGet<{ members: Array<{ id: number; display_name: string }> }>('/api/v2/admin/members')
35
+ const me = data?.members?.find(m => m.display_name === authUser.value)
36
+ if (me) {
37
+ await apiPatch(`/api/v2/admin/members/${me.id}`, { webhook_url: webhookUrl.value || null })
38
+ webhookSaved.value = true
39
+ setTimeout(() => { webhookSaved.value = false }, 3000)
40
+ }
41
+ } catch (_) { /* ignore */ }
42
+ savingWebhook.value = false
43
+ }
44
+
45
+ loadWebhookUrl()
46
+
47
+ function copyText(text: string, id: string) {
48
+ navigator.clipboard.writeText(text)
49
+ copiedId.value = id
50
+ setTimeout(() => { copiedId.value = null }, 2000)
51
+ }
52
+
53
+ const apiUrl = import.meta.env.VITE_API_URL as string
54
+
55
+ const claudeCodeConfig = computed(() => JSON.stringify({
56
+ mcpServers: {
57
+ 'mcp-pm': {
58
+ type: 'http',
59
+ url: `${apiUrl}/mcp`,
60
+ headers: {
61
+ Authorization: `Bearer ${token.value || '<your-token>'}`
62
+ }
63
+ }
64
+ }
65
+ }, null, 2))
66
+
67
+ const codexConfig = computed(() => {
68
+ const t = token.value || '<your-token>'
69
+ return `[mcp_servers.mcp-pm]
70
+ url = "${apiUrl}/mcp"
71
+ http_headers = { "Authorization" = "Bearer ${t}" }`
72
+ })
73
+
74
+ const activeTab = ref<'claude' | 'codex'>('claude')
75
+ </script>
76
+
77
+ <template>
78
+ <div class="my-page" v-if="isAuthenticated">
79
+ <h1>My Profile</h1>
80
+ <p class="subtitle">Hello, {{ authUser }}</p>
81
+
82
+ <!-- Token Section -->
83
+ <section class="card">
84
+ <h2>My Token</h2>
85
+ <p class="card-desc">Personal token used for authentication and MCP connections.</p>
86
+ <div class="token-row">
87
+ <code class="token-display" @click="showFullToken = !showFullToken">
88
+ {{ showFullToken ? token : maskedToken }}
89
+ </code>
90
+ <button class="btn btn--primary" @click="copyText(token, 'token')">{{ copiedId === 'token' ? 'Copied!' : 'Copy' }}</button>
91
+ </div>
92
+ <p class="hint">Click to show full token</p>
93
+ </section>
94
+
95
+ <!-- Webhook URL Section -->
96
+ <section class="card">
97
+ <h2>Notification Webhook URL</h2>
98
+ <p class="card-desc">Set a Discord/Slack webhook URL to receive memo/story notifications.</p>
99
+ <div class="token-row">
100
+ <input v-model="webhookUrl" class="webhook-input" placeholder="https://discord.com/api/webhooks/..." />
101
+ <button class="btn btn--primary" @click="saveWebhookUrl" :disabled="savingWebhook">
102
+ {{ savingWebhook ? 'Saving...' : 'Save' }}
103
+ </button>
104
+ </div>
105
+ <p v-if="webhookSaved" class="hint" style="color: #16a34a;">Saved successfully</p>
106
+ </section>
107
+
108
+ <!-- MCP Guide Section -->
109
+ <section class="card">
110
+ <h2>MCP Connection Guide</h2>
111
+ <p class="card-desc">Use PM tools (tasks, standup, memos, events) directly from your AI coding assistant.</p>
112
+
113
+ <!-- Tab Selector -->
114
+ <div class="tab-bar">
115
+ <button class="tab" :class="{ 'tab--active': activeTab === 'claude' }" @click="activeTab = 'claude'">Claude Code</button>
116
+ <button class="tab" :class="{ 'tab--active': activeTab === 'codex' }" @click="activeTab = 'codex'">Codex CLI</button>
117
+ </div>
118
+
119
+ <!-- Claude Code Tab -->
120
+ <div v-if="activeTab === 'claude'" class="tab-content">
121
+ <div class="step">
122
+ <span class="step-num">1</span>
123
+ <div class="step-body">
124
+ <p>Add the following to <code>.mcp.json</code> in your project root (or create it):</p>
125
+ <div class="code-block">
126
+ <button class="code-copy" @click="copyText(claudeCodeConfig, 'claude-config')">{{ copiedId === 'claude-config' ? 'Copied!' : 'Copy' }}</button>
127
+ <pre>{{ claudeCodeConfig }}</pre>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ <div class="step">
132
+ <span class="step-num">2</span>
133
+ <div class="step-body">
134
+ <p>Restart Claude Code to connect to the MCP server automatically.</p>
135
+ </div>
136
+ </div>
137
+ <div class="step">
138
+ <span class="step-num">3</span>
139
+ <div class="step-body">
140
+ <p>Verify connection:</p>
141
+ <div class="code-block">
142
+ <button class="code-copy" @click="copyText('claude &quot;Show my dashboard&quot;', 'claude-test')">{{ copiedId === 'claude-test' ? 'Copied!' : 'Copy' }}</button>
143
+ <pre>claude "Show my dashboard"</pre>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+
149
+ <!-- Codex Tab -->
150
+ <div v-if="activeTab === 'codex'" class="tab-content">
151
+ <div class="step">
152
+ <span class="step-num">1</span>
153
+ <div class="step-body">
154
+ <p>Add to <code>~/.codex/config.toml</code> (global) or project-level <code>.codex/config.toml</code>:</p>
155
+ <div class="code-block">
156
+ <button class="code-copy" @click="copyText(codexConfig, 'codex-config')">{{ copiedId === 'codex-config' ? 'Copied!' : 'Copy' }}</button>
157
+ <pre>{{ codexConfig }}</pre>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ <div class="step">
162
+ <span class="step-num">2</span>
163
+ <div class="step-body">
164
+ <p>Or add via CLI:</p>
165
+ <div class="code-block">
166
+ <button class="code-copy" @click="copyText(`codex mcp add mcp-pm --transport http --url ${apiUrl}/mcp`, 'codex-add')">{{ copiedId === 'codex-add' ? 'Copied!' : 'Copy' }}</button>
167
+ <pre>codex mcp add mcp-pm --transport http \
168
+ --url {{ apiUrl }}/mcp</pre>
169
+ </div>
170
+ <p>Then manually add the Authorization header to <code>~/.codex/config.toml</code> under <code>http_headers</code>.</p>
171
+ </div>
172
+ </div>
173
+ <div class="step">
174
+ <span class="step-num">3</span>
175
+ <div class="step-body">
176
+ <p>Verify connection:</p>
177
+ <div class="code-block">
178
+ <button class="code-copy" @click="copyText('codex &quot;Show my dashboard&quot;', 'codex-test')">{{ copiedId === 'codex-test' ? 'Copied!' : 'Copy' }}</button>
179
+ <pre>codex "Show my dashboard"</pre>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </div>
184
+
185
+ <!-- Common Notes -->
186
+ <div class="note-box">
187
+ <strong>Notes</strong>
188
+ <ul>
189
+ <li>Sprint is auto-detected (no manual setup required)</li>
190
+ <li>User name is auto-extracted from your token</li>
191
+ <li>DB credentials stay on the server only -- <code>.mcp.json</code> has no secrets</li>
192
+ <li>Add <code>.mcp.json</code> to <code>.gitignore</code> (contains personal token)</li>
193
+ </ul>
194
+ </div>
195
+ </section>
196
+
197
+ <!-- Available MCP Tools -->
198
+ <section class="card">
199
+ <h2>MCP Tools</h2>
200
+ <p class="card-desc">Tools your AI agent can call. Just use natural language and the agent will pick the right tool.</p>
201
+
202
+ <table class="tool-table">
203
+ <thead>
204
+ <tr>
205
+ <th>Tool</th>
206
+ <th>Description</th>
207
+ <th>Example</th>
208
+ </tr>
209
+ </thead>
210
+ <tbody>
211
+ <tr class="tool-group-row"><td colspan="3">Dashboard &amp; Navigation</td></tr>
212
+ <tr><td><code>my_dashboard</code></td><td>Overview of your tasks, unread memos, notifications, and standup status</td><td class="tool-example">"Show my status"</td></tr>
213
+ <tr><td><code>list_sprints</code></td><td>List all sprints (active sprint highlighted)</td><td class="tool-example">"Sprint list"</td></tr>
214
+ <tr><td><code>activate_sprint</code></td><td>Activate a sprint (deactivates others)</td><td class="tool-example">"Activate sprint s55"</td></tr>
215
+ <tr><td><code>sprint_summary</code></td><td>Progress by epic, workload by member, blockers</td><td class="tool-example">"Sprint summary"</td></tr>
216
+ <tr><td><code>list_team_members</code></td><td>List active team members</td><td class="tool-example">"Who's on the team?"</td></tr>
217
+
218
+ <tr class="tool-group-row"><td colspan="3">Epics</td></tr>
219
+ <tr><td><code>list_epics</code></td><td>All epics with story counts</td><td class="tool-example">"Show epics"</td></tr>
220
+ <tr><td><code>add_epic</code></td><td>Create a new epic</td><td class="tool-example">"Add epic: AI Diagnostics"</td></tr>
221
+ <tr><td><code>update_epic</code></td><td>Update epic (title, description, status, owner)</td><td class="tool-example">"Set epic 3 to completed"</td></tr>
222
+ <tr><td><code>delete_epic</code></td><td>Delete epic (child stories get epic_id=null)</td><td class="tool-example">"Delete epic 5"</td></tr>
223
+
224
+ <tr class="tool-group-row"><td colspan="3">Stories</td></tr>
225
+ <tr><td><code>list_stories</code></td><td>Stories with sprint/epic/status/assignee filters</td><td class="tool-example">"s55 stories"</td></tr>
226
+ <tr><td><code>add_story</code></td><td>Create a story under an epic</td><td class="tool-example">"Add story to epic 2: API Design"</td></tr>
227
+ <tr><td><code>update_story</code></td><td>Update story (title, status, assignee, points)</td><td class="tool-example">"Set story 10 to in-progress"</td></tr>
228
+ <tr><td><code>delete_story</code></td><td>Delete story and its tasks</td><td class="tool-example">"Delete story 15"</td></tr>
229
+
230
+ <tr class="tool-group-row"><td colspan="3">Tasks</td></tr>
231
+ <tr><td><code>list_my_tasks</code></td><td>Task tree: epic > story > task, filterable by status</td><td class="tool-example">"In-progress tasks only"</td></tr>
232
+ <tr><td><code>get_task</code></td><td>Task details + parent story + sibling tasks</td><td class="tool-example">"Task 42 details"</td></tr>
233
+ <tr><td><code>update_task_status</code></td><td>Change status (todo > in-progress > done)</td><td class="tool-example">"Mark task 42 done"</td></tr>
234
+ <tr><td><code>update_task</code></td><td>Update task (title, assignee, status, description)</td><td class="tool-example">"Assign task 42 to Alex"</td></tr>
235
+ <tr><td><code>add_task</code></td><td>Add task under a story; auto-assigns to you if unspecified</td><td class="tool-example">"Add task to story 10: API integration"</td></tr>
236
+ <tr><td><code>delete_task</code></td><td>Delete a task</td><td class="tool-example">"Delete task 50"</td></tr>
237
+
238
+ <tr class="tool-group-row"><td colspan="3">Standup</td></tr>
239
+ <tr><td><code>get_standup</code></td><td>Get standup for a date (default: today)</td><td class="tool-example">"Yesterday's standup"</td></tr>
240
+ <tr><td><code>save_standup</code></td><td>Save today's standup (done/planned/blockers); overwrites if exists</td><td class="tool-example">"Standup: done API, plan FE integration"</td></tr>
241
+ <tr><td><code>list_standup_entries</code></td><td>Standup entries (sprint/date filter)</td><td class="tool-example">"This sprint's standups"</td></tr>
242
+
243
+ <tr class="tool-group-row"><td colspan="3">Memos</td></tr>
244
+ <tr><td><code>send_memo</code></td><td>Send memo to team members (comma-separated recipients)</td><td class="tool-example">"Send API spec review request to Alex, Jordan"</td></tr>
245
+ <tr><td><code>list_my_memos</code></td><td>My memos; filter for unread</td><td class="tool-example">"Any unread memos?"</td></tr>
246
+ <tr><td><code>read_memo</code></td><td>Read memo + replies; marks as read</td><td class="tool-example">"Read memo 15"</td></tr>
247
+ <tr><td><code>reply_memo</code></td><td>Reply to a memo</td><td class="tool-example">"Reply to memo 15: acknowledged"</td></tr>
248
+ <tr><td><code>resolve_memo</code></td><td>Mark memo as resolved</td><td class="tool-example">"Resolve memo 15"</td></tr>
249
+
250
+ <tr class="tool-group-row"><td colspan="3">Retrospective</td></tr>
251
+ <tr><td><code>get_retro_session</code></td><td>Retro session with items + actions</td><td class="tool-example">"Show this sprint's retro"</td></tr>
252
+ <tr><td><code>add_retro_item</code></td><td>Add retro item (keep/problem/try)</td><td class="tool-example">"Add to keep: great code reviews"</td></tr>
253
+ <tr><td><code>vote_retro_item</code></td><td>Vote/unvote retro item</td><td class="tool-example">"Vote for item 3"</td></tr>
254
+ <tr><td><code>change_retro_phase</code></td><td>Change phase (collect > vote > discuss > action > done)</td><td class="tool-example">"Move retro to vote phase"</td></tr>
255
+ <tr><td><code>add_retro_action</code></td><td>Add retro action item</td><td class="tool-example">"Action: PR reviews within 24h"</td></tr>
256
+ <tr><td><code>update_retro_action_status</code></td><td>Update action status (todo > in-progress > done)</td><td class="tool-example">"Complete action 2"</td></tr>
257
+ <tr><td><code>export_retro</code></td><td>Export retro summary (keep/problem/try + actions + votes)</td><td class="tool-example">"Export retro results"</td></tr>
258
+
259
+ <tr class="tool-group-row"><td colspan="3">Agent Events (Push-Native)</td></tr>
260
+ <tr><td><code>emit_event</code></td><td>Emit event to agent/user; delivered via SSE in real-time</td><td class="tool-example">"Notify Oscar about sprint"</td></tr>
261
+ <tr><td><code>poll_events</code></td><td>Poll pending events (fallback for non-SSE clients)</td><td class="tool-example">"Check my events"</td></tr>
262
+ <tr><td><code>ack_event</code></td><td>Acknowledge event; unacked events get retried</td><td class="tool-example">"Ack event 3"</td></tr>
263
+
264
+ <tr class="tool-group-row"><td colspan="3">Notifications</td></tr>
265
+ <tr><td><code>check_notifications</code></td><td>Check notifications (unread first)</td><td class="tool-example">"Check notifications"</td></tr>
266
+ <tr><td><code>mark_notification_read</code></td><td>Mark notification as read</td><td class="tool-example">"Mark notification 5 read"</td></tr>
267
+ <tr><td><code>mark_all_notifications_read</code></td><td>Mark all notifications read</td><td class="tool-example">"Mark all read"</td></tr>
268
+ </tbody>
269
+ </table>
270
+ </section>
271
+ </div>
272
+
273
+ <!-- Not Authenticated -->
274
+ <div class="my-page" v-else>
275
+ <div class="card" style="text-align: center; padding: 48px 24px;">
276
+ <h2>Login Required</h2>
277
+ <p class="card-desc">Access via a token link or contact your administrator.</p>
278
+ </div>
279
+ </div>
280
+ </template>
281
+
282
+ <style scoped>
283
+ .my-page { max-width: 720px; margin: 0 auto; padding: 32px 24px; height: calc(100vh - var(--header-height, 48px)); overflow-y: auto; }
284
+ h1 { font-size: 24px; font-weight: 700; color: #1e293b; margin-bottom: 4px; }
285
+ .subtitle { font-size: 14px; color: #64748b; margin-bottom: 28px; }
286
+
287
+ .card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 24px; margin-bottom: 20px; min-width: 0; }
288
+ .card h2 { font-size: 16px; font-weight: 600; color: #1e293b; margin-bottom: 4px; }
289
+ .card-desc { font-size: 13px; color: #94a3b8; margin-bottom: 16px; }
290
+
291
+ /* Token */
292
+ .token-row { display: flex; gap: 8px; align-items: center; }
293
+ .token-display { flex: 1; padding: 10px 14px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; font-family: monospace; font-size: 14px; color: #334155; cursor: pointer; user-select: all; word-break: break-all; }
294
+ .token-display:hover { background: #f1f5f9; }
295
+ .webhook-input { flex: 1; padding: 8px 12px; border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; font-size: 13px; font-family: monospace; }
296
+ .hint { font-size: 11px; color: #94a3b8; margin-top: 6px; }
297
+
298
+ /* Tabs */
299
+ .tab-bar { display: flex; gap: 0; border-bottom: 2px solid #e2e8f0; margin-bottom: 20px; }
300
+ .tab { padding: 8px 20px; border: none; background: none; cursor: pointer; font-size: 13px; font-weight: 500; color: #94a3b8; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.15s; }
301
+ .tab:hover { color: #475569; }
302
+ .tab--active { color: #1e293b; border-bottom-color: #1e293b; }
303
+
304
+ /* Steps */
305
+ .step { display: flex; gap: 12px; margin-bottom: 20px; }
306
+ .step-num { flex-shrink: 0; width: 24px; height: 24px; border-radius: 50%; background: #1e293b; color: #fff; font-size: 12px; font-weight: 700; display: flex; align-items: center; justify-content: center; margin-top: 2px; }
307
+ .step-body { flex: 1; min-width: 0; }
308
+ .step-body p { font-size: 13px; color: #475569; margin-bottom: 8px; line-height: 1.5; }
309
+ .step-body code { background: #f1f5f9; padding: 1px 6px; border-radius: 4px; font-size: 12px; }
310
+
311
+ .code-block { position: relative; background: #1e293b; border-radius: 8px; padding: 16px; overflow: auto; max-width: 100%; }
312
+ .code-block pre { margin: 0; color: #e2e8f0; font-family: monospace; font-size: 12px; line-height: 1.6; white-space: pre; }
313
+ .code-copy { position: absolute; top: 8px; right: 8px; padding: 4px 10px; border: 1px solid #475569; border-radius: 4px; background: #334155; color: #94a3b8; font-size: 11px; cursor: pointer; transition: all 0.15s; }
314
+ .code-copy:hover { background: #475569; color: #fff; }
315
+
316
+ /* Note */
317
+ .note-box { margin-top: 20px; padding: 14px 18px; background: #fffbeb; border: 1px solid #fde68a; border-radius: 8px; font-size: 13px; color: #92400e; }
318
+ .note-box strong { display: block; margin-bottom: 6px; }
319
+ .note-box ul { margin: 0; padding-left: 18px; }
320
+ .note-box li { margin-bottom: 4px; line-height: 1.5; }
321
+ .note-box code { background: #fef3c7; padding: 1px 4px; border-radius: 3px; font-size: 11px; }
322
+
323
+ /* Tools table */
324
+ .tool-table { width: 100%; border-collapse: collapse; font-size: 13px; }
325
+ .tool-table 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; }
326
+ .tool-table td { padding: 8px 10px; border-bottom: 1px solid #f1f5f9; color: #475569; vertical-align: top; }
327
+ .tool-table code { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; font-family: monospace; font-size: 11px; color: #1e293b; white-space: nowrap; }
328
+ .tool-table tr:hover td { background: #f8fafc; }
329
+ .tool-group-row td { font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; padding-top: 14px; border-bottom: 1px solid #e2e8f0; background: none !important; }
330
+ .tool-example { font-size: 12px; color: #94a3b8; font-style: italic; white-space: nowrap; }
331
+
332
+ /* Buttons */
333
+ .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; }
334
+ .btn:hover { background: #f1f5f9; }
335
+ .btn--primary { background: #1e293b; color: #fff; border-color: #1e293b; }
336
+ .btn--primary:hover { background: #334155; }
337
+
338
+ @media (max-width: 767px) {
339
+ .my-page { padding: 16px; }
340
+ .token-row { flex-direction: column; }
341
+ .tab { padding: 8px 14px; font-size: 12px; }
342
+ }
343
+ </style>
@@ -0,0 +1,266 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted } from 'vue'
3
+ import { apiGet, apiPost, apiPatch, isStaticMode } from '@/api/client'
4
+
5
+ interface Reward {
6
+ id: number; member_name: string; type: 'reward' | 'penalty'
7
+ amount: number; reason: string; status: string; issued_by: string
8
+ tx_hash: string | null; paid_at: string | null; created_at: string
9
+ }
10
+ interface Summary {
11
+ member_name: string; total_rewards: number; total_penalties: number
12
+ balance: number; pending_balance: number
13
+ }
14
+ interface Member {
15
+ id: number; display_name: string; wallet_address: string | null
16
+ }
17
+
18
+ const rewards = ref<Reward[]>([])
19
+ const summary = ref<Summary[]>([])
20
+ const members = ref<Member[]>([])
21
+ const loading = ref(true)
22
+ const filterMember = ref('')
23
+ const activeTab = ref<'pending' | 'all'>('pending')
24
+
25
+ // Create form
26
+ const showForm = ref(false)
27
+ const formMember = ref('')
28
+ const formType = ref<'reward' | 'penalty'>('reward')
29
+ const formAmount = ref(0)
30
+ const formReason = ref('')
31
+
32
+ // Wallet editing
33
+ const editingWallet = ref<number | null>(null)
34
+ const walletInput = ref('')
35
+
36
+ // Payment
37
+ const selectedIds = ref<Set<number>>(new Set())
38
+ const txHashInput = ref('')
39
+
40
+ const pendingRewards = computed(() => rewards.value.filter(r => r.status === 'pending'))
41
+
42
+ const filteredRewards = computed(() => {
43
+ let list = activeTab.value === 'pending' ? pendingRewards.value : rewards.value
44
+ if (filterMember.value) list = list.filter(r => r.member_name === filterMember.value)
45
+ return list
46
+ })
47
+
48
+ async function loadData() {
49
+ if (isStaticMode()) { loading.value = false; return }
50
+ loading.value = true
51
+ const [rewardsRes, summaryRes, membersRes] = await Promise.all([
52
+ apiGet('/api/v2/rewards'),
53
+ apiGet('/api/v2/rewards/summary'),
54
+ apiGet('/api/v2/admin/members'),
55
+ ])
56
+ if (rewardsRes.data?.rewards) rewards.value = rewardsRes.data.rewards as Reward[]
57
+ if (summaryRes.data?.summary) summary.value = summaryRes.data.summary as Summary[]
58
+ if (membersRes.data?.members) members.value = (membersRes.data.members as Member[]).filter((m: any) => m.is_active)
59
+ loading.value = false
60
+ }
61
+
62
+ async function addReward() {
63
+ if (!formMember.value || !formAmount.value || !formReason.value) return
64
+ await apiPost('/api/v2/rewards', { memberName: formMember.value, type: formType.value, amount: formAmount.value, reason: formReason.value })
65
+ showForm.value = false; formMember.value = ''; formAmount.value = 0; formReason.value = ''
66
+ await loadData()
67
+ }
68
+
69
+ async function paySelected() {
70
+ for (const id of selectedIds.value) {
71
+ await apiPatch(`/api/v2/rewards/${id}/pay`, { txHash: txHashInput.value || null })
72
+ }
73
+ selectedIds.value.clear(); txHashInput.value = ''
74
+ await loadData()
75
+ }
76
+
77
+ async function batchPay(memberName: string) {
78
+ await apiPatch('/api/v2/rewards/batch-pay', { memberName, txHash: txHashInput.value || null })
79
+ txHashInput.value = ''
80
+ await loadData()
81
+ }
82
+
83
+ function startEditWallet(m: Member) {
84
+ editingWallet.value = m.id; walletInput.value = m.wallet_address || ''
85
+ }
86
+
87
+ async function saveWallet(memberId: number) {
88
+ await apiPatch(`/api/v2/admin/members/${memberId}`, { wallet_address: walletInput.value })
89
+ editingWallet.value = null
90
+ await loadData()
91
+ }
92
+
93
+ function toggleSelect(id: number) {
94
+ if (selectedIds.value.has(id)) selectedIds.value.delete(id)
95
+ else selectedIds.value.add(id)
96
+ }
97
+
98
+ function formatDate(d: string) {
99
+ if (!d) return ''
100
+ const date = new Date(d.endsWith('Z') ? d : d + 'Z')
101
+ return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
102
+ }
103
+
104
+ function getMemberWallet(name: string) {
105
+ return members.value.find(m => m.display_name === name)?.wallet_address || ''
106
+ }
107
+
108
+ function copyAddr(addr: string | null) {
109
+ if (!addr) return
110
+ navigator.clipboard.writeText(addr)
111
+ }
112
+
113
+ onMounted(loadData)
114
+ </script>
115
+
116
+ <template>
117
+ <div class="rewards-page">
118
+ <div class="rewards-header">
119
+ <h1>Rewards &amp; Penalties</h1>
120
+ <button class="btn-add" @click="showForm = !showForm">{{ showForm ? 'Cancel' : '+ Add Entry' }}</button>
121
+ </div>
122
+
123
+ <!-- Create form -->
124
+ <div v-if="showForm" class="form-card">
125
+ <div class="form-row">
126
+ <select v-model="formMember" class="input">
127
+ <option value="">Select Member</option>
128
+ <option v-for="m in members" :key="m.id" :value="m.display_name">{{ m.display_name }}</option>
129
+ </select>
130
+ <select v-model="formType" class="input">
131
+ <option value="reward">Reward (+)</option>
132
+ <option value="penalty">Penalty (-)</option>
133
+ </select>
134
+ <input v-model.number="formAmount" class="input" type="number" placeholder="Amount" />
135
+ </div>
136
+ <input v-model="formReason" class="input full" placeholder="Reason" @keydown.enter="addReward" />
137
+ <button class="btn-submit" @click="addReward" :disabled="!formMember || !formAmount || !formReason">Submit</button>
138
+ </div>
139
+
140
+ <!-- Member balance cards -->
141
+ <section class="summary-section">
142
+ <h2>Balance by Member</h2>
143
+ <div class="summary-grid">
144
+ <div v-for="s in summary" :key="s.member_name" class="summary-card">
145
+ <div class="summary-top">
146
+ <div class="summary-name">{{ s.member_name }}</div>
147
+ <div class="summary-balance" :class="{ negative: s.balance < 0 }">{{ s.balance.toLocaleString() }}</div>
148
+ </div>
149
+ <div class="summary-wallet">
150
+ <template v-if="editingWallet === members.find(m => m.display_name === s.member_name)?.id">
151
+ <input v-model="walletInput" class="wallet-input" placeholder="Wallet address" @keydown.enter="saveWallet(members.find(m => m.display_name === s.member_name)!.id)" />
152
+ <button class="btn-sm" @click="saveWallet(members.find(m => m.display_name === s.member_name)!.id)">Save</button>
153
+ </template>
154
+ <template v-else>
155
+ <span class="wallet-addr" @click="copyAddr(getMemberWallet(s.member_name))" :title="getMemberWallet(s.member_name)">{{ getMemberWallet(s.member_name)?.slice(0,12) || 'Not set' }}...</span>
156
+ <button class="btn-edit" @click="startEditWallet(members.find(m => m.display_name === s.member_name)!)">Edit</button>
157
+ </template>
158
+ </div>
159
+ <div class="summary-detail">
160
+ <span class="reward-text">+{{ s.total_rewards.toLocaleString() }}</span>
161
+ <span class="penalty-text">-{{ s.total_penalties.toLocaleString() }}</span>
162
+ <span v-if="s.pending_balance" class="pending-text">Pending: {{ s.pending_balance.toLocaleString() }}</span>
163
+ </div>
164
+ <button v-if="s.pending_balance > 0" class="btn-batch-pay" @click="batchPay(s.member_name)">Pay All</button>
165
+ </div>
166
+ </div>
167
+ </section>
168
+
169
+ <!-- Tabs -->
170
+ <div class="tab-row">
171
+ <button :class="['tab-btn', { active: activeTab === 'pending' }]" @click="activeTab = 'pending'">Pending</button>
172
+ <button :class="['tab-btn', { active: activeTab === 'all' }]" @click="activeTab = 'all'">All</button>
173
+ <select v-model="filterMember" class="input filter-select">
174
+ <option value="">All Members</option>
175
+ <option v-for="s in summary" :key="s.member_name" :value="s.member_name">{{ s.member_name }}</option>
176
+ </select>
177
+ </div>
178
+
179
+ <!-- Batch pay bar -->
180
+ <div v-if="selectedIds.size > 0 && activeTab === 'pending'" class="pay-bar">
181
+ <span>{{ selectedIds.size }} selected</span>
182
+ <input v-model="txHashInput" class="input" placeholder="TX hash (optional)" />
183
+ <button class="btn-pay" @click="paySelected">Pay Selected</button>
184
+ </div>
185
+
186
+ <!-- List -->
187
+ <div v-if="loading" class="loading">Loading...</div>
188
+ <div v-else-if="!filteredRewards.length" class="empty">No entries found.</div>
189
+ <div v-else class="rewards-list">
190
+ <div v-for="r in filteredRewards" :key="r.id" class="reward-item" :class="'type--' + r.type">
191
+ <input v-if="activeTab === 'pending'" type="checkbox" :checked="selectedIds.has(r.id)" @change="toggleSelect(r.id)" class="reward-check" />
192
+ <div class="reward-main">
193
+ <span class="reward-type-badge">{{ r.type === 'reward' ? 'Reward' : 'Penalty' }}</span>
194
+ <span class="reward-member">{{ r.member_name }}</span>
195
+ <span class="reward-amount" :class="r.type">{{ r.type === 'reward' ? '+' : '-' }}{{ r.amount.toLocaleString() }}</span>
196
+ <span class="reward-status" :class="'st--' + r.status">{{ r.status === 'paid' ? 'Paid' : 'Pending' }}</span>
197
+ </div>
198
+ <div class="reward-reason">{{ r.reason }}</div>
199
+ <div class="reward-meta">
200
+ <span>{{ r.issued_by }}</span>
201
+ <span>{{ formatDate(r.created_at) }}</span>
202
+ <a v-if="r.tx_hash" class="tx-hash" :href="`https://tronscan.org/#/transaction/${r.tx_hash}`" target="_blank">TX: {{ r.tx_hash.slice(0, 12) }}...</a>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </template>
208
+
209
+ <style scoped>
210
+ .rewards-page { max-width: 800px; margin: 0 auto; padding: 24px 16px; }
211
+ .rewards-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
212
+ .rewards-header h1 { font-size: 20px; font-weight: 700; }
213
+ .btn-add { background: #3b82f6; color: #fff; border: none; border-radius: 10px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; }
214
+
215
+ .form-card { background: var(--card-bg, #fff); border-radius: 16px; padding: 16px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
216
+ .form-row { display: flex; gap: 8px; margin-bottom: 8px; }
217
+ .input { border: 1px solid rgba(0,0,0,0.15); border-radius: 8px; padding: 8px 12px; font-size: 13px; }
218
+ .input.full { width: 100%; margin-bottom: 8px; box-sizing: border-box; }
219
+ .btn-submit { background: #22c55e; color: #fff; border: none; border-radius: 8px; padding: 8px 16px; font-size: 13px; cursor: pointer; }
220
+
221
+ .summary-section { margin-bottom: 24px; }
222
+ .summary-section h2 { font-size: 16px; font-weight: 600; margin-bottom: 12px; }
223
+ .summary-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
224
+ .summary-card { background: var(--card-bg, #fff); border-radius: 12px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
225
+ .summary-top { display: flex; justify-content: space-between; align-items: center; }
226
+ .summary-name { font-size: 14px; font-weight: 600; }
227
+ .summary-balance { font-size: 18px; font-weight: 700; color: #22c55e; }
228
+ .summary-balance.negative { color: #ef4444; }
229
+ .summary-wallet { display: flex; align-items: center; gap: 4px; margin-top: 8px; font-size: 11px; color: var(--text-muted, #888); }
230
+ .wallet-input { flex: 1; font-size: 11px; padding: 4px 6px; border: 1px solid #ddd; border-radius: 4px; }
231
+ .wallet-addr { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 160px; cursor: pointer; }
232
+ .wallet-addr:hover { color: #3b82f6; }
233
+ .btn-edit { background: none; border: none; color: #3b82f6; font-size: 11px; cursor: pointer; }
234
+ .btn-sm { background: #3b82f6; color: #fff; border: none; border-radius: 4px; padding: 2px 6px; font-size: 10px; cursor: pointer; }
235
+ .summary-detail { font-size: 11px; color: var(--text-muted, #888); margin-top: 4px; display: flex; gap: 8px; }
236
+ .reward-text { color: #22c55e; } .penalty-text { color: #ef4444; } .pending-text { color: #f59e0b; }
237
+ .btn-batch-pay { width: 100%; margin-top: 8px; background: #3b82f6; color: #fff; border: none; border-radius: 8px; padding: 6px; font-size: 12px; cursor: pointer; }
238
+
239
+ .tab-row { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
240
+ .tab-btn { background: #f3f4f6; border: none; border-radius: 8px; padding: 6px 14px; font-size: 13px; cursor: pointer; }
241
+ .tab-btn.active { background: #3b82f6; color: #fff; }
242
+ .filter-select { margin-left: auto; }
243
+
244
+ .pay-bar { display: flex; gap: 8px; align-items: center; background: #eff6ff; padding: 8px 12px; border-radius: 8px; margin-bottom: 12px; font-size: 13px; }
245
+ .btn-pay { background: #22c55e; color: #fff; border: none; border-radius: 6px; padding: 6px 12px; font-size: 12px; cursor: pointer; }
246
+
247
+ .rewards-list { display: flex; flex-direction: column; gap: 6px; }
248
+ .reward-item { display: flex; align-items: flex-start; gap: 8px; background: var(--card-bg, #fff); border-radius: 10px; padding: 10px 14px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); flex-wrap: wrap; }
249
+ .type--reward { border-left: 3px solid #22c55e; } .type--penalty { border-left: 3px solid #ef4444; }
250
+ .reward-check { margin-top: 4px; }
251
+ .reward-main { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
252
+ .reward-type-badge { font-size: 10px; font-weight: 600; padding: 1px 5px; border-radius: 4px; }
253
+ .type--reward .reward-type-badge { background: #dcfce7; color: #16a34a; }
254
+ .type--penalty .reward-type-badge { background: #fef2f2; color: #dc2626; }
255
+ .reward-member { font-weight: 600; font-size: 13px; }
256
+ .reward-amount { font-weight: 700; font-size: 14px; margin-left: auto; }
257
+ .reward-amount.reward { color: #22c55e; } .reward-amount.penalty { color: #ef4444; }
258
+ .reward-status { font-size: 10px; padding: 1px 5px; border-radius: 4px; }
259
+ .st--pending { background: #fef3c7; color: #d97706; } .st--paid { background: #dcfce7; color: #16a34a; }
260
+ .reward-reason { font-size: 12px; color: var(--text-secondary, #666); width: 100%; }
261
+ .reward-meta { display: flex; gap: 8px; font-size: 11px; color: var(--text-muted, #888); width: 100%; }
262
+ .tx-hash { font-family: monospace; font-size: 10px; color: #3b82f6; text-decoration: none; }
263
+ .tx-hash:hover { text-decoration: underline; }
264
+ .loading, .empty { text-align: center; color: var(--text-muted, #888); padding: 40px; }
265
+ @media (max-width: 640px) { .form-row { flex-direction: column; } .summary-grid { grid-template-columns: 1fr; } .tab-row { flex-wrap: wrap; } }
266
+ </style>