@sienklogic/plan-build-run 2.28.0 → 2.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dashboard/public/css/command-center.css +382 -0
- package/dashboard/public/css/layout.css +11 -2
- package/dashboard/public/css/tokens.css +6 -3
- package/dashboard/src/components/Layout.tsx +6 -0
- package/dashboard/src/components/partials/ActivityStream.tsx +58 -0
- package/dashboard/src/components/partials/AttentionPanel.tsx +48 -0
- package/dashboard/src/components/partials/CurrentPhaseCard.tsx +50 -0
- package/dashboard/src/components/partials/PhaseTimeline.tsx +38 -0
- package/dashboard/src/components/partials/ProgressRing.tsx +51 -0
- package/dashboard/src/components/partials/QuickActions.tsx +30 -0
- package/dashboard/src/components/partials/StatusHeader.tsx +45 -0
- package/dashboard/src/index.tsx +2 -0
- package/dashboard/src/middleware/current-phase.ts +0 -2
- package/dashboard/src/routes/command-center.routes.tsx +69 -0
- package/dashboard/src/routes/index.routes.tsx +79 -9
- package/dashboard/src/services/dashboard.service.d.ts +39 -0
- package/dashboard/src/services/todo.service.d.ts +21 -0
- package/package.json +1 -1
- package/plugins/copilot-pbr/plugin.json +1 -1
- package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to Plan-Build-Run will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.29.0](https://github.com/SienkLogic/plan-build-run/compare/plan-build-run-v2.28.0...plan-build-run-v2.29.0) (2026-02-24)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **39-01:** add Command Center view with live-updating dashboard components ([7520c7f](https://github.com/SienkLogic/plan-build-run/commit/7520c7fd592beae5739cfd96aa73fd354047896d))
|
|
14
|
+
|
|
8
15
|
## [2.28.0](https://github.com/SienkLogic/plan-build-run/compare/plan-build-run-v2.27.2...plan-build-run-v2.28.0) (2026-02-24)
|
|
9
16
|
|
|
10
17
|
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
PBR Dashboard — Command Center View
|
|
3
|
+
Styles for progress ring, phase timeline,
|
|
4
|
+
attention panel, activity stream.
|
|
5
|
+
All values use tokens from tokens.css.
|
|
6
|
+
============================================ */
|
|
7
|
+
|
|
8
|
+
/* --- Command Center Layout --- */
|
|
9
|
+
.command-center {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
gap: var(--space-lg);
|
|
13
|
+
padding: var(--space-lg);
|
|
14
|
+
max-width: 1200px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.command-center__grid {
|
|
18
|
+
display: grid;
|
|
19
|
+
grid-template-columns: 1fr 1fr;
|
|
20
|
+
gap: var(--space-md);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@media (max-width: 768px) {
|
|
24
|
+
.command-center__grid {
|
|
25
|
+
grid-template-columns: 1fr;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* --- Status Header --- */
|
|
30
|
+
.status-header {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: var(--space-md);
|
|
34
|
+
flex-wrap: wrap;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.status-header__project {
|
|
38
|
+
font-size: 1.5rem;
|
|
39
|
+
font-weight: 700;
|
|
40
|
+
letter-spacing: -0.02em;
|
|
41
|
+
color: var(--color-text);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.status-header__phase {
|
|
45
|
+
font-size: 1rem;
|
|
46
|
+
color: var(--color-text-dim);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* --- Milestone Progress Bar --- */
|
|
50
|
+
.milestone-bar {
|
|
51
|
+
width: 100%;
|
|
52
|
+
margin-top: var(--space-sm);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.milestone-bar__track {
|
|
56
|
+
height: 8px;
|
|
57
|
+
border-radius: var(--radius-sm);
|
|
58
|
+
background: var(--color-border);
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.milestone-bar__fill {
|
|
63
|
+
height: 100%;
|
|
64
|
+
border-radius: var(--radius-sm);
|
|
65
|
+
background: var(--color-accent);
|
|
66
|
+
transition: width var(--transition-base);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.milestone-bar__label {
|
|
70
|
+
font-size: 0.8rem;
|
|
71
|
+
color: var(--color-text-muted);
|
|
72
|
+
margin-top: var(--space-xs);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* --- Progress Ring --- */
|
|
76
|
+
.progress-ring-wrapper {
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
padding: var(--space-sm);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.progress-ring__svg {
|
|
84
|
+
transform: rotate(-90deg);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.progress-ring__bg {
|
|
88
|
+
stroke: var(--color-border);
|
|
89
|
+
fill: none;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.progress-ring__fg {
|
|
93
|
+
stroke: var(--color-accent);
|
|
94
|
+
fill: none;
|
|
95
|
+
stroke-linecap: round;
|
|
96
|
+
transition: stroke-dashoffset var(--transition-base);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.progress-ring__text {
|
|
100
|
+
transform: rotate(90deg);
|
|
101
|
+
font-size: 1.25rem;
|
|
102
|
+
font-weight: 700;
|
|
103
|
+
fill: var(--color-text);
|
|
104
|
+
text-anchor: middle;
|
|
105
|
+
dominant-baseline: middle;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* --- Current Phase Card --- */
|
|
109
|
+
.current-phase-card {
|
|
110
|
+
grid-column: 1 / -1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.card {
|
|
114
|
+
background: var(--card-bg);
|
|
115
|
+
border: 1px solid var(--card-border);
|
|
116
|
+
border-radius: var(--card-radius);
|
|
117
|
+
box-shadow: var(--card-shadow);
|
|
118
|
+
padding: var(--card-padding);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.card__header {
|
|
122
|
+
font-size: 0.75rem;
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
text-transform: uppercase;
|
|
125
|
+
letter-spacing: 0.08em;
|
|
126
|
+
color: var(--color-text-muted);
|
|
127
|
+
padding-bottom: var(--space-sm);
|
|
128
|
+
border-bottom: 1px solid var(--color-border);
|
|
129
|
+
margin-bottom: var(--space-md);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.card__title {
|
|
133
|
+
font-size: 1.25rem;
|
|
134
|
+
font-weight: 600;
|
|
135
|
+
margin: 0 0 var(--space-sm) 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.card__meta {
|
|
139
|
+
font-size: 0.85rem;
|
|
140
|
+
color: var(--color-text-dim);
|
|
141
|
+
margin: var(--space-xs) 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* --- Next Action --- */
|
|
145
|
+
.next-action {
|
|
146
|
+
display: flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: var(--space-sm);
|
|
149
|
+
margin-top: var(--space-md);
|
|
150
|
+
padding: var(--space-sm) var(--space-md);
|
|
151
|
+
background: var(--color-surface);
|
|
152
|
+
border: 1px solid var(--color-border);
|
|
153
|
+
border-radius: var(--radius-sm);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.next-action__label {
|
|
157
|
+
font-size: 0.75rem;
|
|
158
|
+
color: var(--color-text-muted);
|
|
159
|
+
text-transform: uppercase;
|
|
160
|
+
letter-spacing: 0.06em;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.next-action__cmd {
|
|
164
|
+
font-family: var(--font-mono);
|
|
165
|
+
font-size: 0.9rem;
|
|
166
|
+
color: var(--color-accent);
|
|
167
|
+
flex: 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* --- Attention Panel --- */
|
|
171
|
+
.attention-panel__header {
|
|
172
|
+
color: var(--orange-7, #c2510f);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.attention-panel__list {
|
|
176
|
+
list-style: none;
|
|
177
|
+
padding: 0;
|
|
178
|
+
margin: 0;
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
gap: var(--space-xs);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.attention-panel__list li {
|
|
185
|
+
font-size: 0.9rem;
|
|
186
|
+
padding: var(--space-xs) 0;
|
|
187
|
+
border-bottom: 1px solid var(--color-border);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.attention-panel__list li:last-child {
|
|
191
|
+
border-bottom: none;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.attention-panel__clear {
|
|
195
|
+
font-size: 0.9rem;
|
|
196
|
+
color: var(--color-text-muted);
|
|
197
|
+
font-style: italic;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* --- Phase Timeline --- */
|
|
201
|
+
.phase-timeline {
|
|
202
|
+
grid-column: 1 / -1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.phase-timeline__label {
|
|
206
|
+
font-size: 0.75rem;
|
|
207
|
+
font-weight: 600;
|
|
208
|
+
text-transform: uppercase;
|
|
209
|
+
letter-spacing: 0.08em;
|
|
210
|
+
color: var(--color-text-muted);
|
|
211
|
+
margin-bottom: var(--space-sm);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.phase-timeline__strip {
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-wrap: wrap;
|
|
217
|
+
gap: 4px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.phase-block {
|
|
221
|
+
display: inline-flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
justify-content: center;
|
|
224
|
+
width: 32px;
|
|
225
|
+
height: 32px;
|
|
226
|
+
border-radius: var(--radius-sm);
|
|
227
|
+
font-size: 0.7rem;
|
|
228
|
+
font-weight: 600;
|
|
229
|
+
text-decoration: none;
|
|
230
|
+
cursor: pointer;
|
|
231
|
+
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.phase-block:hover {
|
|
235
|
+
transform: translateY(-2px);
|
|
236
|
+
box-shadow: var(--shadow-md);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.phase-block--complete {
|
|
240
|
+
background: var(--green-6, #2da44e);
|
|
241
|
+
color: #fff;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.phase-block--in-progress {
|
|
245
|
+
background: var(--blue-6, #0969da);
|
|
246
|
+
color: #fff;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.phase-block--not-started {
|
|
250
|
+
background: var(--color-border);
|
|
251
|
+
color: var(--color-text-dim);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.phase-block--current {
|
|
255
|
+
outline: 3px solid var(--color-accent);
|
|
256
|
+
outline-offset: 2px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* --- Activity Stream --- */
|
|
260
|
+
.activity-stream__list {
|
|
261
|
+
list-style: none;
|
|
262
|
+
padding: 0;
|
|
263
|
+
margin: 0;
|
|
264
|
+
display: flex;
|
|
265
|
+
flex-direction: column;
|
|
266
|
+
gap: 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.activity-item {
|
|
270
|
+
display: flex;
|
|
271
|
+
align-items: baseline;
|
|
272
|
+
gap: var(--space-sm);
|
|
273
|
+
padding: var(--space-xs) 0;
|
|
274
|
+
border-bottom: 1px solid var(--color-border);
|
|
275
|
+
font-size: 0.85rem;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.activity-item:last-child {
|
|
279
|
+
border-bottom: none;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.activity-item__icon {
|
|
283
|
+
color: var(--color-text-muted);
|
|
284
|
+
flex-shrink: 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.activity-item__path {
|
|
288
|
+
font-family: var(--font-mono);
|
|
289
|
+
font-size: 0.8rem;
|
|
290
|
+
color: var(--color-text-dim);
|
|
291
|
+
flex: 1;
|
|
292
|
+
overflow: hidden;
|
|
293
|
+
text-overflow: ellipsis;
|
|
294
|
+
white-space: nowrap;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.activity-item__time {
|
|
298
|
+
font-size: 0.75rem;
|
|
299
|
+
color: var(--color-text-muted);
|
|
300
|
+
flex-shrink: 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.activity-stream__empty {
|
|
304
|
+
font-size: 0.9rem;
|
|
305
|
+
color: var(--color-text-muted);
|
|
306
|
+
font-style: italic;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* --- Quick Actions --- */
|
|
310
|
+
.quick-actions {
|
|
311
|
+
display: flex;
|
|
312
|
+
flex-direction: column;
|
|
313
|
+
gap: var(--space-sm);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.quick-actions__label {
|
|
317
|
+
font-size: 0.75rem;
|
|
318
|
+
font-weight: 600;
|
|
319
|
+
text-transform: uppercase;
|
|
320
|
+
letter-spacing: 0.08em;
|
|
321
|
+
color: var(--color-text-muted);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.quick-actions__buttons {
|
|
325
|
+
display: flex;
|
|
326
|
+
flex-wrap: wrap;
|
|
327
|
+
gap: var(--space-sm);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* --- Buttons --- */
|
|
331
|
+
.btn {
|
|
332
|
+
display: inline-flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
justify-content: center;
|
|
335
|
+
padding: var(--space-xs) var(--space-md);
|
|
336
|
+
border-radius: var(--radius-sm);
|
|
337
|
+
font-size: 0.875rem;
|
|
338
|
+
font-weight: 500;
|
|
339
|
+
text-decoration: none;
|
|
340
|
+
cursor: pointer;
|
|
341
|
+
border: 1px solid transparent;
|
|
342
|
+
transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.btn--primary {
|
|
346
|
+
background: var(--color-accent);
|
|
347
|
+
color: #fff;
|
|
348
|
+
border-color: var(--color-accent);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.btn--primary:hover {
|
|
352
|
+
background: var(--color-accent-hover);
|
|
353
|
+
border-color: var(--color-accent-hover);
|
|
354
|
+
color: #fff;
|
|
355
|
+
text-decoration: none;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.btn--secondary {
|
|
359
|
+
background: transparent;
|
|
360
|
+
color: var(--color-text);
|
|
361
|
+
border-color: var(--color-border);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.btn--secondary:hover {
|
|
365
|
+
background: var(--color-surface-hover);
|
|
366
|
+
text-decoration: none;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.btn--ghost {
|
|
370
|
+
background: transparent;
|
|
371
|
+
color: var(--color-text-dim);
|
|
372
|
+
border-color: transparent;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.btn--ghost:hover {
|
|
376
|
+
background: var(--color-surface-hover);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.btn--sm {
|
|
380
|
+
padding: 2px var(--space-sm);
|
|
381
|
+
font-size: 0.75rem;
|
|
382
|
+
}
|
|
@@ -11,10 +11,19 @@
|
|
|
11
11
|
|
|
12
12
|
html {
|
|
13
13
|
font-family: var(--font-sans);
|
|
14
|
-
font-size:
|
|
15
|
-
|
|
14
|
+
font-size: var(--font-size-base, 14px);
|
|
15
|
+
line-height: var(--line-height-base, 1.5);
|
|
16
|
+
letter-spacing: -0.011em;
|
|
16
17
|
background: var(--color-surface);
|
|
17
18
|
color: var(--color-text);
|
|
19
|
+
-webkit-font-smoothing: antialiased;
|
|
20
|
+
-moz-osx-font-smoothing: grayscale;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
code, pre, kbd, samp {
|
|
24
|
+
font-family: var(--font-mono);
|
|
25
|
+
font-size: 0.9em;
|
|
26
|
+
line-height: var(--line-height-mono, 1.6);
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
body {
|
|
@@ -38,9 +38,12 @@
|
|
|
38
38
|
--shadow-sm: var(--shadow-2);
|
|
39
39
|
--shadow-md: var(--shadow-4);
|
|
40
40
|
|
|
41
|
-
/* Typography */
|
|
42
|
-
--font-sans:
|
|
43
|
-
--font-mono:
|
|
41
|
+
/* Typography — Inter + JetBrains Mono (loaded via Google Fonts CDN) */
|
|
42
|
+
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
43
|
+
--font-mono: 'JetBrains Mono', 'SF Mono', 'Consolas', 'Courier New', monospace;
|
|
44
|
+
--font-size-base: 14px;
|
|
45
|
+
--line-height-base: 1.5;
|
|
46
|
+
--line-height-mono: 1.6;
|
|
44
47
|
|
|
45
48
|
/* Layout dimensions */
|
|
46
49
|
--size-sidebar: 220px;
|
|
@@ -21,6 +21,11 @@ export function Layout({ title, children, currentView }: LayoutProps) {
|
|
|
21
21
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
22
22
|
<title>{title} — PBR Dashboard</title>
|
|
23
23
|
|
|
24
|
+
{/* Google Fonts: Inter + JetBrains Mono */}
|
|
25
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
26
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="" />
|
|
27
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet" />
|
|
28
|
+
|
|
24
29
|
{/* Open Props */}
|
|
25
30
|
<link rel="stylesheet" href="https://unpkg.com/open-props" />
|
|
26
31
|
<link rel="stylesheet" href="https://unpkg.com/open-props/normalize.min.css" />
|
|
@@ -29,6 +34,7 @@ export function Layout({ title, children, currentView }: LayoutProps) {
|
|
|
29
34
|
<link rel="stylesheet" href="/css/tokens.css" />
|
|
30
35
|
<link rel="stylesheet" href="/css/layout.css" />
|
|
31
36
|
<link rel="stylesheet" href="/css/status-colors.css" />
|
|
37
|
+
<link rel="stylesheet" href="/css/command-center.css" />
|
|
32
38
|
|
|
33
39
|
{/* Prevent flash of wrong theme */}
|
|
34
40
|
{html`<script>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { FC } from 'hono/jsx';
|
|
2
|
+
import { html } from 'hono/html';
|
|
3
|
+
|
|
4
|
+
interface ActivityItem {
|
|
5
|
+
path: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
type: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ActivityStreamProps {
|
|
11
|
+
activity: ActivityItem[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ActivityStream: FC<ActivityStreamProps> = ({ activity }) => {
|
|
15
|
+
return (
|
|
16
|
+
<div class="card activity-stream" id="activity-stream">
|
|
17
|
+
<div class="card__header">Recent Activity</div>
|
|
18
|
+
{activity.length === 0 ? (
|
|
19
|
+
<p class="activity-stream__empty">No recent .planning/ commits found.</p>
|
|
20
|
+
) : (
|
|
21
|
+
<>
|
|
22
|
+
<ul class="activity-stream__list">
|
|
23
|
+
{activity.map((item, i) => (
|
|
24
|
+
<li class="activity-item" key={i}>
|
|
25
|
+
<span class="activity-item__icon" aria-hidden="true">◈</span>
|
|
26
|
+
<span class="activity-item__path">{item.path}</span>
|
|
27
|
+
<time
|
|
28
|
+
class="activity-item__time"
|
|
29
|
+
datetime={item.timestamp}
|
|
30
|
+
data-timestamp={item.timestamp}
|
|
31
|
+
>
|
|
32
|
+
…
|
|
33
|
+
</time>
|
|
34
|
+
</li>
|
|
35
|
+
))}
|
|
36
|
+
</ul>
|
|
37
|
+
{html`<script>
|
|
38
|
+
(function() {
|
|
39
|
+
function relativeTime(ts) {
|
|
40
|
+
var diff = Date.now() - new Date(ts).getTime();
|
|
41
|
+
var minutes = Math.floor(diff / 60000);
|
|
42
|
+
if (minutes < 60) return minutes + 'm ago';
|
|
43
|
+
var hours = Math.floor(minutes / 60);
|
|
44
|
+
if (hours < 24) return hours + 'h ago';
|
|
45
|
+
var days = Math.floor(hours / 24);
|
|
46
|
+
return days + 'd ago';
|
|
47
|
+
}
|
|
48
|
+
var els = document.querySelectorAll('.activity-item__time[data-timestamp]');
|
|
49
|
+
for (var i = 0; i < els.length; i++) {
|
|
50
|
+
els[i].textContent = relativeTime(els[i].dataset.timestamp);
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
</script>`}
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FC } from 'hono/jsx';
|
|
2
|
+
|
|
3
|
+
interface AttentionPanelProps {
|
|
4
|
+
todos: Array<{ priority: string }>;
|
|
5
|
+
phases: Array<{ id: number; name?: string; status: string }>;
|
|
6
|
+
currentPhaseId: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const AttentionPanel: FC<AttentionPanelProps> = ({
|
|
10
|
+
todos,
|
|
11
|
+
phases,
|
|
12
|
+
currentPhaseId,
|
|
13
|
+
}) => {
|
|
14
|
+
const highPriorityCount = todos.filter(
|
|
15
|
+
(t) => t.priority === 'P0' || t.priority === 'P1'
|
|
16
|
+
).length;
|
|
17
|
+
|
|
18
|
+
const stalledPhases = phases.filter(
|
|
19
|
+
(p) => p.id < currentPhaseId && p.status !== 'complete'
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const hasItems = highPriorityCount > 0 || stalledPhases.length > 0;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div class="card attention-panel" id="attention-panel">
|
|
26
|
+
<div class="card__header attention-panel__header">Needs Attention</div>
|
|
27
|
+
{hasItems ? (
|
|
28
|
+
<ul class="attention-panel__list">
|
|
29
|
+
{highPriorityCount > 0 && (
|
|
30
|
+
<li>
|
|
31
|
+
<a href="/todos?priority=P0,P1">
|
|
32
|
+
{highPriorityCount} high-priority todo
|
|
33
|
+
{highPriorityCount !== 1 ? 's' : ''} pending
|
|
34
|
+
</a>
|
|
35
|
+
</li>
|
|
36
|
+
)}
|
|
37
|
+
{stalledPhases.map((p) => (
|
|
38
|
+
<li key={p.id}>
|
|
39
|
+
Phase {p.id} ({p.name || `Phase ${p.id}`}) is incomplete — expected to be done by now
|
|
40
|
+
</li>
|
|
41
|
+
))}
|
|
42
|
+
</ul>
|
|
43
|
+
) : (
|
|
44
|
+
<p class="attention-panel__clear">All clear — nothing needs attention.</p>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { FC } from 'hono/jsx';
|
|
2
|
+
|
|
3
|
+
interface CurrentPhaseCardProps {
|
|
4
|
+
currentPhase: {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
status: string;
|
|
8
|
+
planStatus: string;
|
|
9
|
+
};
|
|
10
|
+
lastActivity: {
|
|
11
|
+
date: string;
|
|
12
|
+
description: string;
|
|
13
|
+
};
|
|
14
|
+
nextAction: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const CurrentPhaseCard: FC<CurrentPhaseCardProps> = ({
|
|
18
|
+
currentPhase,
|
|
19
|
+
lastActivity,
|
|
20
|
+
nextAction,
|
|
21
|
+
}) => {
|
|
22
|
+
return (
|
|
23
|
+
<div class="card current-phase-card">
|
|
24
|
+
<div class="card__header">Current Phase</div>
|
|
25
|
+
<h2 class="card__title">
|
|
26
|
+
Phase {currentPhase.id}: {currentPhase.name}
|
|
27
|
+
</h2>
|
|
28
|
+
<span class={`badge status-badge status-badge--${currentPhase.status}`}>
|
|
29
|
+
{currentPhase.status}
|
|
30
|
+
</span>
|
|
31
|
+
<p class="card__meta">Plans: {currentPhase.planStatus}</p>
|
|
32
|
+
<p class="card__meta">
|
|
33
|
+
Last activity: {lastActivity.date || 'N/A'} — {lastActivity.description}
|
|
34
|
+
</p>
|
|
35
|
+
{nextAction && (
|
|
36
|
+
<div class="next-action">
|
|
37
|
+
<span class="next-action__label">Next</span>
|
|
38
|
+
<code class="next-action__cmd">{nextAction}</code>
|
|
39
|
+
<button
|
|
40
|
+
class="btn btn--ghost btn--sm"
|
|
41
|
+
type="button"
|
|
42
|
+
onclick={`navigator.clipboard.writeText('${nextAction}')`}
|
|
43
|
+
>
|
|
44
|
+
Copy
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { FC } from 'hono/jsx';
|
|
2
|
+
|
|
3
|
+
interface PhaseTimelineProps {
|
|
4
|
+
phases: Array<{ id: number; name: string; status: string }>;
|
|
5
|
+
currentPhaseId: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const PhaseTimeline: FC<PhaseTimelineProps> = ({ phases, currentPhaseId }) => {
|
|
9
|
+
return (
|
|
10
|
+
<div class="phase-timeline" id="phase-timeline">
|
|
11
|
+
<p class="phase-timeline__label">Phase Timeline</p>
|
|
12
|
+
<div class="phase-timeline__strip">
|
|
13
|
+
{phases.map((phase) => {
|
|
14
|
+
const isCurrent = phase.id === currentPhaseId;
|
|
15
|
+
const cls = [
|
|
16
|
+
'phase-block',
|
|
17
|
+
`phase-block--${phase.status}`,
|
|
18
|
+
isCurrent ? 'phase-block--current' : '',
|
|
19
|
+
]
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.join(' ');
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<a
|
|
25
|
+
key={phase.id}
|
|
26
|
+
href={`/explorer?phase=${phase.id}`}
|
|
27
|
+
class={cls}
|
|
28
|
+
title={`Phase ${phase.id}: ${phase.name}`}
|
|
29
|
+
aria-label={`Phase ${phase.id}: ${phase.name} (${phase.status})`}
|
|
30
|
+
>
|
|
31
|
+
{phase.id}
|
|
32
|
+
</a>
|
|
33
|
+
);
|
|
34
|
+
})}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { FC } from 'hono/jsx';
|
|
2
|
+
|
|
3
|
+
interface ProgressRingProps {
|
|
4
|
+
percent: number;
|
|
5
|
+
size?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const ProgressRing: FC<ProgressRingProps> = ({ percent, size = 120 }) => {
|
|
9
|
+
const r = (size / 2) - 8;
|
|
10
|
+
const circumference = 2 * Math.PI * r;
|
|
11
|
+
const offset = circumference * (1 - percent / 100);
|
|
12
|
+
const center = size / 2;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div class="progress-ring-wrapper" aria-label={`${percent}% complete`}>
|
|
16
|
+
<svg
|
|
17
|
+
class="progress-ring__svg"
|
|
18
|
+
viewBox={`0 0 ${size} ${size}`}
|
|
19
|
+
width={size}
|
|
20
|
+
height={size}
|
|
21
|
+
role="img"
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
>
|
|
24
|
+
<circle
|
|
25
|
+
class="progress-ring__bg"
|
|
26
|
+
cx={center}
|
|
27
|
+
cy={center}
|
|
28
|
+
r={r}
|
|
29
|
+
stroke-width="8"
|
|
30
|
+
/>
|
|
31
|
+
<circle
|
|
32
|
+
class="progress-ring__fg"
|
|
33
|
+
cx={center}
|
|
34
|
+
cy={center}
|
|
35
|
+
r={r}
|
|
36
|
+
stroke-width="8"
|
|
37
|
+
stroke-dasharray={`${circumference}`}
|
|
38
|
+
stroke-dashoffset={`${offset}`}
|
|
39
|
+
/>
|
|
40
|
+
<text
|
|
41
|
+
class="progress-ring__text"
|
|
42
|
+
x={center}
|
|
43
|
+
y={center}
|
|
44
|
+
transform={`rotate(90, ${center}, ${center})`}
|
|
45
|
+
>
|
|
46
|
+
{percent}%
|
|
47
|
+
</text>
|
|
48
|
+
</svg>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FC } from 'hono/jsx';
|
|
2
|
+
|
|
3
|
+
interface QuickAction {
|
|
4
|
+
label: string;
|
|
5
|
+
href: string;
|
|
6
|
+
primary: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface QuickActionsProps {
|
|
10
|
+
actions: QuickAction[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const QuickActions: FC<QuickActionsProps> = ({ actions }) => {
|
|
14
|
+
return (
|
|
15
|
+
<div class="quick-actions" id="quick-actions">
|
|
16
|
+
<p class="quick-actions__label">Quick Actions</p>
|
|
17
|
+
<div class="quick-actions__buttons">
|
|
18
|
+
{actions.map((action, i) => (
|
|
19
|
+
<a
|
|
20
|
+
key={i}
|
|
21
|
+
href={action.href}
|
|
22
|
+
class={`btn${action.primary ? ' btn--primary' : ' btn--secondary'}`}
|
|
23
|
+
>
|
|
24
|
+
{action.label}
|
|
25
|
+
</a>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { FC } from 'hono/jsx';
|
|
2
|
+
|
|
3
|
+
interface StatusHeaderProps {
|
|
4
|
+
projectName: string;
|
|
5
|
+
currentPhase: {
|
|
6
|
+
id: number;
|
|
7
|
+
total: number;
|
|
8
|
+
name: string;
|
|
9
|
+
status: string;
|
|
10
|
+
};
|
|
11
|
+
completedCount: number;
|
|
12
|
+
totalCount: number;
|
|
13
|
+
progress: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const StatusHeader: FC<StatusHeaderProps> = ({
|
|
17
|
+
projectName,
|
|
18
|
+
currentPhase,
|
|
19
|
+
completedCount,
|
|
20
|
+
totalCount,
|
|
21
|
+
progress,
|
|
22
|
+
}) => {
|
|
23
|
+
return (
|
|
24
|
+
<div class="status-header">
|
|
25
|
+
<span class="status-header__project">{projectName}</span>
|
|
26
|
+
<span class="status-header__phase">
|
|
27
|
+
Phase {currentPhase.id}: {currentPhase.name}
|
|
28
|
+
</span>
|
|
29
|
+
<span class={`badge status-badge status-badge--${currentPhase.status}`}>
|
|
30
|
+
{currentPhase.status}
|
|
31
|
+
</span>
|
|
32
|
+
<div class="milestone-bar">
|
|
33
|
+
<div class="milestone-bar__track">
|
|
34
|
+
<div
|
|
35
|
+
class="milestone-bar__fill"
|
|
36
|
+
style={`width:${progress}%`}
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
<span class="milestone-bar__label">
|
|
40
|
+
{completedCount} of {totalCount} phases — {progress}%
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
package/dashboard/src/index.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { logger } from 'hono/logger';
|
|
|
6
6
|
import { secureHeaders } from 'hono/secure-headers';
|
|
7
7
|
import { Layout } from './components/Layout';
|
|
8
8
|
import { indexRouter } from './routes/index.routes';
|
|
9
|
+
import { commandCenterRouter } from './routes/command-center.routes';
|
|
9
10
|
import { sseHandler } from './sse-handler';
|
|
10
11
|
import { startWatcher } from './watcher-setup';
|
|
11
12
|
import { currentPhaseMiddleware } from './middleware/current-phase';
|
|
@@ -66,6 +67,7 @@ function createApp(config: ServerConfig) {
|
|
|
66
67
|
|
|
67
68
|
// Routes
|
|
68
69
|
app.route('/', indexRouter);
|
|
70
|
+
app.route('/api/command-center', commandCenterRouter);
|
|
69
71
|
|
|
70
72
|
// SSE endpoint — real streamSSE handler with multi-client broadcast
|
|
71
73
|
app.get('/api/events/stream', sseHandler);
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { MiddlewareHandler } from 'hono';
|
|
2
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
3
|
-
// @ts-ignore — dashboard.service.js is plain ESM with no type declarations
|
|
4
2
|
import { parseStateFile } from '../services/dashboard.service.js';
|
|
5
3
|
|
|
6
4
|
export const currentPhaseMiddleware: MiddlewareHandler = async (c, next) => {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { getDashboardData } from '../services/dashboard.service.js';
|
|
3
|
+
import { listPendingTodos } from '../services/todo.service.js';
|
|
4
|
+
import { StatusHeader } from '../components/partials/StatusHeader';
|
|
5
|
+
import { ProgressRing } from '../components/partials/ProgressRing';
|
|
6
|
+
import { CurrentPhaseCard } from '../components/partials/CurrentPhaseCard';
|
|
7
|
+
import { AttentionPanel } from '../components/partials/AttentionPanel';
|
|
8
|
+
import { PhaseTimeline } from '../components/partials/PhaseTimeline';
|
|
9
|
+
import { ActivityStream } from '../components/partials/ActivityStream';
|
|
10
|
+
import { QuickActions } from '../components/partials/QuickActions';
|
|
11
|
+
|
|
12
|
+
type Env = {
|
|
13
|
+
Variables: {
|
|
14
|
+
projectDir: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const router = new Hono<Env>();
|
|
19
|
+
|
|
20
|
+
async function fetchAllData(projectDir: string) {
|
|
21
|
+
const [data, todos] = await Promise.all([
|
|
22
|
+
getDashboardData(projectDir),
|
|
23
|
+
listPendingTodos(projectDir).catch(() => [])
|
|
24
|
+
]);
|
|
25
|
+
const completed = (data.phases as any[]).filter((p: any) => p.status === 'complete').length;
|
|
26
|
+
return { data, todos, completed };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
router.get('/status', async (c) => {
|
|
30
|
+
const projectDir = c.get('projectDir');
|
|
31
|
+
const { data, completed } = await fetchAllData(projectDir);
|
|
32
|
+
return c.html(
|
|
33
|
+
<div id="cc-status">
|
|
34
|
+
<StatusHeader
|
|
35
|
+
projectName={data.projectName}
|
|
36
|
+
currentPhase={data.currentPhase}
|
|
37
|
+
completedCount={completed}
|
|
38
|
+
totalCount={(data.phases as any[]).length}
|
|
39
|
+
progress={data.progress}
|
|
40
|
+
/>
|
|
41
|
+
<ProgressRing percent={data.progress} />
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
router.get('/activity', async (c) => {
|
|
47
|
+
const projectDir = c.get('projectDir');
|
|
48
|
+
const data = await getDashboardData(projectDir);
|
|
49
|
+
return c.html(<ActivityStream activity={data.recentActivity} />);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
router.get('/attention', async (c) => {
|
|
53
|
+
const projectDir = c.get('projectDir');
|
|
54
|
+
const { data, todos } = await fetchAllData(projectDir);
|
|
55
|
+
return c.html(
|
|
56
|
+
<AttentionPanel
|
|
57
|
+
todos={todos}
|
|
58
|
+
phases={data.phases as any[]}
|
|
59
|
+
currentPhaseId={data.currentPhase.id}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Unused components imported but available for future routes
|
|
65
|
+
void CurrentPhaseCard;
|
|
66
|
+
void PhaseTimeline;
|
|
67
|
+
void QuickActions;
|
|
68
|
+
|
|
69
|
+
export { router as commandCenterRouter };
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { Layout } from '../components/Layout';
|
|
3
|
+
import { getDashboardData } from '../services/dashboard.service.js';
|
|
4
|
+
import { listPendingTodos } from '../services/todo.service.js';
|
|
5
|
+
import { StatusHeader } from '../components/partials/StatusHeader';
|
|
6
|
+
import { ProgressRing } from '../components/partials/ProgressRing';
|
|
7
|
+
import { CurrentPhaseCard } from '../components/partials/CurrentPhaseCard';
|
|
8
|
+
import { AttentionPanel } from '../components/partials/AttentionPanel';
|
|
9
|
+
import { PhaseTimeline } from '../components/partials/PhaseTimeline';
|
|
10
|
+
import { ActivityStream } from '../components/partials/ActivityStream';
|
|
11
|
+
import { QuickActions } from '../components/partials/QuickActions';
|
|
3
12
|
|
|
4
13
|
type Env = {
|
|
5
14
|
Variables: {
|
|
@@ -13,22 +22,83 @@ router.get('/favicon.ico', (c) => {
|
|
|
13
22
|
return c.body(null, 204);
|
|
14
23
|
});
|
|
15
24
|
|
|
16
|
-
router.get('/', (c) => {
|
|
25
|
+
router.get('/', async (c) => {
|
|
26
|
+
const projectDir = c.get('projectDir');
|
|
17
27
|
const isHtmx = c.req.header('HX-Request');
|
|
18
28
|
|
|
29
|
+
const [data, todos] = await Promise.all([
|
|
30
|
+
getDashboardData(projectDir),
|
|
31
|
+
listPendingTodos(projectDir).catch(() => [])
|
|
32
|
+
]);
|
|
33
|
+
const completed = (data.phases as any[]).filter((p: any) => p.status === 'complete').length;
|
|
34
|
+
|
|
35
|
+
const content = (
|
|
36
|
+
<main id="main-content" class="command-center">
|
|
37
|
+
<div
|
|
38
|
+
id="cc-status"
|
|
39
|
+
hx-get="/api/command-center/status"
|
|
40
|
+
hx-trigger="sse:file-change"
|
|
41
|
+
hx-swap="innerHTML"
|
|
42
|
+
hx-ext="sse"
|
|
43
|
+
>
|
|
44
|
+
<StatusHeader
|
|
45
|
+
projectName={data.projectName}
|
|
46
|
+
currentPhase={data.currentPhase}
|
|
47
|
+
completedCount={completed}
|
|
48
|
+
totalCount={(data.phases as any[]).length}
|
|
49
|
+
progress={data.progress}
|
|
50
|
+
/>
|
|
51
|
+
<ProgressRing percent={data.progress} />
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="command-center__grid">
|
|
55
|
+
<CurrentPhaseCard
|
|
56
|
+
currentPhase={data.currentPhase}
|
|
57
|
+
lastActivity={data.lastActivity}
|
|
58
|
+
nextAction={(data as any).nextAction ?? null}
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
<div
|
|
62
|
+
id="attention-panel-wrapper"
|
|
63
|
+
hx-get="/api/command-center/attention"
|
|
64
|
+
hx-trigger="sse:file-change"
|
|
65
|
+
hx-swap="outerHTML"
|
|
66
|
+
hx-ext="sse"
|
|
67
|
+
>
|
|
68
|
+
<AttentionPanel
|
|
69
|
+
todos={todos}
|
|
70
|
+
phases={data.phases as any[]}
|
|
71
|
+
currentPhaseId={data.currentPhase.id}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<QuickActions actions={data.quickActions} />
|
|
76
|
+
|
|
77
|
+
<PhaseTimeline
|
|
78
|
+
phases={data.phases as any[]}
|
|
79
|
+
currentPhaseId={data.currentPhase.id}
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
<div
|
|
83
|
+
id="activity-stream-wrapper"
|
|
84
|
+
hx-get="/api/command-center/activity"
|
|
85
|
+
hx-trigger="sse:file-change"
|
|
86
|
+
hx-swap="outerHTML"
|
|
87
|
+
hx-ext="sse"
|
|
88
|
+
>
|
|
89
|
+
<ActivityStream activity={data.recentActivity} />
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</main>
|
|
93
|
+
);
|
|
94
|
+
|
|
19
95
|
if (isHtmx) {
|
|
20
|
-
return c.html(
|
|
21
|
-
<main id="main-content">
|
|
22
|
-
<h1>Command Center</h1>
|
|
23
|
-
<p>Project overview loads here.</p>
|
|
24
|
-
</main>
|
|
25
|
-
);
|
|
96
|
+
return c.html(content);
|
|
26
97
|
}
|
|
27
98
|
|
|
28
99
|
return c.html(
|
|
29
100
|
<Layout title="Command Center" currentView="home">
|
|
30
|
-
|
|
31
|
-
<p>Project overview loads here.</p>
|
|
101
|
+
{content}
|
|
32
102
|
</Layout>
|
|
33
103
|
);
|
|
34
104
|
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface DashboardPhase {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
status: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface DashboardCurrentPhase {
|
|
9
|
+
id: number;
|
|
10
|
+
total: number;
|
|
11
|
+
name: string;
|
|
12
|
+
planStatus: string;
|
|
13
|
+
status: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DashboardData {
|
|
17
|
+
projectName: string;
|
|
18
|
+
currentPhase: DashboardCurrentPhase;
|
|
19
|
+
lastActivity: { date: string; description: string };
|
|
20
|
+
progress: number;
|
|
21
|
+
phases: DashboardPhase[];
|
|
22
|
+
recentActivity: Array<{ path: string; timestamp: string; type: string }>;
|
|
23
|
+
quickActions: Array<{ label: string; href: string; primary: boolean }>;
|
|
24
|
+
nextAction?: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getDashboardData(projectDir: string): Promise<DashboardData>;
|
|
28
|
+
export function parseStateFile(projectDir: string): Promise<{
|
|
29
|
+
projectName: string;
|
|
30
|
+
currentPhase: DashboardCurrentPhase;
|
|
31
|
+
lastActivity: { date: string; description: string };
|
|
32
|
+
progress: number;
|
|
33
|
+
nextAction: string | null;
|
|
34
|
+
}>;
|
|
35
|
+
export function parseRoadmapFile(projectDir: string): Promise<unknown>;
|
|
36
|
+
export function derivePhaseStatuses(phases: DashboardPhase[], currentPhase: DashboardCurrentPhase): DashboardPhase[];
|
|
37
|
+
export function getRecentActivity(projectDir: string): Promise<Array<{ path: string; timestamp: string; type: string }>>;
|
|
38
|
+
export function deriveQuickActions(currentPhase: DashboardCurrentPhase): Array<{ label: string; href: string; primary: boolean }>;
|
|
39
|
+
export function _clearActivityCache(): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface TodoItem {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
priority: string;
|
|
5
|
+
phase: string;
|
|
6
|
+
status: string;
|
|
7
|
+
created: string;
|
|
8
|
+
filename: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TodoFilters {
|
|
12
|
+
priority?: string;
|
|
13
|
+
status?: string;
|
|
14
|
+
q?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function listPendingTodos(projectDir: string, filters?: TodoFilters): Promise<TodoItem[]>;
|
|
18
|
+
export function getTodoDetail(projectDir: string, todoId: string): Promise<TodoItem & { html: string }>;
|
|
19
|
+
export function createTodo(projectDir: string, todoData: { title: string; priority: string; description: string; phase?: string }): Promise<string>;
|
|
20
|
+
export function listDoneTodos(projectDir: string): Promise<Array<{ id: string; filename: string; title: string; priority: string; phase: string; completedAt: string | null }>>;
|
|
21
|
+
export function completeTodo(projectDir: string, todoId: string): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pbr",
|
|
3
3
|
"displayName": "Plan-Build-Run",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.29.0",
|
|
5
5
|
"description": "Plan-Build-Run — Structured development workflow for GitHub Copilot CLI. Solves context rot through disciplined agent delegation, structured planning, atomic execution, and goal-backward verification.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "SienkLogic",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pbr",
|
|
3
3
|
"displayName": "Plan-Build-Run",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.29.0",
|
|
5
5
|
"description": "Plan-Build-Run — Structured development workflow for Cursor. Solves context rot through disciplined subagent delegation, structured planning, atomic execution, and goal-backward verification.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "SienkLogic",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pbr",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.29.0",
|
|
4
4
|
"description": "Plan-Build-Run — Structured development workflow for Claude Code. Solves context rot through disciplined subagent delegation, structured planning, atomic execution, and goal-backward verification.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "SienkLogic",
|