tenbo-dashboard 0.1.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.
Files changed (155) hide show
  1. package/README.md +32 -0
  2. package/bin/tenbo-dashboard.mjs +67 -0
  3. package/index.html +12 -0
  4. package/package.json +75 -0
  5. package/scripts/compute-metrics.test.ts +35 -0
  6. package/scripts/compute-metrics.ts +46 -0
  7. package/scripts/next-id.test.ts +128 -0
  8. package/scripts/next-id.ts +183 -0
  9. package/scripts/validate-cli.test.ts +152 -0
  10. package/scripts/validate-cli.ts +181 -0
  11. package/src/App.tsx +152 -0
  12. package/src/ErrorBoundary.tsx +25 -0
  13. package/src/api/client.ts +41 -0
  14. package/src/api/lib/frontmatterScan.ts +37 -0
  15. package/src/api/lib/health/agingTodos.test.ts +32 -0
  16. package/src/api/lib/health/agingTodos.ts +97 -0
  17. package/src/api/lib/health/archCompliance.test.ts +26 -0
  18. package/src/api/lib/health/archCompliance.ts +53 -0
  19. package/src/api/lib/health/collectAll.test.ts +49 -0
  20. package/src/api/lib/health/collectAll.ts +78 -0
  21. package/src/api/lib/health/config.test.ts +45 -0
  22. package/src/api/lib/health/config.ts +54 -0
  23. package/src/api/lib/health/coupling.test.ts +40 -0
  24. package/src/api/lib/health/coupling.ts +66 -0
  25. package/src/api/lib/health/deadCode.test.ts +23 -0
  26. package/src/api/lib/health/deadCode.ts +76 -0
  27. package/src/api/lib/health/docDrift.test.ts +54 -0
  28. package/src/api/lib/health/docDrift.ts +99 -0
  29. package/src/api/lib/health/hotspotFiles.test.ts +49 -0
  30. package/src/api/lib/health/hotspotFiles.ts +70 -0
  31. package/src/api/lib/health/importGraph.test.ts +29 -0
  32. package/src/api/lib/health/importGraph.ts +51 -0
  33. package/src/api/lib/health/layerFiles.test.ts +46 -0
  34. package/src/api/lib/health/layerFiles.ts +40 -0
  35. package/src/api/lib/health/redundancy.test.ts +24 -0
  36. package/src/api/lib/health/redundancy.ts +162 -0
  37. package/src/api/lib/health/testCoverage.test.ts +33 -0
  38. package/src/api/lib/health/testCoverage.ts +55 -0
  39. package/src/api/lib/health/types.test.ts +29 -0
  40. package/src/api/lib/health/types.ts +85 -0
  41. package/src/api/lib/http.ts +34 -0
  42. package/src/api/lib/metrics.test.ts +81 -0
  43. package/src/api/lib/metrics.ts +78 -0
  44. package/src/api/lib/metricsRefresh.test.ts +54 -0
  45. package/src/api/lib/metricsRefresh.ts +147 -0
  46. package/src/api/lib/phases.test.ts +76 -0
  47. package/src/api/lib/phases.ts +48 -0
  48. package/src/api/lib/relationships.ts +65 -0
  49. package/src/api/lib/repoRoot.ts +12 -0
  50. package/src/api/lib/tenboFs.layerContent.test.ts +36 -0
  51. package/src/api/lib/tenboFs.ts +237 -0
  52. package/src/api/lib/tenboFs.v2.test.ts +86 -0
  53. package/src/api/lib/tenboFs.workspaceContent.test.ts +42 -0
  54. package/src/api/lib/validator.docUpdate.test.ts +99 -0
  55. package/src/api/lib/validator.phases.test.ts +98 -0
  56. package/src/api/lib/validator.relationships.test.ts +126 -0
  57. package/src/api/lib/validator.specLinks.test.ts +96 -0
  58. package/src/api/lib/validator.ts +311 -0
  59. package/src/api/lib/validator.v2.test.ts +55 -0
  60. package/src/api/lib/yamlOrdered.ts +60 -0
  61. package/src/api/plugin.ts +31 -0
  62. package/src/api/routes/items.ts +25 -0
  63. package/src/api/routes/layerContent.ts +22 -0
  64. package/src/api/routes/layerDocs.ts +22 -0
  65. package/src/api/routes/open.ts +18 -0
  66. package/src/api/routes/related.ts +11 -0
  67. package/src/api/routes/reorder.ts +15 -0
  68. package/src/api/routes/state.ts +23 -0
  69. package/src/api/routes/watch.ts +31 -0
  70. package/src/css-modules.d.ts +4 -0
  71. package/src/hooks/useApiPatch.ts +9 -0
  72. package/src/hooks/useLayerContent.ts +25 -0
  73. package/src/hooks/usePrompt.ts +23 -0
  74. package/src/hooks/useTenboState.ts +35 -0
  75. package/src/main.tsx +13 -0
  76. package/src/prompts/populateLayer.ts +12 -0
  77. package/src/router/routes.ts +98 -0
  78. package/src/router/useHashRoute.test.ts +67 -0
  79. package/src/router/useHashRoute.ts +15 -0
  80. package/src/styles.css +228 -0
  81. package/src/test-setup.ts +1 -0
  82. package/src/types.ts +202 -0
  83. package/src/ui/DotGrid.module.css +7 -0
  84. package/src/ui/DotGrid.tsx +87 -0
  85. package/src/ui/EmptyState.module.css +11 -0
  86. package/src/ui/EmptyState.tsx +10 -0
  87. package/src/ui/FindingModal/EvidenceSection.tsx +51 -0
  88. package/src/ui/FindingModal/FindingModal.module.css +31 -0
  89. package/src/ui/FindingModal/FindingModal.test.tsx +36 -0
  90. package/src/ui/FindingModal/HeadlineSection.tsx +17 -0
  91. package/src/ui/FindingModal/RelatedFindingsSection.tsx +29 -0
  92. package/src/ui/FindingModal/SuggestionSection.tsx +13 -0
  93. package/src/ui/FindingModal/index.tsx +36 -0
  94. package/src/ui/HealthPage/Digest.tsx +29 -0
  95. package/src/ui/HealthPage/FindingRow.tsx +21 -0
  96. package/src/ui/HealthPage/HealthPage.module.css +45 -0
  97. package/src/ui/HealthPage/HealthPage.test.tsx +49 -0
  98. package/src/ui/HealthPage/HealthPage.tsx +66 -0
  99. package/src/ui/HealthPage/LayerCard.tsx +36 -0
  100. package/src/ui/HealthPage/SeverityIcon.tsx +29 -0
  101. package/src/ui/HealthPage/SeverityLegend.tsx +27 -0
  102. package/src/ui/HealthPage/severity.test.ts +46 -0
  103. package/src/ui/HealthPage/severity.ts +21 -0
  104. package/src/ui/ItemCard.module.css +131 -0
  105. package/src/ui/ItemCard.tsx +117 -0
  106. package/src/ui/ItemModal/DescriptionField.tsx +22 -0
  107. package/src/ui/ItemModal/EnrichmentSections.tsx +49 -0
  108. package/src/ui/ItemModal/ItemModal.module.css +200 -0
  109. package/src/ui/ItemModal/LinksSection.tsx +26 -0
  110. package/src/ui/ItemModal/NotesSection.tsx +18 -0
  111. package/src/ui/ItemModal/PhasesSection.tsx +79 -0
  112. package/src/ui/ItemModal/RelatedSection.tsx +26 -0
  113. package/src/ui/ItemModal/RelationshipsSection.tsx +69 -0
  114. package/src/ui/ItemModal/StatusLayerControls.tsx +43 -0
  115. package/src/ui/ItemModal/TitleField.tsx +21 -0
  116. package/src/ui/ItemModal/index.tsx +117 -0
  117. package/src/ui/KanbanColumn.module.css +20 -0
  118. package/src/ui/KanbanColumn.tsx +36 -0
  119. package/src/ui/LayerDetailPage/LayerDetailPage.module.css +11 -0
  120. package/src/ui/LayerDetailPage/LayerDetailPage.test.tsx +30 -0
  121. package/src/ui/LayerDetailPage/LayerDetailPage.tsx +59 -0
  122. package/src/ui/LayerDetailPage/SignalSection.tsx +33 -0
  123. package/src/ui/LayerDrawer.module.css +60 -0
  124. package/src/ui/LayerDrawer.tsx +69 -0
  125. package/src/ui/LayerKanban.module.css +45 -0
  126. package/src/ui/LayerKanban.tsx +65 -0
  127. package/src/ui/LayerPage/FillThisInButton.tsx +11 -0
  128. package/src/ui/LayerPage/LayerPage.module.css +52 -0
  129. package/src/ui/LayerPage/LayerPage.tsx +76 -0
  130. package/src/ui/LayerPage/MetricsBadges.tsx +18 -0
  131. package/src/ui/LayerPage/isTemplateOnly.test.ts +45 -0
  132. package/src/ui/LayerPage/isTemplateOnly.ts +19 -0
  133. package/src/ui/LayerPage/tabs/CodeMapTab.tsx +19 -0
  134. package/src/ui/LayerPage/tabs/DeepDivesTab.tsx +35 -0
  135. package/src/ui/LayerPage/tabs/IntentTab.tsx +19 -0
  136. package/src/ui/LayerPage/tabs/PlainEnglishTab.tsx +19 -0
  137. package/src/ui/Markdown.module.css +115 -0
  138. package/src/ui/Markdown.tsx +27 -0
  139. package/src/ui/RoadmapPage/RoadmapPage.module.css +93 -0
  140. package/src/ui/RoadmapPage/RoadmapPage.tsx +255 -0
  141. package/src/ui/ScopePage/ScopePage.module.css +72 -0
  142. package/src/ui/ScopePage/ScopePage.tsx +131 -0
  143. package/src/ui/SummaryStrip.module.css +18 -0
  144. package/src/ui/SummaryStrip.tsx +16 -0
  145. package/src/ui/TaskList.module.css +136 -0
  146. package/src/ui/TaskList.tsx +91 -0
  147. package/src/ui/TopBar.module.css +78 -0
  148. package/src/ui/TopBar.tsx +48 -0
  149. package/src/ui/WorkspacePage/WorkspacePage.module.css +24 -0
  150. package/src/ui/WorkspacePage/WorkspacePage.tsx +36 -0
  151. package/src/ui/WorkspacePage/tabs/GlossaryTab.tsx +6 -0
  152. package/src/ui/WorkspacePage/tabs/OverviewTab.tsx +28 -0
  153. package/src/ui/WorkspacePage/tabs/PrinciplesTab.tsx +6 -0
  154. package/tsconfig.json +18 -0
  155. package/vite.config.ts +9 -0
@@ -0,0 +1,91 @@
1
+ import { CornerLeftUp, ArrowLeftRight } from 'lucide-react';
2
+ import type { Item, Status } from '../types';
3
+ import { effectiveStatus } from '../api/lib/phases';
4
+ import styles from './TaskList.module.css';
5
+
6
+ export interface FlatItem {
7
+ item: Item;
8
+ scopeId: string;
9
+ layerName?: string;
10
+ }
11
+
12
+ interface Props {
13
+ items: FlatItem[];
14
+ onRowClick: (scopeId: string, item: Item) => void;
15
+ }
16
+
17
+ const STATUS_ORDER: Status[] = ['now', 'next', 'later', 'done'];
18
+ const STATUS_LABEL: Record<Status, string> = { now: 'Now', next: 'Next', later: 'Later', done: 'Done' };
19
+
20
+ export function TaskList({ items, onRowClick }: Props) {
21
+ const sorted = STATUS_ORDER.flatMap(s =>
22
+ items.filter(({ item }) => effectiveStatus(item) === s)
23
+ );
24
+
25
+ if (items.length === 0) {
26
+ return <div className={styles.empty}>No items to show.</div>;
27
+ }
28
+
29
+ return (
30
+ <div className={styles.wrap}>
31
+ <table className={styles.table}>
32
+ <thead>
33
+ <tr>
34
+ <th className={styles.thStatus}>Status</th>
35
+ <th className={styles.thId}>ID</th>
36
+ <th className={styles.thPriority}>Priority</th>
37
+ <th className={styles.thTitle}>Title</th>
38
+ <th className={styles.thLayer}>Layer</th>
39
+ <th className={styles.thType}>Type</th>
40
+ </tr>
41
+ </thead>
42
+ <tbody>
43
+ {sorted.map(({ item, scopeId, layerName }) => {
44
+ const status = effectiveStatus(item);
45
+ return (
46
+ <tr
47
+ key={item.id}
48
+ className={styles.row}
49
+ onClick={() => onRowClick(scopeId, item)}
50
+ tabIndex={0}
51
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onRowClick(scopeId, item); } }}
52
+ >
53
+ <td>
54
+ <span className={`${styles.badge} ${styles[`badge_${status}`]}`}>
55
+ {STATUS_LABEL[status]}
56
+ </span>
57
+ </td>
58
+ <td>
59
+ <div className={styles.tdId}>
60
+ <span className={styles.idText}>{item.id}</span>
61
+ {item.spawned_from && (
62
+ <span className={styles.relChip} title={`Spawned from ${item.spawned_from}`}>
63
+ <CornerLeftUp size={10} strokeWidth={1.75} />
64
+ </span>
65
+ )}
66
+ {item.related && item.related.length > 0 && (
67
+ <span className={styles.relChip} title={`Related to ${item.related.length} item(s)`}>
68
+ <ArrowLeftRight size={10} strokeWidth={1.75} /> {item.related.length}
69
+ </span>
70
+ )}
71
+ </div>
72
+ </td>
73
+ <td>
74
+ {item.priority
75
+ ? <span className={`${styles.priority} ${styles[`pri_${item.priority}`]}`}>{item.priority.toUpperCase()}</span>
76
+ : <span className={styles.dash}>—</span>
77
+ }
78
+ </td>
79
+ <td>
80
+ <span className={styles.titleText}>{item.title}</span>
81
+ </td>
82
+ <td className={styles.tdLayer}>{layerName ?? item.layer ?? <span className={styles.dash}>—</span>}</td>
83
+ <td className={styles.tdType}>{item.type ?? <span className={styles.dash}>—</span>}</td>
84
+ </tr>
85
+ );
86
+ })}
87
+ </tbody>
88
+ </table>
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1,78 @@
1
+ .header {
2
+ position: sticky;
3
+ top: 0;
4
+ z-index: var(--z-header, 50);
5
+ display: flex;
6
+ align-items: stretch;
7
+ gap: 24px;
8
+ padding: 0 16px;
9
+ height: 48px;
10
+ background: var(--console-black);
11
+ border-bottom: 1px solid var(--console-mist);
12
+ }
13
+ .brand {
14
+ display: inline-flex;
15
+ align-items: center;
16
+ font-size: 14px;
17
+ font-weight: 600;
18
+ color: var(--console-snow);
19
+ letter-spacing: -0.005em;
20
+ }
21
+
22
+ /* Mode nav — DESIGN.md spec: 2px Signal Cyan bottom strut on active, no pill, no fill. */
23
+ .modeToggle {
24
+ display: flex;
25
+ gap: 0;
26
+ height: 100%;
27
+ }
28
+ .modeButton, .modeActive {
29
+ position: relative;
30
+ display: inline-flex;
31
+ align-items: center;
32
+ height: 100%;
33
+ padding: 0 12px;
34
+ background: transparent;
35
+ border: none;
36
+ border-radius: 0;
37
+ font-size: 14px;
38
+ font-weight: 600;
39
+ color: var(--console-fog);
40
+ cursor: pointer;
41
+ transition: color var(--dur-state) var(--ease);
42
+ }
43
+ .modeButton:hover, .modeActive:hover { color: var(--console-snow); }
44
+ .modeActive {
45
+ color: var(--signal-cyan);
46
+ }
47
+ .modeActive::after {
48
+ content: '';
49
+ position: absolute;
50
+ left: 0;
51
+ right: 0;
52
+ bottom: -1px;
53
+ height: 2px;
54
+ background: var(--signal-cyan);
55
+ }
56
+
57
+ .spacer {
58
+ flex: 1;
59
+ }
60
+ .validateBtn {
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 6px;
64
+ }
65
+ .dot {
66
+ width: 8px;
67
+ height: 8px;
68
+ border-radius: var(--r-full);
69
+ }
70
+ .label {
71
+ font-size: 12px;
72
+ color: var(--console-fog);
73
+ }
74
+
75
+ /* Center the Reload button vertically with the header */
76
+ .header > :global(.outlined-button) {
77
+ align-self: center;
78
+ }
@@ -0,0 +1,48 @@
1
+ import { RefreshCw } from 'lucide-react';
2
+ import type { Mode } from '../router/routes';
3
+ import styles from './TopBar.module.css';
4
+
5
+ interface Props {
6
+ mode: Mode;
7
+ onSelectMode: (m: Mode) => void;
8
+ onReload: () => void;
9
+ }
10
+
11
+ export function TopBar({ mode, onSelectMode, onReload }: Props) {
12
+ return (
13
+ <header className={styles.header}>
14
+ <strong className={styles.brand}>tenbo</strong>
15
+ <nav className={styles.modeToggle} role="tablist" aria-label="Top-level view">
16
+ <button
17
+ role="tab"
18
+ aria-selected={mode === 'roadmap'}
19
+ className={mode === 'roadmap' ? styles.modeActive : styles.modeButton}
20
+ onClick={() => onSelectMode('roadmap')}
21
+ >
22
+ Roadmap
23
+ </button>
24
+ <button
25
+ role="tab"
26
+ aria-selected={mode === 'health'}
27
+ className={mode === 'health' ? styles.modeActive : styles.modeButton}
28
+ onClick={() => onSelectMode('health')}
29
+ >
30
+ Health
31
+ </button>
32
+ <button
33
+ role="tab"
34
+ aria-selected={mode === 'docs'}
35
+ className={mode === 'docs' ? styles.modeActive : styles.modeButton}
36
+ onClick={() => onSelectMode('docs')}
37
+ >
38
+ Docs
39
+ </button>
40
+ </nav>
41
+ <div className={styles.spacer} />
42
+ <button onClick={onReload} className="outlined-button icon-text">
43
+ <RefreshCw size={14} strokeWidth={1.75} />
44
+ Reload
45
+ </button>
46
+ </header>
47
+ );
48
+ }
@@ -0,0 +1,24 @@
1
+ .page {
2
+ max-width: 960px;
3
+ margin: 0 auto;
4
+ padding: 0 32px 48px;
5
+ }
6
+ .breadcrumb {
7
+ padding: 12px 32px;
8
+ border-bottom: 1px solid var(--console-mist);
9
+ font-size: 12px;
10
+ font-weight: 500;
11
+ letter-spacing: 0.02em;
12
+ color: var(--console-fog);
13
+ margin: 0 -32px;
14
+ }
15
+ .tabs {
16
+ display: flex;
17
+ gap: 0;
18
+ padding: 16px 32px 8px;
19
+ border-bottom: 1px solid var(--console-mist);
20
+ margin: 0 -32px 24px;
21
+ }
22
+ .body {
23
+ padding-top: 8px;
24
+ }
@@ -0,0 +1,36 @@
1
+ import type { Route, WorkspaceTab } from '../../router/routes';
2
+ import type { TenboState } from '../../types';
3
+ import { OverviewTab } from './tabs/OverviewTab';
4
+ import { PrinciplesTab } from './tabs/PrinciplesTab';
5
+ import { GlossaryTab } from './tabs/GlossaryTab';
6
+ import styles from './WorkspacePage.module.css';
7
+
8
+ const TABS: WorkspaceTab[] = ['overview', 'principles', 'glossary'];
9
+
10
+ const TAB_LABELS: Record<WorkspaceTab, string> = {
11
+ 'overview': 'Project overview',
12
+ 'principles': 'Principles',
13
+ 'glossary': 'Glossary',
14
+ };
15
+
16
+ export function WorkspacePage(props: { tab: WorkspaceTab; navigate: (r: Route) => void; state: TenboState }) {
17
+ return (
18
+ <div className={styles.page}>
19
+ <nav aria-label="breadcrumb" className={styles.breadcrumb}>
20
+ <span>Project</span>
21
+ </nav>
22
+ <nav className={styles.tabs}>
23
+ {TABS.map((t) => (
24
+ <button key={t} aria-pressed={props.tab === t} onClick={() => props.navigate({ kind: 'docs-project', tab: t })}>
25
+ {TAB_LABELS[t]}
26
+ </button>
27
+ ))}
28
+ </nav>
29
+ <div className={styles.body}>
30
+ {props.tab === 'overview' && <OverviewTab state={props.state} navigate={props.navigate} />}
31
+ {props.tab === 'principles' && <PrinciplesTab state={props.state} />}
32
+ {props.tab === 'glossary' && <GlossaryTab state={props.state} />}
33
+ </div>
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,6 @@
1
+ import { Markdown } from '../../Markdown';
2
+ import type { TenboState } from '../../../types';
3
+
4
+ export function GlossaryTab({ state }: { state: TenboState }) {
5
+ return <Markdown source={state.workspaceContent.glossaryMd} />;
6
+ }
@@ -0,0 +1,28 @@
1
+ import { Markdown } from '../../Markdown';
2
+ import type { Route } from '../../../router/routes';
3
+ import type { TenboState } from '../../../types';
4
+
5
+ export function OverviewTab({ state }: { state: TenboState; navigate: (r: Route) => void }) {
6
+ return (
7
+ <div>
8
+ <Markdown source={state.workspaceContent.overviewMd} />
9
+
10
+ {state.crossCutting.length > 0 && (
11
+ <section style={{ marginTop: 24 }}>
12
+ <h2 style={{ fontSize: 14, fontWeight: 700, marginBottom: 12 }}>Cross-cutting concerns</h2>
13
+ {state.crossCutting.map((cc) => (
14
+ <div key={cc.id} style={{ padding: '8px 0', borderBottom: '1px solid var(--border, #eee)' }}>
15
+ <span style={{ fontSize: 11, color: 'var(--muted, #888)', marginRight: 8 }}>{cc.id}</span>
16
+ <span style={{ fontWeight: 500 }}>{cc.description}</span>
17
+ {cc.spans.length > 0 && (
18
+ <div style={{ fontSize: 12, color: 'var(--muted, #888)', marginTop: 2 }}>
19
+ Spans: {cc.spans.join(', ')}
20
+ </div>
21
+ )}
22
+ </div>
23
+ ))}
24
+ </section>
25
+ )}
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,6 @@
1
+ import { Markdown } from '../../Markdown';
2
+ import type { TenboState } from '../../../types';
3
+
4
+ export function PrinciplesTab({ state }: { state: TenboState }) {
5
+ return <Markdown source={state.workspaceContent.principlesMd} />;
6
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noUnusedLocals": true,
10
+ "noUnusedParameters": true,
11
+ "skipLibCheck": true,
12
+ "esModuleInterop": true,
13
+ "isolatedModules": true,
14
+ "allowImportingTsExtensions": false,
15
+ "noEmit": true
16
+ },
17
+ "include": ["src", "tests", "vite.config.ts"]
18
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import { tenboApiPlugin } from './src/api/plugin';
4
+
5
+ export default defineConfig({
6
+ plugins: [react(), tenboApiPlugin()],
7
+ server: { port: 5174 },
8
+ test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test-setup.ts'] },
9
+ });