@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.
@@ -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-C1zvTZwx.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-DVHWQnep.css">
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 graph scan" to build the knowledge graph.'
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 pending = "Checks have not completed yet";
303
+ const pending2 = "Checks have not completed yet";
286
304
  return {
287
- security: ctx.gatherCache.get("security") ?? { error: pending },
288
- perf: ctx.gatherCache.get("perf") ?? { error: pending },
289
- arch: ctx.gatherCache.get("arch") ?? { error: pending },
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: pending
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 graph scan" to build the knowledge graph.'
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(["done", "in-progress", "planned", "blocked", "backlog"]);
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 graph scan" to build the knowledge graph.'
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);
@@ -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("./-WJBYKRAQ.js");
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.2.2",
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
- "@harness-engineering/core": "0.23.3",
29
- "@harness-engineering/graph": "0.6.0",
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": {