@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,230 +0,0 @@
1
- /**
2
- * TypeScript types for Aesop UI API contracts.
3
- * These types are imported by U4–U7 components and must remain stable across the wave.
4
- */
5
-
6
- export interface HeartbeatStatus {
7
- alive: 'ALIVE' | 'STALE' | 'UNKNOWN' | 'unknown' | 'not running';
8
- age: number; // seconds, bucketed to 3-second intervals; -1 if unknown
9
- threshold: number; // seconds until considered stale
10
- }
11
-
12
- /**
13
- * One fleet agent — the JSON emitted by dash/dash-extra.mjs --json
14
- * (served via GET /api/agents and the `agents` SSE section; also embedded
15
- * as `agents` inside GET /data).
16
- * `status`: 'running' | 'idle' age-derived, overridden by security-log
17
- * severities 'SUSPICIOUS' | 'HIGH' | 'DRIFT' | 'MED'.
18
- * ui/agents.py de-dupes colliding 13-char ids by suffixing "-2", "-3", ...
19
- */
20
- export interface Agent {
21
- id: string;
22
- project: string;
23
- status: 'running' | 'idle' | 'SUSPICIOUS' | 'HIGH' | 'DRIFT' | 'MED' | string;
24
- age_s: number; // seconds since transcript mtime
25
- hint: string; // label, capped at 60 chars
26
- startedAt: string | null; // ISO 8601 transcript timestamp
27
- lastActivity: string | null; // ISO 8601 transcript timestamp
28
- runtimeSeconds?: number;
29
- tokensUsed?: number;
30
- taskLabel: string; // first prompt line, capped at 80 chars
31
- promptFull?: string;
32
- }
33
-
34
- /**
35
- * GET /agent?id=<id> — dispatch prompt + metadata
36
- * (ui/agents.py extract_agent_dispatch_prompt).
37
- * Error responses are {error: string} with 400 (invalid id) or 404 (no transcript).
38
- */
39
- export interface AgentDetail {
40
- id: string;
41
- dispatch_prompt: string;
42
- dispatcher: 'main thread' | 'parent agent';
43
- model: string; // "unknown" when not found in transcript
44
- message_count: number;
45
- first_seen: number; // epoch seconds (file mtime)
46
- last_activity: number; // epoch seconds (file mtime)
47
- }
48
-
49
- /** GET /api/session — CSRF token for same-origin JS (U2 adds this). */
50
- export interface SessionResponse {
51
- token: string;
52
- }
53
-
54
- /** Uniform error payload shape for non-2xx JSON responses. */
55
- export interface ApiError {
56
- error: string;
57
- }
58
-
59
- export interface Alert {
60
- count: number;
61
- lines: string[];
62
- }
63
-
64
- export interface TrackerItem {
65
- id: string;
66
- title: string;
67
- priority: 'P0' | 'P1' | 'P2';
68
- status: 'todo' | 'done' | 'in-progress' | 'archived';
69
- lane: 'proposed' | 'ranked' | 'in-progress' | 'done';
70
- source: string;
71
- tags: string[];
72
- notes: string | null;
73
- pr_link: string | null;
74
- created_at: string; // ISO 8601
75
- completed_at: string | null;
76
- }
77
-
78
- /**
79
- * The `tracker` SSE section and the tracker slice of GET /api/state.
80
- * NOTE: GET /api/tracker returns a BARE TrackerItem[] array (no wrapper).
81
- */
82
- export interface TrackerSnapshot {
83
- items: TrackerItem[];
84
- }
85
-
86
- export interface AuditBacklogItem {
87
- status: '✅' | '🔵' | '⬜' | '⏸';
88
- tag: string; // e.g., "[sec]"
89
- title: string;
90
- }
91
-
92
- export interface AuditBacklogTier {
93
- tier: 'P0' | 'P1' | 'P2' | 'Needs decision';
94
- items: AuditBacklogItem[];
95
- done: number;
96
- inflight: number;
97
- todo: number;
98
- total: number;
99
- }
100
-
101
- export interface AuditBacklog {
102
- tiers: AuditBacklogTier[];
103
- }
104
-
105
- export interface Message {
106
- role: 'user' | 'assistant';
107
- text: string;
108
- timestamp: string; // ISO 8601
109
- }
110
-
111
- export interface OrchestratorEntry {
112
- id?: string;
113
- role?: string;
114
- activity?: string;
115
- phase?: string;
116
- age_seconds: number;
117
- stale: boolean;
118
- updated_at?: string;
119
- }
120
-
121
- export interface OrchestratorStatus {
122
- orchestrators: OrchestratorEntry[];
123
- }
124
-
125
- /**
126
- * Repo status entry from .watchdog-repos.json (list passthrough, or
127
- * {repo, state} pairs when the file holds an object).
128
- */
129
- export interface RepoStatus {
130
- repo?: string;
131
- state?: unknown;
132
- [key: string]: unknown;
133
- }
134
-
135
- /**
136
- * GET /data response AND the `data` SSE section.
137
- * Note: `agents` is present on GET /data only; the SSE `data` section
138
- * (collectors._snapshot_data) omits it — agents arrive on their own
139
- * `agents` SSE section.
140
- */
141
- export interface DashboardData {
142
- watchdog: HeartbeatStatus;
143
- monitor: HeartbeatStatus;
144
- agents?: Agent[];
145
- repos: RepoStatus[];
146
- events: string[];
147
- alerts: Alert;
148
- messages: Message[];
149
- }
150
-
151
- /** POST /submit success response. */
152
- export interface SubmitResponse {
153
- ok: boolean;
154
- }
155
-
156
- export interface FullState {
157
- data: DashboardData;
158
- backlog: AuditBacklog;
159
- agents: Agent[];
160
- tracker: TrackerSnapshot;
161
- status: OrchestratorStatus;
162
- cost?: CostSummary;
163
- }
164
-
165
- /**
166
- * Cost summary from GET /api/cost.
167
- * Mirrors ui/cost.py get_cost_summary() docstring on branch
168
- * feat/wave14-u3-cost-collector — verbatim shape, NOT provisional.
169
- */
170
- export interface CostModelStats {
171
- runs: number;
172
- tokens_in: number;
173
- tokens_out: number;
174
- verdicts: {
175
- OK: number;
176
- FAILED: number;
177
- EMPTY: number;
178
- HUNG: number;
179
- };
180
- }
181
-
182
- export interface CostDailyTotal {
183
- tokens_in: number;
184
- tokens_out: number;
185
- }
186
-
187
- export interface CostOverallScorecard {
188
- total_runs: number;
189
- ok_count: number;
190
- failed_count: number;
191
- empty_count: number;
192
- hung_count: number;
193
- ok_rate: number; // 0.0-1.0
194
- failed_rate: number;
195
- empty_rate: number;
196
- hung_rate: number;
197
- }
198
-
199
- export interface CostEstimate {
200
- input_cost: number; // dollars
201
- output_cost: number; // dollars
202
- total_cost: number; // dollars
203
- }
204
-
205
- export interface CostSummary {
206
- models: Record<string, CostModelStats>; // keyed by model id
207
- daily_totals: Record<string, CostDailyTotal>; // keyed by "YYYY-MM-DD"
208
- overall_scorecard: CostOverallScorecard;
209
- skipped_lines: number;
210
- has_pricing: boolean;
211
- estimates_by_model: Record<string, CostEstimate>; // empty when has_pricing is false
212
- }
213
-
214
- /**
215
- * SSE event sections emitted by GET /events.
216
- * Initial sections: data, backlog, agents, tracker, status
217
- * Added in U3: cost
218
- */
219
- export type SSESection =
220
- | 'data'
221
- | 'backlog'
222
- | 'agents'
223
- | 'tracker'
224
- | 'status'
225
- | 'cost';
226
-
227
- export interface SSEConnectionStatus {
228
- status: 'live' | 'reconnecting' | 'error';
229
- lastError?: string;
230
- }
@@ -1,60 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import { renderHook, act } from '@testing-library/react';
3
- import { useHashRoute, normalizeHash, ROUTES } from './useHashRoute';
4
-
5
- function setHash(hash: string) {
6
- window.location.hash = hash;
7
- window.dispatchEvent(new HashChangeEvent('hashchange'));
8
- }
9
-
10
- afterEach(() => {
11
- window.location.hash = '';
12
- });
13
-
14
- describe('normalizeHash', () => {
15
- it('passes through known routes', () => {
16
- for (const r of ROUTES) {
17
- expect(normalizeHash(r)).toBe(r);
18
- }
19
- });
20
-
21
- it('normalizes empty hash to #/', () => {
22
- expect(normalizeHash('')).toBe('#/');
23
- });
24
-
25
- it('normalizes unknown hashes to #/', () => {
26
- expect(normalizeHash('#/bogus')).toBe('#/');
27
- expect(normalizeHash('#work')).toBe('#/');
28
- });
29
- });
30
-
31
- describe('useHashRoute', () => {
32
- it('returns #/ initially with no hash', () => {
33
- const { result } = renderHook(() => useHashRoute());
34
- expect(result.current).toBe('#/');
35
- });
36
-
37
- it('follows hashchange events', () => {
38
- const { result } = renderHook(() => useHashRoute());
39
- act(() => setHash('#/work'));
40
- expect(result.current).toBe('#/work');
41
- act(() => setHash('#/cost'));
42
- expect(result.current).toBe('#/cost');
43
- act(() => setHash('#/activity'));
44
- expect(result.current).toBe('#/activity');
45
- });
46
-
47
- it('normalizes unknown hash changes to #/', () => {
48
- const { result } = renderHook(() => useHashRoute());
49
- act(() => setHash('#/work'));
50
- expect(result.current).toBe('#/work');
51
- act(() => setHash('#/nonsense'));
52
- expect(result.current).toBe('#/');
53
- });
54
-
55
- it('reads the initial hash on mount', () => {
56
- window.location.hash = '#/cost';
57
- const { result } = renderHook(() => useHashRoute());
58
- expect(result.current).toBe('#/cost');
59
- });
60
- });
@@ -1,23 +0,0 @@
1
- /**
2
- * useHashRoute — ~10-line hash-tab routing hook (deliberately NOT react-router; see plan D1).
3
- * Routes: '#/' (overview), '#/work', '#/activity', '#/cost'.
4
- * Unknown/empty hashes normalize to '#/'.
5
- */
6
- import { useEffect, useState } from 'react';
7
-
8
- export const ROUTES = ['#/', '#/work', '#/activity', '#/cost'] as const;
9
- export type Route = (typeof ROUTES)[number];
10
-
11
- export function normalizeHash(hash: string): Route {
12
- return (ROUTES as readonly string[]).includes(hash) ? (hash as Route) : '#/';
13
- }
14
-
15
- export function useHashRoute(): Route {
16
- const [route, setRoute] = useState<Route>(() => normalizeHash(window.location.hash));
17
- useEffect(() => {
18
- const onHashChange = () => setRoute(normalizeHash(window.location.hash));
19
- window.addEventListener('hashchange', onHashChange);
20
- return () => window.removeEventListener('hashchange', onHashChange);
21
- }, []);
22
- return route;
23
- }
@@ -1,175 +0,0 @@
1
- /**
2
- * useSSE hook — EventSource wrapper with reconnect backoff, per-section state, and connection status.
3
- *
4
- * Usage:
5
- * const { data, backlog, agents, tracker, status, cost, connectionStatus } = useSSE();
6
- *
7
- * Emits on sections: data, backlog, agents, tracker, status, cost
8
- * Handles connection errors and automatic reconnect with exponential backoff (1s → 10s max).
9
- */
10
-
11
- import { useEffect, useRef, useState, useCallback } from 'react';
12
- import type { DashboardData, AuditBacklog, Agent, TrackerSnapshot, OrchestratorStatus, CostSummary, SSEConnectionStatus } from './types';
13
-
14
- export interface SSEState {
15
- data: DashboardData | null;
16
- backlog: AuditBacklog | null;
17
- agents: Agent[] | null;
18
- tracker: TrackerSnapshot | null;
19
- status: OrchestratorStatus | null;
20
- cost: CostSummary | null;
21
- connectionStatus: SSEConnectionStatus;
22
- }
23
-
24
- const initialState: SSEState = {
25
- data: null,
26
- backlog: null,
27
- agents: null,
28
- tracker: null,
29
- status: null,
30
- cost: null,
31
- connectionStatus: { status: 'reconnecting' },
32
- };
33
-
34
- export function useSSE() {
35
- const [state, setState] = useState<SSEState>(initialState);
36
- const eventSourceRef = useRef<EventSource | null>(null);
37
- const reconnectAttemptRef = useRef(0);
38
- const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
39
-
40
- const getReconnectDelay = useCallback(() => {
41
- const baseDelay = 1000; // 1 second
42
- const maxDelay = 10000; // 10 seconds
43
- const delay = Math.min(baseDelay * Math.pow(2, reconnectAttemptRef.current), maxDelay);
44
- return delay;
45
- }, []);
46
-
47
- const connect = useCallback(() => {
48
- if (eventSourceRef.current) {
49
- return; // Already connected or connecting
50
- }
51
-
52
- const eventSource = new EventSource('/events');
53
-
54
- eventSource.addEventListener('data', (e) => {
55
- try {
56
- const payload = JSON.parse(e.data);
57
- setState((prev) => ({
58
- ...prev,
59
- data: payload,
60
- connectionStatus: { status: 'live' },
61
- }));
62
- reconnectAttemptRef.current = 0;
63
- } catch (err) {
64
- console.error('Failed to parse SSE data:', err);
65
- }
66
- });
67
-
68
- eventSource.addEventListener('backlog', (e) => {
69
- try {
70
- const payload = JSON.parse(e.data);
71
- setState((prev) => ({
72
- ...prev,
73
- backlog: payload,
74
- connectionStatus: { status: 'live' },
75
- }));
76
- reconnectAttemptRef.current = 0;
77
- } catch (err) {
78
- console.error('Failed to parse SSE backlog:', err);
79
- }
80
- });
81
-
82
- eventSource.addEventListener('agents', (e) => {
83
- try {
84
- const payload = JSON.parse(e.data);
85
- setState((prev) => ({
86
- ...prev,
87
- agents: Array.isArray(payload) ? payload : [],
88
- connectionStatus: { status: 'live' },
89
- }));
90
- reconnectAttemptRef.current = 0;
91
- } catch (err) {
92
- console.error('Failed to parse SSE agents:', err);
93
- }
94
- });
95
-
96
- eventSource.addEventListener('tracker', (e) => {
97
- try {
98
- const payload = JSON.parse(e.data);
99
- setState((prev) => ({
100
- ...prev,
101
- tracker: payload,
102
- connectionStatus: { status: 'live' },
103
- }));
104
- reconnectAttemptRef.current = 0;
105
- } catch (err) {
106
- console.error('Failed to parse SSE tracker:', err);
107
- }
108
- });
109
-
110
- eventSource.addEventListener('status', (e) => {
111
- try {
112
- const payload = JSON.parse(e.data);
113
- setState((prev) => ({
114
- ...prev,
115
- status: payload,
116
- connectionStatus: { status: 'live' },
117
- }));
118
- reconnectAttemptRef.current = 0;
119
- } catch (err) {
120
- console.error('Failed to parse SSE status:', err);
121
- }
122
- });
123
-
124
- eventSource.addEventListener('cost', (e) => {
125
- try {
126
- const payload = JSON.parse(e.data);
127
- setState((prev) => ({
128
- ...prev,
129
- cost: payload,
130
- connectionStatus: { status: 'live' },
131
- }));
132
- reconnectAttemptRef.current = 0;
133
- } catch (err) {
134
- console.error('Failed to parse SSE cost:', err);
135
- }
136
- });
137
-
138
- eventSource.addEventListener('error', (err) => {
139
- console.error('EventSource error:', err);
140
- eventSource.close();
141
- eventSourceRef.current = null;
142
- setState((prev) => ({
143
- ...prev,
144
- connectionStatus: {
145
- status: 'reconnecting',
146
- lastError: 'Connection lost',
147
- },
148
- }));
149
- reconnectAttemptRef.current += 1;
150
- const delay = getReconnectDelay();
151
- reconnectTimeoutRef.current = setTimeout(connect, delay);
152
- });
153
-
154
- eventSourceRef.current = eventSource;
155
- setState((prev) => ({
156
- ...prev,
157
- connectionStatus: { status: 'reconnecting' },
158
- }));
159
- }, [getReconnectDelay]);
160
-
161
- useEffect(() => {
162
- connect();
163
-
164
- return () => {
165
- if (reconnectTimeoutRef.current) {
166
- clearTimeout(reconnectTimeoutRef.current);
167
- }
168
- if (eventSourceRef.current) {
169
- eventSourceRef.current.close();
170
- }
171
- };
172
- }, [connect]);
173
-
174
- return state;
175
- }
@@ -1,10 +0,0 @@
1
- import React from 'react';
2
- import ReactDOM from 'react-dom/client';
3
- import App from './App';
4
- import './styles/global.css';
5
-
6
- ReactDOM.createRoot(document.getElementById('root')!).render(
7
- <React.StrictMode>
8
- <App />
9
- </React.StrictMode>,
10
- );
@@ -1,179 +0,0 @@
1
- /**
2
- * Aesop global styles — reset, focus-visible rings, sr-only, reduced motion,
3
- * app-shell layout. Component-specific styles belong with components (U4–U7);
4
- * only shared shell + utility styles live here.
5
- */
6
-
7
- @import './theme.css';
8
-
9
- /* ---- Reset ---- */
10
- *,
11
- *::before,
12
- *::after {
13
- box-sizing: border-box;
14
- margin: 0;
15
- padding: 0;
16
- }
17
-
18
- html {
19
- -webkit-text-size-adjust: 100%;
20
- }
21
-
22
- body {
23
- font-family: var(--font-sans);
24
- font-size: var(--font-size-md);
25
- line-height: var(--line-height-normal);
26
- color: var(--color-text);
27
- background: var(--color-bg);
28
- }
29
-
30
- img,
31
- svg {
32
- max-width: 100%;
33
- display: block;
34
- }
35
-
36
- button,
37
- input,
38
- select,
39
- textarea {
40
- font: inherit;
41
- color: inherit;
42
- }
43
-
44
- a {
45
- color: var(--color-accent);
46
- }
47
-
48
- /* ---- Focus-visible rings (D5: visible focus, never removed) ---- */
49
- :focus {
50
- outline: none;
51
- }
52
-
53
- :focus-visible {
54
- outline: 2px solid var(--color-focus-ring);
55
- outline-offset: 2px;
56
- border-radius: var(--radius-sm);
57
- }
58
-
59
- /* ---- Screen-reader-only utility ---- */
60
- .sr-only {
61
- position: absolute;
62
- width: 1px;
63
- height: 1px;
64
- padding: 0;
65
- margin: -1px;
66
- overflow: hidden;
67
- clip: rect(0, 0, 0, 0);
68
- white-space: nowrap;
69
- border: 0;
70
- }
71
-
72
- /* ---- Reduced motion (D5) ---- */
73
- @media (prefers-reduced-motion: reduce) {
74
- *,
75
- *::before,
76
- *::after {
77
- animation-duration: 0.01ms !important;
78
- animation-iteration-count: 1 !important;
79
- transition-duration: 0.01ms !important;
80
- scroll-behavior: auto !important;
81
- }
82
- }
83
-
84
- /* ---- App shell ---- */
85
- .app-header {
86
- position: sticky;
87
- top: 0;
88
- z-index: 100;
89
- display: flex;
90
- align-items: center;
91
- gap: var(--space-3);
92
- min-height: var(--header-height);
93
- padding: 0 var(--space-4);
94
- background: var(--color-bg-subtle);
95
- border-bottom: 1px solid var(--color-border);
96
- }
97
-
98
- .app-title {
99
- font-size: var(--font-size-md);
100
- font-weight: var(--font-weight-bold);
101
- white-space: nowrap;
102
- }
103
-
104
- .app-nav {
105
- display: flex;
106
- gap: var(--space-1);
107
- }
108
-
109
- .app-nav a {
110
- display: inline-block;
111
- padding: var(--space-2) var(--space-3);
112
- color: var(--color-text-muted);
113
- text-decoration: none;
114
- font-size: var(--font-size-sm);
115
- font-weight: var(--font-weight-medium);
116
- border-bottom: 2px solid transparent;
117
- }
118
-
119
- .app-nav a[aria-current='page'] {
120
- color: var(--color-text);
121
- border-bottom-color: var(--color-accent);
122
- }
123
-
124
- .app-nav a:hover {
125
- color: var(--color-text);
126
- }
127
-
128
- .app-main {
129
- max-width: var(--content-max-width);
130
- margin: 0 auto;
131
- padding: var(--space-4);
132
- }
133
-
134
- .view-placeholder {
135
- padding: var(--space-6);
136
- border: 1px dashed var(--color-border);
137
- border-radius: var(--radius-md);
138
- color: var(--color-text-muted);
139
- text-align: center;
140
- }
141
-
142
- .theme-toggle {
143
- margin-left: auto;
144
- padding: var(--space-1) var(--space-2);
145
- background: transparent;
146
- border: 1px solid var(--color-border);
147
- border-radius: var(--radius-sm);
148
- cursor: pointer;
149
- font-size: var(--font-size-sm);
150
- color: var(--color-text-muted);
151
- }
152
-
153
- .theme-toggle:hover {
154
- color: var(--color-text);
155
- background: var(--color-bg-inset);
156
- }
157
-
158
- .sse-status {
159
- font-size: var(--font-size-xs);
160
- font-weight: var(--font-weight-medium);
161
- padding: var(--space-1) var(--space-2);
162
- border-radius: var(--radius-sm);
163
- white-space: nowrap;
164
- }
165
-
166
- .sse-status[data-status='live'] {
167
- color: var(--color-status-ok);
168
- background: var(--color-status-ok-bg);
169
- }
170
-
171
- .sse-status[data-status='reconnecting'] {
172
- color: var(--color-status-warn);
173
- background: var(--color-status-warn-bg);
174
- }
175
-
176
- .sse-status[data-status='error'] {
177
- color: var(--color-status-error);
178
- background: var(--color-status-error-bg);
179
- }