shipwright-cli 2.2.1 → 2.3.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 (156) hide show
  1. package/README.md +19 -19
  2. package/dashboard/public/index.html +224 -8
  3. package/dashboard/public/styles.css +1078 -4
  4. package/dashboard/server.ts +1100 -15
  5. package/dashboard/src/canvas/interactions.ts +74 -0
  6. package/dashboard/src/canvas/layout.ts +85 -0
  7. package/dashboard/src/canvas/overlays.ts +117 -0
  8. package/dashboard/src/canvas/particles.ts +105 -0
  9. package/dashboard/src/canvas/renderer.ts +191 -0
  10. package/dashboard/src/components/charts/bar.ts +54 -0
  11. package/dashboard/src/components/charts/donut.ts +25 -0
  12. package/dashboard/src/components/charts/pipeline-rail.ts +105 -0
  13. package/dashboard/src/components/charts/sparkline.ts +82 -0
  14. package/dashboard/src/components/header.ts +616 -0
  15. package/dashboard/src/components/modal.ts +413 -0
  16. package/dashboard/src/components/terminal.ts +144 -0
  17. package/dashboard/src/core/api.ts +381 -0
  18. package/dashboard/src/core/helpers.ts +118 -0
  19. package/dashboard/src/core/router.ts +190 -0
  20. package/dashboard/src/core/sse.ts +38 -0
  21. package/dashboard/src/core/state.ts +150 -0
  22. package/dashboard/src/core/ws.ts +143 -0
  23. package/dashboard/src/design/icons.ts +131 -0
  24. package/dashboard/src/design/tokens.ts +160 -0
  25. package/dashboard/src/main.ts +68 -0
  26. package/dashboard/src/types/api.ts +337 -0
  27. package/dashboard/src/views/activity.ts +185 -0
  28. package/dashboard/src/views/agent-cockpit.ts +236 -0
  29. package/dashboard/src/views/agents.ts +72 -0
  30. package/dashboard/src/views/fleet-map.ts +299 -0
  31. package/dashboard/src/views/insights.ts +298 -0
  32. package/dashboard/src/views/machines.ts +162 -0
  33. package/dashboard/src/views/metrics.ts +420 -0
  34. package/dashboard/src/views/overview.ts +409 -0
  35. package/dashboard/src/views/pipeline-theater.ts +219 -0
  36. package/dashboard/src/views/pipelines.ts +595 -0
  37. package/dashboard/src/views/team.ts +362 -0
  38. package/dashboard/src/views/timeline.ts +389 -0
  39. package/dashboard/tsconfig.json +21 -0
  40. package/docs/AGI-PLATFORM-PLAN.md +5 -5
  41. package/docs/AGI-WHATS-NEXT.md +19 -16
  42. package/docs/README.md +2 -0
  43. package/package.json +8 -1
  44. package/scripts/check-version-consistency.sh +72 -0
  45. package/scripts/lib/daemon-adaptive.sh +610 -0
  46. package/scripts/lib/daemon-dispatch.sh +489 -0
  47. package/scripts/lib/daemon-failure.sh +387 -0
  48. package/scripts/lib/daemon-patrol.sh +1113 -0
  49. package/scripts/lib/daemon-poll.sh +1202 -0
  50. package/scripts/lib/daemon-state.sh +550 -0
  51. package/scripts/lib/daemon-triage.sh +490 -0
  52. package/scripts/lib/helpers.sh +81 -0
  53. package/scripts/lib/pipeline-intelligence.sh +0 -6
  54. package/scripts/lib/pipeline-quality-checks.sh +3 -1
  55. package/scripts/lib/pipeline-stages.sh +20 -0
  56. package/scripts/sw +109 -168
  57. package/scripts/sw-activity.sh +1 -1
  58. package/scripts/sw-adaptive.sh +2 -2
  59. package/scripts/sw-adversarial.sh +1 -1
  60. package/scripts/sw-architecture-enforcer.sh +1 -1
  61. package/scripts/sw-auth.sh +14 -6
  62. package/scripts/sw-autonomous.sh +1 -1
  63. package/scripts/sw-changelog.sh +2 -2
  64. package/scripts/sw-checkpoint.sh +1 -1
  65. package/scripts/sw-ci.sh +1 -1
  66. package/scripts/sw-cleanup.sh +1 -1
  67. package/scripts/sw-code-review.sh +1 -1
  68. package/scripts/sw-connect.sh +1 -1
  69. package/scripts/sw-context.sh +1 -1
  70. package/scripts/sw-cost.sh +1 -1
  71. package/scripts/sw-daemon.sh +53 -4817
  72. package/scripts/sw-dashboard.sh +1 -1
  73. package/scripts/sw-db.sh +1 -1
  74. package/scripts/sw-decompose.sh +1 -1
  75. package/scripts/sw-deps.sh +1 -1
  76. package/scripts/sw-developer-simulation.sh +1 -1
  77. package/scripts/sw-discovery.sh +1 -1
  78. package/scripts/sw-doc-fleet.sh +1 -1
  79. package/scripts/sw-docs-agent.sh +1 -1
  80. package/scripts/sw-docs.sh +1 -1
  81. package/scripts/sw-doctor.sh +49 -1
  82. package/scripts/sw-dora.sh +1 -1
  83. package/scripts/sw-durable.sh +1 -1
  84. package/scripts/sw-e2e-orchestrator.sh +1 -1
  85. package/scripts/sw-eventbus.sh +1 -1
  86. package/scripts/sw-feedback.sh +1 -1
  87. package/scripts/sw-fix.sh +6 -5
  88. package/scripts/sw-fleet-discover.sh +1 -1
  89. package/scripts/sw-fleet-viz.sh +3 -3
  90. package/scripts/sw-fleet.sh +1 -1
  91. package/scripts/sw-github-app.sh +5 -2
  92. package/scripts/sw-github-checks.sh +1 -1
  93. package/scripts/sw-github-deploy.sh +1 -1
  94. package/scripts/sw-github-graphql.sh +1 -1
  95. package/scripts/sw-guild.sh +1 -1
  96. package/scripts/sw-heartbeat.sh +1 -1
  97. package/scripts/sw-hygiene.sh +1 -1
  98. package/scripts/sw-incident.sh +1 -1
  99. package/scripts/sw-init.sh +112 -9
  100. package/scripts/sw-instrument.sh +6 -1
  101. package/scripts/sw-intelligence.sh +5 -1
  102. package/scripts/sw-jira.sh +1 -1
  103. package/scripts/sw-launchd.sh +1 -1
  104. package/scripts/sw-linear.sh +20 -9
  105. package/scripts/sw-logs.sh +1 -1
  106. package/scripts/sw-loop.sh +2 -1
  107. package/scripts/sw-memory.sh +10 -1
  108. package/scripts/sw-mission-control.sh +1 -1
  109. package/scripts/sw-model-router.sh +4 -1
  110. package/scripts/sw-otel.sh +4 -4
  111. package/scripts/sw-oversight.sh +1 -1
  112. package/scripts/sw-pipeline-composer.sh +3 -1
  113. package/scripts/sw-pipeline-vitals.sh +4 -6
  114. package/scripts/sw-pipeline.sh +19 -56
  115. package/scripts/sw-pipeline.sh.mock +7 -0
  116. package/scripts/sw-pm.sh +5 -2
  117. package/scripts/sw-pr-lifecycle.sh +1 -1
  118. package/scripts/sw-predictive.sh +4 -1
  119. package/scripts/sw-prep.sh +3 -2
  120. package/scripts/sw-ps.sh +1 -1
  121. package/scripts/sw-public-dashboard.sh +10 -4
  122. package/scripts/sw-quality.sh +1 -1
  123. package/scripts/sw-reaper.sh +1 -1
  124. package/scripts/sw-recruit.sh +25 -1
  125. package/scripts/sw-regression.sh +2 -1
  126. package/scripts/sw-release-manager.sh +1 -1
  127. package/scripts/sw-release.sh +7 -5
  128. package/scripts/sw-remote.sh +1 -1
  129. package/scripts/sw-replay.sh +1 -1
  130. package/scripts/sw-retro.sh +1 -1
  131. package/scripts/sw-scale.sh +11 -5
  132. package/scripts/sw-security-audit.sh +1 -1
  133. package/scripts/sw-self-optimize.sh +172 -7
  134. package/scripts/sw-session.sh +1 -1
  135. package/scripts/sw-setup.sh +1 -1
  136. package/scripts/sw-standup.sh +4 -3
  137. package/scripts/sw-status.sh +1 -1
  138. package/scripts/sw-strategic.sh +2 -1
  139. package/scripts/sw-stream.sh +8 -2
  140. package/scripts/sw-swarm.sh +12 -10
  141. package/scripts/sw-team-stages.sh +1 -1
  142. package/scripts/sw-templates.sh +1 -1
  143. package/scripts/sw-testgen.sh +3 -2
  144. package/scripts/sw-tmux-pipeline.sh +2 -1
  145. package/scripts/sw-tmux.sh +1 -1
  146. package/scripts/sw-trace.sh +1 -1
  147. package/scripts/sw-tracker-jira.sh +1 -0
  148. package/scripts/sw-tracker-linear.sh +1 -0
  149. package/scripts/sw-tracker.sh +24 -6
  150. package/scripts/sw-triage.sh +1 -1
  151. package/scripts/sw-upgrade.sh +1 -1
  152. package/scripts/sw-ux.sh +1 -1
  153. package/scripts/sw-webhook.sh +1 -1
  154. package/scripts/sw-widgets.sh +2 -2
  155. package/scripts/sw-worktree.sh +1 -1
  156. package/dashboard/public/app.js +0 -4422
@@ -0,0 +1,68 @@
1
+ // Fleet Command Dashboard - Main Entry Point
2
+ // Boots WebSocket, router, header, modals, and registers all views
3
+
4
+ import { connect } from "./core/ws";
5
+ import { store } from "./core/state";
6
+ import { setupRouter, registerView } from "./core/router";
7
+ import {
8
+ setupHeader,
9
+ renderCostTicker,
10
+ renderAlertBanner,
11
+ updateEmergencyBrakeVisibility,
12
+ updateAmbientIndicator,
13
+ detectCompletions,
14
+ } from "./components/header";
15
+ import { setupInterventionModal, setupBulkActions } from "./components/modal";
16
+
17
+ // Views
18
+ import { overviewView } from "./views/overview";
19
+ import { agentsView } from "./views/agents";
20
+ import { pipelinesView } from "./views/pipelines";
21
+ import { timelineView } from "./views/timeline";
22
+ import { activityView } from "./views/activity";
23
+ import { metricsView } from "./views/metrics";
24
+ import { machinesView } from "./views/machines";
25
+ import { insightsView } from "./views/insights";
26
+ import { teamView } from "./views/team";
27
+
28
+ // New visualization views (lazy-loaded when tabs exist)
29
+ import { fleetMapView } from "./views/fleet-map";
30
+ import { pipelineTheaterView } from "./views/pipeline-theater";
31
+ import { agentCockpitView } from "./views/agent-cockpit";
32
+
33
+ // Register all views
34
+ registerView("overview", overviewView);
35
+ registerView("agents", agentsView);
36
+ registerView("pipelines", pipelinesView);
37
+ registerView("timeline", timelineView);
38
+ registerView("activity", activityView);
39
+ registerView("metrics", metricsView);
40
+ registerView("machines", machinesView);
41
+ registerView("insights", insightsView);
42
+ registerView("team", teamView);
43
+ registerView("fleet-map", fleetMapView);
44
+ registerView("pipeline-theater", pipelineTheaterView);
45
+ registerView("agent-cockpit", agentCockpitView);
46
+
47
+ // Setup header (user menu, daemon control, emergency brake)
48
+ setupHeader();
49
+
50
+ // Setup shared modals
51
+ setupInterventionModal();
52
+ setupBulkActions();
53
+
54
+ // Setup tab routing
55
+ setupRouter();
56
+
57
+ // Subscribe to fleet state for global UI updates
58
+ store.subscribe("fleetState", (data) => {
59
+ if (!data) return;
60
+ renderCostTicker(data);
61
+ renderAlertBanner(data);
62
+ updateEmergencyBrakeVisibility(data);
63
+ updateAmbientIndicator(data);
64
+ detectCompletions(data);
65
+ });
66
+
67
+ // Connect WebSocket
68
+ connect();
@@ -0,0 +1,337 @@
1
+ // Shared TypeScript interfaces for all API responses
2
+ // These types mirror the actual shapes returned by dashboard/server.ts
3
+
4
+ export interface DaemonInfo {
5
+ running: boolean;
6
+ pid: number | null;
7
+ uptime_s: number;
8
+ maxParallel: number;
9
+ pollInterval: number;
10
+ }
11
+
12
+ export interface PipelineInfo {
13
+ issue: number;
14
+ title: string;
15
+ stage: string;
16
+ stagesDone: string[];
17
+ elapsed_s: number;
18
+ worktree?: string;
19
+ iteration: number;
20
+ maxIterations: number;
21
+ linesWritten?: number;
22
+ testsPassing?: boolean;
23
+ cost?: number;
24
+ branch?: string;
25
+ status?: string;
26
+ }
27
+
28
+ export interface QueueItem {
29
+ issue: number;
30
+ title: string;
31
+ score?: number;
32
+ estimated_cost?: number;
33
+ factors?: ScoringFactors;
34
+ }
35
+
36
+ export interface ScoringFactors {
37
+ complexity?: number;
38
+ impact?: number;
39
+ priority?: number;
40
+ age?: number;
41
+ dependency?: number;
42
+ memory?: number;
43
+ }
44
+
45
+ export interface EventItem {
46
+ ts?: string;
47
+ timestamp?: string;
48
+ type: string;
49
+ issue?: number;
50
+ issueTitle?: string;
51
+ title?: string;
52
+ duration_s?: number;
53
+ stage?: string;
54
+ result?: string;
55
+ [key: string]: unknown;
56
+ }
57
+
58
+ export interface ScaleInfo {
59
+ from?: number;
60
+ to?: number;
61
+ cpuCores?: number;
62
+ maxByCpu?: number;
63
+ maxByMem?: number;
64
+ maxByBudget?: number;
65
+ availMemGb?: number;
66
+ }
67
+
68
+ export interface MetricsSummary {
69
+ completed?: number;
70
+ failed?: number;
71
+ cpuCores?: number;
72
+ }
73
+
74
+ export interface CostInfo {
75
+ today_spent: number;
76
+ daily_budget: number;
77
+ pct_used: number;
78
+ }
79
+
80
+ export interface DoraMetric {
81
+ value: number;
82
+ unit: string;
83
+ grade: string;
84
+ }
85
+
86
+ export interface DoraGrades {
87
+ deploy_freq: DoraMetric;
88
+ lead_time: DoraMetric;
89
+ cfr: DoraMetric;
90
+ mttr: DoraMetric;
91
+ }
92
+
93
+ export interface AgentInfo {
94
+ id: string;
95
+ issue: number;
96
+ title: string;
97
+ machine: string;
98
+ stage: string;
99
+ iteration: number;
100
+ activity: string;
101
+ memory_mb: number;
102
+ cpu_pct: number;
103
+ status: "active" | "idle" | "stale" | "dead";
104
+ heartbeat_age_s: number;
105
+ started_at: string;
106
+ elapsed_s: number;
107
+ }
108
+
109
+ export interface FleetState {
110
+ timestamp: string;
111
+ daemon: DaemonInfo;
112
+ pipelines: PipelineInfo[];
113
+ queue: QueueItem[];
114
+ events: EventItem[];
115
+ scale: ScaleInfo;
116
+ metrics: MetricsSummary;
117
+ agents: AgentInfo[];
118
+ machines: MachineInfo[];
119
+ cost: CostInfo;
120
+ dora: DoraGrades;
121
+ team?: TeamData;
122
+ }
123
+
124
+ export interface CostBreakdown {
125
+ by_model?: Record<string, number>;
126
+ by_stage?: Record<string, number>;
127
+ by_issue?: Array<{ issue: number; cost: number }>;
128
+ budget?: number;
129
+ spent?: number;
130
+ }
131
+
132
+ export interface StageHistoryEntry {
133
+ stage: string;
134
+ duration_s: number;
135
+ ts: string;
136
+ }
137
+
138
+ export interface PipelineDetail {
139
+ issue: number;
140
+ title: string;
141
+ stage: string;
142
+ stageHistory: StageHistoryEntry[];
143
+ plan: string;
144
+ design: string;
145
+ dod: string;
146
+ intake: Record<string, unknown> | null;
147
+ elapsed_s: number;
148
+ branch: string;
149
+ prLink: string;
150
+ }
151
+
152
+ export interface TimelineEntry {
153
+ issue: number;
154
+ title: string;
155
+ segments: TimelineSegment[];
156
+ }
157
+
158
+ export interface TimelineSegment {
159
+ stage: string;
160
+ start: string;
161
+ end: string | null;
162
+ status: "complete" | "running" | "failed";
163
+ }
164
+
165
+ export interface MetricsData {
166
+ success_rate: number;
167
+ avg_duration_s: number;
168
+ throughput_per_hour: number;
169
+ total_completed: number;
170
+ total_failed: number;
171
+ stage_durations: Record<string, number>;
172
+ daily_counts: DailyCount[];
173
+ dora_grades: DoraGrades;
174
+ }
175
+
176
+ export interface DailyCount {
177
+ date: string;
178
+ completed: number;
179
+ failed: number;
180
+ }
181
+
182
+ export interface DoraMetrics {
183
+ deploy_freq?: DoraMetric;
184
+ lead_time?: DoraMetric;
185
+ cfr?: DoraMetric;
186
+ mttr?: DoraMetric;
187
+ }
188
+
189
+ export interface MachineInfo {
190
+ name: string;
191
+ host: string;
192
+ role: string;
193
+ max_workers: number;
194
+ active_workers: number;
195
+ registered_at: string;
196
+ ssh_user?: string;
197
+ shipwright_path?: string;
198
+ status: "online" | "degraded" | "offline";
199
+ health: MachineHealth;
200
+ join_token?: string;
201
+ }
202
+
203
+ export interface MachineHealth {
204
+ daemon_running: boolean;
205
+ heartbeat_count: number;
206
+ last_heartbeat_s_ago: number;
207
+ }
208
+
209
+ export interface JoinToken {
210
+ label: string;
211
+ created_at?: string;
212
+ used?: boolean;
213
+ token?: string;
214
+ }
215
+
216
+ export interface InsightsData {
217
+ patterns: FailurePattern[] | null;
218
+ decisions: Decision[] | null;
219
+ patrol: PatrolFinding[] | null;
220
+ heatmap: HeatmapData | null;
221
+ globalLearnings: Array<Record<string, unknown>> | null;
222
+ }
223
+
224
+ export interface FailurePattern {
225
+ description?: string;
226
+ pattern?: string;
227
+ frequency?: number;
228
+ count?: number;
229
+ root_cause?: string;
230
+ fix?: string;
231
+ suggested_fix?: string;
232
+ }
233
+
234
+ export interface Decision {
235
+ timestamp?: string;
236
+ ts?: string;
237
+ action?: string;
238
+ decision?: string;
239
+ outcome?: string;
240
+ result?: string;
241
+ issue?: number;
242
+ }
243
+
244
+ export interface PatrolFinding {
245
+ severity?: string;
246
+ type?: string;
247
+ category?: string;
248
+ description?: string;
249
+ message?: string;
250
+ file?: string;
251
+ }
252
+
253
+ // Heatmap from server: { heatmap: Record<stage, Record<date, count>> }
254
+ export interface HeatmapData {
255
+ heatmap: Record<string, Record<string, number>>;
256
+ }
257
+
258
+ export interface DaemonConfig {
259
+ paused?: boolean;
260
+ config?: Record<string, unknown>;
261
+ budget?: Record<string, unknown>;
262
+ }
263
+
264
+ export interface AlertInfo {
265
+ severity: string;
266
+ message: string;
267
+ type?: string;
268
+ issue?: number;
269
+ actions?: string[];
270
+ }
271
+
272
+ export interface TeamData {
273
+ total_online?: number;
274
+ total_active_pipelines?: number;
275
+ total_queued?: number;
276
+ developers?: TeamDeveloper[];
277
+ }
278
+
279
+ export interface TeamDeveloper {
280
+ developer_id: string;
281
+ machine_name: string;
282
+ hostname?: string;
283
+ platform?: string;
284
+ last_heartbeat?: number;
285
+ daemon_running: boolean;
286
+ daemon_pid?: number | null;
287
+ active_jobs: Array<{ issue: number; title?: string; stage?: string }>;
288
+ queued: number[];
289
+ events_since?: number;
290
+ _presence?: string;
291
+ }
292
+
293
+ export interface TeamActivityEvent {
294
+ ts?: string;
295
+ type: string;
296
+ issue?: number;
297
+ from_developer?: string;
298
+ stage?: string;
299
+ result?: string;
300
+ }
301
+
302
+ export interface StagePerformance {
303
+ name?: string;
304
+ stage?: string;
305
+ avg_s: number;
306
+ min_s?: number;
307
+ max_s?: number;
308
+ count: number;
309
+ trend_pct?: number;
310
+ }
311
+
312
+ export interface UserInfo {
313
+ username?: string;
314
+ avatarUrl?: string;
315
+ isAdmin?: boolean;
316
+ role?: "viewer" | "operator" | "admin";
317
+ }
318
+
319
+ export type TabId =
320
+ | "overview"
321
+ | "agents"
322
+ | "pipelines"
323
+ | "timeline"
324
+ | "activity"
325
+ | "metrics"
326
+ | "machines"
327
+ | "insights"
328
+ | "team"
329
+ | "fleet-map"
330
+ | "pipeline-theater"
331
+ | "agent-cockpit";
332
+
333
+ export interface View {
334
+ init(): void;
335
+ render(state: FleetState): void;
336
+ destroy(): void;
337
+ }
@@ -0,0 +1,185 @@
1
+ // Activity tab - filterable event feed with pagination
2
+
3
+ import { store } from "../core/state";
4
+ import {
5
+ escapeHtml,
6
+ formatDuration,
7
+ formatTime,
8
+ getBadgeClass,
9
+ getTypeShort,
10
+ } from "../core/helpers";
11
+ import { icon } from "../design/icons";
12
+ import * as api from "../core/api";
13
+ import type { FleetState, View } from "../types/api";
14
+
15
+ function setupActivityFilters(): void {
16
+ const chips = document.querySelectorAll("#activity-filters .filter-chip");
17
+ chips.forEach((chip) => {
18
+ chip.addEventListener("click", () => {
19
+ store.set("activityFilter", chip.getAttribute("data-filter") || "all");
20
+ const siblings = document.querySelectorAll(
21
+ "#activity-filters .filter-chip",
22
+ );
23
+ siblings.forEach((s) => s.classList.remove("active"));
24
+ chip.classList.add("active");
25
+ renderActivityTimeline();
26
+ });
27
+ });
28
+
29
+ const issueFilter = document.getElementById(
30
+ "activity-issue-filter",
31
+ ) as HTMLInputElement;
32
+ if (issueFilter) {
33
+ issueFilter.addEventListener("input", () => {
34
+ store.set(
35
+ "activityIssueFilter",
36
+ issueFilter.value.replace(/[^0-9]/g, ""),
37
+ );
38
+ renderActivityTimeline();
39
+ });
40
+ }
41
+
42
+ const loadMoreBtn = document.getElementById("load-more-btn");
43
+ if (loadMoreBtn) loadMoreBtn.addEventListener("click", loadMoreActivity);
44
+ }
45
+
46
+ function loadActivity(): void {
47
+ store.update({ activityOffset: 0, activityEvents: [] });
48
+
49
+ api
50
+ .fetchActivity({ limit: 50, offset: 0 })
51
+ .then((result) => {
52
+ store.update({
53
+ activityEvents: result.events || [],
54
+ activityHasMore: result.hasMore || false,
55
+ activityOffset: (result.events || []).length,
56
+ });
57
+ renderActivityTimeline();
58
+ })
59
+ .catch((err) => {
60
+ const el = document.getElementById("activity-timeline");
61
+ if (el)
62
+ el.innerHTML = `<div class="empty-state"><p>Failed to load: ${escapeHtml(String(err))}</p></div>`;
63
+ });
64
+ }
65
+
66
+ function loadMoreActivity(): void {
67
+ const btn = document.getElementById("load-more-btn") as HTMLButtonElement;
68
+ if (btn) {
69
+ btn.disabled = true;
70
+ btn.textContent = "Loading...";
71
+ }
72
+
73
+ const offset = store.get("activityOffset");
74
+ api
75
+ .fetchActivity({ limit: 50, offset })
76
+ .then((result) => {
77
+ const existing = store.get("activityEvents");
78
+ const newEvents = result.events || [];
79
+ store.update({
80
+ activityEvents: [...existing, ...newEvents],
81
+ activityHasMore: result.hasMore || false,
82
+ activityOffset: existing.length + newEvents.length,
83
+ });
84
+ renderActivityTimeline();
85
+ if (btn) {
86
+ btn.disabled = false;
87
+ btn.textContent = "Load more";
88
+ }
89
+ })
90
+ .catch(() => {
91
+ if (btn) {
92
+ btn.disabled = false;
93
+ btn.textContent = "Load more";
94
+ }
95
+ });
96
+ }
97
+
98
+ function renderActivityTimeline(): void {
99
+ const container = document.getElementById("activity-timeline");
100
+ const loadMoreWrap = document.getElementById("activity-load-more");
101
+ if (!container) return;
102
+
103
+ const activityEvents = store.get("activityEvents");
104
+ const activityFilter = store.get("activityFilter");
105
+ const activityIssueFilter = store.get("activityIssueFilter");
106
+ const activityHasMore = store.get("activityHasMore");
107
+
108
+ const filtered = activityEvents.filter((ev) => {
109
+ const typeRaw = String(ev.type || "");
110
+ const badge = getBadgeClass(typeRaw);
111
+ if (
112
+ activityFilter !== "all" &&
113
+ badge !== activityFilter &&
114
+ !typeRaw.includes(activityFilter)
115
+ )
116
+ return false;
117
+ if (activityIssueFilter && String(ev.issue || "") !== activityIssueFilter)
118
+ return false;
119
+ return true;
120
+ });
121
+
122
+ if (filtered.length === 0) {
123
+ container.innerHTML =
124
+ '<div class="empty-state"><p>No matching events</p></div>';
125
+ if (loadMoreWrap)
126
+ loadMoreWrap.style.display = activityHasMore ? "" : "none";
127
+ return;
128
+ }
129
+
130
+ let html = "";
131
+ for (const ev of filtered) {
132
+ const typeRaw = String(ev.type || "unknown");
133
+ const typeShort = getTypeShort(typeRaw);
134
+ const badgeClass = getBadgeClass(typeRaw);
135
+
136
+ let detail = "";
137
+ if (ev.stage) detail += "stage=" + ev.stage + " ";
138
+ if (ev.issueTitle) detail += ev.issueTitle;
139
+ else if (ev.title) detail += ev.title;
140
+ detail = detail.trim();
141
+
142
+ if (!detail) {
143
+ const skip: Record<string, boolean> = {
144
+ ts: true,
145
+ type: true,
146
+ timestamp: true,
147
+ issue: true,
148
+ stage: true,
149
+ duration_s: true,
150
+ issueTitle: true,
151
+ title: true,
152
+ };
153
+ const dparts: string[] = [];
154
+ for (const [key, val] of Object.entries(ev)) {
155
+ if (!skip[key]) dparts.push(key + "=" + val);
156
+ }
157
+ detail = dparts.join(" ");
158
+ }
159
+
160
+ html +=
161
+ `<div class="timeline-row">` +
162
+ `<span class="timeline-ts">${formatTime(String(ev.ts || ev.timestamp || ""))}</span>` +
163
+ `<span class="activity-badge ${badgeClass}">${escapeHtml(typeShort)}</span>` +
164
+ (ev.issue ? `<span class="timeline-issue">#${ev.issue}</span>` : "") +
165
+ `<span class="timeline-detail">${escapeHtml(detail)}</span>` +
166
+ (ev.duration_s != null
167
+ ? `<span class="timeline-duration">${formatDuration(Number(ev.duration_s))}</span>`
168
+ : "") +
169
+ `</div>`;
170
+ }
171
+
172
+ container.innerHTML = html;
173
+ if (loadMoreWrap) loadMoreWrap.style.display = activityHasMore ? "" : "none";
174
+ }
175
+
176
+ export const activityView: View = {
177
+ init() {
178
+ setupActivityFilters();
179
+ if (store.get("activityEvents").length === 0) loadActivity();
180
+ },
181
+ render(_data: FleetState) {
182
+ renderActivityTimeline();
183
+ },
184
+ destroy() {},
185
+ };