@harness-engineering/dashboard 0.2.2 → 0.4.0
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/README.md +2 -2
- package/dist/client/assets/index-BoQuKl2t.css +1 -0
- package/dist/client/assets/index-CQH-feA_.js +393 -0
- package/dist/client/index.html +2 -2
- package/dist/server/{-WJBYKRAQ.js → -HPCOJXLL.js} +227 -12
- package/dist/server/serve.js +1 -1
- package/package.json +4 -3
- package/dist/client/assets/index-C1zvTZwx.js +0 -347
- package/dist/client/assets/index-DVHWQnep.css +0 -1
package/dist/client/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Harness Dashboard</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CQH-feA_.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BoQuKl2t.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body class="bg-gray-950">
|
|
11
11
|
<div id="root"></div>
|
|
@@ -40,15 +40,25 @@ async function gatherRoadmap(roadmapPath) {
|
|
|
40
40
|
const allFeatures = projectFeatures(roadmap.milestones);
|
|
41
41
|
const milestones = buildMilestoneProgress(roadmap.milestones);
|
|
42
42
|
const totals = countByStatus(allFeatures.map((f) => f.status));
|
|
43
|
+
const assignmentHistory = (roadmap.assignmentHistory ?? []).map(
|
|
44
|
+
(r) => ({
|
|
45
|
+
feature: r.feature,
|
|
46
|
+
assignee: r.assignee,
|
|
47
|
+
action: r.action,
|
|
48
|
+
date: r.date
|
|
49
|
+
})
|
|
50
|
+
);
|
|
43
51
|
return {
|
|
44
52
|
milestones,
|
|
45
53
|
features: allFeatures,
|
|
54
|
+
assignmentHistory,
|
|
46
55
|
totalFeatures: allFeatures.length,
|
|
47
56
|
totalDone: totals.done,
|
|
48
57
|
totalInProgress: totals.inProgress,
|
|
49
58
|
totalPlanned: totals.planned,
|
|
50
59
|
totalBlocked: totals.blocked,
|
|
51
|
-
totalBacklog: totals.backlog
|
|
60
|
+
totalBacklog: totals.backlog,
|
|
61
|
+
totalNeedsHuman: totals.needsHuman
|
|
52
62
|
};
|
|
53
63
|
} catch (err) {
|
|
54
64
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -64,7 +74,11 @@ function projectFeatures(milestones) {
|
|
|
64
74
|
milestone: m.name,
|
|
65
75
|
blockedBy: f.blockedBy,
|
|
66
76
|
assignee: f.assignee ?? null,
|
|
67
|
-
priority: f.priority ?? null
|
|
77
|
+
priority: f.priority ?? null,
|
|
78
|
+
spec: f.spec ?? null,
|
|
79
|
+
plans: f.plans ?? [],
|
|
80
|
+
externalId: f.externalId ?? null,
|
|
81
|
+
updatedAt: f.updatedAt ?? null
|
|
68
82
|
}))
|
|
69
83
|
);
|
|
70
84
|
}
|
|
@@ -82,6 +96,7 @@ function countByStatus(statuses) {
|
|
|
82
96
|
let planned = 0;
|
|
83
97
|
let blocked = 0;
|
|
84
98
|
let backlog = 0;
|
|
99
|
+
let needsHuman = 0;
|
|
85
100
|
for (const s of statuses) {
|
|
86
101
|
switch (s) {
|
|
87
102
|
case "done":
|
|
@@ -99,9 +114,12 @@ function countByStatus(statuses) {
|
|
|
99
114
|
case "backlog":
|
|
100
115
|
backlog++;
|
|
101
116
|
break;
|
|
117
|
+
case "needs-human":
|
|
118
|
+
needsHuman++;
|
|
119
|
+
break;
|
|
102
120
|
}
|
|
103
121
|
}
|
|
104
|
-
return { done, inProgress, planned, blocked, backlog };
|
|
122
|
+
return { done, inProgress, planned, blocked, backlog, needsHuman };
|
|
105
123
|
}
|
|
106
124
|
|
|
107
125
|
// src/server/gather/health.ts
|
|
@@ -176,7 +194,7 @@ async function gatherGraph(projectPath) {
|
|
|
176
194
|
if (!loaded) {
|
|
177
195
|
return {
|
|
178
196
|
available: false,
|
|
179
|
-
reason: 'Graph data not found. Run "harness
|
|
197
|
+
reason: 'Graph data not found. Run "harness scan" to build the knowledge graph.'
|
|
180
198
|
};
|
|
181
199
|
}
|
|
182
200
|
const nodesByType = [];
|
|
@@ -282,14 +300,14 @@ function buildRoadmapRouter(ctx) {
|
|
|
282
300
|
import { Hono as Hono4 } from "hono";
|
|
283
301
|
var CACHE_KEY3 = "health";
|
|
284
302
|
function buildChecksData(ctx) {
|
|
285
|
-
const
|
|
303
|
+
const pending2 = "Checks have not completed yet";
|
|
286
304
|
return {
|
|
287
|
-
security: ctx.gatherCache.get("security") ?? { error:
|
|
288
|
-
perf: ctx.gatherCache.get("perf") ?? { error:
|
|
289
|
-
arch: ctx.gatherCache.get("arch") ?? { error:
|
|
305
|
+
security: ctx.gatherCache.get("security") ?? { error: pending2 },
|
|
306
|
+
perf: ctx.gatherCache.get("perf") ?? { error: pending2 },
|
|
307
|
+
arch: ctx.gatherCache.get("arch") ?? { error: pending2 },
|
|
290
308
|
anomalies: ctx.gatherCache.get("anomalies") ?? {
|
|
291
309
|
available: false,
|
|
292
|
-
reason:
|
|
310
|
+
reason: pending2
|
|
293
311
|
},
|
|
294
312
|
lastRun: (/* @__PURE__ */ new Date()).toISOString()
|
|
295
313
|
};
|
|
@@ -406,6 +424,65 @@ function buildAdoptionRouter(ctx) {
|
|
|
406
424
|
import { Hono as Hono8 } from "hono";
|
|
407
425
|
import { spawn } from "child_process";
|
|
408
426
|
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
427
|
+
import { parseRoadmap as parseRoadmap2, serializeRoadmap } from "@harness-engineering/core";
|
|
428
|
+
|
|
429
|
+
// src/server/identity.ts
|
|
430
|
+
import { execFile } from "child_process";
|
|
431
|
+
var pending = null;
|
|
432
|
+
function execAsync(cmd, args) {
|
|
433
|
+
return new Promise((resolve, reject) => {
|
|
434
|
+
execFile(cmd, args, { timeout: 5e3 }, (err, stdout) => {
|
|
435
|
+
if (err) {
|
|
436
|
+
reject(err);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
resolve(stdout.trim());
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async function resolveFromGithubApi() {
|
|
444
|
+
const token = process.env["GITHUB_TOKEN"];
|
|
445
|
+
if (!token) return null;
|
|
446
|
+
try {
|
|
447
|
+
const res = await fetch("https://api.github.com/user", {
|
|
448
|
+
headers: {
|
|
449
|
+
Authorization: `Bearer ${token}`,
|
|
450
|
+
Accept: "application/vnd.github+json",
|
|
451
|
+
"User-Agent": "harness-dashboard"
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
if (!res.ok) return null;
|
|
455
|
+
const data = await res.json();
|
|
456
|
+
if (data.login) return { username: data.login, source: "github-api" };
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
async function resolveFromGhCli() {
|
|
462
|
+
try {
|
|
463
|
+
const login = await execAsync("gh", ["api", "user", "--jq", ".login"]);
|
|
464
|
+
if (login) return { username: login, source: "gh-cli" };
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
async function resolveFromGitConfig() {
|
|
470
|
+
try {
|
|
471
|
+
const name = await execAsync("git", ["config", "user.name"]);
|
|
472
|
+
if (name) return { username: name, source: "git-config" };
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
async function resolveInner() {
|
|
478
|
+
return await resolveFromGithubApi() ?? await resolveFromGhCli() ?? await resolveFromGitConfig();
|
|
479
|
+
}
|
|
480
|
+
async function resolveIdentity() {
|
|
481
|
+
if (!pending) {
|
|
482
|
+
pending = resolveInner();
|
|
483
|
+
}
|
|
484
|
+
return pending;
|
|
485
|
+
}
|
|
409
486
|
|
|
410
487
|
// src/server/gather/security.ts
|
|
411
488
|
import { SecurityScanner } from "@harness-engineering/core";
|
|
@@ -552,7 +629,7 @@ async function gatherAnomalies(projectPath) {
|
|
|
552
629
|
if (!loaded) {
|
|
553
630
|
return {
|
|
554
631
|
available: false,
|
|
555
|
-
reason: 'Graph data not found. Run "harness
|
|
632
|
+
reason: 'Graph data not found. Run "harness scan" to build the knowledge graph.'
|
|
556
633
|
};
|
|
557
634
|
}
|
|
558
635
|
const adapter = new GraphAnomalyAdapter(store);
|
|
@@ -597,7 +674,14 @@ async function withFileLock(path, fn) {
|
|
|
597
674
|
}
|
|
598
675
|
}
|
|
599
676
|
}
|
|
600
|
-
var VALID_STATUSES = /* @__PURE__ */ new Set([
|
|
677
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set([
|
|
678
|
+
"done",
|
|
679
|
+
"in-progress",
|
|
680
|
+
"planned",
|
|
681
|
+
"blocked",
|
|
682
|
+
"backlog",
|
|
683
|
+
"needs-human"
|
|
684
|
+
]);
|
|
601
685
|
var validating = false;
|
|
602
686
|
var MAX_OUTPUT_BYTES = 512 * 1024;
|
|
603
687
|
var VALIDATE_TIMEOUT_MS = 3e4;
|
|
@@ -771,12 +855,143 @@ async function handleRefreshChecks(c, ctx) {
|
|
|
771
855
|
await ctx.sseManager.broadcast(checksEvent);
|
|
772
856
|
return c.json({ ok: true, checks: checksData });
|
|
773
857
|
}
|
|
858
|
+
function detectWorkflow(spec, plans) {
|
|
859
|
+
if (!spec || spec === "none" || spec === "\u2014") return "brainstorming";
|
|
860
|
+
if (!plans || plans.length === 0 || plans.length === 1 && plans[0] === "\u2014")
|
|
861
|
+
return "planning";
|
|
862
|
+
return "execution";
|
|
863
|
+
}
|
|
864
|
+
async function assignGithubIssue(externalId, assignee) {
|
|
865
|
+
const token = process.env["GITHUB_TOKEN"];
|
|
866
|
+
if (!token) return false;
|
|
867
|
+
const match = externalId.match(/^github:(.+?)#(\d+)$/);
|
|
868
|
+
if (!match) return false;
|
|
869
|
+
const [, repo, issueNum] = match;
|
|
870
|
+
const cleanAssignee = assignee.startsWith("@") ? assignee.slice(1) : assignee;
|
|
871
|
+
try {
|
|
872
|
+
const res = await fetch(`https://api.github.com/repos/${repo}/issues/${issueNum}/assignees`, {
|
|
873
|
+
method: "POST",
|
|
874
|
+
headers: {
|
|
875
|
+
Authorization: `Bearer ${token}`,
|
|
876
|
+
Accept: "application/vnd.github+json",
|
|
877
|
+
"User-Agent": "harness-dashboard",
|
|
878
|
+
"Content-Type": "application/json"
|
|
879
|
+
},
|
|
880
|
+
body: JSON.stringify({ assignees: [cleanAssignee] })
|
|
881
|
+
});
|
|
882
|
+
return res.ok;
|
|
883
|
+
} catch {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
function findFeature(roadmap, name) {
|
|
888
|
+
for (const m of roadmap.milestones) {
|
|
889
|
+
for (const f of m.features) {
|
|
890
|
+
if (f.name === name) return f;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return void 0;
|
|
894
|
+
}
|
|
895
|
+
function validateClaimable(feat) {
|
|
896
|
+
if (feat.status !== "planned" && feat.status !== "backlog") {
|
|
897
|
+
return `Feature '${feat.name}' is not claimable (status: ${feat.status})`;
|
|
898
|
+
}
|
|
899
|
+
if (feat.assignee && feat.assignee !== "\u2014") {
|
|
900
|
+
return `Feature '${feat.name}' is already assigned to ${feat.assignee}`;
|
|
901
|
+
}
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
function applyClaimToRoadmap(roadmap, feat, assignee) {
|
|
905
|
+
const workflow = detectWorkflow(feat.spec, feat.plans);
|
|
906
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
907
|
+
feat.status = "in-progress";
|
|
908
|
+
feat.assignee = assignee;
|
|
909
|
+
feat.updatedAt = now;
|
|
910
|
+
roadmap.assignmentHistory.push({
|
|
911
|
+
feature: feat.name,
|
|
912
|
+
assignee,
|
|
913
|
+
action: "assigned",
|
|
914
|
+
date: now.split("T")[0]
|
|
915
|
+
});
|
|
916
|
+
roadmap.frontmatter.lastManualEdit = now;
|
|
917
|
+
return workflow;
|
|
918
|
+
}
|
|
919
|
+
async function handleClaim(c, ctx) {
|
|
920
|
+
const body = await c.req.json();
|
|
921
|
+
const { feature, assignee } = body;
|
|
922
|
+
if (!feature || !assignee) {
|
|
923
|
+
return c.json({ error: "feature and assignee are required" }, 400);
|
|
924
|
+
}
|
|
925
|
+
if (feature.length > 200 || assignee.length > 100) {
|
|
926
|
+
return c.json({ error: "feature or assignee exceeds maximum length" }, 400);
|
|
927
|
+
}
|
|
928
|
+
let result;
|
|
929
|
+
let externalId = null;
|
|
930
|
+
let workflow = "brainstorming";
|
|
931
|
+
await withFileLock(ctx.roadmapPath, async () => {
|
|
932
|
+
let content;
|
|
933
|
+
try {
|
|
934
|
+
content = await readFile2(ctx.roadmapPath, "utf-8");
|
|
935
|
+
} catch {
|
|
936
|
+
result = c.json({ error: "Could not read roadmap file" }, 500);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const parsed = parseRoadmap2(content);
|
|
940
|
+
if (!parsed.ok) {
|
|
941
|
+
result = c.json({ error: "Failed to parse roadmap" }, 500);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const roadmap = parsed.value;
|
|
945
|
+
const feat = findFeature(roadmap, feature);
|
|
946
|
+
if (!feat) {
|
|
947
|
+
result = c.json({ error: `Feature '${feature}' not found` }, 404);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const validationError = validateClaimable(feat);
|
|
951
|
+
if (validationError) {
|
|
952
|
+
result = c.json({ error: validationError }, 409);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
externalId = feat.externalId ?? null;
|
|
956
|
+
workflow = applyClaimToRoadmap(roadmap, feat, assignee);
|
|
957
|
+
try {
|
|
958
|
+
await writeFile(ctx.roadmapPath, serializeRoadmap(roadmap), "utf-8");
|
|
959
|
+
} catch {
|
|
960
|
+
result = c.json({ error: "Could not write roadmap file" }, 500);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
ctx.cache.invalidate("roadmap");
|
|
964
|
+
ctx.cache.invalidate("overview");
|
|
965
|
+
});
|
|
966
|
+
if (result) return result;
|
|
967
|
+
let githubSynced = false;
|
|
968
|
+
if (externalId) {
|
|
969
|
+
githubSynced = await assignGithubIssue(externalId, assignee);
|
|
970
|
+
}
|
|
971
|
+
return c.json({
|
|
972
|
+
ok: true,
|
|
973
|
+
feature,
|
|
974
|
+
status: "in-progress",
|
|
975
|
+
assignee,
|
|
976
|
+
workflow,
|
|
977
|
+
githubSynced
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
async function handleIdentity(c) {
|
|
981
|
+
const identity = await resolveIdentity();
|
|
982
|
+
if (!identity) {
|
|
983
|
+
return c.json({ error: "Could not resolve GitHub identity" }, 503);
|
|
984
|
+
}
|
|
985
|
+
return c.json(identity);
|
|
986
|
+
}
|
|
774
987
|
function buildActionsRouter(ctx) {
|
|
775
988
|
const router = new Hono8();
|
|
776
989
|
router.post("/actions/roadmap-status", (c) => handleRoadmapStatus(c, ctx));
|
|
990
|
+
router.post("/actions/roadmap/claim", (c) => handleClaim(c, ctx));
|
|
777
991
|
router.post("/actions/validate", (c) => handleValidate(c, ctx));
|
|
778
992
|
router.post("/actions/regen-charts", (c) => handleRegenCharts(c, ctx));
|
|
779
993
|
router.post("/actions/refresh-checks", (c) => handleRefreshChecks(c, ctx));
|
|
994
|
+
router.get("/identity", (c) => handleIdentity(c));
|
|
780
995
|
return router;
|
|
781
996
|
}
|
|
782
997
|
|
|
@@ -907,7 +1122,7 @@ async function gatherBlastRadius(projectPath, nodeId, maxDepth = DEFAULT_MAX_DEP
|
|
|
907
1122
|
const loaded = await store.load(join4(projectPath, GRAPH_DIR));
|
|
908
1123
|
if (!loaded) {
|
|
909
1124
|
return {
|
|
910
|
-
error: 'Graph data not found. Run "harness
|
|
1125
|
+
error: 'Graph data not found. Run "harness scan" to build the knowledge graph.'
|
|
911
1126
|
};
|
|
912
1127
|
}
|
|
913
1128
|
const simulator = new CascadeSimulator(store);
|
package/dist/server/serve.js
CHANGED
|
@@ -16,7 +16,7 @@ if (existsSync(join(clientDir, "index.html")) && existsSync(join(clientDir, "ass
|
|
|
16
16
|
process.env["DASHBOARD_CLIENT_ROOT"] = clientDir;
|
|
17
17
|
console.log(`Serving dashboard client from ${clientDir}`);
|
|
18
18
|
}
|
|
19
|
-
var { app } = await import("./-
|
|
19
|
+
var { app } = await import("./-HPCOJXLL.js");
|
|
20
20
|
var port = Number(process.env["DASHBOARD_API_PORT"] ?? API_PORT);
|
|
21
21
|
var hostname = getBindHost();
|
|
22
22
|
console.log(`Hono server starting on http://${hostname}:${port}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-engineering/dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Local web dashboard for harness project health and roadmap visualization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
"react-syntax-highlighter": "^16.1.1",
|
|
26
26
|
"react-virtuoso": "^4.18.5",
|
|
27
27
|
"remark-gfm": "^4.0.1",
|
|
28
|
-
"
|
|
29
|
-
"@harness-engineering/
|
|
28
|
+
"zustand": "^5.0.12",
|
|
29
|
+
"@harness-engineering/core": "0.23.4",
|
|
30
|
+
"@harness-engineering/graph": "0.7.0",
|
|
30
31
|
"@harness-engineering/types": "0.10.1"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|