@sienklogic/plan-build-run 2.15.0 → 2.16.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 +37 -0
- package/dashboard/package.json +3 -1
- package/dashboard/public/css/layout.css +237 -82
- package/dashboard/public/css/tokens.css +59 -0
- package/dashboard/public/js/sidebar-toggle.js +21 -7
- package/dashboard/public/js/sse-client.js +99 -0
- package/dashboard/public/js/theme-toggle.js +46 -0
- package/dashboard/src/app.js +4 -0
- package/dashboard/src/middleware/current-phase.js +24 -0
- package/dashboard/src/routes/events.routes.js +5 -0
- package/dashboard/src/routes/index.routes.js +2 -1
- package/dashboard/src/routes/pages.routes.js +94 -6
- package/dashboard/src/services/analytics.service.js +143 -0
- package/dashboard/src/services/milestone.service.js +50 -4
- package/dashboard/src/services/roadmap.service.js +73 -0
- package/dashboard/src/services/todo.service.js +11 -2
- package/dashboard/src/services/watcher.service.js +1 -1
- package/dashboard/src/utils/cache.js +55 -0
- package/dashboard/src/views/analytics.ejs +5 -0
- package/dashboard/src/views/dependencies.ejs +5 -0
- package/dashboard/src/views/error.ejs +16 -9
- package/dashboard/src/views/partials/analytics-content.ejs +71 -0
- package/dashboard/src/views/partials/breadcrumbs.ejs +14 -0
- package/dashboard/src/views/partials/dashboard-content.ejs +1 -0
- package/dashboard/src/views/partials/dependencies-content.ejs +16 -0
- package/dashboard/src/views/partials/empty-state.ejs +7 -0
- package/dashboard/src/views/partials/head.ejs +4 -1
- package/dashboard/src/views/partials/header.ejs +9 -0
- package/dashboard/src/views/partials/layout-bottom.ejs +1 -10
- package/dashboard/src/views/partials/layout-top.ejs +7 -0
- package/dashboard/src/views/partials/milestone-detail-content.ejs +1 -0
- package/dashboard/src/views/partials/milestones-content.ejs +55 -19
- package/dashboard/src/views/partials/phase-content.ejs +1 -0
- package/dashboard/src/views/partials/phase-doc-content.ejs +1 -1
- package/dashboard/src/views/partials/phases-content.ejs +1 -0
- package/dashboard/src/views/partials/roadmap-content.ejs +1 -0
- package/dashboard/src/views/partials/sidebar.ejs +88 -43
- package/dashboard/src/views/partials/todo-create-content.ejs +1 -0
- package/dashboard/src/views/partials/todo-detail-content.ejs +5 -1
- package/dashboard/src/views/partials/todos-content.ejs +44 -3
- 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
|
@@ -139,6 +139,79 @@ function extractMilestones(roadmapContent) {
|
|
|
139
139
|
* @param {string} projectDir - Absolute path to the project root
|
|
140
140
|
* @returns {Promise<{phases: Array, milestones: Array}>}
|
|
141
141
|
*/
|
|
142
|
+
/**
|
|
143
|
+
* Generate a Mermaid flowchart string from ROADMAP.md phase dependencies.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} projectDir - Absolute path to the project root
|
|
146
|
+
* @returns {Promise<string>} Mermaid flowchart definition
|
|
147
|
+
*/
|
|
148
|
+
export async function generateDependencyMermaid(projectDir) {
|
|
149
|
+
const { phases, milestones } = await getRoadmapData(projectDir);
|
|
150
|
+
|
|
151
|
+
if (phases.length === 0) {
|
|
152
|
+
return 'graph TD\n empty["No phases found"]';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Read raw ROADMAP.md for dependencies
|
|
156
|
+
let dependencyMap = new Map();
|
|
157
|
+
try {
|
|
158
|
+
const roadmapPath = join(projectDir, '.planning', 'ROADMAP.md');
|
|
159
|
+
const rawContent = stripBOM(await readFile(roadmapPath, 'utf-8')).replace(/\r\n/g, '\n');
|
|
160
|
+
dependencyMap = extractAllDependencies(rawContent);
|
|
161
|
+
} catch (_err) {
|
|
162
|
+
// No ROADMAP.md — proceed with no deps
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Detect current phase (first non-complete phase, or last phase)
|
|
166
|
+
const currentPhase = phases.find(p => p.status !== 'Complete') || phases[phases.length - 1];
|
|
167
|
+
|
|
168
|
+
const lines = ['graph TD'];
|
|
169
|
+
|
|
170
|
+
// Group phases by milestone
|
|
171
|
+
const phaseById = new Map(phases.map(p => [p.id, p]));
|
|
172
|
+
|
|
173
|
+
if (milestones.length > 0) {
|
|
174
|
+
for (const ms of milestones) {
|
|
175
|
+
const msPhases = phases.filter(p => p.id >= ms.startPhase && p.id <= ms.endPhase);
|
|
176
|
+
if (msPhases.length === 0) continue;
|
|
177
|
+
const safeName = ms.name.replace(/"/g, '#quot;');
|
|
178
|
+
lines.push(` subgraph ${safeName}`);
|
|
179
|
+
for (const p of msPhases) {
|
|
180
|
+
const label = `Phase ${p.id}: ${p.name}`.replace(/"/g, '#quot;');
|
|
181
|
+
lines.push(` P${p.id}["${label}"]`);
|
|
182
|
+
}
|
|
183
|
+
lines.push(' end');
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
for (const p of phases) {
|
|
187
|
+
const label = `Phase ${p.id}: ${p.name}`.replace(/"/g, '#quot;');
|
|
188
|
+
lines.push(` P${p.id}["${label}"]`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Add dependency edges
|
|
193
|
+
for (const [phaseId, deps] of dependencyMap) {
|
|
194
|
+
if (!phaseById.has(phaseId)) continue;
|
|
195
|
+
for (const depId of deps) {
|
|
196
|
+
if (phaseById.has(depId)) {
|
|
197
|
+
lines.push(` P${depId} --> P${phaseId}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Style current phase
|
|
203
|
+
if (currentPhase) {
|
|
204
|
+
lines.push(` style P${currentPhase.id} fill:#4caf50,color:#fff`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Click handlers
|
|
208
|
+
for (const p of phases) {
|
|
209
|
+
lines.push(` click P${p.id} "/phases/${p.id}"`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return lines.join('\n');
|
|
213
|
+
}
|
|
214
|
+
|
|
142
215
|
export async function getRoadmapData(projectDir) {
|
|
143
216
|
const { phases: basePhases } = await parseRoadmapFile(projectDir);
|
|
144
217
|
|
|
@@ -75,7 +75,7 @@ function sortTodosByPriority(todos) {
|
|
|
75
75
|
* @param {string} projectDir - Absolute path to the project root
|
|
76
76
|
* @returns {Promise<Array<{id: string, title: string, priority: string, phase: string, status: string, created: string, filename: string}>>}
|
|
77
77
|
*/
|
|
78
|
-
export async function listPendingTodos(projectDir) {
|
|
78
|
+
export async function listPendingTodos(projectDir, filters = {}) {
|
|
79
79
|
const pendingDir = join(projectDir, '.planning', 'todos', 'pending');
|
|
80
80
|
|
|
81
81
|
let entries;
|
|
@@ -124,7 +124,16 @@ export async function listPendingTodos(projectDir) {
|
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
sortTodosByPriority(todos);
|
|
128
|
+
|
|
129
|
+
// Apply filters
|
|
130
|
+
const { priority, status, q } = filters;
|
|
131
|
+
return todos.filter(todo => {
|
|
132
|
+
if (priority && todo.priority !== priority) return false;
|
|
133
|
+
if (status && todo.status !== status) return false;
|
|
134
|
+
if (q && !todo.title.toLowerCase().includes(q.toLowerCase())) return false;
|
|
135
|
+
return true;
|
|
136
|
+
});
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
/**
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple TTL (Time-To-Live) cache backed by a Map.
|
|
3
|
+
*
|
|
4
|
+
* Each entry stores { value, expiry } where expiry is a Unix-ms timestamp.
|
|
5
|
+
*/
|
|
6
|
+
export class TTLCache {
|
|
7
|
+
/**
|
|
8
|
+
* @param {number} [defaultTTL=60000] - Default time-to-live in milliseconds
|
|
9
|
+
*/
|
|
10
|
+
constructor(defaultTTL = 60_000) {
|
|
11
|
+
this._store = new Map();
|
|
12
|
+
this._defaultTTL = defaultTTL;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Retrieve a cached value. Returns undefined if missing or expired.
|
|
17
|
+
* @param {string} key
|
|
18
|
+
* @returns {*}
|
|
19
|
+
*/
|
|
20
|
+
get(key) {
|
|
21
|
+
const entry = this._store.get(key);
|
|
22
|
+
if (!entry) return undefined;
|
|
23
|
+
if (Date.now() > entry.expiry) {
|
|
24
|
+
this._store.delete(key);
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return entry.value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Store a value with an optional per-key TTL.
|
|
32
|
+
* @param {string} key
|
|
33
|
+
* @param {*} value
|
|
34
|
+
* @param {number} [ttlMs] - Override default TTL for this entry
|
|
35
|
+
*/
|
|
36
|
+
set(key, value, ttlMs) {
|
|
37
|
+
const ttl = ttlMs ?? this._defaultTTL;
|
|
38
|
+
this._store.set(key, { value, expiry: Date.now() + ttl });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Remove a single key from the cache.
|
|
43
|
+
* @param {string} key
|
|
44
|
+
*/
|
|
45
|
+
invalidate(key) {
|
|
46
|
+
this._store.delete(key);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Clear the entire cache.
|
|
51
|
+
*/
|
|
52
|
+
invalidateAll() {
|
|
53
|
+
this._store.clear();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
<%- include('partials/layout-top', { title: typeof title !== 'undefined' ? title : 'Error', activePage: '' }) %>
|
|
2
2
|
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<
|
|
3
|
+
<div class="error-card">
|
|
4
|
+
<div class="error-card__icon">⚠</div>
|
|
5
|
+
<h2>Error <%= status %></h2>
|
|
6
|
+
<div class="error-card__message">
|
|
7
|
+
<p><%= message %></p>
|
|
8
|
+
</div>
|
|
9
|
+
<% if (typeof stack !== 'undefined' && stack) { %>
|
|
10
|
+
<details>
|
|
11
|
+
<summary>Stack Trace (Development Only)</summary>
|
|
12
|
+
<pre><code><%= stack %></code></pre>
|
|
13
|
+
</details>
|
|
14
|
+
<% } %>
|
|
15
|
+
<div class="error-card__action">
|
|
16
|
+
<a href="javascript:history.back()">Go back</a> | <a href="/">Return to Dashboard</a>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
12
19
|
|
|
13
20
|
<%- include('partials/layout-bottom') %>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
|
|
2
|
+
|
|
3
|
+
<h1>Project Analytics</h1>
|
|
4
|
+
|
|
5
|
+
<% if (typeof analytics !== 'undefined' && analytics.warning) { %>
|
|
6
|
+
<article aria-label="warning" class="pico-background-yellow-100">
|
|
7
|
+
<strong>Warning:</strong> <%= analytics.warning %>
|
|
8
|
+
</article>
|
|
9
|
+
<% } %>
|
|
10
|
+
|
|
11
|
+
<% if (typeof analytics !== 'undefined' && analytics.summary) { %>
|
|
12
|
+
<div class="grid">
|
|
13
|
+
<article>
|
|
14
|
+
<header>Total Commits</header>
|
|
15
|
+
<strong style="font-size: 2rem;"><%= analytics.summary.totalCommits %></strong>
|
|
16
|
+
</article>
|
|
17
|
+
<article>
|
|
18
|
+
<header>Total Phases</header>
|
|
19
|
+
<strong style="font-size: 2rem;"><%= analytics.summary.totalPhases %></strong>
|
|
20
|
+
</article>
|
|
21
|
+
<article>
|
|
22
|
+
<header>Avg Duration</header>
|
|
23
|
+
<strong style="font-size: 2rem;"><%= analytics.summary.avgDuration %></strong>
|
|
24
|
+
</article>
|
|
25
|
+
<article>
|
|
26
|
+
<header>Lines Changed</header>
|
|
27
|
+
<strong style="font-size: 2rem;"><%= analytics.summary.totalLinesChanged.toLocaleString() %></strong>
|
|
28
|
+
</article>
|
|
29
|
+
</div>
|
|
30
|
+
<% } %>
|
|
31
|
+
|
|
32
|
+
<% if (typeof analytics !== 'undefined' && analytics.phases && analytics.phases.length > 0) { %>
|
|
33
|
+
<article>
|
|
34
|
+
<header>Per-Phase Metrics</header>
|
|
35
|
+
<div class="overflow-auto">
|
|
36
|
+
<table>
|
|
37
|
+
<thead>
|
|
38
|
+
<tr>
|
|
39
|
+
<th>Phase</th>
|
|
40
|
+
<th>Commits</th>
|
|
41
|
+
<th>Duration</th>
|
|
42
|
+
<th>Plans</th>
|
|
43
|
+
<th>Lines Changed</th>
|
|
44
|
+
</tr>
|
|
45
|
+
</thead>
|
|
46
|
+
<tbody>
|
|
47
|
+
<% analytics.phases.sort((a, b) => a.phaseId.localeCompare(b.phaseId)).forEach(phase => { %>
|
|
48
|
+
<tr>
|
|
49
|
+
<td>
|
|
50
|
+
<a href="/phases/<%= phase.phaseId %>"
|
|
51
|
+
hx-get="/phases/<%= phase.phaseId %>"
|
|
52
|
+
hx-target="#main-content"
|
|
53
|
+
hx-push-url="true">
|
|
54
|
+
<%= phase.phaseId %> - <%= phase.phaseName %>
|
|
55
|
+
</a>
|
|
56
|
+
</td>
|
|
57
|
+
<td><%= phase.commitCount %></td>
|
|
58
|
+
<td><%= phase.duration || 'N/A' %></td>
|
|
59
|
+
<td><%= phase.planCount %></td>
|
|
60
|
+
<td><%= phase.linesChanged.toLocaleString() %></td>
|
|
61
|
+
</tr>
|
|
62
|
+
<% }) %>
|
|
63
|
+
</tbody>
|
|
64
|
+
</table>
|
|
65
|
+
</div>
|
|
66
|
+
</article>
|
|
67
|
+
<% } else if (typeof analytics === 'undefined' || !analytics.summary) { %>
|
|
68
|
+
<%- include('empty-state', { icon: '📊', title: 'No analytics data available', action: '' }) %>
|
|
69
|
+
<% } else { %>
|
|
70
|
+
<p>No phase data available.</p>
|
|
71
|
+
<% } %>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<% if (typeof breadcrumbs !== 'undefined' && breadcrumbs.length > 0) { %>
|
|
2
|
+
<nav aria-label="Breadcrumb">
|
|
3
|
+
<ol class="breadcrumbs">
|
|
4
|
+
<li><a href="/" hx-get="/" hx-target="#main-content" hx-push-url="true">⌂</a></li>
|
|
5
|
+
<% breadcrumbs.forEach(function(item, index) { %>
|
|
6
|
+
<% if (index < breadcrumbs.length - 1) { %>
|
|
7
|
+
<li><a href="<%= item.url %>" hx-get="<%= item.url %>" hx-target="#main-content" hx-push-url="true"><%= item.label %></a></li>
|
|
8
|
+
<% } else { %>
|
|
9
|
+
<li aria-current="page"><%= item.label %></li>
|
|
10
|
+
<% } %>
|
|
11
|
+
<% }); %>
|
|
12
|
+
</ol>
|
|
13
|
+
</nav>
|
|
14
|
+
<% } %>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
|
|
2
|
+
|
|
3
|
+
<h1>Phase Dependencies</h1>
|
|
4
|
+
|
|
5
|
+
<% if (typeof mermaidCode !== 'undefined' && mermaidCode && mermaidCode.trim()) { %>
|
|
6
|
+
<article>
|
|
7
|
+
<pre class="mermaid"><%= mermaidCode %></pre>
|
|
8
|
+
</article>
|
|
9
|
+
<% } else { %>
|
|
10
|
+
<%- include('empty-state', { icon: '🔗', title: 'No dependency data available', action: '' }) %>
|
|
11
|
+
<% } %>
|
|
12
|
+
|
|
13
|
+
<script type="module">
|
|
14
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
15
|
+
mermaid.initialize({ startOnLoad: true, theme: 'default', securityLevel: 'loose' });
|
|
16
|
+
</script>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<article class="empty-state">
|
|
2
|
+
<p style="font-size: 2.5rem; margin-bottom: 0;"><%= typeof icon !== 'undefined' ? icon : '' %></p>
|
|
3
|
+
<h3><%= typeof title !== 'undefined' ? title : 'No data available' %></h3>
|
|
4
|
+
<% if (typeof action !== 'undefined' && action) { %>
|
|
5
|
+
<%- action %>
|
|
6
|
+
<% } %>
|
|
7
|
+
</article>
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
<meta charset="UTF-8">
|
|
3
3
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4
4
|
<title><%= typeof title !== 'undefined' ? title : 'Plan-Build-Run' %> - Plan-Build-Run</title>
|
|
5
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='80'>P</text></svg>">
|
|
5
6
|
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
|
|
6
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.0.6/css/pico.min.css">
|
|
7
8
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-400-normal.min.css" media="print" onload="this.media='all'">
|
|
8
9
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-600-normal.min.css" media="print" onload="this.media='all'">
|
|
9
10
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-700-normal.min.css" media="print" onload="this.media='all'">
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-700-normal.min.css">
|
|
15
16
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono@latest/latin-400-normal.min.css">
|
|
16
17
|
</noscript>
|
|
18
|
+
<link rel="stylesheet" href="/css/tokens.css">
|
|
17
19
|
<link rel="stylesheet" href="/css/layout.css">
|
|
18
20
|
<link rel="stylesheet" href="/css/status-colors.css">
|
|
19
21
|
<script
|
|
@@ -24,4 +26,5 @@
|
|
|
24
26
|
<script src="https://cdn.jsdelivr.net/npm/htmx-ext-sse@2.2.2/sse.js"></script>
|
|
25
27
|
<script src="/js/htmx-title.js"></script>
|
|
26
28
|
<script src="/js/sidebar-toggle.js"></script>
|
|
29
|
+
<script src="/js/theme-toggle.js"></script>
|
|
27
30
|
</head>
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.theme-toggle { background: none; border: none; cursor: pointer; font-size: 1.2rem; padding: var(--space-xs); }
|
|
3
|
+
</style>
|
|
1
4
|
<header>
|
|
2
5
|
<nav>
|
|
3
6
|
<ul>
|
|
4
7
|
<li><strong>PBR Dashboard</strong></li>
|
|
5
8
|
</ul>
|
|
6
9
|
<ul>
|
|
10
|
+
<li>
|
|
11
|
+
<span id="sse-status" data-connected="false" aria-label="SSE connection status"></span>
|
|
12
|
+
</li>
|
|
13
|
+
<li>
|
|
14
|
+
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">🌙</button>
|
|
15
|
+
</li>
|
|
7
16
|
<li>
|
|
8
17
|
<button class="sidebar-toggle" aria-label="Toggle navigation">☰</button>
|
|
9
18
|
</li>
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
</main>
|
|
2
2
|
<%- include('footer') %>
|
|
3
3
|
</div>
|
|
4
|
-
<
|
|
5
|
-
sse-connect="/api/events/stream"
|
|
6
|
-
sse-close="close">
|
|
7
|
-
<div hx-trigger="sse:file-change"
|
|
8
|
-
hx-get="<%= typeof currentPath !== 'undefined' ? currentPath : '/' %>"
|
|
9
|
-
hx-target="#main-content"
|
|
10
|
-
hx-swap="innerHTML"
|
|
11
|
-
style="display:none;">
|
|
12
|
-
</div>
|
|
13
|
-
</div>
|
|
4
|
+
<script src="/js/sse-client.js"></script>
|
|
14
5
|
</body>
|
|
15
6
|
</html>
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
|
+
<script>(function(){var t=localStorage.getItem('pbr-theme');if(t)document.documentElement.dataset.theme=t})()</script>
|
|
3
4
|
<%- include('head', { title: typeof title !== 'undefined' ? title : 'PBR Dashboard' }) %>
|
|
4
5
|
<body>
|
|
6
|
+
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
7
|
+
<div class="loading-bar"></div>
|
|
8
|
+
<script>
|
|
9
|
+
document.body.addEventListener('htmx:beforeRequest', function() { document.body.classList.add('htmx-request'); });
|
|
10
|
+
document.body.addEventListener('htmx:afterRequest', function() { document.body.classList.remove('htmx-request'); });
|
|
11
|
+
</script>
|
|
5
12
|
<div class="page-wrapper">
|
|
6
13
|
<%- include('header') %>
|
|
7
14
|
<%- include('sidebar', { activePage: typeof activePage !== 'undefined' ? activePage : '' }) %>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
|
|
1
2
|
<h1>Milestones</h1>
|
|
2
3
|
|
|
3
4
|
<% if (active.length > 0) { %>
|
|
@@ -17,28 +18,63 @@
|
|
|
17
18
|
|
|
18
19
|
<% if (archived.length > 0) { %>
|
|
19
20
|
<h2>Archived</h2>
|
|
20
|
-
<% archived.forEach(function(m) { %>
|
|
21
21
|
<article>
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
22
|
+
<div class="table-wrap">
|
|
23
|
+
<table>
|
|
24
|
+
<thead>
|
|
25
|
+
<tr>
|
|
26
|
+
<th scope="col">Version</th>
|
|
27
|
+
<th scope="col">Name</th>
|
|
28
|
+
<th scope="col">Date</th>
|
|
29
|
+
<th scope="col">Duration</th>
|
|
30
|
+
<th scope="col">Phases</th>
|
|
31
|
+
<th scope="col">Commits</th>
|
|
32
|
+
</tr>
|
|
33
|
+
</thead>
|
|
34
|
+
<tbody>
|
|
35
|
+
<% archived.forEach(function(m) { %>
|
|
36
|
+
<tr>
|
|
37
|
+
<td colspan="6" style="padding:0">
|
|
38
|
+
<details>
|
|
39
|
+
<summary style="padding:0.5rem 1rem;cursor:pointer">
|
|
40
|
+
<span class="grid" style="display:inline-grid;grid-template-columns:repeat(6,1fr);width:100%;text-align:left">
|
|
41
|
+
<span><a href="/milestones/<%= m.version %>"
|
|
42
|
+
hx-get="/milestones/<%= m.version %>"
|
|
43
|
+
hx-target="#main-content"
|
|
44
|
+
hx-push-url="true">v<%= m.version %></a></span>
|
|
45
|
+
<span><%= m.name %></span>
|
|
46
|
+
<span><%= m.date || '—' %></span>
|
|
47
|
+
<span><%= m.duration || '—' %></span>
|
|
48
|
+
<span><%= m.stats && m.stats.phaseCount ? m.stats.phaseCount : '—' %></span>
|
|
49
|
+
<span><%= m.stats && m.stats.commitCount ? m.stats.commitCount : '—' %></span>
|
|
50
|
+
</span>
|
|
51
|
+
</summary>
|
|
52
|
+
<div style="padding:0.5rem 1rem 1rem">
|
|
53
|
+
<% if (m.stats && m.stats.statsHtml) { %>
|
|
54
|
+
<div><%= m.stats.statsHtml %></div>
|
|
55
|
+
<% } %>
|
|
56
|
+
<% if (m.stats && m.stats.deliverables && m.stats.deliverables.length > 0) { %>
|
|
57
|
+
<h4>Deliverables</h4>
|
|
58
|
+
<ul>
|
|
59
|
+
<% m.stats.deliverables.forEach(function(d) { %>
|
|
60
|
+
<li><a href="/milestones/<%= m.version %>"
|
|
61
|
+
hx-get="/milestones/<%= m.version %>"
|
|
62
|
+
hx-target="#main-content"
|
|
63
|
+
hx-push-url="true"><%= d %></a></li>
|
|
64
|
+
<% }); %>
|
|
65
|
+
</ul>
|
|
66
|
+
<% } %>
|
|
67
|
+
</div>
|
|
68
|
+
</details>
|
|
69
|
+
</td>
|
|
70
|
+
</tr>
|
|
71
|
+
<% }); %>
|
|
72
|
+
</tbody>
|
|
73
|
+
</table>
|
|
74
|
+
</div>
|
|
38
75
|
</article>
|
|
39
|
-
<% }); %>
|
|
40
76
|
<% } %>
|
|
41
77
|
|
|
42
78
|
<% if (active.length === 0 && archived.length === 0) { %>
|
|
43
|
-
|
|
79
|
+
<%- include('empty-state', { icon: '🚩', title: 'No milestones yet', action: '' }) %>
|
|
44
80
|
<% } %>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%
|
|
2
2
|
var docLabel = docType === 'plan' ? 'Plan' : 'Summary';
|
|
3
3
|
%>
|
|
4
|
-
|
|
4
|
+
<%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
|
|
5
5
|
<h1><%= docLabel %> <%= planId %></h1>
|
|
6
6
|
|
|
7
7
|
<p><a href="/phases/<%= phaseId %>">← Back to Phase <%= phaseId %>: <%= phaseName %></a></p>
|