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/index.js
CHANGED
|
@@ -6630,13 +6630,13 @@ var error16 = () => {
|
|
|
6630
6630
|
// no unit
|
|
6631
6631
|
};
|
|
6632
6632
|
const typeEntry = (t) => t ? TypeNames[t] : void 0;
|
|
6633
|
-
const
|
|
6633
|
+
const typeLabel5 = (t) => {
|
|
6634
6634
|
const e = typeEntry(t);
|
|
6635
6635
|
if (e)
|
|
6636
6636
|
return e.label;
|
|
6637
6637
|
return t ?? TypeNames.unknown.label;
|
|
6638
6638
|
};
|
|
6639
|
-
const withDefinite = (t) => `\u05D4${
|
|
6639
|
+
const withDefinite = (t) => `\u05D4${typeLabel5(t)}`;
|
|
6640
6640
|
const verbFor = (t) => {
|
|
6641
6641
|
const e = typeEntry(t);
|
|
6642
6642
|
const gender = e?.gender ?? "m";
|
|
@@ -6686,7 +6686,7 @@ var error16 = () => {
|
|
|
6686
6686
|
switch (issue2.code) {
|
|
6687
6687
|
case "invalid_type": {
|
|
6688
6688
|
const expectedKey = issue2.expected;
|
|
6689
|
-
const expected = TypeDictionary[expectedKey ?? ""] ??
|
|
6689
|
+
const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel5(expectedKey);
|
|
6690
6690
|
const receivedType = parsedType(issue2.input);
|
|
6691
6691
|
const received = TypeDictionary[receivedType] ?? TypeNames[receivedType]?.label ?? receivedType;
|
|
6692
6692
|
if (/^[A-Z]/.test(issue2.expected)) {
|
|
@@ -15492,7 +15492,7 @@ function collectSprintSummaryData(store, sprintId) {
|
|
|
15492
15492
|
});
|
|
15493
15493
|
const sprintTag = `sprint:${fm.id}`;
|
|
15494
15494
|
const workItemDocs = allDocs.filter(
|
|
15495
|
-
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
|
|
15495
|
+
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "meeting" && d.frontmatter.tags?.includes(sprintTag)
|
|
15496
15496
|
);
|
|
15497
15497
|
const primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
|
|
15498
15498
|
const byStatus = {};
|
|
@@ -16139,17 +16139,27 @@ function layout(opts, body) {
|
|
|
16139
16139
|
{ href: "/health", label: "Health" }
|
|
16140
16140
|
];
|
|
16141
16141
|
const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
|
|
16142
|
-
const
|
|
16143
|
-
|
|
16144
|
-
|
|
16145
|
-
|
|
16146
|
-
|
|
16147
|
-
|
|
16142
|
+
const switcherHtml = opts.personaSwitcherHtml ?? "";
|
|
16143
|
+
let navHtml;
|
|
16144
|
+
if (opts.personaNavHtml) {
|
|
16145
|
+
navHtml = opts.personaNavHtml;
|
|
16146
|
+
} else {
|
|
16147
|
+
const groupsHtml = opts.navGroups.map((group) => {
|
|
16148
|
+
const links = group.types.map((type) => {
|
|
16149
|
+
const href = `/docs/${type}`;
|
|
16150
|
+
return `<a href="${href}" class="${isActive(href)}">${typeLabel(type)}s</a>`;
|
|
16151
|
+
}).join("\n ");
|
|
16152
|
+
return `
|
|
16148
16153
|
<div class="nav-group">
|
|
16149
16154
|
<div class="nav-group-label">${escapeHtml(group.label)}</div>
|
|
16150
16155
|
${links}
|
|
16151
16156
|
</div>`;
|
|
16152
|
-
|
|
16157
|
+
}).join("\n");
|
|
16158
|
+
navHtml = `
|
|
16159
|
+
${topItems.map((n) => `<a href="${n.href}" class="${isActive(n.href)}">${n.label}</a>`).join("\n ")}
|
|
16160
|
+
${groupsHtml}`;
|
|
16161
|
+
}
|
|
16162
|
+
const accentOverride = opts.personaAccentColor ? ` style="--persona-accent: ${opts.personaAccentColor}"` : "";
|
|
16153
16163
|
return `<!DOCTYPE html>
|
|
16154
16164
|
<html lang="en">
|
|
16155
16165
|
<head>
|
|
@@ -16159,15 +16169,15 @@ function layout(opts, body) {
|
|
|
16159
16169
|
<link rel="stylesheet" href="/styles.css">
|
|
16160
16170
|
</head>
|
|
16161
16171
|
<body>
|
|
16162
|
-
<div class="shell">
|
|
16172
|
+
<div class="shell"${accentOverride}>
|
|
16163
16173
|
<aside class="sidebar">
|
|
16164
16174
|
<div class="sidebar-brand">
|
|
16165
16175
|
<h1>Marvin</h1>
|
|
16166
16176
|
<div class="project-name">${escapeHtml(opts.projectName)}</div>
|
|
16167
16177
|
</div>
|
|
16178
|
+
${switcherHtml}
|
|
16168
16179
|
<nav>
|
|
16169
|
-
${
|
|
16170
|
-
${groupsHtml}
|
|
16180
|
+
${navHtml}
|
|
16171
16181
|
</nav>
|
|
16172
16182
|
</aside>
|
|
16173
16183
|
<main class="main${opts.mainClass ? ` ${opts.mainClass}` : ""}">
|
|
@@ -16175,7 +16185,7 @@ function layout(opts, body) {
|
|
|
16175
16185
|
<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>
|
|
16176
16186
|
<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>
|
|
16177
16187
|
</button>
|
|
16178
|
-
${body}
|
|
16188
|
+
${opts.bodyPrefix ?? ""}${body}
|
|
16179
16189
|
</main>
|
|
16180
16190
|
</div>
|
|
16181
16191
|
<script>
|
|
@@ -16477,6 +16487,12 @@ a:hover { text-decoration: underline; }
|
|
|
16477
16487
|
.badge-closed, .badge-resolved { background: rgba(52, 211, 153, 0.15); color: var(--green); }
|
|
16478
16488
|
.badge-blocked { background: rgba(248, 113, 113, 0.15); color: var(--red); }
|
|
16479
16489
|
.badge-default { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
|
|
16490
|
+
.badge-subtle {
|
|
16491
|
+
background: rgba(139, 143, 164, 0.12);
|
|
16492
|
+
color: var(--text-dim);
|
|
16493
|
+
text-transform: none;
|
|
16494
|
+
font-weight: 500;
|
|
16495
|
+
}
|
|
16480
16496
|
|
|
16481
16497
|
/* Table */
|
|
16482
16498
|
.table-wrap {
|
|
@@ -17154,6 +17170,142 @@ tr:hover td {
|
|
|
17154
17170
|
|
|
17155
17171
|
.text-dim { color: var(--text-dim); }
|
|
17156
17172
|
|
|
17173
|
+
/* Persona switcher */
|
|
17174
|
+
.persona-switcher {
|
|
17175
|
+
padding: 0.5rem 1.25rem 0.75rem;
|
|
17176
|
+
border-bottom: 1px solid var(--border);
|
|
17177
|
+
margin-bottom: 0.5rem;
|
|
17178
|
+
display: flex;
|
|
17179
|
+
align-items: center;
|
|
17180
|
+
gap: 0.5rem;
|
|
17181
|
+
}
|
|
17182
|
+
|
|
17183
|
+
.persona-label {
|
|
17184
|
+
font-size: 0.65rem;
|
|
17185
|
+
text-transform: uppercase;
|
|
17186
|
+
letter-spacing: 0.06em;
|
|
17187
|
+
color: var(--text-dim);
|
|
17188
|
+
font-weight: 600;
|
|
17189
|
+
}
|
|
17190
|
+
|
|
17191
|
+
.persona-select {
|
|
17192
|
+
flex: 1;
|
|
17193
|
+
background: var(--bg);
|
|
17194
|
+
border: 1px solid var(--border);
|
|
17195
|
+
color: var(--text);
|
|
17196
|
+
padding: 0.3rem 0.5rem;
|
|
17197
|
+
border-radius: var(--radius);
|
|
17198
|
+
font-size: 0.8rem;
|
|
17199
|
+
cursor: pointer;
|
|
17200
|
+
font-family: var(--font);
|
|
17201
|
+
}
|
|
17202
|
+
|
|
17203
|
+
.persona-select:focus {
|
|
17204
|
+
outline: none;
|
|
17205
|
+
border-color: var(--persona-accent, var(--accent));
|
|
17206
|
+
}
|
|
17207
|
+
|
|
17208
|
+
/* Persona banner (first-visit picker) */
|
|
17209
|
+
.persona-banner {
|
|
17210
|
+
background: var(--bg-card);
|
|
17211
|
+
border: 1px solid var(--border);
|
|
17212
|
+
border-radius: var(--radius);
|
|
17213
|
+
padding: 1.5rem;
|
|
17214
|
+
margin-bottom: 2rem;
|
|
17215
|
+
}
|
|
17216
|
+
|
|
17217
|
+
.persona-banner-header {
|
|
17218
|
+
display: flex;
|
|
17219
|
+
align-items: center;
|
|
17220
|
+
justify-content: space-between;
|
|
17221
|
+
margin-bottom: 0.25rem;
|
|
17222
|
+
}
|
|
17223
|
+
|
|
17224
|
+
.persona-banner-header h3 {
|
|
17225
|
+
font-size: 1.1rem;
|
|
17226
|
+
font-weight: 600;
|
|
17227
|
+
}
|
|
17228
|
+
|
|
17229
|
+
.persona-banner-dismiss {
|
|
17230
|
+
background: none;
|
|
17231
|
+
border: none;
|
|
17232
|
+
color: var(--text-dim);
|
|
17233
|
+
font-size: 1.25rem;
|
|
17234
|
+
cursor: pointer;
|
|
17235
|
+
padding: 0.25rem;
|
|
17236
|
+
line-height: 1;
|
|
17237
|
+
}
|
|
17238
|
+
|
|
17239
|
+
.persona-banner-dismiss:hover {
|
|
17240
|
+
color: var(--text);
|
|
17241
|
+
}
|
|
17242
|
+
|
|
17243
|
+
.persona-banner-subtitle {
|
|
17244
|
+
color: var(--text-dim);
|
|
17245
|
+
font-size: 0.85rem;
|
|
17246
|
+
margin-bottom: 1rem;
|
|
17247
|
+
}
|
|
17248
|
+
|
|
17249
|
+
.persona-banner-options {
|
|
17250
|
+
display: grid;
|
|
17251
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
17252
|
+
gap: 0.75rem;
|
|
17253
|
+
}
|
|
17254
|
+
|
|
17255
|
+
.persona-banner-option {
|
|
17256
|
+
display: block;
|
|
17257
|
+
background: var(--bg);
|
|
17258
|
+
border: 1px solid var(--border);
|
|
17259
|
+
border-radius: var(--radius);
|
|
17260
|
+
padding: 1rem;
|
|
17261
|
+
text-decoration: none;
|
|
17262
|
+
color: inherit;
|
|
17263
|
+
transition: border-color 0.15s, background 0.15s;
|
|
17264
|
+
border-left: 3px solid var(--persona-card-accent, var(--accent));
|
|
17265
|
+
}
|
|
17266
|
+
|
|
17267
|
+
.persona-banner-option:hover {
|
|
17268
|
+
border-color: var(--persona-card-accent, var(--accent));
|
|
17269
|
+
background: var(--bg-hover);
|
|
17270
|
+
text-decoration: none;
|
|
17271
|
+
}
|
|
17272
|
+
|
|
17273
|
+
.persona-banner-name {
|
|
17274
|
+
font-weight: 600;
|
|
17275
|
+
font-size: 0.95rem;
|
|
17276
|
+
margin-bottom: 0.25rem;
|
|
17277
|
+
}
|
|
17278
|
+
|
|
17279
|
+
.persona-banner-desc {
|
|
17280
|
+
font-size: 0.8rem;
|
|
17281
|
+
color: var(--text-dim);
|
|
17282
|
+
}
|
|
17283
|
+
|
|
17284
|
+
/* Persona accent override */
|
|
17285
|
+
.shell[style*="--persona-accent"] .sidebar nav a.active {
|
|
17286
|
+
color: var(--persona-accent);
|
|
17287
|
+
background: rgba(108, 140, 255, 0.08);
|
|
17288
|
+
border-right-color: var(--persona-accent);
|
|
17289
|
+
}
|
|
17290
|
+
|
|
17291
|
+
.shell[style*="--persona-accent"] .sidebar-brand h1 {
|
|
17292
|
+
color: var(--persona-accent);
|
|
17293
|
+
}
|
|
17294
|
+
|
|
17295
|
+
/* Persona page placeholder */
|
|
17296
|
+
.persona-placeholder {
|
|
17297
|
+
text-align: center;
|
|
17298
|
+
padding: 3rem;
|
|
17299
|
+
color: var(--text-dim);
|
|
17300
|
+
}
|
|
17301
|
+
|
|
17302
|
+
.persona-placeholder h3 {
|
|
17303
|
+
font-size: 1.1rem;
|
|
17304
|
+
font-weight: 600;
|
|
17305
|
+
margin-bottom: 0.5rem;
|
|
17306
|
+
color: var(--text);
|
|
17307
|
+
}
|
|
17308
|
+
|
|
17157
17309
|
/* Sprint Summary */
|
|
17158
17310
|
.sprint-goal {
|
|
17159
17311
|
background: var(--bg-card);
|
|
@@ -17293,6 +17445,22 @@ tr:hover td {
|
|
|
17293
17445
|
max-height: 0;
|
|
17294
17446
|
opacity: 0;
|
|
17295
17447
|
}
|
|
17448
|
+
|
|
17449
|
+
/* Sortable table headers */
|
|
17450
|
+
.sortable-th {
|
|
17451
|
+
cursor: pointer;
|
|
17452
|
+
user-select: none;
|
|
17453
|
+
}
|
|
17454
|
+
.sortable-th:hover {
|
|
17455
|
+
text-decoration: underline;
|
|
17456
|
+
color: var(--text);
|
|
17457
|
+
}
|
|
17458
|
+
.sort-arrow {
|
|
17459
|
+
display: inline-block;
|
|
17460
|
+
margin-left: 0.3rem;
|
|
17461
|
+
font-size: 0.65rem;
|
|
17462
|
+
opacity: 0.7;
|
|
17463
|
+
}
|
|
17296
17464
|
`;
|
|
17297
17465
|
}
|
|
17298
17466
|
|
|
@@ -18214,15 +18382,53 @@ function sprintSummaryPage(data, cached2) {
|
|
|
18214
18382
|
</div>`,
|
|
18215
18383
|
{ titleTag: "h3" }
|
|
18216
18384
|
) : "";
|
|
18385
|
+
const STREAM_PALETTE = [
|
|
18386
|
+
"hsla(220, 30%, 22%, 0.45)",
|
|
18387
|
+
"hsla(160, 30%, 20%, 0.45)",
|
|
18388
|
+
"hsla(280, 25%, 22%, 0.45)",
|
|
18389
|
+
"hsla(30, 35%, 22%, 0.45)",
|
|
18390
|
+
"hsla(340, 25%, 22%, 0.45)",
|
|
18391
|
+
"hsla(190, 30%, 20%, 0.45)",
|
|
18392
|
+
"hsla(60, 25%, 20%, 0.45)",
|
|
18393
|
+
"hsla(120, 20%, 20%, 0.45)"
|
|
18394
|
+
];
|
|
18395
|
+
function hashString(s) {
|
|
18396
|
+
let h = 0;
|
|
18397
|
+
for (let i = 0; i < s.length; i++) {
|
|
18398
|
+
h = (h << 5) - h + s.charCodeAt(i) | 0;
|
|
18399
|
+
}
|
|
18400
|
+
return Math.abs(h);
|
|
18401
|
+
}
|
|
18402
|
+
function collectStreams(items) {
|
|
18403
|
+
const streams = /* @__PURE__ */ new Set();
|
|
18404
|
+
for (const w of items) {
|
|
18405
|
+
if (w.workStream) streams.add(w.workStream);
|
|
18406
|
+
if (w.children) {
|
|
18407
|
+
for (const s of collectStreams(w.children)) streams.add(s);
|
|
18408
|
+
}
|
|
18409
|
+
}
|
|
18410
|
+
return streams;
|
|
18411
|
+
}
|
|
18412
|
+
const uniqueStreams = collectStreams(data.workItems.items);
|
|
18413
|
+
const streamColorMap = /* @__PURE__ */ new Map();
|
|
18414
|
+
for (const name of uniqueStreams) {
|
|
18415
|
+
streamColorMap.set(name, STREAM_PALETTE[hashString(name) % STREAM_PALETTE.length]);
|
|
18416
|
+
}
|
|
18417
|
+
const streamStyleRules = [...streamColorMap.entries()].map(([name, color]) => `tr[data-stream="${escapeHtml(name)}"] td { background: ${color}; }`).join("\n");
|
|
18418
|
+
const streamStyleBlock = streamStyleRules ? `<style>${streamStyleRules}</style>` : "";
|
|
18217
18419
|
function renderItemRows(items, depth = 0) {
|
|
18218
18420
|
return items.flatMap((w) => {
|
|
18219
18421
|
const isChild = depth > 0;
|
|
18220
18422
|
const isContribution = w.type === "contribution";
|
|
18221
|
-
const
|
|
18423
|
+
const classes = [];
|
|
18424
|
+
if (isContribution) classes.push("contribution-row");
|
|
18425
|
+
else if (isChild) classes.push("child-row");
|
|
18426
|
+
const dataStream = w.workStream ? ` data-stream="${escapeHtml(w.workStream)}"` : "";
|
|
18427
|
+
const rowAttrs = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
|
|
18222
18428
|
const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
|
|
18223
18429
|
const streamCell = w.workStream ? `<span class="badge badge-subtle">${escapeHtml(w.workStream)}</span>` : "";
|
|
18224
18430
|
const row = `
|
|
18225
|
-
<tr${
|
|
18431
|
+
<tr${rowAttrs}${dataStream}>
|
|
18226
18432
|
<td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
18227
18433
|
<td>${escapeHtml(w.title)}</td>
|
|
18228
18434
|
<td>${streamCell}</td>
|
|
@@ -18234,13 +18440,21 @@ function sprintSummaryPage(data, cached2) {
|
|
|
18234
18440
|
});
|
|
18235
18441
|
}
|
|
18236
18442
|
const workItemRows = renderItemRows(data.workItems.items);
|
|
18443
|
+
const sortableHeaders = `<tr>
|
|
18444
|
+
<th class="sortable-th" onclick="sortWorkItems(0)">ID<span class="sort-arrow" id="sort-arrow-0"></span></th>
|
|
18445
|
+
<th class="sortable-th" onclick="sortWorkItems(1)">Title<span class="sort-arrow" id="sort-arrow-1"></span></th>
|
|
18446
|
+
<th class="sortable-th" onclick="sortWorkItems(2)">Stream<span class="sort-arrow" id="sort-arrow-2"></span></th>
|
|
18447
|
+
<th class="sortable-th" onclick="sortWorkItems(3)">Type<span class="sort-arrow" id="sort-arrow-3"></span></th>
|
|
18448
|
+
<th class="sortable-th" onclick="sortWorkItems(4)">Status<span class="sort-arrow" id="sort-arrow-4"></span></th>
|
|
18449
|
+
</tr>`;
|
|
18237
18450
|
const workItemsSection = workItemRows.length > 0 ? collapsibleSection(
|
|
18238
18451
|
"ss-work-items",
|
|
18239
18452
|
"Work Items",
|
|
18240
|
-
|
|
18241
|
-
|
|
18453
|
+
`${streamStyleBlock}
|
|
18454
|
+
<div class="table-wrap">
|
|
18455
|
+
<table id="work-items-table">
|
|
18242
18456
|
<thead>
|
|
18243
|
-
|
|
18457
|
+
${sortableHeaders}
|
|
18244
18458
|
</thead>
|
|
18245
18459
|
<tbody>
|
|
18246
18460
|
${workItemRows.join("")}
|
|
@@ -18319,6 +18533,61 @@ function sprintSummaryPage(data, cached2) {
|
|
|
18319
18533
|
</div>
|
|
18320
18534
|
|
|
18321
18535
|
<script>
|
|
18536
|
+
var _sortCol = -1;
|
|
18537
|
+
var _sortAsc = true;
|
|
18538
|
+
|
|
18539
|
+
function sortWorkItems(col) {
|
|
18540
|
+
var table = document.getElementById('work-items-table');
|
|
18541
|
+
if (!table) return;
|
|
18542
|
+
var tbody = table.querySelector('tbody');
|
|
18543
|
+
var allRows = Array.from(tbody.querySelectorAll('tr'));
|
|
18544
|
+
|
|
18545
|
+
// Toggle direction if clicking the same column
|
|
18546
|
+
if (_sortCol === col) {
|
|
18547
|
+
_sortAsc = !_sortAsc;
|
|
18548
|
+
} else {
|
|
18549
|
+
_sortCol = col;
|
|
18550
|
+
_sortAsc = true;
|
|
18551
|
+
}
|
|
18552
|
+
|
|
18553
|
+
// Update sort arrows
|
|
18554
|
+
for (var i = 0; i < 5; i++) {
|
|
18555
|
+
var arrow = document.getElementById('sort-arrow-' + i);
|
|
18556
|
+
if (arrow) arrow.textContent = i === col ? (_sortAsc ? ' \\u25B2' : ' \\u25BC') : '';
|
|
18557
|
+
}
|
|
18558
|
+
|
|
18559
|
+
// Group rows: root rows + their child/contribution rows
|
|
18560
|
+
var groups = [];
|
|
18561
|
+
var current = null;
|
|
18562
|
+
for (var r = 0; r < allRows.length; r++) {
|
|
18563
|
+
var row = allRows[r];
|
|
18564
|
+
var isChild = row.classList.contains('child-row') || row.classList.contains('contribution-row');
|
|
18565
|
+
if (!isChild) {
|
|
18566
|
+
current = { root: row, children: [] };
|
|
18567
|
+
groups.push(current);
|
|
18568
|
+
} else if (current) {
|
|
18569
|
+
current.children.push(row);
|
|
18570
|
+
}
|
|
18571
|
+
}
|
|
18572
|
+
|
|
18573
|
+
// Sort groups by root row text content of target column
|
|
18574
|
+
groups.sort(function(a, b) {
|
|
18575
|
+
var aText = (a.root.children[col] ? a.root.children[col].textContent : '').trim().toLowerCase();
|
|
18576
|
+
var bText = (b.root.children[col] ? b.root.children[col].textContent : '').trim().toLowerCase();
|
|
18577
|
+
if (aText < bText) return _sortAsc ? -1 : 1;
|
|
18578
|
+
if (aText > bText) return _sortAsc ? 1 : -1;
|
|
18579
|
+
return 0;
|
|
18580
|
+
});
|
|
18581
|
+
|
|
18582
|
+
// Re-append rows in sorted order
|
|
18583
|
+
for (var g = 0; g < groups.length; g++) {
|
|
18584
|
+
tbody.appendChild(groups[g].root);
|
|
18585
|
+
for (var c = 0; c < groups[g].children.length; c++) {
|
|
18586
|
+
tbody.appendChild(groups[g].children[c]);
|
|
18587
|
+
}
|
|
18588
|
+
}
|
|
18589
|
+
}
|
|
18590
|
+
|
|
18322
18591
|
async function generateSummary() {
|
|
18323
18592
|
var btn = document.getElementById('generate-btn');
|
|
18324
18593
|
var loading = document.getElementById('summary-loading');
|
|
@@ -18358,12 +18627,16 @@ function sprintSummaryPage(data, cached2) {
|
|
|
18358
18627
|
|
|
18359
18628
|
// src/reports/sprint-summary/generator.ts
|
|
18360
18629
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
18361
|
-
async function generateSprintSummary(data) {
|
|
18630
|
+
async function generateSprintSummary(data, personaSystemPrompt) {
|
|
18362
18631
|
const prompt = buildPrompt(data);
|
|
18632
|
+
const systemPrompt = personaSystemPrompt ? `${SYSTEM_PROMPT}
|
|
18633
|
+
|
|
18634
|
+
Additional persona context:
|
|
18635
|
+
${personaSystemPrompt}` : SYSTEM_PROMPT;
|
|
18363
18636
|
const result = query({
|
|
18364
18637
|
prompt,
|
|
18365
18638
|
options: {
|
|
18366
|
-
systemPrompt
|
|
18639
|
+
systemPrompt,
|
|
18367
18640
|
maxTurns: 1,
|
|
18368
18641
|
tools: [],
|
|
18369
18642
|
allowedTools: []
|
|
@@ -18474,51 +18747,1762 @@ function buildPrompt(data) {
|
|
|
18474
18747
|
return sections.join("\n");
|
|
18475
18748
|
}
|
|
18476
18749
|
|
|
18477
|
-
// src/web/
|
|
18478
|
-
var
|
|
18479
|
-
|
|
18480
|
-
|
|
18481
|
-
|
|
18482
|
-
|
|
18483
|
-
|
|
18484
|
-
|
|
18485
|
-
|
|
18486
|
-
|
|
18487
|
-
|
|
18488
|
-
|
|
18489
|
-
|
|
18750
|
+
// src/web/persona-views.ts
|
|
18751
|
+
var VIEWS = /* @__PURE__ */ new Map();
|
|
18752
|
+
var PAGE_RENDERERS = /* @__PURE__ */ new Map();
|
|
18753
|
+
function registerPersonaView(config2) {
|
|
18754
|
+
VIEWS.set(config2.shortName, config2);
|
|
18755
|
+
}
|
|
18756
|
+
function registerPersonaPage(persona, pageId, renderer) {
|
|
18757
|
+
PAGE_RENDERERS.set(`${persona}/${pageId}`, renderer);
|
|
18758
|
+
}
|
|
18759
|
+
function getPersonaView(mode) {
|
|
18760
|
+
if (!mode) return void 0;
|
|
18761
|
+
return VIEWS.get(mode);
|
|
18762
|
+
}
|
|
18763
|
+
function getPersonaPageRenderer(persona, pageId) {
|
|
18764
|
+
return PAGE_RENDERERS.get(`${persona}/${pageId}`);
|
|
18765
|
+
}
|
|
18766
|
+
function getAllPersonaViews() {
|
|
18767
|
+
return [...VIEWS.values()];
|
|
18768
|
+
}
|
|
18769
|
+
function parsePersonaFromPath(pathname) {
|
|
18770
|
+
const match = pathname.match(/^\/(po|dm|tl)(?:\/|$)/);
|
|
18771
|
+
return match ? match[1] : null;
|
|
18772
|
+
}
|
|
18773
|
+
|
|
18774
|
+
// src/web/templates/persona-switcher.ts
|
|
18775
|
+
function renderPersonaSwitcher(current, currentPath) {
|
|
18776
|
+
const views = getAllPersonaViews();
|
|
18777
|
+
if (views.length === 0) return "";
|
|
18778
|
+
const options = [
|
|
18779
|
+
`<option value=""${current === null ? " selected" : ""}>Admin</option>`,
|
|
18780
|
+
...views.map(
|
|
18781
|
+
(v) => `<option value="${v.shortName}"${current === v.shortName ? " selected" : ""}>${escapeHtml(v.displayName)}</option>`
|
|
18782
|
+
)
|
|
18783
|
+
].join("\n ");
|
|
18784
|
+
return `
|
|
18785
|
+
<div class="persona-switcher">
|
|
18786
|
+
<label class="persona-label" for="persona-select">View</label>
|
|
18787
|
+
<select class="persona-select" id="persona-select" onchange="switchPersona(this.value)">
|
|
18788
|
+
${options}
|
|
18789
|
+
</select>
|
|
18790
|
+
</div>
|
|
18791
|
+
<script>
|
|
18792
|
+
function switchPersona(value) {
|
|
18793
|
+
if (value) {
|
|
18794
|
+
window.location.href = '/' + value + '/dashboard';
|
|
18795
|
+
} else {
|
|
18796
|
+
window.location.href = '/';
|
|
18797
|
+
}
|
|
18798
|
+
}
|
|
18799
|
+
</script>`;
|
|
18800
|
+
}
|
|
18801
|
+
function renderPersonaBanner() {
|
|
18802
|
+
const views = getAllPersonaViews();
|
|
18803
|
+
if (views.length === 0) return "";
|
|
18804
|
+
const cards = views.map(
|
|
18805
|
+
(v) => `
|
|
18806
|
+
<a href="/${v.shortName}/dashboard" class="persona-banner-option" style="--persona-card-accent: ${v.color}" onclick="dismissPersonaBanner()">
|
|
18807
|
+
<div class="persona-banner-name">${escapeHtml(v.displayName)}</div>
|
|
18808
|
+
<div class="persona-banner-desc">${escapeHtml(v.description)}</div>
|
|
18809
|
+
</a>`
|
|
18810
|
+
).join("\n");
|
|
18811
|
+
return `
|
|
18812
|
+
<div class="persona-banner" id="persona-banner">
|
|
18813
|
+
<div class="persona-banner-header">
|
|
18814
|
+
<h3>Choose a View</h3>
|
|
18815
|
+
<button class="persona-banner-dismiss" onclick="dismissPersonaBanner()" title="Dismiss">×</button>
|
|
18816
|
+
</div>
|
|
18817
|
+
<p class="persona-banner-subtitle">Get a curated dashboard for your role, or stay in admin mode for full access.</p>
|
|
18818
|
+
<div class="persona-banner-options">
|
|
18819
|
+
${cards}
|
|
18820
|
+
</div>
|
|
18821
|
+
</div>
|
|
18822
|
+
<script>
|
|
18823
|
+
(function() {
|
|
18824
|
+
if (localStorage.getItem('marvin-persona-banner-dismissed')) {
|
|
18825
|
+
var banner = document.getElementById('persona-banner');
|
|
18826
|
+
if (banner) banner.style.display = 'none';
|
|
18827
|
+
}
|
|
18828
|
+
})();
|
|
18829
|
+
function dismissPersonaBanner() {
|
|
18830
|
+
localStorage.setItem('marvin-persona-banner-dismissed', '1');
|
|
18831
|
+
var banner = document.getElementById('persona-banner');
|
|
18832
|
+
if (banner) banner.style.display = 'none';
|
|
18833
|
+
}
|
|
18834
|
+
</script>`;
|
|
18835
|
+
}
|
|
18836
|
+
|
|
18837
|
+
// src/web/templates/pages/po/dashboard.ts
|
|
18838
|
+
function poDashboardPage(ctx) {
|
|
18839
|
+
const overview = getOverviewData(ctx.store);
|
|
18840
|
+
const upcoming = getUpcomingData(ctx.store);
|
|
18841
|
+
const sprintData = getSprintSummaryData(ctx.store);
|
|
18842
|
+
const features = ctx.store.list({ type: "feature" });
|
|
18843
|
+
const featuresDone = features.filter((d) => ["done", "closed", "resolved"].includes(d.frontmatter.status)).length;
|
|
18844
|
+
const featuresOpen = features.filter((d) => d.frontmatter.status === "open").length;
|
|
18845
|
+
const featuresInProgress = features.filter((d) => d.frontmatter.status === "in-progress").length;
|
|
18846
|
+
const decisions = ctx.store.list({ type: "decision" });
|
|
18847
|
+
const decisionsOpen = decisions.filter((d) => d.frontmatter.status === "open").length;
|
|
18848
|
+
const questions = ctx.store.list({ type: "question" });
|
|
18849
|
+
const questionsOpen = questions.filter((d) => d.frontmatter.status === "open").length;
|
|
18850
|
+
const statsCards = `
|
|
18851
|
+
<div class="cards">
|
|
18852
|
+
<div class="card">
|
|
18853
|
+
<a href="/po/backlog">
|
|
18854
|
+
<div class="card-label">Features</div>
|
|
18855
|
+
<div class="card-value">${features.length}</div>
|
|
18856
|
+
<div class="card-sub">${featuresDone} done, ${featuresInProgress} in progress, ${featuresOpen} open</div>
|
|
18857
|
+
</a>
|
|
18858
|
+
</div>
|
|
18859
|
+
<div class="card">
|
|
18860
|
+
<a href="/po/decisions">
|
|
18861
|
+
<div class="card-label">Pending Decisions</div>
|
|
18862
|
+
<div class="card-value${decisionsOpen > 0 ? " priority-medium" : ""}">${decisionsOpen}</div>
|
|
18863
|
+
<div class="card-sub">${decisions.length} total decisions</div>
|
|
18864
|
+
</a>
|
|
18865
|
+
</div>
|
|
18866
|
+
<div class="card">
|
|
18867
|
+
<a href="/po/backlog">
|
|
18868
|
+
<div class="card-label">Open Questions</div>
|
|
18869
|
+
<div class="card-value${questionsOpen > 0 ? " priority-medium" : ""}">${questionsOpen}</div>
|
|
18870
|
+
<div class="card-sub">${questions.length} total questions</div>
|
|
18871
|
+
</a>
|
|
18872
|
+
</div>
|
|
18873
|
+
<div class="card">
|
|
18874
|
+
<a href="/po/delivery">
|
|
18875
|
+
<div class="card-label">Sprint</div>
|
|
18876
|
+
<div class="card-value">${sprintData ? `${sprintData.workItems.completionPct}%` : "\u2014"}</div>
|
|
18877
|
+
<div class="card-sub">${sprintData ? `${sprintData.workItems.done}/${sprintData.workItems.total} items` : "No active sprint"}</div>
|
|
18878
|
+
</a>
|
|
18879
|
+
</div>
|
|
18880
|
+
</div>`;
|
|
18881
|
+
const poTypes = /* @__PURE__ */ new Set(["feature", "decision", "question"]);
|
|
18882
|
+
const poRecent = overview.recent.filter((d) => poTypes.has(d.frontmatter.type)).slice(0, 10);
|
|
18883
|
+
const recentTable = poRecent.length > 0 ? collapsibleSection(
|
|
18884
|
+
"po-recent",
|
|
18885
|
+
"Recent Activity",
|
|
18886
|
+
`<div class="table-wrap">
|
|
18887
|
+
<table>
|
|
18888
|
+
<thead>
|
|
18889
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Updated</th></tr>
|
|
18890
|
+
</thead>
|
|
18891
|
+
<tbody>
|
|
18892
|
+
${poRecent.map((d) => `
|
|
18893
|
+
<tr>
|
|
18894
|
+
<td><a href="/docs/${d.frontmatter.type}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
18895
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
18896
|
+
<td>${escapeHtml(typeLabel(d.frontmatter.type))}</td>
|
|
18897
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
18898
|
+
<td>${formatDate(d.frontmatter.updated ?? d.frontmatter.created)}</td>
|
|
18899
|
+
</tr>`).join("")}
|
|
18900
|
+
</tbody>
|
|
18901
|
+
</table>
|
|
18902
|
+
</div>`,
|
|
18903
|
+
{ titleTag: "h3" }
|
|
18904
|
+
) : "";
|
|
18905
|
+
const trendingSection = upcoming.trending.length > 0 ? collapsibleSection(
|
|
18906
|
+
"po-trending",
|
|
18907
|
+
"Trending Items",
|
|
18908
|
+
`<div class="table-wrap">
|
|
18909
|
+
<table>
|
|
18910
|
+
<thead>
|
|
18911
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Score</th></tr>
|
|
18912
|
+
</thead>
|
|
18913
|
+
<tbody>
|
|
18914
|
+
${upcoming.trending.slice(0, 8).map((t) => `
|
|
18915
|
+
<tr>
|
|
18916
|
+
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
18917
|
+
<td>${escapeHtml(t.title)}</td>
|
|
18918
|
+
<td>${escapeHtml(typeLabel(t.type))}</td>
|
|
18919
|
+
<td><span class="trending-score">${t.score}</span></td>
|
|
18920
|
+
</tr>`).join("")}
|
|
18921
|
+
</tbody>
|
|
18922
|
+
</table>
|
|
18923
|
+
</div>`,
|
|
18924
|
+
{ titleTag: "h3" }
|
|
18925
|
+
) : "";
|
|
18926
|
+
return `
|
|
18927
|
+
<div class="page-header">
|
|
18928
|
+
<h2>Product Owner Dashboard</h2>
|
|
18929
|
+
<div class="subtitle">Feature delivery, decisions, and stakeholder alignment</div>
|
|
18930
|
+
</div>
|
|
18931
|
+
${statsCards}
|
|
18932
|
+
${recentTable}
|
|
18933
|
+
${trendingSection}
|
|
18934
|
+
`;
|
|
18935
|
+
}
|
|
18936
|
+
|
|
18937
|
+
// src/web/templates/pages/po/backlog.ts
|
|
18938
|
+
function poBacklogPage(ctx) {
|
|
18939
|
+
const features = ctx.store.list({ type: "feature" });
|
|
18940
|
+
const questions = ctx.store.list({ type: "question" });
|
|
18941
|
+
const openQuestions = questions.filter((d) => d.frontmatter.status === "open");
|
|
18942
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
18943
|
+
const statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
18944
|
+
const sortedFeatures = [...features].sort((a, b) => {
|
|
18945
|
+
const sa = statusOrder[a.frontmatter.status] ?? 3;
|
|
18946
|
+
const sb = statusOrder[b.frontmatter.status] ?? 3;
|
|
18947
|
+
if (sa !== sb) return sa - sb;
|
|
18948
|
+
const pa = priorityOrder[a.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
18949
|
+
const pb = priorityOrder[b.frontmatter.priority?.toLowerCase()] ?? 99;
|
|
18950
|
+
if (pa !== pb) return pa - pb;
|
|
18951
|
+
return a.frontmatter.id.localeCompare(b.frontmatter.id);
|
|
18952
|
+
});
|
|
18953
|
+
const epics = ctx.store.list({ type: "epic" });
|
|
18954
|
+
const featureToEpics = /* @__PURE__ */ new Map();
|
|
18955
|
+
for (const epic of epics) {
|
|
18956
|
+
const linked = epic.frontmatter.linkedFeature;
|
|
18957
|
+
const featureIds = Array.isArray(linked) ? linked : linked ? [linked] : [];
|
|
18958
|
+
for (const fid of featureIds) {
|
|
18959
|
+
const existing = featureToEpics.get(String(fid)) ?? [];
|
|
18960
|
+
existing.push(epic.frontmatter.id);
|
|
18961
|
+
featureToEpics.set(String(fid), existing);
|
|
18962
|
+
}
|
|
18963
|
+
}
|
|
18964
|
+
function priorityClass2(p) {
|
|
18965
|
+
if (!p) return "";
|
|
18966
|
+
const lower = p.toLowerCase();
|
|
18967
|
+
if (lower === "critical" || lower === "high") return " priority-high";
|
|
18968
|
+
if (lower === "medium") return " priority-medium";
|
|
18969
|
+
if (lower === "low") return " priority-low";
|
|
18970
|
+
return "";
|
|
18971
|
+
}
|
|
18972
|
+
const featuresTable = sortedFeatures.length > 0 ? `<div class="table-wrap">
|
|
18973
|
+
<table>
|
|
18974
|
+
<thead>
|
|
18975
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Priority</th><th>Linked Epics</th></tr>
|
|
18976
|
+
</thead>
|
|
18977
|
+
<tbody>
|
|
18978
|
+
${sortedFeatures.map((d) => {
|
|
18979
|
+
const linkedEpics = featureToEpics.get(d.frontmatter.id) ?? [];
|
|
18980
|
+
const epicLinks = linkedEpics.map((eid) => `<a href="/docs/epic/${escapeHtml(eid)}">${escapeHtml(eid)}</a>`).join(", ");
|
|
18981
|
+
return `
|
|
18982
|
+
<tr>
|
|
18983
|
+
<td><a href="/docs/feature/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
18984
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
18985
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
18986
|
+
<td><span class="${priorityClass2(d.frontmatter.priority)}">${escapeHtml(d.frontmatter.priority ?? "\u2014")}</span></td>
|
|
18987
|
+
<td>${epicLinks || '<span class="text-dim">\u2014</span>'}</td>
|
|
18988
|
+
</tr>`;
|
|
18989
|
+
}).join("")}
|
|
18990
|
+
</tbody>
|
|
18991
|
+
</table>
|
|
18992
|
+
</div>` : '<div class="empty"><p>No features found.</p></div>';
|
|
18993
|
+
const questionsTable = openQuestions.length > 0 ? collapsibleSection(
|
|
18994
|
+
"po-backlog-questions",
|
|
18995
|
+
`Open Questions (${openQuestions.length})`,
|
|
18996
|
+
`<div class="table-wrap">
|
|
18997
|
+
<table>
|
|
18998
|
+
<thead>
|
|
18999
|
+
<tr><th>ID</th><th>Title</th><th>Owner</th><th>Created</th></tr>
|
|
19000
|
+
</thead>
|
|
19001
|
+
<tbody>
|
|
19002
|
+
${openQuestions.map((d) => `
|
|
19003
|
+
<tr>
|
|
19004
|
+
<td><a href="/docs/question/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
19005
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
19006
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19007
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
19008
|
+
</tr>`).join("")}
|
|
19009
|
+
</tbody>
|
|
19010
|
+
</table>
|
|
19011
|
+
</div>`,
|
|
19012
|
+
{ titleTag: "h3" }
|
|
19013
|
+
) : "";
|
|
19014
|
+
return `
|
|
19015
|
+
<div class="page-header">
|
|
19016
|
+
<h2>Product Backlog</h2>
|
|
19017
|
+
<div class="subtitle">${features.length} features, ${openQuestions.length} open questions</div>
|
|
19018
|
+
</div>
|
|
19019
|
+
${collapsibleSection("po-backlog-features", `Features (${features.length})`, featuresTable, { titleTag: "h3" })}
|
|
19020
|
+
${questionsTable}
|
|
19021
|
+
`;
|
|
19022
|
+
}
|
|
19023
|
+
|
|
19024
|
+
// src/web/templates/pages/po/decisions.ts
|
|
19025
|
+
var DONE_STATUSES3 = /* @__PURE__ */ new Set(["done", "closed", "resolved"]);
|
|
19026
|
+
function poDecisionsPage(ctx) {
|
|
19027
|
+
const decisions = ctx.store.list({ type: "decision" });
|
|
19028
|
+
const openDecisions = decisions.filter((d) => !DONE_STATUSES3.has(d.frontmatter.status));
|
|
19029
|
+
const resolvedDecisions = decisions.filter((d) => DONE_STATUSES3.has(d.frontmatter.status));
|
|
19030
|
+
const statsCards = `
|
|
19031
|
+
<div class="cards">
|
|
19032
|
+
<div class="card">
|
|
19033
|
+
<div class="card-label">Open</div>
|
|
19034
|
+
<div class="card-value${openDecisions.length > 0 ? " priority-medium" : ""}">${openDecisions.length}</div>
|
|
19035
|
+
<div class="card-sub">awaiting resolution</div>
|
|
19036
|
+
</div>
|
|
19037
|
+
<div class="card">
|
|
19038
|
+
<div class="card-label">Resolved</div>
|
|
19039
|
+
<div class="card-value">${resolvedDecisions.length}</div>
|
|
19040
|
+
<div class="card-sub">decisions made</div>
|
|
19041
|
+
</div>
|
|
19042
|
+
<div class="card">
|
|
19043
|
+
<div class="card-label">Total</div>
|
|
19044
|
+
<div class="card-value">${decisions.length}</div>
|
|
19045
|
+
<div class="card-sub">all decisions</div>
|
|
19046
|
+
</div>
|
|
19047
|
+
</div>`;
|
|
19048
|
+
function decisionTable(docs) {
|
|
19049
|
+
if (docs.length === 0) return '<div class="empty"><p>None found.</p></div>';
|
|
19050
|
+
return `<div class="table-wrap">
|
|
19051
|
+
<table>
|
|
19052
|
+
<thead>
|
|
19053
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Owner</th><th>Created</th></tr>
|
|
19054
|
+
</thead>
|
|
19055
|
+
<tbody>
|
|
19056
|
+
${docs.map((d) => `
|
|
19057
|
+
<tr>
|
|
19058
|
+
<td><a href="/docs/decision/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
19059
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
19060
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
19061
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19062
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
19063
|
+
</tr>`).join("")}
|
|
19064
|
+
</tbody>
|
|
19065
|
+
</table>
|
|
19066
|
+
</div>`;
|
|
19067
|
+
}
|
|
19068
|
+
const openSection = collapsibleSection(
|
|
19069
|
+
"po-decisions-open",
|
|
19070
|
+
`Open Decisions (${openDecisions.length})`,
|
|
19071
|
+
decisionTable(openDecisions),
|
|
19072
|
+
{ titleTag: "h3" }
|
|
19073
|
+
);
|
|
19074
|
+
const resolvedSection = collapsibleSection(
|
|
19075
|
+
"po-decisions-resolved",
|
|
19076
|
+
`Resolved Decisions (${resolvedDecisions.length})`,
|
|
19077
|
+
decisionTable(resolvedDecisions),
|
|
19078
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
19079
|
+
);
|
|
19080
|
+
return `
|
|
19081
|
+
<div class="page-header">
|
|
19082
|
+
<h2>Decision Log</h2>
|
|
19083
|
+
<div class="subtitle">Track and manage product decisions</div>
|
|
19084
|
+
</div>
|
|
19085
|
+
${statsCards}
|
|
19086
|
+
${openSection}
|
|
19087
|
+
${resolvedSection}
|
|
19088
|
+
`;
|
|
19089
|
+
}
|
|
19090
|
+
|
|
19091
|
+
// src/web/templates/pages/po/delivery.ts
|
|
19092
|
+
var PO_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
|
|
19093
|
+
"stakeholder-feedback",
|
|
19094
|
+
"acceptance-result",
|
|
19095
|
+
"priority-change",
|
|
19096
|
+
"market-insight"
|
|
19097
|
+
]);
|
|
19098
|
+
function progressBar2(pct) {
|
|
19099
|
+
return `<div class="sprint-progress-bar">
|
|
19100
|
+
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
19101
|
+
<span class="sprint-progress-label">${pct}%</span>
|
|
19102
|
+
</div>`;
|
|
19103
|
+
}
|
|
19104
|
+
function poDeliveryPage(ctx) {
|
|
19105
|
+
const data = getSprintSummaryData(ctx.store);
|
|
19106
|
+
if (!data) {
|
|
19107
|
+
return `
|
|
19108
|
+
<div class="page-header">
|
|
19109
|
+
<h2>Value Delivery</h2>
|
|
19110
|
+
<div class="subtitle">Sprint progress and PO contributions</div>
|
|
19111
|
+
</div>
|
|
19112
|
+
<div class="empty">
|
|
19113
|
+
<h3>No Active Sprint</h3>
|
|
19114
|
+
<p>No active sprint found. Create a sprint and set its status to "active" to track delivery.</p>
|
|
19115
|
+
</div>`;
|
|
19116
|
+
}
|
|
19117
|
+
const doneFeatures = data.workItems.items.filter(
|
|
19118
|
+
(w) => w.type === "feature" && ["done", "closed", "resolved"].includes(w.status)
|
|
19119
|
+
);
|
|
19120
|
+
function findContributions(items, parentId) {
|
|
19121
|
+
const result = [];
|
|
19122
|
+
for (const item of items) {
|
|
19123
|
+
if (item.type === "contribution" && PO_CONTRIBUTION_TYPES.has(item.id.split("-").slice(0, -1).join("-") || "")) {
|
|
19124
|
+
result.push({ id: item.id, title: item.title, type: item.type, status: item.status, parentId });
|
|
19125
|
+
}
|
|
19126
|
+
if (PO_CONTRIBUTION_TYPES.has(item.type)) {
|
|
19127
|
+
result.push({ id: item.id, title: item.title, type: item.type, status: item.status, parentId });
|
|
19128
|
+
}
|
|
19129
|
+
if (item.children) {
|
|
19130
|
+
result.push(...findContributions(item.children, item.id));
|
|
19131
|
+
}
|
|
19132
|
+
}
|
|
19133
|
+
return result;
|
|
19134
|
+
}
|
|
19135
|
+
const allDocs = ctx.store.list();
|
|
19136
|
+
const poContributions = allDocs.filter((d) => PO_CONTRIBUTION_TYPES.has(d.frontmatter.type));
|
|
19137
|
+
const statsCards = `
|
|
19138
|
+
<div class="cards">
|
|
19139
|
+
<div class="card">
|
|
19140
|
+
<div class="card-label">Sprint Progress</div>
|
|
19141
|
+
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
19142
|
+
<div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
|
|
19143
|
+
</div>
|
|
19144
|
+
<div class="card">
|
|
19145
|
+
<div class="card-label">Days Remaining</div>
|
|
19146
|
+
<div class="card-value">${data.timeline.daysRemaining}</div>
|
|
19147
|
+
<div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
|
|
19148
|
+
</div>
|
|
19149
|
+
<div class="card">
|
|
19150
|
+
<div class="card-label">Features Done</div>
|
|
19151
|
+
<div class="card-value">${doneFeatures.length}</div>
|
|
19152
|
+
<div class="card-sub">this sprint</div>
|
|
19153
|
+
</div>
|
|
19154
|
+
<div class="card">
|
|
19155
|
+
<div class="card-label">PO Contributions</div>
|
|
19156
|
+
<div class="card-value">${poContributions.length}</div>
|
|
19157
|
+
<div class="card-sub">feedback, reviews, insights</div>
|
|
19158
|
+
</div>
|
|
19159
|
+
</div>`;
|
|
19160
|
+
const sprintHeader = `
|
|
19161
|
+
<div class="sprint-goal">
|
|
19162
|
+
<strong>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</strong>
|
|
19163
|
+
${data.sprint.goal ? ` | ${escapeHtml(data.sprint.goal)}` : ""}
|
|
19164
|
+
</div>`;
|
|
19165
|
+
const featuresSection = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
19166
|
+
"po-delivery-epics",
|
|
19167
|
+
"Linked Epics",
|
|
19168
|
+
`<div class="table-wrap">
|
|
19169
|
+
<table>
|
|
19170
|
+
<thead>
|
|
19171
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks Done</th></tr>
|
|
19172
|
+
</thead>
|
|
19173
|
+
<tbody>
|
|
19174
|
+
${data.linkedEpics.map((e) => `
|
|
19175
|
+
<tr>
|
|
19176
|
+
<td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
|
|
19177
|
+
<td>${escapeHtml(e.title)}</td>
|
|
19178
|
+
<td>${statusBadge(e.status)}</td>
|
|
19179
|
+
<td>${e.tasksDone} / ${e.tasksTotal}</td>
|
|
19180
|
+
</tr>`).join("")}
|
|
19181
|
+
</tbody>
|
|
19182
|
+
</table>
|
|
19183
|
+
</div>`,
|
|
19184
|
+
{ titleTag: "h3" }
|
|
19185
|
+
) : "";
|
|
19186
|
+
const contributionsSection = poContributions.length > 0 ? collapsibleSection(
|
|
19187
|
+
"po-delivery-contributions",
|
|
19188
|
+
`PO Contributions (${poContributions.length})`,
|
|
19189
|
+
`<div class="table-wrap">
|
|
19190
|
+
<table>
|
|
19191
|
+
<thead>
|
|
19192
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Date</th></tr>
|
|
19193
|
+
</thead>
|
|
19194
|
+
<tbody>
|
|
19195
|
+
${poContributions.map((d) => `
|
|
19196
|
+
<tr>
|
|
19197
|
+
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
19198
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
19199
|
+
<td>${escapeHtml(typeLabel(d.frontmatter.type))}</td>
|
|
19200
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
19201
|
+
<td>${formatDate(d.frontmatter.updated ?? d.frontmatter.created)}</td>
|
|
19202
|
+
</tr>`).join("")}
|
|
19203
|
+
</tbody>
|
|
19204
|
+
</table>
|
|
19205
|
+
</div>`,
|
|
19206
|
+
{ titleTag: "h3" }
|
|
19207
|
+
) : "";
|
|
19208
|
+
return `
|
|
19209
|
+
<div class="page-header">
|
|
19210
|
+
<h2>Value Delivery</h2>
|
|
19211
|
+
<div class="subtitle">Sprint progress and feature delivery tracking</div>
|
|
19212
|
+
</div>
|
|
19213
|
+
${sprintHeader}
|
|
19214
|
+
${progressBar2(data.workItems.completionPct)}
|
|
19215
|
+
${statsCards}
|
|
19216
|
+
${featuresSection}
|
|
19217
|
+
${contributionsSection}
|
|
19218
|
+
`;
|
|
19219
|
+
}
|
|
19220
|
+
|
|
19221
|
+
// src/web/templates/pages/po/stakeholders.ts
|
|
19222
|
+
function poStakeholdersPage(ctx) {
|
|
19223
|
+
const garReport = getGarData(ctx.store, ctx.projectName);
|
|
19224
|
+
const upcoming = getUpcomingData(ctx.store);
|
|
19225
|
+
const actions = ctx.store.list({ type: "action" });
|
|
19226
|
+
const openActions = actions.filter(
|
|
19227
|
+
(d) => !["done", "closed", "resolved", "cancelled"].includes(d.frontmatter.status)
|
|
19228
|
+
);
|
|
19229
|
+
const questions = ctx.store.list({ type: "question" });
|
|
19230
|
+
const openQuestions = questions.filter((d) => d.frontmatter.status === "open");
|
|
19231
|
+
const garDotClass = `dot-${garReport.overall}`;
|
|
19232
|
+
const garAreaCards = garReport.areas.map(
|
|
19233
|
+
(area) => `
|
|
19234
|
+
<div class="gar-area">
|
|
19235
|
+
<div class="area-header">
|
|
19236
|
+
<div class="area-dot dot-${area.status}"></div>
|
|
19237
|
+
<div class="area-name">${escapeHtml(area.name)}</div>
|
|
19238
|
+
</div>
|
|
19239
|
+
<div class="area-summary">${escapeHtml(area.summary)}</div>
|
|
19240
|
+
${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>` : ""}
|
|
19241
|
+
</div>`
|
|
19242
|
+
).join("\n");
|
|
19243
|
+
const garSection = collapsibleSection(
|
|
19244
|
+
"po-stakeholders-gar",
|
|
19245
|
+
"Project Status (GAR)",
|
|
19246
|
+
`<div class="gar-overall">
|
|
19247
|
+
<div class="dot ${garDotClass}"></div>
|
|
19248
|
+
<div class="label">Overall: ${escapeHtml(garReport.overall)}</div>
|
|
19249
|
+
</div>
|
|
19250
|
+
<div class="gar-areas">${garAreaCards}</div>`,
|
|
19251
|
+
{ titleTag: "h3" }
|
|
19252
|
+
);
|
|
19253
|
+
const actionsSection = openActions.length > 0 ? collapsibleSection(
|
|
19254
|
+
"po-stakeholders-actions",
|
|
19255
|
+
`Open Action Items (${openActions.length})`,
|
|
19256
|
+
`<div class="table-wrap">
|
|
19257
|
+
<table>
|
|
19258
|
+
<thead>
|
|
19259
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Owner</th><th>Due Date</th></tr>
|
|
19260
|
+
</thead>
|
|
19261
|
+
<tbody>
|
|
19262
|
+
${openActions.map((d) => `
|
|
19263
|
+
<tr>
|
|
19264
|
+
<td><a href="/docs/action/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
19265
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
19266
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
19267
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19268
|
+
<td>${d.frontmatter.dueDate ? formatDate(d.frontmatter.dueDate) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19269
|
+
</tr>`).join("")}
|
|
19270
|
+
</tbody>
|
|
19271
|
+
</table>
|
|
19272
|
+
</div>`,
|
|
19273
|
+
{ titleTag: "h3" }
|
|
19274
|
+
) : "";
|
|
19275
|
+
const questionsSection = openQuestions.length > 0 ? collapsibleSection(
|
|
19276
|
+
"po-stakeholders-questions",
|
|
19277
|
+
`Questions Needing Input (${openQuestions.length})`,
|
|
19278
|
+
`<div class="table-wrap">
|
|
19279
|
+
<table>
|
|
19280
|
+
<thead>
|
|
19281
|
+
<tr><th>ID</th><th>Title</th><th>Owner</th><th>Created</th></tr>
|
|
19282
|
+
</thead>
|
|
19283
|
+
<tbody>
|
|
19284
|
+
${openQuestions.map((d) => `
|
|
19285
|
+
<tr>
|
|
19286
|
+
<td><a href="/docs/question/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
19287
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
19288
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19289
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
19290
|
+
</tr>`).join("")}
|
|
19291
|
+
</tbody>
|
|
19292
|
+
</table>
|
|
19293
|
+
</div>`,
|
|
19294
|
+
{ titleTag: "h3" }
|
|
19295
|
+
) : "";
|
|
19296
|
+
return `
|
|
19297
|
+
<div class="page-header">
|
|
19298
|
+
<h2>Stakeholder View</h2>
|
|
19299
|
+
<div class="subtitle">Project status overview for stakeholder communication</div>
|
|
19300
|
+
</div>
|
|
19301
|
+
${garSection}
|
|
19302
|
+
${actionsSection}
|
|
19303
|
+
${questionsSection}
|
|
19304
|
+
`;
|
|
19305
|
+
}
|
|
19306
|
+
|
|
19307
|
+
// src/web/persona-configs/po.ts
|
|
19308
|
+
registerPersonaView({
|
|
19309
|
+
shortName: "po",
|
|
19310
|
+
displayName: "Product Owner",
|
|
19311
|
+
description: "Feature delivery, decisions, and stakeholder alignment",
|
|
19312
|
+
color: "#6c8cff",
|
|
19313
|
+
navItems: [
|
|
19314
|
+
{ path: "/po/dashboard", label: "Dashboard" },
|
|
19315
|
+
{ path: "/po/backlog", label: "Product Backlog" },
|
|
19316
|
+
{ path: "/po/decisions", label: "Decision Log" },
|
|
19317
|
+
{ path: "/po/delivery", label: "Value Delivery" },
|
|
19318
|
+
{ path: "/po/stakeholders", label: "Stakeholder View" }
|
|
19319
|
+
]
|
|
19320
|
+
});
|
|
19321
|
+
registerPersonaPage("po", "dashboard", poDashboardPage);
|
|
19322
|
+
registerPersonaPage("po", "backlog", poBacklogPage);
|
|
19323
|
+
registerPersonaPage("po", "decisions", poDecisionsPage);
|
|
19324
|
+
registerPersonaPage("po", "delivery", poDeliveryPage);
|
|
19325
|
+
registerPersonaPage("po", "stakeholders", poStakeholdersPage);
|
|
19326
|
+
|
|
19327
|
+
// src/web/templates/pages/dm/dashboard.ts
|
|
19328
|
+
var DONE_STATUSES4 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19329
|
+
function progressBar3(pct) {
|
|
19330
|
+
return `<div class="sprint-progress-bar">
|
|
19331
|
+
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
19332
|
+
<span class="sprint-progress-label">${pct}%</span>
|
|
19333
|
+
</div>`;
|
|
19334
|
+
}
|
|
19335
|
+
function dmDashboardPage(ctx) {
|
|
19336
|
+
const sprintData = getSprintSummaryData(ctx.store);
|
|
19337
|
+
const upcoming = getUpcomingData(ctx.store);
|
|
19338
|
+
const actions = ctx.store.list({ type: "action" });
|
|
19339
|
+
const openActions = actions.filter((d) => !DONE_STATUSES4.has(d.frontmatter.status));
|
|
19340
|
+
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
19341
|
+
const statsCards = `
|
|
19342
|
+
<div class="cards">
|
|
19343
|
+
<div class="card">
|
|
19344
|
+
<a href="/dm/sprint">
|
|
19345
|
+
<div class="card-label">Sprint Progress</div>
|
|
19346
|
+
<div class="card-value">${sprintData ? `${sprintData.workItems.completionPct}%` : "\u2014"}</div>
|
|
19347
|
+
<div class="card-sub">${sprintData ? `${sprintData.timeline.daysRemaining} days remaining` : "No active sprint"}</div>
|
|
19348
|
+
</a>
|
|
19349
|
+
</div>
|
|
19350
|
+
<div class="card">
|
|
19351
|
+
<a href="/dm/risks">
|
|
19352
|
+
<div class="card-label">Blockers</div>
|
|
19353
|
+
<div class="card-value${(sprintData?.blockers.length ?? 0) > 0 ? " priority-high" : ""}">${sprintData?.blockers.length ?? 0}</div>
|
|
19354
|
+
<div class="card-sub">blocking items</div>
|
|
19355
|
+
</a>
|
|
19356
|
+
</div>
|
|
19357
|
+
<div class="card">
|
|
19358
|
+
<a href="/dm/actions">
|
|
19359
|
+
<div class="card-label">Overdue Actions</div>
|
|
19360
|
+
<div class="card-value${overdueActions.length > 0 ? " priority-high" : ""}">${overdueActions.length}</div>
|
|
19361
|
+
<div class="card-sub">${openActions.length} open total</div>
|
|
19362
|
+
</a>
|
|
19363
|
+
</div>
|
|
19364
|
+
<div class="card">
|
|
19365
|
+
<a href="/dm/meetings">
|
|
19366
|
+
<div class="card-label">Meetings</div>
|
|
19367
|
+
<div class="card-value">${sprintData?.meetings.length ?? 0}</div>
|
|
19368
|
+
<div class="card-sub">this sprint</div>
|
|
19369
|
+
</a>
|
|
19370
|
+
</div>
|
|
19371
|
+
</div>`;
|
|
19372
|
+
const sprintProgress = sprintData ? `
|
|
19373
|
+
<div class="sprint-goal">
|
|
19374
|
+
<strong>${escapeHtml(sprintData.sprint.id)} \u2014 ${escapeHtml(sprintData.sprint.title)}</strong>
|
|
19375
|
+
${sprintData.sprint.goal ? ` | ${escapeHtml(sprintData.sprint.goal)}` : ""}
|
|
19376
|
+
</div>
|
|
19377
|
+
${progressBar3(sprintData.workItems.completionPct)}` : "";
|
|
19378
|
+
const riskItems = [];
|
|
19379
|
+
if (overdueActions.length > 0) riskItems.push(`${overdueActions.length} overdue action(s)`);
|
|
19380
|
+
if ((sprintData?.blockers.length ?? 0) > 0) riskItems.push(`${sprintData.blockers.length} blocker(s)`);
|
|
19381
|
+
if (sprintData && sprintData.timeline.daysRemaining <= 3 && sprintData.workItems.completionPct < 80) {
|
|
19382
|
+
riskItems.push("Sprint deadline approaching with low completion");
|
|
19383
|
+
}
|
|
19384
|
+
const riskSection = riskItems.length > 0 ? `<div class="sprint-goal" style="border-left: 3px solid var(--red);">
|
|
19385
|
+
<strong>Risk Indicators</strong>
|
|
19386
|
+
<ul style="margin: 0.5rem 0 0 1.25rem; font-size: 0.875rem; color: var(--text-dim);">
|
|
19387
|
+
${riskItems.map((r) => `<li>${escapeHtml(r)}</li>`).join("")}
|
|
19388
|
+
</ul>
|
|
19389
|
+
</div>` : "";
|
|
19390
|
+
const dueSoonPreview = upcoming.dueSoonActions.slice(0, 5);
|
|
19391
|
+
const actionsPreview = dueSoonPreview.length > 0 ? collapsibleSection(
|
|
19392
|
+
"dm-dash-actions",
|
|
19393
|
+
"Due Soon Actions",
|
|
19394
|
+
`<div class="table-wrap">
|
|
19395
|
+
<table>
|
|
19396
|
+
<thead>
|
|
19397
|
+
<tr><th>ID</th><th>Title</th><th>Owner</th><th>Due</th><th>Status</th></tr>
|
|
19398
|
+
</thead>
|
|
19399
|
+
<tbody>
|
|
19400
|
+
${dueSoonPreview.map((a) => `
|
|
19401
|
+
<tr>
|
|
19402
|
+
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
19403
|
+
<td>${escapeHtml(a.title)}</td>
|
|
19404
|
+
<td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19405
|
+
<td>${formatDate(a.dueDate)}</td>
|
|
19406
|
+
<td>${statusBadge(a.status)}</td>
|
|
19407
|
+
</tr>`).join("")}
|
|
19408
|
+
</tbody>
|
|
19409
|
+
</table>
|
|
19410
|
+
</div>
|
|
19411
|
+
<p style="margin-top: 0.5rem; font-size: 0.85rem;"><a href="/dm/actions">View all actions →</a></p>`,
|
|
19412
|
+
{ titleTag: "h3" }
|
|
19413
|
+
) : "";
|
|
19414
|
+
return `
|
|
19415
|
+
<div class="page-header">
|
|
19416
|
+
<h2>Delivery Manager Dashboard</h2>
|
|
19417
|
+
<div class="subtitle">Sprint execution, action tracking, and risk management</div>
|
|
19418
|
+
</div>
|
|
19419
|
+
${sprintProgress}
|
|
19420
|
+
${statsCards}
|
|
19421
|
+
${riskSection}
|
|
19422
|
+
${actionsPreview}
|
|
19423
|
+
`;
|
|
19424
|
+
}
|
|
19425
|
+
|
|
19426
|
+
// src/web/templates/pages/dm/sprint.ts
|
|
19427
|
+
function dmSprintPage(ctx) {
|
|
19428
|
+
const data = getSprintSummaryData(ctx.store);
|
|
19429
|
+
return sprintSummaryPage(data);
|
|
19430
|
+
}
|
|
19431
|
+
|
|
19432
|
+
// src/web/templates/pages/dm/actions.ts
|
|
19433
|
+
var DONE_STATUSES5 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19434
|
+
function urgencyBadge2(tier) {
|
|
19435
|
+
const labels = {
|
|
19436
|
+
overdue: "Overdue",
|
|
19437
|
+
"due-3d": "Due in 3d",
|
|
19438
|
+
"due-7d": "Due in 7d",
|
|
19439
|
+
upcoming: "Upcoming",
|
|
19440
|
+
later: "Later"
|
|
19441
|
+
};
|
|
19442
|
+
return `<span class="badge urgency-badge-${tier}">${labels[tier]}</span>`;
|
|
19443
|
+
}
|
|
19444
|
+
function urgencyRowClass2(tier) {
|
|
19445
|
+
if (tier === "overdue") return " urgency-row-overdue";
|
|
19446
|
+
if (tier === "due-3d") return " urgency-row-due-3d";
|
|
19447
|
+
if (tier === "due-7d") return " urgency-row-due-7d";
|
|
19448
|
+
return "";
|
|
19449
|
+
}
|
|
19450
|
+
function dmActionsPage(ctx) {
|
|
19451
|
+
const upcoming = getUpcomingData(ctx.store);
|
|
19452
|
+
const allActions = ctx.store.list({ type: "action" });
|
|
19453
|
+
const openActions = allActions.filter((d) => !DONE_STATUSES5.has(d.frontmatter.status));
|
|
19454
|
+
const overdueActions = upcoming.dueSoonActions.filter((a) => a.urgency === "overdue");
|
|
19455
|
+
const dueThisWeek = upcoming.dueSoonActions.filter((a) => a.urgency === "due-3d" || a.urgency === "due-7d");
|
|
19456
|
+
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
19457
|
+
const statsCards = `
|
|
19458
|
+
<div class="cards">
|
|
19459
|
+
<div class="card">
|
|
19460
|
+
<div class="card-label">Total Open</div>
|
|
19461
|
+
<div class="card-value">${openActions.length}</div>
|
|
19462
|
+
<div class="card-sub">${allActions.length} total actions</div>
|
|
19463
|
+
</div>
|
|
19464
|
+
<div class="card">
|
|
19465
|
+
<div class="card-label">Overdue</div>
|
|
19466
|
+
<div class="card-value${overdueActions.length > 0 ? " priority-high" : ""}">${overdueActions.length}</div>
|
|
19467
|
+
<div class="card-sub">past due date</div>
|
|
19468
|
+
</div>
|
|
19469
|
+
<div class="card">
|
|
19470
|
+
<div class="card-label">Due This Week</div>
|
|
19471
|
+
<div class="card-value${dueThisWeek.length > 0 ? " priority-medium" : ""}">${dueThisWeek.length}</div>
|
|
19472
|
+
<div class="card-sub">next 7 days</div>
|
|
19473
|
+
</div>
|
|
19474
|
+
<div class="card">
|
|
19475
|
+
<div class="card-label">Unowned</div>
|
|
19476
|
+
<div class="card-value${unownedActions.length > 0 ? " priority-medium" : ""}">${unownedActions.length}</div>
|
|
19477
|
+
<div class="card-sub">need assignment</div>
|
|
19478
|
+
</div>
|
|
19479
|
+
</div>`;
|
|
19480
|
+
const dueSoonSection = upcoming.dueSoonActions.length > 0 ? collapsibleSection(
|
|
19481
|
+
"dm-actions-due",
|
|
19482
|
+
`Actions by Due Date (${upcoming.dueSoonActions.length})`,
|
|
19483
|
+
`<div class="table-wrap">
|
|
19484
|
+
<table>
|
|
19485
|
+
<thead>
|
|
19486
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Owner</th><th>Due Date</th><th>Urgency</th></tr>
|
|
19487
|
+
</thead>
|
|
19488
|
+
<tbody>
|
|
19489
|
+
${upcoming.dueSoonActions.map((a) => `
|
|
19490
|
+
<tr class="${urgencyRowClass2(a.urgency)}">
|
|
19491
|
+
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
19492
|
+
<td>${escapeHtml(a.title)}</td>
|
|
19493
|
+
<td>${statusBadge(a.status)}</td>
|
|
19494
|
+
<td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19495
|
+
<td>${formatDate(a.dueDate)}</td>
|
|
19496
|
+
<td>${urgencyBadge2(a.urgency)}</td>
|
|
19497
|
+
</tr>`).join("")}
|
|
19498
|
+
</tbody>
|
|
19499
|
+
</table>
|
|
19500
|
+
</div>`,
|
|
19501
|
+
{ titleTag: "h3" }
|
|
19502
|
+
) : "";
|
|
19503
|
+
const noDueDateActions = openActions.filter((d) => !d.frontmatter.dueDate);
|
|
19504
|
+
const noDueDateSection = noDueDateActions.length > 0 ? collapsibleSection(
|
|
19505
|
+
"dm-actions-nodate",
|
|
19506
|
+
`Actions Without Due Date (${noDueDateActions.length})`,
|
|
19507
|
+
`<div class="table-wrap">
|
|
19508
|
+
<table>
|
|
19509
|
+
<thead>
|
|
19510
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Owner</th><th>Created</th></tr>
|
|
19511
|
+
</thead>
|
|
19512
|
+
<tbody>
|
|
19513
|
+
${noDueDateActions.map((d) => `
|
|
19514
|
+
<tr>
|
|
19515
|
+
<td><a href="/docs/action/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
19516
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
19517
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
19518
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19519
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
19520
|
+
</tr>`).join("")}
|
|
19521
|
+
</tbody>
|
|
19522
|
+
</table>
|
|
19523
|
+
</div>`,
|
|
19524
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
19525
|
+
) : "";
|
|
19526
|
+
return `
|
|
19527
|
+
<div class="page-header">
|
|
19528
|
+
<h2>Action Tracker</h2>
|
|
19529
|
+
<div class="subtitle">Track and manage all action items across the project</div>
|
|
19530
|
+
</div>
|
|
19531
|
+
${statsCards}
|
|
19532
|
+
${dueSoonSection}
|
|
19533
|
+
${noDueDateSection}
|
|
19534
|
+
`;
|
|
19535
|
+
}
|
|
19536
|
+
|
|
19537
|
+
// src/web/templates/pages/dm/risks.ts
|
|
19538
|
+
var DONE_STATUSES6 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19539
|
+
function dmRisksPage(ctx) {
|
|
19540
|
+
const allDocs = ctx.store.list();
|
|
19541
|
+
const upcoming = getUpcomingData(ctx.store);
|
|
19542
|
+
const healthMetrics = collectHealthMetrics(ctx.store);
|
|
19543
|
+
const healthReport = evaluateHealth(ctx.projectName, healthMetrics);
|
|
19544
|
+
const blockedItems = allDocs.filter((d) => d.frontmatter.status === "blocked");
|
|
19545
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
19546
|
+
const todayMs = new Date(today).getTime();
|
|
19547
|
+
const fourteenDaysMs = 14 * 864e5;
|
|
19548
|
+
const agingItems = allDocs.filter((d) => {
|
|
19549
|
+
if (DONE_STATUSES6.has(d.frontmatter.status)) return false;
|
|
19550
|
+
if (!["action", "question"].includes(d.frontmatter.type)) return false;
|
|
19551
|
+
const createdMs = new Date(d.frontmatter.created).getTime();
|
|
19552
|
+
return todayMs - createdMs > fourteenDaysMs;
|
|
19553
|
+
});
|
|
19554
|
+
const statsCards = `
|
|
19555
|
+
<div class="cards">
|
|
19556
|
+
<div class="card">
|
|
19557
|
+
<div class="card-label">Blocked Items</div>
|
|
19558
|
+
<div class="card-value${blockedItems.length > 0 ? " priority-high" : ""}">${blockedItems.length}</div>
|
|
19559
|
+
<div class="card-sub">currently blocked</div>
|
|
19560
|
+
</div>
|
|
19561
|
+
<div class="card">
|
|
19562
|
+
<div class="card-label">Aging Items</div>
|
|
19563
|
+
<div class="card-value${agingItems.length > 0 ? " priority-medium" : ""}">${agingItems.length}</div>
|
|
19564
|
+
<div class="card-sub">>14 days open</div>
|
|
19565
|
+
</div>
|
|
19566
|
+
<div class="card">
|
|
19567
|
+
<div class="card-label">Health</div>
|
|
19568
|
+
<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>
|
|
19569
|
+
<div class="card-sub">overall project health</div>
|
|
19570
|
+
</div>
|
|
19571
|
+
<div class="card">
|
|
19572
|
+
<div class="card-label">Overdue Actions</div>
|
|
19573
|
+
<div class="card-value${upcoming.dueSoonActions.filter((a) => a.urgency === "overdue").length > 0 ? " priority-high" : ""}">${upcoming.dueSoonActions.filter((a) => a.urgency === "overdue").length}</div>
|
|
19574
|
+
<div class="card-sub">past due date</div>
|
|
19575
|
+
</div>
|
|
19576
|
+
</div>`;
|
|
19577
|
+
const blockedSection = blockedItems.length > 0 ? collapsibleSection(
|
|
19578
|
+
"dm-risks-blocked",
|
|
19579
|
+
`Blocked Items (${blockedItems.length})`,
|
|
19580
|
+
`<div class="table-wrap">
|
|
19581
|
+
<table>
|
|
19582
|
+
<thead>
|
|
19583
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Owner</th><th>Created</th></tr>
|
|
19584
|
+
</thead>
|
|
19585
|
+
<tbody>
|
|
19586
|
+
${blockedItems.map((d) => `
|
|
19587
|
+
<tr>
|
|
19588
|
+
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
19589
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
19590
|
+
<td>${escapeHtml(typeLabel(d.frontmatter.type))}</td>
|
|
19591
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19592
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
19593
|
+
</tr>`).join("")}
|
|
19594
|
+
</tbody>
|
|
19595
|
+
</table>
|
|
19596
|
+
</div>`,
|
|
19597
|
+
{ titleTag: "h3" }
|
|
19598
|
+
) : "";
|
|
19599
|
+
const agingSection = agingItems.length > 0 ? collapsibleSection(
|
|
19600
|
+
"dm-risks-aging",
|
|
19601
|
+
`Aging Items (${agingItems.length})`,
|
|
19602
|
+
`<div class="table-wrap">
|
|
19603
|
+
<table>
|
|
19604
|
+
<thead>
|
|
19605
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Created</th><th>Age</th></tr>
|
|
19606
|
+
</thead>
|
|
19607
|
+
<tbody>
|
|
19608
|
+
${agingItems.map((d) => {
|
|
19609
|
+
const ageDays = Math.floor((todayMs - new Date(d.frontmatter.created).getTime()) / 864e5);
|
|
19610
|
+
return `
|
|
19611
|
+
<tr>
|
|
19612
|
+
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
19613
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
19614
|
+
<td>${escapeHtml(typeLabel(d.frontmatter.type))}</td>
|
|
19615
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
19616
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
19617
|
+
<td><span class="${ageDays > 30 ? "priority-high" : "priority-medium"}">${ageDays}d</span></td>
|
|
19618
|
+
</tr>`;
|
|
19619
|
+
}).join("")}
|
|
19620
|
+
</tbody>
|
|
19621
|
+
</table>
|
|
19622
|
+
</div>`,
|
|
19623
|
+
{ titleTag: "h3" }
|
|
19624
|
+
) : "";
|
|
19625
|
+
const healthSection = collapsibleSection(
|
|
19626
|
+
"dm-risks-health",
|
|
19627
|
+
"Health Overview",
|
|
19628
|
+
`<div class="gar-overall">
|
|
19629
|
+
<div class="dot dot-${healthReport.overall}"></div>
|
|
19630
|
+
<div class="label">Overall: ${escapeHtml(healthReport.overall)}</div>
|
|
19631
|
+
</div>
|
|
19632
|
+
<div class="gar-areas">
|
|
19633
|
+
${healthReport.completeness.map((cat) => `
|
|
19634
|
+
<div class="gar-area">
|
|
19635
|
+
<div class="area-header">
|
|
19636
|
+
<div class="area-dot dot-${cat.status}"></div>
|
|
19637
|
+
<div class="area-name">${escapeHtml(cat.name)}</div>
|
|
19638
|
+
</div>
|
|
19639
|
+
<div class="area-summary">${escapeHtml(cat.summary)}</div>
|
|
19640
|
+
</div>`).join("")}
|
|
19641
|
+
${healthReport.process.map((cat) => `
|
|
19642
|
+
<div class="gar-area">
|
|
19643
|
+
<div class="area-header">
|
|
19644
|
+
<div class="area-dot dot-${cat.status}"></div>
|
|
19645
|
+
<div class="area-name">${escapeHtml(cat.name)}</div>
|
|
19646
|
+
</div>
|
|
19647
|
+
<div class="area-summary">${escapeHtml(cat.summary)}</div>
|
|
19648
|
+
</div>`).join("")}
|
|
19649
|
+
</div>`,
|
|
19650
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
19651
|
+
);
|
|
19652
|
+
return `
|
|
19653
|
+
<div class="page-header">
|
|
19654
|
+
<h2>Risk & Blockers</h2>
|
|
19655
|
+
<div class="subtitle">Identify and track project risks, blockers, and aging items</div>
|
|
19656
|
+
</div>
|
|
19657
|
+
${statsCards}
|
|
19658
|
+
${blockedSection}
|
|
19659
|
+
${agingSection}
|
|
19660
|
+
${healthSection}
|
|
19661
|
+
`;
|
|
19662
|
+
}
|
|
19663
|
+
|
|
19664
|
+
// src/web/templates/pages/dm/meetings.ts
|
|
19665
|
+
var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19666
|
+
function dmMeetingsPage(ctx) {
|
|
19667
|
+
const meetings = ctx.store.list({ type: "meeting" });
|
|
19668
|
+
const actions = ctx.store.list({ type: "action" });
|
|
19669
|
+
const sortedMeetings = [...meetings].sort((a, b) => {
|
|
19670
|
+
const dateA = a.frontmatter.date ?? a.frontmatter.created;
|
|
19671
|
+
const dateB = b.frontmatter.date ?? b.frontmatter.created;
|
|
19672
|
+
return dateB.localeCompare(dateA);
|
|
19673
|
+
});
|
|
19674
|
+
const meetingActionMap = /* @__PURE__ */ new Map();
|
|
19675
|
+
for (const meeting of meetings) {
|
|
19676
|
+
const mid = meeting.frontmatter.id;
|
|
19677
|
+
const relatedActions = actions.filter((a) => {
|
|
19678
|
+
const tags = a.frontmatter.tags ?? [];
|
|
19679
|
+
const hasMeetingTag = tags.some((t) => t.startsWith("meeting:") && t.slice(8) === mid);
|
|
19680
|
+
const mentionsInContent = (a.content ?? "").includes(mid);
|
|
19681
|
+
const source = a.frontmatter.source;
|
|
19682
|
+
const fromMeeting = typeof source === "string" && source.includes(mid);
|
|
19683
|
+
return hasMeetingTag || mentionsInContent || fromMeeting;
|
|
19684
|
+
});
|
|
19685
|
+
if (relatedActions.length > 0) {
|
|
19686
|
+
meetingActionMap.set(mid, relatedActions);
|
|
19687
|
+
}
|
|
19688
|
+
}
|
|
19689
|
+
const statsCards = `
|
|
19690
|
+
<div class="cards">
|
|
19691
|
+
<div class="card">
|
|
19692
|
+
<div class="card-label">Total Meetings</div>
|
|
19693
|
+
<div class="card-value">${meetings.length}</div>
|
|
19694
|
+
<div class="card-sub">recorded</div>
|
|
19695
|
+
</div>
|
|
19696
|
+
<div class="card">
|
|
19697
|
+
<div class="card-label">With Actions</div>
|
|
19698
|
+
<div class="card-value">${meetingActionMap.size}</div>
|
|
19699
|
+
<div class="card-sub">meetings with linked actions</div>
|
|
19700
|
+
</div>
|
|
19701
|
+
</div>`;
|
|
19702
|
+
const meetingsTable = sortedMeetings.length > 0 ? `<div class="table-wrap">
|
|
19703
|
+
<table>
|
|
19704
|
+
<thead>
|
|
19705
|
+
<tr><th>Date</th><th>ID</th><th>Title</th><th>Actions</th></tr>
|
|
19706
|
+
</thead>
|
|
19707
|
+
<tbody>
|
|
19708
|
+
${sortedMeetings.map((m) => {
|
|
19709
|
+
const date5 = m.frontmatter.date ?? m.frontmatter.created;
|
|
19710
|
+
const relatedActions = meetingActionMap.get(m.frontmatter.id) ?? [];
|
|
19711
|
+
const openCount = relatedActions.filter((a) => !DONE_STATUSES7.has(a.frontmatter.status)).length;
|
|
19712
|
+
return `
|
|
19713
|
+
<tr>
|
|
19714
|
+
<td>${formatDate(date5)}</td>
|
|
19715
|
+
<td><a href="/docs/meeting/${escapeHtml(m.frontmatter.id)}">${escapeHtml(m.frontmatter.id)}</a></td>
|
|
19716
|
+
<td>${escapeHtml(m.frontmatter.title)}</td>
|
|
19717
|
+
<td>${relatedActions.length > 0 ? `${relatedActions.length} (${openCount} open)` : '<span class="text-dim">\u2014</span>'}</td>
|
|
19718
|
+
</tr>`;
|
|
19719
|
+
}).join("")}
|
|
19720
|
+
</tbody>
|
|
19721
|
+
</table>
|
|
19722
|
+
</div>` : '<div class="empty"><p>No meetings recorded.</p></div>';
|
|
19723
|
+
const recentMeetingActions = [];
|
|
19724
|
+
for (const [mid, acts] of meetingActionMap) {
|
|
19725
|
+
for (const act of acts) {
|
|
19726
|
+
if (!DONE_STATUSES7.has(act.frontmatter.status)) {
|
|
19727
|
+
recentMeetingActions.push({ action: act, meetingId: mid });
|
|
19728
|
+
}
|
|
19729
|
+
}
|
|
19730
|
+
}
|
|
19731
|
+
recentMeetingActions.sort((a, b) => {
|
|
19732
|
+
const da = a.action.frontmatter.dueDate ?? a.action.frontmatter.created;
|
|
19733
|
+
const db = b.action.frontmatter.dueDate ?? b.action.frontmatter.created;
|
|
19734
|
+
return da.localeCompare(db);
|
|
19735
|
+
});
|
|
19736
|
+
const actionItemsSection = recentMeetingActions.length > 0 ? collapsibleSection(
|
|
19737
|
+
"dm-meetings-actions",
|
|
19738
|
+
`Open Meeting Action Items (${recentMeetingActions.length})`,
|
|
19739
|
+
`<div class="table-wrap">
|
|
19740
|
+
<table>
|
|
19741
|
+
<thead>
|
|
19742
|
+
<tr><th>Action ID</th><th>Title</th><th>Meeting</th><th>Owner</th><th>Due</th><th>Status</th></tr>
|
|
19743
|
+
</thead>
|
|
19744
|
+
<tbody>
|
|
19745
|
+
${recentMeetingActions.map(({ action: a, meetingId }) => `
|
|
19746
|
+
<tr>
|
|
19747
|
+
<td><a href="/docs/action/${escapeHtml(a.frontmatter.id)}">${escapeHtml(a.frontmatter.id)}</a></td>
|
|
19748
|
+
<td>${escapeHtml(a.frontmatter.title)}</td>
|
|
19749
|
+
<td><a href="/docs/meeting/${escapeHtml(meetingId)}">${escapeHtml(meetingId)}</a></td>
|
|
19750
|
+
<td>${a.frontmatter.owner ? escapeHtml(a.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19751
|
+
<td>${a.frontmatter.dueDate ? formatDate(a.frontmatter.dueDate) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19752
|
+
<td>${statusBadge(a.frontmatter.status)}</td>
|
|
19753
|
+
</tr>`).join("")}
|
|
19754
|
+
</tbody>
|
|
19755
|
+
</table>
|
|
19756
|
+
</div>`,
|
|
19757
|
+
{ titleTag: "h3" }
|
|
19758
|
+
) : "";
|
|
19759
|
+
return `
|
|
19760
|
+
<div class="page-header">
|
|
19761
|
+
<h2>Meetings</h2>
|
|
19762
|
+
<div class="subtitle">Meeting log and cross-referenced action items</div>
|
|
19763
|
+
</div>
|
|
19764
|
+
${statsCards}
|
|
19765
|
+
${collapsibleSection("dm-meetings-log", `Meeting Log (${sortedMeetings.length})`, meetingsTable, { titleTag: "h3" })}
|
|
19766
|
+
${actionItemsSection}
|
|
19767
|
+
`;
|
|
19768
|
+
}
|
|
19769
|
+
|
|
19770
|
+
// src/web/templates/pages/dm/governance.ts
|
|
19771
|
+
function dmGovernancePage(ctx) {
|
|
19772
|
+
const garReport = getGarData(ctx.store, ctx.projectName);
|
|
19773
|
+
const healthMetrics = collectHealthMetrics(ctx.store);
|
|
19774
|
+
const healthReport = evaluateHealth(ctx.projectName, healthMetrics);
|
|
19775
|
+
const garContent = garPage(garReport);
|
|
19776
|
+
const healthContent = healthPage(healthReport, healthMetrics);
|
|
19777
|
+
return `
|
|
19778
|
+
<div class="page-header">
|
|
19779
|
+
<h2>Governance</h2>
|
|
19780
|
+
<div class="subtitle">GAR report and health check combined view</div>
|
|
19781
|
+
</div>
|
|
19782
|
+
${collapsibleSection("dm-gov-gar", "GAR Report", garContent, { titleTag: "h3" })}
|
|
19783
|
+
${collapsibleSection("dm-gov-health", "Health Check", healthContent, { titleTag: "h3" })}
|
|
19784
|
+
`;
|
|
19785
|
+
}
|
|
19786
|
+
|
|
19787
|
+
// src/web/persona-configs/dm.ts
|
|
19788
|
+
registerPersonaView({
|
|
19789
|
+
shortName: "dm",
|
|
19790
|
+
displayName: "Delivery Manager",
|
|
19791
|
+
description: "Sprint execution, action tracking, and risk management",
|
|
19792
|
+
color: "#34d399",
|
|
19793
|
+
navItems: [
|
|
19794
|
+
{ path: "/dm/dashboard", label: "Dashboard" },
|
|
19795
|
+
{ path: "/dm/sprint", label: "Sprint Execution" },
|
|
19796
|
+
{ path: "/dm/actions", label: "Action Tracker" },
|
|
19797
|
+
{ path: "/dm/risks", label: "Risk & Blockers" },
|
|
19798
|
+
{ path: "/dm/meetings", label: "Meetings" },
|
|
19799
|
+
{ path: "/dm/governance", label: "Governance" }
|
|
19800
|
+
]
|
|
19801
|
+
});
|
|
19802
|
+
registerPersonaPage("dm", "dashboard", dmDashboardPage);
|
|
19803
|
+
registerPersonaPage("dm", "sprint", dmSprintPage);
|
|
19804
|
+
registerPersonaPage("dm", "actions", dmActionsPage);
|
|
19805
|
+
registerPersonaPage("dm", "risks", dmRisksPage);
|
|
19806
|
+
registerPersonaPage("dm", "meetings", dmMeetingsPage);
|
|
19807
|
+
registerPersonaPage("dm", "governance", dmGovernancePage);
|
|
19808
|
+
|
|
19809
|
+
// src/web/templates/pages/tl/dashboard.ts
|
|
19810
|
+
var DONE_STATUSES8 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19811
|
+
function tlDashboardPage(ctx) {
|
|
19812
|
+
const epics = ctx.store.list({ type: "epic" });
|
|
19813
|
+
const tasks = ctx.store.list({ type: "task" });
|
|
19814
|
+
const decisions = ctx.store.list({ type: "decision" });
|
|
19815
|
+
const questions = ctx.store.list({ type: "question" });
|
|
19816
|
+
const diagrams = getDiagramData(ctx.store);
|
|
19817
|
+
const openEpics = epics.filter((d) => !DONE_STATUSES8.has(d.frontmatter.status));
|
|
19818
|
+
const openTasks = tasks.filter((d) => !DONE_STATUSES8.has(d.frontmatter.status));
|
|
19819
|
+
const technicalDecisions = decisions.filter((d) => {
|
|
19820
|
+
const tags = d.frontmatter.tags ?? [];
|
|
19821
|
+
return tags.some((t) => t.toLowerCase().includes("technical") || t.toLowerCase().includes("architecture"));
|
|
19822
|
+
});
|
|
19823
|
+
const openTechDecisions = technicalDecisions.filter((d) => !DONE_STATUSES8.has(d.frontmatter.status));
|
|
19824
|
+
const pendingDecisions = openTechDecisions.length > 0 ? openTechDecisions : decisions.filter((d) => !DONE_STATUSES8.has(d.frontmatter.status));
|
|
19825
|
+
const statsCards = `
|
|
19826
|
+
<div class="cards">
|
|
19827
|
+
<div class="card">
|
|
19828
|
+
<a href="/tl/backlog">
|
|
19829
|
+
<div class="card-label">Open Epics</div>
|
|
19830
|
+
<div class="card-value">${openEpics.length}</div>
|
|
19831
|
+
<div class="card-sub">${epics.length} total</div>
|
|
19832
|
+
</a>
|
|
19833
|
+
</div>
|
|
19834
|
+
<div class="card">
|
|
19835
|
+
<a href="/tl/backlog">
|
|
19836
|
+
<div class="card-label">Open Tasks</div>
|
|
19837
|
+
<div class="card-value">${openTasks.length}</div>
|
|
19838
|
+
<div class="card-sub">${tasks.length} total</div>
|
|
19839
|
+
</a>
|
|
19840
|
+
</div>
|
|
19841
|
+
<div class="card">
|
|
19842
|
+
<a href="/tl/decisions">
|
|
19843
|
+
<div class="card-label">Pending Decisions</div>
|
|
19844
|
+
<div class="card-value${pendingDecisions.length > 0 ? " priority-medium" : ""}">${pendingDecisions.length}</div>
|
|
19845
|
+
<div class="card-sub">needing resolution</div>
|
|
19846
|
+
</a>
|
|
19847
|
+
</div>
|
|
19848
|
+
<div class="card">
|
|
19849
|
+
<a href="/tl/sprint">
|
|
19850
|
+
<div class="card-label">Blocked</div>
|
|
19851
|
+
<div class="card-value${tasks.filter((t) => t.frontmatter.status === "blocked").length > 0 ? " priority-high" : ""}">${tasks.filter((t) => t.frontmatter.status === "blocked").length}</div>
|
|
19852
|
+
<div class="card-sub">blocked tasks</div>
|
|
19853
|
+
</a>
|
|
19854
|
+
</div>
|
|
19855
|
+
</div>`;
|
|
19856
|
+
const diagramSection = collapsibleSection(
|
|
19857
|
+
"tl-dash-diagram",
|
|
19858
|
+
"Architecture Relationships",
|
|
19859
|
+
buildArtifactFlowchart(diagrams),
|
|
19860
|
+
{ titleTag: "h3" }
|
|
19861
|
+
);
|
|
19862
|
+
return `
|
|
19863
|
+
<div class="page-header">
|
|
19864
|
+
<h2>Technical Lead Dashboard</h2>
|
|
19865
|
+
<div class="subtitle">Technical backlog, architecture decisions, and sprint work</div>
|
|
19866
|
+
</div>
|
|
19867
|
+
${statsCards}
|
|
19868
|
+
${diagramSection}
|
|
19869
|
+
`;
|
|
19870
|
+
}
|
|
19871
|
+
|
|
19872
|
+
// src/web/templates/pages/tl/backlog.ts
|
|
19873
|
+
var DONE_STATUSES9 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19874
|
+
function tlBacklogPage(ctx) {
|
|
19875
|
+
const epics = ctx.store.list({ type: "epic" });
|
|
19876
|
+
const tasks = ctx.store.list({ type: "task" });
|
|
19877
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
19878
|
+
for (const task of tasks) {
|
|
19879
|
+
const tags = task.frontmatter.tags ?? [];
|
|
19880
|
+
for (const tag of tags) {
|
|
19881
|
+
if (tag.startsWith("epic:")) {
|
|
19882
|
+
const epicId = tag.slice(5);
|
|
19883
|
+
const existing = epicToTasks.get(epicId) ?? [];
|
|
19884
|
+
existing.push(task);
|
|
19885
|
+
epicToTasks.set(epicId, existing);
|
|
19886
|
+
}
|
|
19887
|
+
}
|
|
19888
|
+
}
|
|
19889
|
+
const epicFeatureMap = /* @__PURE__ */ new Map();
|
|
19890
|
+
for (const epic of epics) {
|
|
19891
|
+
const linked = epic.frontmatter.linkedFeature;
|
|
19892
|
+
const featureIds = Array.isArray(linked) ? linked.map(String) : linked ? [String(linked)] : [];
|
|
19893
|
+
epicFeatureMap.set(epic.frontmatter.id, featureIds);
|
|
19894
|
+
}
|
|
19895
|
+
const statusOrder = { "in-progress": 0, open: 1, draft: 2, blocked: 3, done: 4, closed: 5, resolved: 6 };
|
|
19896
|
+
const sortedEpics = [...epics].sort((a, b) => {
|
|
19897
|
+
const sa = statusOrder[a.frontmatter.status] ?? 3;
|
|
19898
|
+
const sb = statusOrder[b.frontmatter.status] ?? 3;
|
|
19899
|
+
if (sa !== sb) return sa - sb;
|
|
19900
|
+
return a.frontmatter.id.localeCompare(b.frontmatter.id);
|
|
19901
|
+
});
|
|
19902
|
+
const epicsTable = sortedEpics.length > 0 ? `<div class="table-wrap">
|
|
19903
|
+
<table>
|
|
19904
|
+
<thead>
|
|
19905
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th><th>Linked Feature</th></tr>
|
|
19906
|
+
</thead>
|
|
19907
|
+
<tbody>
|
|
19908
|
+
${sortedEpics.map((e) => {
|
|
19909
|
+
const eTasks = epicToTasks.get(e.frontmatter.id) ?? [];
|
|
19910
|
+
const done = eTasks.filter((t) => DONE_STATUSES9.has(t.frontmatter.status)).length;
|
|
19911
|
+
const featureIds = epicFeatureMap.get(e.frontmatter.id) ?? [];
|
|
19912
|
+
const featureLinks = featureIds.map((fid) => `<a href="/docs/feature/${escapeHtml(fid)}">${escapeHtml(fid)}</a>`).join(", ");
|
|
19913
|
+
return `
|
|
19914
|
+
<tr>
|
|
19915
|
+
<td><a href="/docs/epic/${escapeHtml(e.frontmatter.id)}">${escapeHtml(e.frontmatter.id)}</a></td>
|
|
19916
|
+
<td>${escapeHtml(e.frontmatter.title)}</td>
|
|
19917
|
+
<td>${statusBadge(e.frontmatter.status)}</td>
|
|
19918
|
+
<td>${done}/${eTasks.length}</td>
|
|
19919
|
+
<td>${featureLinks || '<span class="text-dim">\u2014</span>'}</td>
|
|
19920
|
+
</tr>`;
|
|
19921
|
+
}).join("")}
|
|
19922
|
+
</tbody>
|
|
19923
|
+
</table>
|
|
19924
|
+
</div>` : '<div class="empty"><p>No epics found.</p></div>';
|
|
19925
|
+
const assignedTaskIds = /* @__PURE__ */ new Set();
|
|
19926
|
+
for (const taskList of epicToTasks.values()) {
|
|
19927
|
+
for (const t of taskList) assignedTaskIds.add(t.frontmatter.id);
|
|
19928
|
+
}
|
|
19929
|
+
const unassignedTasks = tasks.filter(
|
|
19930
|
+
(t) => !assignedTaskIds.has(t.frontmatter.id) && !DONE_STATUSES9.has(t.frontmatter.status)
|
|
19931
|
+
);
|
|
19932
|
+
const unassignedSection = unassignedTasks.length > 0 ? collapsibleSection(
|
|
19933
|
+
"tl-backlog-unassigned",
|
|
19934
|
+
`Unassigned Tasks (${unassignedTasks.length})`,
|
|
19935
|
+
`<div class="table-wrap">
|
|
19936
|
+
<table>
|
|
19937
|
+
<thead>
|
|
19938
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Owner</th><th>Created</th></tr>
|
|
19939
|
+
</thead>
|
|
19940
|
+
<tbody>
|
|
19941
|
+
${unassignedTasks.map((t) => `
|
|
19942
|
+
<tr>
|
|
19943
|
+
<td><a href="/docs/task/${escapeHtml(t.frontmatter.id)}">${escapeHtml(t.frontmatter.id)}</a></td>
|
|
19944
|
+
<td>${escapeHtml(t.frontmatter.title)}</td>
|
|
19945
|
+
<td>${statusBadge(t.frontmatter.status)}</td>
|
|
19946
|
+
<td>${t.frontmatter.owner ? escapeHtml(t.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
19947
|
+
<td>${formatDate(t.frontmatter.created)}</td>
|
|
19948
|
+
</tr>`).join("")}
|
|
19949
|
+
</tbody>
|
|
19950
|
+
</table>
|
|
19951
|
+
</div>`,
|
|
19952
|
+
{ titleTag: "h3" }
|
|
19953
|
+
) : "";
|
|
19954
|
+
const taskBoard = getBoardData(ctx.store, "task");
|
|
19955
|
+
const boardHtml = taskBoard.columns.length > 0 ? `<div class="board">
|
|
19956
|
+
${taskBoard.columns.map((col) => `
|
|
19957
|
+
<div class="board-column">
|
|
19958
|
+
<div class="board-column-header">
|
|
19959
|
+
<span>${escapeHtml(col.status)}</span>
|
|
19960
|
+
<span class="count">${col.docs.length}</span>
|
|
19961
|
+
</div>
|
|
19962
|
+
${col.docs.map((d) => `
|
|
19963
|
+
<div class="board-card">
|
|
19964
|
+
<a href="/docs/task/${escapeHtml(d.frontmatter.id)}">
|
|
19965
|
+
<div class="bc-id">${escapeHtml(d.frontmatter.id)}</div>
|
|
19966
|
+
<div class="bc-title">${escapeHtml(d.frontmatter.title)}</div>
|
|
19967
|
+
${d.frontmatter.owner ? `<div class="bc-owner">${escapeHtml(d.frontmatter.owner)}</div>` : ""}
|
|
19968
|
+
</a>
|
|
19969
|
+
</div>`).join("")}
|
|
19970
|
+
</div>`).join("")}
|
|
19971
|
+
</div>` : "";
|
|
19972
|
+
const boardSection = boardHtml ? collapsibleSection("tl-backlog-board", "Task Board", boardHtml, { titleTag: "h3", defaultCollapsed: true }) : "";
|
|
19973
|
+
return `
|
|
19974
|
+
<div class="page-header">
|
|
19975
|
+
<h2>Technical Backlog</h2>
|
|
19976
|
+
<div class="subtitle">${epics.length} epics, ${tasks.length} tasks</div>
|
|
19977
|
+
</div>
|
|
19978
|
+
${collapsibleSection("tl-backlog-epics", `Epics (${epics.length})`, epicsTable, { titleTag: "h3" })}
|
|
19979
|
+
${unassignedSection}
|
|
19980
|
+
${boardSection}
|
|
19981
|
+
`;
|
|
19982
|
+
}
|
|
19983
|
+
|
|
19984
|
+
// src/web/templates/pages/tl/sprint.ts
|
|
19985
|
+
var TL_CONTRIBUTION_TYPES = /* @__PURE__ */ new Set([
|
|
19986
|
+
"action-result",
|
|
19987
|
+
"spike-findings",
|
|
19988
|
+
"technical-assessment",
|
|
19989
|
+
"architecture-review"
|
|
19990
|
+
]);
|
|
19991
|
+
var DONE_STATUSES10 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19992
|
+
function progressBar4(pct) {
|
|
19993
|
+
return `<div class="sprint-progress-bar">
|
|
19994
|
+
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
19995
|
+
<span class="sprint-progress-label">${pct}%</span>
|
|
19996
|
+
</div>`;
|
|
19997
|
+
}
|
|
19998
|
+
function tlSprintPage(ctx) {
|
|
19999
|
+
const data = getSprintSummaryData(ctx.store);
|
|
20000
|
+
if (!data) {
|
|
20001
|
+
return `
|
|
20002
|
+
<div class="page-header">
|
|
20003
|
+
<h2>Sprint Work</h2>
|
|
20004
|
+
<div class="subtitle">Technical sprint items and contributions</div>
|
|
20005
|
+
</div>
|
|
20006
|
+
<div class="empty">
|
|
20007
|
+
<h3>No Active Sprint</h3>
|
|
20008
|
+
<p>No active sprint found. Create a sprint and set its status to "active" to track sprint work.</p>
|
|
20009
|
+
</div>`;
|
|
20010
|
+
}
|
|
20011
|
+
const techTypes = /* @__PURE__ */ new Set(["epic", "task"]);
|
|
20012
|
+
const techItems = data.workItems.items.filter((w) => techTypes.has(w.type));
|
|
20013
|
+
const techDone = techItems.filter((w) => DONE_STATUSES10.has(w.status)).length;
|
|
20014
|
+
const allDocs = ctx.store.list();
|
|
20015
|
+
const tlContributions = allDocs.filter((d) => TL_CONTRIBUTION_TYPES.has(d.frontmatter.type));
|
|
20016
|
+
const statsCards = `
|
|
20017
|
+
<div class="cards">
|
|
20018
|
+
<div class="card">
|
|
20019
|
+
<div class="card-label">Sprint Progress</div>
|
|
20020
|
+
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
20021
|
+
<div class="card-sub">${data.timeline.daysRemaining} days remaining</div>
|
|
20022
|
+
</div>
|
|
20023
|
+
<div class="card">
|
|
20024
|
+
<div class="card-label">Tech Items</div>
|
|
20025
|
+
<div class="card-value">${techItems.length}</div>
|
|
20026
|
+
<div class="card-sub">${techDone} done</div>
|
|
20027
|
+
</div>
|
|
20028
|
+
<div class="card">
|
|
20029
|
+
<div class="card-label">Epics</div>
|
|
20030
|
+
<div class="card-value">${data.linkedEpics.length}</div>
|
|
20031
|
+
<div class="card-sub">linked to sprint</div>
|
|
20032
|
+
</div>
|
|
20033
|
+
<div class="card">
|
|
20034
|
+
<div class="card-label">TL Contributions</div>
|
|
20035
|
+
<div class="card-value">${tlContributions.length}</div>
|
|
20036
|
+
<div class="card-sub">reviews, spikes, assessments</div>
|
|
20037
|
+
</div>
|
|
20038
|
+
</div>`;
|
|
20039
|
+
const sprintHeader = `
|
|
20040
|
+
<div class="sprint-goal">
|
|
20041
|
+
<strong>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</strong>
|
|
20042
|
+
${data.sprint.goal ? ` | ${escapeHtml(data.sprint.goal)}` : ""}
|
|
20043
|
+
</div>`;
|
|
20044
|
+
const workItemsSection = techItems.length > 0 ? collapsibleSection(
|
|
20045
|
+
"tl-sprint-items",
|
|
20046
|
+
`Sprint Work Items (${techItems.length})`,
|
|
20047
|
+
`<div class="table-wrap">
|
|
20048
|
+
<table>
|
|
20049
|
+
<thead>
|
|
20050
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Stream</th></tr>
|
|
20051
|
+
</thead>
|
|
20052
|
+
<tbody>
|
|
20053
|
+
${techItems.map((w) => `
|
|
20054
|
+
<tr>
|
|
20055
|
+
<td><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
20056
|
+
<td>${escapeHtml(w.title)}</td>
|
|
20057
|
+
<td>${escapeHtml(typeLabel(w.type))}</td>
|
|
20058
|
+
<td>${statusBadge(w.status)}</td>
|
|
20059
|
+
<td>${w.workStream ? `<span class="badge badge-subtle">${escapeHtml(w.workStream)}</span>` : '<span class="text-dim">\u2014</span>'}</td>
|
|
20060
|
+
</tr>`).join("")}
|
|
20061
|
+
</tbody>
|
|
20062
|
+
</table>
|
|
20063
|
+
</div>`,
|
|
20064
|
+
{ titleTag: "h3" }
|
|
20065
|
+
) : "";
|
|
20066
|
+
const contributionsSection = tlContributions.length > 0 ? collapsibleSection(
|
|
20067
|
+
"tl-sprint-contributions",
|
|
20068
|
+
`TL Contributions (${tlContributions.length})`,
|
|
20069
|
+
`<div class="table-wrap">
|
|
20070
|
+
<table>
|
|
20071
|
+
<thead>
|
|
20072
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th><th>Date</th></tr>
|
|
20073
|
+
</thead>
|
|
20074
|
+
<tbody>
|
|
20075
|
+
${tlContributions.map((d) => `
|
|
20076
|
+
<tr>
|
|
20077
|
+
<td><a href="/docs/${escapeHtml(d.frontmatter.type)}/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
20078
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
20079
|
+
<td>${escapeHtml(typeLabel(d.frontmatter.type))}</td>
|
|
20080
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
20081
|
+
<td>${formatDate(d.frontmatter.updated ?? d.frontmatter.created)}</td>
|
|
20082
|
+
</tr>`).join("")}
|
|
20083
|
+
</tbody>
|
|
20084
|
+
</table>
|
|
20085
|
+
</div>`,
|
|
20086
|
+
{ titleTag: "h3" }
|
|
20087
|
+
) : "";
|
|
20088
|
+
const epicsSection = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
20089
|
+
"tl-sprint-epics",
|
|
20090
|
+
"Linked Epics",
|
|
20091
|
+
`<div class="table-wrap">
|
|
20092
|
+
<table>
|
|
20093
|
+
<thead>
|
|
20094
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks Done</th></tr>
|
|
20095
|
+
</thead>
|
|
20096
|
+
<tbody>
|
|
20097
|
+
${data.linkedEpics.map((e) => `
|
|
20098
|
+
<tr>
|
|
20099
|
+
<td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
|
|
20100
|
+
<td>${escapeHtml(e.title)}</td>
|
|
20101
|
+
<td>${statusBadge(e.status)}</td>
|
|
20102
|
+
<td>${e.tasksDone} / ${e.tasksTotal}</td>
|
|
20103
|
+
</tr>`).join("")}
|
|
20104
|
+
</tbody>
|
|
20105
|
+
</table>
|
|
20106
|
+
</div>`,
|
|
20107
|
+
{ titleTag: "h3" }
|
|
20108
|
+
) : "";
|
|
20109
|
+
return `
|
|
20110
|
+
<div class="page-header">
|
|
20111
|
+
<h2>Sprint Work</h2>
|
|
20112
|
+
<div class="subtitle">Technical sprint items and contributions</div>
|
|
20113
|
+
</div>
|
|
20114
|
+
${sprintHeader}
|
|
20115
|
+
${progressBar4(data.workItems.completionPct)}
|
|
20116
|
+
${statsCards}
|
|
20117
|
+
${workItemsSection}
|
|
20118
|
+
${epicsSection}
|
|
20119
|
+
${contributionsSection}
|
|
20120
|
+
`;
|
|
20121
|
+
}
|
|
20122
|
+
|
|
20123
|
+
// src/web/templates/pages/tl/decisions.ts
|
|
20124
|
+
var DONE_STATUSES11 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
20125
|
+
function tlDecisionsPage(ctx) {
|
|
20126
|
+
const decisions = ctx.store.list({ type: "decision" });
|
|
20127
|
+
const questions = ctx.store.list({ type: "question" });
|
|
20128
|
+
const technicalDecisions = decisions.filter((d) => {
|
|
20129
|
+
const tags = d.frontmatter.tags ?? [];
|
|
20130
|
+
return tags.some((t) => {
|
|
20131
|
+
const lower = t.toLowerCase();
|
|
20132
|
+
return lower.includes("technical") || lower.includes("architecture") || lower.includes("design");
|
|
20133
|
+
});
|
|
20134
|
+
});
|
|
20135
|
+
const displayDecisions = technicalDecisions.length > 0 ? technicalDecisions : decisions;
|
|
20136
|
+
const isFiltered = technicalDecisions.length > 0;
|
|
20137
|
+
const openDecisions = displayDecisions.filter((d) => !DONE_STATUSES11.has(d.frontmatter.status));
|
|
20138
|
+
const resolvedDecisions = displayDecisions.filter((d) => DONE_STATUSES11.has(d.frontmatter.status));
|
|
20139
|
+
const technicalQuestions = questions.filter((d) => {
|
|
20140
|
+
const tags = d.frontmatter.tags ?? [];
|
|
20141
|
+
return tags.some((t) => {
|
|
20142
|
+
const lower = t.toLowerCase();
|
|
20143
|
+
return lower.includes("technical") || lower.includes("architecture") || lower.includes("design");
|
|
20144
|
+
});
|
|
20145
|
+
});
|
|
20146
|
+
const displayQuestions = technicalQuestions.length > 0 ? technicalQuestions : questions;
|
|
20147
|
+
const openQuestions = displayQuestions.filter((d) => d.frontmatter.status === "open");
|
|
20148
|
+
const statsCards = `
|
|
20149
|
+
<div class="cards">
|
|
20150
|
+
<div class="card">
|
|
20151
|
+
<div class="card-label">Open Decisions</div>
|
|
20152
|
+
<div class="card-value${openDecisions.length > 0 ? " priority-medium" : ""}">${openDecisions.length}</div>
|
|
20153
|
+
<div class="card-sub">${isFiltered ? "technical" : "all"} decisions</div>
|
|
20154
|
+
</div>
|
|
20155
|
+
<div class="card">
|
|
20156
|
+
<div class="card-label">Resolved</div>
|
|
20157
|
+
<div class="card-value">${resolvedDecisions.length}</div>
|
|
20158
|
+
<div class="card-sub">decisions made</div>
|
|
20159
|
+
</div>
|
|
20160
|
+
<div class="card">
|
|
20161
|
+
<div class="card-label">Open Questions</div>
|
|
20162
|
+
<div class="card-value${openQuestions.length > 0 ? " priority-medium" : ""}">${openQuestions.length}</div>
|
|
20163
|
+
<div class="card-sub">${technicalQuestions.length > 0 ? "technical" : "all"} questions</div>
|
|
20164
|
+
</div>
|
|
20165
|
+
</div>`;
|
|
20166
|
+
function decisionTable(docs) {
|
|
20167
|
+
if (docs.length === 0) return '<div class="empty"><p>None found.</p></div>';
|
|
20168
|
+
return `<div class="table-wrap">
|
|
20169
|
+
<table>
|
|
20170
|
+
<thead>
|
|
20171
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Owner</th><th>Tags</th><th>Created</th></tr>
|
|
20172
|
+
</thead>
|
|
20173
|
+
<tbody>
|
|
20174
|
+
${docs.map((d) => {
|
|
20175
|
+
const tags = d.frontmatter.tags ?? [];
|
|
20176
|
+
return `
|
|
20177
|
+
<tr>
|
|
20178
|
+
<td><a href="/docs/decision/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
20179
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
20180
|
+
<td>${statusBadge(d.frontmatter.status)}</td>
|
|
20181
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
20182
|
+
<td>${tags.length > 0 ? tags.map((t) => `<span class="signal-tag">${escapeHtml(t)}</span>`).join(" ") : '<span class="text-dim">\u2014</span>'}</td>
|
|
20183
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
20184
|
+
</tr>`;
|
|
20185
|
+
}).join("")}
|
|
20186
|
+
</tbody>
|
|
20187
|
+
</table>
|
|
20188
|
+
</div>`;
|
|
20189
|
+
}
|
|
20190
|
+
const openSection = collapsibleSection(
|
|
20191
|
+
"tl-decisions-open",
|
|
20192
|
+
`Open Decisions (${openDecisions.length})`,
|
|
20193
|
+
decisionTable(openDecisions),
|
|
20194
|
+
{ titleTag: "h3" }
|
|
20195
|
+
);
|
|
20196
|
+
const resolvedSection = collapsibleSection(
|
|
20197
|
+
"tl-decisions-resolved",
|
|
20198
|
+
`Resolved Decisions (${resolvedDecisions.length})`,
|
|
20199
|
+
decisionTable(resolvedDecisions),
|
|
20200
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
20201
|
+
);
|
|
20202
|
+
const questionsSection = openQuestions.length > 0 ? collapsibleSection(
|
|
20203
|
+
"tl-decisions-questions",
|
|
20204
|
+
`Open Questions (${openQuestions.length})`,
|
|
20205
|
+
`<div class="table-wrap">
|
|
20206
|
+
<table>
|
|
20207
|
+
<thead>
|
|
20208
|
+
<tr><th>ID</th><th>Title</th><th>Owner</th><th>Created</th></tr>
|
|
20209
|
+
</thead>
|
|
20210
|
+
<tbody>
|
|
20211
|
+
${openQuestions.map((d) => `
|
|
20212
|
+
<tr>
|
|
20213
|
+
<td><a href="/docs/question/${escapeHtml(d.frontmatter.id)}">${escapeHtml(d.frontmatter.id)}</a></td>
|
|
20214
|
+
<td>${escapeHtml(d.frontmatter.title)}</td>
|
|
20215
|
+
<td>${d.frontmatter.owner ? escapeHtml(d.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
20216
|
+
<td>${formatDate(d.frontmatter.created)}</td>
|
|
20217
|
+
</tr>`).join("")}
|
|
20218
|
+
</tbody>
|
|
20219
|
+
</table>
|
|
20220
|
+
</div>`,
|
|
20221
|
+
{ titleTag: "h3" }
|
|
20222
|
+
) : "";
|
|
20223
|
+
return `
|
|
20224
|
+
<div class="page-header">
|
|
20225
|
+
<h2>Architecture Decisions</h2>
|
|
20226
|
+
<div class="subtitle">${isFiltered ? "Technical" : "All"} decisions and open questions</div>
|
|
20227
|
+
</div>
|
|
20228
|
+
${statsCards}
|
|
20229
|
+
${openSection}
|
|
20230
|
+
${questionsSection}
|
|
20231
|
+
${resolvedSection}
|
|
20232
|
+
`;
|
|
20233
|
+
}
|
|
20234
|
+
|
|
20235
|
+
// src/web/templates/pages/tl/health.ts
|
|
20236
|
+
function tlHealthPage(ctx) {
|
|
20237
|
+
const healthMetrics = collectHealthMetrics(ctx.store);
|
|
20238
|
+
const healthReport = evaluateHealth(ctx.projectName, healthMetrics);
|
|
20239
|
+
const upcoming = getUpcomingData(ctx.store);
|
|
20240
|
+
const tasks = ctx.store.list({ type: "task" });
|
|
20241
|
+
const blockedTasks = tasks.filter((t) => t.frontmatter.status === "blocked");
|
|
20242
|
+
const highPriorityBlocked = blockedTasks.filter((t) => {
|
|
20243
|
+
const p = t.frontmatter.priority?.toLowerCase();
|
|
20244
|
+
return p === "critical" || p === "high";
|
|
20245
|
+
});
|
|
20246
|
+
const techTrending = upcoming.trending.filter(
|
|
20247
|
+
(t) => ["task", "action"].includes(t.type)
|
|
20248
|
+
);
|
|
20249
|
+
const statsCards = `
|
|
20250
|
+
<div class="cards">
|
|
20251
|
+
<div class="card">
|
|
20252
|
+
<div class="card-label">Health</div>
|
|
20253
|
+
<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>
|
|
20254
|
+
<div class="card-sub">overall project health</div>
|
|
20255
|
+
</div>
|
|
20256
|
+
<div class="card">
|
|
20257
|
+
<div class="card-label">Blocked Tasks</div>
|
|
20258
|
+
<div class="card-value${blockedTasks.length > 0 ? " priority-high" : ""}">${blockedTasks.length}</div>
|
|
20259
|
+
<div class="card-sub">${highPriorityBlocked.length} high priority</div>
|
|
20260
|
+
</div>
|
|
20261
|
+
<div class="card">
|
|
20262
|
+
<div class="card-label">Completeness</div>
|
|
20263
|
+
<div class="card-value">${healthReport.completeness.filter((c) => c.status === "green").length}/${healthReport.completeness.length}</div>
|
|
20264
|
+
<div class="card-sub">categories green</div>
|
|
20265
|
+
</div>
|
|
20266
|
+
<div class="card">
|
|
20267
|
+
<div class="card-label">Process</div>
|
|
20268
|
+
<div class="card-value">${healthReport.process.filter((c) => c.status === "green").length}/${healthReport.process.length}</div>
|
|
20269
|
+
<div class="card-sub">metrics green</div>
|
|
20270
|
+
</div>
|
|
20271
|
+
</div>`;
|
|
20272
|
+
const gaugeData = Object.entries(healthMetrics.completeness).map(([name, cat]) => ({
|
|
20273
|
+
name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
20274
|
+
complete: cat.complete,
|
|
20275
|
+
total: cat.total
|
|
20276
|
+
}));
|
|
20277
|
+
const gaugeSection = collapsibleSection(
|
|
20278
|
+
"tl-health-gauge",
|
|
20279
|
+
"Completeness Overview",
|
|
20280
|
+
buildHealthGauge(gaugeData),
|
|
20281
|
+
{ titleTag: "h3" }
|
|
20282
|
+
);
|
|
20283
|
+
const processSection = collapsibleSection(
|
|
20284
|
+
"tl-health-process",
|
|
20285
|
+
"Process Health",
|
|
20286
|
+
`<div class="gar-areas">
|
|
20287
|
+
${healthReport.process.map((cat) => `
|
|
20288
|
+
<div class="gar-area">
|
|
20289
|
+
<div class="area-header">
|
|
20290
|
+
<div class="area-dot dot-${cat.status}"></div>
|
|
20291
|
+
<div class="area-name">${escapeHtml(cat.name)}</div>
|
|
20292
|
+
</div>
|
|
20293
|
+
<div class="area-summary">${escapeHtml(cat.summary)}</div>
|
|
20294
|
+
${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>` : ""}
|
|
20295
|
+
</div>`).join("")}
|
|
20296
|
+
</div>`,
|
|
20297
|
+
{ titleTag: "h3" }
|
|
20298
|
+
);
|
|
20299
|
+
const blockedSection = blockedTasks.length > 0 ? collapsibleSection(
|
|
20300
|
+
"tl-health-blocked",
|
|
20301
|
+
`Blocked Tasks (${blockedTasks.length})`,
|
|
20302
|
+
`<div class="table-wrap">
|
|
20303
|
+
<table>
|
|
20304
|
+
<thead>
|
|
20305
|
+
<tr><th>ID</th><th>Title</th><th>Priority</th><th>Owner</th><th>Created</th></tr>
|
|
20306
|
+
</thead>
|
|
20307
|
+
<tbody>
|
|
20308
|
+
${blockedTasks.map((t) => `
|
|
20309
|
+
<tr>
|
|
20310
|
+
<td><a href="/docs/task/${escapeHtml(t.frontmatter.id)}">${escapeHtml(t.frontmatter.id)}</a></td>
|
|
20311
|
+
<td>${escapeHtml(t.frontmatter.title)}</td>
|
|
20312
|
+
<td>${t.frontmatter.priority ? `<span class="${priorityClass(t.frontmatter.priority)}">${escapeHtml(t.frontmatter.priority)}</span>` : '<span class="text-dim">\u2014</span>'}</td>
|
|
20313
|
+
<td>${t.frontmatter.owner ? escapeHtml(t.frontmatter.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
20314
|
+
<td>${formatDate(t.frontmatter.created)}</td>
|
|
20315
|
+
</tr>`).join("")}
|
|
20316
|
+
</tbody>
|
|
20317
|
+
</table>
|
|
20318
|
+
</div>`,
|
|
20319
|
+
{ titleTag: "h3" }
|
|
20320
|
+
) : "";
|
|
20321
|
+
const trendingSection = techTrending.length > 0 ? collapsibleSection(
|
|
20322
|
+
"tl-health-trending",
|
|
20323
|
+
"Trending Technical Items",
|
|
20324
|
+
`<div class="table-wrap">
|
|
20325
|
+
<table>
|
|
20326
|
+
<thead>
|
|
20327
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Score</th><th>Signals</th></tr>
|
|
20328
|
+
</thead>
|
|
20329
|
+
<tbody>
|
|
20330
|
+
${techTrending.slice(0, 10).map((t) => `
|
|
20331
|
+
<tr>
|
|
20332
|
+
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
20333
|
+
<td>${escapeHtml(t.title)}</td>
|
|
20334
|
+
<td>${escapeHtml(typeLabel(t.type))}</td>
|
|
20335
|
+
<td><span class="trending-score">${t.score}</span></td>
|
|
20336
|
+
<td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
|
|
20337
|
+
</tr>`).join("")}
|
|
20338
|
+
</tbody>
|
|
20339
|
+
</table>
|
|
20340
|
+
</div>`,
|
|
20341
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
20342
|
+
) : "";
|
|
20343
|
+
return `
|
|
20344
|
+
<div class="page-header">
|
|
20345
|
+
<h2>Technical Health</h2>
|
|
20346
|
+
<div class="subtitle">Project health, completeness metrics, and blocked items</div>
|
|
20347
|
+
</div>
|
|
20348
|
+
${statsCards}
|
|
20349
|
+
${gaugeSection}
|
|
20350
|
+
${processSection}
|
|
20351
|
+
${blockedSection}
|
|
20352
|
+
${trendingSection}
|
|
20353
|
+
`;
|
|
20354
|
+
}
|
|
20355
|
+
function priorityClass(p) {
|
|
20356
|
+
const lower = p.toLowerCase();
|
|
20357
|
+
if (lower === "critical" || lower === "high") return "priority-high";
|
|
20358
|
+
if (lower === "medium") return "priority-medium";
|
|
20359
|
+
if (lower === "low") return "priority-low";
|
|
20360
|
+
return "";
|
|
20361
|
+
}
|
|
20362
|
+
|
|
20363
|
+
// src/web/persona-configs/tl.ts
|
|
20364
|
+
registerPersonaView({
|
|
20365
|
+
shortName: "tl",
|
|
20366
|
+
displayName: "Technical Lead",
|
|
20367
|
+
description: "Technical backlog, architecture decisions, and sprint work",
|
|
20368
|
+
color: "#fbbf24",
|
|
20369
|
+
navItems: [
|
|
20370
|
+
{ path: "/tl/dashboard", label: "Dashboard" },
|
|
20371
|
+
{ path: "/tl/backlog", label: "Technical Backlog" },
|
|
20372
|
+
{ path: "/tl/sprint", label: "Sprint Work" },
|
|
20373
|
+
{ path: "/tl/decisions", label: "Architecture Decisions" },
|
|
20374
|
+
{ path: "/tl/health", label: "Technical Health" }
|
|
20375
|
+
]
|
|
20376
|
+
});
|
|
20377
|
+
registerPersonaPage("tl", "dashboard", tlDashboardPage);
|
|
20378
|
+
registerPersonaPage("tl", "backlog", tlBacklogPage);
|
|
20379
|
+
registerPersonaPage("tl", "sprint", tlSprintPage);
|
|
20380
|
+
registerPersonaPage("tl", "decisions", tlDecisionsPage);
|
|
20381
|
+
registerPersonaPage("tl", "health", tlHealthPage);
|
|
20382
|
+
|
|
20383
|
+
// src/web/router.ts
|
|
20384
|
+
function buildPersonaLayoutOpts(persona, activePath) {
|
|
20385
|
+
const switcherHtml = renderPersonaSwitcher(persona, activePath);
|
|
20386
|
+
const view = persona ? getPersonaView(persona) : void 0;
|
|
20387
|
+
if (!view) {
|
|
20388
|
+
return { personaSwitcherHtml: switcherHtml };
|
|
20389
|
+
}
|
|
20390
|
+
const isActive = (href) => activePath === href || href !== "/" && activePath.startsWith(href) ? " active" : "";
|
|
20391
|
+
const personaLinks = view.navItems.map(
|
|
20392
|
+
(item) => `<a href="${item.path}" class="${isActive(item.path)}">${escapeHtml(item.label)}</a>`
|
|
20393
|
+
).join("\n ");
|
|
20394
|
+
const navHtml = `
|
|
20395
|
+
${personaLinks}
|
|
20396
|
+
<div class="nav-group">
|
|
20397
|
+
<div class="nav-group-label">Admin</div>
|
|
20398
|
+
<a href="/">Full Dashboard</a>
|
|
20399
|
+
</div>`;
|
|
20400
|
+
return {
|
|
20401
|
+
personaSwitcherHtml: switcherHtml,
|
|
20402
|
+
personaNavHtml: navHtml,
|
|
20403
|
+
personaAccentColor: view.color
|
|
20404
|
+
};
|
|
20405
|
+
}
|
|
20406
|
+
var sprintSummaryCache = /* @__PURE__ */ new Map();
|
|
20407
|
+
function handleRequest(req, res, store, projectName, navGroups) {
|
|
20408
|
+
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
20409
|
+
const pathname = parsed.pathname;
|
|
20410
|
+
const navTypes = store.registeredTypes;
|
|
20411
|
+
const persona = parsePersonaFromPath(pathname);
|
|
20412
|
+
const personaOpts = buildPersonaLayoutOpts(persona, pathname);
|
|
20413
|
+
try {
|
|
20414
|
+
if (pathname === "/styles.css") {
|
|
20415
|
+
res.writeHead(200, {
|
|
20416
|
+
"Content-Type": "text/css",
|
|
20417
|
+
"Cache-Control": "public, max-age=300"
|
|
20418
|
+
});
|
|
20419
|
+
res.end(renderStyles());
|
|
20420
|
+
return;
|
|
20421
|
+
}
|
|
20422
|
+
const personaRootMatch = pathname.match(/^\/(po|dm|tl)$/);
|
|
20423
|
+
if (personaRootMatch) {
|
|
20424
|
+
res.writeHead(302, { Location: `/${personaRootMatch[1]}/dashboard` });
|
|
20425
|
+
res.end();
|
|
20426
|
+
return;
|
|
20427
|
+
}
|
|
20428
|
+
const personaPageMatch = pathname.match(/^\/(po|dm|tl)\/([a-z-]+)$/);
|
|
20429
|
+
if (personaPageMatch) {
|
|
20430
|
+
const [, personaKey, pageId] = personaPageMatch;
|
|
20431
|
+
const pPersona = personaKey;
|
|
20432
|
+
const renderer = getPersonaPageRenderer(personaKey, pageId);
|
|
20433
|
+
const view = getPersonaView(pPersona);
|
|
20434
|
+
const pOpts = buildPersonaLayoutOpts(pPersona, pathname);
|
|
20435
|
+
if (renderer) {
|
|
20436
|
+
const body = renderer({ store, projectName });
|
|
20437
|
+
respond(
|
|
20438
|
+
res,
|
|
20439
|
+
layout(
|
|
20440
|
+
{
|
|
20441
|
+
title: `${view?.displayName ?? personaKey.toUpperCase()} \u2014 ${pageId}`,
|
|
20442
|
+
activePath: pathname,
|
|
20443
|
+
projectName,
|
|
20444
|
+
navGroups,
|
|
20445
|
+
persona: pPersona,
|
|
20446
|
+
...pOpts
|
|
20447
|
+
},
|
|
20448
|
+
body
|
|
20449
|
+
)
|
|
20450
|
+
);
|
|
20451
|
+
} else {
|
|
20452
|
+
const body = `
|
|
20453
|
+
<div class="persona-placeholder">
|
|
20454
|
+
<h3>Coming Soon</h3>
|
|
20455
|
+
<p>The <strong>${pageId}</strong> page for ${view?.displayName ?? personaKey.toUpperCase()} is under construction.</p>
|
|
20456
|
+
<p><a href="/${personaKey}/dashboard">Back to dashboard</a></p>
|
|
20457
|
+
</div>`;
|
|
20458
|
+
respond(
|
|
20459
|
+
res,
|
|
20460
|
+
layout(
|
|
20461
|
+
{
|
|
20462
|
+
title: `${view?.displayName ?? personaKey.toUpperCase()} \u2014 ${pageId}`,
|
|
20463
|
+
activePath: pathname,
|
|
20464
|
+
projectName,
|
|
20465
|
+
navGroups,
|
|
20466
|
+
persona: pPersona,
|
|
20467
|
+
...pOpts
|
|
20468
|
+
},
|
|
20469
|
+
body
|
|
20470
|
+
)
|
|
20471
|
+
);
|
|
20472
|
+
}
|
|
18490
20473
|
return;
|
|
18491
20474
|
}
|
|
18492
20475
|
if (pathname === "/") {
|
|
18493
20476
|
const data = getOverviewData(store);
|
|
18494
20477
|
const diagrams = getDiagramData(store);
|
|
18495
20478
|
const body = overviewPage(data, diagrams, navGroups);
|
|
18496
|
-
|
|
20479
|
+
const banner = renderPersonaBanner();
|
|
20480
|
+
respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups, ...personaOpts, bodyPrefix: banner }, body));
|
|
18497
20481
|
return;
|
|
18498
20482
|
}
|
|
18499
20483
|
if (pathname === "/timeline") {
|
|
18500
20484
|
const diagrams = getDiagramData(store);
|
|
18501
20485
|
const body = timelinePage(diagrams);
|
|
18502
|
-
respond(res, layout({ title: "Timeline", activePath: "/timeline", projectName, navGroups, mainClass: "expanded" }, body));
|
|
20486
|
+
respond(res, layout({ title: "Timeline", activePath: "/timeline", projectName, navGroups, ...personaOpts, mainClass: "expanded" }, body));
|
|
18503
20487
|
return;
|
|
18504
20488
|
}
|
|
18505
20489
|
if (pathname === "/gar") {
|
|
18506
20490
|
const report = getGarData(store, projectName);
|
|
18507
20491
|
const body = garPage(report);
|
|
18508
|
-
respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups }, body));
|
|
20492
|
+
respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups, ...personaOpts }, body));
|
|
18509
20493
|
return;
|
|
18510
20494
|
}
|
|
18511
20495
|
if (pathname === "/health") {
|
|
18512
20496
|
const healthMetrics = collectHealthMetrics(store);
|
|
18513
20497
|
const report = evaluateHealth(projectName, healthMetrics);
|
|
18514
20498
|
const body = healthPage(report, healthMetrics);
|
|
18515
|
-
respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
|
|
20499
|
+
respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups, ...personaOpts }, body));
|
|
18516
20500
|
return;
|
|
18517
20501
|
}
|
|
18518
20502
|
if (pathname === "/upcoming") {
|
|
18519
20503
|
const data = getUpcomingData(store);
|
|
18520
20504
|
const body = upcomingPage(data);
|
|
18521
|
-
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
|
|
20505
|
+
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups, ...personaOpts }, body));
|
|
18522
20506
|
return;
|
|
18523
20507
|
}
|
|
18524
20508
|
if (pathname === "/sprint-summary" && req.method === "GET") {
|
|
@@ -18526,7 +20510,7 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
18526
20510
|
const data = getSprintSummaryData(store, sprintId);
|
|
18527
20511
|
const cached2 = data ? sprintSummaryCache.get(data.sprint.id) : void 0;
|
|
18528
20512
|
const body = sprintSummaryPage(data, cached2 ? { html: cached2.html, generatedAt: cached2.generatedAt } : void 0);
|
|
18529
|
-
respond(res, layout({ title: "Sprint Summary", activePath: "/sprint-summary", projectName, navGroups }, body));
|
|
20513
|
+
respond(res, layout({ title: "Sprint Summary", activePath: "/sprint-summary", projectName, navGroups, ...personaOpts }, body));
|
|
18530
20514
|
return;
|
|
18531
20515
|
}
|
|
18532
20516
|
if (pathname === "/api/sprint-summary" && req.method === "POST") {
|
|
@@ -18536,14 +20520,15 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
18536
20520
|
});
|
|
18537
20521
|
req.on("end", async () => {
|
|
18538
20522
|
try {
|
|
18539
|
-
const { sprintId } = JSON.parse(bodyStr || "{}");
|
|
20523
|
+
const { sprintId, persona: personaKey } = JSON.parse(bodyStr || "{}");
|
|
18540
20524
|
const data = getSprintSummaryData(store, sprintId);
|
|
18541
20525
|
if (!data) {
|
|
18542
20526
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
18543
20527
|
res.end(JSON.stringify({ error: "Sprint not found" }));
|
|
18544
20528
|
return;
|
|
18545
20529
|
}
|
|
18546
|
-
const
|
|
20530
|
+
const personaDef = personaKey ? getPersona(personaKey) : void 0;
|
|
20531
|
+
const summary = await generateSprintSummary(data, personaDef?.systemPrompt);
|
|
18547
20532
|
const html = renderMarkdown(summary);
|
|
18548
20533
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
18549
20534
|
sprintSummaryCache.set(data.sprint.id, { html, generatedAt });
|
|
@@ -18561,12 +20546,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
18561
20546
|
if (boardMatch) {
|
|
18562
20547
|
const type = boardMatch[1];
|
|
18563
20548
|
if (type && !navTypes.includes(type)) {
|
|
18564
|
-
notFound(res, projectName, navGroups, pathname);
|
|
20549
|
+
notFound(res, projectName, navGroups, pathname, personaOpts);
|
|
18565
20550
|
return;
|
|
18566
20551
|
}
|
|
18567
20552
|
const data = getBoardData(store, type);
|
|
18568
20553
|
const body = boardPage(data);
|
|
18569
|
-
respond(res, layout({ title: "Board", activePath: "/board", projectName, navGroups }, body));
|
|
20554
|
+
respond(res, layout({ title: "Board", activePath: "/board", projectName, navGroups, ...personaOpts }, body));
|
|
18570
20555
|
return;
|
|
18571
20556
|
}
|
|
18572
20557
|
const detailMatch = pathname.match(/^\/docs\/([^/]+)\/([^/]+)$/);
|
|
@@ -18574,11 +20559,11 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
18574
20559
|
const [, type, id] = detailMatch;
|
|
18575
20560
|
const doc = getDocumentDetail(store, type, id);
|
|
18576
20561
|
if (!doc) {
|
|
18577
|
-
notFound(res, projectName, navGroups, pathname);
|
|
20562
|
+
notFound(res, projectName, navGroups, pathname, personaOpts);
|
|
18578
20563
|
return;
|
|
18579
20564
|
}
|
|
18580
20565
|
const body = documentDetailPage(doc);
|
|
18581
|
-
respond(res, layout({ title: `${id} \u2014 ${doc.frontmatter.title}`, activePath: `/docs/${type}`, projectName, navGroups }, body));
|
|
20566
|
+
respond(res, layout({ title: `${id} \u2014 ${doc.frontmatter.title}`, activePath: `/docs/${type}`, projectName, navGroups, ...personaOpts }, body));
|
|
18582
20567
|
return;
|
|
18583
20568
|
}
|
|
18584
20569
|
const listMatch = pathname.match(/^\/docs\/([^/]+)$/);
|
|
@@ -18588,14 +20573,14 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
18588
20573
|
const filterOwner = parsed.searchParams.get("owner") ?? void 0;
|
|
18589
20574
|
const data = getDocumentListData(store, type, filterStatus, filterOwner);
|
|
18590
20575
|
if (!data) {
|
|
18591
|
-
notFound(res, projectName, navGroups, pathname);
|
|
20576
|
+
notFound(res, projectName, navGroups, pathname, personaOpts);
|
|
18592
20577
|
return;
|
|
18593
20578
|
}
|
|
18594
20579
|
const body = documentsPage(data);
|
|
18595
|
-
respond(res, layout({ title: `${type}`, activePath: `/docs/${type}`, projectName, navGroups }, body));
|
|
20580
|
+
respond(res, layout({ title: `${type}`, activePath: `/docs/${type}`, projectName, navGroups, ...personaOpts }, body));
|
|
18596
20581
|
return;
|
|
18597
20582
|
}
|
|
18598
|
-
notFound(res, projectName, navGroups, pathname);
|
|
20583
|
+
notFound(res, projectName, navGroups, pathname, personaOpts);
|
|
18599
20584
|
} catch (err) {
|
|
18600
20585
|
console.error("[marvin web] Error handling request:", err);
|
|
18601
20586
|
res.writeHead(500, { "Content-Type": "text/html" });
|
|
@@ -18606,10 +20591,10 @@ function respond(res, html) {
|
|
|
18606
20591
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
18607
20592
|
res.end(html);
|
|
18608
20593
|
}
|
|
18609
|
-
function notFound(res, projectName, navGroups, activePath) {
|
|
20594
|
+
function notFound(res, projectName, navGroups, activePath, pOpts) {
|
|
18610
20595
|
const body = `<div class="empty"><h2>404</h2><p>Page not found.</p><p><a href="/">Go to overview</a></p></div>`;
|
|
18611
20596
|
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
18612
|
-
res.end(layout({ title: "Not Found", activePath, projectName, navGroups }, body));
|
|
20597
|
+
res.end(layout({ title: "Not Found", activePath, projectName, navGroups, ...pOpts }, body));
|
|
18613
20598
|
}
|
|
18614
20599
|
|
|
18615
20600
|
// src/web/server.ts
|
|
@@ -25044,8 +27029,8 @@ function executeImportPlan(plan, store, marvinDir, options) {
|
|
|
25044
27029
|
}
|
|
25045
27030
|
function formatPlanSummary(plan) {
|
|
25046
27031
|
const lines = [];
|
|
25047
|
-
const
|
|
25048
|
-
lines.push(`Detected: ${
|
|
27032
|
+
const typeLabel5 = classificationLabel(plan.classification.type);
|
|
27033
|
+
lines.push(`Detected: ${typeLabel5}`);
|
|
25049
27034
|
lines.push(`Source: ${plan.classification.inputPath}`);
|
|
25050
27035
|
lines.push("");
|
|
25051
27036
|
const imports = plan.items.filter((i) => i.action === "import");
|
|
@@ -26277,7 +28262,7 @@ function createProgram() {
|
|
|
26277
28262
|
const program = new Command();
|
|
26278
28263
|
program.name("marvin").description(
|
|
26279
28264
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
26280
|
-
).version("0.4.
|
|
28265
|
+
).version("0.4.11");
|
|
26281
28266
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
26282
28267
|
await initCommand();
|
|
26283
28268
|
});
|