mrvn-cli 0.4.9 → 0.4.11
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 +2040 -55
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +2281 -296
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +2040 -55
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin-serve.js
CHANGED
|
@@ -6474,13 +6474,13 @@ var error16 = () => {
|
|
|
6474
6474
|
// no unit
|
|
6475
6475
|
};
|
|
6476
6476
|
const typeEntry = (t) => t ? TypeNames[t] : void 0;
|
|
6477
|
-
const
|
|
6477
|
+
const typeLabel5 = (t) => {
|
|
6478
6478
|
const e = typeEntry(t);
|
|
6479
6479
|
if (e)
|
|
6480
6480
|
return e.label;
|
|
6481
6481
|
return t ?? TypeNames.unknown.label;
|
|
6482
6482
|
};
|
|
6483
|
-
const withDefinite = (t) => `\u05D4${
|
|
6483
|
+
const withDefinite = (t) => `\u05D4${typeLabel5(t)}`;
|
|
6484
6484
|
const verbFor = (t) => {
|
|
6485
6485
|
const e = typeEntry(t);
|
|
6486
6486
|
const gender = e?.gender ?? "m";
|
|
@@ -6530,7 +6530,7 @@ var error16 = () => {
|
|
|
6530
6530
|
switch (issue2.code) {
|
|
6531
6531
|
case "invalid_type": {
|
|
6532
6532
|
const expectedKey = issue2.expected;
|
|
6533
|
-
const expected = TypeDictionary[expectedKey ?? ""] ??
|
|
6533
|
+
const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel5(expectedKey);
|
|
6534
6534
|
const receivedType = parsedType(issue2.input);
|
|
6535
6535
|
const received = TypeDictionary[receivedType] ?? TypeNames[receivedType]?.label ?? receivedType;
|
|
6536
6536
|
if (/^[A-Z]/.test(issue2.expected)) {
|
|
@@ -15580,7 +15580,7 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15580
15580
|
});
|
|
15581
15581
|
const sprintTag = `sprint:${fm.id}`;
|
|
15582
15582
|
const workItemDocs = allDocs.filter(
|
|
15583
|
-
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
|
|
15583
|
+
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "meeting" && d.frontmatter.tags?.includes(sprintTag)
|
|
15584
15584
|
);
|
|
15585
15585
|
const primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
|
|
15586
15586
|
const byStatus = {};
|
|
@@ -15769,12 +15769,16 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15769
15769
|
|
|
15770
15770
|
// src/reports/sprint-summary/generator.ts
|
|
15771
15771
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
15772
|
-
async function generateSprintSummary(data) {
|
|
15772
|
+
async function generateSprintSummary(data, personaSystemPrompt) {
|
|
15773
15773
|
const prompt = buildPrompt(data);
|
|
15774
|
+
const systemPrompt = personaSystemPrompt ? `${SYSTEM_PROMPT}
|
|
15775
|
+
|
|
15776
|
+
Additional persona context:
|
|
15777
|
+
${personaSystemPrompt}` : SYSTEM_PROMPT;
|
|
15774
15778
|
const result = query({
|
|
15775
15779
|
prompt,
|
|
15776
15780
|
options: {
|
|
15777
|
-
systemPrompt
|
|
15781
|
+
systemPrompt,
|
|
15778
15782
|
maxTurns: 1,
|
|
15779
15783
|
tools: [],
|
|
15780
15784
|
allowedTools: []
|
|
@@ -19917,17 +19921,27 @@ function layout(opts, body) {
|
|
|
19917
19921
|
{ href: "/health", label: "Health" }
|
|
19918
19922
|
];
|
|
19919
19923
|
const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
|
|
19920
|
-
const
|
|
19921
|
-
|
|
19922
|
-
|
|
19923
|
-
|
|
19924
|
-
|
|
19925
|
-
|
|
19924
|
+
const switcherHtml = opts.personaSwitcherHtml ?? "";
|
|
19925
|
+
let navHtml;
|
|
19926
|
+
if (opts.personaNavHtml) {
|
|
19927
|
+
navHtml = opts.personaNavHtml;
|
|
19928
|
+
} else {
|
|
19929
|
+
const groupsHtml = opts.navGroups.map((group) => {
|
|
19930
|
+
const links = group.types.map((type) => {
|
|
19931
|
+
const href = `/docs/${type}`;
|
|
19932
|
+
return `<a href="${href}" class="${isActive(href)}">${typeLabel(type)}s</a>`;
|
|
19933
|
+
}).join("\n ");
|
|
19934
|
+
return `
|
|
19926
19935
|
<div class="nav-group">
|
|
19927
19936
|
<div class="nav-group-label">${escapeHtml(group.label)}</div>
|
|
19928
19937
|
${links}
|
|
19929
19938
|
</div>`;
|
|
19930
|
-
|
|
19939
|
+
}).join("\n");
|
|
19940
|
+
navHtml = `
|
|
19941
|
+
${topItems.map((n) => `<a href="${n.href}" class="${isActive(n.href)}">${n.label}</a>`).join("\n ")}
|
|
19942
|
+
${groupsHtml}`;
|
|
19943
|
+
}
|
|
19944
|
+
const accentOverride = opts.personaAccentColor ? ` style="--persona-accent: ${opts.personaAccentColor}"` : "";
|
|
19931
19945
|
return `<!DOCTYPE html>
|
|
19932
19946
|
<html lang="en">
|
|
19933
19947
|
<head>
|
|
@@ -19937,15 +19951,15 @@ function layout(opts, body) {
|
|
|
19937
19951
|
<link rel="stylesheet" href="/styles.css">
|
|
19938
19952
|
</head>
|
|
19939
19953
|
<body>
|
|
19940
|
-
<div class="shell">
|
|
19954
|
+
<div class="shell"${accentOverride}>
|
|
19941
19955
|
<aside class="sidebar">
|
|
19942
19956
|
<div class="sidebar-brand">
|
|
19943
19957
|
<h1>Marvin</h1>
|
|
19944
19958
|
<div class="project-name">${escapeHtml(opts.projectName)}</div>
|
|
19945
19959
|
</div>
|
|
19960
|
+
${switcherHtml}
|
|
19946
19961
|
<nav>
|
|
19947
|
-
${
|
|
19948
|
-
${groupsHtml}
|
|
19962
|
+
${navHtml}
|
|
19949
19963
|
</nav>
|
|
19950
19964
|
</aside>
|
|
19951
19965
|
<main class="main${opts.mainClass ? ` ${opts.mainClass}` : ""}">
|
|
@@ -19953,7 +19967,7 @@ function layout(opts, body) {
|
|
|
19953
19967
|
<svg class="icon-expand" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M1 1h5v1.5H3.56l3.72 3.72-1.06 1.06L2.5 3.56V6H1V1zm14 14h-5v-1.5h2.44l-3.72-3.72 1.06-1.06 3.72 3.72V10H15v5z"/></svg>
|
|
19954
19968
|
<svg class="icon-collapse" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M6 7H1V5.5h2.44L0.22 2.28l1.06-1.06L4.5 4.44V2H6v5zm4-1h5v1.5h-2.44l3.22 3.22-1.06 1.06L11.5 8.56V11H10V6z"/></svg>
|
|
19955
19969
|
</button>
|
|
19956
|
-
${body}
|
|
19970
|
+
${opts.bodyPrefix ?? ""}${body}
|
|
19957
19971
|
</main>
|
|
19958
19972
|
</div>
|
|
19959
19973
|
<script>
|
|
@@ -20255,6 +20269,12 @@ a:hover { text-decoration: underline; }
|
|
|
20255
20269
|
.badge-closed, .badge-resolved { background: rgba(52, 211, 153, 0.15); color: var(--green); }
|
|
20256
20270
|
.badge-blocked { background: rgba(248, 113, 113, 0.15); color: var(--red); }
|
|
20257
20271
|
.badge-default { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
|
|
20272
|
+
.badge-subtle {
|
|
20273
|
+
background: rgba(139, 143, 164, 0.12);
|
|
20274
|
+
color: var(--text-dim);
|
|
20275
|
+
text-transform: none;
|
|
20276
|
+
font-weight: 500;
|
|
20277
|
+
}
|
|
20258
20278
|
|
|
20259
20279
|
/* Table */
|
|
20260
20280
|
.table-wrap {
|
|
@@ -20932,6 +20952,142 @@ tr:hover td {
|
|
|
20932
20952
|
|
|
20933
20953
|
.text-dim { color: var(--text-dim); }
|
|
20934
20954
|
|
|
20955
|
+
/* Persona switcher */
|
|
20956
|
+
.persona-switcher {
|
|
20957
|
+
padding: 0.5rem 1.25rem 0.75rem;
|
|
20958
|
+
border-bottom: 1px solid var(--border);
|
|
20959
|
+
margin-bottom: 0.5rem;
|
|
20960
|
+
display: flex;
|
|
20961
|
+
align-items: center;
|
|
20962
|
+
gap: 0.5rem;
|
|
20963
|
+
}
|
|
20964
|
+
|
|
20965
|
+
.persona-label {
|
|
20966
|
+
font-size: 0.65rem;
|
|
20967
|
+
text-transform: uppercase;
|
|
20968
|
+
letter-spacing: 0.06em;
|
|
20969
|
+
color: var(--text-dim);
|
|
20970
|
+
font-weight: 600;
|
|
20971
|
+
}
|
|
20972
|
+
|
|
20973
|
+
.persona-select {
|
|
20974
|
+
flex: 1;
|
|
20975
|
+
background: var(--bg);
|
|
20976
|
+
border: 1px solid var(--border);
|
|
20977
|
+
color: var(--text);
|
|
20978
|
+
padding: 0.3rem 0.5rem;
|
|
20979
|
+
border-radius: var(--radius);
|
|
20980
|
+
font-size: 0.8rem;
|
|
20981
|
+
cursor: pointer;
|
|
20982
|
+
font-family: var(--font);
|
|
20983
|
+
}
|
|
20984
|
+
|
|
20985
|
+
.persona-select:focus {
|
|
20986
|
+
outline: none;
|
|
20987
|
+
border-color: var(--persona-accent, var(--accent));
|
|
20988
|
+
}
|
|
20989
|
+
|
|
20990
|
+
/* Persona banner (first-visit picker) */
|
|
20991
|
+
.persona-banner {
|
|
20992
|
+
background: var(--bg-card);
|
|
20993
|
+
border: 1px solid var(--border);
|
|
20994
|
+
border-radius: var(--radius);
|
|
20995
|
+
padding: 1.5rem;
|
|
20996
|
+
margin-bottom: 2rem;
|
|
20997
|
+
}
|
|
20998
|
+
|
|
20999
|
+
.persona-banner-header {
|
|
21000
|
+
display: flex;
|
|
21001
|
+
align-items: center;
|
|
21002
|
+
justify-content: space-between;
|
|
21003
|
+
margin-bottom: 0.25rem;
|
|
21004
|
+
}
|
|
21005
|
+
|
|
21006
|
+
.persona-banner-header h3 {
|
|
21007
|
+
font-size: 1.1rem;
|
|
21008
|
+
font-weight: 600;
|
|
21009
|
+
}
|
|
21010
|
+
|
|
21011
|
+
.persona-banner-dismiss {
|
|
21012
|
+
background: none;
|
|
21013
|
+
border: none;
|
|
21014
|
+
color: var(--text-dim);
|
|
21015
|
+
font-size: 1.25rem;
|
|
21016
|
+
cursor: pointer;
|
|
21017
|
+
padding: 0.25rem;
|
|
21018
|
+
line-height: 1;
|
|
21019
|
+
}
|
|
21020
|
+
|
|
21021
|
+
.persona-banner-dismiss:hover {
|
|
21022
|
+
color: var(--text);
|
|
21023
|
+
}
|
|
21024
|
+
|
|
21025
|
+
.persona-banner-subtitle {
|
|
21026
|
+
color: var(--text-dim);
|
|
21027
|
+
font-size: 0.85rem;
|
|
21028
|
+
margin-bottom: 1rem;
|
|
21029
|
+
}
|
|
21030
|
+
|
|
21031
|
+
.persona-banner-options {
|
|
21032
|
+
display: grid;
|
|
21033
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
21034
|
+
gap: 0.75rem;
|
|
21035
|
+
}
|
|
21036
|
+
|
|
21037
|
+
.persona-banner-option {
|
|
21038
|
+
display: block;
|
|
21039
|
+
background: var(--bg);
|
|
21040
|
+
border: 1px solid var(--border);
|
|
21041
|
+
border-radius: var(--radius);
|
|
21042
|
+
padding: 1rem;
|
|
21043
|
+
text-decoration: none;
|
|
21044
|
+
color: inherit;
|
|
21045
|
+
transition: border-color 0.15s, background 0.15s;
|
|
21046
|
+
border-left: 3px solid var(--persona-card-accent, var(--accent));
|
|
21047
|
+
}
|
|
21048
|
+
|
|
21049
|
+
.persona-banner-option:hover {
|
|
21050
|
+
border-color: var(--persona-card-accent, var(--accent));
|
|
21051
|
+
background: var(--bg-hover);
|
|
21052
|
+
text-decoration: none;
|
|
21053
|
+
}
|
|
21054
|
+
|
|
21055
|
+
.persona-banner-name {
|
|
21056
|
+
font-weight: 600;
|
|
21057
|
+
font-size: 0.95rem;
|
|
21058
|
+
margin-bottom: 0.25rem;
|
|
21059
|
+
}
|
|
21060
|
+
|
|
21061
|
+
.persona-banner-desc {
|
|
21062
|
+
font-size: 0.8rem;
|
|
21063
|
+
color: var(--text-dim);
|
|
21064
|
+
}
|
|
21065
|
+
|
|
21066
|
+
/* Persona accent override */
|
|
21067
|
+
.shell[style*="--persona-accent"] .sidebar nav a.active {
|
|
21068
|
+
color: var(--persona-accent);
|
|
21069
|
+
background: rgba(108, 140, 255, 0.08);
|
|
21070
|
+
border-right-color: var(--persona-accent);
|
|
21071
|
+
}
|
|
21072
|
+
|
|
21073
|
+
.shell[style*="--persona-accent"] .sidebar-brand h1 {
|
|
21074
|
+
color: var(--persona-accent);
|
|
21075
|
+
}
|
|
21076
|
+
|
|
21077
|
+
/* Persona page placeholder */
|
|
21078
|
+
.persona-placeholder {
|
|
21079
|
+
text-align: center;
|
|
21080
|
+
padding: 3rem;
|
|
21081
|
+
color: var(--text-dim);
|
|
21082
|
+
}
|
|
21083
|
+
|
|
21084
|
+
.persona-placeholder h3 {
|
|
21085
|
+
font-size: 1.1rem;
|
|
21086
|
+
font-weight: 600;
|
|
21087
|
+
margin-bottom: 0.5rem;
|
|
21088
|
+
color: var(--text);
|
|
21089
|
+
}
|
|
21090
|
+
|
|
20935
21091
|
/* Sprint Summary */
|
|
20936
21092
|
.sprint-goal {
|
|
20937
21093
|
background: var(--bg-card);
|
|
@@ -21071,6 +21227,22 @@ tr:hover td {
|
|
|
21071
21227
|
max-height: 0;
|
|
21072
21228
|
opacity: 0;
|
|
21073
21229
|
}
|
|
21230
|
+
|
|
21231
|
+
/* Sortable table headers */
|
|
21232
|
+
.sortable-th {
|
|
21233
|
+
cursor: pointer;
|
|
21234
|
+
user-select: none;
|
|
21235
|
+
}
|
|
21236
|
+
.sortable-th:hover {
|
|
21237
|
+
text-decoration: underline;
|
|
21238
|
+
color: var(--text);
|
|
21239
|
+
}
|
|
21240
|
+
.sort-arrow {
|
|
21241
|
+
display: inline-block;
|
|
21242
|
+
margin-left: 0.3rem;
|
|
21243
|
+
font-size: 0.65rem;
|
|
21244
|
+
opacity: 0.7;
|
|
21245
|
+
}
|
|
21074
21246
|
`;
|
|
21075
21247
|
}
|
|
21076
21248
|
|
|
@@ -21992,15 +22164,53 @@ function sprintSummaryPage(data, cached2) {
|
|
|
21992
22164
|
</div>`,
|
|
21993
22165
|
{ titleTag: "h3" }
|
|
21994
22166
|
) : "";
|
|
22167
|
+
const STREAM_PALETTE = [
|
|
22168
|
+
"hsla(220, 30%, 22%, 0.45)",
|
|
22169
|
+
"hsla(160, 30%, 20%, 0.45)",
|
|
22170
|
+
"hsla(280, 25%, 22%, 0.45)",
|
|
22171
|
+
"hsla(30, 35%, 22%, 0.45)",
|
|
22172
|
+
"hsla(340, 25%, 22%, 0.45)",
|
|
22173
|
+
"hsla(190, 30%, 20%, 0.45)",
|
|
22174
|
+
"hsla(60, 25%, 20%, 0.45)",
|
|
22175
|
+
"hsla(120, 20%, 20%, 0.45)"
|
|
22176
|
+
];
|
|
22177
|
+
function hashString(s) {
|
|
22178
|
+
let h = 0;
|
|
22179
|
+
for (let i = 0; i < s.length; i++) {
|
|
22180
|
+
h = (h << 5) - h + s.charCodeAt(i) | 0;
|
|
22181
|
+
}
|
|
22182
|
+
return Math.abs(h);
|
|
22183
|
+
}
|
|
22184
|
+
function collectStreams(items) {
|
|
22185
|
+
const streams = /* @__PURE__ */ new Set();
|
|
22186
|
+
for (const w of items) {
|
|
22187
|
+
if (w.workStream) streams.add(w.workStream);
|
|
22188
|
+
if (w.children) {
|
|
22189
|
+
for (const s of collectStreams(w.children)) streams.add(s);
|
|
22190
|
+
}
|
|
22191
|
+
}
|
|
22192
|
+
return streams;
|
|
22193
|
+
}
|
|
22194
|
+
const uniqueStreams = collectStreams(data.workItems.items);
|
|
22195
|
+
const streamColorMap = /* @__PURE__ */ new Map();
|
|
22196
|
+
for (const name of uniqueStreams) {
|
|
22197
|
+
streamColorMap.set(name, STREAM_PALETTE[hashString(name) % STREAM_PALETTE.length]);
|
|
22198
|
+
}
|
|
22199
|
+
const streamStyleRules = [...streamColorMap.entries()].map(([name, color]) => `tr[data-stream="${escapeHtml(name)}"] td { background: ${color}; }`).join("\n");
|
|
22200
|
+
const streamStyleBlock = streamStyleRules ? `<style>${streamStyleRules}</style>` : "";
|
|
21995
22201
|
function renderItemRows(items, depth = 0) {
|
|
21996
22202
|
return items.flatMap((w) => {
|
|
21997
22203
|
const isChild = depth > 0;
|
|
21998
22204
|
const isContribution = w.type === "contribution";
|
|
21999
|
-
const
|
|
22205
|
+
const classes = [];
|
|
22206
|
+
if (isContribution) classes.push("contribution-row");
|
|
22207
|
+
else if (isChild) classes.push("child-row");
|
|
22208
|
+
const dataStream = w.workStream ? ` data-stream="${escapeHtml(w.workStream)}"` : "";
|
|
22209
|
+
const rowAttrs = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
|
|
22000
22210
|
const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
|
|
22001
22211
|
const streamCell = w.workStream ? `<span class="badge badge-subtle">${escapeHtml(w.workStream)}</span>` : "";
|
|
22002
22212
|
const row = `
|
|
22003
|
-
<tr${
|
|
22213
|
+
<tr${rowAttrs}${dataStream}>
|
|
22004
22214
|
<td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
22005
22215
|
<td>${escapeHtml(w.title)}</td>
|
|
22006
22216
|
<td>${streamCell}</td>
|
|
@@ -22012,13 +22222,21 @@ function sprintSummaryPage(data, cached2) {
|
|
|
22012
22222
|
});
|
|
22013
22223
|
}
|
|
22014
22224
|
const workItemRows = renderItemRows(data.workItems.items);
|
|
22225
|
+
const sortableHeaders = `<tr>
|
|
22226
|
+
<th class="sortable-th" onclick="sortWorkItems(0)">ID<span class="sort-arrow" id="sort-arrow-0"></span></th>
|
|
22227
|
+
<th class="sortable-th" onclick="sortWorkItems(1)">Title<span class="sort-arrow" id="sort-arrow-1"></span></th>
|
|
22228
|
+
<th class="sortable-th" onclick="sortWorkItems(2)">Stream<span class="sort-arrow" id="sort-arrow-2"></span></th>
|
|
22229
|
+
<th class="sortable-th" onclick="sortWorkItems(3)">Type<span class="sort-arrow" id="sort-arrow-3"></span></th>
|
|
22230
|
+
<th class="sortable-th" onclick="sortWorkItems(4)">Status<span class="sort-arrow" id="sort-arrow-4"></span></th>
|
|
22231
|
+
</tr>`;
|
|
22015
22232
|
const workItemsSection = workItemRows.length > 0 ? collapsibleSection(
|
|
22016
22233
|
"ss-work-items",
|
|
22017
22234
|
"Work Items",
|
|
22018
|
-
|
|
22019
|
-
|
|
22235
|
+
`${streamStyleBlock}
|
|
22236
|
+
<div class="table-wrap">
|
|
22237
|
+
<table id="work-items-table">
|
|
22020
22238
|
<thead>
|
|
22021
|
-
|
|
22239
|
+
${sortableHeaders}
|
|
22022
22240
|
</thead>
|
|
22023
22241
|
<tbody>
|
|
22024
22242
|
${workItemRows.join("")}
|
|
@@ -22097,6 +22315,61 @@ function sprintSummaryPage(data, cached2) {
|
|
|
22097
22315
|
</div>
|
|
22098
22316
|
|
|
22099
22317
|
<script>
|
|
22318
|
+
var _sortCol = -1;
|
|
22319
|
+
var _sortAsc = true;
|
|
22320
|
+
|
|
22321
|
+
function sortWorkItems(col) {
|
|
22322
|
+
var table = document.getElementById('work-items-table');
|
|
22323
|
+
if (!table) return;
|
|
22324
|
+
var tbody = table.querySelector('tbody');
|
|
22325
|
+
var allRows = Array.from(tbody.querySelectorAll('tr'));
|
|
22326
|
+
|
|
22327
|
+
// Toggle direction if clicking the same column
|
|
22328
|
+
if (_sortCol === col) {
|
|
22329
|
+
_sortAsc = !_sortAsc;
|
|
22330
|
+
} else {
|
|
22331
|
+
_sortCol = col;
|
|
22332
|
+
_sortAsc = true;
|
|
22333
|
+
}
|
|
22334
|
+
|
|
22335
|
+
// Update sort arrows
|
|
22336
|
+
for (var i = 0; i < 5; i++) {
|
|
22337
|
+
var arrow = document.getElementById('sort-arrow-' + i);
|
|
22338
|
+
if (arrow) arrow.textContent = i === col ? (_sortAsc ? ' \\u25B2' : ' \\u25BC') : '';
|
|
22339
|
+
}
|
|
22340
|
+
|
|
22341
|
+
// Group rows: root rows + their child/contribution rows
|
|
22342
|
+
var groups = [];
|
|
22343
|
+
var current = null;
|
|
22344
|
+
for (var r = 0; r < allRows.length; r++) {
|
|
22345
|
+
var row = allRows[r];
|
|
22346
|
+
var isChild = row.classList.contains('child-row') || row.classList.contains('contribution-row');
|
|
22347
|
+
if (!isChild) {
|
|
22348
|
+
current = { root: row, children: [] };
|
|
22349
|
+
groups.push(current);
|
|
22350
|
+
} else if (current) {
|
|
22351
|
+
current.children.push(row);
|
|
22352
|
+
}
|
|
22353
|
+
}
|
|
22354
|
+
|
|
22355
|
+
// Sort groups by root row text content of target column
|
|
22356
|
+
groups.sort(function(a, b) {
|
|
22357
|
+
var aText = (a.root.children[col] ? a.root.children[col].textContent : '').trim().toLowerCase();
|
|
22358
|
+
var bText = (b.root.children[col] ? b.root.children[col].textContent : '').trim().toLowerCase();
|
|
22359
|
+
if (aText < bText) return _sortAsc ? -1 : 1;
|
|
22360
|
+
if (aText > bText) return _sortAsc ? 1 : -1;
|
|
22361
|
+
return 0;
|
|
22362
|
+
});
|
|
22363
|
+
|
|
22364
|
+
// Re-append rows in sorted order
|
|
22365
|
+
for (var g = 0; g < groups.length; g++) {
|
|
22366
|
+
tbody.appendChild(groups[g].root);
|
|
22367
|
+
for (var c = 0; c < groups[g].children.length; c++) {
|
|
22368
|
+
tbody.appendChild(groups[g].children[c]);
|
|
22369
|
+
}
|
|
22370
|
+
}
|
|
22371
|
+
}
|
|
22372
|
+
|
|
22100
22373
|
async function generateSummary() {
|
|
22101
22374
|
var btn = document.getElementById('generate-btn');
|
|
22102
22375
|
var loading = document.getElementById('summary-loading');
|
|
@@ -22134,142 +22407,1992 @@ function sprintSummaryPage(data, cached2) {
|
|
|
22134
22407
|
</script>`;
|
|
22135
22408
|
}
|
|
22136
22409
|
|
|
22137
|
-
// src/
|
|
22138
|
-
var
|
|
22139
|
-
|
|
22140
|
-
|
|
22141
|
-
|
|
22142
|
-
|
|
22143
|
-
|
|
22144
|
-
|
|
22145
|
-
|
|
22146
|
-
|
|
22147
|
-
|
|
22148
|
-
|
|
22149
|
-
|
|
22150
|
-
|
|
22151
|
-
|
|
22152
|
-
|
|
22153
|
-
|
|
22154
|
-
|
|
22155
|
-
|
|
22156
|
-
|
|
22157
|
-
|
|
22158
|
-
|
|
22159
|
-
|
|
22160
|
-
|
|
22161
|
-
|
|
22162
|
-
|
|
22163
|
-
|
|
22164
|
-
|
|
22165
|
-
|
|
22166
|
-
|
|
22167
|
-
|
|
22168
|
-
|
|
22169
|
-
|
|
22170
|
-
|
|
22171
|
-
|
|
22172
|
-
|
|
22173
|
-
|
|
22174
|
-
|
|
22175
|
-
|
|
22176
|
-
|
|
22177
|
-
|
|
22178
|
-
|
|
22179
|
-
|
|
22180
|
-
|
|
22181
|
-
|
|
22182
|
-
|
|
22183
|
-
|
|
22184
|
-
|
|
22185
|
-
|
|
22186
|
-
|
|
22187
|
-
|
|
22188
|
-
|
|
22189
|
-
|
|
22190
|
-
|
|
22191
|
-
|
|
22192
|
-
|
|
22193
|
-
|
|
22194
|
-
|
|
22195
|
-
|
|
22196
|
-
|
|
22197
|
-
|
|
22198
|
-
|
|
22199
|
-
|
|
22200
|
-
|
|
22201
|
-
|
|
22202
|
-
|
|
22203
|
-
|
|
22204
|
-
|
|
22205
|
-
|
|
22206
|
-
|
|
22207
|
-
|
|
22208
|
-
|
|
22209
|
-
|
|
22210
|
-
|
|
22211
|
-
|
|
22212
|
-
|
|
22213
|
-
|
|
22214
|
-
|
|
22215
|
-
|
|
22216
|
-
|
|
22217
|
-
|
|
22218
|
-
|
|
22219
|
-
|
|
22220
|
-
|
|
22221
|
-
|
|
22222
|
-
|
|
22223
|
-
|
|
22224
|
-
|
|
22225
|
-
|
|
22226
|
-
|
|
22227
|
-
|
|
22228
|
-
|
|
22229
|
-
|
|
22230
|
-
|
|
22231
|
-
|
|
22232
|
-
|
|
22233
|
-
|
|
22234
|
-
|
|
22235
|
-
|
|
22236
|
-
|
|
22237
|
-
|
|
22238
|
-
|
|
22239
|
-
|
|
22240
|
-
|
|
22241
|
-
|
|
22242
|
-
|
|
22243
|
-
|
|
22244
|
-
|
|
22245
|
-
|
|
22246
|
-
|
|
22247
|
-
|
|
22248
|
-
|
|
22249
|
-
|
|
22250
|
-
|
|
22251
|
-
|
|
22252
|
-
|
|
22253
|
-
|
|
22254
|
-
|
|
22255
|
-
|
|
22256
|
-
|
|
22257
|
-
|
|
22258
|
-
|
|
22259
|
-
|
|
22260
|
-
|
|
22261
|
-
|
|
22262
|
-
|
|
22263
|
-
|
|
22264
|
-
|
|
22265
|
-
function
|
|
22266
|
-
|
|
22267
|
-
|
|
22268
|
-
|
|
22269
|
-
|
|
22270
|
-
|
|
22271
|
-
|
|
22272
|
-
|
|
22410
|
+
// src/personas/builtin/product-owner.ts
|
|
22411
|
+
var productOwner = {
|
|
22412
|
+
id: "product-owner",
|
|
22413
|
+
name: "Product Owner",
|
|
22414
|
+
shortName: "po",
|
|
22415
|
+
description: "Focuses on product vision, stakeholder needs, backlog prioritization, and value delivery.",
|
|
22416
|
+
systemPrompt: `You are Marvin, acting as a **Product Owner**. Your role is to help the team maximize the value delivered by the product.
|
|
22417
|
+
|
|
22418
|
+
## Core Responsibilities
|
|
22419
|
+
- Define and communicate the product vision and strategy
|
|
22420
|
+
- Manage and prioritize the product backlog
|
|
22421
|
+
- Ensure stakeholder needs are understood and addressed
|
|
22422
|
+
- Make decisions about scope, priority, and trade-offs
|
|
22423
|
+
- Accept or reject work results based on acceptance criteria
|
|
22424
|
+
|
|
22425
|
+
## How You Work
|
|
22426
|
+
- Ask clarifying questions to understand business value and user needs
|
|
22427
|
+
- Create and refine decisions (D-xxx) for important product choices
|
|
22428
|
+
- Track questions (Q-xxx) that need stakeholder input
|
|
22429
|
+
- Define acceptance criteria for features and deliverables
|
|
22430
|
+
- Prioritize actions (A-xxx) based on business value
|
|
22431
|
+
|
|
22432
|
+
## Communication Style
|
|
22433
|
+
- Business-oriented language, avoid unnecessary technical jargon
|
|
22434
|
+
- Focus on outcomes and value, not implementation details
|
|
22435
|
+
- Be decisive but transparent about trade-offs
|
|
22436
|
+
- Challenge assumptions that don't align with product goals`,
|
|
22437
|
+
focusAreas: [
|
|
22438
|
+
"Product vision and strategy",
|
|
22439
|
+
"Backlog management",
|
|
22440
|
+
"Stakeholder communication",
|
|
22441
|
+
"Value delivery",
|
|
22442
|
+
"Acceptance criteria",
|
|
22443
|
+
"Feature definition and prioritization"
|
|
22444
|
+
],
|
|
22445
|
+
documentTypes: ["decision", "question", "action", "feature"],
|
|
22446
|
+
contributionTypes: ["stakeholder-feedback", "acceptance-result", "priority-change", "market-insight"]
|
|
22447
|
+
};
|
|
22448
|
+
|
|
22449
|
+
// src/personas/builtin/delivery-manager.ts
|
|
22450
|
+
var deliveryManager = {
|
|
22451
|
+
id: "delivery-manager",
|
|
22452
|
+
name: "Delivery Manager",
|
|
22453
|
+
shortName: "dm",
|
|
22454
|
+
description: "Focuses on project delivery, risk management, team coordination, and process governance.",
|
|
22455
|
+
systemPrompt: `You are Marvin, acting as a **Delivery Manager**. Your role is to ensure the project is delivered on time, within scope, and with managed risks.
|
|
22456
|
+
|
|
22457
|
+
## Core Responsibilities
|
|
22458
|
+
- Track project progress and identify blockers
|
|
22459
|
+
- Manage risks, issues, and dependencies
|
|
22460
|
+
- Coordinate between team members and stakeholders
|
|
22461
|
+
- Ensure governance processes are followed (decisions logged, actions tracked)
|
|
22462
|
+
- Facilitate meetings and ensure outcomes are captured
|
|
22463
|
+
|
|
22464
|
+
## How You Work
|
|
22465
|
+
- Review open actions (A-xxx) and follow up on overdue items
|
|
22466
|
+
- Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
|
|
22467
|
+
- Assign actions to sprints when sprint planning is active, using the sprints parameter
|
|
22468
|
+
- Ensure decisions (D-xxx) are properly documented with rationale
|
|
22469
|
+
- Track questions (Q-xxx) and ensure they get answered
|
|
22470
|
+
- Monitor project health and flag risks early
|
|
22471
|
+
- Create meeting notes and ensure action items are assigned
|
|
22472
|
+
|
|
22473
|
+
## Communication Style
|
|
22474
|
+
- Process-oriented but pragmatic
|
|
22475
|
+
- Focus on status, risks, and blockers
|
|
22476
|
+
- Be proactive about follow-ups and deadlines
|
|
22477
|
+
- Keep stakeholders informed with concise updates`,
|
|
22478
|
+
focusAreas: [
|
|
22479
|
+
"Project delivery",
|
|
22480
|
+
"Risk management",
|
|
22481
|
+
"Team coordination",
|
|
22482
|
+
"Process governance",
|
|
22483
|
+
"Status tracking",
|
|
22484
|
+
"Epic scheduling and tracking",
|
|
22485
|
+
"Sprint planning and tracking"
|
|
22486
|
+
],
|
|
22487
|
+
documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "task", "sprint"],
|
|
22488
|
+
contributionTypes: ["risk-finding", "blocker-report", "dependency-update", "status-assessment"]
|
|
22489
|
+
};
|
|
22490
|
+
|
|
22491
|
+
// src/personas/builtin/tech-lead.ts
|
|
22492
|
+
var techLead = {
|
|
22493
|
+
id: "tech-lead",
|
|
22494
|
+
name: "Technical Lead",
|
|
22495
|
+
shortName: "tl",
|
|
22496
|
+
description: "Focuses on technical architecture, code quality, technical decisions, and implementation guidance.",
|
|
22497
|
+
systemPrompt: `You are Marvin, acting as a **Technical Lead**. Your role is to guide the team on technical decisions and ensure high-quality implementation.
|
|
22498
|
+
|
|
22499
|
+
## Core Responsibilities
|
|
22500
|
+
- Define and maintain technical architecture
|
|
22501
|
+
- Make and document technical decisions with clear rationale
|
|
22502
|
+
- Review technical approaches and identify potential issues
|
|
22503
|
+
- Guide the team on best practices and patterns
|
|
22504
|
+
- Evaluate technical risks and propose mitigations
|
|
22505
|
+
|
|
22506
|
+
## How You Work
|
|
22507
|
+
- Create decisions (D-xxx) for significant technical choices (framework, architecture, patterns)
|
|
22508
|
+
- Document technical questions (Q-xxx) that need investigation or proof-of-concept
|
|
22509
|
+
- Define technical actions (A-xxx) for implementation tasks
|
|
22510
|
+
- Consider non-functional requirements (performance, security, maintainability)
|
|
22511
|
+
- Provide clear technical guidance with examples when helpful
|
|
22512
|
+
|
|
22513
|
+
## Communication Style
|
|
22514
|
+
- Technical but accessible \u2014 explain complex concepts clearly
|
|
22515
|
+
- Evidence-based decision making with documented trade-offs
|
|
22516
|
+
- Pragmatic about technical debt vs. delivery speed
|
|
22517
|
+
- Focus on maintainability and long-term sustainability`,
|
|
22518
|
+
focusAreas: [
|
|
22519
|
+
"Technical architecture",
|
|
22520
|
+
"Code quality",
|
|
22521
|
+
"Technical decisions",
|
|
22522
|
+
"Implementation guidance",
|
|
22523
|
+
"Non-functional requirements",
|
|
22524
|
+
"Epic creation and scoping",
|
|
22525
|
+
"Task creation and breakdown",
|
|
22526
|
+
"Sprint scoping and technical execution"
|
|
22527
|
+
],
|
|
22528
|
+
documentTypes: ["decision", "action", "question", "epic", "task", "sprint"],
|
|
22529
|
+
contributionTypes: ["action-result", "spike-findings", "technical-assessment", "architecture-review"]
|
|
22530
|
+
};
|
|
22531
|
+
|
|
22532
|
+
// src/personas/registry.ts
|
|
22533
|
+
var BUILTIN_PERSONAS = [
|
|
22534
|
+
productOwner,
|
|
22535
|
+
deliveryManager,
|
|
22536
|
+
techLead
|
|
22537
|
+
];
|
|
22538
|
+
function getPersona(idOrShortName) {
|
|
22539
|
+
const key = idOrShortName.toLowerCase();
|
|
22540
|
+
return BUILTIN_PERSONAS.find(
|
|
22541
|
+
(p) => p.id === key || p.shortName === key
|
|
22542
|
+
);
|
|
22543
|
+
}
|
|
22544
|
+
function listPersonas() {
|
|
22545
|
+
return [...BUILTIN_PERSONAS];
|
|
22546
|
+
}
|
|
22547
|
+
|
|
22548
|
+
// src/web/persona-views.ts
|
|
22549
|
+
var VIEWS = /* @__PURE__ */ new Map();
|
|
22550
|
+
var PAGE_RENDERERS = /* @__PURE__ */ new Map();
|
|
22551
|
+
function registerPersonaView(config2) {
|
|
22552
|
+
VIEWS.set(config2.shortName, config2);
|
|
22553
|
+
}
|
|
22554
|
+
function registerPersonaPage(persona, pageId, renderer) {
|
|
22555
|
+
PAGE_RENDERERS.set(`${persona}/${pageId}`, renderer);
|
|
22556
|
+
}
|
|
22557
|
+
function getPersonaView(mode) {
|
|
22558
|
+
if (!mode) return void 0;
|
|
22559
|
+
return VIEWS.get(mode);
|
|
22560
|
+
}
|
|
22561
|
+
function getPersonaPageRenderer(persona, pageId) {
|
|
22562
|
+
return PAGE_RENDERERS.get(`${persona}/${pageId}`);
|
|
22563
|
+
}
|
|
22564
|
+
function getAllPersonaViews() {
|
|
22565
|
+
return [...VIEWS.values()];
|
|
22566
|
+
}
|
|
22567
|
+
function parsePersonaFromPath(pathname) {
|
|
22568
|
+
const match = pathname.match(/^\/(po|dm|tl)(?:\/|$)/);
|
|
22569
|
+
return match ? match[1] : null;
|
|
22570
|
+
}
|
|
22571
|
+
|
|
22572
|
+
// src/web/templates/persona-switcher.ts
|
|
22573
|
+
function renderPersonaSwitcher(current, currentPath) {
|
|
22574
|
+
const views = getAllPersonaViews();
|
|
22575
|
+
if (views.length === 0) return "";
|
|
22576
|
+
const options = [
|
|
22577
|
+
`<option value=""${current === null ? " selected" : ""}>Admin</option>`,
|
|
22578
|
+
...views.map(
|
|
22579
|
+
(v) => `<option value="${v.shortName}"${current === v.shortName ? " selected" : ""}>${escapeHtml(v.displayName)}</option>`
|
|
22580
|
+
)
|
|
22581
|
+
].join("\n ");
|
|
22582
|
+
return `
|
|
22583
|
+
<div class="persona-switcher">
|
|
22584
|
+
<label class="persona-label" for="persona-select">View</label>
|
|
22585
|
+
<select class="persona-select" id="persona-select" onchange="switchPersona(this.value)">
|
|
22586
|
+
${options}
|
|
22587
|
+
</select>
|
|
22588
|
+
</div>
|
|
22589
|
+
<script>
|
|
22590
|
+
function switchPersona(value) {
|
|
22591
|
+
if (value) {
|
|
22592
|
+
window.location.href = '/' + value + '/dashboard';
|
|
22593
|
+
} else {
|
|
22594
|
+
window.location.href = '/';
|
|
22595
|
+
}
|
|
22596
|
+
}
|
|
22597
|
+
</script>`;
|
|
22598
|
+
}
|
|
22599
|
+
function renderPersonaBanner() {
|
|
22600
|
+
const views = getAllPersonaViews();
|
|
22601
|
+
if (views.length === 0) return "";
|
|
22602
|
+
const cards = views.map(
|
|
22603
|
+
(v) => `
|
|
22604
|
+
<a href="/${v.shortName}/dashboard" class="persona-banner-option" style="--persona-card-accent: ${v.color}" onclick="dismissPersonaBanner()">
|
|
22605
|
+
<div class="persona-banner-name">${escapeHtml(v.displayName)}</div>
|
|
22606
|
+
<div class="persona-banner-desc">${escapeHtml(v.description)}</div>
|
|
22607
|
+
</a>`
|
|
22608
|
+
).join("\n");
|
|
22609
|
+
return `
|
|
22610
|
+
<div class="persona-banner" id="persona-banner">
|
|
22611
|
+
<div class="persona-banner-header">
|
|
22612
|
+
<h3>Choose a View</h3>
|
|
22613
|
+
<button class="persona-banner-dismiss" onclick="dismissPersonaBanner()" title="Dismiss">×</button>
|
|
22614
|
+
</div>
|
|
22615
|
+
<p class="persona-banner-subtitle">Get a curated dashboard for your role, or stay in admin mode for full access.</p>
|
|
22616
|
+
<div class="persona-banner-options">
|
|
22617
|
+
${cards}
|
|
22618
|
+
</div>
|
|
22619
|
+
</div>
|
|
22620
|
+
<script>
|
|
22621
|
+
(function() {
|
|
22622
|
+
if (localStorage.getItem('marvin-persona-banner-dismissed')) {
|
|
22623
|
+
var banner = document.getElementById('persona-banner');
|
|
22624
|
+
if (banner) banner.style.display = 'none';
|
|
22625
|
+
}
|
|
22626
|
+
})();
|
|
22627
|
+
function dismissPersonaBanner() {
|
|
22628
|
+
localStorage.setItem('marvin-persona-banner-dismissed', '1');
|
|
22629
|
+
var banner = document.getElementById('persona-banner');
|
|
22630
|
+
if (banner) banner.style.display = 'none';
|
|
22631
|
+
}
|
|
22632
|
+
</script>`;
|
|
22633
|
+
}
|
|
22634
|
+
|
|
22635
|
+
// src/web/templates/pages/po/dashboard.ts
|
|
22636
|
+
function poDashboardPage(ctx) {
|
|
22637
|
+
const overview = getOverviewData(ctx.store);
|
|
22638
|
+
const upcoming = getUpcomingData(ctx.store);
|
|
22639
|
+
const sprintData = getSprintSummaryData(ctx.store);
|
|
22640
|
+
const features = ctx.store.list({ type: "feature" });
|
|
22641
|
+
const featuresDone = features.filter((d) => ["done", "closed", "resolved"].includes(d.frontmatter.status)).length;
|
|
22642
|
+
const featuresOpen = features.filter((d) => d.frontmatter.status === "open").length;
|
|
22643
|
+
const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
|
|
22644
|
+
const decisions = ctx.store.list({ type: "decision" });
|
|
22645
|
+
const decisionsOpen = decisions.filter((d) => d.frontmatter.status === "open").length;
|
|
22646
|
+
const questions = ctx.store.list({ type: "question" });
|
|
22647
|
+
const questionsOpen = questions.filter((d) => d.frontmatter.status === "open").length;
|
|
22648
|
+
const statsCards = `
|
|
22649
|
+
<div class="cards">
|
|
22650
|
+
<div class="card">
|
|
22651
|
+
<a href="/po/backlog">
|
|
22652
|
+
<div class="card-label">Features</div>
|
|
22653
|
+
<div class="card-value">${features.length}</div>
|
|
22654
|
+
<div class="card-sub">${featuresDone} done, ${featuresInProgress} in progress, ${featuresOpen} open</div>
|
|
22655
|
+
</a>
|
|
22656
|
+
</div>
|
|
22657
|
+
<div class="card">
|
|
22658
|
+
<a href="/po/decisions">
|
|
22659
|
+
<div class="card-label">Pending Decisions</div>
|
|
22660
|
+
<div class="card-value${decisionsOpen > 0 ? " priority-medium" : ""}">${decisionsOpen}</div>
|
|
22661
|
+
<div class="card-sub">${decisions.length} total decisions</div>
|
|
22662
|
+
</a>
|
|
22663
|
+
</div>
|
|
22664
|
+
<div class="card">
|
|
22665
|
+
<a href="/po/backlog">
|
|
22666
|
+
<div class="card-label">Open Questions</div>
|
|
22667
|
+
<div class="card-value${questionsOpen > 0 ? " priority-medium" : ""}">${questionsOpen}</div>
|
|
22668
|
+
<div class="card-sub">${questions.length} total questions</div>
|
|
22669
|
+
</a>
|
|
22670
|
+
</div>
|
|
22671
|
+
<div class="card">
|
|
22672
|
+
<a href="/po/delivery">
|
|
22673
|
+
<div class="card-label">Sprint</div>
|
|
22674
|
+
<div class="card-value">${sprintData ? `${sprintData.workItems.completionPct}%` : "\u2014"}</div>
|
|
22675
|
+
<div class="card-sub">${sprintData ? `${sprintData.workItems.done}/${sprintData.workItems.total} items` : "No active sprint"}</div>
|
|
22676
|
+
</a>
|
|
22677
|
+
</div>
|
|
22678
|
+
</div>`;
|
|
22679
|
+
const poTypes = /* @__PURE__ */ new Set(["feature", "decision", "question"]);
|
|
22680
|
+
const poRecent = overview.recent.filter((d) => poTypes.has(d.frontmatter.type)).slice(0, 10);
|
|
22681
|
+
const recentTable = poRecent.length > 0 ? collapsibleSection(
|
|
22682
|
+
"po-recent",
|
|
22683
|
+
"Recent Activity",
|
|
22684
|
+
`<div class="table-wrap">
|
|
22685
|
+
<table>
|
|
22686
|
+
<thead>
|
|
22687
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Updated</th></tr>
|
|
22688
|
+
</thead>
|
|
22689
|
+
<tbody>
|
|
22690
|
+
${poRecent.map((d) => `
|
|
22691
|
+
<tr>
|
|
22692
|
+
<td><a href="/docs/${d.frontmatter.type}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
22693
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
22694
|
+
<td>${escapeHtml(typeLabel(d.frontmatter.type))}</td>
|
|
22695
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
22696
|
+
<td>${formatDate(d.frontmatter.updated ?? d.frontmatter.created)}</td>
|
|
22697
|
+
</tr>`).join("")}
|
|
22698
|
+
</tbody>
|
|
22699
|
+
</table>
|
|
22700
|
+
</div>`,
|
|
22701
|
+
{ titleTag: "h3" }
|
|
22702
|
+
) : "";
|
|
22703
|
+
const trendingSection = upcoming.trending.length > 0 ? collapsibleSection(
|
|
22704
|
+
"po-trending",
|
|
22705
|
+
"Trending Items",
|
|
22706
|
+
`<div class="table-wrap">
|
|
22707
|
+
<table>
|
|
22708
|
+
<thead>
|
|
22709
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Score</th></tr>
|
|
22710
|
+
</thead>
|
|
22711
|
+
<tbody>
|
|
22712
|
+
${upcoming.trending.slice(0, 8).map((t) => `
|
|
22713
|
+
<tr>
|
|
22714
|
+
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
22715
|
+
<td>${escapeHtml(t.title)}</td>
|
|
22716
|
+
<td>${escapeHtml(typeLabel(t.type))}</td>
|
|
22717
|
+
<td><span class="trending-score">${t.score}</span></td>
|
|
22718
|
+
</tr>`).join("")}
|
|
22719
|
+
</tbody>
|
|
22720
|
+
</table>
|
|
22721
|
+
</div>`,
|
|
22722
|
+
{ titleTag: "h3" }
|
|
22723
|
+
) : "";
|
|
22724
|
+
return `
|
|
22725
|
+
<div class="page-header">
|
|
22726
|
+
<h2>Product Owner Dashboard</h2>
|
|
22727
|
+
<div class="subtitle">Feature delivery, decisions, and stakeholder alignment</div>
|
|
22728
|
+
</div>
|
|
22729
|
+
${statsCards}
|
|
22730
|
+
${recentTable}
|
|
22731
|
+
${trendingSection}
|
|
22732
|
+
`;
|
|
22733
|
+
}
|
|
22734
|
+
|
|
22735
|
+
// src/web/templates/pages/po/backlog.ts
|
|
22736
|
+
function poBacklogPage(ctx) {
|
|
22737
|
+
const features = ctx.store.list({ type: "feature" });
|
|
22738
|
+
const questions = ctx.store.list({ type: "question" });
|
|
22739
|
+
const openQuestions = questions.filter((d) => d.frontmatter.status === "open");
|
|
22740
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
22741
|
+
const statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
22742
|
+
const sortedFeatures = [...features].sort((a, b) => {
|
|
22743
|
+
const sa = statusOrder[a.frontmatter.status] ?? 3;
|
|
22744
|
+
const sb = statusOrder[b.frontmatter.status] ?? 3;
|
|
22745
|
+
if (sa !== sb) return sa - sb;
|
|
22746
|
+
const pa = priorityOrder[a.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
22747
|
+
const pb = priorityOrder[b.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
22748
|
+
if (pa !== pb) return pa - pb;
|
|
22749
|
+
return a.frontmatter.id.localeCompare(b.frontmatter.id);
|
|
22750
|
+
});
|
|
22751
|
+
const epics = ctx.store.list({ type: "epic" });
|
|
22752
|
+
const featureToEpics = /* @__PURE__ */ new Map();
|
|
22753
|
+
for (const epic of epics) {
|
|
22754
|
+
const linked = epic.frontmatter.linkedFeature;
|
|
22755
|
+
const featureIds = Array.isArray(linked) ? linked : linked ? [linked] : [];
|
|
22756
|
+
for (const fid of featureIds) {
|
|
22757
|
+
const existing = featureToEpics.get(String(fid)) ?? [];
|
|
22758
|
+
existing.push(epic.frontmatter.id);
|
|
22759
|
+
featureToEpics.set(String(fid), existing);
|
|
22760
|
+
}
|
|
22761
|
+
}
|
|
22762
|
+
function priorityClass2(p) {
|
|
22763
|
+
if (!p) return "";
|
|
22764
|
+
const lower = p.toLowerCase();
|
|
22765
|
+
if (lower === "critical" || lower === "high") return " priority-high";
|
|
22766
|
+
if (lower === "medium") return " priority-medium";
|
|
22767
|
+
if (lower === "low") return " priority-low";
|
|
22768
|
+
return "";
|
|
22769
|
+
}
|
|
22770
|
+
const featuresTable = sortedFeatures.length > 0 ? `<div class="table-wrap">
|
|
22771
|
+
<table>
|
|
22772
|
+
<thead>
|
|
22773
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Priority</th><th>Linked Epics</th></tr>
|
|
22774
|
+
</thead>
|
|
22775
|
+
<tbody>
|
|
22776
|
+
${sortedFeatures.map((d) => {
|
|
22777
|
+
const linkedEpics = featureToEpics.get(d.frontmatter.id) ?? [];
|
|
22778
|
+
const epicLinks = linkedEpics.map((eid) => `<a href="/docs/epic/${escapeHtml(eid)}">${escapeHtml(eid)}</a>`).join(", ");
|
|
22779
|
+
return `
|
|
22780
|
+
<tr>
|
|
22781
|
+
<td><a href="/docs/feature/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
22782
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
22783
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
22784
|
+
<td><span class="${priorityClass2(d.frontmatter.priority)}">${escapeHtml(d.frontmatter.priority ?? "\u2014")}</span></td>
|
|
22785
|
+
<td>${epicLinks || '<span class="text-dim">\u2014</span>'}</td>
|
|
22786
|
+
</tr>`;
|
|
22787
|
+
}).join("")}
|
|
22788
|
+
</tbody>
|
|
22789
|
+
</table>
|
|
22790
|
+
</div>` : '<div class="empty"><p>No features found.</p></div>';
|
|
22791
|
+
const questionsTable = openQuestions.length > 0 ? collapsibleSection(
|
|
22792
|
+
"po-backlog-questions",
|
|
22793
|
+
`Open Questions (${openQuestions.length})`,
|
|
22794
|
+
`<div class="table-wrap">
|
|
22795
|
+
<table>
|
|
22796
|
+
<thead>
|
|
22797
|
+
<tr><th>ID</th><th>Title</th><th>Owner</th><th>Created</th></tr>
|
|
22798
|
+
</thead>
|
|
22799
|
+
<tbody>
|
|
22800
|
+
${openQuestions.map((d) => `
|
|
22801
|
+
<tr>
|
|
22802
|
+
<td><a href="/docs/question/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
22803
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
22804
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
22805
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
22806
|
+
</tr>`).join("")}
|
|
22807
|
+
</tbody>
|
|
22808
|
+
</table>
|
|
22809
|
+
</div>`,
|
|
22810
|
+
{ titleTag: "h3" }
|
|
22811
|
+
) : "";
|
|
22812
|
+
return `
|
|
22813
|
+
<div class="page-header">
|
|
22814
|
+
<h2>Product Backlog</h2>
|
|
22815
|
+
<div class="subtitle">${features.length} features, ${openQuestions.length} open questions</div>
|
|
22816
|
+
</div>
|
|
22817
|
+
${collapsibleSection("po-backlog-features", `Features (${features.length})`, featuresTable, { titleTag: "h3" })}
|
|
22818
|
+
${questionsTable}
|
|
22819
|
+
`;
|
|
22820
|
+
}
|
|
22821
|
+
|
|
22822
|
+
// src/web/templates/pages/po/decisions.ts
|
|
22823
|
+
var DONE_STATUSES3 = /* @__PURE__ */ new Set(["done", "closed", "resolved"]);
|
|
22824
|
+
function poDecisionsPage(ctx) {
|
|
22825
|
+
const decisions = ctx.store.list({ type: "decision" });
|
|
22826
|
+
const openDecisions = decisions.filter((d) => !DONE_STATUSES3.has(d.frontmatter.status));
|
|
22827
|
+
const resolvedDecisions = decisions.filter((d) => DONE_STATUSES3.has(d.frontmatter.status));
|
|
22828
|
+
const statsCards = `
|
|
22829
|
+
<div class="cards">
|
|
22830
|
+
<div class="card">
|
|
22831
|
+
<div class="card-label">Open</div>
|
|
22832
|
+
<div class="card-value${openDecisions.length > 0 ? " priority-medium" : ""}">${openDecisions.length}</div>
|
|
22833
|
+
<div class="card-sub">awaiting resolution</div>
|
|
22834
|
+
</div>
|
|
22835
|
+
<div class="card">
|
|
22836
|
+
<div class="card-label">Resolved</div>
|
|
22837
|
+
<div class="card-value">${resolvedDecisions.length}</div>
|
|
22838
|
+
<div class="card-sub">decisions made</div>
|
|
22839
|
+
</div>
|
|
22840
|
+
<div class="card">
|
|
22841
|
+
<div class="card-label">Total</div>
|
|
22842
|
+
<div class="card-value">${decisions.length}</div>
|
|
22843
|
+
<div class="card-sub">all decisions</div>
|
|
22844
|
+
</div>
|
|
22845
|
+
</div>`;
|
|
22846
|
+
function decisionTable(docs) {
|
|
22847
|
+
if (docs.length === 0) return '<div class="empty"><p>None found.</p></div>';
|
|
22848
|
+
return `<div class="table-wrap">
|
|
22849
|
+
<table>
|
|
22850
|
+
<thead>
|
|
22851
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Owner</th><th>Created</th></tr>
|
|
22852
|
+
</thead>
|
|
22853
|
+
<tbody>
|
|
22854
|
+
${docs.map((d) => `
|
|
22855
|
+
<tr>
|
|
22856
|
+
<td><a href="/docs/decision/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
22857
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
22858
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
22859
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
22860
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
22861
|
+
</tr>`).join("")}
|
|
22862
|
+
</tbody>
|
|
22863
|
+
</table>
|
|
22864
|
+
</div>`;
|
|
22865
|
+
}
|
|
22866
|
+
const openSection = collapsibleSection(
|
|
22867
|
+
"po-decisions-open",
|
|
22868
|
+
`Open Decisions (${openDecisions.length})`,
|
|
22869
|
+
decisionTable(openDecisions),
|
|
22870
|
+
{ titleTag: "h3" }
|
|
22871
|
+
);
|
|
22872
|
+
const resolvedSection = collapsibleSection(
|
|
22873
|
+
"po-decisions-resolved",
|
|
22874
|
+
`Resolved Decisions (${resolvedDecisions.length})`,
|
|
22875
|
+
decisionTable(resolvedDecisions),
|
|
22876
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
22877
|
+
);
|
|
22878
|
+
return `
|
|
22879
|
+
<div class="page-header">
|
|
22880
|
+
<h2>Decision Log</h2>
|
|
22881
|
+
<div class="subtitle">Track and manage product decisions</div>
|
|
22882
|
+
</div>
|
|
22883
|
+
${statsCards}
|
|
22884
|
+
${openSection}
|
|
22885
|
+
${resolvedSection}
|
|
22886
|
+
`;
|
|
22887
|
+
}
|
|
22888
|
+
|
|
22889
|
+
// src/web/templates/pages/po/delivery.ts
|
|
22890
|
+
var PO_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
|
|
22891
|
+
"stakeholder-feedback",
|
|
22892
|
+
"acceptance-result",
|
|
22893
|
+
"priority-change",
|
|
22894
|
+
"market-insight"
|
|
22895
|
+
]);
|
|
22896
|
+
function progressBar2(pct) {
|
|
22897
|
+
return `<div class="sprint-progress-bar">
|
|
22898
|
+
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
22899
|
+
<span class="sprint-progress-label">${pct}%</span>
|
|
22900
|
+
</div>`;
|
|
22901
|
+
}
|
|
22902
|
+
function poDeliveryPage(ctx) {
|
|
22903
|
+
const data = getSprintSummaryData(ctx.store);
|
|
22904
|
+
if (!data) {
|
|
22905
|
+
return `
|
|
22906
|
+
<div class="page-header">
|
|
22907
|
+
<h2>Value Delivery</h2>
|
|
22908
|
+
<div class="subtitle">Sprint progress and PO contributions</div>
|
|
22909
|
+
</div>
|
|
22910
|
+
<div class="empty">
|
|
22911
|
+
<h3>No Active Sprint</h3>
|
|
22912
|
+
<p>No active sprint found. Create a sprint and set its status to "active" to track delivery.</p>
|
|
22913
|
+
</div>`;
|
|
22914
|
+
}
|
|
22915
|
+
const doneFeatures = data.workItems.items.filter(
|
|
22916
|
+
(w) => w.type === "feature" && ["done", "closed", "resolved"].includes(w.status)
|
|
22917
|
+
);
|
|
22918
|
+
function findContributions(items, parentId) {
|
|
22919
|
+
const result = [];
|
|
22920
|
+
for (const item of items) {
|
|
22921
|
+
if (item.type === "contribution" && PO_CONTRIBUTION_TYPES.has(item.id.split("-").slice(0, -1).join("-") || "")) {
|
|
22922
|
+
result.push({ id: item.id, title: item.title, type: item.type, status: item.status, parentId });
|
|
22923
|
+
}
|
|
22924
|
+
if (PO_CONTRIBUTION_TYPES.has(item.type)) {
|
|
22925
|
+
result.push({ id: item.id, title: item.title, type: item.type, status: item.status, parentId });
|
|
22926
|
+
}
|
|
22927
|
+
if (item.children) {
|
|
22928
|
+
result.push(...findContributions(item.children, item.id));
|
|
22929
|
+
}
|
|
22930
|
+
}
|
|
22931
|
+
return result;
|
|
22932
|
+
}
|
|
22933
|
+
const allDocs = ctx.store.list();
|
|
22934
|
+
const poContributions = allDocs.filter((d) => PO_CONTRIBUTION_TYPES.has(d.frontmatter.type));
|
|
22935
|
+
const statsCards = `
|
|
22936
|
+
<div class="cards">
|
|
22937
|
+
<div class="card">
|
|
22938
|
+
<div class="card-label">Sprint Progress</div>
|
|
22939
|
+
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
22940
|
+
<div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
|
|
22941
|
+
</div>
|
|
22942
|
+
<div class="card">
|
|
22943
|
+
<div class="card-label">Days Remaining</div>
|
|
22944
|
+
<div class="card-value">${data.timeline.daysRemaining}</div>
|
|
22945
|
+
<div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
|
|
22946
|
+
</div>
|
|
22947
|
+
<div class="card">
|
|
22948
|
+
<div class="card-label">Features Done</div>
|
|
22949
|
+
<div class="card-value">${doneFeatures.length}</div>
|
|
22950
|
+
<div class="card-sub">this sprint</div>
|
|
22951
|
+
</div>
|
|
22952
|
+
<div class="card">
|
|
22953
|
+
<div class="card-label">PO Contributions</div>
|
|
22954
|
+
<div class="card-value">${poContributions.length}</div>
|
|
22955
|
+
<div class="card-sub">feedback, reviews, insights</div>
|
|
22956
|
+
</div>
|
|
22957
|
+
</div>`;
|
|
22958
|
+
const sprintHeader = `
|
|
22959
|
+
<div class="sprint-goal">
|
|
22960
|
+
<strong>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</strong>
|
|
22961
|
+
${data.sprint.goal ? ` | ${escapeHtml(data.sprint.goal)}` : ""}
|
|
22962
|
+
</div>`;
|
|
22963
|
+
const featuresSection = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
22964
|
+
"po-delivery-epics",
|
|
22965
|
+
"Linked Epics",
|
|
22966
|
+
`<div class="table-wrap">
|
|
22967
|
+
<table>
|
|
22968
|
+
<thead>
|
|
22969
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks Done</th></tr>
|
|
22970
|
+
</thead>
|
|
22971
|
+
<tbody>
|
|
22972
|
+
${data.linkedEpics.map((e) => `
|
|
22973
|
+
<tr>
|
|
22974
|
+
<td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
|
|
22975
|
+
<td>${escapeHtml(e.title)}</td>
|
|
22976
|
+
<td>${statusBadge(e.status)}</td>
|
|
22977
|
+
<td>${e.tasksDone} / ${e.tasksTotal}</td>
|
|
22978
|
+
</tr>`).join("")}
|
|
22979
|
+
</tbody>
|
|
22980
|
+
</table>
|
|
22981
|
+
</div>`,
|
|
22982
|
+
{ titleTag: "h3" }
|
|
22983
|
+
) : "";
|
|
22984
|
+
const contributionsSection = poContributions.length > 0 ? collapsibleSection(
|
|
22985
|
+
"po-delivery-contributions",
|
|
22986
|
+
`PO Contributions (${poContributions.length})`,
|
|
22987
|
+
`<div class="table-wrap">
|
|
22988
|
+
<table>
|
|
22989
|
+
<thead>
|
|
22990
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Date</th></tr>
|
|
22991
|
+
</thead>
|
|
22992
|
+
<tbody>
|
|
22993
|
+
${poContributions.map((d) => `
|
|
22994
|
+
<tr>
|
|
22995
|
+
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
22996
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
22997
|
+
<td>${escapeHtml(typeLabel(d.frontmatter.type))}</td>
|
|
22998
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
22999
|
+
<td>${formatDate(d.frontmatter.updated ?? d.frontmatter.created)}</td>
|
|
23000
|
+
</tr>`).join("")}
|
|
23001
|
+
</tbody>
|
|
23002
|
+
</table>
|
|
23003
|
+
</div>`,
|
|
23004
|
+
{ titleTag: "h3" }
|
|
23005
|
+
) : "";
|
|
23006
|
+
return `
|
|
23007
|
+
<div class="page-header">
|
|
23008
|
+
<h2>Value Delivery</h2>
|
|
23009
|
+
<div class="subtitle">Sprint progress and feature delivery tracking</div>
|
|
23010
|
+
</div>
|
|
23011
|
+
${sprintHeader}
|
|
23012
|
+
${progressBar2(data.workItems.completionPct)}
|
|
23013
|
+
${statsCards}
|
|
23014
|
+
${featuresSection}
|
|
23015
|
+
${contributionsSection}
|
|
23016
|
+
`;
|
|
23017
|
+
}
|
|
23018
|
+
|
|
23019
|
+
// src/web/templates/pages/po/stakeholders.ts
|
|
23020
|
+
function poStakeholdersPage(ctx) {
|
|
23021
|
+
const garReport = getGarData(ctx.store, ctx.projectName);
|
|
23022
|
+
const upcoming = getUpcomingData(ctx.store);
|
|
23023
|
+
const actions = ctx.store.list({ type: "action" });
|
|
23024
|
+
const openActions = actions.filter(
|
|
23025
|
+
(d) => !["done", "closed", "resolved", "cancelled"].includes(d.frontmatter.status)
|
|
23026
|
+
);
|
|
23027
|
+
const questions = ctx.store.list({ type: "question" });
|
|
23028
|
+
const openQuestions = questions.filter((d) => d.frontmatter.status === "open");
|
|
23029
|
+
const garDotClass = `dot-${garReport.overall}`;
|
|
23030
|
+
const garAreaCards = garReport.areas.map(
|
|
23031
|
+
(area) => `
|
|
23032
|
+
<div class="gar-area">
|
|
23033
|
+
<div class="area-header">
|
|
23034
|
+
<div class="area-dot dot-${area.status}"></div>
|
|
23035
|
+
<div class="area-name">${escapeHtml(area.name)}</div>
|
|
23036
|
+
</div>
|
|
23037
|
+
<div class="area-summary">${escapeHtml(area.summary)}</div>
|
|
23038
|
+
${area.items.length > 0 ? `<ul>${area.items.slice(0, 5).map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.title)}</li>`).join("")}</ul>` : ""}
|
|
23039
|
+
</div>`
|
|
23040
|
+
).join("\n");
|
|
23041
|
+
const garSection = collapsibleSection(
|
|
23042
|
+
"po-stakeholders-gar",
|
|
23043
|
+
"Project Status (GAR)",
|
|
23044
|
+
`<div class="gar-overall">
|
|
23045
|
+
<div class="dot ${garDotClass}"></div>
|
|
23046
|
+
<div class="label">Overall: ${escapeHtml(garReport.overall)}</div>
|
|
23047
|
+
</div>
|
|
23048
|
+
<div class="gar-areas">${garAreaCards}</div>`,
|
|
23049
|
+
{ titleTag: "h3" }
|
|
23050
|
+
);
|
|
23051
|
+
const actionsSection = openActions.length > 0 ? collapsibleSection(
|
|
23052
|
+
"po-stakeholders-actions",
|
|
23053
|
+
`Open Action Items (${openActions.length})`,
|
|
23054
|
+
`<div class="table-wrap">
|
|
23055
|
+
<table>
|
|
23056
|
+
<thead>
|
|
23057
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Owner</th><th>Due Date</th></tr>
|
|
23058
|
+
</thead>
|
|
23059
|
+
<tbody>
|
|
23060
|
+
${openActions.map((d) => `
|
|
23061
|
+
<tr>
|
|
23062
|
+
<td><a href="/docs/action/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
23063
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
23064
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
23065
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23066
|
+
<td>${d.frontmatter.dueDate ? formatDate(d.frontmatter.dueDate) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23067
|
+
</tr>`).join("")}
|
|
23068
|
+
</tbody>
|
|
23069
|
+
</table>
|
|
23070
|
+
</div>`,
|
|
23071
|
+
{ titleTag: "h3" }
|
|
23072
|
+
) : "";
|
|
23073
|
+
const questionsSection = openQuestions.length > 0 ? collapsibleSection(
|
|
23074
|
+
"po-stakeholders-questions",
|
|
23075
|
+
`Questions Needing Input (${openQuestions.length})`,
|
|
23076
|
+
`<div class="table-wrap">
|
|
23077
|
+
<table>
|
|
23078
|
+
<thead>
|
|
23079
|
+
<tr><th>ID</th><th>Title</th><th>Owner</th><th>Created</th></tr>
|
|
23080
|
+
</thead>
|
|
23081
|
+
<tbody>
|
|
23082
|
+
${openQuestions.map((d) => `
|
|
23083
|
+
<tr>
|
|
23084
|
+
<td><a href="/docs/question/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
23085
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
23086
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23087
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
23088
|
+
</tr>`).join("")}
|
|
23089
|
+
</tbody>
|
|
23090
|
+
</table>
|
|
23091
|
+
</div>`,
|
|
23092
|
+
{ titleTag: "h3" }
|
|
23093
|
+
) : "";
|
|
23094
|
+
return `
|
|
23095
|
+
<div class="page-header">
|
|
23096
|
+
<h2>Stakeholder View</h2>
|
|
23097
|
+
<div class="subtitle">Project status overview for stakeholder communication</div>
|
|
23098
|
+
</div>
|
|
23099
|
+
${garSection}
|
|
23100
|
+
${actionsSection}
|
|
23101
|
+
${questionsSection}
|
|
23102
|
+
`;
|
|
23103
|
+
}
|
|
23104
|
+
|
|
23105
|
+
// src/web/persona-configs/po.ts
|
|
23106
|
+
registerPersonaView({
|
|
23107
|
+
shortName: "po",
|
|
23108
|
+
displayName: "Product Owner",
|
|
23109
|
+
description: "Feature delivery, decisions, and stakeholder alignment",
|
|
23110
|
+
color: "#6c8cff",
|
|
23111
|
+
navItems: [
|
|
23112
|
+
{ path: "/po/dashboard", label: "Dashboard" },
|
|
23113
|
+
{ path: "/po/backlog", label: "Product Backlog" },
|
|
23114
|
+
{ path: "/po/decisions", label: "Decision Log" },
|
|
23115
|
+
{ path: "/po/delivery", label: "Value Delivery" },
|
|
23116
|
+
{ path: "/po/stakeholders", label: "Stakeholder View" }
|
|
23117
|
+
]
|
|
23118
|
+
});
|
|
23119
|
+
registerPersonaPage("po", "dashboard", poDashboardPage);
|
|
23120
|
+
registerPersonaPage("po", "backlog", poBacklogPage);
|
|
23121
|
+
registerPersonaPage("po", "decisions", poDecisionsPage);
|
|
23122
|
+
registerPersonaPage("po", "delivery", poDeliveryPage);
|
|
23123
|
+
registerPersonaPage("po", "stakeholders", poStakeholdersPage);
|
|
23124
|
+
|
|
23125
|
+
// src/web/templates/pages/dm/dashboard.ts
|
|
23126
|
+
var DONE_STATUSES4 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23127
|
+
function progressBar3(pct) {
|
|
23128
|
+
return `<div class="sprint-progress-bar">
|
|
23129
|
+
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
23130
|
+
<span class="sprint-progress-label">${pct}%</span>
|
|
23131
|
+
</div>`;
|
|
23132
|
+
}
|
|
23133
|
+
function dmDashboardPage(ctx) {
|
|
23134
|
+
const sprintData = getSprintSummaryData(ctx.store);
|
|
23135
|
+
const upcoming = getUpcomingData(ctx.store);
|
|
23136
|
+
const actions = ctx.store.list({ type: "action" });
|
|
23137
|
+
const openActions = actions.filter((d) => !DONE_STATUSES4.has(d.frontmatter.status));
|
|
23138
|
+
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
23139
|
+
const statsCards = `
|
|
23140
|
+
<div class="cards">
|
|
23141
|
+
<div class="card">
|
|
23142
|
+
<a href="/dm/sprint">
|
|
23143
|
+
<div class="card-label">Sprint Progress</div>
|
|
23144
|
+
<div class="card-value">${sprintData ? `${sprintData.workItems.completionPct}%` : "\u2014"}</div>
|
|
23145
|
+
<div class="card-sub">${sprintData ? `${sprintData.timeline.daysRemaining} days remaining` : "No active sprint"}</div>
|
|
23146
|
+
</a>
|
|
23147
|
+
</div>
|
|
23148
|
+
<div class="card">
|
|
23149
|
+
<a href="/dm/risks">
|
|
23150
|
+
<div class="card-label">Blockers</div>
|
|
23151
|
+
<div class="card-value${(sprintData?.blockers.length ?? 0) > 0 ? " priority-high" : ""}">${sprintData?.blockers.length ?? 0}</div>
|
|
23152
|
+
<div class="card-sub">blocking items</div>
|
|
23153
|
+
</a>
|
|
23154
|
+
</div>
|
|
23155
|
+
<div class="card">
|
|
23156
|
+
<a href="/dm/actions">
|
|
23157
|
+
<div class="card-label">Overdue Actions</div>
|
|
23158
|
+
<div class="card-value${overdueActions.length > 0 ? " priority-high" : ""}">${overdueActions.length}</div>
|
|
23159
|
+
<div class="card-sub">${openActions.length} open total</div>
|
|
23160
|
+
</a>
|
|
23161
|
+
</div>
|
|
23162
|
+
<div class="card">
|
|
23163
|
+
<a href="/dm/meetings">
|
|
23164
|
+
<div class="card-label">Meetings</div>
|
|
23165
|
+
<div class="card-value">${sprintData?.meetings.length ?? 0}</div>
|
|
23166
|
+
<div class="card-sub">this sprint</div>
|
|
23167
|
+
</a>
|
|
23168
|
+
</div>
|
|
23169
|
+
</div>`;
|
|
23170
|
+
const sprintProgress = sprintData ? `
|
|
23171
|
+
<div class="sprint-goal">
|
|
23172
|
+
<strong>${escapeHtml(sprintData.sprint.id)} \u2014 ${escapeHtml(sprintData.sprint.title)}</strong>
|
|
23173
|
+
${sprintData.sprint.goal ? ` | ${escapeHtml(sprintData.sprint.goal)}` : ""}
|
|
23174
|
+
</div>
|
|
23175
|
+
${progressBar3(sprintData.workItems.completionPct)}` : "";
|
|
23176
|
+
const riskItems = [];
|
|
23177
|
+
if (overdueActions.length > 0) riskItems.push(`${overdueActions.length} overdue action(s)`);
|
|
23178
|
+
if ((sprintData?.blockers.length ?? 0) > 0) riskItems.push(`${sprintData.blockers.length} blocker(s)`);
|
|
23179
|
+
if (sprintData && sprintData.timeline.daysRemaining <= 3 && sprintData.workItems.completionPct < 80) {
|
|
23180
|
+
riskItems.push("Sprint deadline approaching with low completion");
|
|
23181
|
+
}
|
|
23182
|
+
const riskSection = riskItems.length > 0 ? `<div class="sprint-goal" style="border-left: 3px solid var(--red);">
|
|
23183
|
+
<strong>Risk Indicators</strong>
|
|
23184
|
+
<ul style="margin: 0.5rem 0 0 1.25rem; font-size: 0.875rem; color: var(--text-dim);">
|
|
23185
|
+
${riskItems.map((r) => `<li>${escapeHtml(r)}</li>`).join("")}
|
|
23186
|
+
</ul>
|
|
23187
|
+
</div>` : "";
|
|
23188
|
+
const dueSoonPreview = upcoming.dueSoonActions.slice(0, 5);
|
|
23189
|
+
const actionsPreview = dueSoonPreview.length > 0 ? collapsibleSection(
|
|
23190
|
+
"dm-dash-actions",
|
|
23191
|
+
"Due Soon Actions",
|
|
23192
|
+
`<div class="table-wrap">
|
|
23193
|
+
<table>
|
|
23194
|
+
<thead>
|
|
23195
|
+
<tr><th>ID</th><th>Title</th><th>Owner</th><th>Due</th><th>Status</th></tr>
|
|
23196
|
+
</thead>
|
|
23197
|
+
<tbody>
|
|
23198
|
+
${dueSoonPreview.map((a) => `
|
|
23199
|
+
<tr>
|
|
23200
|
+
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
23201
|
+
<td>${escapeHtml(a.title)}</td>
|
|
23202
|
+
<td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23203
|
+
<td>${formatDate(a.dueDate)}</td>
|
|
23204
|
+
<td>${statusBadge(a.status)}</td>
|
|
23205
|
+
</tr>`).join("")}
|
|
23206
|
+
</tbody>
|
|
23207
|
+
</table>
|
|
23208
|
+
</div>
|
|
23209
|
+
<p style="margin-top: 0.5rem; font-size: 0.85rem;"><a href="/dm/actions">View all actions →</a></p>`,
|
|
23210
|
+
{ titleTag: "h3" }
|
|
23211
|
+
) : "";
|
|
23212
|
+
return `
|
|
23213
|
+
<div class="page-header">
|
|
23214
|
+
<h2>Delivery Manager Dashboard</h2>
|
|
23215
|
+
<div class="subtitle">Sprint execution, action tracking, and risk management</div>
|
|
23216
|
+
</div>
|
|
23217
|
+
${sprintProgress}
|
|
23218
|
+
${statsCards}
|
|
23219
|
+
${riskSection}
|
|
23220
|
+
${actionsPreview}
|
|
23221
|
+
`;
|
|
23222
|
+
}
|
|
23223
|
+
|
|
23224
|
+
// src/web/templates/pages/dm/sprint.ts
|
|
23225
|
+
function dmSprintPage(ctx) {
|
|
23226
|
+
const data = getSprintSummaryData(ctx.store);
|
|
23227
|
+
return sprintSummaryPage(data);
|
|
23228
|
+
}
|
|
23229
|
+
|
|
23230
|
+
// src/web/templates/pages/dm/actions.ts
|
|
23231
|
+
var DONE_STATUSES5 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23232
|
+
function urgencyBadge2(tier) {
|
|
23233
|
+
const labels = {
|
|
23234
|
+
overdue: "Overdue",
|
|
23235
|
+
"due-3d": "Due in 3d",
|
|
23236
|
+
"due-7d": "Due in 7d",
|
|
23237
|
+
upcoming: "Upcoming",
|
|
23238
|
+
later: "Later"
|
|
23239
|
+
};
|
|
23240
|
+
return `<span class="badge urgency-badge-${tier}">${labels[tier]}</span>`;
|
|
23241
|
+
}
|
|
23242
|
+
function urgencyRowClass2(tier) {
|
|
23243
|
+
if (tier === "overdue") return " urgency-row-overdue";
|
|
23244
|
+
if (tier === "due-3d") return " urgency-row-due-3d";
|
|
23245
|
+
if (tier === "due-7d") return " urgency-row-due-7d";
|
|
23246
|
+
return "";
|
|
23247
|
+
}
|
|
23248
|
+
function dmActionsPage(ctx) {
|
|
23249
|
+
const upcoming = getUpcomingData(ctx.store);
|
|
23250
|
+
const allActions = ctx.store.list({ type: "action" });
|
|
23251
|
+
const openActions = allActions.filter((d) => !DONE_STATUSES5.has(d.frontmatter.status));
|
|
23252
|
+
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
23253
|
+
const dueThisWeek = upcoming.dueSoonActions.filter((a) => a.urgency === "due-3d" || a.urgency === "due-7d");
|
|
23254
|
+
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
23255
|
+
const statsCards = `
|
|
23256
|
+
<div class="cards">
|
|
23257
|
+
<div class="card">
|
|
23258
|
+
<div class="card-label">Total Open</div>
|
|
23259
|
+
<div class="card-value">${openActions.length}</div>
|
|
23260
|
+
<div class="card-sub">${allActions.length} total actions</div>
|
|
23261
|
+
</div>
|
|
23262
|
+
<div class="card">
|
|
23263
|
+
<div class="card-label">Overdue</div>
|
|
23264
|
+
<div class="card-value${overdueActions.length > 0 ? " priority-high" : ""}">${overdueActions.length}</div>
|
|
23265
|
+
<div class="card-sub">past due date</div>
|
|
23266
|
+
</div>
|
|
23267
|
+
<div class="card">
|
|
23268
|
+
<div class="card-label">Due This Week</div>
|
|
23269
|
+
<div class="card-value${dueThisWeek.length > 0 ? " priority-medium" : ""}">${dueThisWeek.length}</div>
|
|
23270
|
+
<div class="card-sub">next 7 days</div>
|
|
23271
|
+
</div>
|
|
23272
|
+
<div class="card">
|
|
23273
|
+
<div class="card-label">Unowned</div>
|
|
23274
|
+
<div class="card-value${unownedActions.length > 0 ? " priority-medium" : ""}">${unownedActions.length}</div>
|
|
23275
|
+
<div class="card-sub">need assignment</div>
|
|
23276
|
+
</div>
|
|
23277
|
+
</div>`;
|
|
23278
|
+
const dueSoonSection = upcoming.dueSoonActions.length > 0 ? collapsibleSection(
|
|
23279
|
+
"dm-actions-due",
|
|
23280
|
+
`Actions by Due Date (${upcoming.dueSoonActions.length})`,
|
|
23281
|
+
`<div class="table-wrap">
|
|
23282
|
+
<table>
|
|
23283
|
+
<thead>
|
|
23284
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Owner</th><th>Due Date</th><th>Urgency</th></tr>
|
|
23285
|
+
</thead>
|
|
23286
|
+
<tbody>
|
|
23287
|
+
${upcoming.dueSoonActions.map((a) => `
|
|
23288
|
+
<tr class="${urgencyRowClass2(a.urgency)}">
|
|
23289
|
+
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
23290
|
+
<td>${escapeHtml(a.title)}</td>
|
|
23291
|
+
<td>${statusBadge(a.status)}</td>
|
|
23292
|
+
<td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23293
|
+
<td>${formatDate(a.dueDate)}</td>
|
|
23294
|
+
<td>${urgencyBadge2(a.urgency)}</td>
|
|
23295
|
+
</tr>`).join("")}
|
|
23296
|
+
</tbody>
|
|
23297
|
+
</table>
|
|
23298
|
+
</div>`,
|
|
23299
|
+
{ titleTag: "h3" }
|
|
23300
|
+
) : "";
|
|
23301
|
+
const noDueDateActions = openActions.filter((d) => !d.frontmatter.dueDate);
|
|
23302
|
+
const noDueDateSection = noDueDateActions.length > 0 ? collapsibleSection(
|
|
23303
|
+
"dm-actions-nodate",
|
|
23304
|
+
`Actions Without Due Date (${noDueDateActions.length})`,
|
|
23305
|
+
`<div class="table-wrap">
|
|
23306
|
+
<table>
|
|
23307
|
+
<thead>
|
|
23308
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Owner</th><th>Created</th></tr>
|
|
23309
|
+
</thead>
|
|
23310
|
+
<tbody>
|
|
23311
|
+
${noDueDateActions.map((d) => `
|
|
23312
|
+
<tr>
|
|
23313
|
+
<td><a href="/docs/action/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
23314
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
23315
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
23316
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23317
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
23318
|
+
</tr>`).join("")}
|
|
23319
|
+
</tbody>
|
|
23320
|
+
</table>
|
|
23321
|
+
</div>`,
|
|
23322
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
23323
|
+
) : "";
|
|
23324
|
+
return `
|
|
23325
|
+
<div class="page-header">
|
|
23326
|
+
<h2>Action Tracker</h2>
|
|
23327
|
+
<div class="subtitle">Track and manage all action items across the project</div>
|
|
23328
|
+
</div>
|
|
23329
|
+
${statsCards}
|
|
23330
|
+
${dueSoonSection}
|
|
23331
|
+
${noDueDateSection}
|
|
23332
|
+
`;
|
|
23333
|
+
}
|
|
23334
|
+
|
|
23335
|
+
// src/web/templates/pages/dm/risks.ts
|
|
23336
|
+
var DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23337
|
+
function dmRisksPage(ctx) {
|
|
23338
|
+
const allDocs = ctx.store.list();
|
|
23339
|
+
const upcoming = getUpcomingData(ctx.store);
|
|
23340
|
+
const healthMetrics = collectHealthMetrics(ctx.store);
|
|
23341
|
+
const healthReport = evaluateHealth(ctx.projectName, healthMetrics);
|
|
23342
|
+
const blockedItems = allDocs.filter((d) => d.frontmatter.status === "blocked");
|
|
23343
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
23344
|
+
const todayMs = new Date(today).getTime();
|
|
23345
|
+
const fourteenDaysMs = 14 * 864e5;
|
|
23346
|
+
const agingItems = allDocs.filter((d) => {
|
|
23347
|
+
if (DONE_STATUSES6.has(d.frontmatter.status)) return false;
|
|
23348
|
+
if (!["action", "question"].includes(d.frontmatter.type)) return false;
|
|
23349
|
+
const createdMs = new Date(d.frontmatter.created).getTime();
|
|
23350
|
+
return todayMs - createdMs > fourteenDaysMs;
|
|
23351
|
+
});
|
|
23352
|
+
const statsCards = `
|
|
23353
|
+
<div class="cards">
|
|
23354
|
+
<div class="card">
|
|
23355
|
+
<div class="card-label">Blocked Items</div>
|
|
23356
|
+
<div class="card-value${blockedItems.length > 0 ? " priority-high" : ""}">${blockedItems.length}</div>
|
|
23357
|
+
<div class="card-sub">currently blocked</div>
|
|
23358
|
+
</div>
|
|
23359
|
+
<div class="card">
|
|
23360
|
+
<div class="card-label">Aging Items</div>
|
|
23361
|
+
<div class="card-value${agingItems.length > 0 ? " priority-medium" : ""}">${agingItems.length}</div>
|
|
23362
|
+
<div class="card-sub">>14 days open</div>
|
|
23363
|
+
</div>
|
|
23364
|
+
<div class="card">
|
|
23365
|
+
<div class="card-label">Health</div>
|
|
23366
|
+
<div class="card-value"><span class="dot-${healthReport.overall}" style="display:inline-block;width:12px;height:12px;border-radius:50%;margin-right:0.3rem;vertical-align:middle;"></span>${healthReport.overall}</div>
|
|
23367
|
+
<div class="card-sub">overall project health</div>
|
|
23368
|
+
</div>
|
|
23369
|
+
<div class="card">
|
|
23370
|
+
<div class="card-label">Overdue Actions</div>
|
|
23371
|
+
<div class="card-value${upcoming.dueSoonActions.filter((a) => a.urgency === "overdue").length > 0 ? " priority-high" : ""}">${upcoming.dueSoonActions.filter((a) => a.urgency === "overdue").length}</div>
|
|
23372
|
+
<div class="card-sub">past due date</div>
|
|
23373
|
+
</div>
|
|
23374
|
+
</div>`;
|
|
23375
|
+
const blockedSection = blockedItems.length > 0 ? collapsibleSection(
|
|
23376
|
+
"dm-risks-blocked",
|
|
23377
|
+
`Blocked Items (${blockedItems.length})`,
|
|
23378
|
+
`<div class="table-wrap">
|
|
23379
|
+
<table>
|
|
23380
|
+
<thead>
|
|
23381
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Owner</th><th>Created</th></tr>
|
|
23382
|
+
</thead>
|
|
23383
|
+
<tbody>
|
|
23384
|
+
${blockedItems.map((d) => `
|
|
23385
|
+
<tr>
|
|
23386
|
+
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
23387
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
23388
|
+
<td>${escapeHtml(typeLabel(d.frontmatter.type))}</td>
|
|
23389
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23390
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
23391
|
+
</tr>`).join("")}
|
|
23392
|
+
</tbody>
|
|
23393
|
+
</table>
|
|
23394
|
+
</div>`,
|
|
23395
|
+
{ titleTag: "h3" }
|
|
23396
|
+
) : "";
|
|
23397
|
+
const agingSection = agingItems.length > 0 ? collapsibleSection(
|
|
23398
|
+
"dm-risks-aging",
|
|
23399
|
+
`Aging Items (${agingItems.length})`,
|
|
23400
|
+
`<div class="table-wrap">
|
|
23401
|
+
<table>
|
|
23402
|
+
<thead>
|
|
23403
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Created</th><th>Age</th></tr>
|
|
23404
|
+
</thead>
|
|
23405
|
+
<tbody>
|
|
23406
|
+
${agingItems.map((d) => {
|
|
23407
|
+
const ageDays = Math.floor((todayMs - new Date(d.frontmatter.created).getTime()) / 864e5);
|
|
23408
|
+
return `
|
|
23409
|
+
<tr>
|
|
23410
|
+
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
23411
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
23412
|
+
<td>${escapeHtml(typeLabel(d.frontmatter.type))}</td>
|
|
23413
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
23414
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
23415
|
+
<td><span class="${ageDays > 30 ? "priority-high" : "priority-medium"}">${ageDays}d</span></td>
|
|
23416
|
+
</tr>`;
|
|
23417
|
+
}).join("")}
|
|
23418
|
+
</tbody>
|
|
23419
|
+
</table>
|
|
23420
|
+
</div>`,
|
|
23421
|
+
{ titleTag: "h3" }
|
|
23422
|
+
) : "";
|
|
23423
|
+
const healthSection = collapsibleSection(
|
|
23424
|
+
"dm-risks-health",
|
|
23425
|
+
"Health Overview",
|
|
23426
|
+
`<div class="gar-overall">
|
|
23427
|
+
<div class="dot dot-${healthReport.overall}"></div>
|
|
23428
|
+
<div class="label">Overall: ${escapeHtml(healthReport.overall)}</div>
|
|
23429
|
+
</div>
|
|
23430
|
+
<div class="gar-areas">
|
|
23431
|
+
${healthReport.completeness.map((cat) => `
|
|
23432
|
+
<div class="gar-area">
|
|
23433
|
+
<div class="area-header">
|
|
23434
|
+
<div class="area-dot dot-${cat.status}"></div>
|
|
23435
|
+
<div class="area-name">${escapeHtml(cat.name)}</div>
|
|
23436
|
+
</div>
|
|
23437
|
+
<div class="area-summary">${escapeHtml(cat.summary)}</div>
|
|
23438
|
+
</div>`).join("")}
|
|
23439
|
+
${healthReport.process.map((cat) => `
|
|
23440
|
+
<div class="gar-area">
|
|
23441
|
+
<div class="area-header">
|
|
23442
|
+
<div class="area-dot dot-${cat.status}"></div>
|
|
23443
|
+
<div class="area-name">${escapeHtml(cat.name)}</div>
|
|
23444
|
+
</div>
|
|
23445
|
+
<div class="area-summary">${escapeHtml(cat.summary)}</div>
|
|
23446
|
+
</div>`).join("")}
|
|
23447
|
+
</div>`,
|
|
23448
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
23449
|
+
);
|
|
23450
|
+
return `
|
|
23451
|
+
<div class="page-header">
|
|
23452
|
+
<h2>Risk & Blockers</h2>
|
|
23453
|
+
<div class="subtitle">Identify and track project risks, blockers, and aging items</div>
|
|
23454
|
+
</div>
|
|
23455
|
+
${statsCards}
|
|
23456
|
+
${blockedSection}
|
|
23457
|
+
${agingSection}
|
|
23458
|
+
${healthSection}
|
|
23459
|
+
`;
|
|
23460
|
+
}
|
|
23461
|
+
|
|
23462
|
+
// src/web/templates/pages/dm/meetings.ts
|
|
23463
|
+
var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23464
|
+
function dmMeetingsPage(ctx) {
|
|
23465
|
+
const meetings = ctx.store.list({ type: "meeting" });
|
|
23466
|
+
const actions = ctx.store.list({ type: "action" });
|
|
23467
|
+
const sortedMeetings = [...meetings].sort((a, b) => {
|
|
23468
|
+
const dateA = a.frontmatter.date ?? a.frontmatter.created;
|
|
23469
|
+
const dateB = b.frontmatter.date ?? b.frontmatter.created;
|
|
23470
|
+
return dateB.localeCompare(dateA);
|
|
23471
|
+
});
|
|
23472
|
+
const meetingActionMap = /* @__PURE__ */ new Map();
|
|
23473
|
+
for (const meeting of meetings) {
|
|
23474
|
+
const mid = meeting.frontmatter.id;
|
|
23475
|
+
const relatedActions = actions.filter((a) => {
|
|
23476
|
+
const tags = a.frontmatter.tags ?? [];
|
|
23477
|
+
const hasMeetingTag = tags.some((t) => t.startsWith("meeting:") && t.slice(8) === mid);
|
|
23478
|
+
const mentionsInContent = (a.content ?? "").includes(mid);
|
|
23479
|
+
const source = a.frontmatter.source;
|
|
23480
|
+
const fromMeeting = typeof source === "string" && source.includes(mid);
|
|
23481
|
+
return hasMeetingTag || mentionsInContent || fromMeeting;
|
|
23482
|
+
});
|
|
23483
|
+
if (relatedActions.length > 0) {
|
|
23484
|
+
meetingActionMap.set(mid, relatedActions);
|
|
23485
|
+
}
|
|
23486
|
+
}
|
|
23487
|
+
const statsCards = `
|
|
23488
|
+
<div class="cards">
|
|
23489
|
+
<div class="card">
|
|
23490
|
+
<div class="card-label">Total Meetings</div>
|
|
23491
|
+
<div class="card-value">${meetings.length}</div>
|
|
23492
|
+
<div class="card-sub">recorded</div>
|
|
23493
|
+
</div>
|
|
23494
|
+
<div class="card">
|
|
23495
|
+
<div class="card-label">With Actions</div>
|
|
23496
|
+
<div class="card-value">${meetingActionMap.size}</div>
|
|
23497
|
+
<div class="card-sub">meetings with linked actions</div>
|
|
23498
|
+
</div>
|
|
23499
|
+
</div>`;
|
|
23500
|
+
const meetingsTable = sortedMeetings.length > 0 ? `<div class="table-wrap">
|
|
23501
|
+
<table>
|
|
23502
|
+
<thead>
|
|
23503
|
+
<tr><th>Date</th><th>ID</th><th>Title</th><th>Actions</th></tr>
|
|
23504
|
+
</thead>
|
|
23505
|
+
<tbody>
|
|
23506
|
+
${sortedMeetings.map((m) => {
|
|
23507
|
+
const date5 = m.frontmatter.date ?? m.frontmatter.created;
|
|
23508
|
+
const relatedActions = meetingActionMap.get(m.frontmatter.id) ?? [];
|
|
23509
|
+
const openCount = relatedActions.filter((a) => !DONE_STATUSES7.has(a.frontmatter.status)).length;
|
|
23510
|
+
return `
|
|
23511
|
+
<tr>
|
|
23512
|
+
<td>${formatDate(date5)}</td>
|
|
23513
|
+
<td><a href="/docs/meeting/${escapeHtml(m.frontmatter.id)}">${escapeHtml(m.frontmatter.id)}</a></td>
|
|
23514
|
+
<td>${escapeHtml(m.frontmatter.title)}</td>
|
|
23515
|
+
<td>${relatedActions.length > 0 ? `${relatedActions.length} (${openCount} open)` : '<span class="text-dim">\u2014</span>'}</td>
|
|
23516
|
+
</tr>`;
|
|
23517
|
+
}).join("")}
|
|
23518
|
+
</tbody>
|
|
23519
|
+
</table>
|
|
23520
|
+
</div>` : '<div class="empty"><p>No meetings recorded.</p></div>';
|
|
23521
|
+
const recentMeetingActions = [];
|
|
23522
|
+
for (const [mid, acts] of meetingActionMap) {
|
|
23523
|
+
for (const act of acts) {
|
|
23524
|
+
if (!DONE_STATUSES7.has(act.frontmatter.status)) {
|
|
23525
|
+
recentMeetingActions.push({ action: act, meetingId: mid });
|
|
23526
|
+
}
|
|
23527
|
+
}
|
|
23528
|
+
}
|
|
23529
|
+
recentMeetingActions.sort((a, b) => {
|
|
23530
|
+
const da = a.action.frontmatter.dueDate ?? a.action.frontmatter.created;
|
|
23531
|
+
const db = b.action.frontmatter.dueDate ?? b.action.frontmatter.created;
|
|
23532
|
+
return da.localeCompare(db);
|
|
23533
|
+
});
|
|
23534
|
+
const actionItemsSection = recentMeetingActions.length > 0 ? collapsibleSection(
|
|
23535
|
+
"dm-meetings-actions",
|
|
23536
|
+
`Open Meeting Action Items (${recentMeetingActions.length})`,
|
|
23537
|
+
`<div class="table-wrap">
|
|
23538
|
+
<table>
|
|
23539
|
+
<thead>
|
|
23540
|
+
<tr><th>Action ID</th><th>Title</th><th>Meeting</th><th>Owner</th><th>Due</th><th>Status</th></tr>
|
|
23541
|
+
</thead>
|
|
23542
|
+
<tbody>
|
|
23543
|
+
${recentMeetingActions.map(({ action: a, meetingId }) => `
|
|
23544
|
+
<tr>
|
|
23545
|
+
<td><a href="/docs/action/${escapeHtml(a.frontmatter.id)}">${escapeHtml(a.frontmatter.id)}</a></td>
|
|
23546
|
+
<td>${escapeHtml(a.frontmatter.title)}</td>
|
|
23547
|
+
<td><a href="/docs/meeting/${escapeHtml(meetingId)}">${escapeHtml(meetingId)}</a></td>
|
|
23548
|
+
<td>${a.frontmatter.owner ? escapeHtml(a.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23549
|
+
<td>${a.frontmatter.dueDate ? formatDate(a.frontmatter.dueDate) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23550
|
+
<td>${statusBadge(a.frontmatter.status)}</td>
|
|
23551
|
+
</tr>`).join("")}
|
|
23552
|
+
</tbody>
|
|
23553
|
+
</table>
|
|
23554
|
+
</div>`,
|
|
23555
|
+
{ titleTag: "h3" }
|
|
23556
|
+
) : "";
|
|
23557
|
+
return `
|
|
23558
|
+
<div class="page-header">
|
|
23559
|
+
<h2>Meetings</h2>
|
|
23560
|
+
<div class="subtitle">Meeting log and cross-referenced action items</div>
|
|
23561
|
+
</div>
|
|
23562
|
+
${statsCards}
|
|
23563
|
+
${collapsibleSection("dm-meetings-log", `Meeting Log (${sortedMeetings.length})`, meetingsTable, { titleTag: "h3" })}
|
|
23564
|
+
${actionItemsSection}
|
|
23565
|
+
`;
|
|
23566
|
+
}
|
|
23567
|
+
|
|
23568
|
+
// src/web/templates/pages/dm/governance.ts
|
|
23569
|
+
function dmGovernancePage(ctx) {
|
|
23570
|
+
const garReport = getGarData(ctx.store, ctx.projectName);
|
|
23571
|
+
const healthMetrics = collectHealthMetrics(ctx.store);
|
|
23572
|
+
const healthReport = evaluateHealth(ctx.projectName, healthMetrics);
|
|
23573
|
+
const garContent = garPage(garReport);
|
|
23574
|
+
const healthContent = healthPage(healthReport, healthMetrics);
|
|
23575
|
+
return `
|
|
23576
|
+
<div class="page-header">
|
|
23577
|
+
<h2>Governance</h2>
|
|
23578
|
+
<div class="subtitle">GAR report and health check combined view</div>
|
|
23579
|
+
</div>
|
|
23580
|
+
${collapsibleSection("dm-gov-gar", "GAR Report", garContent, { titleTag: "h3" })}
|
|
23581
|
+
${collapsibleSection("dm-gov-health", "Health Check", healthContent, { titleTag: "h3" })}
|
|
23582
|
+
`;
|
|
23583
|
+
}
|
|
23584
|
+
|
|
23585
|
+
// src/web/persona-configs/dm.ts
|
|
23586
|
+
registerPersonaView({
|
|
23587
|
+
shortName: "dm",
|
|
23588
|
+
displayName: "Delivery Manager",
|
|
23589
|
+
description: "Sprint execution, action tracking, and risk management",
|
|
23590
|
+
color: "#34d399",
|
|
23591
|
+
navItems: [
|
|
23592
|
+
{ path: "/dm/dashboard", label: "Dashboard" },
|
|
23593
|
+
{ path: "/dm/sprint", label: "Sprint Execution" },
|
|
23594
|
+
{ path: "/dm/actions", label: "Action Tracker" },
|
|
23595
|
+
{ path: "/dm/risks", label: "Risk & Blockers" },
|
|
23596
|
+
{ path: "/dm/meetings", label: "Meetings" },
|
|
23597
|
+
{ path: "/dm/governance", label: "Governance" }
|
|
23598
|
+
]
|
|
23599
|
+
});
|
|
23600
|
+
registerPersonaPage("dm", "dashboard", dmDashboardPage);
|
|
23601
|
+
registerPersonaPage("dm", "sprint", dmSprintPage);
|
|
23602
|
+
registerPersonaPage("dm", "actions", dmActionsPage);
|
|
23603
|
+
registerPersonaPage("dm", "risks", dmRisksPage);
|
|
23604
|
+
registerPersonaPage("dm", "meetings", dmMeetingsPage);
|
|
23605
|
+
registerPersonaPage("dm", "governance", dmGovernancePage);
|
|
23606
|
+
|
|
23607
|
+
// src/web/templates/pages/tl/dashboard.ts
|
|
23608
|
+
var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23609
|
+
function tlDashboardPage(ctx) {
|
|
23610
|
+
const epics = ctx.store.list({ type: "epic" });
|
|
23611
|
+
const tasks = ctx.store.list({ type: "task" });
|
|
23612
|
+
const decisions = ctx.store.list({ type: "decision" });
|
|
23613
|
+
const questions = ctx.store.list({ type: "question" });
|
|
23614
|
+
const diagrams = getDiagramData(ctx.store);
|
|
23615
|
+
const openEpics = epics.filter((d) => !DONE_STATUSES8.has(d.frontmatter.status));
|
|
23616
|
+
const openTasks = tasks.filter((d) => !DONE_STATUSES8.has(d.frontmatter.status));
|
|
23617
|
+
const technicalDecisions = decisions.filter((d) => {
|
|
23618
|
+
const tags = d.frontmatter.tags ?? [];
|
|
23619
|
+
return tags.some((t) => t.toLowerCase().includes("technical") || t.toLowerCase().includes("architecture"));
|
|
23620
|
+
});
|
|
23621
|
+
const openTechDecisions = technicalDecisions.filter((d) => !DONE_STATUSES8.has(d.frontmatter.status));
|
|
23622
|
+
const pendingDecisions = openTechDecisions.length > 0 ? openTechDecisions : decisions.filter((d) => !DONE_STATUSES8.has(d.frontmatter.status));
|
|
23623
|
+
const statsCards = `
|
|
23624
|
+
<div class="cards">
|
|
23625
|
+
<div class="card">
|
|
23626
|
+
<a href="/tl/backlog">
|
|
23627
|
+
<div class="card-label">Open Epics</div>
|
|
23628
|
+
<div class="card-value">${openEpics.length}</div>
|
|
23629
|
+
<div class="card-sub">${epics.length} total</div>
|
|
23630
|
+
</a>
|
|
23631
|
+
</div>
|
|
23632
|
+
<div class="card">
|
|
23633
|
+
<a href="/tl/backlog">
|
|
23634
|
+
<div class="card-label">Open Tasks</div>
|
|
23635
|
+
<div class="card-value">${openTasks.length}</div>
|
|
23636
|
+
<div class="card-sub">${tasks.length} total</div>
|
|
23637
|
+
</a>
|
|
23638
|
+
</div>
|
|
23639
|
+
<div class="card">
|
|
23640
|
+
<a href="/tl/decisions">
|
|
23641
|
+
<div class="card-label">Pending Decisions</div>
|
|
23642
|
+
<div class="card-value${pendingDecisions.length > 0 ? " priority-medium" : ""}">${pendingDecisions.length}</div>
|
|
23643
|
+
<div class="card-sub">needing resolution</div>
|
|
23644
|
+
</a>
|
|
23645
|
+
</div>
|
|
23646
|
+
<div class="card">
|
|
23647
|
+
<a href="/tl/sprint">
|
|
23648
|
+
<div class="card-label">Blocked</div>
|
|
23649
|
+
<div class="card-value${tasks.filter((t) => t.frontmatter.status === "blocked").length > 0 ? " priority-high" : ""}">${tasks.filter((t) => t.frontmatter.status === "blocked").length}</div>
|
|
23650
|
+
<div class="card-sub">blocked tasks</div>
|
|
23651
|
+
</a>
|
|
23652
|
+
</div>
|
|
23653
|
+
</div>`;
|
|
23654
|
+
const diagramSection = collapsibleSection(
|
|
23655
|
+
"tl-dash-diagram",
|
|
23656
|
+
"Architecture Relationships",
|
|
23657
|
+
buildArtifactFlowchart(diagrams),
|
|
23658
|
+
{ titleTag: "h3" }
|
|
23659
|
+
);
|
|
23660
|
+
return `
|
|
23661
|
+
<div class="page-header">
|
|
23662
|
+
<h2>Technical Lead Dashboard</h2>
|
|
23663
|
+
<div class="subtitle">Technical backlog, architecture decisions, and sprint work</div>
|
|
23664
|
+
</div>
|
|
23665
|
+
${statsCards}
|
|
23666
|
+
${diagramSection}
|
|
23667
|
+
`;
|
|
23668
|
+
}
|
|
23669
|
+
|
|
23670
|
+
// src/web/templates/pages/tl/backlog.ts
|
|
23671
|
+
var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23672
|
+
function tlBacklogPage(ctx) {
|
|
23673
|
+
const epics = ctx.store.list({ type: "epic" });
|
|
23674
|
+
const tasks = ctx.store.list({ type: "task" });
|
|
23675
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
23676
|
+
for (const task of tasks) {
|
|
23677
|
+
const tags = task.frontmatter.tags ?? [];
|
|
23678
|
+
for (const tag of tags) {
|
|
23679
|
+
if (tag.startsWith("epic:")) {
|
|
23680
|
+
const epicId = tag.slice(5);
|
|
23681
|
+
const existing = epicToTasks.get(epicId) ?? [];
|
|
23682
|
+
existing.push(task);
|
|
23683
|
+
epicToTasks.set(epicId, existing);
|
|
23684
|
+
}
|
|
23685
|
+
}
|
|
23686
|
+
}
|
|
23687
|
+
const epicFeatureMap = /* @__PURE__ */ new Map();
|
|
23688
|
+
for (const epic of epics) {
|
|
23689
|
+
const linked = epic.frontmatter.linkedFeature;
|
|
23690
|
+
const featureIds = Array.isArray(linked) ? linked.map(String) : linked ? [String(linked)] : [];
|
|
23691
|
+
epicFeatureMap.set(epic.frontmatter.id, featureIds);
|
|
23692
|
+
}
|
|
23693
|
+
const statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
23694
|
+
const sortedEpics = [...epics].sort((a, b) => {
|
|
23695
|
+
const sa = statusOrder[a.frontmatter.status] ?? 3;
|
|
23696
|
+
const sb = statusOrder[b.frontmatter.status] ?? 3;
|
|
23697
|
+
if (sa !== sb) return sa - sb;
|
|
23698
|
+
return a.frontmatter.id.localeCompare(b.frontmatter.id);
|
|
23699
|
+
});
|
|
23700
|
+
const epicsTable = sortedEpics.length > 0 ? `<div class="table-wrap">
|
|
23701
|
+
<table>
|
|
23702
|
+
<thead>
|
|
23703
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th><th>Linked Feature</th></tr>
|
|
23704
|
+
</thead>
|
|
23705
|
+
<tbody>
|
|
23706
|
+
${sortedEpics.map((e) => {
|
|
23707
|
+
const eTasks = epicToTasks.get(e.frontmatter.id) ?? [];
|
|
23708
|
+
const done = eTasks.filter((t) => DONE_STATUSES9.has(t.frontmatter.status)).length;
|
|
23709
|
+
const featureIds = epicFeatureMap.get(e.frontmatter.id) ?? [];
|
|
23710
|
+
const featureLinks = featureIds.map((fid) => `<a href="/docs/feature/${escapeHtml(fid)}">${escapeHtml(fid)}</a>`).join(", ");
|
|
23711
|
+
return `
|
|
23712
|
+
<tr>
|
|
23713
|
+
<td><a href="/docs/epic/${escapeHtml(e.frontmatter.id)}">${escapeHtml(e.frontmatter.id)}</a></td>
|
|
23714
|
+
<td>${escapeHtml(e.frontmatter.title)}</td>
|
|
23715
|
+
<td>${statusBadge(e.frontmatter.status)}</td>
|
|
23716
|
+
<td>${done}/${eTasks.length}</td>
|
|
23717
|
+
<td>${featureLinks || '<span class="text-dim">\u2014</span>'}</td>
|
|
23718
|
+
</tr>`;
|
|
23719
|
+
}).join("")}
|
|
23720
|
+
</tbody>
|
|
23721
|
+
</table>
|
|
23722
|
+
</div>` : '<div class="empty"><p>No epics found.</p></div>';
|
|
23723
|
+
const assignedTaskIds = /* @__PURE__ */ new Set();
|
|
23724
|
+
for (const taskList of epicToTasks.values()) {
|
|
23725
|
+
for (const t of taskList) assignedTaskIds.add(t.frontmatter.id);
|
|
23726
|
+
}
|
|
23727
|
+
const unassignedTasks = tasks.filter(
|
|
23728
|
+
(t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES9.has(t.frontmatter.status)
|
|
23729
|
+
);
|
|
23730
|
+
const unassignedSection = unassignedTasks.length > 0 ? collapsibleSection(
|
|
23731
|
+
"tl-backlog-unassigned",
|
|
23732
|
+
`Unassigned Tasks (${unassignedTasks.length})`,
|
|
23733
|
+
`<div class="table-wrap">
|
|
23734
|
+
<table>
|
|
23735
|
+
<thead>
|
|
23736
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Owner</th><th>Created</th></tr>
|
|
23737
|
+
</thead>
|
|
23738
|
+
<tbody>
|
|
23739
|
+
${unassignedTasks.map((t) => `
|
|
23740
|
+
<tr>
|
|
23741
|
+
<td><a href="/docs/task/${escapeHtml(t.frontmatter.id)}">${escapeHtml(t.frontmatter.id)}</a></td>
|
|
23742
|
+
<td>${escapeHtml(t.frontmatter.title)}</td>
|
|
23743
|
+
<td>${statusBadge(t.frontmatter.status)}</td>
|
|
23744
|
+
<td>${t.frontmatter.owner ? escapeHtml(t.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23745
|
+
<td>${formatDate(t.frontmatter.created)}</td>
|
|
23746
|
+
</tr>`).join("")}
|
|
23747
|
+
</tbody>
|
|
23748
|
+
</table>
|
|
23749
|
+
</div>`,
|
|
23750
|
+
{ titleTag: "h3" }
|
|
23751
|
+
) : "";
|
|
23752
|
+
const taskBoard = getBoardData(ctx.store, "task");
|
|
23753
|
+
const boardHtml = taskBoard.columns.length > 0 ? `<div class="board">
|
|
23754
|
+
${taskBoard.columns.map((col) => `
|
|
23755
|
+
<div class="board-column">
|
|
23756
|
+
<div class="board-column-header">
|
|
23757
|
+
<span>${escapeHtml(col.status)}</span>
|
|
23758
|
+
<span class="count">${col.docs.length}</span>
|
|
23759
|
+
</div>
|
|
23760
|
+
${col.docs.map((d) => `
|
|
23761
|
+
<div class="board-card">
|
|
23762
|
+
<a href="/docs/task/${escapeHtml(d.frontmatter.id)}">
|
|
23763
|
+
<div class="bc-id">${escapeHtml(d.frontmatter.id)}</div>
|
|
23764
|
+
<div class="bc-title">${escapeHtml(d.frontmatter.title)}</div>
|
|
23765
|
+
${d.frontmatter.owner ? `<div class="bc-owner">${escapeHtml(d.frontmatter.owner)}</div>` : ""}
|
|
23766
|
+
</a>
|
|
23767
|
+
</div>`).join("")}
|
|
23768
|
+
</div>`).join("")}
|
|
23769
|
+
</div>` : "";
|
|
23770
|
+
const boardSection = boardHtml ? collapsibleSection("tl-backlog-board", "Task Board", boardHtml, { titleTag: "h3", defaultCollapsed: true }) : "";
|
|
23771
|
+
return `
|
|
23772
|
+
<div class="page-header">
|
|
23773
|
+
<h2>Technical Backlog</h2>
|
|
23774
|
+
<div class="subtitle">${epics.length} epics, ${tasks.length} tasks</div>
|
|
23775
|
+
</div>
|
|
23776
|
+
${collapsibleSection("tl-backlog-epics", `Epics (${epics.length})`, epicsTable, { titleTag: "h3" })}
|
|
23777
|
+
${unassignedSection}
|
|
23778
|
+
${boardSection}
|
|
23779
|
+
`;
|
|
23780
|
+
}
|
|
23781
|
+
|
|
23782
|
+
// src/web/templates/pages/tl/sprint.ts
|
|
23783
|
+
var TL_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
|
|
23784
|
+
"action-result",
|
|
23785
|
+
"spike-findings",
|
|
23786
|
+
"technical-assessment",
|
|
23787
|
+
"architecture-review"
|
|
23788
|
+
]);
|
|
23789
|
+
var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23790
|
+
function progressBar4(pct) {
|
|
23791
|
+
return `<div class="sprint-progress-bar">
|
|
23792
|
+
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
23793
|
+
<span class="sprint-progress-label">${pct}%</span>
|
|
23794
|
+
</div>`;
|
|
23795
|
+
}
|
|
23796
|
+
function tlSprintPage(ctx) {
|
|
23797
|
+
const data = getSprintSummaryData(ctx.store);
|
|
23798
|
+
if (!data) {
|
|
23799
|
+
return `
|
|
23800
|
+
<div class="page-header">
|
|
23801
|
+
<h2>Sprint Work</h2>
|
|
23802
|
+
<div class="subtitle">Technical sprint items and contributions</div>
|
|
23803
|
+
</div>
|
|
23804
|
+
<div class="empty">
|
|
23805
|
+
<h3>No Active Sprint</h3>
|
|
23806
|
+
<p>No active sprint found. Create a sprint and set its status to "active" to track sprint work.</p>
|
|
23807
|
+
</div>`;
|
|
23808
|
+
}
|
|
23809
|
+
const techTypes = /* @__PURE__ */ new Set(["epic", "task"]);
|
|
23810
|
+
const techItems = data.workItems.items.filter((w) => techTypes.has(w.type));
|
|
23811
|
+
const techDone = techItems.filter((w) => DONE_STATUSES10.has(w.status)).length;
|
|
23812
|
+
const allDocs = ctx.store.list();
|
|
23813
|
+
const tlContributions = allDocs.filter((d) => TL_CONTRIBUTION_TYPES.has(d.frontmatter.type));
|
|
23814
|
+
const statsCards = `
|
|
23815
|
+
<div class="cards">
|
|
23816
|
+
<div class="card">
|
|
23817
|
+
<div class="card-label">Sprint Progress</div>
|
|
23818
|
+
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
23819
|
+
<div class="card-sub">${data.timeline.daysRemaining} days remaining</div>
|
|
23820
|
+
</div>
|
|
23821
|
+
<div class="card">
|
|
23822
|
+
<div class="card-label">Tech Items</div>
|
|
23823
|
+
<div class="card-value">${techItems.length}</div>
|
|
23824
|
+
<div class="card-sub">${techDone} done</div>
|
|
23825
|
+
</div>
|
|
23826
|
+
<div class="card">
|
|
23827
|
+
<div class="card-label">Epics</div>
|
|
23828
|
+
<div class="card-value">${data.linkedEpics.length}</div>
|
|
23829
|
+
<div class="card-sub">linked to sprint</div>
|
|
23830
|
+
</div>
|
|
23831
|
+
<div class="card">
|
|
23832
|
+
<div class="card-label">TL Contributions</div>
|
|
23833
|
+
<div class="card-value">${tlContributions.length}</div>
|
|
23834
|
+
<div class="card-sub">reviews, spikes, assessments</div>
|
|
23835
|
+
</div>
|
|
23836
|
+
</div>`;
|
|
23837
|
+
const sprintHeader = `
|
|
23838
|
+
<div class="sprint-goal">
|
|
23839
|
+
<strong>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</strong>
|
|
23840
|
+
${data.sprint.goal ? ` | ${escapeHtml(data.sprint.goal)}` : ""}
|
|
23841
|
+
</div>`;
|
|
23842
|
+
const workItemsSection = techItems.length > 0 ? collapsibleSection(
|
|
23843
|
+
"tl-sprint-items",
|
|
23844
|
+
`Sprint Work Items (${techItems.length})`,
|
|
23845
|
+
`<div class="table-wrap">
|
|
23846
|
+
<table>
|
|
23847
|
+
<thead>
|
|
23848
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Stream</th></tr>
|
|
23849
|
+
</thead>
|
|
23850
|
+
<tbody>
|
|
23851
|
+
${techItems.map((w) => `
|
|
23852
|
+
<tr>
|
|
23853
|
+
<td><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
23854
|
+
<td>${escapeHtml(w.title)}</td>
|
|
23855
|
+
<td>${escapeHtml(typeLabel(w.type))}</td>
|
|
23856
|
+
<td>${statusBadge(w.status)}</td>
|
|
23857
|
+
<td>${w.workStream ? `<span class="badge badge-subtle">${escapeHtml(w.workStream)}</span>` : '<span class="text-dim">\u2014</span>'}</td>
|
|
23858
|
+
</tr>`).join("")}
|
|
23859
|
+
</tbody>
|
|
23860
|
+
</table>
|
|
23861
|
+
</div>`,
|
|
23862
|
+
{ titleTag: "h3" }
|
|
23863
|
+
) : "";
|
|
23864
|
+
const contributionsSection = tlContributions.length > 0 ? collapsibleSection(
|
|
23865
|
+
"tl-sprint-contributions",
|
|
23866
|
+
`TL Contributions (${tlContributions.length})`,
|
|
23867
|
+
`<div class="table-wrap">
|
|
23868
|
+
<table>
|
|
23869
|
+
<thead>
|
|
23870
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Date</th></tr>
|
|
23871
|
+
</thead>
|
|
23872
|
+
<tbody>
|
|
23873
|
+
${tlContributions.map((d) => `
|
|
23874
|
+
<tr>
|
|
23875
|
+
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
23876
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
23877
|
+
<td>${escapeHtml(typeLabel(d.frontmatter.type))}</td>
|
|
23878
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
23879
|
+
<td>${formatDate(d.frontmatter.updated ?? d.frontmatter.created)}</td>
|
|
23880
|
+
</tr>`).join("")}
|
|
23881
|
+
</tbody>
|
|
23882
|
+
</table>
|
|
23883
|
+
</div>`,
|
|
23884
|
+
{ titleTag: "h3" }
|
|
23885
|
+
) : "";
|
|
23886
|
+
const epicsSection = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
23887
|
+
"tl-sprint-epics",
|
|
23888
|
+
"Linked Epics",
|
|
23889
|
+
`<div class="table-wrap">
|
|
23890
|
+
<table>
|
|
23891
|
+
<thead>
|
|
23892
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks Done</th></tr>
|
|
23893
|
+
</thead>
|
|
23894
|
+
<tbody>
|
|
23895
|
+
${data.linkedEpics.map((e) => `
|
|
23896
|
+
<tr>
|
|
23897
|
+
<td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
|
|
23898
|
+
<td>${escapeHtml(e.title)}</td>
|
|
23899
|
+
<td>${statusBadge(e.status)}</td>
|
|
23900
|
+
<td>${e.tasksDone} / ${e.tasksTotal}</td>
|
|
23901
|
+
</tr>`).join("")}
|
|
23902
|
+
</tbody>
|
|
23903
|
+
</table>
|
|
23904
|
+
</div>`,
|
|
23905
|
+
{ titleTag: "h3" }
|
|
23906
|
+
) : "";
|
|
23907
|
+
return `
|
|
23908
|
+
<div class="page-header">
|
|
23909
|
+
<h2>Sprint Work</h2>
|
|
23910
|
+
<div class="subtitle">Technical sprint items and contributions</div>
|
|
23911
|
+
</div>
|
|
23912
|
+
${sprintHeader}
|
|
23913
|
+
${progressBar4(data.workItems.completionPct)}
|
|
23914
|
+
${statsCards}
|
|
23915
|
+
${workItemsSection}
|
|
23916
|
+
${epicsSection}
|
|
23917
|
+
${contributionsSection}
|
|
23918
|
+
`;
|
|
23919
|
+
}
|
|
23920
|
+
|
|
23921
|
+
// src/web/templates/pages/tl/decisions.ts
|
|
23922
|
+
var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
23923
|
+
function tlDecisionsPage(ctx) {
|
|
23924
|
+
const decisions = ctx.store.list({ type: "decision" });
|
|
23925
|
+
const questions = ctx.store.list({ type: "question" });
|
|
23926
|
+
const technicalDecisions = decisions.filter((d) => {
|
|
23927
|
+
const tags = d.frontmatter.tags ?? [];
|
|
23928
|
+
return tags.some((t) => {
|
|
23929
|
+
const lower = t.toLowerCase();
|
|
23930
|
+
return lower.includes("technical") || lower.includes("architecture") || lower.includes("design");
|
|
23931
|
+
});
|
|
23932
|
+
});
|
|
23933
|
+
const displayDecisions = technicalDecisions.length > 0 ? technicalDecisions : decisions;
|
|
23934
|
+
const isFiltered = technicalDecisions.length > 0;
|
|
23935
|
+
const openDecisions = displayDecisions.filter((d) => !DONE_STATUSES11.has(d.frontmatter.status));
|
|
23936
|
+
const resolvedDecisions = displayDecisions.filter((d) => DONE_STATUSES11.has(d.frontmatter.status));
|
|
23937
|
+
const technicalQuestions = questions.filter((d) => {
|
|
23938
|
+
const tags = d.frontmatter.tags ?? [];
|
|
23939
|
+
return tags.some((t) => {
|
|
23940
|
+
const lower = t.toLowerCase();
|
|
23941
|
+
return lower.includes("technical") || lower.includes("architecture") || lower.includes("design");
|
|
23942
|
+
});
|
|
23943
|
+
});
|
|
23944
|
+
const displayQuestions = technicalQuestions.length > 0 ? technicalQuestions : questions;
|
|
23945
|
+
const openQuestions = displayQuestions.filter((d) => d.frontmatter.status === "open");
|
|
23946
|
+
const statsCards = `
|
|
23947
|
+
<div class="cards">
|
|
23948
|
+
<div class="card">
|
|
23949
|
+
<div class="card-label">Open Decisions</div>
|
|
23950
|
+
<div class="card-value${openDecisions.length > 0 ? " priority-medium" : ""}">${openDecisions.length}</div>
|
|
23951
|
+
<div class="card-sub">${isFiltered ? "technical" : "all"} decisions</div>
|
|
23952
|
+
</div>
|
|
23953
|
+
<div class="card">
|
|
23954
|
+
<div class="card-label">Resolved</div>
|
|
23955
|
+
<div class="card-value">${resolvedDecisions.length}</div>
|
|
23956
|
+
<div class="card-sub">decisions made</div>
|
|
23957
|
+
</div>
|
|
23958
|
+
<div class="card">
|
|
23959
|
+
<div class="card-label">Open Questions</div>
|
|
23960
|
+
<div class="card-value${openQuestions.length > 0 ? " priority-medium" : ""}">${openQuestions.length}</div>
|
|
23961
|
+
<div class="card-sub">${technicalQuestions.length > 0 ? "technical" : "all"} questions</div>
|
|
23962
|
+
</div>
|
|
23963
|
+
</div>`;
|
|
23964
|
+
function decisionTable(docs) {
|
|
23965
|
+
if (docs.length === 0) return '<div class="empty"><p>None found.</p></div>';
|
|
23966
|
+
return `<div class="table-wrap">
|
|
23967
|
+
<table>
|
|
23968
|
+
<thead>
|
|
23969
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Owner</th><th>Tags</th><th>Created</th></tr>
|
|
23970
|
+
</thead>
|
|
23971
|
+
<tbody>
|
|
23972
|
+
${docs.map((d) => {
|
|
23973
|
+
const tags = d.frontmatter.tags ?? [];
|
|
23974
|
+
return `
|
|
23975
|
+
<tr>
|
|
23976
|
+
<td><a href="/docs/decision/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
23977
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
23978
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
23979
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
23980
|
+
<td>${tags.length > 0 ? tags.map((t) => `<span class="signal-tag">${escapeHtml(t)}</span>`).join(" ") : '<span class="text-dim">\u2014</span>'}</td>
|
|
23981
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
23982
|
+
</tr>`;
|
|
23983
|
+
}).join("")}
|
|
23984
|
+
</tbody>
|
|
23985
|
+
</table>
|
|
23986
|
+
</div>`;
|
|
23987
|
+
}
|
|
23988
|
+
const openSection = collapsibleSection(
|
|
23989
|
+
"tl-decisions-open",
|
|
23990
|
+
`Open Decisions (${openDecisions.length})`,
|
|
23991
|
+
decisionTable(openDecisions),
|
|
23992
|
+
{ titleTag: "h3" }
|
|
23993
|
+
);
|
|
23994
|
+
const resolvedSection = collapsibleSection(
|
|
23995
|
+
"tl-decisions-resolved",
|
|
23996
|
+
`Resolved Decisions (${resolvedDecisions.length})`,
|
|
23997
|
+
decisionTable(resolvedDecisions),
|
|
23998
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
23999
|
+
);
|
|
24000
|
+
const questionsSection = openQuestions.length > 0 ? collapsibleSection(
|
|
24001
|
+
"tl-decisions-questions",
|
|
24002
|
+
`Open Questions (${openQuestions.length})`,
|
|
24003
|
+
`<div class="table-wrap">
|
|
24004
|
+
<table>
|
|
24005
|
+
<thead>
|
|
24006
|
+
<tr><th>ID</th><th>Title</th><th>Owner</th><th>Created</th></tr>
|
|
24007
|
+
</thead>
|
|
24008
|
+
<tbody>
|
|
24009
|
+
${openQuestions.map((d) => `
|
|
24010
|
+
<tr>
|
|
24011
|
+
<td><a href="/docs/question/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
24012
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
24013
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
24014
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
24015
|
+
</tr>`).join("")}
|
|
24016
|
+
</tbody>
|
|
24017
|
+
</table>
|
|
24018
|
+
</div>`,
|
|
24019
|
+
{ titleTag: "h3" }
|
|
24020
|
+
) : "";
|
|
24021
|
+
return `
|
|
24022
|
+
<div class="page-header">
|
|
24023
|
+
<h2>Architecture Decisions</h2>
|
|
24024
|
+
<div class="subtitle">${isFiltered ? "Technical" : "All"} decisions and open questions</div>
|
|
24025
|
+
</div>
|
|
24026
|
+
${statsCards}
|
|
24027
|
+
${openSection}
|
|
24028
|
+
${questionsSection}
|
|
24029
|
+
${resolvedSection}
|
|
24030
|
+
`;
|
|
24031
|
+
}
|
|
24032
|
+
|
|
24033
|
+
// src/web/templates/pages/tl/health.ts
|
|
24034
|
+
function tlHealthPage(ctx) {
|
|
24035
|
+
const healthMetrics = collectHealthMetrics(ctx.store);
|
|
24036
|
+
const healthReport = evaluateHealth(ctx.projectName, healthMetrics);
|
|
24037
|
+
const upcoming = getUpcomingData(ctx.store);
|
|
24038
|
+
const tasks = ctx.store.list({ type: "task" });
|
|
24039
|
+
const blockedTasks = tasks.filter((t) => t.frontmatter.status === "blocked");
|
|
24040
|
+
const highPriorityBlocked = blockedTasks.filter((t) => {
|
|
24041
|
+
const p = t.frontmatter.priority?.toLowerCase();
|
|
24042
|
+
return p === "critical" || p === "high";
|
|
24043
|
+
});
|
|
24044
|
+
const techTrending = upcoming.trending.filter(
|
|
24045
|
+
(t) => ["task", "action"].includes(t.type)
|
|
24046
|
+
);
|
|
24047
|
+
const statsCards = `
|
|
24048
|
+
<div class="cards">
|
|
24049
|
+
<div class="card">
|
|
24050
|
+
<div class="card-label">Health</div>
|
|
24051
|
+
<div class="card-value"><span class="dot-${healthReport.overall}" style="display:inline-block;width:12px;height:12px;border-radius:50%;margin-right:0.3rem;vertical-align:middle;"></span>${healthReport.overall}</div>
|
|
24052
|
+
<div class="card-sub">overall project health</div>
|
|
24053
|
+
</div>
|
|
24054
|
+
<div class="card">
|
|
24055
|
+
<div class="card-label">Blocked Tasks</div>
|
|
24056
|
+
<div class="card-value${blockedTasks.length > 0 ? " priority-high" : ""}">${blockedTasks.length}</div>
|
|
24057
|
+
<div class="card-sub">${highPriorityBlocked.length} high priority</div>
|
|
24058
|
+
</div>
|
|
24059
|
+
<div class="card">
|
|
24060
|
+
<div class="card-label">Completeness</div>
|
|
24061
|
+
<div class="card-value">${healthReport.completeness.filter((c) => c.status === "green").length}/${healthReport.completeness.length}</div>
|
|
24062
|
+
<div class="card-sub">categories green</div>
|
|
24063
|
+
</div>
|
|
24064
|
+
<div class="card">
|
|
24065
|
+
<div class="card-label">Process</div>
|
|
24066
|
+
<div class="card-value">${healthReport.process.filter((c) => c.status === "green").length}/${healthReport.process.length}</div>
|
|
24067
|
+
<div class="card-sub">metrics green</div>
|
|
24068
|
+
</div>
|
|
24069
|
+
</div>`;
|
|
24070
|
+
const gaugeData = Object.entries(healthMetrics.completeness).map(([name, cat]) => ({
|
|
24071
|
+
name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
24072
|
+
complete: cat.complete,
|
|
24073
|
+
total: cat.total
|
|
24074
|
+
}));
|
|
24075
|
+
const gaugeSection = collapsibleSection(
|
|
24076
|
+
"tl-health-gauge",
|
|
24077
|
+
"Completeness Overview",
|
|
24078
|
+
buildHealthGauge(gaugeData),
|
|
24079
|
+
{ titleTag: "h3" }
|
|
24080
|
+
);
|
|
24081
|
+
const processSection = collapsibleSection(
|
|
24082
|
+
"tl-health-process",
|
|
24083
|
+
"Process Health",
|
|
24084
|
+
`<div class="gar-areas">
|
|
24085
|
+
${healthReport.process.map((cat) => `
|
|
24086
|
+
<div class="gar-area">
|
|
24087
|
+
<div class="area-header">
|
|
24088
|
+
<div class="area-dot dot-${cat.status}"></div>
|
|
24089
|
+
<div class="area-name">${escapeHtml(cat.name)}</div>
|
|
24090
|
+
</div>
|
|
24091
|
+
<div class="area-summary">${escapeHtml(cat.summary)}</div>
|
|
24092
|
+
${cat.items.length > 0 ? `<ul>${cat.items.slice(0, 5).map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
|
|
24093
|
+
</div>`).join("")}
|
|
24094
|
+
</div>`,
|
|
24095
|
+
{ titleTag: "h3" }
|
|
24096
|
+
);
|
|
24097
|
+
const blockedSection = blockedTasks.length > 0 ? collapsibleSection(
|
|
24098
|
+
"tl-health-blocked",
|
|
24099
|
+
`Blocked Tasks (${blockedTasks.length})`,
|
|
24100
|
+
`<div class="table-wrap">
|
|
24101
|
+
<table>
|
|
24102
|
+
<thead>
|
|
24103
|
+
<tr><th>ID</th><th>Title</th><th>Priority</th><th>Owner</th><th>Created</th></tr>
|
|
24104
|
+
</thead>
|
|
24105
|
+
<tbody>
|
|
24106
|
+
${blockedTasks.map((t) => `
|
|
24107
|
+
<tr>
|
|
24108
|
+
<td><a href="/docs/task/${escapeHtml(t.frontmatter.id)}">${escapeHtml(t.frontmatter.id)}</a></td>
|
|
24109
|
+
<td>${escapeHtml(t.frontmatter.title)}</td>
|
|
24110
|
+
<td>${t.frontmatter.priority ? `<span class="${priorityClass(t.frontmatter.priority)}">${escapeHtml(t.frontmatter.priority)}</span>` : '<span class="text-dim">\u2014</span>'}</td>
|
|
24111
|
+
<td>${t.frontmatter.owner ? escapeHtml(t.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
24112
|
+
<td>${formatDate(t.frontmatter.created)}</td>
|
|
24113
|
+
</tr>`).join("")}
|
|
24114
|
+
</tbody>
|
|
24115
|
+
</table>
|
|
24116
|
+
</div>`,
|
|
24117
|
+
{ titleTag: "h3" }
|
|
24118
|
+
) : "";
|
|
24119
|
+
const trendingSection = techTrending.length > 0 ? collapsibleSection(
|
|
24120
|
+
"tl-health-trending",
|
|
24121
|
+
"Trending Technical Items",
|
|
24122
|
+
`<div class="table-wrap">
|
|
24123
|
+
<table>
|
|
24124
|
+
<thead>
|
|
24125
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Score</th><th>Signals</th></tr>
|
|
24126
|
+
</thead>
|
|
24127
|
+
<tbody>
|
|
24128
|
+
${techTrending.slice(0, 10).map((t) => `
|
|
24129
|
+
<tr>
|
|
24130
|
+
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
24131
|
+
<td>${escapeHtml(t.title)}</td>
|
|
24132
|
+
<td>${escapeHtml(typeLabel(t.type))}</td>
|
|
24133
|
+
<td><span class="trending-score">${t.score}</span></td>
|
|
24134
|
+
<td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
|
|
24135
|
+
</tr>`).join("")}
|
|
24136
|
+
</tbody>
|
|
24137
|
+
</table>
|
|
24138
|
+
</div>`,
|
|
24139
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
24140
|
+
) : "";
|
|
24141
|
+
return `
|
|
24142
|
+
<div class="page-header">
|
|
24143
|
+
<h2>Technical Health</h2>
|
|
24144
|
+
<div class="subtitle">Project health, completeness metrics, and blocked items</div>
|
|
24145
|
+
</div>
|
|
24146
|
+
${statsCards}
|
|
24147
|
+
${gaugeSection}
|
|
24148
|
+
${processSection}
|
|
24149
|
+
${blockedSection}
|
|
24150
|
+
${trendingSection}
|
|
24151
|
+
`;
|
|
24152
|
+
}
|
|
24153
|
+
function priorityClass(p) {
|
|
24154
|
+
const lower = p.toLowerCase();
|
|
24155
|
+
if (lower === "critical" || lower === "high") return "priority-high";
|
|
24156
|
+
if (lower === "medium") return "priority-medium";
|
|
24157
|
+
if (lower === "low") return "priority-low";
|
|
24158
|
+
return "";
|
|
24159
|
+
}
|
|
24160
|
+
|
|
24161
|
+
// src/web/persona-configs/tl.ts
|
|
24162
|
+
registerPersonaView({
|
|
24163
|
+
shortName: "tl",
|
|
24164
|
+
displayName: "Technical Lead",
|
|
24165
|
+
description: "Technical backlog, architecture decisions, and sprint work",
|
|
24166
|
+
color: "#fbbf24",
|
|
24167
|
+
navItems: [
|
|
24168
|
+
{ path: "/tl/dashboard", label: "Dashboard" },
|
|
24169
|
+
{ path: "/tl/backlog", label: "Technical Backlog" },
|
|
24170
|
+
{ path: "/tl/sprint", label: "Sprint Work" },
|
|
24171
|
+
{ path: "/tl/decisions", label: "Architecture Decisions" },
|
|
24172
|
+
{ path: "/tl/health", label: "Technical Health" }
|
|
24173
|
+
]
|
|
24174
|
+
});
|
|
24175
|
+
registerPersonaPage("tl", "dashboard", tlDashboardPage);
|
|
24176
|
+
registerPersonaPage("tl", "backlog", tlBacklogPage);
|
|
24177
|
+
registerPersonaPage("tl", "sprint", tlSprintPage);
|
|
24178
|
+
registerPersonaPage("tl", "decisions", tlDecisionsPage);
|
|
24179
|
+
registerPersonaPage("tl", "health", tlHealthPage);
|
|
24180
|
+
|
|
24181
|
+
// src/web/router.ts
|
|
24182
|
+
function buildPersonaLayoutOpts(persona, activePath) {
|
|
24183
|
+
const switcherHtml = renderPersonaSwitcher(persona, activePath);
|
|
24184
|
+
const view = persona ? getPersonaView(persona) : void 0;
|
|
24185
|
+
if (!view) {
|
|
24186
|
+
return { personaSwitcherHtml: switcherHtml };
|
|
24187
|
+
}
|
|
24188
|
+
const isActive = (href) => activePath === href || href !== "/" && activePath.startsWith(href) ? " active" : "";
|
|
24189
|
+
const personaLinks = view.navItems.map(
|
|
24190
|
+
(item) => `<a href="${item.path}" class="${isActive(item.path)}">${escapeHtml(item.label)}</a>`
|
|
24191
|
+
).join("\n ");
|
|
24192
|
+
const navHtml = `
|
|
24193
|
+
${personaLinks}
|
|
24194
|
+
<div class="nav-group">
|
|
24195
|
+
<div class="nav-group-label">Admin</div>
|
|
24196
|
+
<a href="/">Full Dashboard</a>
|
|
24197
|
+
</div>`;
|
|
24198
|
+
return {
|
|
24199
|
+
personaSwitcherHtml: switcherHtml,
|
|
24200
|
+
personaNavHtml: navHtml,
|
|
24201
|
+
personaAccentColor: view.color
|
|
24202
|
+
};
|
|
24203
|
+
}
|
|
24204
|
+
var sprintSummaryCache = /* @__PURE__ */ new Map();
|
|
24205
|
+
function handleRequest(req, res, store, projectName, navGroups) {
|
|
24206
|
+
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
24207
|
+
const pathname = parsed.pathname;
|
|
24208
|
+
const navTypes = store.registeredTypes;
|
|
24209
|
+
const persona = parsePersonaFromPath(pathname);
|
|
24210
|
+
const personaOpts = buildPersonaLayoutOpts(persona, pathname);
|
|
24211
|
+
try {
|
|
24212
|
+
if (pathname === "/styles.css") {
|
|
24213
|
+
res.writeHead(200, {
|
|
24214
|
+
"Content-Type": "text/css",
|
|
24215
|
+
"Cache-Control": "public, max-age=300"
|
|
24216
|
+
});
|
|
24217
|
+
res.end(renderStyles());
|
|
24218
|
+
return;
|
|
24219
|
+
}
|
|
24220
|
+
const personaRootMatch = pathname.match(/^\/(po|dm|tl)$/);
|
|
24221
|
+
if (personaRootMatch) {
|
|
24222
|
+
res.writeHead(302, { Location: `/${personaRootMatch[1]}/dashboard` });
|
|
24223
|
+
res.end();
|
|
24224
|
+
return;
|
|
24225
|
+
}
|
|
24226
|
+
const personaPageMatch = pathname.match(/^\/(po|dm|tl)\/([a-z-]+)$/);
|
|
24227
|
+
if (personaPageMatch) {
|
|
24228
|
+
const [, personaKey, pageId] = personaPageMatch;
|
|
24229
|
+
const pPersona = personaKey;
|
|
24230
|
+
const renderer = getPersonaPageRenderer(personaKey, pageId);
|
|
24231
|
+
const view = getPersonaView(pPersona);
|
|
24232
|
+
const pOpts = buildPersonaLayoutOpts(pPersona, pathname);
|
|
24233
|
+
if (renderer) {
|
|
24234
|
+
const body = renderer({ store, projectName });
|
|
24235
|
+
respond(
|
|
24236
|
+
res,
|
|
24237
|
+
layout(
|
|
24238
|
+
{
|
|
24239
|
+
title: `${view?.displayName ?? personaKey.toUpperCase()} \u2014 ${pageId}`,
|
|
24240
|
+
activePath: pathname,
|
|
24241
|
+
projectName,
|
|
24242
|
+
navGroups,
|
|
24243
|
+
persona: pPersona,
|
|
24244
|
+
...pOpts
|
|
24245
|
+
},
|
|
24246
|
+
body
|
|
24247
|
+
)
|
|
24248
|
+
);
|
|
24249
|
+
} else {
|
|
24250
|
+
const body = `
|
|
24251
|
+
<div class="persona-placeholder">
|
|
24252
|
+
<h3>Coming Soon</h3>
|
|
24253
|
+
<p>The <strong>${pageId}</strong> page for ${view?.displayName ?? personaKey.toUpperCase()} is under construction.</p>
|
|
24254
|
+
<p><a href="/${personaKey}/dashboard">Back to dashboard</a></p>
|
|
24255
|
+
</div>`;
|
|
24256
|
+
respond(
|
|
24257
|
+
res,
|
|
24258
|
+
layout(
|
|
24259
|
+
{
|
|
24260
|
+
title: `${view?.displayName ?? personaKey.toUpperCase()} \u2014 ${pageId}`,
|
|
24261
|
+
activePath: pathname,
|
|
24262
|
+
projectName,
|
|
24263
|
+
navGroups,
|
|
24264
|
+
persona: pPersona,
|
|
24265
|
+
...pOpts
|
|
24266
|
+
},
|
|
24267
|
+
body
|
|
24268
|
+
)
|
|
24269
|
+
);
|
|
24270
|
+
}
|
|
24271
|
+
return;
|
|
24272
|
+
}
|
|
24273
|
+
if (pathname === "/") {
|
|
24274
|
+
const data = getOverviewData(store);
|
|
24275
|
+
const diagrams = getDiagramData(store);
|
|
24276
|
+
const body = overviewPage(data, diagrams, navGroups);
|
|
24277
|
+
const banner = renderPersonaBanner();
|
|
24278
|
+
respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups, ...personaOpts, bodyPrefix: banner }, body));
|
|
24279
|
+
return;
|
|
24280
|
+
}
|
|
24281
|
+
if (pathname === "/timeline") {
|
|
24282
|
+
const diagrams = getDiagramData(store);
|
|
24283
|
+
const body = timelinePage(diagrams);
|
|
24284
|
+
respond(res, layout({ title: "Timeline", activePath: "/timeline", projectName, navGroups, ...personaOpts, mainClass: "expanded" }, body));
|
|
24285
|
+
return;
|
|
24286
|
+
}
|
|
24287
|
+
if (pathname === "/gar") {
|
|
24288
|
+
const report = getGarData(store, projectName);
|
|
24289
|
+
const body = garPage(report);
|
|
24290
|
+
respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups, ...personaOpts }, body));
|
|
24291
|
+
return;
|
|
24292
|
+
}
|
|
24293
|
+
if (pathname === "/health") {
|
|
24294
|
+
const healthMetrics = collectHealthMetrics(store);
|
|
24295
|
+
const report = evaluateHealth(projectName, healthMetrics);
|
|
24296
|
+
const body = healthPage(report, healthMetrics);
|
|
24297
|
+
respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups, ...personaOpts }, body));
|
|
24298
|
+
return;
|
|
24299
|
+
}
|
|
24300
|
+
if (pathname === "/upcoming") {
|
|
24301
|
+
const data = getUpcomingData(store);
|
|
24302
|
+
const body = upcomingPage(data);
|
|
24303
|
+
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups, ...personaOpts }, body));
|
|
24304
|
+
return;
|
|
24305
|
+
}
|
|
24306
|
+
if (pathname === "/sprint-summary" && req.method === "GET") {
|
|
24307
|
+
const sprintId = parsed.searchParams.get("sprint") ?? void 0;
|
|
24308
|
+
const data = getSprintSummaryData(store, sprintId);
|
|
24309
|
+
const cached2 = data ? sprintSummaryCache.get(data.sprint.id) : void 0;
|
|
24310
|
+
const body = sprintSummaryPage(data, cached2 ? { html: cached2.html, generatedAt: cached2.generatedAt } : void 0);
|
|
24311
|
+
respond(res, layout({ title: "Sprint Summary", activePath: "/sprint-summary", projectName, navGroups, ...personaOpts }, body));
|
|
24312
|
+
return;
|
|
24313
|
+
}
|
|
24314
|
+
if (pathname === "/api/sprint-summary" && req.method === "POST") {
|
|
24315
|
+
let bodyStr = "";
|
|
24316
|
+
req.on("data", (chunk) => {
|
|
24317
|
+
bodyStr += chunk;
|
|
24318
|
+
});
|
|
24319
|
+
req.on("end", async () => {
|
|
24320
|
+
try {
|
|
24321
|
+
const { sprintId, persona: personaKey } = JSON.parse(bodyStr || "{}");
|
|
24322
|
+
const data = getSprintSummaryData(store, sprintId);
|
|
24323
|
+
if (!data) {
|
|
24324
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
24325
|
+
res.end(JSON.stringify({ error: "Sprint not found" }));
|
|
24326
|
+
return;
|
|
24327
|
+
}
|
|
24328
|
+
const personaDef = personaKey ? getPersona(personaKey) : void 0;
|
|
24329
|
+
const summary = await generateSprintSummary(data, personaDef?.systemPrompt);
|
|
24330
|
+
const html = renderMarkdown(summary);
|
|
24331
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
24332
|
+
sprintSummaryCache.set(data.sprint.id, { html, generatedAt });
|
|
24333
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
24334
|
+
res.end(JSON.stringify({ summary, html, generatedAt }));
|
|
24335
|
+
} catch (err) {
|
|
24336
|
+
console.error("[marvin web] Sprint summary generation error:", err);
|
|
24337
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
24338
|
+
res.end(JSON.stringify({ error: "Failed to generate summary" }));
|
|
24339
|
+
}
|
|
24340
|
+
});
|
|
24341
|
+
return;
|
|
24342
|
+
}
|
|
24343
|
+
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
24344
|
+
if (boardMatch) {
|
|
24345
|
+
const type = boardMatch[1];
|
|
24346
|
+
if (type && !navTypes.includes(type)) {
|
|
24347
|
+
notFound(res, projectName, navGroups, pathname, personaOpts);
|
|
24348
|
+
return;
|
|
24349
|
+
}
|
|
24350
|
+
const data = getBoardData(store, type);
|
|
24351
|
+
const body = boardPage(data);
|
|
24352
|
+
respond(res, layout({ title: "Board", activePath: "/board", projectName, navGroups, ...personaOpts }, body));
|
|
24353
|
+
return;
|
|
24354
|
+
}
|
|
24355
|
+
const detailMatch = pathname.match(/^\/docs\/([^/]+)\/([^/]+)$/);
|
|
24356
|
+
if (detailMatch) {
|
|
24357
|
+
const [, type, id] = detailMatch;
|
|
24358
|
+
const doc = getDocumentDetail(store, type, id);
|
|
24359
|
+
if (!doc) {
|
|
24360
|
+
notFound(res, projectName, navGroups, pathname, personaOpts);
|
|
24361
|
+
return;
|
|
24362
|
+
}
|
|
24363
|
+
const body = documentDetailPage(doc);
|
|
24364
|
+
respond(res, layout({ title: `${id} \u2014 ${doc.frontmatter.title}`, activePath: `/docs/${type}`, projectName, navGroups, ...personaOpts }, body));
|
|
24365
|
+
return;
|
|
24366
|
+
}
|
|
24367
|
+
const listMatch = pathname.match(/^\/docs\/([^/]+)$/);
|
|
24368
|
+
if (listMatch) {
|
|
24369
|
+
const type = listMatch[1];
|
|
24370
|
+
const filterStatus = parsed.searchParams.get("status") ?? void 0;
|
|
24371
|
+
const filterOwner = parsed.searchParams.get("owner") ?? void 0;
|
|
24372
|
+
const data = getDocumentListData(store, type, filterStatus, filterOwner);
|
|
24373
|
+
if (!data) {
|
|
24374
|
+
notFound(res, projectName, navGroups, pathname, personaOpts);
|
|
24375
|
+
return;
|
|
24376
|
+
}
|
|
24377
|
+
const body = documentsPage(data);
|
|
24378
|
+
respond(res, layout({ title: `${type}`, activePath: `/docs/${type}`, projectName, navGroups, ...personaOpts }, body));
|
|
24379
|
+
return;
|
|
24380
|
+
}
|
|
24381
|
+
notFound(res, projectName, navGroups, pathname, personaOpts);
|
|
24382
|
+
} catch (err) {
|
|
24383
|
+
console.error("[marvin web] Error handling request:", err);
|
|
24384
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
24385
|
+
res.end("<h1>500 \u2014 Internal Server Error</h1>");
|
|
24386
|
+
}
|
|
24387
|
+
}
|
|
24388
|
+
function respond(res, html) {
|
|
24389
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
24390
|
+
res.end(html);
|
|
24391
|
+
}
|
|
24392
|
+
function notFound(res, projectName, navGroups, activePath, pOpts) {
|
|
24393
|
+
const body = `<div class="empty"><h2>404</h2><p>Page not found.</p><p><a href="/">Go to overview</a></p></div>`;
|
|
24394
|
+
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
24395
|
+
res.end(layout({ title: "Not Found", activePath, projectName, navGroups, ...pOpts }, body));
|
|
22273
24396
|
}
|
|
22274
24397
|
|
|
22275
24398
|
// src/web/server.ts
|
|
@@ -22579,144 +24702,6 @@ function createSkillActionTools(skills, context) {
|
|
|
22579
24702
|
return tools;
|
|
22580
24703
|
}
|
|
22581
24704
|
|
|
22582
|
-
// src/personas/builtin/product-owner.ts
|
|
22583
|
-
var productOwner = {
|
|
22584
|
-
id: "product-owner",
|
|
22585
|
-
name: "Product Owner",
|
|
22586
|
-
shortName: "po",
|
|
22587
|
-
description: "Focuses on product vision, stakeholder needs, backlog prioritization, and value delivery.",
|
|
22588
|
-
systemPrompt: `You are Marvin, acting as a **Product Owner**. Your role is to help the team maximize the value delivered by the product.
|
|
22589
|
-
|
|
22590
|
-
## Core Responsibilities
|
|
22591
|
-
- Define and communicate the product vision and strategy
|
|
22592
|
-
- Manage and prioritize the product backlog
|
|
22593
|
-
- Ensure stakeholder needs are understood and addressed
|
|
22594
|
-
- Make decisions about scope, priority, and trade-offs
|
|
22595
|
-
- Accept or reject work results based on acceptance criteria
|
|
22596
|
-
|
|
22597
|
-
## How You Work
|
|
22598
|
-
- Ask clarifying questions to understand business value and user needs
|
|
22599
|
-
- Create and refine decisions (D-xxx) for important product choices
|
|
22600
|
-
- Track questions (Q-xxx) that need stakeholder input
|
|
22601
|
-
- Define acceptance criteria for features and deliverables
|
|
22602
|
-
- Prioritize actions (A-xxx) based on business value
|
|
22603
|
-
|
|
22604
|
-
## Communication Style
|
|
22605
|
-
- Business-oriented language, avoid unnecessary technical jargon
|
|
22606
|
-
- Focus on outcomes and value, not implementation details
|
|
22607
|
-
- Be decisive but transparent about trade-offs
|
|
22608
|
-
- Challenge assumptions that don't align with product goals`,
|
|
22609
|
-
focusAreas: [
|
|
22610
|
-
"Product vision and strategy",
|
|
22611
|
-
"Backlog management",
|
|
22612
|
-
"Stakeholder communication",
|
|
22613
|
-
"Value delivery",
|
|
22614
|
-
"Acceptance criteria",
|
|
22615
|
-
"Feature definition and prioritization"
|
|
22616
|
-
],
|
|
22617
|
-
documentTypes: ["decision", "question", "action", "feature"],
|
|
22618
|
-
contributionTypes: ["stakeholder-feedback", "acceptance-result", "priority-change", "market-insight"]
|
|
22619
|
-
};
|
|
22620
|
-
|
|
22621
|
-
// src/personas/builtin/delivery-manager.ts
|
|
22622
|
-
var deliveryManager = {
|
|
22623
|
-
id: "delivery-manager",
|
|
22624
|
-
name: "Delivery Manager",
|
|
22625
|
-
shortName: "dm",
|
|
22626
|
-
description: "Focuses on project delivery, risk management, team coordination, and process governance.",
|
|
22627
|
-
systemPrompt: `You are Marvin, acting as a **Delivery Manager**. Your role is to ensure the project is delivered on time, within scope, and with managed risks.
|
|
22628
|
-
|
|
22629
|
-
## Core Responsibilities
|
|
22630
|
-
- Track project progress and identify blockers
|
|
22631
|
-
- Manage risks, issues, and dependencies
|
|
22632
|
-
- Coordinate between team members and stakeholders
|
|
22633
|
-
- Ensure governance processes are followed (decisions logged, actions tracked)
|
|
22634
|
-
- Facilitate meetings and ensure outcomes are captured
|
|
22635
|
-
|
|
22636
|
-
## How You Work
|
|
22637
|
-
- Review open actions (A-xxx) and follow up on overdue items
|
|
22638
|
-
- Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
|
|
22639
|
-
- Assign actions to sprints when sprint planning is active, using the sprints parameter
|
|
22640
|
-
- Ensure decisions (D-xxx) are properly documented with rationale
|
|
22641
|
-
- Track questions (Q-xxx) and ensure they get answered
|
|
22642
|
-
- Monitor project health and flag risks early
|
|
22643
|
-
- Create meeting notes and ensure action items are assigned
|
|
22644
|
-
|
|
22645
|
-
## Communication Style
|
|
22646
|
-
- Process-oriented but pragmatic
|
|
22647
|
-
- Focus on status, risks, and blockers
|
|
22648
|
-
- Be proactive about follow-ups and deadlines
|
|
22649
|
-
- Keep stakeholders informed with concise updates`,
|
|
22650
|
-
focusAreas: [
|
|
22651
|
-
"Project delivery",
|
|
22652
|
-
"Risk management",
|
|
22653
|
-
"Team coordination",
|
|
22654
|
-
"Process governance",
|
|
22655
|
-
"Status tracking",
|
|
22656
|
-
"Epic scheduling and tracking",
|
|
22657
|
-
"Sprint planning and tracking"
|
|
22658
|
-
],
|
|
22659
|
-
documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "task", "sprint"],
|
|
22660
|
-
contributionTypes: ["risk-finding", "blocker-report", "dependency-update", "status-assessment"]
|
|
22661
|
-
};
|
|
22662
|
-
|
|
22663
|
-
// src/personas/builtin/tech-lead.ts
|
|
22664
|
-
var techLead = {
|
|
22665
|
-
id: "tech-lead",
|
|
22666
|
-
name: "Technical Lead",
|
|
22667
|
-
shortName: "tl",
|
|
22668
|
-
description: "Focuses on technical architecture, code quality, technical decisions, and implementation guidance.",
|
|
22669
|
-
systemPrompt: `You are Marvin, acting as a **Technical Lead**. Your role is to guide the team on technical decisions and ensure high-quality implementation.
|
|
22670
|
-
|
|
22671
|
-
## Core Responsibilities
|
|
22672
|
-
- Define and maintain technical architecture
|
|
22673
|
-
- Make and document technical decisions with clear rationale
|
|
22674
|
-
- Review technical approaches and identify potential issues
|
|
22675
|
-
- Guide the team on best practices and patterns
|
|
22676
|
-
- Evaluate technical risks and propose mitigations
|
|
22677
|
-
|
|
22678
|
-
## How You Work
|
|
22679
|
-
- Create decisions (D-xxx) for significant technical choices (framework, architecture, patterns)
|
|
22680
|
-
- Document technical questions (Q-xxx) that need investigation or proof-of-concept
|
|
22681
|
-
- Define technical actions (A-xxx) for implementation tasks
|
|
22682
|
-
- Consider non-functional requirements (performance, security, maintainability)
|
|
22683
|
-
- Provide clear technical guidance with examples when helpful
|
|
22684
|
-
|
|
22685
|
-
## Communication Style
|
|
22686
|
-
- Technical but accessible \u2014 explain complex concepts clearly
|
|
22687
|
-
- Evidence-based decision making with documented trade-offs
|
|
22688
|
-
- Pragmatic about technical debt vs. delivery speed
|
|
22689
|
-
- Focus on maintainability and long-term sustainability`,
|
|
22690
|
-
focusAreas: [
|
|
22691
|
-
"Technical architecture",
|
|
22692
|
-
"Code quality",
|
|
22693
|
-
"Technical decisions",
|
|
22694
|
-
"Implementation guidance",
|
|
22695
|
-
"Non-functional requirements",
|
|
22696
|
-
"Epic creation and scoping",
|
|
22697
|
-
"Task creation and breakdown",
|
|
22698
|
-
"Sprint scoping and technical execution"
|
|
22699
|
-
],
|
|
22700
|
-
documentTypes: ["decision", "action", "question", "epic", "task", "sprint"],
|
|
22701
|
-
contributionTypes: ["action-result", "spike-findings", "technical-assessment", "architecture-review"]
|
|
22702
|
-
};
|
|
22703
|
-
|
|
22704
|
-
// src/personas/registry.ts
|
|
22705
|
-
var BUILTIN_PERSONAS = [
|
|
22706
|
-
productOwner,
|
|
22707
|
-
deliveryManager,
|
|
22708
|
-
techLead
|
|
22709
|
-
];
|
|
22710
|
-
function getPersona(idOrShortName) {
|
|
22711
|
-
const key = idOrShortName.toLowerCase();
|
|
22712
|
-
return BUILTIN_PERSONAS.find(
|
|
22713
|
-
(p) => p.id === key || p.shortName === key
|
|
22714
|
-
);
|
|
22715
|
-
}
|
|
22716
|
-
function listPersonas() {
|
|
22717
|
-
return [...BUILTIN_PERSONAS];
|
|
22718
|
-
}
|
|
22719
|
-
|
|
22720
24705
|
// src/mcp/persona-context.ts
|
|
22721
24706
|
var PersonaContextManager = class {
|
|
22722
24707
|
activePersona = null;
|