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,74 @@
1
+ // Hit testing, hover, click, zoom handlers for canvas
2
+
3
+ import type { LayoutNode, StageColumn } from "./layout";
4
+
5
+ export interface HoverState {
6
+ node: LayoutNode | null;
7
+ column: StageColumn | null;
8
+ }
9
+
10
+ export function hitTestNode(
11
+ nodes: LayoutNode[],
12
+ x: number,
13
+ y: number,
14
+ ): LayoutNode | null {
15
+ for (const node of nodes) {
16
+ const dx = x - node.x;
17
+ const dy = y - node.y;
18
+ if (dx * dx + dy * dy <= node.radius * node.radius) {
19
+ return node;
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+
25
+ export function hitTestColumn(
26
+ columns: StageColumn[],
27
+ x: number,
28
+ _y: number,
29
+ ): StageColumn | null {
30
+ for (const col of columns) {
31
+ if (x >= col.x && x <= col.x + col.width) {
32
+ return col;
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+
38
+ export class ZoomPan {
39
+ public scale = 1;
40
+ public offsetX = 0;
41
+ public offsetY = 0;
42
+ private minScale = 0.5;
43
+ private maxScale = 3;
44
+
45
+ zoom(delta: number, cx: number, cy: number): void {
46
+ const factor = delta > 0 ? 0.95 : 1.05;
47
+ const newScale = Math.max(
48
+ this.minScale,
49
+ Math.min(this.maxScale, this.scale * factor),
50
+ );
51
+ const ratio = newScale / this.scale;
52
+ this.offsetX = cx - (cx - this.offsetX) * ratio;
53
+ this.offsetY = cy - (cy - this.offsetY) * ratio;
54
+ this.scale = newScale;
55
+ }
56
+
57
+ screenToWorld(sx: number, sy: number): { x: number; y: number } {
58
+ return {
59
+ x: (sx - this.offsetX) / this.scale,
60
+ y: (sy - this.offsetY) / this.scale,
61
+ };
62
+ }
63
+
64
+ apply(ctx: CanvasRenderingContext2D): void {
65
+ ctx.translate(this.offsetX, this.offsetY);
66
+ ctx.scale(this.scale, this.scale);
67
+ }
68
+
69
+ reset(): void {
70
+ this.scale = 1;
71
+ this.offsetX = 0;
72
+ this.offsetY = 0;
73
+ }
74
+ }
@@ -0,0 +1,85 @@
1
+ // Stage-column layout algorithm for fleet topology map
2
+
3
+ import { STAGES, STAGE_HEX, type StageName } from "../design/tokens";
4
+ import { colors } from "../design/tokens";
5
+ import type { PipelineInfo } from "../types/api";
6
+
7
+ export interface LayoutNode {
8
+ issue: number;
9
+ title: string;
10
+ stage: string;
11
+ stageIndex: number;
12
+ x: number;
13
+ y: number;
14
+ targetX: number;
15
+ targetY: number;
16
+ radius: number;
17
+ color: string;
18
+ status: string;
19
+ progress: number; // 0-1 within current stage
20
+ velocity: number;
21
+ }
22
+
23
+ export interface StageColumn {
24
+ name: string;
25
+ x: number;
26
+ width: number;
27
+ color: string;
28
+ }
29
+
30
+ export function computeLayout(
31
+ pipelines: PipelineInfo[],
32
+ width: number,
33
+ height: number,
34
+ ): { nodes: LayoutNode[]; columns: StageColumn[] } {
35
+ const padding = 60;
36
+ const colWidth = (width - padding * 2) / STAGES.length;
37
+ const columns: StageColumn[] = STAGES.map((stage, i) => ({
38
+ name: stage === "compound_quality" ? "quality" : stage,
39
+ x: padding + i * colWidth,
40
+ width: colWidth,
41
+ color: STAGE_HEX[stage],
42
+ }));
43
+
44
+ const stageNodes: Record<string, PipelineInfo[]> = {};
45
+ for (const p of pipelines) {
46
+ const stage = p.stage || "intake";
47
+ if (!stageNodes[stage]) stageNodes[stage] = [];
48
+ stageNodes[stage].push(p);
49
+ }
50
+
51
+ const nodes: LayoutNode[] = [];
52
+ for (const p of pipelines) {
53
+ const stage = p.stage || "intake";
54
+ const stageIdx = STAGES.indexOf(stage as StageName);
55
+ const col = columns[stageIdx >= 0 ? stageIdx : 0];
56
+ const pipelinesInStage = stageNodes[stage] || [];
57
+ const indexInStage = pipelinesInStage.indexOf(p);
58
+ const spacing = Math.min(
59
+ 60,
60
+ (height - padding * 2) / (pipelinesInStage.length + 1),
61
+ );
62
+ const yOffset = padding + (indexInStage + 1) * spacing;
63
+
64
+ const nodeColor = p.status === "failed" ? colors.semantic.error : col.color;
65
+ const progress = (p.stagesDone?.length || 0) / STAGES.length;
66
+
67
+ nodes.push({
68
+ issue: p.issue,
69
+ title: p.title || "",
70
+ stage,
71
+ stageIndex: stageIdx >= 0 ? stageIdx : 0,
72
+ x: col.x + col.width / 2,
73
+ y: yOffset,
74
+ targetX: col.x + col.width / 2,
75
+ targetY: yOffset,
76
+ radius: 18,
77
+ color: nodeColor,
78
+ status: p.status || "active",
79
+ progress,
80
+ velocity: 0.5 + Math.random() * 0.5,
81
+ });
82
+ }
83
+
84
+ return { nodes, columns };
85
+ }
@@ -0,0 +1,117 @@
1
+ // Floating labels, tooltips, prediction overlays
2
+
3
+ import {
4
+ colors,
5
+ fonts,
6
+ typeScale,
7
+ radius as borderRadius,
8
+ } from "../design/tokens";
9
+ import { drawRoundRect, drawText } from "./renderer";
10
+ import { formatDuration } from "../core/helpers";
11
+ import type { LayoutNode } from "./layout";
12
+
13
+ export function drawTooltip(
14
+ ctx: CanvasRenderingContext2D,
15
+ node: LayoutNode,
16
+ predictions?: {
17
+ eta_s?: number;
18
+ success_probability?: number;
19
+ estimated_cost?: number;
20
+ },
21
+ ): void {
22
+ const padding = 12;
23
+ const lineHeight = 20;
24
+ const lines: string[] = [
25
+ `#${node.issue} ${node.title.substring(0, 40)}`,
26
+ `Stage: ${node.stage}`,
27
+ `Status: ${node.status}`,
28
+ ];
29
+
30
+ if (predictions) {
31
+ if (predictions.eta_s != null)
32
+ lines.push(`ETA: ${formatDuration(predictions.eta_s)}`);
33
+ if (predictions.success_probability != null)
34
+ lines.push(
35
+ `Success: ${(predictions.success_probability * 100).toFixed(0)}%`,
36
+ );
37
+ if (predictions.estimated_cost != null)
38
+ lines.push(`Est. cost: $${predictions.estimated_cost.toFixed(2)}`);
39
+ }
40
+
41
+ const style = typeScale.caption;
42
+ ctx.font = `${style.weight} ${style.size}px ${style.family}`;
43
+
44
+ let maxWidth = 0;
45
+ for (const line of lines) {
46
+ const w = ctx.measureText(line).width;
47
+ if (w > maxWidth) maxWidth = w;
48
+ }
49
+
50
+ const boxWidth = maxWidth + padding * 2;
51
+ const boxHeight = lines.length * lineHeight + padding * 2;
52
+ const x = node.x + node.radius + 10;
53
+ const y = node.y - boxHeight / 2;
54
+
55
+ // Background
56
+ ctx.fillStyle = colors.bg.ocean;
57
+ drawRoundRect(ctx, x, y, boxWidth, boxHeight, borderRadius.md);
58
+ ctx.fill();
59
+
60
+ // Border
61
+ ctx.strokeStyle = colors.accent.cyanDim;
62
+ ctx.lineWidth = 1;
63
+ drawRoundRect(ctx, x, y, boxWidth, boxHeight, borderRadius.md);
64
+ ctx.stroke();
65
+
66
+ // Text
67
+ for (let i = 0; i < lines.length; i++) {
68
+ drawText(ctx, lines[i], x + padding, y + padding + i * lineHeight, {
69
+ font: "caption",
70
+ color: i === 0 ? colors.text.primary : colors.text.secondary,
71
+ });
72
+ }
73
+ }
74
+
75
+ export function drawPredictionGhost(
76
+ ctx: CanvasRenderingContext2D,
77
+ fromX: number,
78
+ fromY: number,
79
+ toX: number,
80
+ toY: number,
81
+ progress: number,
82
+ color: string,
83
+ ): void {
84
+ const x = fromX + (toX - fromX) * progress;
85
+ const y = fromY + (toY - fromY) * progress;
86
+
87
+ ctx.globalAlpha = 0.3;
88
+ ctx.beginPath();
89
+ ctx.arc(x, y, 8, 0, Math.PI * 2);
90
+ ctx.fillStyle = color;
91
+ ctx.fill();
92
+
93
+ // Dashed line to destination
94
+ ctx.setLineDash([4, 4]);
95
+ ctx.strokeStyle = color;
96
+ ctx.lineWidth = 1;
97
+ ctx.beginPath();
98
+ ctx.moveTo(x, y);
99
+ ctx.lineTo(toX, toY);
100
+ ctx.stroke();
101
+ ctx.setLineDash([]);
102
+ ctx.globalAlpha = 1;
103
+ }
104
+
105
+ export function drawStageLabel(
106
+ ctx: CanvasRenderingContext2D,
107
+ text: string,
108
+ x: number,
109
+ y: number,
110
+ color: string,
111
+ ): void {
112
+ drawText(ctx, text.toUpperCase(), x, y, {
113
+ font: "monoSm",
114
+ color,
115
+ align: "center",
116
+ });
117
+ }
@@ -0,0 +1,105 @@
1
+ // Pipeline particle system - movement, trail effects, completion bursts
2
+
3
+ import { colors } from "../design/tokens";
4
+
5
+ export interface Particle {
6
+ x: number;
7
+ y: number;
8
+ vx: number;
9
+ vy: number;
10
+ life: number;
11
+ maxLife: number;
12
+ size: number;
13
+ color: string;
14
+ type: "trail" | "burst" | "ambient";
15
+ alpha: number;
16
+ }
17
+
18
+ export class ParticleSystem {
19
+ private particles: Particle[] = [];
20
+ private maxParticles = 500;
21
+
22
+ emit(
23
+ x: number,
24
+ y: number,
25
+ type: "trail" | "burst" | "ambient",
26
+ color: string,
27
+ count = 1,
28
+ ): void {
29
+ for (let i = 0; i < count; i++) {
30
+ if (this.particles.length >= this.maxParticles) break;
31
+
32
+ const angle = Math.random() * Math.PI * 2;
33
+ const speed =
34
+ type === "burst"
35
+ ? 30 + Math.random() * 60
36
+ : type === "trail"
37
+ ? 5 + Math.random() * 10
38
+ : 2 + Math.random() * 5;
39
+
40
+ this.particles.push({
41
+ x,
42
+ y,
43
+ vx: Math.cos(angle) * speed,
44
+ vy: Math.sin(angle) * speed,
45
+ life:
46
+ type === "burst"
47
+ ? 0.5 + Math.random() * 0.5
48
+ : type === "trail"
49
+ ? 0.3 + Math.random() * 0.3
50
+ : 2 + Math.random() * 3,
51
+ maxLife: type === "burst" ? 1 : type === "trail" ? 0.6 : 5,
52
+ size:
53
+ type === "burst"
54
+ ? 2 + Math.random() * 3
55
+ : type === "trail"
56
+ ? 1 + Math.random() * 2
57
+ : 1,
58
+ color,
59
+ type,
60
+ alpha: 1,
61
+ });
62
+ }
63
+ }
64
+
65
+ update(dt: number): void {
66
+ for (let i = this.particles.length - 1; i >= 0; i--) {
67
+ const p = this.particles[i];
68
+ p.x += p.vx * dt;
69
+ p.y += p.vy * dt;
70
+ p.life -= dt;
71
+ p.alpha = Math.max(0, p.life / p.maxLife);
72
+
73
+ // Slow down
74
+ p.vx *= 0.98;
75
+ p.vy *= 0.98;
76
+
77
+ if (p.life <= 0) {
78
+ this.particles.splice(i, 1);
79
+ }
80
+ }
81
+ }
82
+
83
+ draw(ctx: CanvasRenderingContext2D): void {
84
+ for (const p of this.particles) {
85
+ ctx.globalAlpha = p.alpha * 0.8;
86
+ ctx.beginPath();
87
+ ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
88
+ ctx.fillStyle = p.color;
89
+ ctx.fill();
90
+ }
91
+ ctx.globalAlpha = 1;
92
+ }
93
+
94
+ burstAt(x: number, y: number, color: string): void {
95
+ this.emit(x, y, "burst", color, 20);
96
+ }
97
+
98
+ clear(): void {
99
+ this.particles = [];
100
+ }
101
+
102
+ get count(): number {
103
+ return this.particles.length;
104
+ }
105
+ }
@@ -0,0 +1,191 @@
1
+ // Base Canvas2D renderer with requestAnimationFrame loop and dirty tracking
2
+
3
+ import { colors, fonts, typeScale } from "../design/tokens";
4
+
5
+ export interface CanvasScene {
6
+ update(dt: number): void;
7
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void;
8
+ onResize(width: number, height: number): void;
9
+ onMouseMove(x: number, y: number): void;
10
+ onMouseClick(x: number, y: number): void;
11
+ onMouseWheel(delta: number): void;
12
+ }
13
+
14
+ export class CanvasRenderer {
15
+ private canvas: HTMLCanvasElement;
16
+ private ctx: CanvasRenderingContext2D;
17
+ private scene: CanvasScene | null = null;
18
+ private animationId: number | null = null;
19
+ private lastTime = 0;
20
+ private dpr = 1;
21
+ private running = false;
22
+
23
+ constructor(container: HTMLElement) {
24
+ this.canvas = document.createElement("canvas");
25
+ this.canvas.style.width = "100%";
26
+ this.canvas.style.height = "100%";
27
+ this.canvas.style.display = "block";
28
+ container.appendChild(this.canvas);
29
+
30
+ this.ctx = this.canvas.getContext("2d")!;
31
+ this.dpr = window.devicePixelRatio || 1;
32
+
33
+ this.handleResize();
34
+ window.addEventListener("resize", () => this.handleResize());
35
+ this.canvas.addEventListener("mousemove", (e) => this.handleMouseMove(e));
36
+ this.canvas.addEventListener("click", (e) => this.handleClick(e));
37
+ this.canvas.addEventListener("wheel", (e) => this.handleWheel(e), {
38
+ passive: true,
39
+ });
40
+ }
41
+
42
+ setScene(scene: CanvasScene): void {
43
+ this.scene = scene;
44
+ scene.onResize(this.canvas.width / this.dpr, this.canvas.height / this.dpr);
45
+ }
46
+
47
+ start(): void {
48
+ if (this.running) return;
49
+ this.running = true;
50
+ this.lastTime = performance.now();
51
+ this.loop(this.lastTime);
52
+ }
53
+
54
+ stop(): void {
55
+ this.running = false;
56
+ if (this.animationId != null) {
57
+ cancelAnimationFrame(this.animationId);
58
+ this.animationId = null;
59
+ }
60
+ }
61
+
62
+ destroy(): void {
63
+ this.stop();
64
+ this.canvas.remove();
65
+ }
66
+
67
+ getCanvas(): HTMLCanvasElement {
68
+ return this.canvas;
69
+ }
70
+
71
+ private loop(time: number): void {
72
+ if (!this.running) return;
73
+ const dt = (time - this.lastTime) / 1000;
74
+ this.lastTime = time;
75
+
76
+ if (this.scene) {
77
+ this.scene.update(dt);
78
+ this.ctx.save();
79
+ this.ctx.scale(this.dpr, this.dpr);
80
+ this.ctx.clearRect(
81
+ 0,
82
+ 0,
83
+ this.canvas.width / this.dpr,
84
+ this.canvas.height / this.dpr,
85
+ );
86
+ this.scene.draw(
87
+ this.ctx,
88
+ this.canvas.width / this.dpr,
89
+ this.canvas.height / this.dpr,
90
+ );
91
+ this.ctx.restore();
92
+ }
93
+
94
+ this.animationId = requestAnimationFrame((t) => this.loop(t));
95
+ }
96
+
97
+ private handleResize(): void {
98
+ const rect = this.canvas.parentElement?.getBoundingClientRect();
99
+ if (!rect) return;
100
+ this.canvas.width = rect.width * this.dpr;
101
+ this.canvas.height = rect.height * this.dpr;
102
+ this.canvas.style.width = rect.width + "px";
103
+ this.canvas.style.height = rect.height + "px";
104
+ if (this.scene) this.scene.onResize(rect.width, rect.height);
105
+ }
106
+
107
+ private handleMouseMove(e: MouseEvent): void {
108
+ const rect = this.canvas.getBoundingClientRect();
109
+ if (this.scene)
110
+ this.scene.onMouseMove(e.clientX - rect.left, e.clientY - rect.top);
111
+ }
112
+
113
+ private handleClick(e: MouseEvent): void {
114
+ const rect = this.canvas.getBoundingClientRect();
115
+ if (this.scene)
116
+ this.scene.onMouseClick(e.clientX - rect.left, e.clientY - rect.top);
117
+ }
118
+
119
+ private handleWheel(e: WheelEvent): void {
120
+ if (this.scene) this.scene.onMouseWheel(e.deltaY);
121
+ }
122
+ }
123
+
124
+ // Canvas drawing helpers
125
+ export function drawText(
126
+ ctx: CanvasRenderingContext2D,
127
+ text: string,
128
+ x: number,
129
+ y: number,
130
+ options: {
131
+ font?: keyof typeof typeScale;
132
+ color?: string;
133
+ align?: CanvasTextAlign;
134
+ baseline?: CanvasTextBaseline;
135
+ maxWidth?: number;
136
+ } = {},
137
+ ): void {
138
+ const style = typeScale[options.font || "body"];
139
+ ctx.font = `${style.weight} ${style.size}px ${style.family}`;
140
+ ctx.fillStyle = options.color || colors.text.primary;
141
+ ctx.textAlign = options.align || "left";
142
+ ctx.textBaseline = options.baseline || "top";
143
+ if (options.maxWidth) {
144
+ ctx.fillText(text, x, y, options.maxWidth);
145
+ } else {
146
+ ctx.fillText(text, x, y);
147
+ }
148
+ }
149
+
150
+ export function drawRoundRect(
151
+ ctx: CanvasRenderingContext2D,
152
+ x: number,
153
+ y: number,
154
+ w: number,
155
+ h: number,
156
+ radius: number,
157
+ ): void {
158
+ ctx.beginPath();
159
+ ctx.moveTo(x + radius, y);
160
+ ctx.lineTo(x + w - radius, y);
161
+ ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
162
+ ctx.lineTo(x + w, y + h - radius);
163
+ ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
164
+ ctx.lineTo(x + radius, y + h);
165
+ ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
166
+ ctx.lineTo(x, y + radius);
167
+ ctx.quadraticCurveTo(x, y, x + radius, y);
168
+ ctx.closePath();
169
+ }
170
+
171
+ export function drawCircle(
172
+ ctx: CanvasRenderingContext2D,
173
+ x: number,
174
+ y: number,
175
+ r: number,
176
+ fill?: string,
177
+ stroke?: string,
178
+ lineWidth?: number,
179
+ ): void {
180
+ ctx.beginPath();
181
+ ctx.arc(x, y, r, 0, Math.PI * 2);
182
+ if (fill) {
183
+ ctx.fillStyle = fill;
184
+ ctx.fill();
185
+ }
186
+ if (stroke) {
187
+ ctx.strokeStyle = stroke;
188
+ ctx.lineWidth = lineWidth || 1;
189
+ ctx.stroke();
190
+ }
191
+ }
@@ -0,0 +1,54 @@
1
+ // SVG Bar Chart
2
+
3
+ import { escapeHtml } from "../../core/helpers";
4
+ import type { DailyCount } from "../../types/api";
5
+
6
+ export function renderSVGBarChart(dailyCounts: DailyCount[]): string {
7
+ if (!dailyCounts || dailyCounts.length === 0) return "";
8
+
9
+ const chartW = 700;
10
+ const chartH = 100;
11
+ const barGap = 4;
12
+ const barW = Math.max(
13
+ 8,
14
+ (chartW - (dailyCounts.length - 1) * barGap) / dailyCounts.length,
15
+ );
16
+
17
+ let maxCount = 0;
18
+ for (const day of dailyCounts) {
19
+ const total = (day.completed || 0) + (day.failed || 0);
20
+ if (total > maxCount) maxCount = total;
21
+ }
22
+ if (maxCount === 0) maxCount = 1;
23
+
24
+ let svg = `<svg class="svg-bar-chart" viewBox="0 0 ${chartW} ${chartH + 20}" width="100%" height="${chartH + 20}">`;
25
+
26
+ for (let i = 0; i < dailyCounts.length; i++) {
27
+ const day = dailyCounts[i];
28
+ const completed = day.completed || 0;
29
+ const failed = day.failed || 0;
30
+ const x = i * (barW + barGap);
31
+ const cH = (completed / maxCount) * chartH;
32
+ const fH = (failed / maxCount) * chartH;
33
+
34
+ if (cH > 0) {
35
+ svg += `<rect x="${x}" y="${chartH - cH - fH}" width="${barW}" height="${cH}" rx="3" fill="#4ade80" opacity="0.85"/>`;
36
+ }
37
+ if (fH > 0) {
38
+ svg += `<rect x="${x}" y="${chartH - fH}" width="${barW}" height="${fH}" rx="3" fill="#f43f5e" opacity="0.85"/>`;
39
+ }
40
+ if (cH === 0 && fH === 0) {
41
+ svg += `<rect x="${x}" y="${chartH - 1}" width="${barW}" height="1" fill="#0d1f3c"/>`;
42
+ }
43
+
44
+ const dateStr = day.date || "";
45
+ const parts = dateStr.split("-");
46
+ const label = parts.length >= 3 ? parts[1] + "/" + parts[2] : dateStr;
47
+ svg +=
48
+ `<text x="${x + barW / 2}" y="${chartH + 14}" text-anchor="middle" fill="#5a6d8a" ` +
49
+ `font-family="'JetBrains Mono', monospace" font-size="8">${escapeHtml(label)}</text>`;
50
+ }
51
+
52
+ svg += "</svg>";
53
+ return svg;
54
+ }
@@ -0,0 +1,25 @@
1
+ // SVG Donut Chart
2
+
3
+ export function renderSVGDonut(rate: number): string {
4
+ const size = 120;
5
+ const strokeW = 12;
6
+ const r = (size - strokeW) / 2;
7
+ const c = Math.PI * 2 * r;
8
+ const pct = Math.max(0, Math.min(100, rate));
9
+ const offset = c - (pct / 100) * c;
10
+
11
+ let svg = `<svg class="svg-donut" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">`;
12
+ svg +=
13
+ '<defs><linearGradient id="donut-grad" x1="0%" y1="0%" x2="100%" y2="100%">' +
14
+ '<stop offset="0%" stop-color="#00d4ff"/><stop offset="100%" stop-color="#7c3aed"/></linearGradient></defs>';
15
+ svg += `<circle cx="${size / 2}" cy="${size / 2}" r="${r}" fill="none" stroke="#0d1f3c" stroke-width="${strokeW}"/>`;
16
+ svg +=
17
+ `<circle cx="${size / 2}" cy="${size / 2}" r="${r}" fill="none" stroke="url(#donut-grad)" stroke-width="${strokeW}" ` +
18
+ `stroke-linecap="round" stroke-dasharray="${c}" stroke-dashoffset="${offset}" ` +
19
+ `transform="rotate(-90 ${size / 2} ${size / 2})" style="transition: stroke-dashoffset 0.8s ease"/>`;
20
+ svg +=
21
+ `<text x="${size / 2}" y="${size / 2 + 8}" text-anchor="middle" fill="#e8ecf4" ` +
22
+ `font-family="'Instrument Serif', serif" font-size="24">${pct.toFixed(1)}%</text>`;
23
+ svg += "</svg>";
24
+ return svg;
25
+ }