@tenonhq/dovetail-dashboard 0.0.28 → 0.0.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-dashboard",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
4
4
  "description": "Update Set Dashboard for Dovetail",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -23,6 +23,21 @@
23
23
  border-color: var(--brand);
24
24
  color: var(--brand);
25
25
  }
26
+ .cp-lints-badge {
27
+ display: inline-flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ min-width: 18px;
31
+ height: 18px;
32
+ margin: 0 2px;
33
+ padding: 0 5px;
34
+ background: var(--danger);
35
+ color: var(--fg-on-dark);
36
+ border-radius: var(--radius-pill);
37
+ font-size: 11px;
38
+ font-weight: var(--weight-bold);
39
+ line-height: 1;
40
+ }
26
41
  .cp-storage {
27
42
  font-size: 11px;
28
43
  font-family: var(--font-mono);
@@ -30,7 +30,9 @@
30
30
  </div>
31
31
  <div class="header-right">
32
32
  <a class="cp-nav-link" href="/">&larr; Update Sets</a>
33
- <a class="cp-nav-link" href="/prompt-lints">Prompt Lints &rarr;</a>
33
+ <a class="cp-nav-link" href="/prompt-lints">Prompt Lints
34
+ <span class="cp-lints-badge" id="cp-lints-badge" title="Prompts needing review" hidden>0</span>
35
+ &rarr;</a>
34
36
  <button class="cp-theme-toggle" id="cp-theme-toggle" type="button"
35
37
  aria-label="Toggle dark mode" title="Toggle dark mode"></button>
36
38
  <span class="cp-storage" id="cp-storage" title="storage root"></span>
@@ -43,7 +45,7 @@
43
45
  <span class="cp-rail-title">Plans</span>
44
46
  <span class="cp-count" id="cp-count">0</span>
45
47
  </div>
46
- <details class="cp-topics" id="cp-topics" open>
48
+ <details class="cp-topics" id="cp-topics">
47
49
  <summary class="cp-topics-summary">
48
50
  <span class="cp-topics-caret" aria-hidden="true">▾</span>
49
51
  <span class="cp-topics-title">Topics</span>
@@ -42,6 +42,7 @@
42
42
 
43
43
  var els = {
44
44
  storage: document.getElementById("cp-storage"),
45
+ lintsBadge: document.getElementById("cp-lints-badge"),
45
46
  list: document.getElementById("cp-list"),
46
47
  count: document.getElementById("cp-count"),
47
48
  railEmpty: document.getElementById("cp-rail-empty"),
@@ -219,7 +220,7 @@
219
220
  if (els.topics) {
220
221
  try {
221
222
  var stored = localStorage.getItem("cp-topics-collapsed");
222
- if (stored === "1") els.topics.open = false;
223
+ els.topics.open = stored === "0";
223
224
  } catch (_) {}
224
225
  els.topics.addEventListener("toggle", function () {
225
226
  try {
@@ -709,6 +710,10 @@
709
710
 
710
711
  function selectPlan(slug) {
711
712
  state.selectedSlug = slug;
713
+ if (window.history && window.history.replaceState) {
714
+ var url = window.location.pathname + "?plan=" + encodeURIComponent(slug);
715
+ window.history.replaceState(null, "", url);
716
+ }
712
717
  renderRail();
713
718
  renderDetail();
714
719
  }
@@ -831,6 +836,42 @@
831
836
  if (state.selectedSlug === planSlug) renderDetail();
832
837
  }
833
838
 
839
+ // A lint event "needs attention" if its score fell below the recording
840
+ // threshold (default 50 when no threshold is supplied) or it flagged any
841
+ // missing tags, antipatterns, or ceremony. The badge surfaces that count.
842
+ function lintNeedsAttention(e) {
843
+ if (!e) return false;
844
+ var threshold = typeof e.threshold === "number" ? e.threshold : 50;
845
+ if (typeof e.score === "number" && e.score < threshold) return true;
846
+ if (Array.isArray(e.missing) && e.missing.length) return true;
847
+ if (Array.isArray(e.antipatterns) && e.antipatterns.length) return true;
848
+ if (Array.isArray(e.ceremony) && e.ceremony.length) return true;
849
+ return false;
850
+ }
851
+
852
+ function setLintsBadge(count) {
853
+ if (!els.lintsBadge) return;
854
+ if (count > 0) {
855
+ els.lintsBadge.textContent = String(count);
856
+ els.lintsBadge.hidden = false;
857
+ } else {
858
+ els.lintsBadge.hidden = true;
859
+ }
860
+ }
861
+
862
+ function loadLintsBadge() {
863
+ return fetch("/api/prompt-lints")
864
+ .then(function (r) { return r.json(); })
865
+ .then(function (data) {
866
+ var events = data && Array.isArray(data.events) ? data.events : [];
867
+ setLintsBadge(events.filter(lintNeedsAttention).length);
868
+ })
869
+ .catch(function (err) {
870
+ console.warn("[claude-plans] failed to load prompt lints:", err);
871
+ setLintsBadge(0);
872
+ });
873
+ }
874
+
834
875
  async function loadInitial() {
835
876
  try {
836
877
  var res = await fetch("/api/claude-plans");
@@ -863,11 +904,10 @@
863
904
  }));
864
905
  renderRail();
865
906
  if (!state.selectedSlug) {
866
- var pathMatch = window.location.pathname.match(/^\/claude-plans\/([a-z0-9][a-z0-9-]{0,63})$/);
867
- var pathSlug = pathMatch && pathMatch[1];
907
+ var paramSlug = new URLSearchParams(window.location.search).get("plan");
868
908
  var sorted = sortedPlans();
869
- var target = pathSlug && state.plans.has(pathSlug)
870
- ? pathSlug
909
+ var target = paramSlug && state.plans.has(paramSlug)
910
+ ? paramSlug
871
911
  : (sorted.length > 0 ? sorted[0].slug : null);
872
912
  if (target) selectPlan(target);
873
913
  }
@@ -941,6 +981,7 @@
941
981
  setupSearch();
942
982
  setupTopics();
943
983
  setActiveTab("plan");
984
+ loadLintsBadge();
944
985
  loadInitial().then(startStream);
945
986
  });
946
987
  })();
package/server.js CHANGED
@@ -1038,10 +1038,16 @@ function startClaudePlanWatcher() {
1038
1038
  claudePlanWatcher.on("unlink", function (p) { handleWatcherChange("unlink", p); });
1039
1039
  }
1040
1040
 
1041
- app.get(["/claude-plans", "/claude-plans/:slug"], function (req, res) {
1041
+ app.get("/claude-plans", function (req, res) {
1042
1042
  res.sendFile(path.join(__dirname, "public", "claude-plans.html"));
1043
1043
  });
1044
1044
 
1045
+ // Legacy path-segment deep links (/claude-plans/:slug) redirect to the
1046
+ // query-param form so relative assets resolve against /claude-plans.
1047
+ app.get("/claude-plans/:slug", function (req, res) {
1048
+ res.redirect(301, "/claude-plans?plan=" + encodeURIComponent(req.params.slug));
1049
+ });
1050
+
1045
1051
  app.get("/prompt-lints", function (req, res) {
1046
1052
  res.sendFile(path.join(__dirname, "public", "prompt-lints.html"));
1047
1053
  });