@sienklogic/plan-build-run 2.24.0 → 2.26.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 (58) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +62 -13
  3. package/dashboard/package.json +1 -1
  4. package/dashboard/public/css/layout.css +128 -21
  5. package/dashboard/public/css/status-colors.css +14 -2
  6. package/dashboard/public/css/tokens.css +36 -0
  7. package/dashboard/src/middleware/current-phase.js +2 -1
  8. package/dashboard/src/routes/events.routes.js +49 -0
  9. package/dashboard/src/routes/pages.routes.js +250 -1
  10. package/dashboard/src/services/config.service.js +140 -0
  11. package/dashboard/src/services/dashboard.service.js +156 -11
  12. package/dashboard/src/services/log.service.js +105 -0
  13. package/dashboard/src/services/notes.service.js +16 -0
  14. package/dashboard/src/services/phase.service.js +58 -9
  15. package/dashboard/src/services/requirements.service.js +130 -0
  16. package/dashboard/src/services/research.service.js +137 -0
  17. package/dashboard/src/services/todo.service.js +30 -0
  18. package/dashboard/src/views/config.ejs +5 -0
  19. package/dashboard/src/views/logs.ejs +3 -0
  20. package/dashboard/src/views/note-detail.ejs +3 -0
  21. package/dashboard/src/views/partials/activity-feed.ejs +12 -0
  22. package/dashboard/src/views/partials/config-content.ejs +196 -0
  23. package/dashboard/src/views/partials/dashboard-content.ejs +71 -46
  24. package/dashboard/src/views/partials/log-entries-content.ejs +17 -0
  25. package/dashboard/src/views/partials/logs-content.ejs +131 -0
  26. package/dashboard/src/views/partials/note-detail-content.ejs +22 -0
  27. package/dashboard/src/views/partials/notes-content.ejs +7 -1
  28. package/dashboard/src/views/partials/phase-content.ejs +181 -146
  29. package/dashboard/src/views/partials/phase-timeline.ejs +16 -0
  30. package/dashboard/src/views/partials/requirements-content.ejs +44 -0
  31. package/dashboard/src/views/partials/research-content.ejs +49 -0
  32. package/dashboard/src/views/partials/research-detail-content.ejs +23 -0
  33. package/dashboard/src/views/partials/sidebar.ejs +63 -26
  34. package/dashboard/src/views/partials/todos-done-content.ejs +44 -0
  35. package/dashboard/src/views/requirements.ejs +3 -0
  36. package/dashboard/src/views/research-detail.ejs +3 -0
  37. package/dashboard/src/views/research.ejs +3 -0
  38. package/dashboard/src/views/todos-done.ejs +3 -0
  39. package/package.json +1 -1
  40. package/plugins/copilot-pbr/agents/dev-sync.agent.md +114 -0
  41. package/plugins/copilot-pbr/hooks/hooks.json +12 -0
  42. package/plugins/copilot-pbr/plugin.json +1 -1
  43. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  44. package/plugins/cursor-pbr/agents/dev-sync.md +113 -0
  45. package/plugins/cursor-pbr/hooks/hooks.json +10 -0
  46. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  47. package/plugins/pbr/agents/dev-sync.md +120 -0
  48. package/plugins/pbr/hooks/hooks.json +10 -0
  49. package/plugins/pbr/scripts/config-schema.json +4 -1
  50. package/plugins/pbr/scripts/local-llm/health.js +4 -1
  51. package/plugins/pbr/scripts/local-llm/operations/classify-commit.js +68 -0
  52. package/plugins/pbr/scripts/local-llm/operations/classify-file-intent.js +73 -0
  53. package/plugins/pbr/scripts/local-llm/operations/triage-test-output.js +72 -0
  54. package/plugins/pbr/scripts/post-bash-triage.js +132 -0
  55. package/plugins/pbr/scripts/post-write-dispatch.js +44 -0
  56. package/plugins/pbr/scripts/pre-bash-dispatch.js +17 -11
  57. package/plugins/pbr/scripts/status-line.js +50 -5
  58. package/plugins/pbr/scripts/validate-commit.js +66 -2
@@ -0,0 +1,196 @@
1
+ <div class="config-page">
2
+ <div class="page-header">
3
+ <h1>Config</h1>
4
+ <p class="subtitle">Manage <code>.planning/config.json</code></p>
5
+ </div>
6
+
7
+ <% if (!config || !config.version) { %>
8
+ <div class="card">
9
+ <div class="card__body">
10
+ <p>No <code>.planning/config.json</code> found for this project.</p>
11
+ </div>
12
+ </div>
13
+ <% } else { %>
14
+
15
+ <form
16
+ hx-post="/api/config"
17
+ hx-target="#config-feedback"
18
+ hx-swap="innerHTML"
19
+ id="config-form"
20
+ >
21
+ <%# ── Common Settings ─────────────────────────────────────────── %>
22
+ <details class="card config-section" open>
23
+ <summary class="card__header"><h2>Common Settings</h2></summary>
24
+ <div class="card__body">
25
+
26
+ <%# Mode & Depth %>
27
+ <div class="config-row">
28
+ <label for="cfg-mode">Mode</label>
29
+ <select id="cfg-mode" name="mode">
30
+ <% ['normal', 'strict', 'relaxed'].forEach(opt => { %>
31
+ <option value="<%= opt %>" <%= config.mode === opt ? 'selected' : '' %>><%= opt %></option>
32
+ <% }); %>
33
+ </select>
34
+ </div>
35
+
36
+ <div class="config-row">
37
+ <label for="cfg-depth">Depth</label>
38
+ <select id="cfg-depth" name="depth">
39
+ <% ['minimal', 'standard', 'comprehensive'].forEach(opt => { %>
40
+ <option value="<%= opt %>" <%= config.depth === opt ? 'selected' : '' %>><%= opt %></option>
41
+ <% }); %>
42
+ </select>
43
+ </div>
44
+
45
+ <div class="config-row">
46
+ <label for="cfg-context-strategy">Context Strategy</label>
47
+ <select id="cfg-context-strategy" name="context_strategy">
48
+ <% ['conservative', 'balanced', 'aggressive'].forEach(opt => { %>
49
+ <option value="<%= opt %>" <%= config.context_strategy === opt ? 'selected' : '' %>><%= opt %></option>
50
+ <% }); %>
51
+ </select>
52
+ </div>
53
+
54
+ <%# Features toggles %>
55
+ <% if (config.features && typeof config.features === 'object') { %>
56
+ <fieldset class="config-fieldset">
57
+ <legend>Features</legend>
58
+ <% for (const [key, val] of Object.entries(config.features)) { %>
59
+ <div class="config-toggle-row">
60
+ <label>
61
+ <input
62
+ type="checkbox"
63
+ role="switch"
64
+ name="features.<%= key %>"
65
+ value="on"
66
+ <%= val ? 'checked' : '' %>
67
+ />
68
+ <%= key %>
69
+ </label>
70
+ </div>
71
+ <% } %>
72
+ </fieldset>
73
+ <% } %>
74
+
75
+ <%# Gates toggles %>
76
+ <% if (config.gates && typeof config.gates === 'object') { %>
77
+ <fieldset class="config-fieldset">
78
+ <legend>Gates</legend>
79
+ <% for (const [key, val] of Object.entries(config.gates)) { %>
80
+ <div class="config-toggle-row">
81
+ <label>
82
+ <input
83
+ type="checkbox"
84
+ role="switch"
85
+ name="gates.<%= key %>"
86
+ value="on"
87
+ <%= val ? 'checked' : '' %>
88
+ />
89
+ <%= key %>
90
+ </label>
91
+ </div>
92
+ <% } %>
93
+ </fieldset>
94
+ <% } %>
95
+
96
+ <%# Safety toggles %>
97
+ <% if (config.safety && typeof config.safety === 'object') { %>
98
+ <fieldset class="config-fieldset">
99
+ <legend>Safety</legend>
100
+ <% for (const [key, val] of Object.entries(config.safety)) { %>
101
+ <div class="config-toggle-row">
102
+ <label>
103
+ <input
104
+ type="checkbox"
105
+ role="switch"
106
+ name="safety.<%= key %>"
107
+ value="on"
108
+ <%= val ? 'checked' : '' %>
109
+ />
110
+ <%= key %>
111
+ </label>
112
+ </div>
113
+ <% } %>
114
+ </fieldset>
115
+ <% } %>
116
+
117
+ <%# Model selections %>
118
+ <% if (config.models && typeof config.models === 'object') { %>
119
+ <fieldset class="config-fieldset">
120
+ <legend>Models</legend>
121
+ <% for (const [key, val] of Object.entries(config.models)) { %>
122
+ <div class="config-row">
123
+ <label for="cfg-model-<%= key %>"><%= key %></label>
124
+ <input
125
+ id="cfg-model-<%= key %>"
126
+ type="text"
127
+ name="models.<%= key %>"
128
+ value="<%= val %>"
129
+ />
130
+ </div>
131
+ <% } %>
132
+ </fieldset>
133
+ <% } %>
134
+
135
+ <%# Parallelization number inputs %>
136
+ <% if (config.parallelization && typeof config.parallelization === 'object') { %>
137
+ <fieldset class="config-fieldset">
138
+ <legend>Parallelization</legend>
139
+ <% for (const [key, val] of Object.entries(config.parallelization)) { %>
140
+ <div class="config-row">
141
+ <label for="cfg-par-<%= key %>"><%= key %></label>
142
+ <% if (typeof val === 'number') { %>
143
+ <input id="cfg-par-<%= key %>" type="number" name="parallelization.<%= key %>" value="<%= val %>" min="1" />
144
+ <% } else { %>
145
+ <input id="cfg-par-<%= key %>" type="text" name="parallelization.<%= key %>" value="<%= val %>" />
146
+ <% } %>
147
+ </div>
148
+ <% } %>
149
+ </fieldset>
150
+ <% } %>
151
+
152
+ <%# Planning number/text inputs %>
153
+ <% if (config.planning && typeof config.planning === 'object') { %>
154
+ <fieldset class="config-fieldset">
155
+ <legend>Planning</legend>
156
+ <% for (const [key, val] of Object.entries(config.planning)) { %>
157
+ <div class="config-row">
158
+ <label for="cfg-plan-<%= key %>"><%= key %></label>
159
+ <% if (typeof val === 'number') { %>
160
+ <input id="cfg-plan-<%= key %>" type="number" name="planning.<%= key %>" value="<%= val %>" />
161
+ <% } else { %>
162
+ <input id="cfg-plan-<%= key %>" type="text" name="planning.<%= key %>" value="<%= val %>" />
163
+ <% } %>
164
+ </div>
165
+ <% } %>
166
+ </fieldset>
167
+ <% } %>
168
+
169
+ </div><%# end card__body %>
170
+ </details>
171
+
172
+ <%# ── Advanced (raw JSON) ─────────────────────────────────────── %>
173
+ <details class="card config-section">
174
+ <summary class="card__header"><h2>Advanced (Raw JSON)</h2></summary>
175
+ <div class="card__body">
176
+ <p class="config-hint">Edit the full config as JSON. On save, this value overrides the form above.</p>
177
+ <textarea
178
+ id="cfg-raw-json"
179
+ name="rawJson"
180
+ class="config-raw-json"
181
+ rows="24"
182
+ spellcheck="false"
183
+ ><%= JSON.stringify(config, null, 2) %></textarea>
184
+ <p class="config-hint">Tip: clear this field to save using the form controls above.</p>
185
+ </div>
186
+ </details>
187
+
188
+ <div class="config-actions">
189
+ <button type="submit">Save Config</button>
190
+ <span id="config-feedback"></span>
191
+ </div>
192
+
193
+ </form>
194
+
195
+ <% } %>
196
+ </div>
@@ -1,37 +1,49 @@
1
1
  <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
2
  <h1><%= projectName %></h1>
3
3
 
4
- <!-- Hero Stat Row -->
5
- <div class="hero-stats">
6
- <!-- Current Phase -->
7
- <article>
4
+ <!-- Status Cards -->
5
+ <div class="status-cards">
6
+ <!-- Current Phase Card -->
7
+ <article class="status-card">
8
8
  <header><strong>Current Phase</strong></header>
9
9
  <% if (currentPhase.id > 0) { %>
10
- <p class="stat-value">
10
+ <p>
11
11
  <a href="/phases/<%= String(currentPhase.id).padStart(2, '0') %>"
12
12
  hx-get="/phases/<%= String(currentPhase.id).padStart(2, '0') %>"
13
13
  hx-target="#main-content"
14
14
  hx-push-url="true">
15
- <span class="status-badge" data-status="in-progress">Phase <%= currentPhase.id %></span>
15
+ <span class="status-badge" data-status="<%= currentPhase.status %>">Phase <%= currentPhase.id %></span>
16
16
  <%= currentPhase.name %>
17
17
  </a>
18
18
  </p>
19
19
  <% } else { %>
20
- <p class="stat-value">No active phase</p>
20
+ <p>No active phase</p>
21
21
  <% } %>
22
+ <footer><small><%= currentPhase.planStatus %></small></footer>
22
23
  </article>
23
24
 
24
- <!-- Progress -->
25
- <article>
26
- <header><strong>Progress</strong></header>
27
- <p class="stat-value"><%= progress %>%</p>
28
- <progress value="<%= progress %>" max="100"></progress>
25
+ <!-- Milestone Progress Card -->
26
+ <article class="status-card">
27
+ <header><strong>Milestone Progress</strong></header>
28
+ <p>
29
+ <progress value="<%= progress %>" max="100"></progress>
30
+ <span><%= progress %>%</span>
31
+ </p>
32
+ <footer>
33
+ <small>
34
+ <% if (phases && phases.length > 0) { %>
35
+ <%= phases.filter(function(p) { return p.status === 'complete'; }).length %> of <%= phases.length %> phases complete
36
+ <% } else { %>
37
+ &mdash;
38
+ <% } %>
39
+ </small>
40
+ </footer>
29
41
  </article>
30
42
 
31
- <!-- Pending Todos -->
32
- <article>
43
+ <!-- Pending Todos Card -->
44
+ <article class="status-card">
33
45
  <header><strong>Pending Todos</strong></header>
34
- <p class="stat-value">
46
+ <p>
35
47
  <a href="/todos"
36
48
  hx-get="/todos"
37
49
  hx-target="#main-content"
@@ -39,46 +51,59 @@
39
51
  <%= typeof pendingTodoCount !== 'undefined' ? pendingTodoCount : 0 %>
40
52
  </a>
41
53
  </p>
54
+ <footer><small>P0/P1 items need attention</small></footer>
42
55
  </article>
43
56
 
44
- <!-- Last Activity -->
45
- <article>
57
+ <!-- Last Activity Card -->
58
+ <article class="status-card">
46
59
  <header><strong>Last Activity</strong></header>
47
60
  <% if (lastActivity.date) { %>
48
- <p class="stat-value"><%= lastActivity.date %></p>
49
- <p><small><%= lastActivity.description %></small></p>
61
+ <p><%= lastActivity.date %></p>
62
+ <footer><small><%= lastActivity.description %></small></footer>
50
63
  <% } else { %>
51
- <p class="stat-value">None</p>
64
+ <p>&mdash;</p>
52
65
  <% } %>
53
66
  </article>
54
67
  </div>
55
68
 
69
+ <!-- Phase Timeline -->
70
+ <section class="section-timeline">
71
+ <h2>Phase Timeline</h2>
72
+ <%- include('phase-timeline', { phases: phases, currentPhase: currentPhase }) %>
73
+ </section>
74
+
75
+ <!-- Activity Feed -->
76
+ <section class="section-activity">
77
+ <h2>Recent Activity</h2>
78
+ <%- include('activity-feed', { recentActivity: typeof recentActivity !== 'undefined' ? recentActivity : [] }) %>
79
+ </section>
80
+
56
81
  <!-- Quick Actions -->
57
82
  <div class="quick-actions">
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>
83
+ <% if (typeof quickActions !== 'undefined' && quickActions && quickActions.length > 0) { %>
84
+ <% quickActions.forEach(function(action) { %>
85
+ <a role="button" class="<%= action.primary ? '' : 'outline' %>"
86
+ href="<%= action.href %>"
87
+ hx-get="<%= action.href %>"
88
+ hx-target="#main-content"
89
+ hx-push-url="true"><%= action.label %></a>
90
+ <% }); %>
91
+ <% } else { %>
92
+ <% var phaseId = String(currentPhase.id).padStart(2, '0'); %>
93
+ <a role="button" class="outline"
94
+ href="/phases/<%= phaseId %>"
95
+ hx-get="/phases/<%= phaseId %>"
96
+ hx-target="#main-content"
97
+ hx-push-url="true">Current Phase</a>
98
+ <a role="button" class="outline"
99
+ href="/roadmap"
100
+ hx-get="/roadmap"
101
+ hx-target="#main-content"
102
+ hx-push-url="true">Roadmap</a>
103
+ <a role="button" class="outline"
104
+ href="/todos/new"
105
+ hx-get="/todos/new"
106
+ hx-target="#main-content"
107
+ hx-push-url="true">Create Todo</a>
108
+ <% } %>
74
109
  </div>
75
-
76
- <!-- Project Notes (if available) -->
77
- <% if (content && content !== '<p>No project README found.</p>') { %>
78
- <article>
79
- <header>
80
- <strong>Project Notes</strong>
81
- </header>
82
- <%- content %>
83
- </article>
84
- <% } %>
@@ -0,0 +1,17 @@
1
+ <div id="log-entries"
2
+ style="max-height:60vh;overflow-y:auto;font-family:var(--font-mono,monospace);font-size:0.78rem;background:var(--card-bg);border:1px solid var(--card-border);border-radius:var(--card-radius);padding:0.5rem">
3
+ <% if (typeof logData !== 'undefined' && logData && logData.entries.length > 0) { %>
4
+ <% logData.entries.forEach(function(entry) { %>
5
+ <div class="log-entry" style="padding:0.2rem 0;border-bottom:1px solid var(--card-border);word-break:break-all">
6
+ <span style="color:var(--muted-fg,#888)"><%= entry.timestamp ? String(entry.timestamp).slice(0,19).replace('T',' ') : '' %></span>
7
+ <% if (entry.type) { %><span class="status-badge status-badge--sm" data-status="<%= entry.type %>"><%= entry.type %></span><% } %>
8
+ <% if (entry.tool) { %><strong><%= entry.tool %></strong><% } %>
9
+ <% if (entry.message) { %><span><%= String(entry.message).slice(0,200) %></span><% } %>
10
+ </div>
11
+ <% }); %>
12
+ <% } else if (typeof logData !== 'undefined' && logData) { %>
13
+ <p style="padding:0.5rem;margin:0;color:var(--muted-fg,#888)">No entries match the current filter.</p>
14
+ <% } else { %>
15
+ <p style="padding:0.5rem;margin:0;color:var(--muted-fg,#888)">Waiting for live entries...</p>
16
+ <% } %>
17
+ </div>
@@ -0,0 +1,131 @@
1
+ <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
+ <h1>Logs</h1>
3
+
4
+ <% if (!logFiles || logFiles.length === 0) { %>
5
+ <%- include('empty-state', { icon: 'L', title: 'No log files found', action: 'Log files appear in .planning/logs/ when PBR hooks run with local LLM routing enabled.' }) %>
6
+ <% } else { %>
7
+
8
+ <div class="logs-layout" style="display:flex;gap:var(--spacing-md);align-items:flex-start">
9
+
10
+ <%# File browser sidebar %>
11
+ <aside style="min-width:220px;max-width:260px;flex-shrink:0">
12
+ <article>
13
+ <header><strong>Log Files</strong></header>
14
+ <ul style="list-style:none;padding:0;margin:0">
15
+ <% logFiles.forEach(function(lf) { %>
16
+ <li style="padding:0.25rem 0;border-bottom:1px solid var(--card-border)">
17
+ <a href="/logs?file=<%= lf.name %>"
18
+ hx-get="/logs?file=<%= lf.name %>"
19
+ hx-target="#main-content"
20
+ hx-push-url="true"
21
+ <% if (selectedFile === lf.name) { %>aria-current="page"<% } %>>
22
+ <%= lf.name %>
23
+ </a>
24
+ <br>
25
+ <small><%= Math.round(lf.size / 1024) %> KB &middot; <%= lf.modified ? lf.modified.slice(0,10) : '' %></small>
26
+ </li>
27
+ <% }); %>
28
+ </ul>
29
+ </article>
30
+ </aside>
31
+
32
+ <%# Main log viewer %>
33
+ <div style="flex:1;min-width:0">
34
+ <% if (selectedFile) { %>
35
+
36
+ <%# Filter controls %>
37
+ <form hx-get="/logs"
38
+ hx-target="#main-content"
39
+ hx-push-url="true"
40
+ style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:var(--spacing-sm)">
41
+ <input type="hidden" name="file" value="<%= selectedFile %>">
42
+ <input type="text" name="type" placeholder="Type filter (e.g. tool_use)"
43
+ value="<%= typeof filters !== 'undefined' ? filters.type : '' %>"
44
+ style="width:200px">
45
+ <input type="search" name="q" placeholder="Search entries..."
46
+ value="<%= typeof filters !== 'undefined' ? filters.q : '' %>"
47
+ style="flex:1;min-width:160px">
48
+ <button type="submit">Filter</button>
49
+ <% if (typeof filters !== 'undefined' && (filters.type || filters.q)) { %>
50
+ <a href="/logs?file=<%= selectedFile %>"
51
+ hx-get="/logs?file=<%= selectedFile %>"
52
+ hx-target="#main-content"
53
+ hx-push-url="true"
54
+ role="button" class="secondary">Clear</a>
55
+ <% } %>
56
+ </form>
57
+
58
+ <%# Auto-scroll toggle + live indicator %>
59
+ <div style="display:flex;align-items:center;gap:1rem;margin-bottom:var(--spacing-sm)">
60
+ <label style="display:flex;align-items:center;gap:0.4rem;cursor:pointer">
61
+ <input type="checkbox" id="auto-scroll-toggle" checked style="margin:0">
62
+ <small>Auto-scroll</small>
63
+ </label>
64
+ <span id="live-indicator" class="status-badge" data-status="active" style="display:none">LIVE</span>
65
+ </div>
66
+
67
+ <%# Entry list: SSE target + initial content %>
68
+ <div id="log-entries-wrap"
69
+ hx-ext="sse"
70
+ sse-connect="/logs/stream?file=<%= selectedFile %>"
71
+ sse-swap="log-entry"
72
+ hx-swap="beforeend"
73
+ hx-target="#log-entries"
74
+ style="position:relative">
75
+ <%- include('log-entries-content') %>
76
+ </div>
77
+
78
+ <%# Pagination %>
79
+ <% if (logData && logData.total > logData.pageSize) { %>
80
+ <nav style="display:flex;gap:0.5rem;margin-top:var(--spacing-sm);align-items:center">
81
+ <% const totalPages = Math.ceil(logData.total / logData.pageSize); %>
82
+ <% if (logData.page > 1) { %>
83
+ <a href="/logs?file=<%= selectedFile %>&page=<%= logData.page - 1 %>&type=<%= (filters || {}).type || '' %>&q=<%= (filters || {}).q || '' %>"
84
+ hx-get="/logs?file=<%= selectedFile %>&page=<%= logData.page - 1 %>&type=<%= (filters || {}).type || '' %>&q=<%= (filters || {}).q || '' %>"
85
+ hx-target="#main-content"
86
+ hx-push-url="true"
87
+ role="button" class="secondary">&larr; Prev</a>
88
+ <% } %>
89
+ <small>Page <%= logData.page %> of <%= totalPages %> (<%= logData.total %> entries)</small>
90
+ <% if (logData.page < totalPages) { %>
91
+ <a href="/logs?file=<%= selectedFile %>&page=<%= logData.page + 1 %>&type=<%= (filters || {}).type || '' %>&q=<%= (filters || {}).q || '' %>"
92
+ hx-get="/logs?file=<%= selectedFile %>&page=<%= logData.page + 1 %>&type=<%= (filters || {}).type || '' %>&q=<%= (filters || {}).q || '' %>"
93
+ hx-target="#main-content"
94
+ hx-push-url="true"
95
+ role="button" class="secondary">Next &rarr;</a>
96
+ <% } %>
97
+ </nav>
98
+ <% } %>
99
+
100
+ <% } else { %>
101
+ <p>Select a log file to view entries.</p>
102
+ <% } %>
103
+ </div>
104
+ </div>
105
+
106
+ <script>
107
+ (function () {
108
+ const wrap = document.getElementById('log-entries-wrap');
109
+ const toggle = document.getElementById('auto-scroll-toggle');
110
+ const indicator = document.getElementById('live-indicator');
111
+
112
+ if (!wrap || !toggle) return;
113
+
114
+ // Show live indicator when SSE connects
115
+ wrap.addEventListener('htmx:sseOpen', () => {
116
+ if (indicator) indicator.style.display = '';
117
+ });
118
+ wrap.addEventListener('htmx:sseClose', () => {
119
+ if (indicator) indicator.style.display = 'none';
120
+ });
121
+
122
+ // Auto-scroll: when a new log-entry SSE event adds content, scroll to bottom
123
+ wrap.addEventListener('htmx:afterSwap', () => {
124
+ if (toggle && toggle.checked) {
125
+ const list = document.getElementById('log-entries');
126
+ if (list) list.scrollTop = list.scrollHeight;
127
+ }
128
+ });
129
+ })();
130
+ </script>
131
+ <% } %>
@@ -0,0 +1,22 @@
1
+ <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
+ <h1>
3
+ <%= title %>
4
+ <% if (promoted) { %>
5
+ &nbsp;<span class="status-badge" data-status="complete">promoted</span>
6
+ <% } %>
7
+ </h1>
8
+
9
+ <% if (date) { %>
10
+ <p><small><%= date %></small></p>
11
+ <% } %>
12
+
13
+ <article>
14
+ <div class="markdown-body">
15
+ <%- html %>
16
+ </div>
17
+ </article>
18
+
19
+ <p><a href="/notes"
20
+ hx-get="/notes"
21
+ hx-target="#main-content"
22
+ hx-push-url="true">Back to Notes</a></p>
@@ -5,9 +5,15 @@
5
5
  <%- include('empty-state', { icon: 'N', title: 'No notes yet', action: 'Use /pbr:note to create project notes.' }) %>
6
6
  <% } else { %>
7
7
  <% notes.forEach(function(note) { %>
8
+ <% const noteSlug = note.filename.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.md$/, ''); %>
8
9
  <article>
9
10
  <header>
10
- <strong><%= note.title %></strong>
11
+ <strong>
12
+ <a href="/notes/<%= noteSlug %>"
13
+ hx-get="/notes/<%= noteSlug %>"
14
+ hx-target="#main-content"
15
+ hx-push-url="true"><%= note.title %></a>
16
+ </strong>
11
17
  <% if (note.promoted) { %>
12
18
  &nbsp; <span class="status-badge" data-status="complete">promoted</span>
13
19
  <% } %>