@matt82198/aesop 0.1.0-beta.5 → 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 (198) hide show
  1. package/CHANGELOG.md +158 -5
  2. package/LICENSE +66 -21
  3. package/README.md +97 -33
  4. package/aesop.config.example.json +6 -0
  5. package/bin/CLAUDE.md +12 -3
  6. package/bin/cli.js +211 -40
  7. package/daemons/CLAUDE.md +1 -1
  8. package/daemons/backup-fleet.sh +209 -51
  9. package/daemons/run-watchdog.sh +208 -62
  10. package/dash/dash-extra.mjs +73 -4
  11. package/dash/watchdog-gui.sh +41 -35
  12. package/docs/ARCHITECTURE.md +296 -0
  13. package/docs/CONCEPTS.md +254 -0
  14. package/docs/CONFIGURE.md +256 -0
  15. package/docs/FIRST-WAVE.md +276 -0
  16. package/docs/INSTALL.md +224 -0
  17. package/docs/README.md +176 -27
  18. package/docs/autonomous-swe.md +149 -0
  19. package/docs/reproduce.md +121 -0
  20. package/hooks/CLAUDE.md +28 -0
  21. package/hooks/install-waveguard.sh +68 -0
  22. package/hooks/pre-commit-waveguard.sh +26 -0
  23. package/hooks/pre-push-policy.sh +253 -15
  24. package/monitor/CLAUDE.md +3 -3
  25. package/monitor/collect-signals.mjs +133 -20
  26. package/package.json +15 -7
  27. package/scan/CLAUDE.md +30 -0
  28. package/skills/CLAUDE.md +1 -0
  29. package/state_store/ingest.py +18 -1
  30. package/state_store/projections.py +78 -4
  31. package/state_store/store.py +102 -7
  32. package/tools/CLAUDE.md +25 -18
  33. package/tools/alert_bridge.py +14 -10
  34. package/tools/bench_runner.py +438 -0
  35. package/tools/buildlog.py +4 -7
  36. package/tools/ci_merge_wait.py +215 -34
  37. package/tools/common.py +62 -0
  38. package/tools/cost_ceiling.py +191 -0
  39. package/tools/dash.js +102 -0
  40. package/tools/doctor.js +220 -0
  41. package/tools/fleet_ledger.py +189 -17
  42. package/tools/halt.py +172 -0
  43. package/tools/healthcheck.py +24 -21
  44. package/tools/heartbeat.py +4 -7
  45. package/tools/metrics_gate.py +8 -2
  46. package/tools/orchestrator_status.py +4 -9
  47. package/tools/reconcile.py +277 -0
  48. package/tools/rotate_logs.py +152 -62
  49. package/tools/scanner_selftest.py +28 -3
  50. package/tools/secret_scan.py +359 -90
  51. package/tools/self_stats.py +292 -4
  52. package/tools/status.js +187 -0
  53. package/tools/verify_agent_inspector.py +289 -0
  54. package/tools/verify_prboard.py +344 -0
  55. package/tools/watch.js +49 -0
  56. package/ui/CLAUDE.md +2 -0
  57. package/ui/agents.py +332 -33
  58. package/ui/api/tracker.py +15 -7
  59. package/ui/collectors.py +125 -32
  60. package/ui/config.py +22 -2
  61. package/ui/cost.py +98 -3
  62. package/ui/csrf.py +2 -1
  63. package/ui/handler.py +209 -21
  64. package/ui/sse.py +102 -16
  65. package/ui/wave_prs.py +230 -0
  66. package/ui/web/dist/assets/index-0qQYnvMC.js +9 -0
  67. package/ui/web/dist/assets/index-BdIlFieV.css +1 -0
  68. package/ui/web/dist/index.html +2 -2
  69. package/monitor/.signal-state.json +0 -3
  70. package/monitor/ACTIONS.log +0 -1
  71. package/monitor/BRIEF.md +0 -24
  72. package/monitor/SIGNALS.json +0 -54
  73. package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
  74. package/state_store/__pycache__/api.cpython-314.pyc +0 -0
  75. package/state_store/__pycache__/export.cpython-314.pyc +0 -0
  76. package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
  77. package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
  78. package/state_store/__pycache__/store.cpython-314.pyc +0 -0
  79. package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
  80. package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
  81. package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
  82. package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
  83. package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
  84. package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
  85. package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
  86. package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
  87. package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
  88. package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
  89. package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
  90. package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
  91. package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
  92. package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
  93. package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
  94. package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
  95. package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
  96. package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
  97. package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
  98. package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
  99. package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
  100. package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
  101. package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
  102. package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
  103. package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
  104. package/ui/__pycache__/agents.cpython-314.pyc +0 -0
  105. package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
  106. package/ui/__pycache__/config.cpython-314.pyc +0 -0
  107. package/ui/__pycache__/cost.cpython-314.pyc +0 -0
  108. package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
  109. package/ui/__pycache__/handler.cpython-314.pyc +0 -0
  110. package/ui/__pycache__/render.cpython-314.pyc +0 -0
  111. package/ui/__pycache__/serve.cpython-314.pyc +0 -0
  112. package/ui/__pycache__/sse.cpython-314.pyc +0 -0
  113. package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
  114. package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
  115. package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
  116. package/ui/web/.gitattributes +0 -13
  117. package/ui/web/dist/assets/index-2LZDQirC.js +0 -9
  118. package/ui/web/dist/assets/index-D4M1qyOv.css +0 -1
  119. package/ui/web/index.html +0 -13
  120. package/ui/web/package-lock.json +0 -2225
  121. package/ui/web/package.json +0 -26
  122. package/ui/web/src/App.test.tsx +0 -74
  123. package/ui/web/src/App.tsx +0 -142
  124. package/ui/web/src/CONTRIBUTING-UI.md +0 -49
  125. package/ui/web/src/components/AgentRow.css +0 -187
  126. package/ui/web/src/components/AgentRow.test.tsx +0 -209
  127. package/ui/web/src/components/AgentRow.tsx +0 -207
  128. package/ui/web/src/components/AgentsPanel.css +0 -108
  129. package/ui/web/src/components/AgentsPanel.test.tsx +0 -41
  130. package/ui/web/src/components/AgentsPanel.tsx +0 -58
  131. package/ui/web/src/components/AlertsPanel.css +0 -88
  132. package/ui/web/src/components/AlertsPanel.test.tsx +0 -51
  133. package/ui/web/src/components/AlertsPanel.tsx +0 -67
  134. package/ui/web/src/components/BacklogPanel.test.tsx +0 -126
  135. package/ui/web/src/components/BacklogPanel.tsx +0 -122
  136. package/ui/web/src/components/CostChart.css +0 -110
  137. package/ui/web/src/components/CostChart.test.tsx +0 -144
  138. package/ui/web/src/components/CostChart.tsx +0 -152
  139. package/ui/web/src/components/CostTable.css +0 -93
  140. package/ui/web/src/components/CostTable.test.tsx +0 -165
  141. package/ui/web/src/components/CostTable.tsx +0 -94
  142. package/ui/web/src/components/EventsFeed.css +0 -68
  143. package/ui/web/src/components/EventsFeed.test.tsx +0 -36
  144. package/ui/web/src/components/EventsFeed.tsx +0 -31
  145. package/ui/web/src/components/HealthHeader.css +0 -137
  146. package/ui/web/src/components/HealthHeader.test.tsx +0 -278
  147. package/ui/web/src/components/HealthHeader.tsx +0 -281
  148. package/ui/web/src/components/InboxForm.css +0 -135
  149. package/ui/web/src/components/InboxForm.test.tsx +0 -208
  150. package/ui/web/src/components/InboxForm.tsx +0 -116
  151. package/ui/web/src/components/MessagesTail.module.css +0 -144
  152. package/ui/web/src/components/MessagesTail.test.tsx +0 -176
  153. package/ui/web/src/components/MessagesTail.tsx +0 -94
  154. package/ui/web/src/components/ReposPanel.css +0 -90
  155. package/ui/web/src/components/ReposPanel.test.tsx +0 -45
  156. package/ui/web/src/components/ReposPanel.tsx +0 -67
  157. package/ui/web/src/components/Scorecard.css +0 -106
  158. package/ui/web/src/components/Scorecard.test.tsx +0 -117
  159. package/ui/web/src/components/Scorecard.tsx +0 -85
  160. package/ui/web/src/components/Timeline.module.css +0 -151
  161. package/ui/web/src/components/Timeline.test.tsx +0 -215
  162. package/ui/web/src/components/Timeline.tsx +0 -99
  163. package/ui/web/src/components/TrackerBoard.test.tsx +0 -121
  164. package/ui/web/src/components/TrackerBoard.tsx +0 -107
  165. package/ui/web/src/components/TrackerCard.test.tsx +0 -180
  166. package/ui/web/src/components/TrackerCard.tsx +0 -160
  167. package/ui/web/src/components/TrackerForm.test.tsx +0 -189
  168. package/ui/web/src/components/TrackerForm.tsx +0 -144
  169. package/ui/web/src/lib/api.ts +0 -218
  170. package/ui/web/src/lib/format.test.ts +0 -89
  171. package/ui/web/src/lib/format.ts +0 -103
  172. package/ui/web/src/lib/sanitizeUrl.test.ts +0 -84
  173. package/ui/web/src/lib/sanitizeUrl.ts +0 -38
  174. package/ui/web/src/lib/types.ts +0 -230
  175. package/ui/web/src/lib/useHashRoute.test.ts +0 -60
  176. package/ui/web/src/lib/useHashRoute.ts +0 -23
  177. package/ui/web/src/lib/useSSE.ts +0 -175
  178. package/ui/web/src/main.tsx +0 -10
  179. package/ui/web/src/styles/global.css +0 -179
  180. package/ui/web/src/styles/theme.css +0 -184
  181. package/ui/web/src/styles/work.css +0 -572
  182. package/ui/web/src/test/fixtures.ts +0 -385
  183. package/ui/web/src/test/setup.ts +0 -49
  184. package/ui/web/src/views/Activity.module.css +0 -43
  185. package/ui/web/src/views/Activity.test.tsx +0 -89
  186. package/ui/web/src/views/Activity.tsx +0 -31
  187. package/ui/web/src/views/Cost.css +0 -87
  188. package/ui/web/src/views/Cost.test.tsx +0 -142
  189. package/ui/web/src/views/Cost.tsx +0 -54
  190. package/ui/web/src/views/Overview.css +0 -51
  191. package/ui/web/src/views/Overview.test.tsx +0 -76
  192. package/ui/web/src/views/Overview.tsx +0 -46
  193. package/ui/web/src/views/Work.test.tsx +0 -82
  194. package/ui/web/src/views/Work.tsx +0 -79
  195. package/ui/web/src/vite-env.d.ts +0 -10
  196. package/ui/web/tsconfig.json +0 -22
  197. package/ui/web/vite.config.ts +0 -25
  198. package/ui/web/vitest.config.ts +0 -12
@@ -1,93 +0,0 @@
1
- /* CostTable — per-model cost and token display */
2
-
3
- .cost-table {
4
- width: 100%;
5
- border-collapse: collapse;
6
- font-size: var(--font-size-sm);
7
- background: var(--color-surface);
8
- }
9
-
10
- .cost-table caption {
11
- text-align: left;
12
- padding: var(--space-2);
13
- font-size: var(--font-size-sm);
14
- color: var(--color-text-muted);
15
- caption-side: top;
16
- }
17
-
18
- .cost-table thead {
19
- background: var(--color-bg-inset);
20
- border-bottom: 2px solid var(--color-border);
21
- }
22
-
23
- .cost-table th {
24
- padding: var(--space-2) var(--space-3);
25
- text-align: left;
26
- font-weight: var(--font-weight-bold);
27
- color: var(--color-text);
28
- }
29
-
30
- .cost-table th.col-numeric {
31
- text-align: right;
32
- }
33
-
34
- .cost-table tbody tr {
35
- border-bottom: 1px solid var(--color-border-subtle);
36
- }
37
-
38
- .cost-table tbody tr:hover {
39
- background: var(--color-bg-subtle);
40
- }
41
-
42
- .cost-table td {
43
- padding: var(--space-2) var(--space-3);
44
- text-align: left;
45
- color: var(--color-text);
46
- }
47
-
48
- .cost-table td.col-numeric {
49
- text-align: right;
50
- font-family: var(--font-mono);
51
- }
52
-
53
- .cost-table .model-name {
54
- font-weight: var(--font-weight-medium);
55
- font-family: var(--font-mono);
56
- font-size: var(--font-size-xs);
57
- word-break: break-word;
58
- max-width: 180px;
59
- }
60
-
61
- /* Verdict severity indicators */
62
- .cost-table .verdict-failed {
63
- color: var(--color-status-error);
64
- }
65
-
66
- .cost-table .verdict-empty {
67
- color: var(--color-status-warn);
68
- }
69
-
70
- .cost-table .verdict-hung {
71
- color: var(--color-status-error);
72
- }
73
-
74
- /* Total cost column emphasis */
75
- .cost-table .col-total {
76
- font-weight: var(--font-weight-bold);
77
- }
78
-
79
- /* Responsive: overflow on smaller screens */
80
- @media (max-width: 768px) {
81
- .cost-table {
82
- font-size: var(--font-size-xs);
83
- }
84
-
85
- .cost-table th,
86
- .cost-table td {
87
- padding: var(--space-1) var(--space-2);
88
- }
89
-
90
- .cost-table .model-name {
91
- max-width: 100px;
92
- }
93
- }
@@ -1,165 +0,0 @@
1
- /**
2
- * CostTable component tests — per-model cost and token display.
3
- * Tests tokens-only mode (no pricing) and pricing mode separately.
4
- * Tests proper table semantics, caption, thead/tbody, scope attributes.
5
- */
6
-
7
- import { describe, it, expect } from 'vitest';
8
- import { render, screen } from '@testing-library/react';
9
- import { CostTable } from './CostTable';
10
- import { fixtureCost, fixtureCostWithPricing, TESTIDS } from '../test/fixtures';
11
-
12
- describe('CostTable', () => {
13
- it('renders table with testid', () => {
14
- render(<CostTable cost={fixtureCost} />);
15
- expect(screen.getByTestId(TESTIDS.costTable)).toBeInTheDocument();
16
- });
17
-
18
- it('renders as a proper <table> element', () => {
19
- render(<CostTable cost={fixtureCost} />);
20
- const table = screen.getByTestId(TESTIDS.costTable);
21
- expect(table.tagName).toBe('TABLE');
22
- });
23
-
24
- it('has a caption describing the table', () => {
25
- render(<CostTable cost={fixtureCost} />);
26
- const table = screen.getByTestId(TESTIDS.costTable);
27
- const caption = table.querySelector('caption');
28
- expect(caption).toBeInTheDocument();
29
- expect(caption?.textContent).toBeTruthy();
30
- });
31
-
32
- it('has proper thead and tbody structure', () => {
33
- render(<CostTable cost={fixtureCost} />);
34
- const table = screen.getByTestId(TESTIDS.costTable);
35
- expect(table.querySelector('thead')).toBeInTheDocument();
36
- expect(table.querySelector('tbody')).toBeInTheDocument();
37
- });
38
-
39
- it('has column headers with scope="col"', () => {
40
- render(<CostTable cost={fixtureCost} />);
41
- const table = screen.getByTestId(TESTIDS.costTable);
42
- const ths = table.querySelectorAll('th[scope="col"]');
43
- expect(ths.length).toBeGreaterThan(0);
44
- });
45
-
46
- it('lists all models in fixture data', () => {
47
- render(<CostTable cost={fixtureCost} />);
48
- expect(screen.getByText(/haiku/i)).toBeInTheDocument();
49
- expect(screen.getByText(/sonnet/i)).toBeInTheDocument();
50
- });
51
-
52
- it('displays runs count for each model', () => {
53
- render(<CostTable cost={fixtureCost} />);
54
- // haiku has 128 runs
55
- expect(screen.getByText('128')).toBeInTheDocument();
56
- // sonnet has 14 runs
57
- expect(screen.getByText('14')).toBeInTheDocument();
58
- });
59
-
60
- it('displays token counts in readable format (e.g., "2.1M")', () => {
61
- render(<CostTable cost={fixtureCost} />);
62
- const table = screen.getByTestId(TESTIDS.costTable);
63
- // haiku tokens_in is 2140050, should display as "2.1M"
64
- expect(table.textContent).toContain('2.1M');
65
- });
66
-
67
- it('displays verdict counts (OK/FAILED/EMPTY/HUNG)', () => {
68
- render(<CostTable cost={fixtureCost} />);
69
- const table = screen.getByTestId(TESTIDS.costTable);
70
- // haiku has verdicts: OK: 119, FAILED: 6, EMPTY: 2, HUNG: 1
71
- expect(table.textContent).toContain('119');
72
- expect(table.textContent).toContain('6');
73
- expect(table.textContent).toContain('2');
74
- expect(table.textContent).toContain('1');
75
- });
76
-
77
- describe('tokens-only mode (no pricing)', () => {
78
- it('shows tokens columns but no currency columns when has_pricing=false', () => {
79
- render(<CostTable cost={fixtureCost} />);
80
- const table = screen.getByTestId(TESTIDS.costTable);
81
- // Should have "tokens in/out" headers (case-insensitive)
82
- expect(table.textContent).toMatch(/tokens\s+in/i);
83
- expect(table.textContent).toMatch(/tokens\s+out/i);
84
- // Should NOT have a $ symbol (currency) or "estimate" label
85
- expect(table.innerHTML).not.toContain('$');
86
- });
87
- });
88
-
89
- describe('pricing mode', () => {
90
- it('shows currency columns labeled "estimate" when has_pricing=true', () => {
91
- render(<CostTable cost={fixtureCostWithPricing} />);
92
- const table = screen.getByTestId(TESTIDS.costTable);
93
- // Should show "estimate" label for pricing
94
- expect(table.textContent).toContain('estimate');
95
- // Should show $ symbols
96
- expect(table.innerHTML).toContain('$');
97
- });
98
-
99
- it('displays dollar amounts for input cost', () => {
100
- render(<CostTable cost={fixtureCostWithPricing} />);
101
- const table = screen.getByTestId(TESTIDS.costTable);
102
- // haiku input_cost is 2.14
103
- expect(table.textContent).toContain('2.14');
104
- });
105
-
106
- it('displays dollar amounts for output cost', () => {
107
- render(<CostTable cost={fixtureCostWithPricing} />);
108
- const table = screen.getByTestId(TESTIDS.costTable);
109
- // haiku output_cost is 2.05
110
- expect(table.textContent).toContain('2.05');
111
- });
112
-
113
- it('displays total cost', () => {
114
- render(<CostTable cost={fixtureCostWithPricing} />);
115
- const table = screen.getByTestId(TESTIDS.costTable);
116
- // haiku total_cost is 4.19
117
- expect(table.textContent).toContain('4.19');
118
- });
119
- });
120
-
121
- it('handles single row (1 model) without breaking layout', () => {
122
- const single = {
123
- ...fixtureCost,
124
- models: {
125
- 'claude-haiku-4-5-20251001': fixtureCost.models['claude-haiku-4-5-20251001'],
126
- },
127
- };
128
- render(<CostTable cost={single} />);
129
- const table = screen.getByTestId(TESTIDS.costTable);
130
- const rows = table.querySelectorAll('tbody tr');
131
- expect(rows.length).toBe(1);
132
- });
133
-
134
- it('handles many rows without breaking layout', () => {
135
- const many = {
136
- ...fixtureCost,
137
- models: {
138
- ...fixtureCost.models,
139
- 'model-1': {
140
- runs: 10,
141
- tokens_in: 1000,
142
- tokens_out: 500,
143
- verdicts: { OK: 9, FAILED: 1, EMPTY: 0, HUNG: 0 },
144
- },
145
- 'model-2': {
146
- runs: 20,
147
- tokens_in: 2000,
148
- tokens_out: 1000,
149
- verdicts: { OK: 18, FAILED: 2, EMPTY: 0, HUNG: 0 },
150
- },
151
- },
152
- };
153
- render(<CostTable cost={many} />);
154
- const table = screen.getByTestId(TESTIDS.costTable);
155
- const rows = table.querySelectorAll('tbody tr');
156
- expect(rows.length).toBeGreaterThan(2);
157
- });
158
-
159
- it('uses theme color tokens (no hex colors in output)', () => {
160
- render(<CostTable cost={fixtureCost} />);
161
- const table = screen.getByTestId(TESTIDS.costTable);
162
- // Should not have inline hex color styles
163
- expect(table.innerHTML).not.toMatch(/#[0-9a-fA-F]{3,6}(?![0-9a-fA-F])/);
164
- });
165
- });
@@ -1,94 +0,0 @@
1
- /**
2
- * CostTable component — per-model cost and token summary table.
3
- * Proper table semantics: caption, thead with scope="col", tbody.
4
- * Columns: Model | Runs | Tokens In | Tokens Out | Verdicts (OK/FAILED/EMPTY/HUNG) | [Pricing if has_pricing]
5
- * Dollar columns labeled "estimate" when pricing is available.
6
- * Uses formatTokens() for readable token display.
7
- */
8
-
9
- import type { CostSummary, CostModelStats } from '../lib/types';
10
- import { formatTokens, formatCurrency } from '../lib/format';
11
- import { TESTIDS } from '../test/fixtures';
12
- import './CostTable.css';
13
-
14
- interface CostTableProps {
15
- cost: CostSummary;
16
- }
17
-
18
- export function CostTable({ cost }: CostTableProps) {
19
- const { models, has_pricing, estimates_by_model } = cost;
20
- const modelIds = Object.keys(models).sort();
21
-
22
- return (
23
- <table className="cost-table" data-testid={TESTIDS.costTable}>
24
- <caption>Per-model cost and token usage summary</caption>
25
- <thead>
26
- <tr>
27
- <th scope="col">Model</th>
28
- <th scope="col" className="col-numeric">
29
- Runs
30
- </th>
31
- <th scope="col" className="col-numeric">
32
- Tokens In
33
- </th>
34
- <th scope="col" className="col-numeric">
35
- Tokens Out
36
- </th>
37
- <th scope="col" className="col-numeric">
38
- OK
39
- </th>
40
- <th scope="col" className="col-numeric">
41
- FAILED
42
- </th>
43
- <th scope="col" className="col-numeric">
44
- EMPTY
45
- </th>
46
- <th scope="col" className="col-numeric">
47
- HUNG
48
- </th>
49
- {has_pricing && (
50
- <>
51
- <th scope="col" className="col-numeric">
52
- Input (estimate)
53
- </th>
54
- <th scope="col" className="col-numeric">
55
- Output (estimate)
56
- </th>
57
- <th scope="col" className="col-numeric">
58
- Total (estimate)
59
- </th>
60
- </>
61
- )}
62
- </tr>
63
- </thead>
64
- <tbody>
65
- {modelIds.map((modelId) => {
66
- const stats = models[modelId] as CostModelStats;
67
- const estimate = has_pricing ? estimates_by_model[modelId] : null;
68
-
69
- return (
70
- <tr key={modelId}>
71
- <td className="model-name">{modelId}</td>
72
- <td className="col-numeric">{stats.runs}</td>
73
- <td className="col-numeric">{formatTokens(stats.tokens_in)}</td>
74
- <td className="col-numeric">{formatTokens(stats.tokens_out)}</td>
75
- <td className="col-numeric">{stats.verdicts.OK}</td>
76
- <td className="col-numeric verdict-failed">{stats.verdicts.FAILED}</td>
77
- <td className="col-numeric verdict-empty">{stats.verdicts.EMPTY}</td>
78
- <td className="col-numeric verdict-hung">{stats.verdicts.HUNG}</td>
79
- {has_pricing && estimate && (
80
- <>
81
- <td className="col-numeric">{formatCurrency(estimate.input_cost)}</td>
82
- <td className="col-numeric">{formatCurrency(estimate.output_cost)}</td>
83
- <td className="col-numeric col-total">
84
- {formatCurrency(estimate.total_cost)}
85
- </td>
86
- </>
87
- )}
88
- </tr>
89
- );
90
- })}
91
- </tbody>
92
- </table>
93
- );
94
- }
@@ -1,68 +0,0 @@
1
- .events-feed {
2
- flex: 1;
3
- min-width: 0;
4
- padding: var(--space-5);
5
- background: var(--color-surface);
6
- border: 1px solid var(--color-border);
7
- border-radius: var(--radius-md);
8
- }
9
-
10
- .events-feed h2 {
11
- margin: 0 0 var(--space-4) 0;
12
- font-size: var(--font-size-lg);
13
- font-weight: var(--font-weight-bold);
14
- color: var(--color-text);
15
- }
16
-
17
- .events-feed__list {
18
- list-style: none;
19
- margin: 0;
20
- padding: 0;
21
- display: flex;
22
- flex-direction: column;
23
- gap: var(--space-2);
24
- max-height: 300px;
25
- overflow-y: auto;
26
- }
27
-
28
- .events-feed__item {
29
- padding: var(--space-2) var(--space-3);
30
- background: var(--color-bg-inset);
31
- border: 1px solid var(--color-border-subtle);
32
- border-radius: var(--radius-sm);
33
- font-size: var(--font-size-xs);
34
- line-height: var(--line-height-normal);
35
- color: var(--color-text-muted);
36
- }
37
-
38
- .events-feed__item code {
39
- font-family: var(--font-mono);
40
- color: var(--color-text);
41
- word-break: break-word;
42
- }
43
-
44
- .empty-state {
45
- padding: var(--space-4);
46
- text-align: center;
47
- color: var(--color-text-muted);
48
- font-size: var(--font-size-sm);
49
- }
50
-
51
- @media (max-width: 768px) {
52
- .events-feed {
53
- padding: var(--space-3);
54
- }
55
-
56
- .events-feed h2 {
57
- font-size: var(--font-size-md);
58
- }
59
-
60
- .events-feed__list {
61
- max-height: 200px;
62
- }
63
-
64
- .events-feed__item {
65
- padding: var(--space-2);
66
- font-size: var(--font-size-xs);
67
- }
68
- }
@@ -1,36 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { render, screen } from '@testing-library/react';
3
- import { EventsFeed } from './EventsFeed';
4
- import { fixtureEvents, TESTIDS } from '../test/fixtures';
5
-
6
- describe('EventsFeed', () => {
7
- it('renders events list', () => {
8
- render(<EventsFeed events={fixtureEvents} />);
9
-
10
- expect(screen.getByTestId(TESTIDS.eventsFeed)).toBeInTheDocument();
11
- fixtureEvents.forEach((event) => {
12
- expect(screen.getByText(event)).toBeInTheDocument();
13
- });
14
- });
15
-
16
- it('renders empty state when no events', () => {
17
- render(<EventsFeed events={[]} />);
18
-
19
- expect(screen.getByText('No events.')).toBeInTheDocument();
20
- });
21
-
22
- it('renders empty state when events is null', () => {
23
- render(<EventsFeed events={null} />);
24
-
25
- expect(screen.getByText('No events.')).toBeInTheDocument();
26
- });
27
-
28
- it('displays events in monospace font', () => {
29
- render(<EventsFeed events={fixtureEvents} />);
30
-
31
- const codeElements = screen.getAllByText(/BACKUP|SCAN|PUSH/);
32
- codeElements.forEach((el) => {
33
- expect(el.tagName).toBe('CODE');
34
- });
35
- });
36
- });
@@ -1,31 +0,0 @@
1
- /**
2
- * EventsFeed — FLEET-BACKUP.log tail (recent events).
3
- */
4
-
5
- import { TESTIDS } from '../test/fixtures';
6
- import './EventsFeed.css';
7
-
8
- interface EventsFeedProps {
9
- events: string[] | null;
10
- }
11
-
12
- export function EventsFeed({ events }: EventsFeedProps) {
13
- const hasEvents = events && events.length > 0;
14
-
15
- return (
16
- <section className="events-feed" data-testid={TESTIDS.eventsFeed}>
17
- <h2>Recent Events</h2>
18
- {!hasEvents ? (
19
- <p className="empty-state">No events.</p>
20
- ) : (
21
- <ul className="events-feed__list">
22
- {events.map((event, idx) => (
23
- <li key={idx} className="events-feed__item">
24
- <code>{event}</code>
25
- </li>
26
- ))}
27
- </ul>
28
- )}
29
- </section>
30
- );
31
- }
@@ -1,137 +0,0 @@
1
- .health-header {
2
- position: sticky;
3
- top: 0;
4
- z-index: 100;
5
- background: var(--color-surface);
6
- border-bottom: 1px solid var(--color-border);
7
- padding: var(--space-3) var(--space-5);
8
- }
9
-
10
- .health-header__cells {
11
- display: flex;
12
- flex-wrap: wrap;
13
- gap: var(--space-4);
14
- align-items: center;
15
- max-width: var(--content-max-width);
16
- margin: 0 auto;
17
- }
18
-
19
- .health-header__cell {
20
- display: flex;
21
- flex-direction: column;
22
- align-items: flex-start;
23
- gap: var(--space-2);
24
- padding: var(--space-2) var(--space-3);
25
- background: var(--color-bg-subtle);
26
- border: 1px solid var(--color-border-subtle);
27
- border-radius: var(--radius-md);
28
- font-size: var(--font-size-sm);
29
- cursor: pointer;
30
- transition: background-color var(--transition-fast), border-color var(--transition-fast),
31
- box-shadow var(--transition-fast);
32
- }
33
-
34
- .health-header__cell:hover {
35
- background: var(--color-bg-inset);
36
- border-color: var(--color-border);
37
- }
38
-
39
- .health-header__cell:focus-visible {
40
- outline: 2px solid var(--color-focus-ring);
41
- outline-offset: 2px;
42
- }
43
-
44
- .health-header__label {
45
- font-size: var(--font-size-xs);
46
- color: var(--color-text-muted);
47
- font-weight: var(--font-weight-medium);
48
- text-transform: uppercase;
49
- letter-spacing: 0.05em;
50
- }
51
-
52
- .health-header__status {
53
- font-family: var(--font-mono);
54
- font-size: var(--font-size-sm);
55
- font-weight: var(--font-weight-bold);
56
- color: var(--color-text);
57
- }
58
-
59
- .health-header__count {
60
- font-family: var(--font-mono);
61
- font-size: var(--font-size-lg);
62
- font-weight: var(--font-weight-bold);
63
- color: var(--color-accent);
64
- }
65
-
66
- .health-header__badge {
67
- display: inline-block;
68
- padding: var(--space-1) var(--space-2);
69
- background: var(--color-status-warn-bg);
70
- color: var(--color-status-warn);
71
- border-radius: var(--radius-sm);
72
- font-size: var(--font-size-xs);
73
- font-weight: var(--font-weight-bold);
74
- }
75
-
76
- .health-header__icon {
77
- font-size: var(--font-size-lg);
78
- line-height: 1;
79
- }
80
-
81
- .health-header__cell--sse {
82
- cursor: default;
83
- }
84
-
85
- .health-header__cell--sse:hover {
86
- background: var(--color-bg-subtle);
87
- border-color: var(--color-border-subtle);
88
- }
89
-
90
- .health-header__cell--timestamp {
91
- cursor: default;
92
- }
93
-
94
- .health-header__cell--timestamp:hover {
95
- background: var(--color-bg-subtle);
96
- border-color: var(--color-border-subtle);
97
- }
98
-
99
- .health-header__cell--stale {
100
- background: var(--color-status-warn-bg);
101
- border-color: var(--color-status-warn);
102
- }
103
-
104
- .health-header__cell--stale:hover {
105
- background: var(--color-status-warn-bg);
106
- border-color: var(--color-status-warn);
107
- }
108
-
109
- .health-header__cell--theme,
110
- .health-header__cell--refresh {
111
- margin-left: auto;
112
- }
113
-
114
- @media (max-width: 768px) {
115
- .health-header {
116
- padding: var(--space-2) var(--space-3);
117
- }
118
-
119
- .health-header__cells {
120
- gap: var(--space-3);
121
- }
122
-
123
- .health-header__cell {
124
- padding: var(--space-2);
125
- font-size: var(--font-size-xs);
126
- }
127
-
128
- .health-header__count {
129
- font-size: var(--font-size-md);
130
- }
131
- }
132
-
133
- @media (prefers-reduced-motion: reduce) {
134
- .health-header__cell {
135
- transition: none;
136
- }
137
- }