mrvn-cli 0.4.4 → 0.4.6
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.d.ts +6 -2
- package/dist/index.js +844 -242
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +697 -94
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +700 -96
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin-serve.js
CHANGED
|
@@ -18054,8 +18054,9 @@ function findByJiraKey(store, jiraKey) {
|
|
|
18054
18054
|
const docs = store.list({ type: JIRA_TYPE });
|
|
18055
18055
|
return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
|
|
18056
18056
|
}
|
|
18057
|
-
function createJiraTools(store) {
|
|
18057
|
+
function createJiraTools(store, projectConfig) {
|
|
18058
18058
|
const jiraUserConfig = loadUserConfig().jira;
|
|
18059
|
+
const defaultProjectKey = projectConfig?.jira?.projectKey;
|
|
18059
18060
|
return [
|
|
18060
18061
|
// --- Local read tools ---
|
|
18061
18062
|
tool20(
|
|
@@ -18219,10 +18220,22 @@ function createJiraTools(store) {
|
|
|
18219
18220
|
"Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
|
|
18220
18221
|
{
|
|
18221
18222
|
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
|
|
18222
|
-
projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
|
|
18223
|
+
projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'PROJ'). Falls back to jira.projectKey from .marvin/config.yaml if not provided."),
|
|
18223
18224
|
issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
|
|
18224
18225
|
},
|
|
18225
18226
|
async (args) => {
|
|
18227
|
+
const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
|
|
18228
|
+
if (!resolvedProjectKey) {
|
|
18229
|
+
return {
|
|
18230
|
+
content: [
|
|
18231
|
+
{
|
|
18232
|
+
type: "text",
|
|
18233
|
+
text: "No projectKey provided and no default configured. Either pass projectKey or set jira.projectKey in .marvin/config.yaml."
|
|
18234
|
+
}
|
|
18235
|
+
],
|
|
18236
|
+
isError: true
|
|
18237
|
+
};
|
|
18238
|
+
}
|
|
18226
18239
|
const jira = createJiraClient(jiraUserConfig);
|
|
18227
18240
|
if (!jira) return jiraNotConfiguredError();
|
|
18228
18241
|
const artifact = store.get(args.artifactId);
|
|
@@ -18242,7 +18255,7 @@ function createJiraTools(store) {
|
|
|
18242
18255
|
`Status: ${artifact.frontmatter.status}`
|
|
18243
18256
|
].join("\n");
|
|
18244
18257
|
const jiraResult = await jira.client.createIssue({
|
|
18245
|
-
project: { key:
|
|
18258
|
+
project: { key: resolvedProjectKey },
|
|
18246
18259
|
summary: artifact.frontmatter.title,
|
|
18247
18260
|
description,
|
|
18248
18261
|
issuetype: { name: args.issueType ?? "Task" }
|
|
@@ -18383,14 +18396,14 @@ var jiraSkill = {
|
|
|
18383
18396
|
documentTypeRegistrations: [
|
|
18384
18397
|
{ type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
|
|
18385
18398
|
],
|
|
18386
|
-
tools: (store) => createJiraTools(store),
|
|
18399
|
+
tools: (store, projectConfig) => createJiraTools(store, projectConfig),
|
|
18387
18400
|
promptFragments: {
|
|
18388
18401
|
"product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
|
|
18389
18402
|
|
|
18390
18403
|
**Available tools:**
|
|
18391
18404
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
18392
18405
|
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
18393
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
|
|
18406
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
|
|
18394
18407
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
18395
18408
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
18396
18409
|
|
|
@@ -18404,7 +18417,7 @@ var jiraSkill = {
|
|
|
18404
18417
|
**Available tools:**
|
|
18405
18418
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
18406
18419
|
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
18407
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.)
|
|
18420
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
|
|
18408
18421
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
18409
18422
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
18410
18423
|
|
|
@@ -18418,7 +18431,7 @@ var jiraSkill = {
|
|
|
18418
18431
|
**Available tools:**
|
|
18419
18432
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
18420
18433
|
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
18421
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
|
|
18434
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
|
|
18422
18435
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
18423
18436
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
18424
18437
|
|
|
@@ -18489,8 +18502,8 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
|
|
|
18489
18502
|
title: e.frontmatter.title,
|
|
18490
18503
|
status: e.frontmatter.status,
|
|
18491
18504
|
linkedFeature: normalizeLinkedFeatures(e.frontmatter.linkedFeature),
|
|
18492
|
-
targetDate: e.frontmatter.targetDate
|
|
18493
|
-
estimatedEffort: e.frontmatter.estimatedEffort
|
|
18505
|
+
targetDate: typeof e.frontmatter.targetDate === "string" ? e.frontmatter.targetDate : null,
|
|
18506
|
+
estimatedEffort: typeof e.frontmatter.estimatedEffort === "string" ? e.frontmatter.estimatedEffort : null,
|
|
18494
18507
|
content: e.content,
|
|
18495
18508
|
linkedTaskCount: tasks.filter(
|
|
18496
18509
|
(t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).includes(e.frontmatter.id)
|
|
@@ -18501,10 +18514,10 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
|
|
|
18501
18514
|
title: t.frontmatter.title,
|
|
18502
18515
|
status: t.frontmatter.status,
|
|
18503
18516
|
linkedEpic: normalizeLinkedEpics(t.frontmatter.linkedEpic),
|
|
18504
|
-
acceptanceCriteria: t.frontmatter.acceptanceCriteria
|
|
18505
|
-
technicalNotes: t.frontmatter.technicalNotes
|
|
18506
|
-
complexity: t.frontmatter.complexity
|
|
18507
|
-
estimatedPoints: t.frontmatter.estimatedPoints
|
|
18517
|
+
acceptanceCriteria: typeof t.frontmatter.acceptanceCriteria === "string" ? t.frontmatter.acceptanceCriteria : null,
|
|
18518
|
+
technicalNotes: typeof t.frontmatter.technicalNotes === "string" ? t.frontmatter.technicalNotes : null,
|
|
18519
|
+
complexity: typeof t.frontmatter.complexity === "string" ? t.frontmatter.complexity : null,
|
|
18520
|
+
estimatedPoints: typeof t.frontmatter.estimatedPoints === "number" ? t.frontmatter.estimatedPoints : null,
|
|
18508
18521
|
priority: t.frontmatter.priority ?? null
|
|
18509
18522
|
})),
|
|
18510
18523
|
decisions: allDecisions.map((d) => ({
|
|
@@ -18979,12 +18992,12 @@ function collectSkillRegistrations(skillIds, allSkills) {
|
|
|
18979
18992
|
}
|
|
18980
18993
|
return registrations;
|
|
18981
18994
|
}
|
|
18982
|
-
function getSkillTools(skillIds, allSkills, store) {
|
|
18995
|
+
function getSkillTools(skillIds, allSkills, store, projectConfig) {
|
|
18983
18996
|
const tools = [];
|
|
18984
18997
|
for (const id of skillIds) {
|
|
18985
18998
|
const skill = allSkills.get(id);
|
|
18986
18999
|
if (skill?.tools) {
|
|
18987
|
-
tools.push(...skill.tools(store));
|
|
19000
|
+
tools.push(...skill.tools(store, projectConfig));
|
|
18988
19001
|
}
|
|
18989
19002
|
}
|
|
18990
19003
|
return tools;
|
|
@@ -19129,8 +19142,223 @@ function getDiagramData(store) {
|
|
|
19129
19142
|
}
|
|
19130
19143
|
return { sprints, epics, features, statusCounts };
|
|
19131
19144
|
}
|
|
19145
|
+
function computeUrgency(dueDateStr, todayStr) {
|
|
19146
|
+
const due = new Date(dueDateStr).getTime();
|
|
19147
|
+
const today = new Date(todayStr).getTime();
|
|
19148
|
+
const diffDays = Math.floor((due - today) / 864e5);
|
|
19149
|
+
if (diffDays < 0) return "overdue";
|
|
19150
|
+
if (diffDays <= 3) return "due-3d";
|
|
19151
|
+
if (diffDays <= 7) return "due-7d";
|
|
19152
|
+
if (diffDays <= 14) return "upcoming";
|
|
19153
|
+
return "later";
|
|
19154
|
+
}
|
|
19155
|
+
var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19156
|
+
function getUpcomingData(store) {
|
|
19157
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
19158
|
+
const allDocs = store.list();
|
|
19159
|
+
const docById = /* @__PURE__ */ new Map();
|
|
19160
|
+
for (const doc of allDocs) {
|
|
19161
|
+
docById.set(doc.frontmatter.id, doc);
|
|
19162
|
+
}
|
|
19163
|
+
const actions = allDocs.filter(
|
|
19164
|
+
(d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status)
|
|
19165
|
+
);
|
|
19166
|
+
const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
|
|
19167
|
+
const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
19168
|
+
const epics = allDocs.filter((d) => d.frontmatter.type === "epic");
|
|
19169
|
+
const tasks = allDocs.filter((d) => d.frontmatter.type === "task");
|
|
19170
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
19171
|
+
for (const task of tasks) {
|
|
19172
|
+
const tags = task.frontmatter.tags ?? [];
|
|
19173
|
+
for (const tag of tags) {
|
|
19174
|
+
if (tag.startsWith("epic:")) {
|
|
19175
|
+
const epicId = tag.slice(5);
|
|
19176
|
+
if (!epicToTasks.has(epicId)) epicToTasks.set(epicId, []);
|
|
19177
|
+
epicToTasks.get(epicId).push(task);
|
|
19178
|
+
}
|
|
19179
|
+
}
|
|
19180
|
+
}
|
|
19181
|
+
function getSprintTasks(sprintDoc) {
|
|
19182
|
+
const linkedEpics = normalizeLinkedEpics(sprintDoc.frontmatter.linkedEpics);
|
|
19183
|
+
const result = [];
|
|
19184
|
+
for (const epicId of linkedEpics) {
|
|
19185
|
+
const epicTasks = epicToTasks.get(epicId) ?? [];
|
|
19186
|
+
result.push(...epicTasks);
|
|
19187
|
+
}
|
|
19188
|
+
return result;
|
|
19189
|
+
}
|
|
19190
|
+
function countRelatedTasks(actionDoc) {
|
|
19191
|
+
const actionTags = actionDoc.frontmatter.tags ?? [];
|
|
19192
|
+
const relatedTaskIds = /* @__PURE__ */ new Set();
|
|
19193
|
+
for (const tag of actionTags) {
|
|
19194
|
+
if (tag.startsWith("sprint:")) {
|
|
19195
|
+
const sprintId = tag.slice(7);
|
|
19196
|
+
const sprint = docById.get(sprintId);
|
|
19197
|
+
if (sprint) {
|
|
19198
|
+
const sprintTaskDocs = getSprintTasks(sprint);
|
|
19199
|
+
for (const t of sprintTaskDocs) relatedTaskIds.add(t.frontmatter.id);
|
|
19200
|
+
}
|
|
19201
|
+
}
|
|
19202
|
+
}
|
|
19203
|
+
return relatedTaskIds.size;
|
|
19204
|
+
}
|
|
19205
|
+
const dueSoonActions = actionsWithDue.map((d) => ({
|
|
19206
|
+
id: d.frontmatter.id,
|
|
19207
|
+
title: d.frontmatter.title,
|
|
19208
|
+
status: d.frontmatter.status,
|
|
19209
|
+
owner: d.frontmatter.owner,
|
|
19210
|
+
dueDate: d.frontmatter.dueDate,
|
|
19211
|
+
urgency: computeUrgency(d.frontmatter.dueDate, today),
|
|
19212
|
+
relatedTaskCount: countRelatedTasks(d)
|
|
19213
|
+
})).sort((a, b) => a.dueDate.localeCompare(b.dueDate));
|
|
19214
|
+
const todayMs = new Date(today).getTime();
|
|
19215
|
+
const fourteenDaysMs = 14 * 864e5;
|
|
19216
|
+
const nearSprints = sprints.filter((s) => {
|
|
19217
|
+
const endDate = s.frontmatter.endDate;
|
|
19218
|
+
if (!endDate) return false;
|
|
19219
|
+
const endMs = new Date(endDate).getTime();
|
|
19220
|
+
const diff = endMs - todayMs;
|
|
19221
|
+
return diff >= 0 && diff <= fourteenDaysMs;
|
|
19222
|
+
});
|
|
19223
|
+
const taskSprintMap = /* @__PURE__ */ new Map();
|
|
19224
|
+
for (const sprint of nearSprints) {
|
|
19225
|
+
const sprintEnd = sprint.frontmatter.endDate;
|
|
19226
|
+
const sprintTaskDocs = getSprintTasks(sprint);
|
|
19227
|
+
for (const task of sprintTaskDocs) {
|
|
19228
|
+
if (DONE_STATUSES.has(task.frontmatter.status)) continue;
|
|
19229
|
+
const existing = taskSprintMap.get(task.frontmatter.id);
|
|
19230
|
+
if (!existing || sprintEnd < existing.sprintEnd) {
|
|
19231
|
+
taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
|
|
19232
|
+
}
|
|
19233
|
+
}
|
|
19234
|
+
}
|
|
19235
|
+
const dueSoonSprintTasks = [...taskSprintMap.values()].map(({ task, sprint, sprintEnd }) => ({
|
|
19236
|
+
id: task.frontmatter.id,
|
|
19237
|
+
title: task.frontmatter.title,
|
|
19238
|
+
status: task.frontmatter.status,
|
|
19239
|
+
sprintId: sprint.frontmatter.id,
|
|
19240
|
+
sprintTitle: sprint.frontmatter.title,
|
|
19241
|
+
sprintEndDate: sprintEnd,
|
|
19242
|
+
urgency: computeUrgency(sprintEnd, today)
|
|
19243
|
+
})).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
|
|
19244
|
+
const openItems = allDocs.filter(
|
|
19245
|
+
(d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES.has(d.frontmatter.status)
|
|
19246
|
+
);
|
|
19247
|
+
const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
|
|
19248
|
+
const recentMeetings = allDocs.filter(
|
|
19249
|
+
(d) => d.frontmatter.type === "meeting" && (d.frontmatter.updated ?? d.frontmatter.created) >= fourteenDaysAgo
|
|
19250
|
+
);
|
|
19251
|
+
const crossRefCounts = /* @__PURE__ */ new Map();
|
|
19252
|
+
for (const doc of allDocs) {
|
|
19253
|
+
const content = doc.content ?? "";
|
|
19254
|
+
for (const item of openItems) {
|
|
19255
|
+
if (doc.frontmatter.id === item.frontmatter.id) continue;
|
|
19256
|
+
if (content.includes(item.frontmatter.id)) {
|
|
19257
|
+
crossRefCounts.set(
|
|
19258
|
+
item.frontmatter.id,
|
|
19259
|
+
(crossRefCounts.get(item.frontmatter.id) ?? 0) + 1
|
|
19260
|
+
);
|
|
19261
|
+
}
|
|
19262
|
+
}
|
|
19263
|
+
}
|
|
19264
|
+
const activeSprints = sprints.filter((s) => {
|
|
19265
|
+
const status = s.frontmatter.status;
|
|
19266
|
+
if (status === "active") return true;
|
|
19267
|
+
const startDate = s.frontmatter.startDate;
|
|
19268
|
+
if (!startDate) return false;
|
|
19269
|
+
const startMs = new Date(startDate).getTime();
|
|
19270
|
+
const diff = startMs - todayMs;
|
|
19271
|
+
return diff >= 0 && diff <= fourteenDaysMs;
|
|
19272
|
+
});
|
|
19273
|
+
const activeSprintIds = new Set(activeSprints.map((s) => s.frontmatter.id));
|
|
19274
|
+
const activeEpicIds = /* @__PURE__ */ new Set();
|
|
19275
|
+
for (const s of activeSprints) {
|
|
19276
|
+
for (const epicId of normalizeLinkedEpics(s.frontmatter.linkedEpics)) {
|
|
19277
|
+
activeEpicIds.add(epicId);
|
|
19278
|
+
}
|
|
19279
|
+
}
|
|
19280
|
+
const trending = openItems.map((doc) => {
|
|
19281
|
+
const signals = [];
|
|
19282
|
+
let score = 0;
|
|
19283
|
+
const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
|
|
19284
|
+
const ageDays = daysBetween(updated, today);
|
|
19285
|
+
const recencyPts = Math.max(0, Math.round(20 * (1 - ageDays / 30)));
|
|
19286
|
+
if (recencyPts > 0) {
|
|
19287
|
+
signals.push({ factor: "recency", points: recencyPts });
|
|
19288
|
+
score += recencyPts;
|
|
19289
|
+
}
|
|
19290
|
+
const tags = doc.frontmatter.tags ?? [];
|
|
19291
|
+
const linkedToActiveSprint = tags.some(
|
|
19292
|
+
(t) => t.startsWith("sprint:") && activeSprintIds.has(t.slice(7))
|
|
19293
|
+
);
|
|
19294
|
+
const linkedToActiveEpic = tags.some(
|
|
19295
|
+
(t) => t.startsWith("epic:") && activeEpicIds.has(t.slice(5))
|
|
19296
|
+
);
|
|
19297
|
+
if (linkedToActiveSprint) {
|
|
19298
|
+
signals.push({ factor: "sprint proximity", points: 25 });
|
|
19299
|
+
score += 25;
|
|
19300
|
+
} else if (linkedToActiveEpic) {
|
|
19301
|
+
signals.push({ factor: "sprint proximity", points: 15 });
|
|
19302
|
+
score += 15;
|
|
19303
|
+
}
|
|
19304
|
+
const mentionCount = recentMeetings.filter(
|
|
19305
|
+
(m) => (m.content ?? "").includes(doc.frontmatter.id)
|
|
19306
|
+
).length;
|
|
19307
|
+
if (mentionCount > 0) {
|
|
19308
|
+
const meetingPts = Math.min(15, mentionCount * 5);
|
|
19309
|
+
signals.push({ factor: "meeting mentions", points: meetingPts });
|
|
19310
|
+
score += meetingPts;
|
|
19311
|
+
}
|
|
19312
|
+
const priority = doc.frontmatter.priority?.toLowerCase();
|
|
19313
|
+
const priorityPts = priority === "critical" ? 15 : priority === "high" ? 10 : priority === "medium" ? 3 : 0;
|
|
19314
|
+
if (priorityPts > 0) {
|
|
19315
|
+
signals.push({ factor: "priority", points: priorityPts });
|
|
19316
|
+
score += priorityPts;
|
|
19317
|
+
}
|
|
19318
|
+
if (["action", "question"].includes(doc.frontmatter.type)) {
|
|
19319
|
+
const createdDays = daysBetween(doc.frontmatter.created, today);
|
|
19320
|
+
if (createdDays >= 14) {
|
|
19321
|
+
const agingPts = Math.min(10, Math.floor((createdDays - 14) / 7) * 3 + 5);
|
|
19322
|
+
signals.push({ factor: "aging", points: agingPts });
|
|
19323
|
+
score += agingPts;
|
|
19324
|
+
}
|
|
19325
|
+
}
|
|
19326
|
+
const refs = crossRefCounts.get(doc.frontmatter.id) ?? 0;
|
|
19327
|
+
if (refs > 0) {
|
|
19328
|
+
const crossRefPts = Math.min(15, refs * 5);
|
|
19329
|
+
signals.push({ factor: "cross-references", points: crossRefPts });
|
|
19330
|
+
score += crossRefPts;
|
|
19331
|
+
}
|
|
19332
|
+
return {
|
|
19333
|
+
id: doc.frontmatter.id,
|
|
19334
|
+
title: doc.frontmatter.title,
|
|
19335
|
+
type: doc.frontmatter.type,
|
|
19336
|
+
status: doc.frontmatter.status,
|
|
19337
|
+
score,
|
|
19338
|
+
signals
|
|
19339
|
+
};
|
|
19340
|
+
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
|
|
19341
|
+
return { dueSoonActions, dueSoonSprintTasks, trending };
|
|
19342
|
+
}
|
|
19132
19343
|
|
|
19133
19344
|
// src/web/templates/layout.ts
|
|
19345
|
+
function collapsibleSection(sectionId, title, content, opts) {
|
|
19346
|
+
const tag = opts?.titleTag ?? "div";
|
|
19347
|
+
const cls = opts?.titleClass ?? "section-title";
|
|
19348
|
+
const collapsed = opts?.defaultCollapsed ? " collapsed" : "";
|
|
19349
|
+
return `
|
|
19350
|
+
<div class="collapsible${collapsed}" data-section-id="${escapeHtml(sectionId)}">
|
|
19351
|
+
<${tag} class="${cls} collapsible-header" onclick="toggleSection(this)">
|
|
19352
|
+
<svg class="collapsible-chevron" viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
|
|
19353
|
+
<path d="M4.94 5.72a.75.75 0 0 1 1.06-.02L8 7.56l1.97-1.84a.75.75 0 1 1 1.02 1.1l-2.5 2.34a.75.75 0 0 1-1.02 0l-2.5-2.34a.75.75 0 0 1-.03-1.06z"/>
|
|
19354
|
+
</svg>
|
|
19355
|
+
<span>${title}</span>
|
|
19356
|
+
</${tag}>
|
|
19357
|
+
<div class="collapsible-body">
|
|
19358
|
+
${content}
|
|
19359
|
+
</div>
|
|
19360
|
+
</div>`;
|
|
19361
|
+
}
|
|
19134
19362
|
function escapeHtml(str) {
|
|
19135
19363
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
19136
19364
|
}
|
|
@@ -19252,6 +19480,7 @@ function inline(text) {
|
|
|
19252
19480
|
function layout(opts, body) {
|
|
19253
19481
|
const topItems = [
|
|
19254
19482
|
{ href: "/", label: "Overview" },
|
|
19483
|
+
{ href: "/upcoming", label: "Upcoming" },
|
|
19255
19484
|
{ href: "/timeline", label: "Timeline" },
|
|
19256
19485
|
{ href: "/board", label: "Board" },
|
|
19257
19486
|
{ href: "/gar", label: "GAR Report" },
|
|
@@ -19297,6 +19526,32 @@ function layout(opts, body) {
|
|
|
19297
19526
|
${body}
|
|
19298
19527
|
</main>
|
|
19299
19528
|
</div>
|
|
19529
|
+
<script>
|
|
19530
|
+
function toggleSection(header) {
|
|
19531
|
+
var section = header.closest('.collapsible');
|
|
19532
|
+
if (!section) return;
|
|
19533
|
+
section.classList.toggle('collapsed');
|
|
19534
|
+
var id = section.getAttribute('data-section-id');
|
|
19535
|
+
if (id) {
|
|
19536
|
+
try {
|
|
19537
|
+
var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
|
|
19538
|
+
state[id] = section.classList.contains('collapsed');
|
|
19539
|
+
localStorage.setItem('marvin-collapsed', JSON.stringify(state));
|
|
19540
|
+
} catch(e) {}
|
|
19541
|
+
}
|
|
19542
|
+
}
|
|
19543
|
+
// Restore collapsed state on load
|
|
19544
|
+
(function() {
|
|
19545
|
+
try {
|
|
19546
|
+
var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
|
|
19547
|
+
document.querySelectorAll('.collapsible[data-section-id]').forEach(function(el) {
|
|
19548
|
+
var id = el.getAttribute('data-section-id');
|
|
19549
|
+
if (state[id] === true) el.classList.add('collapsed');
|
|
19550
|
+
else if (state[id] === false) el.classList.remove('collapsed');
|
|
19551
|
+
});
|
|
19552
|
+
} catch(e) {}
|
|
19553
|
+
})();
|
|
19554
|
+
</script>
|
|
19300
19555
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
19301
19556
|
<script>mermaid.initialize({
|
|
19302
19557
|
startOnLoad: true,
|
|
@@ -20120,13 +20375,60 @@ tr:hover td {
|
|
|
20120
20375
|
white-space: nowrap;
|
|
20121
20376
|
}
|
|
20122
20377
|
|
|
20378
|
+
.gantt-grid-line {
|
|
20379
|
+
position: absolute;
|
|
20380
|
+
top: 0;
|
|
20381
|
+
bottom: 0;
|
|
20382
|
+
width: 1px;
|
|
20383
|
+
background: var(--border);
|
|
20384
|
+
opacity: 0.35;
|
|
20385
|
+
}
|
|
20386
|
+
|
|
20387
|
+
.gantt-sprint-line {
|
|
20388
|
+
position: absolute;
|
|
20389
|
+
top: 0;
|
|
20390
|
+
bottom: 0;
|
|
20391
|
+
width: 1px;
|
|
20392
|
+
background: var(--text-dim);
|
|
20393
|
+
opacity: 0.3;
|
|
20394
|
+
}
|
|
20395
|
+
|
|
20123
20396
|
.gantt-today {
|
|
20124
20397
|
position: absolute;
|
|
20125
20398
|
top: 0;
|
|
20126
20399
|
bottom: 0;
|
|
20127
|
-
width:
|
|
20400
|
+
width: 3px;
|
|
20128
20401
|
background: var(--red);
|
|
20129
|
-
opacity: 0.
|
|
20402
|
+
opacity: 0.8;
|
|
20403
|
+
border-radius: 1px;
|
|
20404
|
+
}
|
|
20405
|
+
|
|
20406
|
+
/* Sprint band in timeline */
|
|
20407
|
+
.gantt-sprint-band-row {
|
|
20408
|
+
border-bottom: 1px solid var(--border);
|
|
20409
|
+
margin-bottom: 0.25rem;
|
|
20410
|
+
}
|
|
20411
|
+
|
|
20412
|
+
.gantt-sprint-band {
|
|
20413
|
+
height: 32px;
|
|
20414
|
+
}
|
|
20415
|
+
|
|
20416
|
+
.gantt-sprint-block {
|
|
20417
|
+
position: absolute;
|
|
20418
|
+
top: 2px;
|
|
20419
|
+
bottom: 2px;
|
|
20420
|
+
background: var(--bg-hover);
|
|
20421
|
+
border: 1px solid var(--border);
|
|
20422
|
+
border-radius: 4px;
|
|
20423
|
+
font-size: 0.65rem;
|
|
20424
|
+
color: var(--text-dim);
|
|
20425
|
+
display: flex;
|
|
20426
|
+
align-items: center;
|
|
20427
|
+
justify-content: center;
|
|
20428
|
+
overflow: hidden;
|
|
20429
|
+
white-space: nowrap;
|
|
20430
|
+
text-overflow: ellipsis;
|
|
20431
|
+
padding: 0 0.4rem;
|
|
20130
20432
|
}
|
|
20131
20433
|
|
|
20132
20434
|
/* Pie chart color overrides */
|
|
@@ -20138,6 +20440,90 @@ tr:hover td {
|
|
|
20138
20440
|
fill: var(--bg) !important;
|
|
20139
20441
|
font-weight: 600;
|
|
20140
20442
|
}
|
|
20443
|
+
|
|
20444
|
+
/* Urgency row indicators */
|
|
20445
|
+
.urgency-row-overdue { border-left: 3px solid var(--red); }
|
|
20446
|
+
.urgency-row-due-3d { border-left: 3px solid var(--amber); }
|
|
20447
|
+
.urgency-row-due-7d { border-left: 3px solid #e2a308; }
|
|
20448
|
+
|
|
20449
|
+
/* Urgency badge pills */
|
|
20450
|
+
.urgency-badge-overdue { background: rgba(248, 113, 113, 0.15); color: var(--red); }
|
|
20451
|
+
.urgency-badge-due-3d { background: rgba(251, 191, 36, 0.15); color: var(--amber); }
|
|
20452
|
+
.urgency-badge-due-7d { background: rgba(226, 163, 8, 0.15); color: #e2a308; }
|
|
20453
|
+
.urgency-badge-upcoming { background: rgba(108, 140, 255, 0.15); color: var(--accent); }
|
|
20454
|
+
.urgency-badge-later { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
|
|
20455
|
+
|
|
20456
|
+
/* Trending */
|
|
20457
|
+
.trending-rank {
|
|
20458
|
+
display: inline-flex;
|
|
20459
|
+
align-items: center;
|
|
20460
|
+
justify-content: center;
|
|
20461
|
+
width: 24px;
|
|
20462
|
+
height: 24px;
|
|
20463
|
+
border-radius: 50%;
|
|
20464
|
+
background: var(--bg-hover);
|
|
20465
|
+
font-size: 0.75rem;
|
|
20466
|
+
font-weight: 600;
|
|
20467
|
+
color: var(--text-dim);
|
|
20468
|
+
}
|
|
20469
|
+
|
|
20470
|
+
.trending-score {
|
|
20471
|
+
display: inline-block;
|
|
20472
|
+
padding: 0.15rem 0.6rem;
|
|
20473
|
+
border-radius: 999px;
|
|
20474
|
+
font-size: 0.7rem;
|
|
20475
|
+
font-weight: 700;
|
|
20476
|
+
background: rgba(108, 140, 255, 0.15);
|
|
20477
|
+
color: var(--accent);
|
|
20478
|
+
}
|
|
20479
|
+
|
|
20480
|
+
.signal-tag {
|
|
20481
|
+
display: inline-block;
|
|
20482
|
+
padding: 0.1rem 0.45rem;
|
|
20483
|
+
border-radius: 4px;
|
|
20484
|
+
font-size: 0.65rem;
|
|
20485
|
+
background: var(--bg-hover);
|
|
20486
|
+
color: var(--text-dim);
|
|
20487
|
+
margin-right: 0.25rem;
|
|
20488
|
+
margin-bottom: 0.15rem;
|
|
20489
|
+
white-space: nowrap;
|
|
20490
|
+
}
|
|
20491
|
+
|
|
20492
|
+
.text-dim { color: var(--text-dim); }
|
|
20493
|
+
|
|
20494
|
+
/* Collapsible sections */
|
|
20495
|
+
.collapsible-header {
|
|
20496
|
+
cursor: pointer;
|
|
20497
|
+
display: flex;
|
|
20498
|
+
align-items: center;
|
|
20499
|
+
gap: 0.4rem;
|
|
20500
|
+
user-select: none;
|
|
20501
|
+
}
|
|
20502
|
+
|
|
20503
|
+
.collapsible-header:hover {
|
|
20504
|
+
color: var(--accent);
|
|
20505
|
+
}
|
|
20506
|
+
|
|
20507
|
+
.collapsible-chevron {
|
|
20508
|
+
transition: transform 0.2s ease;
|
|
20509
|
+
flex-shrink: 0;
|
|
20510
|
+
}
|
|
20511
|
+
|
|
20512
|
+
.collapsible.collapsed .collapsible-chevron {
|
|
20513
|
+
transform: rotate(-90deg);
|
|
20514
|
+
}
|
|
20515
|
+
|
|
20516
|
+
.collapsible-body {
|
|
20517
|
+
overflow: hidden;
|
|
20518
|
+
max-height: 5000px;
|
|
20519
|
+
transition: max-height 0.3s ease, opacity 0.2s ease;
|
|
20520
|
+
opacity: 1;
|
|
20521
|
+
}
|
|
20522
|
+
|
|
20523
|
+
.collapsible.collapsed .collapsible-body {
|
|
20524
|
+
max-height: 0;
|
|
20525
|
+
opacity: 0;
|
|
20526
|
+
}
|
|
20141
20527
|
`;
|
|
20142
20528
|
}
|
|
20143
20529
|
|
|
@@ -20190,35 +20576,73 @@ function buildTimelineGantt(data, maxSprints = 6) {
|
|
|
20190
20576
|
);
|
|
20191
20577
|
tick += 7 * DAY;
|
|
20192
20578
|
}
|
|
20579
|
+
const gridLines = [];
|
|
20580
|
+
let gridTick = timelineStart;
|
|
20581
|
+
const gridStartDay = new Date(gridTick).getDay();
|
|
20582
|
+
gridTick += (8 - gridStartDay) % 7 * DAY;
|
|
20583
|
+
while (gridTick <= timelineEnd) {
|
|
20584
|
+
gridLines.push(`<div class="gantt-grid-line" style="left:${pct(gridTick).toFixed(2)}%"></div>`);
|
|
20585
|
+
gridTick += 7 * DAY;
|
|
20586
|
+
}
|
|
20587
|
+
const sprintBoundaries = /* @__PURE__ */ new Set();
|
|
20588
|
+
for (const sprint of visibleSprints) {
|
|
20589
|
+
sprintBoundaries.add(toMs(sprint.startDate));
|
|
20590
|
+
sprintBoundaries.add(toMs(sprint.endDate));
|
|
20591
|
+
}
|
|
20592
|
+
const sprintLines = [...sprintBoundaries].map(
|
|
20593
|
+
(ms) => `<div class="gantt-sprint-line" style="left:${pct(ms).toFixed(2)}%"></div>`
|
|
20594
|
+
);
|
|
20193
20595
|
const now = Date.now();
|
|
20194
20596
|
let todayMarker = "";
|
|
20195
20597
|
if (now >= timelineStart && now <= timelineEnd) {
|
|
20196
20598
|
todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
|
|
20197
20599
|
}
|
|
20198
|
-
const
|
|
20600
|
+
const sprintBlocks = visibleSprints.map((sprint) => {
|
|
20601
|
+
const sStart = toMs(sprint.startDate);
|
|
20602
|
+
const sEnd = toMs(sprint.endDate);
|
|
20603
|
+
const left = pct(sStart).toFixed(2);
|
|
20604
|
+
const width = (pct(sEnd) - pct(sStart)).toFixed(2);
|
|
20605
|
+
return `<div class="gantt-sprint-block" style="left:${left}%;width:${width}%">${sanitize(sprint.id, 20)}</div>`;
|
|
20606
|
+
}).join("");
|
|
20607
|
+
const sprintBandRow = `<div class="gantt-row gantt-sprint-band-row">
|
|
20608
|
+
<div class="gantt-label gantt-section-label">Sprints</div>
|
|
20609
|
+
<div class="gantt-track gantt-sprint-band">${sprintBlocks}</div>
|
|
20610
|
+
</div>`;
|
|
20611
|
+
const epicSpanMap = /* @__PURE__ */ new Map();
|
|
20199
20612
|
for (const sprint of visibleSprints) {
|
|
20200
20613
|
const sStart = toMs(sprint.startDate);
|
|
20201
20614
|
const sEnd = toMs(sprint.endDate);
|
|
20202
|
-
|
|
20203
|
-
|
|
20204
|
-
|
|
20205
|
-
|
|
20206
|
-
|
|
20207
|
-
|
|
20208
|
-
|
|
20209
|
-
|
|
20210
|
-
|
|
20211
|
-
|
|
20212
|
-
|
|
20213
|
-
|
|
20214
|
-
|
|
20215
|
-
|
|
20615
|
+
for (const eid of sprint.linkedEpics) {
|
|
20616
|
+
if (!epicMap.has(eid)) continue;
|
|
20617
|
+
const existing = epicSpanMap.get(eid);
|
|
20618
|
+
if (existing) {
|
|
20619
|
+
existing.startMs = Math.min(existing.startMs, sStart);
|
|
20620
|
+
existing.endMs = Math.max(existing.endMs, sEnd);
|
|
20621
|
+
} else {
|
|
20622
|
+
epicSpanMap.set(eid, { startMs: sStart, endMs: sEnd });
|
|
20623
|
+
}
|
|
20624
|
+
}
|
|
20625
|
+
}
|
|
20626
|
+
const sortedEpicIds = [...epicSpanMap.keys()].sort((a, b) => {
|
|
20627
|
+
const aSpan = epicSpanMap.get(a);
|
|
20628
|
+
const bSpan = epicSpanMap.get(b);
|
|
20629
|
+
if (aSpan.startMs !== bSpan.startMs) return aSpan.startMs - bSpan.startMs;
|
|
20630
|
+
return a.localeCompare(b);
|
|
20631
|
+
});
|
|
20632
|
+
const epicRows = sortedEpicIds.map((eid) => {
|
|
20633
|
+
const epic = epicMap.get(eid);
|
|
20634
|
+
const { startMs, endMs } = epicSpanMap.get(eid);
|
|
20635
|
+
const cls = epic.status === "done" || epic.status === "completed" ? "gantt-bar-done" : epic.status === "in-progress" || epic.status === "active" ? "gantt-bar-active" : epic.status === "blocked" ? "gantt-bar-blocked" : "gantt-bar-default";
|
|
20636
|
+
const left = pct(startMs).toFixed(2);
|
|
20637
|
+
const width = (pct(endMs) - pct(startMs)).toFixed(2);
|
|
20638
|
+
const label = sanitize(epic.id + " " + epic.title);
|
|
20639
|
+
return `<div class="gantt-row">
|
|
20640
|
+
<div class="gantt-label">${label}</div>
|
|
20216
20641
|
<div class="gantt-track">
|
|
20217
20642
|
<div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
|
|
20218
20643
|
</div>
|
|
20219
|
-
</div
|
|
20220
|
-
|
|
20221
|
-
}
|
|
20644
|
+
</div>`;
|
|
20645
|
+
}).join("\n");
|
|
20222
20646
|
const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
|
|
20223
20647
|
return `${note}
|
|
20224
20648
|
<div class="gantt">
|
|
@@ -20227,11 +20651,12 @@ function buildTimelineGantt(data, maxSprints = 6) {
|
|
|
20227
20651
|
<div class="gantt-label"></div>
|
|
20228
20652
|
<div class="gantt-track gantt-dates">${markers.join("")}</div>
|
|
20229
20653
|
</div>
|
|
20230
|
-
${
|
|
20654
|
+
${sprintBandRow}
|
|
20655
|
+
${epicRows}
|
|
20231
20656
|
</div>
|
|
20232
20657
|
<div class="gantt-overlay">
|
|
20233
20658
|
<div class="gantt-label"></div>
|
|
20234
|
-
<div class="gantt-track">${todayMarker}</div>
|
|
20659
|
+
<div class="gantt-track">${gridLines.join("")}${sprintLines.join("")}${todayMarker}</div>
|
|
20235
20660
|
</div>
|
|
20236
20661
|
</div>`;
|
|
20237
20662
|
}
|
|
@@ -20310,13 +20735,14 @@ function buildArtifactFlowchart(data) {
|
|
|
20310
20735
|
var svg = document.getElementById('flow-lines');
|
|
20311
20736
|
if (!container || !svg) return;
|
|
20312
20737
|
|
|
20313
|
-
// Build adjacency
|
|
20314
|
-
var
|
|
20738
|
+
// Build directed adjacency maps for traversal
|
|
20739
|
+
var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
|
|
20740
|
+
var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
|
|
20315
20741
|
edges.forEach(function(e) {
|
|
20316
|
-
if (!
|
|
20317
|
-
if (!
|
|
20318
|
-
|
|
20319
|
-
|
|
20742
|
+
if (!fwd[e.from]) fwd[e.from] = [];
|
|
20743
|
+
if (!bwd[e.to]) bwd[e.to] = [];
|
|
20744
|
+
fwd[e.from].push(e.to);
|
|
20745
|
+
bwd[e.to].push(e.from);
|
|
20320
20746
|
});
|
|
20321
20747
|
|
|
20322
20748
|
function drawLines() {
|
|
@@ -20349,14 +20775,28 @@ function buildArtifactFlowchart(data) {
|
|
|
20349
20775
|
});
|
|
20350
20776
|
}
|
|
20351
20777
|
|
|
20352
|
-
// Find
|
|
20778
|
+
// Find directly related nodes via directed traversal
|
|
20779
|
+
// Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
|
|
20780
|
+
// (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
|
|
20353
20781
|
function findConnected(startId) {
|
|
20354
20782
|
var visited = {};
|
|
20355
|
-
var queue = [startId];
|
|
20356
20783
|
visited[startId] = true;
|
|
20784
|
+
// Traverse forward (from\u2192to direction)
|
|
20785
|
+
var queue = [startId];
|
|
20786
|
+
while (queue.length) {
|
|
20787
|
+
var id = queue.shift();
|
|
20788
|
+
(fwd[id] || []).forEach(function(neighbor) {
|
|
20789
|
+
if (!visited[neighbor]) {
|
|
20790
|
+
visited[neighbor] = true;
|
|
20791
|
+
queue.push(neighbor);
|
|
20792
|
+
}
|
|
20793
|
+
});
|
|
20794
|
+
}
|
|
20795
|
+
// Traverse backward (to\u2192from direction)
|
|
20796
|
+
queue = [startId];
|
|
20357
20797
|
while (queue.length) {
|
|
20358
20798
|
var id = queue.shift();
|
|
20359
|
-
(
|
|
20799
|
+
(bwd[id] || []).forEach(function(neighbor) {
|
|
20360
20800
|
if (!visited[neighbor]) {
|
|
20361
20801
|
visited[neighbor] = true;
|
|
20362
20802
|
queue.push(neighbor);
|
|
@@ -20496,11 +20936,12 @@ function overviewPage(data, diagrams, navGroups) {
|
|
|
20496
20936
|
|
|
20497
20937
|
<div class="section-title"><a href="/timeline">Project Timeline →</a></div>
|
|
20498
20938
|
|
|
20499
|
-
|
|
20500
|
-
${buildArtifactFlowchart(diagrams)}
|
|
20939
|
+
${collapsibleSection("overview-relationships", "Artifact Relationships", buildArtifactFlowchart(diagrams))}
|
|
20501
20940
|
|
|
20502
|
-
|
|
20503
|
-
|
|
20941
|
+
${collapsibleSection(
|
|
20942
|
+
"overview-recent",
|
|
20943
|
+
"Recent Activity",
|
|
20944
|
+
data.recent.length > 0 ? `
|
|
20504
20945
|
<div class="table-wrap">
|
|
20505
20946
|
<table>
|
|
20506
20947
|
<thead>
|
|
@@ -20516,7 +20957,8 @@ function overviewPage(data, diagrams, navGroups) {
|
|
|
20516
20957
|
${rows}
|
|
20517
20958
|
</tbody>
|
|
20518
20959
|
</table>
|
|
20519
|
-
</div>` : `<div class="empty"><p>No documents yet.</p></div>`
|
|
20960
|
+
</div>` : `<div class="empty"><p>No documents yet.</p></div>`
|
|
20961
|
+
)}
|
|
20520
20962
|
`;
|
|
20521
20963
|
}
|
|
20522
20964
|
|
|
@@ -20661,23 +21103,24 @@ function garPage(report) {
|
|
|
20661
21103
|
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
20662
21104
|
</div>
|
|
20663
21105
|
|
|
20664
|
-
|
|
20665
|
-
${areaCards}
|
|
20666
|
-
</div>
|
|
21106
|
+
${collapsibleSection("gar-areas", "Areas", `<div class="gar-areas">${areaCards}</div>`)}
|
|
20667
21107
|
|
|
20668
|
-
|
|
20669
|
-
|
|
20670
|
-
|
|
20671
|
-
|
|
20672
|
-
|
|
20673
|
-
|
|
21108
|
+
${collapsibleSection(
|
|
21109
|
+
"gar-status-dist",
|
|
21110
|
+
"Status Distribution",
|
|
21111
|
+
buildStatusPie("Action Status", {
|
|
21112
|
+
Open: report.metrics.scope.open,
|
|
21113
|
+
Done: report.metrics.scope.done,
|
|
21114
|
+
"In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
|
|
21115
|
+
})
|
|
21116
|
+
)}
|
|
20674
21117
|
`;
|
|
20675
21118
|
}
|
|
20676
21119
|
|
|
20677
21120
|
// src/web/templates/pages/health.ts
|
|
20678
21121
|
function healthPage(report, metrics) {
|
|
20679
21122
|
const dotClass = `dot-${report.overall}`;
|
|
20680
|
-
function renderSection(title, categories) {
|
|
21123
|
+
function renderSection(sectionId, title, categories) {
|
|
20681
21124
|
const cards = categories.map(
|
|
20682
21125
|
(cat) => `
|
|
20683
21126
|
<div class="gar-area">
|
|
@@ -20689,10 +21132,9 @@ function healthPage(report, metrics) {
|
|
|
20689
21132
|
${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
|
|
20690
21133
|
</div>`
|
|
20691
21134
|
).join("\n");
|
|
20692
|
-
return
|
|
20693
|
-
|
|
20694
|
-
|
|
20695
|
-
`;
|
|
21135
|
+
return collapsibleSection(sectionId, title, `<div class="gar-areas">${cards}</div>`, {
|
|
21136
|
+
titleClass: "health-section-title"
|
|
21137
|
+
});
|
|
20696
21138
|
}
|
|
20697
21139
|
return `
|
|
20698
21140
|
<div class="page-header">
|
|
@@ -20705,35 +21147,43 @@ function healthPage(report, metrics) {
|
|
|
20705
21147
|
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
20706
21148
|
</div>
|
|
20707
21149
|
|
|
20708
|
-
${renderSection("Completeness", report.completeness)}
|
|
20709
|
-
|
|
20710
|
-
|
|
20711
|
-
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
|
|
20715
|
-
|
|
20716
|
-
|
|
20717
|
-
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
|
|
20721
|
-
|
|
20722
|
-
|
|
20723
|
-
|
|
21150
|
+
${renderSection("health-completeness", "Completeness", report.completeness)}
|
|
21151
|
+
|
|
21152
|
+
${collapsibleSection(
|
|
21153
|
+
"health-completeness-overview",
|
|
21154
|
+
"Completeness Overview",
|
|
21155
|
+
buildHealthGauge(
|
|
21156
|
+
metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
|
|
21157
|
+
name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
21158
|
+
complete: cat.complete,
|
|
21159
|
+
total: cat.total
|
|
21160
|
+
})) : report.completeness.map((c) => {
|
|
21161
|
+
const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
|
|
21162
|
+
return {
|
|
21163
|
+
name: c.name,
|
|
21164
|
+
complete: match ? parseInt(match[1], 10) : 0,
|
|
21165
|
+
total: match ? parseInt(match[2], 10) : 0
|
|
21166
|
+
};
|
|
21167
|
+
})
|
|
21168
|
+
),
|
|
21169
|
+
{ titleClass: "health-section-title" }
|
|
20724
21170
|
)}
|
|
20725
21171
|
|
|
20726
|
-
${renderSection("Process", report.process)}
|
|
20727
|
-
|
|
20728
|
-
|
|
20729
|
-
|
|
20730
|
-
|
|
20731
|
-
"
|
|
20732
|
-
|
|
20733
|
-
|
|
20734
|
-
|
|
20735
|
-
|
|
20736
|
-
|
|
21172
|
+
${renderSection("health-process", "Process", report.process)}
|
|
21173
|
+
|
|
21174
|
+
${collapsibleSection(
|
|
21175
|
+
"health-process-summary",
|
|
21176
|
+
"Process Summary",
|
|
21177
|
+
metrics ? buildStatusPie("Process Health", {
|
|
21178
|
+
Stale: metrics.process.stale.length,
|
|
21179
|
+
"Aging Actions": metrics.process.agingActions.length,
|
|
21180
|
+
Healthy: Math.max(
|
|
21181
|
+
0,
|
|
21182
|
+
(metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
|
|
21183
|
+
)
|
|
21184
|
+
}) : "",
|
|
21185
|
+
{ titleClass: "health-section-title" }
|
|
21186
|
+
)}
|
|
20737
21187
|
`;
|
|
20738
21188
|
}
|
|
20739
21189
|
|
|
@@ -20791,13 +21241,147 @@ function timelinePage(diagrams) {
|
|
|
20791
21241
|
return `
|
|
20792
21242
|
<div class="page-header">
|
|
20793
21243
|
<h2>Project Timeline</h2>
|
|
20794
|
-
<div class="subtitle">
|
|
21244
|
+
<div class="subtitle">Epic timeline across sprints</div>
|
|
20795
21245
|
</div>
|
|
20796
21246
|
|
|
20797
21247
|
${buildTimelineGantt(diagrams)}
|
|
20798
21248
|
`;
|
|
20799
21249
|
}
|
|
20800
21250
|
|
|
21251
|
+
// src/web/templates/pages/upcoming.ts
|
|
21252
|
+
function urgencyBadge(tier) {
|
|
21253
|
+
const labels = {
|
|
21254
|
+
overdue: "Overdue",
|
|
21255
|
+
"due-3d": "Due in 3d",
|
|
21256
|
+
"due-7d": "Due in 7d",
|
|
21257
|
+
upcoming: "Upcoming",
|
|
21258
|
+
later: "Later"
|
|
21259
|
+
};
|
|
21260
|
+
return `<span class="badge urgency-badge-${tier}">${labels[tier]}</span>`;
|
|
21261
|
+
}
|
|
21262
|
+
function urgencyRowClass(tier) {
|
|
21263
|
+
if (tier === "overdue") return " urgency-row-overdue";
|
|
21264
|
+
if (tier === "due-3d") return " urgency-row-due-3d";
|
|
21265
|
+
if (tier === "due-7d") return " urgency-row-due-7d";
|
|
21266
|
+
return "";
|
|
21267
|
+
}
|
|
21268
|
+
function upcomingPage(data) {
|
|
21269
|
+
const hasActions = data.dueSoonActions.length > 0;
|
|
21270
|
+
const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
|
|
21271
|
+
const hasTrending = data.trending.length > 0;
|
|
21272
|
+
const actionsTable = hasActions ? collapsibleSection(
|
|
21273
|
+
"upcoming-actions",
|
|
21274
|
+
"Due Soon \u2014 Actions",
|
|
21275
|
+
`<div class="table-wrap">
|
|
21276
|
+
<table>
|
|
21277
|
+
<thead>
|
|
21278
|
+
<tr>
|
|
21279
|
+
<th>ID</th>
|
|
21280
|
+
<th>Title</th>
|
|
21281
|
+
<th>Status</th>
|
|
21282
|
+
<th>Owner</th>
|
|
21283
|
+
<th>Due Date</th>
|
|
21284
|
+
<th>Urgency</th>
|
|
21285
|
+
<th>Tasks</th>
|
|
21286
|
+
</tr>
|
|
21287
|
+
</thead>
|
|
21288
|
+
<tbody>
|
|
21289
|
+
${data.dueSoonActions.map(
|
|
21290
|
+
(a) => `
|
|
21291
|
+
<tr class="${urgencyRowClass(a.urgency)}">
|
|
21292
|
+
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
21293
|
+
<td>${escapeHtml(a.title)}</td>
|
|
21294
|
+
<td>${statusBadge(a.status)}</td>
|
|
21295
|
+
<td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
21296
|
+
<td>${formatDate(a.dueDate)}</td>
|
|
21297
|
+
<td>${urgencyBadge(a.urgency)}</td>
|
|
21298
|
+
<td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
|
|
21299
|
+
</tr>`
|
|
21300
|
+
).join("")}
|
|
21301
|
+
</tbody>
|
|
21302
|
+
</table>
|
|
21303
|
+
</div>`,
|
|
21304
|
+
{ titleTag: "h3" }
|
|
21305
|
+
) : "";
|
|
21306
|
+
const sprintTasksTable = hasSprintTasks ? collapsibleSection(
|
|
21307
|
+
"upcoming-sprint-tasks",
|
|
21308
|
+
"Due Soon \u2014 Sprint Tasks",
|
|
21309
|
+
`<div class="table-wrap">
|
|
21310
|
+
<table>
|
|
21311
|
+
<thead>
|
|
21312
|
+
<tr>
|
|
21313
|
+
<th>ID</th>
|
|
21314
|
+
<th>Title</th>
|
|
21315
|
+
<th>Status</th>
|
|
21316
|
+
<th>Sprint</th>
|
|
21317
|
+
<th>Sprint Ends</th>
|
|
21318
|
+
<th>Urgency</th>
|
|
21319
|
+
</tr>
|
|
21320
|
+
</thead>
|
|
21321
|
+
<tbody>
|
|
21322
|
+
${data.dueSoonSprintTasks.map(
|
|
21323
|
+
(t) => `
|
|
21324
|
+
<tr class="${urgencyRowClass(t.urgency)}">
|
|
21325
|
+
<td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
21326
|
+
<td>${escapeHtml(t.title)}</td>
|
|
21327
|
+
<td>${statusBadge(t.status)}</td>
|
|
21328
|
+
<td><a href="/docs/sprint/${escapeHtml(t.sprintId)}">${escapeHtml(t.sprintId)}</a></td>
|
|
21329
|
+
<td>${formatDate(t.sprintEndDate)}</td>
|
|
21330
|
+
<td>${urgencyBadge(t.urgency)}</td>
|
|
21331
|
+
</tr>`
|
|
21332
|
+
).join("")}
|
|
21333
|
+
</tbody>
|
|
21334
|
+
</table>
|
|
21335
|
+
</div>`,
|
|
21336
|
+
{ titleTag: "h3" }
|
|
21337
|
+
) : "";
|
|
21338
|
+
const trendingTable = hasTrending ? collapsibleSection(
|
|
21339
|
+
"upcoming-trending",
|
|
21340
|
+
"Trending",
|
|
21341
|
+
`<div class="table-wrap">
|
|
21342
|
+
<table>
|
|
21343
|
+
<thead>
|
|
21344
|
+
<tr>
|
|
21345
|
+
<th>#</th>
|
|
21346
|
+
<th>ID</th>
|
|
21347
|
+
<th>Title</th>
|
|
21348
|
+
<th>Type</th>
|
|
21349
|
+
<th>Status</th>
|
|
21350
|
+
<th>Score</th>
|
|
21351
|
+
<th>Signals</th>
|
|
21352
|
+
</tr>
|
|
21353
|
+
</thead>
|
|
21354
|
+
<tbody>
|
|
21355
|
+
${data.trending.map(
|
|
21356
|
+
(t, i) => `
|
|
21357
|
+
<tr>
|
|
21358
|
+
<td><span class="trending-rank">${i + 1}</span></td>
|
|
21359
|
+
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
21360
|
+
<td>${escapeHtml(t.title)}</td>
|
|
21361
|
+
<td>${escapeHtml(typeLabel(t.type))}</td>
|
|
21362
|
+
<td>${statusBadge(t.status)}</td>
|
|
21363
|
+
<td><span class="trending-score">${t.score}</span></td>
|
|
21364
|
+
<td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
|
|
21365
|
+
</tr>`
|
|
21366
|
+
).join("")}
|
|
21367
|
+
</tbody>
|
|
21368
|
+
</table>
|
|
21369
|
+
</div>`,
|
|
21370
|
+
{ titleTag: "h3" }
|
|
21371
|
+
) : "";
|
|
21372
|
+
const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
|
|
21373
|
+
return `
|
|
21374
|
+
<div class="page-header">
|
|
21375
|
+
<h2>Upcoming</h2>
|
|
21376
|
+
<div class="subtitle">Time-sensitive items and trending activity</div>
|
|
21377
|
+
</div>
|
|
21378
|
+
${actionsTable}
|
|
21379
|
+
${sprintTasksTable}
|
|
21380
|
+
${trendingTable}
|
|
21381
|
+
${emptyState}
|
|
21382
|
+
`;
|
|
21383
|
+
}
|
|
21384
|
+
|
|
20801
21385
|
// src/web/router.ts
|
|
20802
21386
|
function handleRequest(req, res, store, projectName, navGroups) {
|
|
20803
21387
|
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
@@ -20838,6 +21422,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
20838
21422
|
respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
|
|
20839
21423
|
return;
|
|
20840
21424
|
}
|
|
21425
|
+
if (pathname === "/upcoming") {
|
|
21426
|
+
const data = getUpcomingData(store);
|
|
21427
|
+
const body = upcomingPage(data);
|
|
21428
|
+
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
|
|
21429
|
+
return;
|
|
21430
|
+
}
|
|
20841
21431
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
20842
21432
|
if (boardMatch) {
|
|
20843
21433
|
const type = boardMatch[1];
|
|
@@ -20995,6 +21585,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
20995
21585
|
const base = `http://localhost:${runningServer.port}`;
|
|
20996
21586
|
const urls = {
|
|
20997
21587
|
overview: base,
|
|
21588
|
+
upcoming: `${base}/upcoming`,
|
|
20998
21589
|
gar: `${base}/gar`,
|
|
20999
21590
|
board: `${base}/board`
|
|
21000
21591
|
};
|
|
@@ -21068,6 +21659,18 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
21068
21659
|
};
|
|
21069
21660
|
},
|
|
21070
21661
|
{ annotations: { readOnlyHint: true } }
|
|
21662
|
+
),
|
|
21663
|
+
tool22(
|
|
21664
|
+
"get_dashboard_upcoming",
|
|
21665
|
+
"Get upcoming data: due-soon actions and sprint tasks, plus trending items scored by relevance signals. Works without the web server running.",
|
|
21666
|
+
{},
|
|
21667
|
+
async () => {
|
|
21668
|
+
const data = getUpcomingData(store);
|
|
21669
|
+
return {
|
|
21670
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
21671
|
+
};
|
|
21672
|
+
},
|
|
21673
|
+
{ annotations: { readOnlyHint: true } }
|
|
21071
21674
|
)
|
|
21072
21675
|
];
|
|
21073
21676
|
}
|
|
@@ -21527,7 +22130,7 @@ function collectTools(marvinDir) {
|
|
|
21527
22130
|
const sessionStore = new SessionStore(marvinDir);
|
|
21528
22131
|
const allSkills = loadAllSkills(marvinDir);
|
|
21529
22132
|
const allSkillIds = [...allSkills.keys()];
|
|
21530
|
-
const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
|
|
22133
|
+
const codeSkillTools = getSkillTools(allSkillIds, allSkills, store, config2);
|
|
21531
22134
|
const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
|
|
21532
22135
|
const projectRoot = path9.dirname(marvinDir);
|
|
21533
22136
|
const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
|