@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 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: 15px;
15
- letter-spacing: -0.01em;
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: var(--font-sans);
43
- --font-mono: var(--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
+ };
@@ -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
- <h1>Command Center</h1>
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@sienklogic/plan-build-run",
3
- "version": "2.28.0",
3
+ "version": "2.29.0",
4
4
  "description": "Plan it, Build it, Run it — structured development workflow for Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pbr",
3
3
  "displayName": "Plan-Build-Run",
4
- "version": "2.28.0",
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.28.0",
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.28.0",
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",