mrvn-cli 0.5.5 → 0.5.7
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/dist/index.js +770 -341
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +769 -340
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +770 -341
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15771,6 +15771,7 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15771
15771
|
type: doc.frontmatter.type,
|
|
15772
15772
|
status: doc.frontmatter.status,
|
|
15773
15773
|
progress: getEffectiveProgress(doc.frontmatter),
|
|
15774
|
+
owner: doc.frontmatter.owner,
|
|
15774
15775
|
workFocus: focusTag ? focusTag.slice(6) : void 0,
|
|
15775
15776
|
aboutArtifact: about
|
|
15776
15777
|
};
|
|
@@ -15977,10 +15978,10 @@ function getBoardData(store, type) {
|
|
|
15977
15978
|
if (!byStatus.has(status)) byStatus.set(status, []);
|
|
15978
15979
|
byStatus.get(status).push(doc);
|
|
15979
15980
|
}
|
|
15980
|
-
const
|
|
15981
|
+
const statusOrder2 = ["open", "draft", "in-progress", "blocked"];
|
|
15981
15982
|
const allStatuses = [...byStatus.keys()];
|
|
15982
15983
|
const ordered = [];
|
|
15983
|
-
for (const s of
|
|
15984
|
+
for (const s of statusOrder2) {
|
|
15984
15985
|
if (allStatuses.includes(s)) ordered.push(s);
|
|
15985
15986
|
}
|
|
15986
15987
|
for (const s of allStatuses.sort()) {
|
|
@@ -18131,6 +18132,31 @@ tr:hover td {
|
|
|
18131
18132
|
.focus-group-progress {
|
|
18132
18133
|
width: 96px;
|
|
18133
18134
|
}
|
|
18135
|
+
|
|
18136
|
+
/* Owner badges for DM sprint view */
|
|
18137
|
+
.owner-badge {
|
|
18138
|
+
display: inline-block;
|
|
18139
|
+
padding: 0.1rem 0.5rem;
|
|
18140
|
+
border-radius: 999px;
|
|
18141
|
+
font-size: 0.65rem;
|
|
18142
|
+
font-weight: 700;
|
|
18143
|
+
text-transform: uppercase;
|
|
18144
|
+
letter-spacing: 0.04em;
|
|
18145
|
+
white-space: nowrap;
|
|
18146
|
+
}
|
|
18147
|
+
.owner-badge-po { background: rgba(108, 140, 255, 0.18); color: #6c8cff; }
|
|
18148
|
+
.owner-badge-tl { background: rgba(251, 191, 36, 0.18); color: #fbbf24; }
|
|
18149
|
+
.owner-badge-dm { background: rgba(52, 211, 153, 0.18); color: #34d399; }
|
|
18150
|
+
.owner-badge-other { background: rgba(139, 143, 164, 0.12); color: var(--text-dim); }
|
|
18151
|
+
|
|
18152
|
+
/* Group header rows (PO dashboard decisions/deps) */
|
|
18153
|
+
.group-header-row td {
|
|
18154
|
+
background: var(--bg-hover);
|
|
18155
|
+
padding-top: 0.5rem;
|
|
18156
|
+
padding-bottom: 0.5rem;
|
|
18157
|
+
border-bottom: 1px solid var(--border);
|
|
18158
|
+
font-size: 0.8rem;
|
|
18159
|
+
}
|
|
18134
18160
|
`;
|
|
18135
18161
|
}
|
|
18136
18162
|
|
|
@@ -18962,19 +18988,55 @@ function buildHealthGauge(categories) {
|
|
|
18962
18988
|
}
|
|
18963
18989
|
|
|
18964
18990
|
// src/web/templates/pages/po/dashboard.ts
|
|
18991
|
+
var DONE_STATUSES5 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
18992
|
+
var RESOLVED_DECISION_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
18965
18993
|
function poDashboardPage(ctx) {
|
|
18966
18994
|
const overview = getOverviewData(ctx.store);
|
|
18967
|
-
const upcoming = getUpcomingData(ctx.store);
|
|
18968
18995
|
const sprintData = getSprintSummaryData(ctx.store);
|
|
18969
18996
|
const diagrams = getDiagramData(ctx.store);
|
|
18970
18997
|
const features = ctx.store.list({ type: "feature" });
|
|
18971
|
-
const
|
|
18972
|
-
const
|
|
18973
|
-
const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
|
|
18974
|
-
const RESOLVED_DECISION_STATUSES2 = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
18998
|
+
const epics = ctx.store.list({ type: "epic" });
|
|
18999
|
+
const allTasks = ctx.store.list({ type: "task" });
|
|
18975
19000
|
const decisions = ctx.store.list({ type: "decision" });
|
|
18976
|
-
const decisionsOpen = decisions.filter((d) => !RESOLVED_DECISION_STATUSES2.has(d.frontmatter.status)).length;
|
|
18977
19001
|
const questions = ctx.store.list({ type: "question" });
|
|
19002
|
+
const sprints = ctx.store.list({ type: "sprint" });
|
|
19003
|
+
const featureToEpics = /* @__PURE__ */ new Map();
|
|
19004
|
+
for (const epic of epics) {
|
|
19005
|
+
const featureIds = normalizeLinkedFeatures(epic.frontmatter.linkedFeature);
|
|
19006
|
+
for (const fid of featureIds) {
|
|
19007
|
+
const arr = featureToEpics.get(fid) ?? [];
|
|
19008
|
+
arr.push(epic);
|
|
19009
|
+
featureToEpics.set(fid, arr);
|
|
19010
|
+
}
|
|
19011
|
+
}
|
|
19012
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
19013
|
+
for (const task of allTasks) {
|
|
19014
|
+
const tags = task.frontmatter.tags ?? [];
|
|
19015
|
+
for (const tag of tags) {
|
|
19016
|
+
if (tag.startsWith("epic:")) {
|
|
19017
|
+
const epicId = tag.slice(5);
|
|
19018
|
+
const arr = epicToTasks.get(epicId) ?? [];
|
|
19019
|
+
arr.push(task);
|
|
19020
|
+
epicToTasks.set(epicId, arr);
|
|
19021
|
+
}
|
|
19022
|
+
}
|
|
19023
|
+
}
|
|
19024
|
+
const activeSprint = sprints.find((s) => s.frontmatter.status === "active");
|
|
19025
|
+
let sprintTimelinePct = 0;
|
|
19026
|
+
if (activeSprint) {
|
|
19027
|
+
const startDate = activeSprint.frontmatter.startDate;
|
|
19028
|
+
const endDate = activeSprint.frontmatter.endDate;
|
|
19029
|
+
if (startDate && endDate) {
|
|
19030
|
+
const startMs = new Date(startDate).getTime();
|
|
19031
|
+
const endMs = new Date(endDate).getTime();
|
|
19032
|
+
const totalDays = Math.max(1, endMs - startMs);
|
|
19033
|
+
sprintTimelinePct = Math.min(100, Math.max(0, Math.round((Date.now() - startMs) / totalDays * 100)));
|
|
19034
|
+
}
|
|
19035
|
+
}
|
|
19036
|
+
const featuresDone = features.filter((d) => DONE_STATUSES5.has(d.frontmatter.status)).length;
|
|
19037
|
+
const featuresOpen = features.filter((d) => d.frontmatter.status === "open").length;
|
|
19038
|
+
const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
|
|
19039
|
+
const decisionsOpen = decisions.filter((d) => !RESOLVED_DECISION_STATUSES.has(d.frontmatter.status)).length;
|
|
18978
19040
|
const questionsOpen = questions.filter((d) => d.frontmatter.status === "open").length;
|
|
18979
19041
|
const statsCards = `
|
|
18980
19042
|
<div class="cards">
|
|
@@ -19001,7 +19063,7 @@ function poDashboardPage(ctx) {
|
|
|
19001
19063
|
</div>
|
|
19002
19064
|
<div class="card">
|
|
19003
19065
|
<a href="/po/delivery">
|
|
19004
|
-
<div class="card-label">Sprint</div>
|
|
19066
|
+
<div class="card-label">Current Sprint</div>
|
|
19005
19067
|
<div class="card-value">${sprintData ? `${sprintData.workItems.completionPct}%` : "\u2014"}</div>
|
|
19006
19068
|
<div class="card-sub">${sprintData ? `${sprintData.workItems.done}/${sprintData.workItems.total} items` : "No active sprint"}</div>
|
|
19007
19069
|
</a>
|
|
@@ -19037,43 +19099,72 @@ function poDashboardPage(ctx) {
|
|
|
19037
19099
|
</div>`,
|
|
19038
19100
|
{ titleTag: "h3" }
|
|
19039
19101
|
) : "";
|
|
19040
|
-
|
|
19041
|
-
|
|
19042
|
-
|
|
19043
|
-
|
|
19044
|
-
|
|
19045
|
-
|
|
19046
|
-
|
|
19047
|
-
|
|
19102
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
19103
|
+
const atRiskItems = [];
|
|
19104
|
+
for (const f of features) {
|
|
19105
|
+
if (DONE_STATUSES5.has(f.frontmatter.status)) continue;
|
|
19106
|
+
const fEpics = featureToEpics.get(f.frontmatter.id) ?? [];
|
|
19107
|
+
const reasons = [];
|
|
19108
|
+
let blocked = 0;
|
|
19109
|
+
for (const epic of fEpics) {
|
|
19110
|
+
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
19111
|
+
if (t.frontmatter.status === "blocked") blocked++;
|
|
19112
|
+
}
|
|
19113
|
+
}
|
|
19114
|
+
if (blocked > 0) reasons.push(`${blocked} blocked task${blocked > 1 ? "s" : ""}`);
|
|
19115
|
+
for (const epic of fEpics) {
|
|
19116
|
+
const td = epic.frontmatter.targetDate;
|
|
19117
|
+
if (td && td < today && !DONE_STATUSES5.has(epic.frontmatter.status)) {
|
|
19118
|
+
reasons.push(`${epic.frontmatter.id} overdue`);
|
|
19119
|
+
}
|
|
19120
|
+
}
|
|
19121
|
+
let totalTasks = 0;
|
|
19122
|
+
let progressSum = 0;
|
|
19123
|
+
for (const epic of fEpics) {
|
|
19124
|
+
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
19125
|
+
totalTasks++;
|
|
19126
|
+
progressSum += getEffectiveProgress(t.frontmatter);
|
|
19127
|
+
}
|
|
19128
|
+
}
|
|
19129
|
+
const avgProgress = totalTasks > 0 ? Math.round(progressSum / totalTasks) : 0;
|
|
19130
|
+
if (avgProgress < 30 && sprintTimelinePct > 60 && totalTasks > 0) {
|
|
19131
|
+
reasons.push("Low progress vs sprint timeline");
|
|
19132
|
+
}
|
|
19133
|
+
if (reasons.length > 0) atRiskItems.push({ feature: f, reasons });
|
|
19134
|
+
}
|
|
19135
|
+
const atRiskSection = atRiskItems.length > 0 ? collapsibleSection(
|
|
19136
|
+
"po-at-risk",
|
|
19137
|
+
`At-Risk Delivery (${atRiskItems.length})`,
|
|
19048
19138
|
`<div class="table-wrap">
|
|
19049
19139
|
<table>
|
|
19050
19140
|
<thead>
|
|
19051
|
-
<tr><th>
|
|
19141
|
+
<tr><th>Feature</th><th>Risk Reasons</th></tr>
|
|
19052
19142
|
</thead>
|
|
19053
19143
|
<tbody>
|
|
19054
|
-
${
|
|
19055
|
-
const tags = t.signals.map((s) => `<span class="${signalTagClass(s.points)}" title="${escapeHtml(s.factor)}: +${s.points}pts">${escapeHtml(s.factor)}</span>`).join("");
|
|
19056
|
-
return `
|
|
19144
|
+
${atRiskItems.map((r) => `
|
|
19057
19145
|
<tr>
|
|
19058
|
-
<td><a href="/docs/${escapeHtml(
|
|
19059
|
-
<td>${
|
|
19060
|
-
|
|
19061
|
-
</tr>`;
|
|
19062
|
-
}).join("")}
|
|
19146
|
+
<td><a href="/docs/feature/${escapeHtml(r.feature.frontmatter.id)}">${escapeHtml(r.feature.frontmatter.id)}</a> ${escapeHtml(r.feature.frontmatter.title)}</td>
|
|
19147
|
+
<td>${r.reasons.map((reason) => `<span class="signal-tag signal-tag-high">${escapeHtml(reason)}</span>`).join(" ")}</td>
|
|
19148
|
+
</tr>`).join("")}
|
|
19063
19149
|
</tbody>
|
|
19064
19150
|
</table>
|
|
19065
19151
|
</div>`,
|
|
19066
19152
|
{ titleTag: "h3" }
|
|
19067
|
-
) :
|
|
19153
|
+
) : collapsibleSection(
|
|
19154
|
+
"po-at-risk",
|
|
19155
|
+
"At-Risk Delivery",
|
|
19156
|
+
'<div class="empty"><p style="color: var(--green);">No at-risk items \u2014 all features on track.</p></div>',
|
|
19157
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
19158
|
+
);
|
|
19068
19159
|
return `
|
|
19069
19160
|
<div class="page-header">
|
|
19070
19161
|
<h2>Product Owner Dashboard</h2>
|
|
19071
19162
|
<div class="subtitle">Feature delivery, decisions, and stakeholder alignment</div>
|
|
19072
19163
|
</div>
|
|
19073
19164
|
${statsCards}
|
|
19165
|
+
${atRiskSection}
|
|
19074
19166
|
${diagramSection}
|
|
19075
19167
|
${recentTable}
|
|
19076
|
-
${trendingSection}
|
|
19077
19168
|
`;
|
|
19078
19169
|
}
|
|
19079
19170
|
|
|
@@ -19252,44 +19343,74 @@ function tableDateFilter(tableId, colIndex) {
|
|
|
19252
19343
|
}
|
|
19253
19344
|
|
|
19254
19345
|
// src/web/templates/pages/po/backlog.ts
|
|
19346
|
+
function priorityClass(p) {
|
|
19347
|
+
if (!p) return "";
|
|
19348
|
+
const lower = p.toLowerCase();
|
|
19349
|
+
if (lower === "critical" || lower === "high") return " priority-high";
|
|
19350
|
+
if (lower === "medium") return " priority-medium";
|
|
19351
|
+
if (lower === "low") return " priority-low";
|
|
19352
|
+
return "";
|
|
19353
|
+
}
|
|
19354
|
+
function miniProgressBar(pct) {
|
|
19355
|
+
return `<div class="mini-progress-bar"><div class="mini-progress-fill" style="width:${pct}%"></div><span class="mini-progress-label">${pct}%</span></div>`;
|
|
19356
|
+
}
|
|
19255
19357
|
function poBacklogPage(ctx) {
|
|
19256
19358
|
const features = ctx.store.list({ type: "feature" });
|
|
19257
19359
|
const questions = ctx.store.list({ type: "question" });
|
|
19258
19360
|
const openQuestions = questions.filter((d) => d.frontmatter.status === "open");
|
|
19259
|
-
const
|
|
19260
|
-
const
|
|
19361
|
+
const priorityOrder2 = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
19362
|
+
const statusOrder2 = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
19261
19363
|
const sortedFeatures = [...features].sort((a, b) => {
|
|
19262
|
-
const sa =
|
|
19263
|
-
const sb =
|
|
19364
|
+
const sa = statusOrder2[a.frontmatter.status] ?? 3;
|
|
19365
|
+
const sb = statusOrder2[b.frontmatter.status] ?? 3;
|
|
19264
19366
|
if (sa !== sb) return sa - sb;
|
|
19265
|
-
const pa =
|
|
19266
|
-
const pb =
|
|
19367
|
+
const pa = priorityOrder2[a.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
19368
|
+
const pb = priorityOrder2[b.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
19267
19369
|
if (pa !== pb) return pa - pb;
|
|
19268
19370
|
return a.frontmatter.id.localeCompare(b.frontmatter.id);
|
|
19269
19371
|
});
|
|
19270
19372
|
const epics = ctx.store.list({ type: "epic" });
|
|
19271
19373
|
const featureToEpics = /* @__PURE__ */ new Map();
|
|
19272
19374
|
for (const epic of epics) {
|
|
19273
|
-
const
|
|
19274
|
-
const featureIds = Array.isArray(linked) ? linked : linked ? [linked] : [];
|
|
19375
|
+
const featureIds = normalizeLinkedFeatures(epic.frontmatter.linkedFeature);
|
|
19275
19376
|
for (const fid of featureIds) {
|
|
19276
|
-
const
|
|
19277
|
-
|
|
19278
|
-
featureToEpics.set(
|
|
19377
|
+
const arr = featureToEpics.get(fid) ?? [];
|
|
19378
|
+
arr.push(epic);
|
|
19379
|
+
featureToEpics.set(fid, arr);
|
|
19279
19380
|
}
|
|
19280
19381
|
}
|
|
19281
|
-
|
|
19282
|
-
|
|
19283
|
-
|
|
19284
|
-
|
|
19285
|
-
|
|
19286
|
-
|
|
19287
|
-
|
|
19382
|
+
const allTasks = ctx.store.list({ type: "task" });
|
|
19383
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
19384
|
+
for (const task of allTasks) {
|
|
19385
|
+
const tags = task.frontmatter.tags ?? [];
|
|
19386
|
+
for (const tag of tags) {
|
|
19387
|
+
if (tag.startsWith("epic:")) {
|
|
19388
|
+
const epicId = tag.slice(5);
|
|
19389
|
+
const arr = epicToTasks.get(epicId) ?? [];
|
|
19390
|
+
arr.push(task);
|
|
19391
|
+
epicToTasks.set(epicId, arr);
|
|
19392
|
+
}
|
|
19393
|
+
}
|
|
19394
|
+
}
|
|
19395
|
+
const DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19396
|
+
function featureTaskStats(featureId) {
|
|
19397
|
+
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
19398
|
+
let total = 0;
|
|
19399
|
+
let done = 0;
|
|
19400
|
+
let progressSum = 0;
|
|
19401
|
+
for (const epic of fEpics) {
|
|
19402
|
+
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
19403
|
+
total++;
|
|
19404
|
+
if (DONE_STATUSES14.has(t.frontmatter.status)) done++;
|
|
19405
|
+
progressSum += getEffectiveProgress(t.frontmatter);
|
|
19406
|
+
}
|
|
19407
|
+
}
|
|
19408
|
+
return { epicCount: fEpics.length, total, done, avgProgress: total > 0 ? Math.round(progressSum / total) : 0 };
|
|
19288
19409
|
}
|
|
19289
19410
|
const featureStatuses = [...new Set(features.map((d) => d.frontmatter.status))].sort();
|
|
19290
19411
|
const featurePriorities = [...new Set(features.map((d) => d.frontmatter.priority ?? "").filter(Boolean))].sort();
|
|
19291
19412
|
const featureEpicIds = [...new Set(
|
|
19292
|
-
features.flatMap((d) => featureToEpics.get(d.frontmatter.id) ?? [])
|
|
19413
|
+
features.flatMap((d) => (featureToEpics.get(d.frontmatter.id) ?? []).map((e) => e.frontmatter.id))
|
|
19293
19414
|
)].sort();
|
|
19294
19415
|
const featuresFilters = `<div class="filters">
|
|
19295
19416
|
${tableFilter("features-table", 2, "Status", featureStatuses)}
|
|
@@ -19300,12 +19421,13 @@ function poBacklogPage(ctx) {
|
|
|
19300
19421
|
<div class="table-wrap table-short">
|
|
19301
19422
|
<table id="features-table">
|
|
19302
19423
|
<thead>
|
|
19303
|
-
<tr>${sortableTh("ID", "features-table", 0)}${sortableTh("Title", "features-table", 1)}${sortableTh("Status", "features-table", 2)}${sortableTh("Priority", "features-table", 3)}<th>
|
|
19424
|
+
<tr>${sortableTh("ID", "features-table", 0)}${sortableTh("Title", "features-table", 1)}${sortableTh("Status", "features-table", 2)}${sortableTh("Priority", "features-table", 3)}<th>Epics</th><th>Tasks</th><th>Progress</th></tr>
|
|
19304
19425
|
</thead>
|
|
19305
19426
|
<tbody>
|
|
19306
19427
|
${sortedFeatures.map((d) => {
|
|
19307
|
-
const
|
|
19308
|
-
const
|
|
19428
|
+
const stats = featureTaskStats(d.frontmatter.id);
|
|
19429
|
+
const linkedEpicDocs = featureToEpics.get(d.frontmatter.id) ?? [];
|
|
19430
|
+
const epicLinks = linkedEpicDocs.map((e) => `<a href="/docs/epic/${escapeHtml(e.frontmatter.id)}">${escapeHtml(e.frontmatter.id)}</a>`).join(", ");
|
|
19309
19431
|
return `
|
|
19310
19432
|
<tr>
|
|
19311
19433
|
<td><a href="/docs/feature/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
@@ -19313,6 +19435,8 @@ function poBacklogPage(ctx) {
|
|
|
19313
19435
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
19314
19436
|
<td><span class="${priorityClass(d.frontmatter.priority)}">${escapeHtml(d.frontmatter.priority ?? "\u2014")}</span></td>
|
|
19315
19437
|
<td>${epicLinks || '<span class="text-dim">\u2014</span>'}</td>
|
|
19438
|
+
<td>${stats.total > 0 ? `${stats.done}/${stats.total}` : '<span class="text-dim">\u2014</span>'}</td>
|
|
19439
|
+
<td>${stats.total > 0 ? miniProgressBar(stats.avgProgress) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19316
19440
|
</tr>`;
|
|
19317
19441
|
}).join("")}
|
|
19318
19442
|
</tbody>
|
|
@@ -19354,14 +19478,23 @@ function poBacklogPage(ctx) {
|
|
|
19354
19478
|
|
|
19355
19479
|
// src/web/templates/pages/po/decisions.ts
|
|
19356
19480
|
var RESOLVED_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
19481
|
+
var KNOWN_OWNERS = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
|
|
19482
|
+
function ownerBadge(owner) {
|
|
19483
|
+
if (!owner) return '<span class="text-dim">\u2014</span>';
|
|
19484
|
+
const cls = KNOWN_OWNERS.has(owner.toLowerCase()) ? `owner-badge-${owner.toLowerCase()}` : "owner-badge-other";
|
|
19485
|
+
return `<span class="owner-badge ${cls}">${escapeHtml(owner.toUpperCase())}</span>`;
|
|
19486
|
+
}
|
|
19357
19487
|
function poDecisionsPage(ctx) {
|
|
19358
19488
|
const decisions = ctx.store.list({ type: "decision" });
|
|
19489
|
+
const questions = ctx.store.list({ type: "question" });
|
|
19490
|
+
const features = ctx.store.list({ type: "feature" });
|
|
19359
19491
|
const openDecisions = decisions.filter((d) => !RESOLVED_STATUSES.has(d.frontmatter.status));
|
|
19360
19492
|
const resolvedDecisions = decisions.filter((d) => RESOLVED_STATUSES.has(d.frontmatter.status));
|
|
19493
|
+
const openQuestions = questions.filter((d) => d.frontmatter.status === "open");
|
|
19361
19494
|
const statsCards = `
|
|
19362
19495
|
<div class="cards">
|
|
19363
19496
|
<div class="card">
|
|
19364
|
-
<div class="card-label">Open</div>
|
|
19497
|
+
<div class="card-label">Open Decisions</div>
|
|
19365
19498
|
<div class="card-value${openDecisions.length > 0 ? " priority-medium" : ""}">${openDecisions.length}</div>
|
|
19366
19499
|
<div class="card-sub">awaiting resolution</div>
|
|
19367
19500
|
</div>
|
|
@@ -19370,12 +19503,79 @@ function poDecisionsPage(ctx) {
|
|
|
19370
19503
|
<div class="card-value">${resolvedDecisions.length}</div>
|
|
19371
19504
|
<div class="card-sub">decisions made</div>
|
|
19372
19505
|
</div>
|
|
19506
|
+
<div class="card">
|
|
19507
|
+
<div class="card-label">Open Questions</div>
|
|
19508
|
+
<div class="card-value${openQuestions.length > 0 ? " priority-medium" : ""}">${openQuestions.length}</div>
|
|
19509
|
+
<div class="card-sub">needing answers</div>
|
|
19510
|
+
</div>
|
|
19373
19511
|
<div class="card">
|
|
19374
19512
|
<div class="card-label">Total</div>
|
|
19375
19513
|
<div class="card-value">${decisions.length}</div>
|
|
19376
19514
|
<div class="card-sub">all decisions</div>
|
|
19377
19515
|
</div>
|
|
19378
19516
|
</div>`;
|
|
19517
|
+
function daysSince(isoDate) {
|
|
19518
|
+
if (!isoDate) return 0;
|
|
19519
|
+
return Math.max(0, Math.floor((Date.now() - new Date(isoDate).getTime()) / 864e5));
|
|
19520
|
+
}
|
|
19521
|
+
const featureGroups = /* @__PURE__ */ new Map();
|
|
19522
|
+
const unlinked = [];
|
|
19523
|
+
function addToGroup(doc, docType) {
|
|
19524
|
+
const tags = doc.frontmatter.tags ?? [];
|
|
19525
|
+
const featureTags = tags.filter((t) => t.startsWith("feature:")).map((t) => t.slice(8));
|
|
19526
|
+
const item = { doc, docType, ageDays: daysSince(doc.frontmatter.created) };
|
|
19527
|
+
if (featureTags.length === 0) {
|
|
19528
|
+
unlinked.push(item);
|
|
19529
|
+
} else {
|
|
19530
|
+
for (const fid of featureTags) {
|
|
19531
|
+
const arr = featureGroups.get(fid) ?? [];
|
|
19532
|
+
arr.push(item);
|
|
19533
|
+
featureGroups.set(fid, arr);
|
|
19534
|
+
}
|
|
19535
|
+
}
|
|
19536
|
+
}
|
|
19537
|
+
for (const d of openDecisions) addToGroup(d, "decision");
|
|
19538
|
+
for (const q of openQuestions) addToGroup(q, "question");
|
|
19539
|
+
const totalDeps = openDecisions.length + openQuestions.length;
|
|
19540
|
+
const featureLookup = new Map(features.map((f) => [f.frontmatter.id, f]));
|
|
19541
|
+
function renderDepRows(items) {
|
|
19542
|
+
return items.map((item) => `
|
|
19543
|
+
<tr>
|
|
19544
|
+
<td><a href="/docs/${item.docType}/${escapeHtml(item.doc.frontmatter.id)}">${escapeHtml(item.doc.frontmatter.id)}</a></td>
|
|
19545
|
+
<td>${escapeHtml(item.doc.frontmatter.title)}</td>
|
|
19546
|
+
<td>${escapeHtml(typeLabel(item.docType))}</td>
|
|
19547
|
+
<td>${ownerBadge(item.doc.frontmatter.owner)}</td>
|
|
19548
|
+
<td>${item.ageDays}d</td>
|
|
19549
|
+
</tr>`).join("");
|
|
19550
|
+
}
|
|
19551
|
+
let depRows = "";
|
|
19552
|
+
for (const [fid, items] of featureGroups) {
|
|
19553
|
+
const feat = featureLookup.get(fid);
|
|
19554
|
+
const label = feat ? `${escapeHtml(fid)}: ${escapeHtml(feat.frontmatter.title)}` : escapeHtml(fid);
|
|
19555
|
+
depRows += `
|
|
19556
|
+
<tr class="group-header-row"><td colspan="5"><strong>${label}</strong></td></tr>
|
|
19557
|
+
${renderDepRows(items)}`;
|
|
19558
|
+
}
|
|
19559
|
+
if (unlinked.length > 0) {
|
|
19560
|
+
depRows += `
|
|
19561
|
+
<tr class="group-header-row"><td colspan="5"><strong>Unlinked</strong></td></tr>
|
|
19562
|
+
${renderDepRows(unlinked)}`;
|
|
19563
|
+
}
|
|
19564
|
+
const depsSection = totalDeps > 0 ? collapsibleSection(
|
|
19565
|
+
"po-decisions-deps",
|
|
19566
|
+
`By Feature (${totalDeps})`,
|
|
19567
|
+
`<div class="table-wrap">
|
|
19568
|
+
<table>
|
|
19569
|
+
<thead>
|
|
19570
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Owner</th><th>Age</th></tr>
|
|
19571
|
+
</thead>
|
|
19572
|
+
<tbody>
|
|
19573
|
+
${depRows}
|
|
19574
|
+
</tbody>
|
|
19575
|
+
</table>
|
|
19576
|
+
</div>`,
|
|
19577
|
+
{ titleTag: "h3" }
|
|
19578
|
+
) : "";
|
|
19379
19579
|
function decisionTable(docs, tableId) {
|
|
19380
19580
|
if (docs.length === 0) return '<div class="empty"><p>None found.</p></div>';
|
|
19381
19581
|
const statuses = [...new Set(docs.map((d) => d.frontmatter.status))].sort();
|
|
@@ -19397,7 +19597,7 @@ function poDecisionsPage(ctx) {
|
|
|
19397
19597
|
<td><a href="/docs/decision/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
19398
19598
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
19399
19599
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
19400
|
-
<td>${
|
|
19600
|
+
<td>${ownerBadge(d.frontmatter.owner)}</td>
|
|
19401
19601
|
<td>${formatDate(d.frontmatter.created)}</td>
|
|
19402
19602
|
</tr>`).join("")}
|
|
19403
19603
|
</tbody>
|
|
@@ -19419,16 +19619,185 @@ function poDecisionsPage(ctx) {
|
|
|
19419
19619
|
return `
|
|
19420
19620
|
<div class="page-header">
|
|
19421
19621
|
<h2>Decision Log</h2>
|
|
19422
|
-
<div class="subtitle">Track and manage product decisions</div>
|
|
19622
|
+
<div class="subtitle">Track and manage product decisions and dependencies</div>
|
|
19423
19623
|
</div>
|
|
19424
19624
|
${statsCards}
|
|
19625
|
+
${depsSection}
|
|
19425
19626
|
${openSection}
|
|
19426
19627
|
${resolvedSection}
|
|
19427
19628
|
${renderTableUtilsScript()}
|
|
19428
19629
|
`;
|
|
19429
19630
|
}
|
|
19430
19631
|
|
|
19632
|
+
// src/web/templates/components/work-items-table.ts
|
|
19633
|
+
var FOCUS_BORDER_PALETTE = [
|
|
19634
|
+
"hsl(220, 60%, 55%)",
|
|
19635
|
+
"hsl(160, 50%, 45%)",
|
|
19636
|
+
"hsl(280, 45%, 55%)",
|
|
19637
|
+
"hsl(30, 65%, 55%)",
|
|
19638
|
+
"hsl(340, 50%, 55%)",
|
|
19639
|
+
"hsl(190, 50%, 45%)",
|
|
19640
|
+
"hsl(60, 50%, 50%)",
|
|
19641
|
+
"hsl(120, 40%, 45%)"
|
|
19642
|
+
];
|
|
19643
|
+
function hashString(s) {
|
|
19644
|
+
let h = 0;
|
|
19645
|
+
for (let i = 0; i < s.length; i++) {
|
|
19646
|
+
h = (h << 5) - h + s.charCodeAt(i) | 0;
|
|
19647
|
+
}
|
|
19648
|
+
return Math.abs(h);
|
|
19649
|
+
}
|
|
19650
|
+
function countFocusStats(items) {
|
|
19651
|
+
let total = 0;
|
|
19652
|
+
let done = 0;
|
|
19653
|
+
let inProgress = 0;
|
|
19654
|
+
function walk(list) {
|
|
19655
|
+
for (const w of list) {
|
|
19656
|
+
if (w.type !== "contribution") {
|
|
19657
|
+
total++;
|
|
19658
|
+
const s = w.status.toLowerCase();
|
|
19659
|
+
if (s === "done" || s === "closed" || s === "resolved" || s === "decided") done++;
|
|
19660
|
+
else if (s === "in-progress" || s === "in progress") inProgress++;
|
|
19661
|
+
}
|
|
19662
|
+
if (w.children) walk(w.children);
|
|
19663
|
+
}
|
|
19664
|
+
}
|
|
19665
|
+
walk(items);
|
|
19666
|
+
return { total, done, inProgress };
|
|
19667
|
+
}
|
|
19668
|
+
var KNOWN_OWNERS2 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
|
|
19669
|
+
function ownerBadge2(owner) {
|
|
19670
|
+
if (!owner) return '<span class="text-dim">\u2014</span>';
|
|
19671
|
+
const cls = KNOWN_OWNERS2.has(owner) ? `owner-badge-${owner}` : "owner-badge-other";
|
|
19672
|
+
return `<span class="owner-badge ${cls}">${escapeHtml(owner.toUpperCase())}</span>`;
|
|
19673
|
+
}
|
|
19674
|
+
function renderItemRows(items, borderColor, showOwner, depth = 0) {
|
|
19675
|
+
return items.flatMap((w) => {
|
|
19676
|
+
const isChild = depth > 0;
|
|
19677
|
+
const isContribution = w.type === "contribution";
|
|
19678
|
+
const classes = ["focus-row"];
|
|
19679
|
+
if (isContribution) classes.push("contribution-row");
|
|
19680
|
+
else if (isChild) classes.push("child-row");
|
|
19681
|
+
const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
|
|
19682
|
+
const progressCell = !isContribution && w.progress !== void 0 ? `<div class="mini-progress-bar"><div class="mini-progress-fill" style="width:${w.progress}%"></div><span class="mini-progress-label">${w.progress}%</span></div>` : "";
|
|
19683
|
+
const ownerCell = showOwner ? `<td>${ownerBadge2(w.owner)}</td>` : "";
|
|
19684
|
+
const row = `
|
|
19685
|
+
<tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
|
|
19686
|
+
<td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
19687
|
+
<td>${escapeHtml(w.title)}</td>
|
|
19688
|
+
${ownerCell}
|
|
19689
|
+
<td>${statusBadge(w.status)}</td>
|
|
19690
|
+
<td>${progressCell}</td>
|
|
19691
|
+
</tr>`;
|
|
19692
|
+
const childRows = w.children ? renderItemRows(w.children, borderColor, showOwner, depth + 1) : [];
|
|
19693
|
+
return [row, ...childRows];
|
|
19694
|
+
});
|
|
19695
|
+
}
|
|
19696
|
+
function renderWorkItemsTable(items, options) {
|
|
19697
|
+
const sectionId = options?.sectionId ?? "work-items";
|
|
19698
|
+
const title = options?.title ?? "Work Items";
|
|
19699
|
+
const defaultCollapsed = options?.defaultCollapsed ?? false;
|
|
19700
|
+
const showOwner = options?.showOwner ?? false;
|
|
19701
|
+
const focusGroups = /* @__PURE__ */ new Map();
|
|
19702
|
+
for (const item of items) {
|
|
19703
|
+
const focus = item.workFocus ?? "Unassigned";
|
|
19704
|
+
if (!focusGroups.has(focus)) focusGroups.set(focus, []);
|
|
19705
|
+
focusGroups.get(focus).push(item);
|
|
19706
|
+
}
|
|
19707
|
+
const focusColorMap = /* @__PURE__ */ new Map();
|
|
19708
|
+
for (const name of focusGroups.keys()) {
|
|
19709
|
+
focusColorMap.set(name, FOCUS_BORDER_PALETTE[hashString(name) % FOCUS_BORDER_PALETTE.length]);
|
|
19710
|
+
}
|
|
19711
|
+
const allWorkItemRows = [];
|
|
19712
|
+
for (const [focus, groupItems] of focusGroups) {
|
|
19713
|
+
const color = focusColorMap.get(focus);
|
|
19714
|
+
const stats = countFocusStats(groupItems);
|
|
19715
|
+
const pct = stats.total > 0 ? Math.round(stats.done / stats.total * 100) : 0;
|
|
19716
|
+
const summaryParts = [];
|
|
19717
|
+
if (stats.done > 0) summaryParts.push(`${stats.done} done`);
|
|
19718
|
+
if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
|
|
19719
|
+
const remaining = stats.total - stats.done - stats.inProgress;
|
|
19720
|
+
if (remaining > 0) summaryParts.push(`${remaining} open`);
|
|
19721
|
+
const leftColspan = showOwner ? 3 : 2;
|
|
19722
|
+
allWorkItemRows.push(`
|
|
19723
|
+
<tr class="focus-group-header" style="--focus-color: ${color}">
|
|
19724
|
+
<td colspan="${leftColspan}">
|
|
19725
|
+
<span class="focus-group-name">${escapeHtml(focus)}</span>
|
|
19726
|
+
<span class="focus-group-stats">${summaryParts.join(" / ")}</span>
|
|
19727
|
+
</td>
|
|
19728
|
+
<td colspan="2">
|
|
19729
|
+
<div class="mini-progress-bar focus-group-progress"><div class="mini-progress-fill" style="width:${pct}%"></div><span class="mini-progress-label">${pct}%</span></div>
|
|
19730
|
+
</td>
|
|
19731
|
+
</tr>`);
|
|
19732
|
+
allWorkItemRows.push(...renderItemRows(groupItems, color, showOwner));
|
|
19733
|
+
}
|
|
19734
|
+
if (allWorkItemRows.length === 0) return "";
|
|
19735
|
+
const ownerHeader = showOwner ? "<th>Owner</th>" : "";
|
|
19736
|
+
return collapsibleSection(
|
|
19737
|
+
sectionId,
|
|
19738
|
+
title,
|
|
19739
|
+
`<div class="table-wrap">
|
|
19740
|
+
<table id="${sectionId}-table">
|
|
19741
|
+
<thead>
|
|
19742
|
+
<tr>
|
|
19743
|
+
<th>ID</th>
|
|
19744
|
+
<th>Title</th>
|
|
19745
|
+
${ownerHeader}
|
|
19746
|
+
<th>Status</th>
|
|
19747
|
+
<th>Progress</th>
|
|
19748
|
+
</tr>
|
|
19749
|
+
</thead>
|
|
19750
|
+
<tbody>
|
|
19751
|
+
${allWorkItemRows.join("")}
|
|
19752
|
+
</tbody>
|
|
19753
|
+
</table>
|
|
19754
|
+
</div>`,
|
|
19755
|
+
{ titleTag: "h3", defaultCollapsed }
|
|
19756
|
+
);
|
|
19757
|
+
}
|
|
19758
|
+
var DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled", "decided"]);
|
|
19759
|
+
function computeOwnerCompletionPct(items, owner) {
|
|
19760
|
+
let total = 0;
|
|
19761
|
+
let progressSum = 0;
|
|
19762
|
+
function walk(list) {
|
|
19763
|
+
for (const w of list) {
|
|
19764
|
+
if (w.type !== "contribution" && w.owner === owner) {
|
|
19765
|
+
total++;
|
|
19766
|
+
progressSum += w.progress ?? (DONE_STATUSES6.has(w.status) ? 100 : 0);
|
|
19767
|
+
}
|
|
19768
|
+
if (w.children) walk(w.children);
|
|
19769
|
+
}
|
|
19770
|
+
}
|
|
19771
|
+
walk(items);
|
|
19772
|
+
return total > 0 ? Math.round(progressSum / total) : 0;
|
|
19773
|
+
}
|
|
19774
|
+
function filterItemsByOwner(items, owner) {
|
|
19775
|
+
const result = [];
|
|
19776
|
+
for (const item of items) {
|
|
19777
|
+
if (item.owner === owner) {
|
|
19778
|
+
result.push(item);
|
|
19779
|
+
} else if (item.children) {
|
|
19780
|
+
result.push(...filterItemsByOwner(item.children, owner));
|
|
19781
|
+
}
|
|
19782
|
+
}
|
|
19783
|
+
return result;
|
|
19784
|
+
}
|
|
19785
|
+
|
|
19431
19786
|
// src/web/templates/pages/po/delivery.ts
|
|
19787
|
+
var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19788
|
+
var priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
19789
|
+
var statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
19790
|
+
function priorityClass2(p) {
|
|
19791
|
+
if (!p) return "";
|
|
19792
|
+
const lower = p.toLowerCase();
|
|
19793
|
+
if (lower === "critical" || lower === "high") return " priority-high";
|
|
19794
|
+
if (lower === "medium") return " priority-medium";
|
|
19795
|
+
if (lower === "low") return " priority-low";
|
|
19796
|
+
return "";
|
|
19797
|
+
}
|
|
19798
|
+
function miniProgressBar2(pct) {
|
|
19799
|
+
return `<div class="mini-progress-bar"><div class="mini-progress-fill" style="width:${pct}%"></div><span class="mini-progress-label">${pct}%</span></div>`;
|
|
19800
|
+
}
|
|
19432
19801
|
var PO_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
|
|
19433
19802
|
"stakeholder-feedback",
|
|
19434
19803
|
"acceptance-result",
|
|
@@ -19454,24 +19823,8 @@ function poDeliveryPage(ctx) {
|
|
|
19454
19823
|
<p>No active sprint found. Create a sprint and set its status to "active" to track delivery.</p>
|
|
19455
19824
|
</div>`;
|
|
19456
19825
|
}
|
|
19457
|
-
const
|
|
19458
|
-
|
|
19459
|
-
);
|
|
19460
|
-
function findContributions(items, parentId) {
|
|
19461
|
-
const result = [];
|
|
19462
|
-
for (const item of items) {
|
|
19463
|
-
if (item.type === "contribution" && PO_CONTRIBUTION_TYPES.has(item.id.split("-").slice(0, -1).join("-") || "")) {
|
|
19464
|
-
result.push({ id: item.id, title: item.title, type: item.type, status: item.status, parentId });
|
|
19465
|
-
}
|
|
19466
|
-
if (PO_CONTRIBUTION_TYPES.has(item.type)) {
|
|
19467
|
-
result.push({ id: item.id, title: item.title, type: item.type, status: item.status, parentId });
|
|
19468
|
-
}
|
|
19469
|
-
if (item.children) {
|
|
19470
|
-
result.push(...findContributions(item.children, item.id));
|
|
19471
|
-
}
|
|
19472
|
-
}
|
|
19473
|
-
return result;
|
|
19474
|
-
}
|
|
19826
|
+
const poItems = filterItemsByOwner(data.workItems.items, "po");
|
|
19827
|
+
const poCompletionPct = computeOwnerCompletionPct(data.workItems.items, "po");
|
|
19475
19828
|
const allDocs = ctx.store.list();
|
|
19476
19829
|
const poContributions = allDocs.filter((d) => PO_CONTRIBUTION_TYPES.has(d.frontmatter.type));
|
|
19477
19830
|
const statsCards = `
|
|
@@ -19481,16 +19834,16 @@ function poDeliveryPage(ctx) {
|
|
|
19481
19834
|
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
19482
19835
|
<div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
|
|
19483
19836
|
</div>
|
|
19837
|
+
<div class="card">
|
|
19838
|
+
<div class="card-label">PO Completion</div>
|
|
19839
|
+
<div class="card-value">${poCompletionPct}%</div>
|
|
19840
|
+
<div class="card-sub">${poItems.length} owned items</div>
|
|
19841
|
+
</div>
|
|
19484
19842
|
<div class="card">
|
|
19485
19843
|
<div class="card-label">Days Remaining</div>
|
|
19486
19844
|
<div class="card-value">${data.timeline.daysRemaining}</div>
|
|
19487
19845
|
<div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
|
|
19488
19846
|
</div>
|
|
19489
|
-
<div class="card">
|
|
19490
|
-
<div class="card-label">Features Done</div>
|
|
19491
|
-
<div class="card-value">${doneFeatures.length}</div>
|
|
19492
|
-
<div class="card-sub">this sprint</div>
|
|
19493
|
-
</div>
|
|
19494
19847
|
<div class="card">
|
|
19495
19848
|
<div class="card-label">PO Contributions</div>
|
|
19496
19849
|
<div class="card-value">${poContributions.length}</div>
|
|
@@ -19502,7 +19855,11 @@ function poDeliveryPage(ctx) {
|
|
|
19502
19855
|
<strong>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</strong>
|
|
19503
19856
|
${data.sprint.goal ? ` | ${escapeHtml(data.sprint.goal)}` : ""}
|
|
19504
19857
|
</div>`;
|
|
19505
|
-
const
|
|
19858
|
+
const workItemsSection = renderWorkItemsTable(poItems, {
|
|
19859
|
+
sectionId: "po-delivery-items",
|
|
19860
|
+
title: "PO Work Items"
|
|
19861
|
+
});
|
|
19862
|
+
const epicsSection = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
19506
19863
|
"po-delivery-epics",
|
|
19507
19864
|
"Linked Epics",
|
|
19508
19865
|
`<div class="table-wrap">
|
|
@@ -19523,6 +19880,82 @@ function poDeliveryPage(ctx) {
|
|
|
19523
19880
|
</div>`,
|
|
19524
19881
|
{ titleTag: "h3" }
|
|
19525
19882
|
) : "";
|
|
19883
|
+
const features = ctx.store.list({ type: "feature" });
|
|
19884
|
+
const epics = ctx.store.list({ type: "epic" });
|
|
19885
|
+
const allTasks = ctx.store.list({ type: "task" });
|
|
19886
|
+
const sprints = ctx.store.list({ type: "sprint" });
|
|
19887
|
+
const featureToEpics = /* @__PURE__ */ new Map();
|
|
19888
|
+
for (const epic of epics) {
|
|
19889
|
+
const featureIds = normalizeLinkedFeatures(epic.frontmatter.linkedFeature);
|
|
19890
|
+
for (const fid of featureIds) {
|
|
19891
|
+
const arr = featureToEpics.get(fid) ?? [];
|
|
19892
|
+
arr.push(epic);
|
|
19893
|
+
featureToEpics.set(fid, arr);
|
|
19894
|
+
}
|
|
19895
|
+
}
|
|
19896
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
19897
|
+
for (const task of allTasks) {
|
|
19898
|
+
const tags = task.frontmatter.tags ?? [];
|
|
19899
|
+
for (const tag of tags) {
|
|
19900
|
+
if (tag.startsWith("epic:")) {
|
|
19901
|
+
const arr = epicToTasks.get(tag.slice(5)) ?? [];
|
|
19902
|
+
arr.push(task);
|
|
19903
|
+
epicToTasks.set(tag.slice(5), arr);
|
|
19904
|
+
}
|
|
19905
|
+
}
|
|
19906
|
+
}
|
|
19907
|
+
const activeSprint = sprints.find((s) => s.frontmatter.status === "active");
|
|
19908
|
+
const activeSprintEpicIds = new Set(
|
|
19909
|
+
activeSprint ? normalizeLinkedEpics(activeSprint.frontmatter.linkedEpics) : []
|
|
19910
|
+
);
|
|
19911
|
+
function featureSprintLabel(featureId) {
|
|
19912
|
+
if (!activeSprint) return "\u2014";
|
|
19913
|
+
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
19914
|
+
return fEpics.some((e) => activeSprintEpicIds.has(e.frontmatter.id)) ? escapeHtml(activeSprint.frontmatter.id) : "\u2014";
|
|
19915
|
+
}
|
|
19916
|
+
function featureProgress(featureId) {
|
|
19917
|
+
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
19918
|
+
let total = 0;
|
|
19919
|
+
let progressSum = 0;
|
|
19920
|
+
for (const epic of fEpics) {
|
|
19921
|
+
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
19922
|
+
total++;
|
|
19923
|
+
progressSum += getEffectiveProgress(t.frontmatter);
|
|
19924
|
+
}
|
|
19925
|
+
}
|
|
19926
|
+
return total > 0 ? Math.round(progressSum / total) : 0;
|
|
19927
|
+
}
|
|
19928
|
+
const nonDoneFeatures = features.filter((f) => !DONE_STATUSES7.has(f.frontmatter.status)).sort((a, b) => {
|
|
19929
|
+
const pa = priorityOrder[a.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
19930
|
+
const pb = priorityOrder[b.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
19931
|
+
if (pa !== pb) return pa - pb;
|
|
19932
|
+
const sa = statusOrder[a.frontmatter.status] ?? 3;
|
|
19933
|
+
const sb = statusOrder[b.frontmatter.status] ?? 3;
|
|
19934
|
+
return sa - sb;
|
|
19935
|
+
});
|
|
19936
|
+
const priorityQueueSection = collapsibleSection(
|
|
19937
|
+
"po-priority-queue",
|
|
19938
|
+
`Priority Queue (${nonDoneFeatures.length})`,
|
|
19939
|
+
nonDoneFeatures.length > 0 ? `<div class="table-wrap">
|
|
19940
|
+
<table>
|
|
19941
|
+
<thead>
|
|
19942
|
+
<tr><th>Priority</th><th>ID</th><th>Title</th><th>Status</th><th>Sprint</th><th>Progress</th></tr>
|
|
19943
|
+
</thead>
|
|
19944
|
+
<tbody>
|
|
19945
|
+
${nonDoneFeatures.map((f) => `
|
|
19946
|
+
<tr>
|
|
19947
|
+
<td><span class="${priorityClass2(f.frontmatter.priority)}">${escapeHtml(f.frontmatter.priority ?? "\u2014")}</span></td>
|
|
19948
|
+
<td><a href="/docs/feature/${escapeHtml(f.frontmatter.id)}">${escapeHtml(f.frontmatter.id)}</a></td>
|
|
19949
|
+
<td>${escapeHtml(f.frontmatter.title)}</td>
|
|
19950
|
+
<td>${statusBadge(f.frontmatter.status)}</td>
|
|
19951
|
+
<td>${featureSprintLabel(f.frontmatter.id)}</td>
|
|
19952
|
+
<td>${miniProgressBar2(featureProgress(f.frontmatter.id))}</td>
|
|
19953
|
+
</tr>`).join("")}
|
|
19954
|
+
</tbody>
|
|
19955
|
+
</table>
|
|
19956
|
+
</div>` : '<div class="empty"><p>No active features in the queue.</p></div>',
|
|
19957
|
+
{ titleTag: "h3" }
|
|
19958
|
+
);
|
|
19526
19959
|
const contributionsSection = poContributions.length > 0 ? collapsibleSection(
|
|
19527
19960
|
"po-delivery-contributions",
|
|
19528
19961
|
`PO Contributions (${poContributions.length})`,
|
|
@@ -19553,7 +19986,9 @@ function poDeliveryPage(ctx) {
|
|
|
19553
19986
|
${sprintHeader}
|
|
19554
19987
|
${progressBar(data.workItems.completionPct)}
|
|
19555
19988
|
${statsCards}
|
|
19556
|
-
${
|
|
19989
|
+
${workItemsSection}
|
|
19990
|
+
${epicsSection}
|
|
19991
|
+
${priorityQueueSection}
|
|
19557
19992
|
${contributionsSection}
|
|
19558
19993
|
`;
|
|
19559
19994
|
}
|
|
@@ -19579,6 +20014,12 @@ function renderGarWidget(report) {
|
|
|
19579
20014
|
}
|
|
19580
20015
|
|
|
19581
20016
|
// src/web/templates/pages/po/stakeholders.ts
|
|
20017
|
+
var KNOWN_OWNERS3 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
|
|
20018
|
+
function ownerBadge3(owner) {
|
|
20019
|
+
if (!owner) return '<span class="text-dim">\u2014</span>';
|
|
20020
|
+
const cls = KNOWN_OWNERS3.has(owner.toLowerCase()) ? `owner-badge-${owner.toLowerCase()}` : "owner-badge-other";
|
|
20021
|
+
return `<span class="owner-badge ${cls}">${escapeHtml(owner.toUpperCase())}</span>`;
|
|
20022
|
+
}
|
|
19582
20023
|
function poStakeholdersPage(ctx) {
|
|
19583
20024
|
const garReport = getGarData(ctx.store, ctx.projectName);
|
|
19584
20025
|
const actions = ctx.store.list({ type: "action" });
|
|
@@ -19629,7 +20070,7 @@ function poStakeholdersPage(ctx) {
|
|
|
19629
20070
|
<td><a href="/docs/action/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
19630
20071
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
19631
20072
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
19632
|
-
<td>${
|
|
20073
|
+
<td>${ownerBadge3(d.frontmatter.owner)}</td>
|
|
19633
20074
|
<td>${d.frontmatter.dueDate ? formatDate(d.frontmatter.dueDate) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19634
20075
|
</tr>`).join("")}
|
|
19635
20076
|
</tbody>
|
|
@@ -19652,7 +20093,7 @@ function poStakeholdersPage(ctx) {
|
|
|
19652
20093
|
<tr>
|
|
19653
20094
|
<td><a href="/docs/question/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
19654
20095
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
19655
|
-
<td>${
|
|
20096
|
+
<td>${ownerBadge3(d.frontmatter.owner)}</td>
|
|
19656
20097
|
<td>${formatDate(d.frontmatter.created)}</td>
|
|
19657
20098
|
</tr>`).join("")}
|
|
19658
20099
|
</tbody>
|
|
@@ -19693,7 +20134,7 @@ registerPersonaPage("po", "delivery", poDeliveryPage);
|
|
|
19693
20134
|
registerPersonaPage("po", "stakeholders", poStakeholdersPage);
|
|
19694
20135
|
|
|
19695
20136
|
// src/web/templates/pages/dm/dashboard.ts
|
|
19696
|
-
var
|
|
20137
|
+
var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19697
20138
|
function progressBar2(pct) {
|
|
19698
20139
|
return `<div class="sprint-progress-bar">
|
|
19699
20140
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
@@ -19704,7 +20145,7 @@ function dmDashboardPage(ctx) {
|
|
|
19704
20145
|
const sprintData = getSprintSummaryData(ctx.store);
|
|
19705
20146
|
const upcoming = getUpcomingData(ctx.store);
|
|
19706
20147
|
const actions = ctx.store.list({ type: "action" });
|
|
19707
|
-
const openActions = actions.filter((d) => !
|
|
20148
|
+
const openActions = actions.filter((d) => !DONE_STATUSES8.has(d.frontmatter.status));
|
|
19708
20149
|
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
19709
20150
|
const statsCards = `
|
|
19710
20151
|
<div class="cards">
|
|
@@ -19791,42 +20232,46 @@ function dmDashboardPage(ctx) {
|
|
|
19791
20232
|
`;
|
|
19792
20233
|
}
|
|
19793
20234
|
|
|
19794
|
-
// src/web/templates/pages/sprint
|
|
20235
|
+
// src/web/templates/pages/dm/sprint.ts
|
|
19795
20236
|
function progressBar3(pct) {
|
|
19796
20237
|
return `<div class="sprint-progress-bar">
|
|
19797
20238
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
19798
20239
|
<span class="sprint-progress-label">${pct}%</span>
|
|
19799
20240
|
</div>`;
|
|
19800
20241
|
}
|
|
19801
|
-
function
|
|
20242
|
+
function dmSprintPage(ctx) {
|
|
20243
|
+
const data = getSprintSummaryData(ctx.store);
|
|
19802
20244
|
if (!data) {
|
|
19803
20245
|
return `
|
|
19804
20246
|
<div class="page-header">
|
|
19805
|
-
<h2>Sprint
|
|
19806
|
-
<div class="subtitle">
|
|
20247
|
+
<h2>Sprint Execution</h2>
|
|
20248
|
+
<div class="subtitle">Full sprint oversight and delivery tracking</div>
|
|
19807
20249
|
</div>
|
|
19808
20250
|
<div class="empty">
|
|
19809
20251
|
<h3>No Active Sprint</h3>
|
|
19810
|
-
<p>No active sprint found. Create a sprint and set its status to "active" to
|
|
20252
|
+
<p>No active sprint found. Create a sprint and set its status to "active" to track execution.</p>
|
|
19811
20253
|
</div>`;
|
|
19812
20254
|
}
|
|
20255
|
+
const dmCompletionPct = computeOwnerCompletionPct(data.workItems.items, "dm");
|
|
20256
|
+
const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
|
|
20257
|
+
const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
|
|
19813
20258
|
const statsCards = `
|
|
19814
20259
|
<div class="cards">
|
|
19815
20260
|
<div class="card">
|
|
19816
|
-
<div class="card-label">Completion</div>
|
|
20261
|
+
<div class="card-label">Sprint Completion</div>
|
|
19817
20262
|
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
19818
20263
|
<div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
|
|
19819
20264
|
</div>
|
|
20265
|
+
<div class="card">
|
|
20266
|
+
<div class="card-label">DM Completion</div>
|
|
20267
|
+
<div class="card-value">${dmCompletionPct}%</div>
|
|
20268
|
+
<div class="card-sub">DM-owned items</div>
|
|
20269
|
+
</div>
|
|
19820
20270
|
<div class="card">
|
|
19821
20271
|
<div class="card-label">Days Remaining</div>
|
|
19822
20272
|
<div class="card-value">${data.timeline.daysRemaining}</div>
|
|
19823
20273
|
<div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
|
|
19824
20274
|
</div>
|
|
19825
|
-
<div class="card">
|
|
19826
|
-
<div class="card-label">Epics</div>
|
|
19827
|
-
<div class="card-value">${data.linkedEpics.length}</div>
|
|
19828
|
-
<div class="card-sub">linked to sprint</div>
|
|
19829
|
-
</div>
|
|
19830
20275
|
<a class="card card-link" href="sprint-blockers">
|
|
19831
20276
|
<div class="card-label">Blockers</div>
|
|
19832
20277
|
<div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
|
|
@@ -19838,13 +20283,18 @@ function sprintSummaryPage(data, cached2) {
|
|
|
19838
20283
|
<div class="card-sub">open risk items</div>
|
|
19839
20284
|
</a>
|
|
19840
20285
|
</div>`;
|
|
19841
|
-
const
|
|
19842
|
-
"
|
|
20286
|
+
const workItemsSection = renderWorkItemsTable(data.workItems.items, {
|
|
20287
|
+
sectionId: "dm-sprint-items",
|
|
20288
|
+
title: "Sprint Work Items",
|
|
20289
|
+
showOwner: true
|
|
20290
|
+
});
|
|
20291
|
+
const epicsSection = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
20292
|
+
"dm-sprint-epics",
|
|
19843
20293
|
"Linked Epics",
|
|
19844
20294
|
`<div class="table-wrap">
|
|
19845
20295
|
<table>
|
|
19846
20296
|
<thead>
|
|
19847
|
-
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
|
|
20297
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks Done</th></tr>
|
|
19848
20298
|
</thead>
|
|
19849
20299
|
<tbody>
|
|
19850
20300
|
${data.linkedEpics.map((e) => `
|
|
@@ -19859,138 +20309,29 @@ function sprintSummaryPage(data, cached2) {
|
|
|
19859
20309
|
</div>`,
|
|
19860
20310
|
{ titleTag: "h3" }
|
|
19861
20311
|
) : "";
|
|
19862
|
-
const
|
|
19863
|
-
"
|
|
19864
|
-
|
|
19865
|
-
"hsl(280, 45%, 55%)",
|
|
19866
|
-
"hsl(30, 65%, 55%)",
|
|
19867
|
-
"hsl(340, 50%, 55%)",
|
|
19868
|
-
"hsl(190, 50%, 45%)",
|
|
19869
|
-
"hsl(60, 50%, 50%)",
|
|
19870
|
-
"hsl(120, 40%, 45%)"
|
|
19871
|
-
];
|
|
19872
|
-
function hashString(s) {
|
|
19873
|
-
let h = 0;
|
|
19874
|
-
for (let i = 0; i < s.length; i++) {
|
|
19875
|
-
h = (h << 5) - h + s.charCodeAt(i) | 0;
|
|
19876
|
-
}
|
|
19877
|
-
return Math.abs(h);
|
|
19878
|
-
}
|
|
19879
|
-
const focusGroups = /* @__PURE__ */ new Map();
|
|
19880
|
-
for (const item of data.workItems.items) {
|
|
19881
|
-
const focus = item.workFocus ?? "Unassigned";
|
|
19882
|
-
if (!focusGroups.has(focus)) focusGroups.set(focus, []);
|
|
19883
|
-
focusGroups.get(focus).push(item);
|
|
19884
|
-
}
|
|
19885
|
-
const focusColorMap = /* @__PURE__ */ new Map();
|
|
19886
|
-
for (const name of focusGroups.keys()) {
|
|
19887
|
-
focusColorMap.set(name, FOCUS_BORDER_PALETTE[hashString(name) % FOCUS_BORDER_PALETTE.length]);
|
|
19888
|
-
}
|
|
19889
|
-
function countFocusStats(items) {
|
|
19890
|
-
let total = 0;
|
|
19891
|
-
let done = 0;
|
|
19892
|
-
let inProgress = 0;
|
|
19893
|
-
function walk(list) {
|
|
19894
|
-
for (const w of list) {
|
|
19895
|
-
if (w.type !== "contribution") {
|
|
19896
|
-
total++;
|
|
19897
|
-
const s = w.status.toLowerCase();
|
|
19898
|
-
if (s === "done" || s === "closed" || s === "resolved" || s === "decided") done++;
|
|
19899
|
-
else if (s === "in-progress" || s === "in progress") inProgress++;
|
|
19900
|
-
}
|
|
19901
|
-
if (w.children) walk(w.children);
|
|
19902
|
-
}
|
|
19903
|
-
}
|
|
19904
|
-
walk(items);
|
|
19905
|
-
return { total, done, inProgress };
|
|
19906
|
-
}
|
|
19907
|
-
function renderItemRows(items, borderColor, depth = 0) {
|
|
19908
|
-
return items.flatMap((w) => {
|
|
19909
|
-
const isChild = depth > 0;
|
|
19910
|
-
const isContribution = w.type === "contribution";
|
|
19911
|
-
const classes = ["focus-row"];
|
|
19912
|
-
if (isContribution) classes.push("contribution-row");
|
|
19913
|
-
else if (isChild) classes.push("child-row");
|
|
19914
|
-
const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
|
|
19915
|
-
const progressCell = !isContribution && w.progress !== void 0 ? `<div class="mini-progress-bar"><div class="mini-progress-fill" style="width:${w.progress}%"></div><span class="mini-progress-label">${w.progress}%</span></div>` : "";
|
|
19916
|
-
const row = `
|
|
19917
|
-
<tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
|
|
19918
|
-
<td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
19919
|
-
<td>${escapeHtml(w.title)}</td>
|
|
19920
|
-
<td>${statusBadge(w.status)}</td>
|
|
19921
|
-
<td>${progressCell}</td>
|
|
19922
|
-
</tr>`;
|
|
19923
|
-
const childRows = w.children ? renderItemRows(w.children, borderColor, depth + 1) : [];
|
|
19924
|
-
return [row, ...childRows];
|
|
19925
|
-
});
|
|
19926
|
-
}
|
|
19927
|
-
const allWorkItemRows = [];
|
|
19928
|
-
for (const [focus, items] of focusGroups) {
|
|
19929
|
-
const color = focusColorMap.get(focus);
|
|
19930
|
-
const stats = countFocusStats(items);
|
|
19931
|
-
const pct = stats.total > 0 ? Math.round(stats.done / stats.total * 100) : 0;
|
|
19932
|
-
const summaryParts = [];
|
|
19933
|
-
if (stats.done > 0) summaryParts.push(`${stats.done} done`);
|
|
19934
|
-
if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
|
|
19935
|
-
const remaining = stats.total - stats.done - stats.inProgress;
|
|
19936
|
-
if (remaining > 0) summaryParts.push(`${remaining} open`);
|
|
19937
|
-
allWorkItemRows.push(`
|
|
19938
|
-
<tr class="focus-group-header" style="--focus-color: ${color}">
|
|
19939
|
-
<td colspan="2">
|
|
19940
|
-
<span class="focus-group-name">${escapeHtml(focus)}</span>
|
|
19941
|
-
<span class="focus-group-stats">${summaryParts.join(" / ")}</span>
|
|
19942
|
-
</td>
|
|
19943
|
-
<td colspan="2">
|
|
19944
|
-
<div class="mini-progress-bar focus-group-progress"><div class="mini-progress-fill" style="width:${pct}%"></div><span class="mini-progress-label">${pct}%</span></div>
|
|
19945
|
-
</td>
|
|
19946
|
-
</tr>`);
|
|
19947
|
-
allWorkItemRows.push(...renderItemRows(items, color));
|
|
19948
|
-
}
|
|
19949
|
-
const tableHeaders = `<tr>
|
|
19950
|
-
<th>ID</th>
|
|
19951
|
-
<th>Title</th>
|
|
19952
|
-
<th>Status</th>
|
|
19953
|
-
<th>Progress</th>
|
|
19954
|
-
</tr>`;
|
|
19955
|
-
const workItemsSection = allWorkItemRows.length > 0 ? collapsibleSection(
|
|
19956
|
-
"ss-work-items",
|
|
19957
|
-
"Work Items",
|
|
19958
|
-
`<div class="table-wrap">
|
|
19959
|
-
<table id="work-items-table">
|
|
19960
|
-
<thead>
|
|
19961
|
-
${tableHeaders}
|
|
19962
|
-
</thead>
|
|
19963
|
-
<tbody>
|
|
19964
|
-
${allWorkItemRows.join("")}
|
|
19965
|
-
</tbody>
|
|
19966
|
-
</table>
|
|
19967
|
-
</div>`,
|
|
19968
|
-
{ titleTag: "h3", defaultCollapsed: true }
|
|
19969
|
-
) : "";
|
|
19970
|
-
const activitySection = data.artifacts.length > 0 ? collapsibleSection(
|
|
19971
|
-
"ss-activity",
|
|
19972
|
-
"Recent Activity",
|
|
20312
|
+
const actionsSection = data.openActions.length > 0 ? collapsibleSection(
|
|
20313
|
+
"dm-sprint-actions",
|
|
20314
|
+
`Open Actions (${data.openActions.length})`,
|
|
19973
20315
|
`<div class="table-wrap">
|
|
19974
20316
|
<table>
|
|
19975
20317
|
<thead>
|
|
19976
|
-
<tr><th>
|
|
20318
|
+
<tr><th>ID</th><th>Title</th><th>Owner</th><th>Due Date</th></tr>
|
|
19977
20319
|
</thead>
|
|
19978
20320
|
<tbody>
|
|
19979
|
-
${data.
|
|
20321
|
+
${data.openActions.map((a) => `
|
|
19980
20322
|
<tr>
|
|
19981
|
-
<td>${
|
|
19982
|
-
<td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
20323
|
+
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
19983
20324
|
<td>${escapeHtml(a.title)}</td>
|
|
19984
|
-
<td>${escapeHtml(
|
|
19985
|
-
<td>${
|
|
20325
|
+
<td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
20326
|
+
<td>${a.dueDate ? formatDate(a.dueDate) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19986
20327
|
</tr>`).join("")}
|
|
19987
20328
|
</tbody>
|
|
19988
20329
|
</table>
|
|
19989
20330
|
</div>`,
|
|
19990
|
-
{ titleTag: "h3"
|
|
20331
|
+
{ titleTag: "h3" }
|
|
19991
20332
|
) : "";
|
|
19992
20333
|
const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
|
|
19993
|
-
"
|
|
20334
|
+
"dm-sprint-meetings",
|
|
19994
20335
|
`Meetings (${data.meetings.length})`,
|
|
19995
20336
|
`<div class="table-wrap">
|
|
19996
20337
|
<table>
|
|
@@ -20009,79 +20350,23 @@ function sprintSummaryPage(data, cached2) {
|
|
|
20009
20350
|
</div>`,
|
|
20010
20351
|
{ titleTag: "h3", defaultCollapsed: true }
|
|
20011
20352
|
) : "";
|
|
20012
|
-
const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
|
|
20013
|
-
const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
|
|
20014
20353
|
return `
|
|
20015
20354
|
<div class="page-header">
|
|
20016
20355
|
<h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
|
|
20017
|
-
<div class="subtitle">Sprint
|
|
20356
|
+
<div class="subtitle">Sprint Execution ${dateRange}</div>
|
|
20018
20357
|
</div>
|
|
20019
20358
|
${goalHtml}
|
|
20020
20359
|
${progressBar3(data.timeline.percentComplete)}
|
|
20021
20360
|
${statsCards}
|
|
20022
|
-
${epicsTable}
|
|
20023
20361
|
${workItemsSection}
|
|
20024
|
-
${
|
|
20362
|
+
${epicsSection}
|
|
20363
|
+
${actionsSection}
|
|
20025
20364
|
${meetingsSection}
|
|
20026
|
-
|
|
20027
|
-
<div class="sprint-ai-section">
|
|
20028
|
-
<h3>AI Summary</h3>
|
|
20029
|
-
${cached2 ? `<p class="text-dim">Generated ${formatDate(cached2.generatedAt)} at ${cached2.generatedAt.slice(11, 16)} UTC</p>` : `<p class="text-dim">Generate a narrative summary of this sprint's progress, risks, and projections.</p>`}
|
|
20030
|
-
<button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
|
|
20031
|
-
<div id="summary-loading" class="sprint-loading" style="display:none">
|
|
20032
|
-
<div class="sprint-spinner"></div>
|
|
20033
|
-
<span>Generating summary...</span>
|
|
20034
|
-
</div>
|
|
20035
|
-
<div id="summary-error" class="sprint-error" style="display:none"></div>
|
|
20036
|
-
<div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
|
|
20037
|
-
</div>
|
|
20038
|
-
|
|
20039
|
-
<script>
|
|
20040
|
-
async function generateSummary() {
|
|
20041
|
-
var btn = document.getElementById('generate-btn');
|
|
20042
|
-
var loading = document.getElementById('summary-loading');
|
|
20043
|
-
var errorEl = document.getElementById('summary-error');
|
|
20044
|
-
var content = document.getElementById('summary-content');
|
|
20045
|
-
|
|
20046
|
-
btn.disabled = true;
|
|
20047
|
-
btn.style.display = 'none';
|
|
20048
|
-
loading.style.display = 'flex';
|
|
20049
|
-
errorEl.style.display = 'none';
|
|
20050
|
-
content.style.display = 'none';
|
|
20051
|
-
|
|
20052
|
-
try {
|
|
20053
|
-
var res = await fetch('/api/sprint-summary', {
|
|
20054
|
-
method: 'POST',
|
|
20055
|
-
headers: { 'Content-Type': 'application/json' },
|
|
20056
|
-
body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
|
|
20057
|
-
});
|
|
20058
|
-
var json = await res.json();
|
|
20059
|
-
if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
|
|
20060
|
-
loading.style.display = 'none';
|
|
20061
|
-
content.innerHTML = json.html;
|
|
20062
|
-
content.style.display = 'block';
|
|
20063
|
-
btn.textContent = 'Regenerate';
|
|
20064
|
-
btn.style.display = '';
|
|
20065
|
-
btn.disabled = false;
|
|
20066
|
-
} catch (e) {
|
|
20067
|
-
loading.style.display = 'none';
|
|
20068
|
-
errorEl.textContent = e.message;
|
|
20069
|
-
errorEl.style.display = 'block';
|
|
20070
|
-
btn.style.display = '';
|
|
20071
|
-
btn.disabled = false;
|
|
20072
|
-
}
|
|
20073
|
-
}
|
|
20074
|
-
</script>`;
|
|
20075
|
-
}
|
|
20076
|
-
|
|
20077
|
-
// src/web/templates/pages/dm/sprint.ts
|
|
20078
|
-
function dmSprintPage(ctx) {
|
|
20079
|
-
const data = getSprintSummaryData(ctx.store);
|
|
20080
|
-
return sprintSummaryPage(data);
|
|
20365
|
+
`;
|
|
20081
20366
|
}
|
|
20082
20367
|
|
|
20083
20368
|
// src/web/templates/pages/dm/actions.ts
|
|
20084
|
-
var
|
|
20369
|
+
var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
20085
20370
|
function urgencyBadge(tier) {
|
|
20086
20371
|
const labels = {
|
|
20087
20372
|
overdue: "Overdue",
|
|
@@ -20101,7 +20386,7 @@ function urgencyRowClass(tier) {
|
|
|
20101
20386
|
function dmActionsPage(ctx) {
|
|
20102
20387
|
const upcoming = getUpcomingData(ctx.store);
|
|
20103
20388
|
const allActions = ctx.store.list({ type: "action" });
|
|
20104
|
-
const openActions = allActions.filter((d) => !
|
|
20389
|
+
const openActions = allActions.filter((d) => !DONE_STATUSES9.has(d.frontmatter.status));
|
|
20105
20390
|
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
20106
20391
|
const dueThisWeek = upcoming.dueSoonActions.filter((a) => a.urgency === "due-3d" || a.urgency === "due-7d");
|
|
20107
20392
|
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
@@ -20186,7 +20471,7 @@ function dmActionsPage(ctx) {
|
|
|
20186
20471
|
}
|
|
20187
20472
|
|
|
20188
20473
|
// src/web/templates/pages/dm/risks.ts
|
|
20189
|
-
var
|
|
20474
|
+
var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
20190
20475
|
function dmRisksPage(ctx) {
|
|
20191
20476
|
const allDocs = ctx.store.list();
|
|
20192
20477
|
const upcoming = getUpcomingData(ctx.store);
|
|
@@ -20197,7 +20482,7 @@ function dmRisksPage(ctx) {
|
|
|
20197
20482
|
const todayMs = new Date(today).getTime();
|
|
20198
20483
|
const fourteenDaysMs = 14 * 864e5;
|
|
20199
20484
|
const agingItems = allDocs.filter((d) => {
|
|
20200
|
-
if (
|
|
20485
|
+
if (DONE_STATUSES10.has(d.frontmatter.status)) return false;
|
|
20201
20486
|
if (!["action", "question"].includes(d.frontmatter.type)) return false;
|
|
20202
20487
|
const createdMs = new Date(d.frontmatter.created).getTime();
|
|
20203
20488
|
return todayMs - createdMs > fourteenDaysMs;
|
|
@@ -20311,7 +20596,7 @@ function dmRisksPage(ctx) {
|
|
|
20311
20596
|
}
|
|
20312
20597
|
|
|
20313
20598
|
// src/web/templates/pages/dm/meetings.ts
|
|
20314
|
-
var
|
|
20599
|
+
var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
20315
20600
|
function dmMeetingsPage(ctx) {
|
|
20316
20601
|
const meetings = ctx.store.list({ type: "meeting" });
|
|
20317
20602
|
const actions = ctx.store.list({ type: "action" });
|
|
@@ -20357,7 +20642,7 @@ function dmMeetingsPage(ctx) {
|
|
|
20357
20642
|
${sortedMeetings.map((m) => {
|
|
20358
20643
|
const date5 = m.frontmatter.date ?? m.frontmatter.created;
|
|
20359
20644
|
const relatedActions = meetingActionMap.get(m.frontmatter.id) ?? [];
|
|
20360
|
-
const openCount = relatedActions.filter((a) => !
|
|
20645
|
+
const openCount = relatedActions.filter((a) => !DONE_STATUSES11.has(a.frontmatter.status)).length;
|
|
20361
20646
|
return `
|
|
20362
20647
|
<tr>
|
|
20363
20648
|
<td>${formatDate(date5)}</td>
|
|
@@ -20372,7 +20657,7 @@ function dmMeetingsPage(ctx) {
|
|
|
20372
20657
|
const recentMeetingActions = [];
|
|
20373
20658
|
for (const [mid, acts] of meetingActionMap) {
|
|
20374
20659
|
for (const act of acts) {
|
|
20375
|
-
if (!
|
|
20660
|
+
if (!DONE_STATUSES11.has(act.frontmatter.status)) {
|
|
20376
20661
|
recentMeetingActions.push({ action: act, meetingId: mid });
|
|
20377
20662
|
}
|
|
20378
20663
|
}
|
|
@@ -20567,16 +20852,16 @@ registerPersonaPage("dm", "meetings", dmMeetingsPage);
|
|
|
20567
20852
|
registerPersonaPage("dm", "governance", dmGovernancePage);
|
|
20568
20853
|
|
|
20569
20854
|
// src/web/templates/pages/tl/dashboard.ts
|
|
20570
|
-
var
|
|
20571
|
-
var
|
|
20855
|
+
var DONE_STATUSES12 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
20856
|
+
var RESOLVED_DECISION_STATUSES2 = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
20572
20857
|
function tlDashboardPage(ctx) {
|
|
20573
20858
|
const epics = ctx.store.list({ type: "epic" });
|
|
20574
20859
|
const tasks = ctx.store.list({ type: "task" });
|
|
20575
20860
|
const decisions = ctx.store.list({ type: "decision" });
|
|
20576
20861
|
const questions = ctx.store.list({ type: "question" });
|
|
20577
20862
|
const diagrams = getDiagramData(ctx.store);
|
|
20578
|
-
const openEpics = epics.filter((d) => !
|
|
20579
|
-
const openTasks = tasks.filter((d) => !
|
|
20863
|
+
const openEpics = epics.filter((d) => !DONE_STATUSES12.has(d.frontmatter.status));
|
|
20864
|
+
const openTasks = tasks.filter((d) => !DONE_STATUSES12.has(d.frontmatter.status));
|
|
20580
20865
|
const technicalDecisions = decisions.filter((d) => {
|
|
20581
20866
|
const tags = d.frontmatter.tags ?? [];
|
|
20582
20867
|
return tags.some((t) => {
|
|
@@ -20585,7 +20870,7 @@ function tlDashboardPage(ctx) {
|
|
|
20585
20870
|
});
|
|
20586
20871
|
});
|
|
20587
20872
|
const displayDecisions = technicalDecisions.length > 0 ? technicalDecisions : decisions;
|
|
20588
|
-
const pendingDecisions = displayDecisions.filter((d) => !
|
|
20873
|
+
const pendingDecisions = displayDecisions.filter((d) => !RESOLVED_DECISION_STATUSES2.has(d.frontmatter.status));
|
|
20589
20874
|
const statsCards = `
|
|
20590
20875
|
<div class="cards">
|
|
20591
20876
|
<div class="card">
|
|
@@ -20634,7 +20919,7 @@ function tlDashboardPage(ctx) {
|
|
|
20634
20919
|
}
|
|
20635
20920
|
|
|
20636
20921
|
// src/web/templates/pages/tl/backlog.ts
|
|
20637
|
-
var
|
|
20922
|
+
var DONE_STATUSES13 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
20638
20923
|
function tlBacklogPage(ctx) {
|
|
20639
20924
|
const epics = ctx.store.list({ type: "epic" });
|
|
20640
20925
|
const tasks = ctx.store.list({ type: "task" });
|
|
@@ -20656,10 +20941,10 @@ function tlBacklogPage(ctx) {
|
|
|
20656
20941
|
const featureIds = Array.isArray(linked) ? linked.map(String) : linked ? [String(linked)] : [];
|
|
20657
20942
|
epicFeatureMap.set(epic.frontmatter.id, featureIds);
|
|
20658
20943
|
}
|
|
20659
|
-
const
|
|
20944
|
+
const statusOrder2 = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
20660
20945
|
const sortedEpics = [...epics].sort((a, b) => {
|
|
20661
|
-
const sa =
|
|
20662
|
-
const sb =
|
|
20946
|
+
const sa = statusOrder2[a.frontmatter.status] ?? 3;
|
|
20947
|
+
const sb = statusOrder2[b.frontmatter.status] ?? 3;
|
|
20663
20948
|
if (sa !== sb) return sa - sb;
|
|
20664
20949
|
return a.frontmatter.id.localeCompare(b.frontmatter.id);
|
|
20665
20950
|
});
|
|
@@ -20671,7 +20956,7 @@ function tlBacklogPage(ctx) {
|
|
|
20671
20956
|
<tbody>
|
|
20672
20957
|
${sortedEpics.map((e) => {
|
|
20673
20958
|
const eTasks = epicToTasks.get(e.frontmatter.id) ?? [];
|
|
20674
|
-
const done = eTasks.filter((t) =>
|
|
20959
|
+
const done = eTasks.filter((t) => DONE_STATUSES13.has(t.frontmatter.status)).length;
|
|
20675
20960
|
const featureIds = epicFeatureMap.get(e.frontmatter.id) ?? [];
|
|
20676
20961
|
const featureLinks = featureIds.map((fid) => `<a href="/docs/feature/${escapeHtml(fid)}">${escapeHtml(fid)}</a>`).join(", ");
|
|
20677
20962
|
return `
|
|
@@ -20691,7 +20976,7 @@ function tlBacklogPage(ctx) {
|
|
|
20691
20976
|
for (const t of taskList) assignedTaskIds.add(t.frontmatter.id);
|
|
20692
20977
|
}
|
|
20693
20978
|
const unassignedTasks = tasks.filter(
|
|
20694
|
-
(t) => !assignedTaskIds.has(t.frontmatter.id) && !
|
|
20979
|
+
(t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES13.has(t.frontmatter.status)
|
|
20695
20980
|
);
|
|
20696
20981
|
const unassignedSection = unassignedTasks.length > 0 ? collapsibleSection(
|
|
20697
20982
|
"tl-backlog-unassigned",
|
|
@@ -20752,7 +21037,6 @@ var TL_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
|
|
|
20752
21037
|
"technical-assessment",
|
|
20753
21038
|
"architecture-review"
|
|
20754
21039
|
]);
|
|
20755
|
-
var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
20756
21040
|
function progressBar4(pct) {
|
|
20757
21041
|
return `<div class="sprint-progress-bar">
|
|
20758
21042
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
@@ -20772,25 +21056,8 @@ function tlSprintPage(ctx) {
|
|
|
20772
21056
|
<p>No active sprint found. Create a sprint and set its status to "active" to track sprint work.</p>
|
|
20773
21057
|
</div>`;
|
|
20774
21058
|
}
|
|
20775
|
-
const
|
|
20776
|
-
const
|
|
20777
|
-
for (const item of data.workItems.items) {
|
|
20778
|
-
if (techTypes.has(item.type)) {
|
|
20779
|
-
techItems.push(item);
|
|
20780
|
-
} else if (item.children) {
|
|
20781
|
-
const promoteChildren = (children) => {
|
|
20782
|
-
for (const child of children) {
|
|
20783
|
-
if (techTypes.has(child.type)) {
|
|
20784
|
-
techItems.push(child);
|
|
20785
|
-
} else if (child.children) {
|
|
20786
|
-
promoteChildren(child.children);
|
|
20787
|
-
}
|
|
20788
|
-
}
|
|
20789
|
-
};
|
|
20790
|
-
promoteChildren(item.children);
|
|
20791
|
-
}
|
|
20792
|
-
}
|
|
20793
|
-
const techDone = techItems.filter((w) => DONE_STATUSES11.has(w.status)).length;
|
|
21059
|
+
const tlItems = filterItemsByOwner(data.workItems.items, "tl");
|
|
21060
|
+
const tlCompletionPct = computeOwnerCompletionPct(data.workItems.items, "tl");
|
|
20794
21061
|
const allDocs = ctx.store.list();
|
|
20795
21062
|
const tlContributions = allDocs.filter((d) => TL_CONTRIBUTION_TYPES.has(d.frontmatter.type));
|
|
20796
21063
|
const statsCards = `
|
|
@@ -20801,9 +21068,9 @@ function tlSprintPage(ctx) {
|
|
|
20801
21068
|
<div class="card-sub">${data.timeline.daysRemaining} days remaining</div>
|
|
20802
21069
|
</div>
|
|
20803
21070
|
<div class="card">
|
|
20804
|
-
<div class="card-label">
|
|
20805
|
-
<div class="card-value">${
|
|
20806
|
-
<div class="card-sub">${
|
|
21071
|
+
<div class="card-label">TL Completion</div>
|
|
21072
|
+
<div class="card-value">${tlCompletionPct}%</div>
|
|
21073
|
+
<div class="card-sub">${tlItems.length} owned items</div>
|
|
20807
21074
|
</div>
|
|
20808
21075
|
<div class="card">
|
|
20809
21076
|
<div class="card-label">Epics</div>
|
|
@@ -20821,28 +21088,10 @@ function tlSprintPage(ctx) {
|
|
|
20821
21088
|
<strong>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</strong>
|
|
20822
21089
|
${data.sprint.goal ? ` | ${escapeHtml(data.sprint.goal)}` : ""}
|
|
20823
21090
|
</div>`;
|
|
20824
|
-
const workItemsSection =
|
|
20825
|
-
"tl-sprint-items",
|
|
20826
|
-
|
|
20827
|
-
|
|
20828
|
-
<table>
|
|
20829
|
-
<thead>
|
|
20830
|
-
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Focus</th></tr>
|
|
20831
|
-
</thead>
|
|
20832
|
-
<tbody>
|
|
20833
|
-
${techItems.map((w) => `
|
|
20834
|
-
<tr>
|
|
20835
|
-
<td><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
20836
|
-
<td>${escapeHtml(w.title)}</td>
|
|
20837
|
-
<td>${escapeHtml(typeLabel(w.type))}</td>
|
|
20838
|
-
<td>${statusBadge(w.status)}</td>
|
|
20839
|
-
<td>${w.workFocus ? `<span class="badge badge-subtle">${escapeHtml(w.workFocus)}</span>` : '<span class="text-dim">\u2014</span>'}</td>
|
|
20840
|
-
</tr>`).join("")}
|
|
20841
|
-
</tbody>
|
|
20842
|
-
</table>
|
|
20843
|
-
</div>`,
|
|
20844
|
-
{ titleTag: "h3" }
|
|
20845
|
-
) : "";
|
|
21091
|
+
const workItemsSection = renderWorkItemsTable(tlItems, {
|
|
21092
|
+
sectionId: "tl-sprint-items",
|
|
21093
|
+
title: "TL Work Items"
|
|
21094
|
+
});
|
|
20846
21095
|
const contributionsSection = tlContributions.length > 0 ? collapsibleSection(
|
|
20847
21096
|
"tl-sprint-contributions",
|
|
20848
21097
|
`TL Contributions (${tlContributions.length})`,
|
|
@@ -21226,6 +21475,186 @@ function upcomingPage(data) {
|
|
|
21226
21475
|
`;
|
|
21227
21476
|
}
|
|
21228
21477
|
|
|
21478
|
+
// src/web/templates/pages/sprint-summary.ts
|
|
21479
|
+
function progressBar5(pct) {
|
|
21480
|
+
return `<div class="sprint-progress-bar">
|
|
21481
|
+
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
21482
|
+
<span class="sprint-progress-label">${pct}%</span>
|
|
21483
|
+
</div>`;
|
|
21484
|
+
}
|
|
21485
|
+
function sprintSummaryPage(data, cached2) {
|
|
21486
|
+
if (!data) {
|
|
21487
|
+
return `
|
|
21488
|
+
<div class="page-header">
|
|
21489
|
+
<h2>Sprint Summary</h2>
|
|
21490
|
+
<div class="subtitle">AI-powered sprint narrative</div>
|
|
21491
|
+
</div>
|
|
21492
|
+
<div class="empty">
|
|
21493
|
+
<h3>No Active Sprint</h3>
|
|
21494
|
+
<p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
|
|
21495
|
+
</div>`;
|
|
21496
|
+
}
|
|
21497
|
+
const statsCards = `
|
|
21498
|
+
<div class="cards">
|
|
21499
|
+
<div class="card">
|
|
21500
|
+
<div class="card-label">Completion</div>
|
|
21501
|
+
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
21502
|
+
<div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
|
|
21503
|
+
</div>
|
|
21504
|
+
<div class="card">
|
|
21505
|
+
<div class="card-label">Days Remaining</div>
|
|
21506
|
+
<div class="card-value">${data.timeline.daysRemaining}</div>
|
|
21507
|
+
<div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
|
|
21508
|
+
</div>
|
|
21509
|
+
<div class="card">
|
|
21510
|
+
<div class="card-label">Epics</div>
|
|
21511
|
+
<div class="card-value">${data.linkedEpics.length}</div>
|
|
21512
|
+
<div class="card-sub">linked to sprint</div>
|
|
21513
|
+
</div>
|
|
21514
|
+
<a class="card card-link" href="sprint-blockers">
|
|
21515
|
+
<div class="card-label">Blockers</div>
|
|
21516
|
+
<div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
|
|
21517
|
+
<div class="card-sub">${data.workItems.blocked} blocked items</div>
|
|
21518
|
+
</a>
|
|
21519
|
+
<a class="card card-link" href="sprint-risks">
|
|
21520
|
+
<div class="card-label">Risks</div>
|
|
21521
|
+
<div class="card-value${data.risks.length > 0 ? " priority-medium" : ""}">${data.risks.length}</div>
|
|
21522
|
+
<div class="card-sub">open risk items</div>
|
|
21523
|
+
</a>
|
|
21524
|
+
</div>`;
|
|
21525
|
+
const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
21526
|
+
"ss-epics",
|
|
21527
|
+
"Linked Epics",
|
|
21528
|
+
`<div class="table-wrap">
|
|
21529
|
+
<table>
|
|
21530
|
+
<thead>
|
|
21531
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
|
|
21532
|
+
</thead>
|
|
21533
|
+
<tbody>
|
|
21534
|
+
${data.linkedEpics.map((e) => `
|
|
21535
|
+
<tr>
|
|
21536
|
+
<td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
|
|
21537
|
+
<td>${escapeHtml(e.title)}</td>
|
|
21538
|
+
<td>${statusBadge(e.status)}</td>
|
|
21539
|
+
<td>${e.tasksDone} / ${e.tasksTotal}</td>
|
|
21540
|
+
</tr>`).join("")}
|
|
21541
|
+
</tbody>
|
|
21542
|
+
</table>
|
|
21543
|
+
</div>`,
|
|
21544
|
+
{ titleTag: "h3" }
|
|
21545
|
+
) : "";
|
|
21546
|
+
const workItemsSection = renderWorkItemsTable(data.workItems.items, {
|
|
21547
|
+
sectionId: "ss-work-items",
|
|
21548
|
+
title: "Work Items",
|
|
21549
|
+
defaultCollapsed: true
|
|
21550
|
+
});
|
|
21551
|
+
const activitySection = data.artifacts.length > 0 ? collapsibleSection(
|
|
21552
|
+
"ss-activity",
|
|
21553
|
+
"Recent Activity",
|
|
21554
|
+
`<div class="table-wrap">
|
|
21555
|
+
<table>
|
|
21556
|
+
<thead>
|
|
21557
|
+
<tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
|
|
21558
|
+
</thead>
|
|
21559
|
+
<tbody>
|
|
21560
|
+
${data.artifacts.slice(0, 15).map((a) => `
|
|
21561
|
+
<tr>
|
|
21562
|
+
<td>${formatDate(a.date)}</td>
|
|
21563
|
+
<td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
21564
|
+
<td>${escapeHtml(a.title)}</td>
|
|
21565
|
+
<td>${escapeHtml(typeLabel(a.type))}</td>
|
|
21566
|
+
<td>${escapeHtml(a.action)}</td>
|
|
21567
|
+
</tr>`).join("")}
|
|
21568
|
+
</tbody>
|
|
21569
|
+
</table>
|
|
21570
|
+
</div>`,
|
|
21571
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
21572
|
+
) : "";
|
|
21573
|
+
const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
|
|
21574
|
+
"ss-meetings",
|
|
21575
|
+
`Meetings (${data.meetings.length})`,
|
|
21576
|
+
`<div class="table-wrap">
|
|
21577
|
+
<table>
|
|
21578
|
+
<thead>
|
|
21579
|
+
<tr><th>Date</th><th>ID</th><th>Title</th></tr>
|
|
21580
|
+
</thead>
|
|
21581
|
+
<tbody>
|
|
21582
|
+
${data.meetings.map((m) => `
|
|
21583
|
+
<tr>
|
|
21584
|
+
<td>${formatDate(m.date)}</td>
|
|
21585
|
+
<td><a href="/docs/meeting/${escapeHtml(m.id)}">${escapeHtml(m.id)}</a></td>
|
|
21586
|
+
<td>${escapeHtml(m.title)}</td>
|
|
21587
|
+
</tr>`).join("")}
|
|
21588
|
+
</tbody>
|
|
21589
|
+
</table>
|
|
21590
|
+
</div>`,
|
|
21591
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
21592
|
+
) : "";
|
|
21593
|
+
const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
|
|
21594
|
+
const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
|
|
21595
|
+
return `
|
|
21596
|
+
<div class="page-header">
|
|
21597
|
+
<h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
|
|
21598
|
+
<div class="subtitle">Sprint Summary ${dateRange}</div>
|
|
21599
|
+
</div>
|
|
21600
|
+
${goalHtml}
|
|
21601
|
+
${progressBar5(data.timeline.percentComplete)}
|
|
21602
|
+
${statsCards}
|
|
21603
|
+
${epicsTable}
|
|
21604
|
+
${workItemsSection}
|
|
21605
|
+
${activitySection}
|
|
21606
|
+
${meetingsSection}
|
|
21607
|
+
|
|
21608
|
+
<div class="sprint-ai-section">
|
|
21609
|
+
<h3>AI Summary</h3>
|
|
21610
|
+
${cached2 ? `<p class="text-dim">Generated ${formatDate(cached2.generatedAt)} at ${cached2.generatedAt.slice(11, 16)} UTC</p>` : `<p class="text-dim">Generate a narrative summary of this sprint's progress, risks, and projections.</p>`}
|
|
21611
|
+
<button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
|
|
21612
|
+
<div id="summary-loading" class="sprint-loading" style="display:none">
|
|
21613
|
+
<div class="sprint-spinner"></div>
|
|
21614
|
+
<span>Generating summary...</span>
|
|
21615
|
+
</div>
|
|
21616
|
+
<div id="summary-error" class="sprint-error" style="display:none"></div>
|
|
21617
|
+
<div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
|
|
21618
|
+
</div>
|
|
21619
|
+
|
|
21620
|
+
<script>
|
|
21621
|
+
async function generateSummary() {
|
|
21622
|
+
var btn = document.getElementById('generate-btn');
|
|
21623
|
+
var loading = document.getElementById('summary-loading');
|
|
21624
|
+
var errorEl = document.getElementById('summary-error');
|
|
21625
|
+
var content = document.getElementById('summary-content');
|
|
21626
|
+
|
|
21627
|
+
btn.disabled = true;
|
|
21628
|
+
btn.style.display = 'none';
|
|
21629
|
+
loading.style.display = 'flex';
|
|
21630
|
+
errorEl.style.display = 'none';
|
|
21631
|
+
content.style.display = 'none';
|
|
21632
|
+
|
|
21633
|
+
try {
|
|
21634
|
+
var res = await fetch('/api/sprint-summary', {
|
|
21635
|
+
method: 'POST',
|
|
21636
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21637
|
+
body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
|
|
21638
|
+
});
|
|
21639
|
+
var json = await res.json();
|
|
21640
|
+
if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
|
|
21641
|
+
loading.style.display = 'none';
|
|
21642
|
+
content.innerHTML = json.html;
|
|
21643
|
+
content.style.display = 'block';
|
|
21644
|
+
btn.textContent = 'Regenerate';
|
|
21645
|
+
btn.style.display = '';
|
|
21646
|
+
btn.disabled = false;
|
|
21647
|
+
} catch (e) {
|
|
21648
|
+
loading.style.display = 'none';
|
|
21649
|
+
errorEl.textContent = e.message;
|
|
21650
|
+
errorEl.style.display = 'block';
|
|
21651
|
+
btn.style.display = '';
|
|
21652
|
+
btn.disabled = false;
|
|
21653
|
+
}
|
|
21654
|
+
}
|
|
21655
|
+
</script>`;
|
|
21656
|
+
}
|
|
21657
|
+
|
|
21229
21658
|
// src/web/templates/pages/sprint-blockers.ts
|
|
21230
21659
|
function sprintBlockersPage(data, store) {
|
|
21231
21660
|
if (!data) {
|
|
@@ -25151,8 +25580,8 @@ function generateTaskMasterPrd(title, ctx, projectOverview) {
|
|
|
25151
25580
|
let priorityIdx = 1;
|
|
25152
25581
|
for (const feature of ctx.features) {
|
|
25153
25582
|
const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id)).sort((a, b) => {
|
|
25154
|
-
const
|
|
25155
|
-
return (
|
|
25583
|
+
const statusOrder2 = { "in-progress": 0, planned: 1, done: 2 };
|
|
25584
|
+
return (statusOrder2[a.status] ?? 99) - (statusOrder2[b.status] ?? 99);
|
|
25156
25585
|
});
|
|
25157
25586
|
if (featureEpics.length === 0) continue;
|
|
25158
25587
|
lines.push(`${priorityIdx}. **${feature.title}** (${feature.priority})`);
|
|
@@ -30074,7 +30503,7 @@ function createProgram() {
|
|
|
30074
30503
|
const program = new Command();
|
|
30075
30504
|
program.name("marvin").description(
|
|
30076
30505
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
30077
|
-
).version("0.5.
|
|
30506
|
+
).version("0.5.7");
|
|
30078
30507
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
30079
30508
|
await initCommand();
|
|
30080
30509
|
});
|