@sienklogic/plan-build-run 2.23.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.
- package/CHANGELOG.md +66 -0
- package/README.md +62 -13
- package/dashboard/package.json +2 -2
- package/dashboard/public/css/layout.css +128 -21
- package/dashboard/public/css/status-colors.css +14 -2
- package/dashboard/public/css/tokens.css +36 -0
- package/dashboard/src/middleware/current-phase.js +2 -1
- package/dashboard/src/repositories/planning.repository.js +1 -11
- package/dashboard/src/routes/events.routes.js +49 -0
- package/dashboard/src/routes/pages.routes.js +367 -3
- package/dashboard/src/server.js +4 -0
- package/dashboard/src/services/audit.service.js +42 -0
- package/dashboard/src/services/config.service.js +140 -0
- package/dashboard/src/services/dashboard.service.js +153 -19
- package/dashboard/src/services/log.service.js +105 -0
- package/dashboard/src/services/notes.service.js +16 -0
- package/dashboard/src/services/phase.service.js +58 -9
- package/dashboard/src/services/requirements.service.js +130 -0
- package/dashboard/src/services/research.service.js +137 -0
- package/dashboard/src/services/roadmap.service.js +1 -11
- package/dashboard/src/services/todo.service.js +30 -0
- package/dashboard/src/utils/strip-bom.js +8 -0
- package/dashboard/src/views/audit-detail.ejs +5 -0
- package/dashboard/src/views/audits.ejs +5 -0
- package/dashboard/src/views/config.ejs +5 -0
- package/dashboard/src/views/logs.ejs +3 -0
- package/dashboard/src/views/note-detail.ejs +3 -0
- package/dashboard/src/views/partials/activity-feed.ejs +12 -0
- package/dashboard/src/views/partials/audit-detail-content.ejs +12 -0
- package/dashboard/src/views/partials/audits-content.ejs +34 -0
- package/dashboard/src/views/partials/config-content.ejs +196 -0
- package/dashboard/src/views/partials/dashboard-content.ejs +71 -46
- package/dashboard/src/views/partials/log-entries-content.ejs +17 -0
- package/dashboard/src/views/partials/logs-content.ejs +131 -0
- package/dashboard/src/views/partials/note-detail-content.ejs +22 -0
- package/dashboard/src/views/partials/notes-content.ejs +7 -1
- package/dashboard/src/views/partials/phase-content.ejs +181 -146
- package/dashboard/src/views/partials/phase-timeline.ejs +16 -0
- package/dashboard/src/views/partials/requirements-content.ejs +44 -0
- package/dashboard/src/views/partials/research-content.ejs +49 -0
- package/dashboard/src/views/partials/research-detail-content.ejs +23 -0
- package/dashboard/src/views/partials/sidebar.ejs +67 -22
- package/dashboard/src/views/partials/todos-content.ejs +13 -3
- package/dashboard/src/views/partials/todos-done-content.ejs +44 -0
- package/dashboard/src/views/requirements.ejs +3 -0
- package/dashboard/src/views/research-detail.ejs +3 -0
- package/dashboard/src/views/research.ejs +3 -0
- package/dashboard/src/views/todos-done.ejs +3 -0
- package/package.json +1 -1
- package/plugins/copilot-pbr/agents/dev-sync.agent.md +114 -0
- package/plugins/copilot-pbr/agents/integration-checker.agent.md +9 -2
- package/plugins/copilot-pbr/agents/planner.agent.md +19 -0
- package/plugins/copilot-pbr/agents/verifier.agent.md +22 -2
- package/plugins/copilot-pbr/hooks/hooks.json +12 -0
- package/plugins/copilot-pbr/plugin.json +1 -1
- package/plugins/copilot-pbr/references/plan-format.md +22 -0
- package/plugins/copilot-pbr/templates/INTEGRATION-REPORT.md.tmpl +18 -2
- package/plugins/copilot-pbr/templates/VERIFICATION-DETAIL.md.tmpl +2 -1
- package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-pbr/agents/dev-sync.md +113 -0
- package/plugins/cursor-pbr/agents/integration-checker.md +9 -2
- package/plugins/cursor-pbr/agents/planner.md +19 -0
- package/plugins/cursor-pbr/agents/verifier.md +22 -2
- package/plugins/cursor-pbr/hooks/hooks.json +10 -0
- package/plugins/cursor-pbr/references/plan-format.md +22 -0
- package/plugins/cursor-pbr/templates/INTEGRATION-REPORT.md.tmpl +18 -2
- package/plugins/cursor-pbr/templates/VERIFICATION-DETAIL.md.tmpl +2 -1
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
- package/plugins/pbr/agents/dev-sync.md +120 -0
- package/plugins/pbr/agents/integration-checker.md +9 -2
- package/plugins/pbr/agents/planner.md +19 -0
- package/plugins/pbr/agents/verifier.md +22 -2
- package/plugins/pbr/hooks/hooks.json +10 -0
- package/plugins/pbr/references/plan-format.md +22 -0
- package/plugins/pbr/scripts/check-plan-format.js +2 -2
- package/plugins/pbr/scripts/check-subagent-output.js +2 -2
- package/plugins/pbr/scripts/config-schema.json +4 -1
- package/plugins/pbr/scripts/local-llm/health.js +4 -1
- package/plugins/pbr/scripts/local-llm/operations/classify-commit.js +68 -0
- package/plugins/pbr/scripts/local-llm/operations/classify-file-intent.js +73 -0
- package/plugins/pbr/scripts/local-llm/operations/triage-test-output.js +72 -0
- package/plugins/pbr/scripts/post-bash-triage.js +132 -0
- package/plugins/pbr/scripts/post-write-dispatch.js +44 -0
- package/plugins/pbr/scripts/pre-bash-dispatch.js +17 -11
- package/plugins/pbr/scripts/status-line.js +50 -5
- package/plugins/pbr/scripts/validate-commit.js +66 -2
- package/plugins/pbr/scripts/validate-task.js +1 -1
- package/plugins/pbr/templates/INTEGRATION-REPORT.md.tmpl +18 -2
- package/plugins/pbr/templates/VERIFICATION-DETAIL.md.tmpl +2 -1
- package/dashboard/src/views/coming-soon.ejs +0 -11
|
@@ -0,0 +1,137 @@
|
|
|
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 research docs from .planning/research/*.md, sorted by filename descending.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} projectDir - Absolute path to the project root
|
|
9
|
+
* @returns {Promise<Array>}
|
|
10
|
+
*/
|
|
11
|
+
export async function listResearchDocs(projectDir) {
|
|
12
|
+
const dir = join(projectDir, '.planning', 'research');
|
|
13
|
+
let entries;
|
|
14
|
+
try {
|
|
15
|
+
entries = await readdir(dir);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (err.code === 'ENOENT') return [];
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
const mdFiles = entries.filter(f => f.endsWith('.md')).sort().reverse();
|
|
21
|
+
const results = await Promise.allSettled(
|
|
22
|
+
mdFiles.map(f => readMarkdownFile(join(dir, f)))
|
|
23
|
+
);
|
|
24
|
+
const docs = [];
|
|
25
|
+
for (let i = 0; i < mdFiles.length; i++) {
|
|
26
|
+
if (results[i].status !== 'fulfilled') continue;
|
|
27
|
+
const { frontmatter, html } = results[i].value;
|
|
28
|
+
const filename = mdFiles[i];
|
|
29
|
+
const slug = filename.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.md$/, '');
|
|
30
|
+
const title = slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
31
|
+
docs.push({
|
|
32
|
+
filename,
|
|
33
|
+
slug,
|
|
34
|
+
title,
|
|
35
|
+
topic: frontmatter.topic || null,
|
|
36
|
+
date: frontmatter.research_date
|
|
37
|
+
? (frontmatter.research_date instanceof Date
|
|
38
|
+
? frontmatter.research_date.toISOString().slice(0, 10)
|
|
39
|
+
: String(frontmatter.research_date))
|
|
40
|
+
: null,
|
|
41
|
+
confidence: frontmatter.confidence || null,
|
|
42
|
+
coverage: frontmatter.coverage || null,
|
|
43
|
+
html
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return docs;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* List all codebase docs from .planning/codebase/*.md, sorted by filename descending.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} projectDir - Absolute path to the project root
|
|
53
|
+
* @returns {Promise<Array>}
|
|
54
|
+
*/
|
|
55
|
+
export async function listCodebaseDocs(projectDir) {
|
|
56
|
+
const dir = join(projectDir, '.planning', 'codebase');
|
|
57
|
+
let entries;
|
|
58
|
+
try {
|
|
59
|
+
entries = await readdir(dir);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err.code === 'ENOENT') return [];
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
const mdFiles = entries.filter(f => f.endsWith('.md')).sort().reverse();
|
|
65
|
+
const results = await Promise.allSettled(
|
|
66
|
+
mdFiles.map(f => readMarkdownFile(join(dir, f)))
|
|
67
|
+
);
|
|
68
|
+
const docs = [];
|
|
69
|
+
for (let i = 0; i < mdFiles.length; i++) {
|
|
70
|
+
if (results[i].status !== 'fulfilled') continue;
|
|
71
|
+
const { frontmatter, html } = results[i].value;
|
|
72
|
+
const filename = mdFiles[i];
|
|
73
|
+
const slug = filename.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.md$/, '');
|
|
74
|
+
const title = slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
75
|
+
docs.push({
|
|
76
|
+
filename,
|
|
77
|
+
slug,
|
|
78
|
+
title,
|
|
79
|
+
date: frontmatter.scan_date
|
|
80
|
+
? (frontmatter.scan_date instanceof Date
|
|
81
|
+
? frontmatter.scan_date.toISOString().slice(0, 10)
|
|
82
|
+
: String(frontmatter.scan_date))
|
|
83
|
+
: null,
|
|
84
|
+
html
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return docs;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get a single research or codebase doc by slug.
|
|
92
|
+
* Searches research/ first, then codebase/.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} projectDir - Absolute path to the project root
|
|
95
|
+
* @param {string} slug - Slug derived from filename (without date prefix and .md extension)
|
|
96
|
+
* @returns {Promise<object|null>}
|
|
97
|
+
*/
|
|
98
|
+
export async function getResearchDocBySlug(projectDir, slug) {
|
|
99
|
+
for (const subdir of ['research', 'codebase']) {
|
|
100
|
+
const dir = join(projectDir, '.planning', subdir);
|
|
101
|
+
let entries;
|
|
102
|
+
try {
|
|
103
|
+
entries = await readdir(dir);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (err.code === 'ENOENT') continue;
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
const filename = entries.find(
|
|
109
|
+
f => f.endsWith('.md') &&
|
|
110
|
+
f.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.md$/, '') === slug
|
|
111
|
+
);
|
|
112
|
+
if (!filename) continue;
|
|
113
|
+
const { frontmatter, html } = await readMarkdownFile(join(dir, filename));
|
|
114
|
+
const title = slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
115
|
+
return {
|
|
116
|
+
filename,
|
|
117
|
+
slug,
|
|
118
|
+
title,
|
|
119
|
+
topic: frontmatter.topic || null,
|
|
120
|
+
date: frontmatter.research_date
|
|
121
|
+
? (frontmatter.research_date instanceof Date
|
|
122
|
+
? frontmatter.research_date.toISOString().slice(0, 10)
|
|
123
|
+
: String(frontmatter.research_date))
|
|
124
|
+
: frontmatter.scan_date
|
|
125
|
+
? (frontmatter.scan_date instanceof Date
|
|
126
|
+
? frontmatter.scan_date.toISOString().slice(0, 10)
|
|
127
|
+
: String(frontmatter.scan_date))
|
|
128
|
+
: null,
|
|
129
|
+
confidence: frontmatter.confidence || null,
|
|
130
|
+
sources_checked: frontmatter.sources_checked || null,
|
|
131
|
+
coverage: frontmatter.coverage || null,
|
|
132
|
+
section: subdir,
|
|
133
|
+
html
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import { readFile, readdir } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { parseRoadmapFile } from './dashboard.service.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Strip UTF-8 BOM from file content.
|
|
7
|
-
* Duplicated intentionally -- this service reads raw text, not via the repository layer.
|
|
8
|
-
*
|
|
9
|
-
* @param {string} content - Raw file content
|
|
10
|
-
* @returns {string} Content without BOM
|
|
11
|
-
*/
|
|
12
|
-
function stripBOM(content) {
|
|
13
|
-
return content.replace(/^\uFEFF/, '');
|
|
14
|
-
}
|
|
4
|
+
import { stripBOM } from '../utils/strip-bom.js';
|
|
15
5
|
|
|
16
6
|
/**
|
|
17
7
|
* Count the number of PLAN.md files in a phase directory.
|
|
@@ -230,6 +230,36 @@ export async function createTodo(projectDir, todoData) {
|
|
|
230
230
|
});
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
export async function listDoneTodos(projectDir) {
|
|
234
|
+
const doneDir = join(projectDir, '.planning', 'todos', 'done');
|
|
235
|
+
let files;
|
|
236
|
+
try {
|
|
237
|
+
files = await readdir(doneDir);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (err.code === 'ENOENT') return [];
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
const mdFiles = files.filter(f => f.endsWith('.md')).sort().reverse();
|
|
243
|
+
const todos = [];
|
|
244
|
+
for (const filename of mdFiles) {
|
|
245
|
+
const match = filename.match(/^(\d{3})-(.+)\.md$/);
|
|
246
|
+
if (!match) continue;
|
|
247
|
+
const [, id, slugPart] = match;
|
|
248
|
+
try {
|
|
249
|
+
const raw = await readFile(join(doneDir, filename), 'utf-8');
|
|
250
|
+
const { data } = matter(raw);
|
|
251
|
+
todos.push({
|
|
252
|
+
id, filename,
|
|
253
|
+
title: data.title || slugPart,
|
|
254
|
+
priority: data.priority || '',
|
|
255
|
+
phase: data.phase || '',
|
|
256
|
+
completedAt: data.completed_at || null
|
|
257
|
+
});
|
|
258
|
+
} catch { /* skip */ }
|
|
259
|
+
}
|
|
260
|
+
return todos;
|
|
261
|
+
}
|
|
262
|
+
|
|
233
263
|
export async function completeTodo(projectDir, todoId) {
|
|
234
264
|
return writeQueue.enqueue(async () => {
|
|
235
265
|
const pendingDir = join(projectDir, '.planning', 'todos', 'pending');
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% if (!recentActivity || recentActivity.length === 0) { %>
|
|
2
|
+
<p class="muted">No recent activity.</p>
|
|
3
|
+
<% } else { %>
|
|
4
|
+
<ul class="activity-feed">
|
|
5
|
+
<% recentActivity.forEach(function(item) { %>
|
|
6
|
+
<li class="activity-item">
|
|
7
|
+
<span class="activity-path"><%= item.path %></span>
|
|
8
|
+
<time class="activity-time" datetime="<%= item.timestamp %>"><%= item.timestamp %></time>
|
|
9
|
+
</li>
|
|
10
|
+
<% }); %>
|
|
11
|
+
</ul>
|
|
12
|
+
<% } %>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
|
|
2
|
+
<h1><%= title %></h1>
|
|
3
|
+
|
|
4
|
+
<p><a href="/audits">← Back to Audit Reports</a></p>
|
|
5
|
+
|
|
6
|
+
<% if (typeof date !== 'undefined' && date) { %>
|
|
7
|
+
<p><small>Date: <%= date %></small></p>
|
|
8
|
+
<% } %>
|
|
9
|
+
|
|
10
|
+
<article class="markdown-body">
|
|
11
|
+
<%- html %>
|
|
12
|
+
</article>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
|
|
2
|
+
<h1>Audit Reports</h1>
|
|
3
|
+
|
|
4
|
+
<% if (typeof reports !== 'undefined' && reports.length > 0) { %>
|
|
5
|
+
<article>
|
|
6
|
+
<div class="table-wrap">
|
|
7
|
+
<table>
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th scope="col">Date</th>
|
|
11
|
+
<th scope="col">Report</th>
|
|
12
|
+
</tr>
|
|
13
|
+
</thead>
|
|
14
|
+
<tbody>
|
|
15
|
+
<% reports.forEach(function(report) { %>
|
|
16
|
+
<tr>
|
|
17
|
+
<td><%= report.date || '—' %></td>
|
|
18
|
+
<td>
|
|
19
|
+
<a href="/audits/<%= report.filename %>"
|
|
20
|
+
hx-get="/audits/<%= report.filename %>"
|
|
21
|
+
hx-target="#main-content"
|
|
22
|
+
hx-push-url="true">
|
|
23
|
+
<%= report.title %>
|
|
24
|
+
</a>
|
|
25
|
+
</td>
|
|
26
|
+
</tr>
|
|
27
|
+
<% }); %>
|
|
28
|
+
</tbody>
|
|
29
|
+
</table>
|
|
30
|
+
</div>
|
|
31
|
+
</article>
|
|
32
|
+
<% } else { %>
|
|
33
|
+
<%- include('empty-state', { icon: '🔍', title: 'No audit reports found', action: 'Run /pbr:audit to generate a session audit report.' }) %>
|
|
34
|
+
<% } %>
|
|
@@ -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
|
-
<!--
|
|
5
|
-
<div class="
|
|
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
|
|
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="
|
|
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
|
|
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
|
|
28
|
-
|
|
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
|
+
—
|
|
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
|
|
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
|
|
49
|
-
<
|
|
61
|
+
<p><%= lastActivity.date %></p>
|
|
62
|
+
<footer><small><%= lastActivity.description %></small></footer>
|
|
50
63
|
<% } else { %>
|
|
51
|
-
<p
|
|
64
|
+
<p>—</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
|
-
<%
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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>
|