@sienklogic/plan-build-run 2.17.1 → 2.18.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 (28) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dashboard/public/css/layout.css +9 -0
  3. package/dashboard/public/js/sse-client.js +1 -0
  4. package/dashboard/src/app.js +9 -0
  5. package/dashboard/src/routes/index.routes.js +5 -2
  6. package/dashboard/src/routes/pages.routes.js +35 -5
  7. package/dashboard/src/services/analytics.service.js +2 -4
  8. package/dashboard/src/services/notes.service.js +50 -0
  9. package/dashboard/src/services/phase.service.js +8 -3
  10. package/dashboard/src/views/error.ejs +1 -1
  11. package/dashboard/src/views/notes.ejs +5 -0
  12. package/dashboard/src/views/partials/analytics-content.ejs +23 -4
  13. package/dashboard/src/views/partials/dashboard-content.ejs +69 -63
  14. package/dashboard/src/views/partials/dependencies-content.ejs +33 -1
  15. package/dashboard/src/views/partials/footer.ejs +1 -1
  16. package/dashboard/src/views/partials/header.ejs +1 -1
  17. package/dashboard/src/views/partials/milestones-content.ejs +9 -1
  18. package/dashboard/src/views/partials/notes-content.ejs +23 -0
  19. package/dashboard/src/views/partials/phase-content.ejs +4 -1
  20. package/dashboard/src/views/partials/phase-doc-content.ejs +1 -1
  21. package/dashboard/src/views/partials/phases-content.ejs +8 -2
  22. package/dashboard/src/views/partials/roadmap-content.ejs +16 -4
  23. package/dashboard/src/views/partials/sidebar.ejs +8 -0
  24. package/package.json +1 -1
  25. package/plugins/copilot-pbr/plugin.json +1 -1
  26. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  27. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  28. package/dashboard/src/views/layout.ejs +0 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ 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.18.0](https://github.com/SienkLogic/plan-build-run/compare/plan-build-run-v2.17.1...plan-build-run-v2.18.0) (2026-02-22)
9
+
10
+
11
+ ### Features
12
+
13
+ * **16-01:** redesign dashboard home, fix analytics duration, add bar charts, mermaid dark mode ([9ea3936](https://github.com/SienkLogic/plan-build-run/commit/9ea3936bb5c35305b57250c83e596c4704bc8868))
14
+ * **17-01:** add notes page, verification viewer, milestone progress bars, dynamic footer version ([71625ff](https://github.com/SienkLogic/plan-build-run/commit/71625ff769638be8d75840b48f455a1fe5664b88))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **18-01:** HTMX navigation consistency, SSE tooltip, error page fix, remove deprecated layout ([0f40f3c](https://github.com/SienkLogic/plan-build-run/commit/0f40f3c9de389f7eb33f8eb83c78a0880f485e4e))
20
+
8
21
  ## [2.17.1](https://github.com/SienkLogic/plan-build-run/compare/plan-build-run-v2.17.0...plan-build-run-v2.17.1) (2026-02-22)
9
22
 
10
23
 
@@ -500,6 +500,15 @@ main > p:first-of-type > a[href="/"]:hover {
500
500
  font-size: 0.875rem;
501
501
  }
502
502
 
503
+ /* --- Stat Values --- */
504
+ .stat-value { font-size: 2rem; font-weight: 700; line-height: 1.2; }
505
+ .stat-unit { font-size: 0.875rem; color: var(--color-dim); margin-left: 0.25rem; }
506
+
507
+ /* --- Bar Chart --- */
508
+ .bar-chart-row { display: flex; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-xs); }
509
+ .bar-chart-label { min-width: 160px; font-size: 0.875rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
510
+ .bar-chart-bar { height: 1.5rem; border-radius: var(--radius-sm); background: var(--pico-primary); color: var(--pico-primary-inverse, #fff); font-size: 0.75rem; display: flex; align-items: center; padding-left: var(--space-sm); min-width: 2rem; transition: width 0.3s ease; }
511
+
503
512
  /* --- Loading Bar --- */
504
513
  .loading-bar {
505
514
  position: fixed;
@@ -69,6 +69,7 @@
69
69
  const dot = document.getElementById('sse-status');
70
70
  if (dot) {
71
71
  dot.setAttribute('data-connected', String(connected));
72
+ dot.setAttribute('title', connected ? 'Live updates: connected' : 'Live updates: disconnected');
72
73
  }
73
74
  }
74
75
 
@@ -9,6 +9,7 @@ import eventsRouter from './routes/events.routes.js';
9
9
  import notFoundHandler from './middleware/notFoundHandler.js';
10
10
  import errorHandler from './middleware/errorHandler.js';
11
11
  import currentPhaseMiddleware from './middleware/current-phase.js';
12
+ import { readFileSync } from 'node:fs';
12
13
 
13
14
  const __filename = fileURLToPath(import.meta.url);
14
15
  const __dirname = dirname(__filename);
@@ -35,6 +36,14 @@ export function createApp(config) {
35
36
  // Store config for access in routes/services
36
37
  app.locals.projectDir = config.projectDir;
37
38
 
39
+ // Read dashboard version from package.json for footer display
40
+ try {
41
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
42
+ app.locals.dashboardVersion = pkg.version || '0.0.0';
43
+ } catch (_e) {
44
+ app.locals.dashboardVersion = '0.0.0';
45
+ }
46
+
38
47
  // View engine setup -- all paths use path.join (cross-platform)
39
48
  app.set('views', join(__dirname, 'views'));
40
49
  app.set('view engine', 'ejs');
@@ -1,20 +1,23 @@
1
1
  import { Router } from 'express';
2
2
  import { getHomepage } from '../services/project.service.js';
3
3
  import { getDashboardData } from '../services/dashboard.service.js';
4
+ import { listPendingTodos } from '../services/todo.service.js';
4
5
 
5
6
  const router = Router();
6
7
 
7
8
  router.get('/', async (req, res) => {
8
9
  const projectDir = req.app.locals.projectDir;
9
10
 
10
- const [homepageData, dashboardData] = await Promise.all([
11
+ const [homepageData, dashboardData, pendingTodos] = await Promise.all([
11
12
  getHomepage(projectDir),
12
- getDashboardData(projectDir)
13
+ getDashboardData(projectDir),
14
+ listPendingTodos(projectDir).catch(() => [])
13
15
  ]);
14
16
 
15
17
  const templateData = {
16
18
  ...homepageData,
17
19
  ...dashboardData,
20
+ pendingTodoCount: pendingTodos.length,
18
21
  activePage: 'dashboard',
19
22
  currentPath: '/',
20
23
  breadcrumbs: []
@@ -5,6 +5,7 @@ import { parseStateFile, derivePhaseStatuses } from '../services/dashboard.servi
5
5
  import { listPendingTodos, getTodoDetail, createTodo, completeTodo } from '../services/todo.service.js';
6
6
  import { getAllMilestones, getMilestoneDetail } from '../services/milestone.service.js';
7
7
  import { getProjectAnalytics } from '../services/analytics.service.js';
8
+ import { listNotes } from '../services/notes.service.js';
8
9
 
9
10
  const router = Router();
10
11
 
@@ -81,8 +82,8 @@ router.get('/phases/:phaseId/:planId/:docType', async (req, res) => {
81
82
  }
82
83
 
83
84
  // Validate docType
84
- if (docType !== 'plan' && docType !== 'summary') {
85
- const err = new Error('Document type must be "plan" or "summary"');
85
+ if (docType !== 'plan' && docType !== 'summary' && docType !== 'verification') {
86
+ const err = new Error('Document type must be "plan", "summary", or "verification"');
86
87
  err.status = 404;
87
88
  throw err;
88
89
  }
@@ -91,12 +92,13 @@ router.get('/phases/:phaseId/:planId/:docType', async (req, res) => {
91
92
  const doc = await getPhaseDocument(projectDir, phaseId, planId, docType);
92
93
 
93
94
  if (!doc) {
94
- const err = new Error(`${docType === 'plan' ? 'Plan' : 'Summary'} ${planId} not found for phase ${phaseId}`);
95
+ const labels = { plan: 'Plan', summary: 'Summary', verification: 'Verification' };
96
+ const err = new Error(`${labels[docType] || docType} ${planId} not found for phase ${phaseId}`);
95
97
  err.status = 404;
96
98
  throw err;
97
99
  }
98
100
 
99
- const docLabel = docType === 'plan' ? 'Plan' : 'Summary';
101
+ const docLabel = docType === 'plan' ? 'Plan' : docType === 'verification' ? 'Verification' : 'Summary';
100
102
  const templateData = {
101
103
  title: `${docLabel} ${planId} — Phase ${phaseId}: ${doc.phaseName}`,
102
104
  activePage: 'phases',
@@ -271,13 +273,20 @@ router.post('/todos/:id/done', async (req, res) => {
271
273
 
272
274
  router.get('/milestones', async (req, res) => {
273
275
  const projectDir = req.app.locals.projectDir;
274
- const milestoneData = await getAllMilestones(projectDir);
276
+ const [milestoneData, roadmapData, stateData] = await Promise.all([
277
+ getAllMilestones(projectDir),
278
+ getRoadmapData(projectDir),
279
+ parseStateFile(projectDir)
280
+ ]);
281
+
282
+ const phases = derivePhaseStatuses(roadmapData.phases, stateData.currentPhase);
275
283
 
276
284
  const templateData = {
277
285
  title: 'Milestones',
278
286
  activePage: 'milestones',
279
287
  currentPath: '/milestones',
280
288
  breadcrumbs: [{ label: 'Milestones' }],
289
+ phases,
281
290
  ...milestoneData
282
291
  };
283
292
 
@@ -368,6 +377,27 @@ router.get('/analytics', async (req, res) => {
368
377
  }
369
378
  });
370
379
 
380
+ router.get('/notes', async (req, res) => {
381
+ const projectDir = req.app.locals.projectDir;
382
+ const notes = await listNotes(projectDir);
383
+
384
+ const templateData = {
385
+ title: 'Notes',
386
+ activePage: 'notes',
387
+ currentPath: '/notes',
388
+ breadcrumbs: [{ label: 'Notes' }],
389
+ notes
390
+ };
391
+
392
+ res.setHeader('Vary', 'HX-Request');
393
+
394
+ if (req.get('HX-Request') === 'true') {
395
+ res.render('partials/notes-content', templateData);
396
+ } else {
397
+ res.render('notes', templateData);
398
+ }
399
+ });
400
+
371
401
  router.get('/roadmap', async (req, res) => {
372
402
  const projectDir = req.app.locals.projectDir;
373
403
  const [roadmapData, stateData] = await Promise.all([
@@ -71,9 +71,9 @@ export async function getProjectAnalytics(projectDir) {
71
71
  const scopePattern = new RegExp(`\\(${phaseNum}-`);
72
72
  const commitCount = allLogLines.filter(line => scopePattern.test(line)).length;
73
73
 
74
- // Phase duration from git dates
74
+ // Phase duration from git dates (match commits by scope pattern in message)
75
75
  const dateOutput = await git(projectDir, [
76
- 'log', '--format=%aI', '--', `.planning/phases/${dir.name}/`
76
+ 'log', '--all', `--format=%aI`, `--grep=(${phaseNum}-`
77
77
  ]);
78
78
  const dates = dateOutput.trim().split('\n').filter(Boolean).map(d => new Date(d));
79
79
  let duration = null;
@@ -82,8 +82,6 @@ export async function getProjectAnalytics(projectDir) {
82
82
  const latest = new Date(Math.max(...dates));
83
83
  const days = Math.round((latest - earliest) / (1000 * 60 * 60 * 24));
84
84
  duration = `${days}d`;
85
- } else if (dates.length === 1) {
86
- duration = '0d';
87
85
  }
88
86
 
89
87
  // Plan count
@@ -0,0 +1,50 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { readMarkdownFile } from '../repositories/planning.repository.js';
4
+
5
+ /**
6
+ * List all notes from .planning/notes/*.md, sorted by date descending.
7
+ *
8
+ * @param {string} projectDir - Absolute path to the project root
9
+ * @returns {Promise<Array<{filename: string, title: string, date: string|null, promoted: boolean, html: string}>>}
10
+ */
11
+ export async function listNotes(projectDir) {
12
+ const notesDir = join(projectDir, '.planning', 'notes');
13
+
14
+ let entries;
15
+ try {
16
+ entries = await readdir(notesDir);
17
+ } catch (err) {
18
+ if (err.code === 'ENOENT') return [];
19
+ throw err;
20
+ }
21
+
22
+ const mdFiles = entries.filter(f => f.endsWith('.md')).sort().reverse();
23
+
24
+ const results = await Promise.allSettled(
25
+ mdFiles.map(f => readMarkdownFile(join(notesDir, f)))
26
+ );
27
+
28
+ const notes = [];
29
+ for (let i = 0; i < mdFiles.length; i++) {
30
+ const result = results[i];
31
+ if (result.status !== 'fulfilled') continue;
32
+
33
+ const { frontmatter, html } = result.value;
34
+ const filename = mdFiles[i];
35
+
36
+ // Derive title from filename: strip date prefix and extension, title-case
37
+ const slug = filename.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.md$/, '');
38
+ const title = slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
39
+
40
+ notes.push({
41
+ filename,
42
+ title,
43
+ date: frontmatter.date || null,
44
+ promoted: !!frontmatter.promoted,
45
+ html
46
+ });
47
+ }
48
+
49
+ return notes;
50
+ }
@@ -202,9 +202,14 @@ export async function getPhaseDocument(projectDir, phaseId, planId, docType) {
202
202
 
203
203
  // Try plan ID-prefixed filename first, then fall back to plain PLAN.md
204
204
  // Supports both "01-01-PLAN.md" and "PLAN.md" naming conventions
205
- const fileNames = docType === 'plan'
206
- ? [`${planId}-PLAN.md`, 'PLAN.md']
207
- : [`SUMMARY-${planId}.md`];
205
+ let fileNames;
206
+ if (docType === 'plan') {
207
+ fileNames = [`${planId}-PLAN.md`, 'PLAN.md'];
208
+ } else if (docType === 'verification') {
209
+ fileNames = ['VERIFICATION.md'];
210
+ } else {
211
+ fileNames = [`SUMMARY-${planId}.md`];
212
+ }
208
213
 
209
214
  for (const fileName of fileNames) {
210
215
  const filePath = validatePath(phaseFullPath, fileName);
@@ -13,7 +13,7 @@
13
13
  </details>
14
14
  <% } %>
15
15
  <div class="error-card__action">
16
- <a href="javascript:history.back()">Go back</a> | <a href="/">Return to Dashboard</a>
16
+ <button onclick="history.back()" class="outline">Go back</button> | <a href="/">Return to Dashboard</a>
17
17
  </div>
18
18
  </div>
19
19
 
@@ -0,0 +1,5 @@
1
+ <%- include('partials/layout-top', { title: 'Notes', activePage: 'notes' }) %>
2
+
3
+ <%- include('partials/notes-content') %>
4
+
5
+ <%- include('partials/layout-bottom') %>
@@ -12,19 +12,23 @@
12
12
  <div class="grid">
13
13
  <article>
14
14
  <header>Total Commits</header>
15
- <strong style="font-size: 2rem;"><%= analytics.summary.totalCommits %></strong>
15
+ <strong class="stat-value"><%= analytics.summary.totalCommits %></strong>
16
+ <span class="stat-unit">commits</span>
16
17
  </article>
17
18
  <article>
18
19
  <header>Total Phases</header>
19
- <strong style="font-size: 2rem;"><%= analytics.summary.totalPhases %></strong>
20
+ <strong class="stat-value"><%= analytics.summary.totalPhases %></strong>
21
+ <span class="stat-unit">phases</span>
20
22
  </article>
21
23
  <article>
22
24
  <header>Avg Duration</header>
23
- <strong style="font-size: 2rem;"><%= analytics.summary.avgDuration %></strong>
25
+ <strong class="stat-value"><%= analytics.summary.avgDuration %></strong>
26
+ <span class="stat-unit">days</span>
24
27
  </article>
25
28
  <article>
26
29
  <header>Lines Changed</header>
27
- <strong style="font-size: 2rem;"><%= analytics.summary.totalLinesChanged.toLocaleString() %></strong>
30
+ <strong class="stat-value"><%= analytics.summary.totalLinesChanged.toLocaleString() %></strong>
31
+ <span class="stat-unit">lines</span>
28
32
  </article>
29
33
  </div>
30
34
  <% } %>
@@ -64,6 +68,21 @@
64
68
  </table>
65
69
  </div>
66
70
  </article>
71
+
72
+ <% const maxCommits = Math.max(...analytics.phases.map(p => p.commitCount), 1); %>
73
+ <article>
74
+ <header>Commits by Phase</header>
75
+ <div style="padding: var(--space-md) var(--space-lg);">
76
+ <% analytics.phases.sort((a, b) => a.phaseId.localeCompare(b.phaseId)).forEach(phase => { %>
77
+ <div class="bar-chart-row">
78
+ <span class="bar-chart-label"><%= phase.phaseId %> - <%= phase.phaseName %></span>
79
+ <div class="bar-chart-bar" style="width: <%= (phase.commitCount / maxCommits * 100) %>%">
80
+ <%= phase.commitCount %>
81
+ </div>
82
+ </div>
83
+ <% }) %>
84
+ </div>
85
+ </article>
67
86
  <% } else if (typeof analytics === 'undefined' || !analytics.summary) { %>
68
87
  <%- include('empty-state', { icon: '📊', title: 'No analytics data available', action: '' }) %>
69
88
  <% } else { %>
@@ -1,73 +1,79 @@
1
1
  <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
2
  <h1><%= projectName %></h1>
3
3
 
4
- <!-- Project Overview Card -->
5
- <article>
6
- <header>
7
- <strong>Current Phase</strong>
8
- </header>
9
- <% if (currentPhase.id > 0) { %>
10
- <p>
11
- <span class="status-badge" data-status="in-progress">Phase <%= currentPhase.id %></span>
12
- <%= currentPhase.name %>
13
- </p>
14
- <p><small><%= currentPhase.planStatus %></small></p>
15
- <% } else { %>
16
- <p>No active phase. Project has not started yet.</p>
17
- <% } %>
4
+ <!-- Hero Stat Row -->
5
+ <div class="grid">
6
+ <!-- Current Phase -->
7
+ <article>
8
+ <header><strong>Current Phase</strong></header>
9
+ <% if (currentPhase.id > 0) { %>
10
+ <p class="stat-value">
11
+ <a href="/phases/<%= String(currentPhase.id).padStart(2, '0') %>"
12
+ hx-get="/phases/<%= String(currentPhase.id).padStart(2, '0') %>"
13
+ hx-target="#main-content"
14
+ hx-push-url="true">
15
+ <span class="status-badge" data-status="in-progress">Phase <%= currentPhase.id %></span>
16
+ <%= currentPhase.name %>
17
+ </a>
18
+ </p>
19
+ <% } else { %>
20
+ <p class="stat-value">No active phase</p>
21
+ <% } %>
22
+ </article>
18
23
 
19
- <% if (lastActivity.date) { %>
20
- <p><small>Last activity: <%= lastActivity.date %> &mdash; <%= lastActivity.description %></small></p>
21
- <% } %>
22
- </article>
24
+ <!-- Progress -->
25
+ <article>
26
+ <header><strong>Progress</strong></header>
27
+ <p class="stat-value"><%= progress %>%</p>
28
+ <progress value="<%= progress %>" max="100"></progress>
29
+ </article>
23
30
 
24
- <!-- Overall Progress -->
25
- <article>
26
- <header>
27
- <strong>Overall Progress</strong>
28
- </header>
29
- <progress value="<%= progress %>" max="100"></progress>
30
- <p><%= progress %>% complete (<%= phases.filter(p => p.status === 'complete').length %> of <%= phases.length %> phases)</p>
31
- </article>
31
+ <!-- Pending Todos -->
32
+ <article>
33
+ <header><strong>Pending Todos</strong></header>
34
+ <p class="stat-value">
35
+ <a href="/todos"
36
+ hx-get="/todos"
37
+ hx-target="#main-content"
38
+ hx-push-url="true">
39
+ <%= typeof pendingTodoCount !== 'undefined' ? pendingTodoCount : 0 %>
40
+ </a>
41
+ </p>
42
+ </article>
32
43
 
33
- <!-- Phase List -->
34
- <article>
35
- <header>
36
- <strong>All Phases</strong>
37
- </header>
38
- <% if (phases.length > 0) { %>
39
- <div class="table-wrap">
40
- <table>
41
- <thead>
42
- <tr>
43
- <th scope="col">Phase</th>
44
- <th scope="col">Name</th>
45
- <th scope="col">Description</th>
46
- <th scope="col">Status</th>
47
- </tr>
48
- </thead>
49
- <tbody>
50
- <% phases.forEach(function(phase) { %>
51
- <tr>
52
- <td><%= String(phase.id).padStart(2, '0') %></td>
53
- <td><a href="/phases/<%= String(phase.id).padStart(2, '0') %>"><%= phase.name %></a></td>
54
- <td><%= phase.description %></td>
55
- <td>
56
- <span class="status-badge" data-status="<%= phase.status %>">
57
- <%= phase.status.replace('-', ' ') %>
58
- </span>
59
- </td>
60
- </tr>
61
- <% }); %>
62
- </tbody>
63
- </table>
64
- </div>
65
- <% } else { %>
66
- <p>No phases found. Add a ROADMAP.md file to your .planning/ directory to see phases here.</p>
67
- <% } %>
68
- </article>
44
+ <!-- Last Activity -->
45
+ <article>
46
+ <header><strong>Last Activity</strong></header>
47
+ <% if (lastActivity.date) { %>
48
+ <p class="stat-value"><%= lastActivity.date %></p>
49
+ <p><small><%= lastActivity.description %></small></p>
50
+ <% } else { %>
51
+ <p class="stat-value">None</p>
52
+ <% } %>
53
+ </article>
54
+ </div>
55
+
56
+ <!-- Quick Actions -->
57
+ <div>
58
+ <% var phaseId = String(currentPhase.id).padStart(2, '0'); %>
59
+ <a role="button" class="outline"
60
+ href="/phases/<%= phaseId %>"
61
+ hx-get="/phases/<%= phaseId %>"
62
+ hx-target="#main-content"
63
+ hx-push-url="true">Current Phase</a>
64
+ <a role="button" class="outline"
65
+ href="/roadmap"
66
+ hx-get="/roadmap"
67
+ hx-target="#main-content"
68
+ hx-push-url="true">Roadmap</a>
69
+ <a role="button" class="outline"
70
+ href="/todos/new"
71
+ hx-get="/todos/new"
72
+ hx-target="#main-content"
73
+ hx-push-url="true">Create Todo</a>
74
+ </div>
69
75
 
70
- <!-- README Content (if available) -->
76
+ <!-- Project Notes (if available) -->
71
77
  <% if (content && content !== '<p>No project README found.</p>') { %>
72
78
  <article>
73
79
  <header>
@@ -12,5 +12,37 @@
12
12
 
13
13
  <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
14
14
  <script>
15
- mermaid.initialize({ startOnLoad: true, theme: 'default', securityLevel: 'loose' });
15
+ (function() {
16
+ function detectDark() {
17
+ var explicit = document.documentElement.dataset.theme;
18
+ if (explicit) return explicit === 'dark';
19
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
20
+ }
21
+ var mermaidTheme = detectDark() ? 'dark' : 'default';
22
+
23
+ // Store original source before mermaid processes it
24
+ document.querySelectorAll('.mermaid').forEach(function(el) {
25
+ el.dataset.source = el.textContent;
26
+ });
27
+
28
+ mermaid.initialize({ startOnLoad: true, theme: mermaidTheme, securityLevel: 'loose' });
29
+
30
+ // Watch for theme changes and re-render
31
+ var observer = new MutationObserver(function(mutations) {
32
+ mutations.forEach(function(mutation) {
33
+ if (mutation.attributeName === 'data-theme') {
34
+ var newTheme = detectDark() ? 'dark' : 'default';
35
+ mermaid.initialize({ startOnLoad: false, theme: newTheme, securityLevel: 'loose' });
36
+ document.querySelectorAll('.mermaid').forEach(function(el) {
37
+ if (el.dataset.source) {
38
+ el.removeAttribute('data-processed');
39
+ el.innerHTML = el.dataset.source;
40
+ }
41
+ });
42
+ mermaid.run();
43
+ }
44
+ });
45
+ });
46
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
47
+ })();
16
48
  </script>
@@ -1,3 +1,3 @@
1
1
  <footer>
2
- <small>PBR Dashboard v0.1.0</small>
2
+ <small>PBR Dashboard v<%= typeof dashboardVersion !== 'undefined' ? dashboardVersion : '0.1.0' %></small>
3
3
  </footer>
@@ -8,7 +8,7 @@
8
8
  </ul>
9
9
  <ul>
10
10
  <li>
11
- <span id="sse-status" data-connected="false" aria-label="SSE connection status"></span>
11
+ <span id="sse-status" data-connected="false" aria-label="SSE connection status" title="Live updates: disconnected"></span>
12
12
  </li>
13
13
  <li>
14
14
  <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">🌙</button>
@@ -3,7 +3,11 @@
3
3
 
4
4
  <% if (active.length > 0) { %>
5
5
  <h2>Active</h2>
6
- <% active.forEach(function(m) { %>
6
+ <% active.forEach(function(m) {
7
+ var msPhases = (typeof phases !== 'undefined' && phases) ? phases.filter(function(p) { return p.id >= m.startPhase && p.id <= m.endPhase; }) : [];
8
+ var totalPhases = msPhases.length;
9
+ var completedPhases = msPhases.filter(function(p) { return p.status === 'complete'; }).length;
10
+ %>
7
11
  <article>
8
12
  <header>
9
13
  <strong><%= m.name %></strong>
@@ -12,6 +16,10 @@
12
16
  <p>
13
17
  Phases <%= m.startPhase %> &ndash; <%= m.endPhase %>
14
18
  </p>
19
+ <% if (totalPhases > 0) { %>
20
+ <progress value="<%= completedPhases %>" max="<%= totalPhases %>"></progress>
21
+ <p><small><%= completedPhases %> of <%= totalPhases %> phases complete</small></p>
22
+ <% } %>
15
23
  </article>
16
24
  <% }); %>
17
25
  <% } %>
@@ -0,0 +1,23 @@
1
+ <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
+ <h1>Notes</h1>
3
+
4
+ <% if (notes.length === 0) { %>
5
+ <%- include('empty-state', { icon: 'N', title: 'No notes yet', action: 'Use /pbr:note to create project notes.' }) %>
6
+ <% } else { %>
7
+ <% notes.forEach(function(note) { %>
8
+ <article>
9
+ <header>
10
+ <strong><%= note.title %></strong>
11
+ <% if (note.promoted) { %>
12
+ &nbsp; <span class="status-badge" data-status="complete">promoted</span>
13
+ <% } %>
14
+ <% if (note.date) { %>
15
+ <small style="float:right"><%= note.date %></small>
16
+ <% } %>
17
+ </header>
18
+ <div class="markdown-body">
19
+ <%- note.html %>
20
+ </div>
21
+ </article>
22
+ <% }); %>
23
+ <% } %>
@@ -1,7 +1,7 @@
1
1
  <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
2
  <h1>Phase <%= phaseId %>: <%= phaseName %></h1>
3
3
 
4
- <p><a href="/">&larr; Back to Dashboard</a></p>
4
+ <p><a href="/phases" hx-get="/phases" hx-target="#main-content" hx-push-url="true">&larr; Back to Phases</a></p>
5
5
 
6
6
  <% if (verification) { %>
7
7
  <!-- Verification Summary Card -->
@@ -73,6 +73,9 @@
73
73
  <% if (plan.summary) { %>
74
74
  &nbsp;|&nbsp; <a href="/phases/<%= phaseId %>/<%= plan.planId %>/summary">View Summary</a>
75
75
  <% } %>
76
+ <% if (verification) { %>
77
+ &nbsp;|&nbsp; <a href="/phases/<%= phaseId %>/<%= plan.planId %>/verification">View Verification</a>
78
+ <% } %>
76
79
  </p>
77
80
 
78
81
  <% if (plan.summary) { %>
@@ -1,5 +1,5 @@
1
1
  <%
2
- var docLabel = docType === 'plan' ? 'Plan' : 'Summary';
2
+ var docLabel = docType === 'plan' ? 'Plan' : docType === 'verification' ? 'Verification' : 'Summary';
3
3
  %>
4
4
  <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
5
5
  <h1><%= docLabel %> <%= planId %></h1>
@@ -61,7 +61,10 @@
61
61
  <tr>
62
62
  <td><%= String(phase.id).padStart(2, '0') %></td>
63
63
  <td>
64
- <a href="/phases/<%= String(phase.id).padStart(2, '0') %>">
64
+ <a href="/phases/<%= String(phase.id).padStart(2, '0') %>"
65
+ hx-get="/phases/<%= String(phase.id).padStart(2, '0') %>"
66
+ hx-target="#main-content"
67
+ hx-push-url="true">
65
68
  <%= phase.name %>
66
69
  </a>
67
70
  </td>
@@ -97,7 +100,10 @@
97
100
  <tr>
98
101
  <td><%= String(phase.id).padStart(2, '0') %></td>
99
102
  <td>
100
- <a href="/phases/<%= String(phase.id).padStart(2, '0') %>">
103
+ <a href="/phases/<%= String(phase.id).padStart(2, '0') %>"
104
+ hx-get="/phases/<%= String(phase.id).padStart(2, '0') %>"
105
+ hx-target="#main-content"
106
+ hx-push-url="true">
101
107
  <%= phase.name %>
102
108
  </a>
103
109
  </td>
@@ -67,7 +67,10 @@
67
67
  <tr>
68
68
  <td><%= String(phase.id).padStart(2, '0') %></td>
69
69
  <td>
70
- <a href="/phases/<%= String(phase.id).padStart(2, '0') %>">
70
+ <a href="/phases/<%= String(phase.id).padStart(2, '0') %>"
71
+ hx-get="/phases/<%= String(phase.id).padStart(2, '0') %>"
72
+ hx-target="#main-content"
73
+ hx-push-url="true">
71
74
  <%= phase.name %>
72
75
  </a>
73
76
  </td>
@@ -80,7 +83,10 @@
80
83
  <td>
81
84
  <% if (phase.dependencies && phase.dependencies.length > 0) { %>
82
85
  <% phase.dependencies.forEach(function(dep, idx) { %>
83
- <a href="/phases/<%= String(dep).padStart(2, '0') %>"><%= String(dep).padStart(2, '0') %></a><% if (idx < phase.dependencies.length - 1) { %>, <% } %>
86
+ <a href="/phases/<%= String(dep).padStart(2, '0') %>"
87
+ hx-get="/phases/<%= String(dep).padStart(2, '0') %>"
88
+ hx-target="#main-content"
89
+ hx-push-url="true"><%= String(dep).padStart(2, '0') %></a><% if (idx < phase.dependencies.length - 1) { %>, <% } %>
84
90
  <% }); %>
85
91
  <% } else { %>
86
92
  <small>None</small>
@@ -113,7 +119,10 @@
113
119
  <tr>
114
120
  <td><%= String(phase.id).padStart(2, '0') %></td>
115
121
  <td>
116
- <a href="/phases/<%= String(phase.id).padStart(2, '0') %>">
122
+ <a href="/phases/<%= String(phase.id).padStart(2, '0') %>"
123
+ hx-get="/phases/<%= String(phase.id).padStart(2, '0') %>"
124
+ hx-target="#main-content"
125
+ hx-push-url="true">
117
126
  <%= phase.name %>
118
127
  </a>
119
128
  </td>
@@ -126,7 +135,10 @@
126
135
  <td>
127
136
  <% if (phase.dependencies && phase.dependencies.length > 0) { %>
128
137
  <% phase.dependencies.forEach(function(dep, idx) { %>
129
- <a href="/phases/<%= String(dep).padStart(2, '0') %>"><%= String(dep).padStart(2, '0') %></a><% if (idx < phase.dependencies.length - 1) { %>, <% } %>
138
+ <a href="/phases/<%= String(dep).padStart(2, '0') %>"
139
+ hx-get="/phases/<%= String(dep).padStart(2, '0') %>"
140
+ hx-target="#main-content"
141
+ hx-push-url="true"><%= String(dep).padStart(2, '0') %></a><% if (idx < phase.dependencies.length - 1) { %>, <% } %>
130
142
  <% }); %>
131
143
  <% } else { %>
132
144
  <small>None</small>
@@ -71,6 +71,14 @@
71
71
  Todos
72
72
  </a>
73
73
  </li>
74
+ <li>
75
+ <a href="/notes"
76
+ hx-get="/notes"
77
+ hx-target="#main-content"
78
+ hx-push-url="true"<%= typeof activePage !== 'undefined' && activePage === 'notes' ? ' aria-current="page"' : '' %>>
79
+ Notes
80
+ </a>
81
+ </li>
74
82
  </ul>
75
83
  </details>
76
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sienklogic/plan-build-run",
3
- "version": "2.17.1",
3
+ "version": "2.18.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.17.1",
4
+ "version": "2.18.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.17.1",
4
+ "version": "2.18.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.17.1",
3
+ "version": "2.18.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",
@@ -1 +0,0 @@
1
- <%# This file is deprecated. See partials/layout-top.ejs and partials/layout-bottom.ejs %>