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/marvin-serve.js
CHANGED
|
@@ -15707,6 +15707,7 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15707
15707
|
type: doc.frontmatter.type,
|
|
15708
15708
|
status: doc.frontmatter.status,
|
|
15709
15709
|
progress: getEffectiveProgress(doc.frontmatter),
|
|
15710
|
+
owner: doc.frontmatter.owner,
|
|
15710
15711
|
workFocus: focusTag ? focusTag.slice(6) : void 0,
|
|
15711
15712
|
aboutArtifact: about
|
|
15712
15713
|
};
|
|
@@ -15913,10 +15914,10 @@ function getBoardData(store, type) {
|
|
|
15913
15914
|
if (!byStatus.has(status)) byStatus.set(status, []);
|
|
15914
15915
|
byStatus.get(status).push(doc);
|
|
15915
15916
|
}
|
|
15916
|
-
const
|
|
15917
|
+
const statusOrder2 = ["open", "draft", "in-progress", "blocked"];
|
|
15917
15918
|
const allStatuses = [...byStatus.keys()];
|
|
15918
15919
|
const ordered = [];
|
|
15919
|
-
for (const s of
|
|
15920
|
+
for (const s of statusOrder2) {
|
|
15920
15921
|
if (allStatuses.includes(s)) ordered.push(s);
|
|
15921
15922
|
}
|
|
15922
15923
|
for (const s of allStatuses.sort()) {
|
|
@@ -19707,8 +19708,8 @@ function generateTaskMasterPrd(title, ctx, projectOverview) {
|
|
|
19707
19708
|
let priorityIdx = 1;
|
|
19708
19709
|
for (const feature of ctx.features) {
|
|
19709
19710
|
const featureEpics = ctx.epics.filter((e) => e.linkedFeature.includes(feature.id)).sort((a, b) => {
|
|
19710
|
-
const
|
|
19711
|
-
return (
|
|
19711
|
+
const statusOrder2 = { "in-progress": 0, planned: 1, done: 2 };
|
|
19712
|
+
return (statusOrder2[a.status] ?? 99) - (statusOrder2[b.status] ?? 99);
|
|
19712
19713
|
});
|
|
19713
19714
|
if (featureEpics.length === 0) continue;
|
|
19714
19715
|
lines.push(`${priorityIdx}. **${feature.title}** (${feature.priority})`);
|
|
@@ -21991,6 +21992,31 @@ tr:hover td {
|
|
|
21991
21992
|
.focus-group-progress {
|
|
21992
21993
|
width: 96px;
|
|
21993
21994
|
}
|
|
21995
|
+
|
|
21996
|
+
/* Owner badges for DM sprint view */
|
|
21997
|
+
.owner-badge {
|
|
21998
|
+
display: inline-block;
|
|
21999
|
+
padding: 0.1rem 0.5rem;
|
|
22000
|
+
border-radius: 999px;
|
|
22001
|
+
font-size: 0.65rem;
|
|
22002
|
+
font-weight: 700;
|
|
22003
|
+
text-transform: uppercase;
|
|
22004
|
+
letter-spacing: 0.04em;
|
|
22005
|
+
white-space: nowrap;
|
|
22006
|
+
}
|
|
22007
|
+
.owner-badge-po { background: rgba(108, 140, 255, 0.18); color: #6c8cff; }
|
|
22008
|
+
.owner-badge-tl { background: rgba(251, 191, 36, 0.18); color: #fbbf24; }
|
|
22009
|
+
.owner-badge-dm { background: rgba(52, 211, 153, 0.18); color: #34d399; }
|
|
22010
|
+
.owner-badge-other { background: rgba(139, 143, 164, 0.12); color: var(--text-dim); }
|
|
22011
|
+
|
|
22012
|
+
/* Group header rows (PO dashboard decisions/deps) */
|
|
22013
|
+
.group-header-row td {
|
|
22014
|
+
background: var(--bg-hover);
|
|
22015
|
+
padding-top: 0.5rem;
|
|
22016
|
+
padding-bottom: 0.5rem;
|
|
22017
|
+
border-bottom: 1px solid var(--border);
|
|
22018
|
+
font-size: 0.8rem;
|
|
22019
|
+
}
|
|
21994
22020
|
`;
|
|
21995
22021
|
}
|
|
21996
22022
|
|
|
@@ -22831,19 +22857,55 @@ function buildHealthGauge(categories) {
|
|
|
22831
22857
|
}
|
|
22832
22858
|
|
|
22833
22859
|
// src/web/templates/pages/po/dashboard.ts
|
|
22860
|
+
var DONE_STATUSES5 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
22861
|
+
var RESOLVED_DECISION_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
22834
22862
|
function poDashboardPage(ctx) {
|
|
22835
22863
|
const overview = getOverviewData(ctx.store);
|
|
22836
|
-
const upcoming = getUpcomingData(ctx.store);
|
|
22837
22864
|
const sprintData = getSprintSummaryData(ctx.store);
|
|
22838
22865
|
const diagrams = getDiagramData(ctx.store);
|
|
22839
22866
|
const features = ctx.store.list({ type: "feature" });
|
|
22840
|
-
const
|
|
22841
|
-
const
|
|
22842
|
-
const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
|
|
22843
|
-
const RESOLVED_DECISION_STATUSES2 = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
22867
|
+
const epics = ctx.store.list({ type: "epic" });
|
|
22868
|
+
const allTasks = ctx.store.list({ type: "task" });
|
|
22844
22869
|
const decisions = ctx.store.list({ type: "decision" });
|
|
22845
|
-
const decisionsOpen = decisions.filter((d) => !RESOLVED_DECISION_STATUSES2.has(d.frontmatter.status)).length;
|
|
22846
22870
|
const questions = ctx.store.list({ type: "question" });
|
|
22871
|
+
const sprints = ctx.store.list({ type: "sprint" });
|
|
22872
|
+
const featureToEpics = /* @__PURE__ */ new Map();
|
|
22873
|
+
for (const epic of epics) {
|
|
22874
|
+
const featureIds = normalizeLinkedFeatures(epic.frontmatter.linkedFeature);
|
|
22875
|
+
for (const fid of featureIds) {
|
|
22876
|
+
const arr = featureToEpics.get(fid) ?? [];
|
|
22877
|
+
arr.push(epic);
|
|
22878
|
+
featureToEpics.set(fid, arr);
|
|
22879
|
+
}
|
|
22880
|
+
}
|
|
22881
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
22882
|
+
for (const task of allTasks) {
|
|
22883
|
+
const tags = task.frontmatter.tags ?? [];
|
|
22884
|
+
for (const tag of tags) {
|
|
22885
|
+
if (tag.startsWith("epic:")) {
|
|
22886
|
+
const epicId = tag.slice(5);
|
|
22887
|
+
const arr = epicToTasks.get(epicId) ?? [];
|
|
22888
|
+
arr.push(task);
|
|
22889
|
+
epicToTasks.set(epicId, arr);
|
|
22890
|
+
}
|
|
22891
|
+
}
|
|
22892
|
+
}
|
|
22893
|
+
const activeSprint = sprints.find((s) => s.frontmatter.status === "active");
|
|
22894
|
+
let sprintTimelinePct = 0;
|
|
22895
|
+
if (activeSprint) {
|
|
22896
|
+
const startDate = activeSprint.frontmatter.startDate;
|
|
22897
|
+
const endDate = activeSprint.frontmatter.endDate;
|
|
22898
|
+
if (startDate && endDate) {
|
|
22899
|
+
const startMs = new Date(startDate).getTime();
|
|
22900
|
+
const endMs = new Date(endDate).getTime();
|
|
22901
|
+
const totalDays = Math.max(1, endMs - startMs);
|
|
22902
|
+
sprintTimelinePct = Math.min(100, Math.max(0, Math.round((Date.now() - startMs) / totalDays * 100)));
|
|
22903
|
+
}
|
|
22904
|
+
}
|
|
22905
|
+
const featuresDone = features.filter((d) => DONE_STATUSES5.has(d.frontmatter.status)).length;
|
|
22906
|
+
const featuresOpen = features.filter((d) => d.frontmatter.status === "open").length;
|
|
22907
|
+
const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
|
|
22908
|
+
const decisionsOpen = decisions.filter((d) => !RESOLVED_DECISION_STATUSES.has(d.frontmatter.status)).length;
|
|
22847
22909
|
const questionsOpen = questions.filter((d) => d.frontmatter.status === "open").length;
|
|
22848
22910
|
const statsCards = `
|
|
22849
22911
|
<div class="cards">
|
|
@@ -22870,7 +22932,7 @@ function poDashboardPage(ctx) {
|
|
|
22870
22932
|
</div>
|
|
22871
22933
|
<div class="card">
|
|
22872
22934
|
<a href="/po/delivery">
|
|
22873
|
-
<div class="card-label">Sprint</div>
|
|
22935
|
+
<div class="card-label">Current Sprint</div>
|
|
22874
22936
|
<div class="card-value">${sprintData ? `${sprintData.workItems.completionPct}%` : "\u2014"}</div>
|
|
22875
22937
|
<div class="card-sub">${sprintData ? `${sprintData.workItems.done}/${sprintData.workItems.total} items` : "No active sprint"}</div>
|
|
22876
22938
|
</a>
|
|
@@ -22906,43 +22968,72 @@ function poDashboardPage(ctx) {
|
|
|
22906
22968
|
</div>`,
|
|
22907
22969
|
{ titleTag: "h3" }
|
|
22908
22970
|
) : "";
|
|
22909
|
-
|
|
22910
|
-
|
|
22911
|
-
|
|
22912
|
-
|
|
22913
|
-
|
|
22914
|
-
|
|
22915
|
-
|
|
22916
|
-
|
|
22971
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
22972
|
+
const atRiskItems = [];
|
|
22973
|
+
for (const f of features) {
|
|
22974
|
+
if (DONE_STATUSES5.has(f.frontmatter.status)) continue;
|
|
22975
|
+
const fEpics = featureToEpics.get(f.frontmatter.id) ?? [];
|
|
22976
|
+
const reasons = [];
|
|
22977
|
+
let blocked = 0;
|
|
22978
|
+
for (const epic of fEpics) {
|
|
22979
|
+
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
22980
|
+
if (t.frontmatter.status === "blocked") blocked++;
|
|
22981
|
+
}
|
|
22982
|
+
}
|
|
22983
|
+
if (blocked > 0) reasons.push(`${blocked} blocked task${blocked > 1 ? "s" : ""}`);
|
|
22984
|
+
for (const epic of fEpics) {
|
|
22985
|
+
const td = epic.frontmatter.targetDate;
|
|
22986
|
+
if (td && td < today && !DONE_STATUSES5.has(epic.frontmatter.status)) {
|
|
22987
|
+
reasons.push(`${epic.frontmatter.id} overdue`);
|
|
22988
|
+
}
|
|
22989
|
+
}
|
|
22990
|
+
let totalTasks = 0;
|
|
22991
|
+
let progressSum = 0;
|
|
22992
|
+
for (const epic of fEpics) {
|
|
22993
|
+
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
22994
|
+
totalTasks++;
|
|
22995
|
+
progressSum += getEffectiveProgress(t.frontmatter);
|
|
22996
|
+
}
|
|
22997
|
+
}
|
|
22998
|
+
const avgProgress = totalTasks > 0 ? Math.round(progressSum / totalTasks) : 0;
|
|
22999
|
+
if (avgProgress < 30 && sprintTimelinePct > 60 && totalTasks > 0) {
|
|
23000
|
+
reasons.push("Low progress vs sprint timeline");
|
|
23001
|
+
}
|
|
23002
|
+
if (reasons.length > 0) atRiskItems.push({ feature: f, reasons });
|
|
23003
|
+
}
|
|
23004
|
+
const atRiskSection = atRiskItems.length > 0 ? collapsibleSection(
|
|
23005
|
+
"po-at-risk",
|
|
23006
|
+
`At-Risk Delivery (${atRiskItems.length})`,
|
|
22917
23007
|
`<div class="table-wrap">
|
|
22918
23008
|
<table>
|
|
22919
23009
|
<thead>
|
|
22920
|
-
<tr><th>
|
|
23010
|
+
<tr><th>Feature</th><th>Risk Reasons</th></tr>
|
|
22921
23011
|
</thead>
|
|
22922
23012
|
<tbody>
|
|
22923
|
-
${
|
|
22924
|
-
const tags = t.signals.map((s) => `<span class="${signalTagClass(s.points)}" title="${escapeHtml(s.factor)}: +${s.points}pts">${escapeHtml(s.factor)}</span>`).join("");
|
|
22925
|
-
return `
|
|
23013
|
+
${atRiskItems.map((r) => `
|
|
22926
23014
|
<tr>
|
|
22927
|
-
<td><a href="/docs/${escapeHtml(
|
|
22928
|
-
<td>${
|
|
22929
|
-
|
|
22930
|
-
</tr>`;
|
|
22931
|
-
}).join("")}
|
|
23015
|
+
<td><a href="/docs/feature/${escapeHtml(r.feature.frontmatter.id)}">${escapeHtml(r.feature.frontmatter.id)}</a> ${escapeHtml(r.feature.frontmatter.title)}</td>
|
|
23016
|
+
<td>${r.reasons.map((reason) => `<span class="signal-tag signal-tag-high">${escapeHtml(reason)}</span>`).join(" ")}</td>
|
|
23017
|
+
</tr>`).join("")}
|
|
22932
23018
|
</tbody>
|
|
22933
23019
|
</table>
|
|
22934
23020
|
</div>`,
|
|
22935
23021
|
{ titleTag: "h3" }
|
|
22936
|
-
) :
|
|
23022
|
+
) : collapsibleSection(
|
|
23023
|
+
"po-at-risk",
|
|
23024
|
+
"At-Risk Delivery",
|
|
23025
|
+
'<div class="empty"><p style="color: var(--green);">No at-risk items \u2014 all features on track.</p></div>',
|
|
23026
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
23027
|
+
);
|
|
22937
23028
|
return `
|
|
22938
23029
|
<div class="page-header">
|
|
22939
23030
|
<h2>Product Owner Dashboard</h2>
|
|
22940
23031
|
<div class="subtitle">Feature delivery, decisions, and stakeholder alignment</div>
|
|
22941
23032
|
</div>
|
|
22942
23033
|
${statsCards}
|
|
23034
|
+
${atRiskSection}
|
|
22943
23035
|
${diagramSection}
|
|
22944
23036
|
${recentTable}
|
|
22945
|
-
${trendingSection}
|
|
22946
23037
|
`;
|
|
22947
23038
|
}
|
|
22948
23039
|
|
|
@@ -23121,44 +23212,74 @@ function tableDateFilter(tableId, colIndex) {
|
|
|
23121
23212
|
}
|
|
23122
23213
|
|
|
23123
23214
|
// src/web/templates/pages/po/backlog.ts
|
|
23215
|
+
function priorityClass(p) {
|
|
23216
|
+
if (!p) return "";
|
|
23217
|
+
const lower = p.toLowerCase();
|
|
23218
|
+
if (lower === "critical" || lower === "high") return " priority-high";
|
|
23219
|
+
if (lower === "medium") return " priority-medium";
|
|
23220
|
+
if (lower === "low") return " priority-low";
|
|
23221
|
+
return "";
|
|
23222
|
+
}
|
|
23223
|
+
function miniProgressBar(pct) {
|
|
23224
|
+
return `<div class="mini-progress-bar"><div class="mini-progress-fill" style="width:${pct}%"></div><span class="mini-progress-label">${pct}%</span></div>`;
|
|
23225
|
+
}
|
|
23124
23226
|
function poBacklogPage(ctx) {
|
|
23125
23227
|
const features = ctx.store.list({ type: "feature" });
|
|
23126
23228
|
const questions = ctx.store.list({ type: "question" });
|
|
23127
23229
|
const openQuestions = questions.filter((d) => d.frontmatter.status === "open");
|
|
23128
|
-
const
|
|
23129
|
-
const
|
|
23230
|
+
const priorityOrder2 = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
23231
|
+
const statusOrder2 = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
23130
23232
|
const sortedFeatures = [...features].sort((a, b) => {
|
|
23131
|
-
const sa =
|
|
23132
|
-
const sb =
|
|
23233
|
+
const sa = statusOrder2[a.frontmatter.status] ?? 3;
|
|
23234
|
+
const sb = statusOrder2[b.frontmatter.status] ?? 3;
|
|
23133
23235
|
if (sa !== sb) return sa - sb;
|
|
23134
|
-
const pa =
|
|
23135
|
-
const pb =
|
|
23236
|
+
const pa = priorityOrder2[a.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
23237
|
+
const pb = priorityOrder2[b.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
23136
23238
|
if (pa !== pb) return pa - pb;
|
|
23137
23239
|
return a.frontmatter.id.localeCompare(b.frontmatter.id);
|
|
23138
23240
|
});
|
|
23139
23241
|
const epics = ctx.store.list({ type: "epic" });
|
|
23140
23242
|
const featureToEpics = /* @__PURE__ */ new Map();
|
|
23141
23243
|
for (const epic of epics) {
|
|
23142
|
-
const
|
|
23143
|
-
const featureIds = Array.isArray(linked) ? linked : linked ? [linked] : [];
|
|
23244
|
+
const featureIds = normalizeLinkedFeatures(epic.frontmatter.linkedFeature);
|
|
23144
23245
|
for (const fid of featureIds) {
|
|
23145
|
-
const
|
|
23146
|
-
|
|
23147
|
-
featureToEpics.set(
|
|
23246
|
+
const arr = featureToEpics.get(fid) ?? [];
|
|
23247
|
+
arr.push(epic);
|
|
23248
|
+
featureToEpics.set(fid, arr);
|
|
23148
23249
|
}
|
|
23149
23250
|
}
|
|
23150
|
-
|
|
23151
|
-
|
|
23152
|
-
|
|
23153
|
-
|
|
23154
|
-
|
|
23155
|
-
|
|
23156
|
-
|
|
23251
|
+
const allTasks = ctx.store.list({ type: "task" });
|
|
23252
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
23253
|
+
for (const task of allTasks) {
|
|
23254
|
+
const tags = task.frontmatter.tags ?? [];
|
|
23255
|
+
for (const tag of tags) {
|
|
23256
|
+
if (tag.startsWith("epic:")) {
|
|
23257
|
+
const epicId = tag.slice(5);
|
|
23258
|
+
const arr = epicToTasks.get(epicId) ?? [];
|
|
23259
|
+
arr.push(task);
|
|
23260
|
+
epicToTasks.set(epicId, arr);
|
|
23261
|
+
}
|
|
23262
|
+
}
|
|
23263
|
+
}
|
|
23264
|
+
const DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23265
|
+
function featureTaskStats(featureId) {
|
|
23266
|
+
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
23267
|
+
let total = 0;
|
|
23268
|
+
let done = 0;
|
|
23269
|
+
let progressSum = 0;
|
|
23270
|
+
for (const epic of fEpics) {
|
|
23271
|
+
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
23272
|
+
total++;
|
|
23273
|
+
if (DONE_STATUSES14.has(t.frontmatter.status)) done++;
|
|
23274
|
+
progressSum += getEffectiveProgress(t.frontmatter);
|
|
23275
|
+
}
|
|
23276
|
+
}
|
|
23277
|
+
return { epicCount: fEpics.length, total, done, avgProgress: total > 0 ? Math.round(progressSum / total) : 0 };
|
|
23157
23278
|
}
|
|
23158
23279
|
const featureStatuses = [...new Set(features.map((d) => d.frontmatter.status))].sort();
|
|
23159
23280
|
const featurePriorities = [...new Set(features.map((d) => d.frontmatter.priority ?? "").filter(Boolean))].sort();
|
|
23160
23281
|
const featureEpicIds = [...new Set(
|
|
23161
|
-
features.flatMap((d) => featureToEpics.get(d.frontmatter.id) ?? [])
|
|
23282
|
+
features.flatMap((d) => (featureToEpics.get(d.frontmatter.id) ?? []).map((e) => e.frontmatter.id))
|
|
23162
23283
|
)].sort();
|
|
23163
23284
|
const featuresFilters = `<div class="filters">
|
|
23164
23285
|
${tableFilter("features-table", 2, "Status", featureStatuses)}
|
|
@@ -23169,12 +23290,13 @@ function poBacklogPage(ctx) {
|
|
|
23169
23290
|
<div class="table-wrap table-short">
|
|
23170
23291
|
<table id="features-table">
|
|
23171
23292
|
<thead>
|
|
23172
|
-
<tr>${sortableTh("ID", "features-table", 0)}${sortableTh("Title", "features-table", 1)}${sortableTh("Status", "features-table", 2)}${sortableTh("Priority", "features-table", 3)}<th>
|
|
23293
|
+
<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>
|
|
23173
23294
|
</thead>
|
|
23174
23295
|
<tbody>
|
|
23175
23296
|
${sortedFeatures.map((d) => {
|
|
23176
|
-
const
|
|
23177
|
-
const
|
|
23297
|
+
const stats = featureTaskStats(d.frontmatter.id);
|
|
23298
|
+
const linkedEpicDocs = featureToEpics.get(d.frontmatter.id) ?? [];
|
|
23299
|
+
const epicLinks = linkedEpicDocs.map((e) => `<a href="/docs/epic/${escapeHtml(e.frontmatter.id)}">${escapeHtml(e.frontmatter.id)}</a>`).join(", ");
|
|
23178
23300
|
return `
|
|
23179
23301
|
<tr>
|
|
23180
23302
|
<td><a href="/docs/feature/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
@@ -23182,6 +23304,8 @@ function poBacklogPage(ctx) {
|
|
|
23182
23304
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
23183
23305
|
<td><span class="${priorityClass(d.frontmatter.priority)}">${escapeHtml(d.frontmatter.priority ?? "\u2014")}</span></td>
|
|
23184
23306
|
<td>${epicLinks || '<span class="text-dim">\u2014</span>'}</td>
|
|
23307
|
+
<td>${stats.total > 0 ? `${stats.done}/${stats.total}` : '<span class="text-dim">\u2014</span>'}</td>
|
|
23308
|
+
<td>${stats.total > 0 ? miniProgressBar(stats.avgProgress) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23185
23309
|
</tr>`;
|
|
23186
23310
|
}).join("")}
|
|
23187
23311
|
</tbody>
|
|
@@ -23223,14 +23347,23 @@ function poBacklogPage(ctx) {
|
|
|
23223
23347
|
|
|
23224
23348
|
// src/web/templates/pages/po/decisions.ts
|
|
23225
23349
|
var RESOLVED_STATUSES = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
23350
|
+
var KNOWN_OWNERS = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
|
|
23351
|
+
function ownerBadge(owner) {
|
|
23352
|
+
if (!owner) return '<span class="text-dim">\u2014</span>';
|
|
23353
|
+
const cls = KNOWN_OWNERS.has(owner.toLowerCase()) ? `owner-badge-${owner.toLowerCase()}` : "owner-badge-other";
|
|
23354
|
+
return `<span class="owner-badge ${cls}">${escapeHtml(owner.toUpperCase())}</span>`;
|
|
23355
|
+
}
|
|
23226
23356
|
function poDecisionsPage(ctx) {
|
|
23227
23357
|
const decisions = ctx.store.list({ type: "decision" });
|
|
23358
|
+
const questions = ctx.store.list({ type: "question" });
|
|
23359
|
+
const features = ctx.store.list({ type: "feature" });
|
|
23228
23360
|
const openDecisions = decisions.filter((d) => !RESOLVED_STATUSES.has(d.frontmatter.status));
|
|
23229
23361
|
const resolvedDecisions = decisions.filter((d) => RESOLVED_STATUSES.has(d.frontmatter.status));
|
|
23362
|
+
const openQuestions = questions.filter((d) => d.frontmatter.status === "open");
|
|
23230
23363
|
const statsCards = `
|
|
23231
23364
|
<div class="cards">
|
|
23232
23365
|
<div class="card">
|
|
23233
|
-
<div class="card-label">Open</div>
|
|
23366
|
+
<div class="card-label">Open Decisions</div>
|
|
23234
23367
|
<div class="card-value${openDecisions.length > 0 ? " priority-medium" : ""}">${openDecisions.length}</div>
|
|
23235
23368
|
<div class="card-sub">awaiting resolution</div>
|
|
23236
23369
|
</div>
|
|
@@ -23239,12 +23372,79 @@ function poDecisionsPage(ctx) {
|
|
|
23239
23372
|
<div class="card-value">${resolvedDecisions.length}</div>
|
|
23240
23373
|
<div class="card-sub">decisions made</div>
|
|
23241
23374
|
</div>
|
|
23375
|
+
<div class="card">
|
|
23376
|
+
<div class="card-label">Open Questions</div>
|
|
23377
|
+
<div class="card-value${openQuestions.length > 0 ? " priority-medium" : ""}">${openQuestions.length}</div>
|
|
23378
|
+
<div class="card-sub">needing answers</div>
|
|
23379
|
+
</div>
|
|
23242
23380
|
<div class="card">
|
|
23243
23381
|
<div class="card-label">Total</div>
|
|
23244
23382
|
<div class="card-value">${decisions.length}</div>
|
|
23245
23383
|
<div class="card-sub">all decisions</div>
|
|
23246
23384
|
</div>
|
|
23247
23385
|
</div>`;
|
|
23386
|
+
function daysSince(isoDate) {
|
|
23387
|
+
if (!isoDate) return 0;
|
|
23388
|
+
return Math.max(0, Math.floor((Date.now() - new Date(isoDate).getTime()) / 864e5));
|
|
23389
|
+
}
|
|
23390
|
+
const featureGroups = /* @__PURE__ */ new Map();
|
|
23391
|
+
const unlinked = [];
|
|
23392
|
+
function addToGroup(doc, docType) {
|
|
23393
|
+
const tags = doc.frontmatter.tags ?? [];
|
|
23394
|
+
const featureTags = tags.filter((t) => t.startsWith("feature:")).map((t) => t.slice(8));
|
|
23395
|
+
const item = { doc, docType, ageDays: daysSince(doc.frontmatter.created) };
|
|
23396
|
+
if (featureTags.length === 0) {
|
|
23397
|
+
unlinked.push(item);
|
|
23398
|
+
} else {
|
|
23399
|
+
for (const fid of featureTags) {
|
|
23400
|
+
const arr = featureGroups.get(fid) ?? [];
|
|
23401
|
+
arr.push(item);
|
|
23402
|
+
featureGroups.set(fid, arr);
|
|
23403
|
+
}
|
|
23404
|
+
}
|
|
23405
|
+
}
|
|
23406
|
+
for (const d of openDecisions) addToGroup(d, "decision");
|
|
23407
|
+
for (const q of openQuestions) addToGroup(q, "question");
|
|
23408
|
+
const totalDeps = openDecisions.length + openQuestions.length;
|
|
23409
|
+
const featureLookup = new Map(features.map((f) => [f.frontmatter.id, f]));
|
|
23410
|
+
function renderDepRows(items) {
|
|
23411
|
+
return items.map((item) => `
|
|
23412
|
+
<tr>
|
|
23413
|
+
<td><a href="/docs/${item.docType}/${escapeHtml(item.doc.frontmatter.id)}">${escapeHtml(item.doc.frontmatter.id)}</a></td>
|
|
23414
|
+
<td>${escapeHtml(item.doc.frontmatter.title)}</td>
|
|
23415
|
+
<td>${escapeHtml(typeLabel(item.docType))}</td>
|
|
23416
|
+
<td>${ownerBadge(item.doc.frontmatter.owner)}</td>
|
|
23417
|
+
<td>${item.ageDays}d</td>
|
|
23418
|
+
</tr>`).join("");
|
|
23419
|
+
}
|
|
23420
|
+
let depRows = "";
|
|
23421
|
+
for (const [fid, items] of featureGroups) {
|
|
23422
|
+
const feat = featureLookup.get(fid);
|
|
23423
|
+
const label = feat ? `${escapeHtml(fid)}: ${escapeHtml(feat.frontmatter.title)}` : escapeHtml(fid);
|
|
23424
|
+
depRows += `
|
|
23425
|
+
<tr class="group-header-row"><td colspan="5"><strong>${label}</strong></td></tr>
|
|
23426
|
+
${renderDepRows(items)}`;
|
|
23427
|
+
}
|
|
23428
|
+
if (unlinked.length > 0) {
|
|
23429
|
+
depRows += `
|
|
23430
|
+
<tr class="group-header-row"><td colspan="5"><strong>Unlinked</strong></td></tr>
|
|
23431
|
+
${renderDepRows(unlinked)}`;
|
|
23432
|
+
}
|
|
23433
|
+
const depsSection = totalDeps > 0 ? collapsibleSection(
|
|
23434
|
+
"po-decisions-deps",
|
|
23435
|
+
`By Feature (${totalDeps})`,
|
|
23436
|
+
`<div class="table-wrap">
|
|
23437
|
+
<table>
|
|
23438
|
+
<thead>
|
|
23439
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Owner</th><th>Age</th></tr>
|
|
23440
|
+
</thead>
|
|
23441
|
+
<tbody>
|
|
23442
|
+
${depRows}
|
|
23443
|
+
</tbody>
|
|
23444
|
+
</table>
|
|
23445
|
+
</div>`,
|
|
23446
|
+
{ titleTag: "h3" }
|
|
23447
|
+
) : "";
|
|
23248
23448
|
function decisionTable(docs, tableId) {
|
|
23249
23449
|
if (docs.length === 0) return '<div class="empty"><p>None found.</p></div>';
|
|
23250
23450
|
const statuses = [...new Set(docs.map((d) => d.frontmatter.status))].sort();
|
|
@@ -23266,7 +23466,7 @@ function poDecisionsPage(ctx) {
|
|
|
23266
23466
|
<td><a href="/docs/decision/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
23267
23467
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
23268
23468
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
23269
|
-
<td>${
|
|
23469
|
+
<td>${ownerBadge(d.frontmatter.owner)}</td>
|
|
23270
23470
|
<td>${formatDate(d.frontmatter.created)}</td>
|
|
23271
23471
|
</tr>`).join("")}
|
|
23272
23472
|
</tbody>
|
|
@@ -23288,16 +23488,185 @@ function poDecisionsPage(ctx) {
|
|
|
23288
23488
|
return `
|
|
23289
23489
|
<div class="page-header">
|
|
23290
23490
|
<h2>Decision Log</h2>
|
|
23291
|
-
<div class="subtitle">Track and manage product decisions</div>
|
|
23491
|
+
<div class="subtitle">Track and manage product decisions and dependencies</div>
|
|
23292
23492
|
</div>
|
|
23293
23493
|
${statsCards}
|
|
23494
|
+
${depsSection}
|
|
23294
23495
|
${openSection}
|
|
23295
23496
|
${resolvedSection}
|
|
23296
23497
|
${renderTableUtilsScript()}
|
|
23297
23498
|
`;
|
|
23298
23499
|
}
|
|
23299
23500
|
|
|
23501
|
+
// src/web/templates/components/work-items-table.ts
|
|
23502
|
+
var FOCUS_BORDER_PALETTE = [
|
|
23503
|
+
"hsl(220, 60%, 55%)",
|
|
23504
|
+
"hsl(160, 50%, 45%)",
|
|
23505
|
+
"hsl(280, 45%, 55%)",
|
|
23506
|
+
"hsl(30, 65%, 55%)",
|
|
23507
|
+
"hsl(340, 50%, 55%)",
|
|
23508
|
+
"hsl(190, 50%, 45%)",
|
|
23509
|
+
"hsl(60, 50%, 50%)",
|
|
23510
|
+
"hsl(120, 40%, 45%)"
|
|
23511
|
+
];
|
|
23512
|
+
function hashString(s) {
|
|
23513
|
+
let h = 0;
|
|
23514
|
+
for (let i = 0; i < s.length; i++) {
|
|
23515
|
+
h = (h << 5) - h + s.charCodeAt(i) | 0;
|
|
23516
|
+
}
|
|
23517
|
+
return Math.abs(h);
|
|
23518
|
+
}
|
|
23519
|
+
function countFocusStats(items) {
|
|
23520
|
+
let total = 0;
|
|
23521
|
+
let done = 0;
|
|
23522
|
+
let inProgress = 0;
|
|
23523
|
+
function walk(list) {
|
|
23524
|
+
for (const w of list) {
|
|
23525
|
+
if (w.type !== "contribution") {
|
|
23526
|
+
total++;
|
|
23527
|
+
const s = w.status.toLowerCase();
|
|
23528
|
+
if (s === "done" || s === "closed" || s === "resolved" || s === "decided") done++;
|
|
23529
|
+
else if (s === "in-progress" || s === "in progress") inProgress++;
|
|
23530
|
+
}
|
|
23531
|
+
if (w.children) walk(w.children);
|
|
23532
|
+
}
|
|
23533
|
+
}
|
|
23534
|
+
walk(items);
|
|
23535
|
+
return { total, done, inProgress };
|
|
23536
|
+
}
|
|
23537
|
+
var KNOWN_OWNERS2 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
|
|
23538
|
+
function ownerBadge2(owner) {
|
|
23539
|
+
if (!owner) return '<span class="text-dim">\u2014</span>';
|
|
23540
|
+
const cls = KNOWN_OWNERS2.has(owner) ? `owner-badge-${owner}` : "owner-badge-other";
|
|
23541
|
+
return `<span class="owner-badge ${cls}">${escapeHtml(owner.toUpperCase())}</span>`;
|
|
23542
|
+
}
|
|
23543
|
+
function renderItemRows(items, borderColor, showOwner, depth = 0) {
|
|
23544
|
+
return items.flatMap((w) => {
|
|
23545
|
+
const isChild = depth > 0;
|
|
23546
|
+
const isContribution = w.type === "contribution";
|
|
23547
|
+
const classes = ["focus-row"];
|
|
23548
|
+
if (isContribution) classes.push("contribution-row");
|
|
23549
|
+
else if (isChild) classes.push("child-row");
|
|
23550
|
+
const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
|
|
23551
|
+
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>` : "";
|
|
23552
|
+
const ownerCell = showOwner ? `<td>${ownerBadge2(w.owner)}</td>` : "";
|
|
23553
|
+
const row = `
|
|
23554
|
+
<tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
|
|
23555
|
+
<td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
23556
|
+
<td>${escapeHtml(w.title)}</td>
|
|
23557
|
+
${ownerCell}
|
|
23558
|
+
<td>${statusBadge(w.status)}</td>
|
|
23559
|
+
<td>${progressCell}</td>
|
|
23560
|
+
</tr>`;
|
|
23561
|
+
const childRows = w.children ? renderItemRows(w.children, borderColor, showOwner, depth + 1) : [];
|
|
23562
|
+
return [row, ...childRows];
|
|
23563
|
+
});
|
|
23564
|
+
}
|
|
23565
|
+
function renderWorkItemsTable(items, options) {
|
|
23566
|
+
const sectionId = options?.sectionId ?? "work-items";
|
|
23567
|
+
const title = options?.title ?? "Work Items";
|
|
23568
|
+
const defaultCollapsed = options?.defaultCollapsed ?? false;
|
|
23569
|
+
const showOwner = options?.showOwner ?? false;
|
|
23570
|
+
const focusGroups = /* @__PURE__ */ new Map();
|
|
23571
|
+
for (const item of items) {
|
|
23572
|
+
const focus = item.workFocus ?? "Unassigned";
|
|
23573
|
+
if (!focusGroups.has(focus)) focusGroups.set(focus, []);
|
|
23574
|
+
focusGroups.get(focus).push(item);
|
|
23575
|
+
}
|
|
23576
|
+
const focusColorMap = /* @__PURE__ */ new Map();
|
|
23577
|
+
for (const name of focusGroups.keys()) {
|
|
23578
|
+
focusColorMap.set(name, FOCUS_BORDER_PALETTE[hashString(name) % FOCUS_BORDER_PALETTE.length]);
|
|
23579
|
+
}
|
|
23580
|
+
const allWorkItemRows = [];
|
|
23581
|
+
for (const [focus, groupItems] of focusGroups) {
|
|
23582
|
+
const color = focusColorMap.get(focus);
|
|
23583
|
+
const stats = countFocusStats(groupItems);
|
|
23584
|
+
const pct = stats.total > 0 ? Math.round(stats.done / stats.total * 100) : 0;
|
|
23585
|
+
const summaryParts = [];
|
|
23586
|
+
if (stats.done > 0) summaryParts.push(`${stats.done} done`);
|
|
23587
|
+
if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
|
|
23588
|
+
const remaining = stats.total - stats.done - stats.inProgress;
|
|
23589
|
+
if (remaining > 0) summaryParts.push(`${remaining} open`);
|
|
23590
|
+
const leftColspan = showOwner ? 3 : 2;
|
|
23591
|
+
allWorkItemRows.push(`
|
|
23592
|
+
<tr class="focus-group-header" style="--focus-color: ${color}">
|
|
23593
|
+
<td colspan="${leftColspan}">
|
|
23594
|
+
<span class="focus-group-name">${escapeHtml(focus)}</span>
|
|
23595
|
+
<span class="focus-group-stats">${summaryParts.join(" / ")}</span>
|
|
23596
|
+
</td>
|
|
23597
|
+
<td colspan="2">
|
|
23598
|
+
<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>
|
|
23599
|
+
</td>
|
|
23600
|
+
</tr>`);
|
|
23601
|
+
allWorkItemRows.push(...renderItemRows(groupItems, color, showOwner));
|
|
23602
|
+
}
|
|
23603
|
+
if (allWorkItemRows.length === 0) return "";
|
|
23604
|
+
const ownerHeader = showOwner ? "<th>Owner</th>" : "";
|
|
23605
|
+
return collapsibleSection(
|
|
23606
|
+
sectionId,
|
|
23607
|
+
title,
|
|
23608
|
+
`<div class="table-wrap">
|
|
23609
|
+
<table id="${sectionId}-table">
|
|
23610
|
+
<thead>
|
|
23611
|
+
<tr>
|
|
23612
|
+
<th>ID</th>
|
|
23613
|
+
<th>Title</th>
|
|
23614
|
+
${ownerHeader}
|
|
23615
|
+
<th>Status</th>
|
|
23616
|
+
<th>Progress</th>
|
|
23617
|
+
</tr>
|
|
23618
|
+
</thead>
|
|
23619
|
+
<tbody>
|
|
23620
|
+
${allWorkItemRows.join("")}
|
|
23621
|
+
</tbody>
|
|
23622
|
+
</table>
|
|
23623
|
+
</div>`,
|
|
23624
|
+
{ titleTag: "h3", defaultCollapsed }
|
|
23625
|
+
);
|
|
23626
|
+
}
|
|
23627
|
+
var DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled", "decided"]);
|
|
23628
|
+
function computeOwnerCompletionPct(items, owner) {
|
|
23629
|
+
let total = 0;
|
|
23630
|
+
let progressSum = 0;
|
|
23631
|
+
function walk(list) {
|
|
23632
|
+
for (const w of list) {
|
|
23633
|
+
if (w.type !== "contribution" && w.owner === owner) {
|
|
23634
|
+
total++;
|
|
23635
|
+
progressSum += w.progress ?? (DONE_STATUSES6.has(w.status) ? 100 : 0);
|
|
23636
|
+
}
|
|
23637
|
+
if (w.children) walk(w.children);
|
|
23638
|
+
}
|
|
23639
|
+
}
|
|
23640
|
+
walk(items);
|
|
23641
|
+
return total > 0 ? Math.round(progressSum / total) : 0;
|
|
23642
|
+
}
|
|
23643
|
+
function filterItemsByOwner(items, owner) {
|
|
23644
|
+
const result = [];
|
|
23645
|
+
for (const item of items) {
|
|
23646
|
+
if (item.owner === owner) {
|
|
23647
|
+
result.push(item);
|
|
23648
|
+
} else if (item.children) {
|
|
23649
|
+
result.push(...filterItemsByOwner(item.children, owner));
|
|
23650
|
+
}
|
|
23651
|
+
}
|
|
23652
|
+
return result;
|
|
23653
|
+
}
|
|
23654
|
+
|
|
23300
23655
|
// src/web/templates/pages/po/delivery.ts
|
|
23656
|
+
var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23657
|
+
var priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
23658
|
+
var statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
23659
|
+
function priorityClass2(p) {
|
|
23660
|
+
if (!p) return "";
|
|
23661
|
+
const lower = p.toLowerCase();
|
|
23662
|
+
if (lower === "critical" || lower === "high") return " priority-high";
|
|
23663
|
+
if (lower === "medium") return " priority-medium";
|
|
23664
|
+
if (lower === "low") return " priority-low";
|
|
23665
|
+
return "";
|
|
23666
|
+
}
|
|
23667
|
+
function miniProgressBar2(pct) {
|
|
23668
|
+
return `<div class="mini-progress-bar"><div class="mini-progress-fill" style="width:${pct}%"></div><span class="mini-progress-label">${pct}%</span></div>`;
|
|
23669
|
+
}
|
|
23301
23670
|
var PO_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
|
|
23302
23671
|
"stakeholder-feedback",
|
|
23303
23672
|
"acceptance-result",
|
|
@@ -23323,24 +23692,8 @@ function poDeliveryPage(ctx) {
|
|
|
23323
23692
|
<p>No active sprint found. Create a sprint and set its status to "active" to track delivery.</p>
|
|
23324
23693
|
</div>`;
|
|
23325
23694
|
}
|
|
23326
|
-
const
|
|
23327
|
-
|
|
23328
|
-
);
|
|
23329
|
-
function findContributions(items, parentId) {
|
|
23330
|
-
const result = [];
|
|
23331
|
-
for (const item of items) {
|
|
23332
|
-
if (item.type === "contribution" && PO_CONTRIBUTION_TYPES.has(item.id.split("-").slice(0, -1).join("-") || "")) {
|
|
23333
|
-
result.push({ id: item.id, title: item.title, type: item.type, status: item.status, parentId });
|
|
23334
|
-
}
|
|
23335
|
-
if (PO_CONTRIBUTION_TYPES.has(item.type)) {
|
|
23336
|
-
result.push({ id: item.id, title: item.title, type: item.type, status: item.status, parentId });
|
|
23337
|
-
}
|
|
23338
|
-
if (item.children) {
|
|
23339
|
-
result.push(...findContributions(item.children, item.id));
|
|
23340
|
-
}
|
|
23341
|
-
}
|
|
23342
|
-
return result;
|
|
23343
|
-
}
|
|
23695
|
+
const poItems = filterItemsByOwner(data.workItems.items, "po");
|
|
23696
|
+
const poCompletionPct = computeOwnerCompletionPct(data.workItems.items, "po");
|
|
23344
23697
|
const allDocs = ctx.store.list();
|
|
23345
23698
|
const poContributions = allDocs.filter((d) => PO_CONTRIBUTION_TYPES.has(d.frontmatter.type));
|
|
23346
23699
|
const statsCards = `
|
|
@@ -23350,16 +23703,16 @@ function poDeliveryPage(ctx) {
|
|
|
23350
23703
|
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
23351
23704
|
<div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
|
|
23352
23705
|
</div>
|
|
23706
|
+
<div class="card">
|
|
23707
|
+
<div class="card-label">PO Completion</div>
|
|
23708
|
+
<div class="card-value">${poCompletionPct}%</div>
|
|
23709
|
+
<div class="card-sub">${poItems.length} owned items</div>
|
|
23710
|
+
</div>
|
|
23353
23711
|
<div class="card">
|
|
23354
23712
|
<div class="card-label">Days Remaining</div>
|
|
23355
23713
|
<div class="card-value">${data.timeline.daysRemaining}</div>
|
|
23356
23714
|
<div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
|
|
23357
23715
|
</div>
|
|
23358
|
-
<div class="card">
|
|
23359
|
-
<div class="card-label">Features Done</div>
|
|
23360
|
-
<div class="card-value">${doneFeatures.length}</div>
|
|
23361
|
-
<div class="card-sub">this sprint</div>
|
|
23362
|
-
</div>
|
|
23363
23716
|
<div class="card">
|
|
23364
23717
|
<div class="card-label">PO Contributions</div>
|
|
23365
23718
|
<div class="card-value">${poContributions.length}</div>
|
|
@@ -23371,7 +23724,11 @@ function poDeliveryPage(ctx) {
|
|
|
23371
23724
|
<strong>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</strong>
|
|
23372
23725
|
${data.sprint.goal ? ` | ${escapeHtml(data.sprint.goal)}` : ""}
|
|
23373
23726
|
</div>`;
|
|
23374
|
-
const
|
|
23727
|
+
const workItemsSection = renderWorkItemsTable(poItems, {
|
|
23728
|
+
sectionId: "po-delivery-items",
|
|
23729
|
+
title: "PO Work Items"
|
|
23730
|
+
});
|
|
23731
|
+
const epicsSection = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
23375
23732
|
"po-delivery-epics",
|
|
23376
23733
|
"Linked Epics",
|
|
23377
23734
|
`<div class="table-wrap">
|
|
@@ -23392,6 +23749,82 @@ function poDeliveryPage(ctx) {
|
|
|
23392
23749
|
</div>`,
|
|
23393
23750
|
{ titleTag: "h3" }
|
|
23394
23751
|
) : "";
|
|
23752
|
+
const features = ctx.store.list({ type: "feature" });
|
|
23753
|
+
const epics = ctx.store.list({ type: "epic" });
|
|
23754
|
+
const allTasks = ctx.store.list({ type: "task" });
|
|
23755
|
+
const sprints = ctx.store.list({ type: "sprint" });
|
|
23756
|
+
const featureToEpics = /* @__PURE__ */ new Map();
|
|
23757
|
+
for (const epic of epics) {
|
|
23758
|
+
const featureIds = normalizeLinkedFeatures(epic.frontmatter.linkedFeature);
|
|
23759
|
+
for (const fid of featureIds) {
|
|
23760
|
+
const arr = featureToEpics.get(fid) ?? [];
|
|
23761
|
+
arr.push(epic);
|
|
23762
|
+
featureToEpics.set(fid, arr);
|
|
23763
|
+
}
|
|
23764
|
+
}
|
|
23765
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
23766
|
+
for (const task of allTasks) {
|
|
23767
|
+
const tags = task.frontmatter.tags ?? [];
|
|
23768
|
+
for (const tag of tags) {
|
|
23769
|
+
if (tag.startsWith("epic:")) {
|
|
23770
|
+
const arr = epicToTasks.get(tag.slice(5)) ?? [];
|
|
23771
|
+
arr.push(task);
|
|
23772
|
+
epicToTasks.set(tag.slice(5), arr);
|
|
23773
|
+
}
|
|
23774
|
+
}
|
|
23775
|
+
}
|
|
23776
|
+
const activeSprint = sprints.find((s) => s.frontmatter.status === "active");
|
|
23777
|
+
const activeSprintEpicIds = new Set(
|
|
23778
|
+
activeSprint ? normalizeLinkedEpics(activeSprint.frontmatter.linkedEpics) : []
|
|
23779
|
+
);
|
|
23780
|
+
function featureSprintLabel(featureId) {
|
|
23781
|
+
if (!activeSprint) return "\u2014";
|
|
23782
|
+
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
23783
|
+
return fEpics.some((e) => activeSprintEpicIds.has(e.frontmatter.id)) ? escapeHtml(activeSprint.frontmatter.id) : "\u2014";
|
|
23784
|
+
}
|
|
23785
|
+
function featureProgress(featureId) {
|
|
23786
|
+
const fEpics = featureToEpics.get(featureId) ?? [];
|
|
23787
|
+
let total = 0;
|
|
23788
|
+
let progressSum = 0;
|
|
23789
|
+
for (const epic of fEpics) {
|
|
23790
|
+
for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
|
|
23791
|
+
total++;
|
|
23792
|
+
progressSum += getEffectiveProgress(t.frontmatter);
|
|
23793
|
+
}
|
|
23794
|
+
}
|
|
23795
|
+
return total > 0 ? Math.round(progressSum / total) : 0;
|
|
23796
|
+
}
|
|
23797
|
+
const nonDoneFeatures = features.filter((f) => !DONE_STATUSES7.has(f.frontmatter.status)).sort((a, b) => {
|
|
23798
|
+
const pa = priorityOrder[a.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
23799
|
+
const pb = priorityOrder[b.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
23800
|
+
if (pa !== pb) return pa - pb;
|
|
23801
|
+
const sa = statusOrder[a.frontmatter.status] ?? 3;
|
|
23802
|
+
const sb = statusOrder[b.frontmatter.status] ?? 3;
|
|
23803
|
+
return sa - sb;
|
|
23804
|
+
});
|
|
23805
|
+
const priorityQueueSection = collapsibleSection(
|
|
23806
|
+
"po-priority-queue",
|
|
23807
|
+
`Priority Queue (${nonDoneFeatures.length})`,
|
|
23808
|
+
nonDoneFeatures.length > 0 ? `<div class="table-wrap">
|
|
23809
|
+
<table>
|
|
23810
|
+
<thead>
|
|
23811
|
+
<tr><th>Priority</th><th>ID</th><th>Title</th><th>Status</th><th>Sprint</th><th>Progress</th></tr>
|
|
23812
|
+
</thead>
|
|
23813
|
+
<tbody>
|
|
23814
|
+
${nonDoneFeatures.map((f) => `
|
|
23815
|
+
<tr>
|
|
23816
|
+
<td><span class="${priorityClass2(f.frontmatter.priority)}">${escapeHtml(f.frontmatter.priority ?? "\u2014")}</span></td>
|
|
23817
|
+
<td><a href="/docs/feature/${escapeHtml(f.frontmatter.id)}">${escapeHtml(f.frontmatter.id)}</a></td>
|
|
23818
|
+
<td>${escapeHtml(f.frontmatter.title)}</td>
|
|
23819
|
+
<td>${statusBadge(f.frontmatter.status)}</td>
|
|
23820
|
+
<td>${featureSprintLabel(f.frontmatter.id)}</td>
|
|
23821
|
+
<td>${miniProgressBar2(featureProgress(f.frontmatter.id))}</td>
|
|
23822
|
+
</tr>`).join("")}
|
|
23823
|
+
</tbody>
|
|
23824
|
+
</table>
|
|
23825
|
+
</div>` : '<div class="empty"><p>No active features in the queue.</p></div>',
|
|
23826
|
+
{ titleTag: "h3" }
|
|
23827
|
+
);
|
|
23395
23828
|
const contributionsSection = poContributions.length > 0 ? collapsibleSection(
|
|
23396
23829
|
"po-delivery-contributions",
|
|
23397
23830
|
`PO Contributions (${poContributions.length})`,
|
|
@@ -23422,7 +23855,9 @@ function poDeliveryPage(ctx) {
|
|
|
23422
23855
|
${sprintHeader}
|
|
23423
23856
|
${progressBar(data.workItems.completionPct)}
|
|
23424
23857
|
${statsCards}
|
|
23425
|
-
${
|
|
23858
|
+
${workItemsSection}
|
|
23859
|
+
${epicsSection}
|
|
23860
|
+
${priorityQueueSection}
|
|
23426
23861
|
${contributionsSection}
|
|
23427
23862
|
`;
|
|
23428
23863
|
}
|
|
@@ -23448,6 +23883,12 @@ function renderGarWidget(report) {
|
|
|
23448
23883
|
}
|
|
23449
23884
|
|
|
23450
23885
|
// src/web/templates/pages/po/stakeholders.ts
|
|
23886
|
+
var KNOWN_OWNERS3 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
|
|
23887
|
+
function ownerBadge3(owner) {
|
|
23888
|
+
if (!owner) return '<span class="text-dim">\u2014</span>';
|
|
23889
|
+
const cls = KNOWN_OWNERS3.has(owner.toLowerCase()) ? `owner-badge-${owner.toLowerCase()}` : "owner-badge-other";
|
|
23890
|
+
return `<span class="owner-badge ${cls}">${escapeHtml(owner.toUpperCase())}</span>`;
|
|
23891
|
+
}
|
|
23451
23892
|
function poStakeholdersPage(ctx) {
|
|
23452
23893
|
const garReport = getGarData(ctx.store, ctx.projectName);
|
|
23453
23894
|
const actions = ctx.store.list({ type: "action" });
|
|
@@ -23498,7 +23939,7 @@ function poStakeholdersPage(ctx) {
|
|
|
23498
23939
|
<td><a href="/docs/action/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
23499
23940
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
23500
23941
|
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
23501
|
-
<td>${
|
|
23942
|
+
<td>${ownerBadge3(d.frontmatter.owner)}</td>
|
|
23502
23943
|
<td>${d.frontmatter.dueDate ? formatDate(d.frontmatter.dueDate) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23503
23944
|
</tr>`).join("")}
|
|
23504
23945
|
</tbody>
|
|
@@ -23521,7 +23962,7 @@ function poStakeholdersPage(ctx) {
|
|
|
23521
23962
|
<tr>
|
|
23522
23963
|
<td><a href="/docs/question/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
23523
23964
|
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
23524
|
-
<td>${
|
|
23965
|
+
<td>${ownerBadge3(d.frontmatter.owner)}</td>
|
|
23525
23966
|
<td>${formatDate(d.frontmatter.created)}</td>
|
|
23526
23967
|
</tr>`).join("")}
|
|
23527
23968
|
</tbody>
|
|
@@ -23562,7 +24003,7 @@ registerPersonaPage("po", "delivery", poDeliveryPage);
|
|
|
23562
24003
|
registerPersonaPage("po", "stakeholders", poStakeholdersPage);
|
|
23563
24004
|
|
|
23564
24005
|
// src/web/templates/pages/dm/dashboard.ts
|
|
23565
|
-
var
|
|
24006
|
+
var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23566
24007
|
function progressBar2(pct) {
|
|
23567
24008
|
return `<div class="sprint-progress-bar">
|
|
23568
24009
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
@@ -23573,7 +24014,7 @@ function dmDashboardPage(ctx) {
|
|
|
23573
24014
|
const sprintData = getSprintSummaryData(ctx.store);
|
|
23574
24015
|
const upcoming = getUpcomingData(ctx.store);
|
|
23575
24016
|
const actions = ctx.store.list({ type: "action" });
|
|
23576
|
-
const openActions = actions.filter((d) => !
|
|
24017
|
+
const openActions = actions.filter((d) => !DONE_STATUSES8.has(d.frontmatter.status));
|
|
23577
24018
|
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
23578
24019
|
const statsCards = `
|
|
23579
24020
|
<div class="cards">
|
|
@@ -23660,42 +24101,46 @@ function dmDashboardPage(ctx) {
|
|
|
23660
24101
|
`;
|
|
23661
24102
|
}
|
|
23662
24103
|
|
|
23663
|
-
// src/web/templates/pages/sprint
|
|
24104
|
+
// src/web/templates/pages/dm/sprint.ts
|
|
23664
24105
|
function progressBar3(pct) {
|
|
23665
24106
|
return `<div class="sprint-progress-bar">
|
|
23666
24107
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
23667
24108
|
<span class="sprint-progress-label">${pct}%</span>
|
|
23668
24109
|
</div>`;
|
|
23669
24110
|
}
|
|
23670
|
-
function
|
|
24111
|
+
function dmSprintPage(ctx) {
|
|
24112
|
+
const data = getSprintSummaryData(ctx.store);
|
|
23671
24113
|
if (!data) {
|
|
23672
24114
|
return `
|
|
23673
24115
|
<div class="page-header">
|
|
23674
|
-
<h2>Sprint
|
|
23675
|
-
<div class="subtitle">
|
|
24116
|
+
<h2>Sprint Execution</h2>
|
|
24117
|
+
<div class="subtitle">Full sprint oversight and delivery tracking</div>
|
|
23676
24118
|
</div>
|
|
23677
24119
|
<div class="empty">
|
|
23678
24120
|
<h3>No Active Sprint</h3>
|
|
23679
|
-
<p>No active sprint found. Create a sprint and set its status to "active" to
|
|
24121
|
+
<p>No active sprint found. Create a sprint and set its status to "active" to track execution.</p>
|
|
23680
24122
|
</div>`;
|
|
23681
24123
|
}
|
|
24124
|
+
const dmCompletionPct = computeOwnerCompletionPct(data.workItems.items, "dm");
|
|
24125
|
+
const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
|
|
24126
|
+
const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
|
|
23682
24127
|
const statsCards = `
|
|
23683
24128
|
<div class="cards">
|
|
23684
24129
|
<div class="card">
|
|
23685
|
-
<div class="card-label">Completion</div>
|
|
24130
|
+
<div class="card-label">Sprint Completion</div>
|
|
23686
24131
|
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
23687
24132
|
<div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
|
|
23688
24133
|
</div>
|
|
24134
|
+
<div class="card">
|
|
24135
|
+
<div class="card-label">DM Completion</div>
|
|
24136
|
+
<div class="card-value">${dmCompletionPct}%</div>
|
|
24137
|
+
<div class="card-sub">DM-owned items</div>
|
|
24138
|
+
</div>
|
|
23689
24139
|
<div class="card">
|
|
23690
24140
|
<div class="card-label">Days Remaining</div>
|
|
23691
24141
|
<div class="card-value">${data.timeline.daysRemaining}</div>
|
|
23692
24142
|
<div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
|
|
23693
24143
|
</div>
|
|
23694
|
-
<div class="card">
|
|
23695
|
-
<div class="card-label">Epics</div>
|
|
23696
|
-
<div class="card-value">${data.linkedEpics.length}</div>
|
|
23697
|
-
<div class="card-sub">linked to sprint</div>
|
|
23698
|
-
</div>
|
|
23699
24144
|
<a class="card card-link" href="sprint-blockers">
|
|
23700
24145
|
<div class="card-label">Blockers</div>
|
|
23701
24146
|
<div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
|
|
@@ -23707,13 +24152,18 @@ function sprintSummaryPage(data, cached2) {
|
|
|
23707
24152
|
<div class="card-sub">open risk items</div>
|
|
23708
24153
|
</a>
|
|
23709
24154
|
</div>`;
|
|
23710
|
-
const
|
|
23711
|
-
"
|
|
24155
|
+
const workItemsSection = renderWorkItemsTable(data.workItems.items, {
|
|
24156
|
+
sectionId: "dm-sprint-items",
|
|
24157
|
+
title: "Sprint Work Items",
|
|
24158
|
+
showOwner: true
|
|
24159
|
+
});
|
|
24160
|
+
const epicsSection = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
24161
|
+
"dm-sprint-epics",
|
|
23712
24162
|
"Linked Epics",
|
|
23713
24163
|
`<div class="table-wrap">
|
|
23714
24164
|
<table>
|
|
23715
24165
|
<thead>
|
|
23716
|
-
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
|
|
24166
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks Done</th></tr>
|
|
23717
24167
|
</thead>
|
|
23718
24168
|
<tbody>
|
|
23719
24169
|
${data.linkedEpics.map((e) => `
|
|
@@ -23728,138 +24178,29 @@ function sprintSummaryPage(data, cached2) {
|
|
|
23728
24178
|
</div>`,
|
|
23729
24179
|
{ titleTag: "h3" }
|
|
23730
24180
|
) : "";
|
|
23731
|
-
const
|
|
23732
|
-
"
|
|
23733
|
-
|
|
23734
|
-
"hsl(280, 45%, 55%)",
|
|
23735
|
-
"hsl(30, 65%, 55%)",
|
|
23736
|
-
"hsl(340, 50%, 55%)",
|
|
23737
|
-
"hsl(190, 50%, 45%)",
|
|
23738
|
-
"hsl(60, 50%, 50%)",
|
|
23739
|
-
"hsl(120, 40%, 45%)"
|
|
23740
|
-
];
|
|
23741
|
-
function hashString(s) {
|
|
23742
|
-
let h = 0;
|
|
23743
|
-
for (let i = 0; i < s.length; i++) {
|
|
23744
|
-
h = (h << 5) - h + s.charCodeAt(i) | 0;
|
|
23745
|
-
}
|
|
23746
|
-
return Math.abs(h);
|
|
23747
|
-
}
|
|
23748
|
-
const focusGroups = /* @__PURE__ */ new Map();
|
|
23749
|
-
for (const item of data.workItems.items) {
|
|
23750
|
-
const focus = item.workFocus ?? "Unassigned";
|
|
23751
|
-
if (!focusGroups.has(focus)) focusGroups.set(focus, []);
|
|
23752
|
-
focusGroups.get(focus).push(item);
|
|
23753
|
-
}
|
|
23754
|
-
const focusColorMap = /* @__PURE__ */ new Map();
|
|
23755
|
-
for (const name of focusGroups.keys()) {
|
|
23756
|
-
focusColorMap.set(name, FOCUS_BORDER_PALETTE[hashString(name) % FOCUS_BORDER_PALETTE.length]);
|
|
23757
|
-
}
|
|
23758
|
-
function countFocusStats(items) {
|
|
23759
|
-
let total = 0;
|
|
23760
|
-
let done = 0;
|
|
23761
|
-
let inProgress = 0;
|
|
23762
|
-
function walk(list) {
|
|
23763
|
-
for (const w of list) {
|
|
23764
|
-
if (w.type !== "contribution") {
|
|
23765
|
-
total++;
|
|
23766
|
-
const s = w.status.toLowerCase();
|
|
23767
|
-
if (s === "done" || s === "closed" || s === "resolved" || s === "decided") done++;
|
|
23768
|
-
else if (s === "in-progress" || s === "in progress") inProgress++;
|
|
23769
|
-
}
|
|
23770
|
-
if (w.children) walk(w.children);
|
|
23771
|
-
}
|
|
23772
|
-
}
|
|
23773
|
-
walk(items);
|
|
23774
|
-
return { total, done, inProgress };
|
|
23775
|
-
}
|
|
23776
|
-
function renderItemRows(items, borderColor, depth = 0) {
|
|
23777
|
-
return items.flatMap((w) => {
|
|
23778
|
-
const isChild = depth > 0;
|
|
23779
|
-
const isContribution = w.type === "contribution";
|
|
23780
|
-
const classes = ["focus-row"];
|
|
23781
|
-
if (isContribution) classes.push("contribution-row");
|
|
23782
|
-
else if (isChild) classes.push("child-row");
|
|
23783
|
-
const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
|
|
23784
|
-
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>` : "";
|
|
23785
|
-
const row = `
|
|
23786
|
-
<tr class="${classes.join(" ")}" style="--focus-color: ${borderColor}">
|
|
23787
|
-
<td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
23788
|
-
<td>${escapeHtml(w.title)}</td>
|
|
23789
|
-
<td>${statusBadge(w.status)}</td>
|
|
23790
|
-
<td>${progressCell}</td>
|
|
23791
|
-
</tr>`;
|
|
23792
|
-
const childRows = w.children ? renderItemRows(w.children, borderColor, depth + 1) : [];
|
|
23793
|
-
return [row, ...childRows];
|
|
23794
|
-
});
|
|
23795
|
-
}
|
|
23796
|
-
const allWorkItemRows = [];
|
|
23797
|
-
for (const [focus, items] of focusGroups) {
|
|
23798
|
-
const color = focusColorMap.get(focus);
|
|
23799
|
-
const stats = countFocusStats(items);
|
|
23800
|
-
const pct = stats.total > 0 ? Math.round(stats.done / stats.total * 100) : 0;
|
|
23801
|
-
const summaryParts = [];
|
|
23802
|
-
if (stats.done > 0) summaryParts.push(`${stats.done} done`);
|
|
23803
|
-
if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
|
|
23804
|
-
const remaining = stats.total - stats.done - stats.inProgress;
|
|
23805
|
-
if (remaining > 0) summaryParts.push(`${remaining} open`);
|
|
23806
|
-
allWorkItemRows.push(`
|
|
23807
|
-
<tr class="focus-group-header" style="--focus-color: ${color}">
|
|
23808
|
-
<td colspan="2">
|
|
23809
|
-
<span class="focus-group-name">${escapeHtml(focus)}</span>
|
|
23810
|
-
<span class="focus-group-stats">${summaryParts.join(" / ")}</span>
|
|
23811
|
-
</td>
|
|
23812
|
-
<td colspan="2">
|
|
23813
|
-
<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>
|
|
23814
|
-
</td>
|
|
23815
|
-
</tr>`);
|
|
23816
|
-
allWorkItemRows.push(...renderItemRows(items, color));
|
|
23817
|
-
}
|
|
23818
|
-
const tableHeaders = `<tr>
|
|
23819
|
-
<th>ID</th>
|
|
23820
|
-
<th>Title</th>
|
|
23821
|
-
<th>Status</th>
|
|
23822
|
-
<th>Progress</th>
|
|
23823
|
-
</tr>`;
|
|
23824
|
-
const workItemsSection = allWorkItemRows.length > 0 ? collapsibleSection(
|
|
23825
|
-
"ss-work-items",
|
|
23826
|
-
"Work Items",
|
|
23827
|
-
`<div class="table-wrap">
|
|
23828
|
-
<table id="work-items-table">
|
|
23829
|
-
<thead>
|
|
23830
|
-
${tableHeaders}
|
|
23831
|
-
</thead>
|
|
23832
|
-
<tbody>
|
|
23833
|
-
${allWorkItemRows.join("")}
|
|
23834
|
-
</tbody>
|
|
23835
|
-
</table>
|
|
23836
|
-
</div>`,
|
|
23837
|
-
{ titleTag: "h3", defaultCollapsed: true }
|
|
23838
|
-
) : "";
|
|
23839
|
-
const activitySection = data.artifacts.length > 0 ? collapsibleSection(
|
|
23840
|
-
"ss-activity",
|
|
23841
|
-
"Recent Activity",
|
|
24181
|
+
const actionsSection = data.openActions.length > 0 ? collapsibleSection(
|
|
24182
|
+
"dm-sprint-actions",
|
|
24183
|
+
`Open Actions (${data.openActions.length})`,
|
|
23842
24184
|
`<div class="table-wrap">
|
|
23843
24185
|
<table>
|
|
23844
24186
|
<thead>
|
|
23845
|
-
<tr><th>
|
|
24187
|
+
<tr><th>ID</th><th>Title</th><th>Owner</th><th>Due Date</th></tr>
|
|
23846
24188
|
</thead>
|
|
23847
24189
|
<tbody>
|
|
23848
|
-
${data.
|
|
24190
|
+
${data.openActions.map((a) => `
|
|
23849
24191
|
<tr>
|
|
23850
|
-
<td>${
|
|
23851
|
-
<td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
24192
|
+
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
23852
24193
|
<td>${escapeHtml(a.title)}</td>
|
|
23853
|
-
<td>${escapeHtml(
|
|
23854
|
-
<td>${
|
|
24194
|
+
<td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
24195
|
+
<td>${a.dueDate ? formatDate(a.dueDate) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23855
24196
|
</tr>`).join("")}
|
|
23856
24197
|
</tbody>
|
|
23857
24198
|
</table>
|
|
23858
24199
|
</div>`,
|
|
23859
|
-
{ titleTag: "h3"
|
|
24200
|
+
{ titleTag: "h3" }
|
|
23860
24201
|
) : "";
|
|
23861
24202
|
const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
|
|
23862
|
-
"
|
|
24203
|
+
"dm-sprint-meetings",
|
|
23863
24204
|
`Meetings (${data.meetings.length})`,
|
|
23864
24205
|
`<div class="table-wrap">
|
|
23865
24206
|
<table>
|
|
@@ -23878,79 +24219,23 @@ function sprintSummaryPage(data, cached2) {
|
|
|
23878
24219
|
</div>`,
|
|
23879
24220
|
{ titleTag: "h3", defaultCollapsed: true }
|
|
23880
24221
|
) : "";
|
|
23881
|
-
const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
|
|
23882
|
-
const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
|
|
23883
24222
|
return `
|
|
23884
24223
|
<div class="page-header">
|
|
23885
24224
|
<h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
|
|
23886
|
-
<div class="subtitle">Sprint
|
|
24225
|
+
<div class="subtitle">Sprint Execution ${dateRange}</div>
|
|
23887
24226
|
</div>
|
|
23888
24227
|
${goalHtml}
|
|
23889
24228
|
${progressBar3(data.timeline.percentComplete)}
|
|
23890
24229
|
${statsCards}
|
|
23891
|
-
${epicsTable}
|
|
23892
24230
|
${workItemsSection}
|
|
23893
|
-
${
|
|
24231
|
+
${epicsSection}
|
|
24232
|
+
${actionsSection}
|
|
23894
24233
|
${meetingsSection}
|
|
23895
|
-
|
|
23896
|
-
<div class="sprint-ai-section">
|
|
23897
|
-
<h3>AI Summary</h3>
|
|
23898
|
-
${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>`}
|
|
23899
|
-
<button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
|
|
23900
|
-
<div id="summary-loading" class="sprint-loading" style="display:none">
|
|
23901
|
-
<div class="sprint-spinner"></div>
|
|
23902
|
-
<span>Generating summary...</span>
|
|
23903
|
-
</div>
|
|
23904
|
-
<div id="summary-error" class="sprint-error" style="display:none"></div>
|
|
23905
|
-
<div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
|
|
23906
|
-
</div>
|
|
23907
|
-
|
|
23908
|
-
<script>
|
|
23909
|
-
async function generateSummary() {
|
|
23910
|
-
var btn = document.getElementById('generate-btn');
|
|
23911
|
-
var loading = document.getElementById('summary-loading');
|
|
23912
|
-
var errorEl = document.getElementById('summary-error');
|
|
23913
|
-
var content = document.getElementById('summary-content');
|
|
23914
|
-
|
|
23915
|
-
btn.disabled = true;
|
|
23916
|
-
btn.style.display = 'none';
|
|
23917
|
-
loading.style.display = 'flex';
|
|
23918
|
-
errorEl.style.display = 'none';
|
|
23919
|
-
content.style.display = 'none';
|
|
23920
|
-
|
|
23921
|
-
try {
|
|
23922
|
-
var res = await fetch('/api/sprint-summary', {
|
|
23923
|
-
method: 'POST',
|
|
23924
|
-
headers: { 'Content-Type': 'application/json' },
|
|
23925
|
-
body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
|
|
23926
|
-
});
|
|
23927
|
-
var json = await res.json();
|
|
23928
|
-
if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
|
|
23929
|
-
loading.style.display = 'none';
|
|
23930
|
-
content.innerHTML = json.html;
|
|
23931
|
-
content.style.display = 'block';
|
|
23932
|
-
btn.textContent = 'Regenerate';
|
|
23933
|
-
btn.style.display = '';
|
|
23934
|
-
btn.disabled = false;
|
|
23935
|
-
} catch (e) {
|
|
23936
|
-
loading.style.display = 'none';
|
|
23937
|
-
errorEl.textContent = e.message;
|
|
23938
|
-
errorEl.style.display = 'block';
|
|
23939
|
-
btn.style.display = '';
|
|
23940
|
-
btn.disabled = false;
|
|
23941
|
-
}
|
|
23942
|
-
}
|
|
23943
|
-
</script>`;
|
|
23944
|
-
}
|
|
23945
|
-
|
|
23946
|
-
// src/web/templates/pages/dm/sprint.ts
|
|
23947
|
-
function dmSprintPage(ctx) {
|
|
23948
|
-
const data = getSprintSummaryData(ctx.store);
|
|
23949
|
-
return sprintSummaryPage(data);
|
|
24234
|
+
`;
|
|
23950
24235
|
}
|
|
23951
24236
|
|
|
23952
24237
|
// src/web/templates/pages/dm/actions.ts
|
|
23953
|
-
var
|
|
24238
|
+
var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23954
24239
|
function urgencyBadge(tier) {
|
|
23955
24240
|
const labels = {
|
|
23956
24241
|
overdue: "Overdue",
|
|
@@ -23970,7 +24255,7 @@ function urgencyRowClass(tier) {
|
|
|
23970
24255
|
function dmActionsPage(ctx) {
|
|
23971
24256
|
const upcoming = getUpcomingData(ctx.store);
|
|
23972
24257
|
const allActions = ctx.store.list({ type: "action" });
|
|
23973
|
-
const openActions = allActions.filter((d) => !
|
|
24258
|
+
const openActions = allActions.filter((d) => !DONE_STATUSES9.has(d.frontmatter.status));
|
|
23974
24259
|
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
23975
24260
|
const dueThisWeek = upcoming.dueSoonActions.filter((a) => a.urgency === "due-3d" || a.urgency === "due-7d");
|
|
23976
24261
|
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
@@ -24055,7 +24340,7 @@ function dmActionsPage(ctx) {
|
|
|
24055
24340
|
}
|
|
24056
24341
|
|
|
24057
24342
|
// src/web/templates/pages/dm/risks.ts
|
|
24058
|
-
var
|
|
24343
|
+
var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24059
24344
|
function dmRisksPage(ctx) {
|
|
24060
24345
|
const allDocs = ctx.store.list();
|
|
24061
24346
|
const upcoming = getUpcomingData(ctx.store);
|
|
@@ -24066,7 +24351,7 @@ function dmRisksPage(ctx) {
|
|
|
24066
24351
|
const todayMs = new Date(today).getTime();
|
|
24067
24352
|
const fourteenDaysMs = 14 * 864e5;
|
|
24068
24353
|
const agingItems = allDocs.filter((d) => {
|
|
24069
|
-
if (
|
|
24354
|
+
if (DONE_STATUSES10.has(d.frontmatter.status)) return false;
|
|
24070
24355
|
if (!["action", "question"].includes(d.frontmatter.type)) return false;
|
|
24071
24356
|
const createdMs = new Date(d.frontmatter.created).getTime();
|
|
24072
24357
|
return todayMs - createdMs > fourteenDaysMs;
|
|
@@ -24180,7 +24465,7 @@ function dmRisksPage(ctx) {
|
|
|
24180
24465
|
}
|
|
24181
24466
|
|
|
24182
24467
|
// src/web/templates/pages/dm/meetings.ts
|
|
24183
|
-
var
|
|
24468
|
+
var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24184
24469
|
function dmMeetingsPage(ctx) {
|
|
24185
24470
|
const meetings = ctx.store.list({ type: "meeting" });
|
|
24186
24471
|
const actions = ctx.store.list({ type: "action" });
|
|
@@ -24226,7 +24511,7 @@ function dmMeetingsPage(ctx) {
|
|
|
24226
24511
|
${sortedMeetings.map((m) => {
|
|
24227
24512
|
const date5 = m.frontmatter.date ?? m.frontmatter.created;
|
|
24228
24513
|
const relatedActions = meetingActionMap.get(m.frontmatter.id) ?? [];
|
|
24229
|
-
const openCount = relatedActions.filter((a) => !
|
|
24514
|
+
const openCount = relatedActions.filter((a) => !DONE_STATUSES11.has(a.frontmatter.status)).length;
|
|
24230
24515
|
return `
|
|
24231
24516
|
<tr>
|
|
24232
24517
|
<td>${formatDate(date5)}</td>
|
|
@@ -24241,7 +24526,7 @@ function dmMeetingsPage(ctx) {
|
|
|
24241
24526
|
const recentMeetingActions = [];
|
|
24242
24527
|
for (const [mid, acts] of meetingActionMap) {
|
|
24243
24528
|
for (const act of acts) {
|
|
24244
|
-
if (!
|
|
24529
|
+
if (!DONE_STATUSES11.has(act.frontmatter.status)) {
|
|
24245
24530
|
recentMeetingActions.push({ action: act, meetingId: mid });
|
|
24246
24531
|
}
|
|
24247
24532
|
}
|
|
@@ -24436,16 +24721,16 @@ registerPersonaPage("dm", "meetings", dmMeetingsPage);
|
|
|
24436
24721
|
registerPersonaPage("dm", "governance", dmGovernancePage);
|
|
24437
24722
|
|
|
24438
24723
|
// src/web/templates/pages/tl/dashboard.ts
|
|
24439
|
-
var
|
|
24440
|
-
var
|
|
24724
|
+
var DONE_STATUSES12 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24725
|
+
var RESOLVED_DECISION_STATUSES2 = /* @__PURE__ */ new Set(["decided", "superseded", "dismissed"]);
|
|
24441
24726
|
function tlDashboardPage(ctx) {
|
|
24442
24727
|
const epics = ctx.store.list({ type: "epic" });
|
|
24443
24728
|
const tasks = ctx.store.list({ type: "task" });
|
|
24444
24729
|
const decisions = ctx.store.list({ type: "decision" });
|
|
24445
24730
|
const questions = ctx.store.list({ type: "question" });
|
|
24446
24731
|
const diagrams = getDiagramData(ctx.store);
|
|
24447
|
-
const openEpics = epics.filter((d) => !
|
|
24448
|
-
const openTasks = tasks.filter((d) => !
|
|
24732
|
+
const openEpics = epics.filter((d) => !DONE_STATUSES12.has(d.frontmatter.status));
|
|
24733
|
+
const openTasks = tasks.filter((d) => !DONE_STATUSES12.has(d.frontmatter.status));
|
|
24449
24734
|
const technicalDecisions = decisions.filter((d) => {
|
|
24450
24735
|
const tags = d.frontmatter.tags ?? [];
|
|
24451
24736
|
return tags.some((t) => {
|
|
@@ -24454,7 +24739,7 @@ function tlDashboardPage(ctx) {
|
|
|
24454
24739
|
});
|
|
24455
24740
|
});
|
|
24456
24741
|
const displayDecisions = technicalDecisions.length > 0 ? technicalDecisions : decisions;
|
|
24457
|
-
const pendingDecisions = displayDecisions.filter((d) => !
|
|
24742
|
+
const pendingDecisions = displayDecisions.filter((d) => !RESOLVED_DECISION_STATUSES2.has(d.frontmatter.status));
|
|
24458
24743
|
const statsCards = `
|
|
24459
24744
|
<div class="cards">
|
|
24460
24745
|
<div class="card">
|
|
@@ -24503,7 +24788,7 @@ function tlDashboardPage(ctx) {
|
|
|
24503
24788
|
}
|
|
24504
24789
|
|
|
24505
24790
|
// src/web/templates/pages/tl/backlog.ts
|
|
24506
|
-
var
|
|
24791
|
+
var DONE_STATUSES13 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24507
24792
|
function tlBacklogPage(ctx) {
|
|
24508
24793
|
const epics = ctx.store.list({ type: "epic" });
|
|
24509
24794
|
const tasks = ctx.store.list({ type: "task" });
|
|
@@ -24525,10 +24810,10 @@ function tlBacklogPage(ctx) {
|
|
|
24525
24810
|
const featureIds = Array.isArray(linked) ? linked.map(String) : linked ? [String(linked)] : [];
|
|
24526
24811
|
epicFeatureMap.set(epic.frontmatter.id, featureIds);
|
|
24527
24812
|
}
|
|
24528
|
-
const
|
|
24813
|
+
const statusOrder2 = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
24529
24814
|
const sortedEpics = [...epics].sort((a, b) => {
|
|
24530
|
-
const sa =
|
|
24531
|
-
const sb =
|
|
24815
|
+
const sa = statusOrder2[a.frontmatter.status] ?? 3;
|
|
24816
|
+
const sb = statusOrder2[b.frontmatter.status] ?? 3;
|
|
24532
24817
|
if (sa !== sb) return sa - sb;
|
|
24533
24818
|
return a.frontmatter.id.localeCompare(b.frontmatter.id);
|
|
24534
24819
|
});
|
|
@@ -24540,7 +24825,7 @@ function tlBacklogPage(ctx) {
|
|
|
24540
24825
|
<tbody>
|
|
24541
24826
|
${sortedEpics.map((e) => {
|
|
24542
24827
|
const eTasks = epicToTasks.get(e.frontmatter.id) ?? [];
|
|
24543
|
-
const done = eTasks.filter((t) =>
|
|
24828
|
+
const done = eTasks.filter((t) => DONE_STATUSES13.has(t.frontmatter.status)).length;
|
|
24544
24829
|
const featureIds = epicFeatureMap.get(e.frontmatter.id) ?? [];
|
|
24545
24830
|
const featureLinks = featureIds.map((fid) => `<a href="/docs/feature/${escapeHtml(fid)}">${escapeHtml(fid)}</a>`).join(", ");
|
|
24546
24831
|
return `
|
|
@@ -24560,7 +24845,7 @@ function tlBacklogPage(ctx) {
|
|
|
24560
24845
|
for (const t of taskList) assignedTaskIds.add(t.frontmatter.id);
|
|
24561
24846
|
}
|
|
24562
24847
|
const unassignedTasks = tasks.filter(
|
|
24563
|
-
(t) => !assignedTaskIds.has(t.frontmatter.id) && !
|
|
24848
|
+
(t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES13.has(t.frontmatter.status)
|
|
24564
24849
|
);
|
|
24565
24850
|
const unassignedSection = unassignedTasks.length > 0 ? collapsibleSection(
|
|
24566
24851
|
"tl-backlog-unassigned",
|
|
@@ -24621,7 +24906,6 @@ var TL_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
|
|
|
24621
24906
|
"technical-assessment",
|
|
24622
24907
|
"architecture-review"
|
|
24623
24908
|
]);
|
|
24624
|
-
var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
24625
24909
|
function progressBar4(pct) {
|
|
24626
24910
|
return `<div class="sprint-progress-bar">
|
|
24627
24911
|
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
@@ -24641,25 +24925,8 @@ function tlSprintPage(ctx) {
|
|
|
24641
24925
|
<p>No active sprint found. Create a sprint and set its status to "active" to track sprint work.</p>
|
|
24642
24926
|
</div>`;
|
|
24643
24927
|
}
|
|
24644
|
-
const
|
|
24645
|
-
const
|
|
24646
|
-
for (const item of data.workItems.items) {
|
|
24647
|
-
if (techTypes.has(item.type)) {
|
|
24648
|
-
techItems.push(item);
|
|
24649
|
-
} else if (item.children) {
|
|
24650
|
-
const promoteChildren = (children) => {
|
|
24651
|
-
for (const child of children) {
|
|
24652
|
-
if (techTypes.has(child.type)) {
|
|
24653
|
-
techItems.push(child);
|
|
24654
|
-
} else if (child.children) {
|
|
24655
|
-
promoteChildren(child.children);
|
|
24656
|
-
}
|
|
24657
|
-
}
|
|
24658
|
-
};
|
|
24659
|
-
promoteChildren(item.children);
|
|
24660
|
-
}
|
|
24661
|
-
}
|
|
24662
|
-
const techDone = techItems.filter((w) => DONE_STATUSES11.has(w.status)).length;
|
|
24928
|
+
const tlItems = filterItemsByOwner(data.workItems.items, "tl");
|
|
24929
|
+
const tlCompletionPct = computeOwnerCompletionPct(data.workItems.items, "tl");
|
|
24663
24930
|
const allDocs = ctx.store.list();
|
|
24664
24931
|
const tlContributions = allDocs.filter((d) => TL_CONTRIBUTION_TYPES.has(d.frontmatter.type));
|
|
24665
24932
|
const statsCards = `
|
|
@@ -24670,9 +24937,9 @@ function tlSprintPage(ctx) {
|
|
|
24670
24937
|
<div class="card-sub">${data.timeline.daysRemaining} days remaining</div>
|
|
24671
24938
|
</div>
|
|
24672
24939
|
<div class="card">
|
|
24673
|
-
<div class="card-label">
|
|
24674
|
-
<div class="card-value">${
|
|
24675
|
-
<div class="card-sub">${
|
|
24940
|
+
<div class="card-label">TL Completion</div>
|
|
24941
|
+
<div class="card-value">${tlCompletionPct}%</div>
|
|
24942
|
+
<div class="card-sub">${tlItems.length} owned items</div>
|
|
24676
24943
|
</div>
|
|
24677
24944
|
<div class="card">
|
|
24678
24945
|
<div class="card-label">Epics</div>
|
|
@@ -24690,28 +24957,10 @@ function tlSprintPage(ctx) {
|
|
|
24690
24957
|
<strong>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</strong>
|
|
24691
24958
|
${data.sprint.goal ? ` | ${escapeHtml(data.sprint.goal)}` : ""}
|
|
24692
24959
|
</div>`;
|
|
24693
|
-
const workItemsSection =
|
|
24694
|
-
"tl-sprint-items",
|
|
24695
|
-
|
|
24696
|
-
|
|
24697
|
-
<table>
|
|
24698
|
-
<thead>
|
|
24699
|
-
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Focus</th></tr>
|
|
24700
|
-
</thead>
|
|
24701
|
-
<tbody>
|
|
24702
|
-
${techItems.map((w) => `
|
|
24703
|
-
<tr>
|
|
24704
|
-
<td><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
24705
|
-
<td>${escapeHtml(w.title)}</td>
|
|
24706
|
-
<td>${escapeHtml(typeLabel(w.type))}</td>
|
|
24707
|
-
<td>${statusBadge(w.status)}</td>
|
|
24708
|
-
<td>${w.workFocus ? `<span class="badge badge-subtle">${escapeHtml(w.workFocus)}</span>` : '<span class="text-dim">\u2014</span>'}</td>
|
|
24709
|
-
</tr>`).join("")}
|
|
24710
|
-
</tbody>
|
|
24711
|
-
</table>
|
|
24712
|
-
</div>`,
|
|
24713
|
-
{ titleTag: "h3" }
|
|
24714
|
-
) : "";
|
|
24960
|
+
const workItemsSection = renderWorkItemsTable(tlItems, {
|
|
24961
|
+
sectionId: "tl-sprint-items",
|
|
24962
|
+
title: "TL Work Items"
|
|
24963
|
+
});
|
|
24715
24964
|
const contributionsSection = tlContributions.length > 0 ? collapsibleSection(
|
|
24716
24965
|
"tl-sprint-contributions",
|
|
24717
24966
|
`TL Contributions (${tlContributions.length})`,
|
|
@@ -25095,6 +25344,186 @@ function upcomingPage(data) {
|
|
|
25095
25344
|
`;
|
|
25096
25345
|
}
|
|
25097
25346
|
|
|
25347
|
+
// src/web/templates/pages/sprint-summary.ts
|
|
25348
|
+
function progressBar5(pct) {
|
|
25349
|
+
return `<div class="sprint-progress-bar">
|
|
25350
|
+
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
25351
|
+
<span class="sprint-progress-label">${pct}%</span>
|
|
25352
|
+
</div>`;
|
|
25353
|
+
}
|
|
25354
|
+
function sprintSummaryPage(data, cached2) {
|
|
25355
|
+
if (!data) {
|
|
25356
|
+
return `
|
|
25357
|
+
<div class="page-header">
|
|
25358
|
+
<h2>Sprint Summary</h2>
|
|
25359
|
+
<div class="subtitle">AI-powered sprint narrative</div>
|
|
25360
|
+
</div>
|
|
25361
|
+
<div class="empty">
|
|
25362
|
+
<h3>No Active Sprint</h3>
|
|
25363
|
+
<p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
|
|
25364
|
+
</div>`;
|
|
25365
|
+
}
|
|
25366
|
+
const statsCards = `
|
|
25367
|
+
<div class="cards">
|
|
25368
|
+
<div class="card">
|
|
25369
|
+
<div class="card-label">Completion</div>
|
|
25370
|
+
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
25371
|
+
<div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
|
|
25372
|
+
</div>
|
|
25373
|
+
<div class="card">
|
|
25374
|
+
<div class="card-label">Days Remaining</div>
|
|
25375
|
+
<div class="card-value">${data.timeline.daysRemaining}</div>
|
|
25376
|
+
<div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
|
|
25377
|
+
</div>
|
|
25378
|
+
<div class="card">
|
|
25379
|
+
<div class="card-label">Epics</div>
|
|
25380
|
+
<div class="card-value">${data.linkedEpics.length}</div>
|
|
25381
|
+
<div class="card-sub">linked to sprint</div>
|
|
25382
|
+
</div>
|
|
25383
|
+
<a class="card card-link" href="sprint-blockers">
|
|
25384
|
+
<div class="card-label">Blockers</div>
|
|
25385
|
+
<div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
|
|
25386
|
+
<div class="card-sub">${data.workItems.blocked} blocked items</div>
|
|
25387
|
+
</a>
|
|
25388
|
+
<a class="card card-link" href="sprint-risks">
|
|
25389
|
+
<div class="card-label">Risks</div>
|
|
25390
|
+
<div class="card-value${data.risks.length > 0 ? " priority-medium" : ""}">${data.risks.length}</div>
|
|
25391
|
+
<div class="card-sub">open risk items</div>
|
|
25392
|
+
</a>
|
|
25393
|
+
</div>`;
|
|
25394
|
+
const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
25395
|
+
"ss-epics",
|
|
25396
|
+
"Linked Epics",
|
|
25397
|
+
`<div class="table-wrap">
|
|
25398
|
+
<table>
|
|
25399
|
+
<thead>
|
|
25400
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
|
|
25401
|
+
</thead>
|
|
25402
|
+
<tbody>
|
|
25403
|
+
${data.linkedEpics.map((e) => `
|
|
25404
|
+
<tr>
|
|
25405
|
+
<td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
|
|
25406
|
+
<td>${escapeHtml(e.title)}</td>
|
|
25407
|
+
<td>${statusBadge(e.status)}</td>
|
|
25408
|
+
<td>${e.tasksDone} / ${e.tasksTotal}</td>
|
|
25409
|
+
</tr>`).join("")}
|
|
25410
|
+
</tbody>
|
|
25411
|
+
</table>
|
|
25412
|
+
</div>`,
|
|
25413
|
+
{ titleTag: "h3" }
|
|
25414
|
+
) : "";
|
|
25415
|
+
const workItemsSection = renderWorkItemsTable(data.workItems.items, {
|
|
25416
|
+
sectionId: "ss-work-items",
|
|
25417
|
+
title: "Work Items",
|
|
25418
|
+
defaultCollapsed: true
|
|
25419
|
+
});
|
|
25420
|
+
const activitySection = data.artifacts.length > 0 ? collapsibleSection(
|
|
25421
|
+
"ss-activity",
|
|
25422
|
+
"Recent Activity",
|
|
25423
|
+
`<div class="table-wrap">
|
|
25424
|
+
<table>
|
|
25425
|
+
<thead>
|
|
25426
|
+
<tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
|
|
25427
|
+
</thead>
|
|
25428
|
+
<tbody>
|
|
25429
|
+
${data.artifacts.slice(0, 15).map((a) => `
|
|
25430
|
+
<tr>
|
|
25431
|
+
<td>${formatDate(a.date)}</td>
|
|
25432
|
+
<td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
25433
|
+
<td>${escapeHtml(a.title)}</td>
|
|
25434
|
+
<td>${escapeHtml(typeLabel(a.type))}</td>
|
|
25435
|
+
<td>${escapeHtml(a.action)}</td>
|
|
25436
|
+
</tr>`).join("")}
|
|
25437
|
+
</tbody>
|
|
25438
|
+
</table>
|
|
25439
|
+
</div>`,
|
|
25440
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
25441
|
+
) : "";
|
|
25442
|
+
const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
|
|
25443
|
+
"ss-meetings",
|
|
25444
|
+
`Meetings (${data.meetings.length})`,
|
|
25445
|
+
`<div class="table-wrap">
|
|
25446
|
+
<table>
|
|
25447
|
+
<thead>
|
|
25448
|
+
<tr><th>Date</th><th>ID</th><th>Title</th></tr>
|
|
25449
|
+
</thead>
|
|
25450
|
+
<tbody>
|
|
25451
|
+
${data.meetings.map((m) => `
|
|
25452
|
+
<tr>
|
|
25453
|
+
<td>${formatDate(m.date)}</td>
|
|
25454
|
+
<td><a href="/docs/meeting/${escapeHtml(m.id)}">${escapeHtml(m.id)}</a></td>
|
|
25455
|
+
<td>${escapeHtml(m.title)}</td>
|
|
25456
|
+
</tr>`).join("")}
|
|
25457
|
+
</tbody>
|
|
25458
|
+
</table>
|
|
25459
|
+
</div>`,
|
|
25460
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
25461
|
+
) : "";
|
|
25462
|
+
const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
|
|
25463
|
+
const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
|
|
25464
|
+
return `
|
|
25465
|
+
<div class="page-header">
|
|
25466
|
+
<h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
|
|
25467
|
+
<div class="subtitle">Sprint Summary ${dateRange}</div>
|
|
25468
|
+
</div>
|
|
25469
|
+
${goalHtml}
|
|
25470
|
+
${progressBar5(data.timeline.percentComplete)}
|
|
25471
|
+
${statsCards}
|
|
25472
|
+
${epicsTable}
|
|
25473
|
+
${workItemsSection}
|
|
25474
|
+
${activitySection}
|
|
25475
|
+
${meetingsSection}
|
|
25476
|
+
|
|
25477
|
+
<div class="sprint-ai-section">
|
|
25478
|
+
<h3>AI Summary</h3>
|
|
25479
|
+
${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>`}
|
|
25480
|
+
<button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
|
|
25481
|
+
<div id="summary-loading" class="sprint-loading" style="display:none">
|
|
25482
|
+
<div class="sprint-spinner"></div>
|
|
25483
|
+
<span>Generating summary...</span>
|
|
25484
|
+
</div>
|
|
25485
|
+
<div id="summary-error" class="sprint-error" style="display:none"></div>
|
|
25486
|
+
<div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
|
|
25487
|
+
</div>
|
|
25488
|
+
|
|
25489
|
+
<script>
|
|
25490
|
+
async function generateSummary() {
|
|
25491
|
+
var btn = document.getElementById('generate-btn');
|
|
25492
|
+
var loading = document.getElementById('summary-loading');
|
|
25493
|
+
var errorEl = document.getElementById('summary-error');
|
|
25494
|
+
var content = document.getElementById('summary-content');
|
|
25495
|
+
|
|
25496
|
+
btn.disabled = true;
|
|
25497
|
+
btn.style.display = 'none';
|
|
25498
|
+
loading.style.display = 'flex';
|
|
25499
|
+
errorEl.style.display = 'none';
|
|
25500
|
+
content.style.display = 'none';
|
|
25501
|
+
|
|
25502
|
+
try {
|
|
25503
|
+
var res = await fetch('/api/sprint-summary', {
|
|
25504
|
+
method: 'POST',
|
|
25505
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25506
|
+
body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
|
|
25507
|
+
});
|
|
25508
|
+
var json = await res.json();
|
|
25509
|
+
if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
|
|
25510
|
+
loading.style.display = 'none';
|
|
25511
|
+
content.innerHTML = json.html;
|
|
25512
|
+
content.style.display = 'block';
|
|
25513
|
+
btn.textContent = 'Regenerate';
|
|
25514
|
+
btn.style.display = '';
|
|
25515
|
+
btn.disabled = false;
|
|
25516
|
+
} catch (e) {
|
|
25517
|
+
loading.style.display = 'none';
|
|
25518
|
+
errorEl.textContent = e.message;
|
|
25519
|
+
errorEl.style.display = 'block';
|
|
25520
|
+
btn.style.display = '';
|
|
25521
|
+
btn.disabled = false;
|
|
25522
|
+
}
|
|
25523
|
+
}
|
|
25524
|
+
</script>`;
|
|
25525
|
+
}
|
|
25526
|
+
|
|
25098
25527
|
// src/web/templates/pages/sprint-blockers.ts
|
|
25099
25528
|
function sprintBlockersPage(data, store) {
|
|
25100
25529
|
if (!data) {
|