pi-mega-compact 0.14.0 → 0.15.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 (91) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +6 -0
  2. package/dist/extensions/dashboard-server/routes-cache.js +17 -5
  3. package/dist/extensions/dashboard-server/routes-game.js +60 -0
  4. package/dist/extensions/dashboard-server/routes.js +1 -1
  5. package/dist/extensions/dashboard-server/server.js +3 -1
  6. package/dist/extensions/mega-runtime/capture-model.js +15 -2
  7. package/dist/extensions/mega-runtime/runtime-snapshot.js +26 -0
  8. package/dist/extensions/mega-runtime/runtime.js +1 -0
  9. package/extensions/dashboard-client/dist/assets/AreaChart-rJdaOUlj.js +2 -0
  10. package/extensions/dashboard-client/dist/assets/AreaChart-rJdaOUlj.js.map +1 -0
  11. package/extensions/dashboard-client/dist/assets/BarChart-DOfc8me9.js +2 -0
  12. package/extensions/dashboard-client/dist/assets/BarChart-DOfc8me9.js.map +1 -0
  13. package/extensions/dashboard-client/dist/assets/{CacheTab-CDyZqMO-.js → CacheTab-EhqkEglo.js} +2 -2
  14. package/extensions/dashboard-client/dist/assets/{CacheTab-CDyZqMO-.js.map → CacheTab-EhqkEglo.js.map} +1 -1
  15. package/extensions/dashboard-client/dist/assets/{EventsTab-ByPYMJVG.js → EventsTab-BUACwWXf.js} +2 -2
  16. package/extensions/dashboard-client/dist/assets/{EventsTab-ByPYMJVG.js.map → EventsTab-BUACwWXf.js.map} +1 -1
  17. package/extensions/dashboard-client/dist/assets/{HealthTab-CeHQR0py.js → HealthTab-CDEYVRBb.js} +2 -2
  18. package/extensions/dashboard-client/dist/assets/{HealthTab-CeHQR0py.js.map → HealthTab-CDEYVRBb.js.map} +1 -1
  19. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-ceTD_c1A.js → MaintenanceTab-DtanIeCQ.js} +2 -2
  20. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-ceTD_c1A.js.map → MaintenanceTab-DtanIeCQ.js.map} +1 -1
  21. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CRUdVR7u.js → MemoryMapTab-BGEhL7OG.js} +2 -2
  22. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CRUdVR7u.js.map → MemoryMapTab-BGEhL7OG.js.map} +1 -1
  23. package/extensions/dashboard-client/dist/assets/MetricsTab-Cktozn4s.js +2 -0
  24. package/extensions/dashboard-client/dist/assets/MetricsTab-Cktozn4s.js.map +1 -0
  25. package/extensions/dashboard-client/dist/assets/OverviewTab-08v6Ayuf.js +16 -0
  26. package/extensions/dashboard-client/dist/assets/OverviewTab-08v6Ayuf.js.map +1 -0
  27. package/extensions/dashboard-client/dist/assets/{ReposTab-DUrP7Efa.js → ReposTab-Bg72Ozdc.js} +2 -2
  28. package/extensions/dashboard-client/dist/assets/{ReposTab-DUrP7Efa.js.map → ReposTab-Bg72Ozdc.js.map} +1 -1
  29. package/extensions/dashboard-client/dist/assets/SessionsTab-BcTK9Lg6.js +2 -0
  30. package/extensions/dashboard-client/dist/assets/{SessionsTab-Cg-IsijD.js.map → SessionsTab-BcTK9Lg6.js.map} +1 -1
  31. package/extensions/dashboard-client/dist/assets/{SetupTab-ZE1daXjN.js → SetupTab-Bt-Ru1xp.js} +2 -2
  32. package/extensions/dashboard-client/dist/assets/{SetupTab-ZE1daXjN.js.map → SetupTab-Bt-Ru1xp.js.map} +1 -1
  33. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BkTgi3Af.js → TimeSavedCard-ic_LjJ79.js} +2 -2
  34. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BkTgi3Af.js.map → TimeSavedCard-ic_LjJ79.js.map} +1 -1
  35. package/extensions/dashboard-client/dist/assets/{TurnsTab-CZvudHQF.js → TurnsTab-D9AZTMgG.js} +4 -4
  36. package/extensions/dashboard-client/dist/assets/{TurnsTab-CZvudHQF.js.map → TurnsTab-D9AZTMgG.js.map} +1 -1
  37. package/extensions/dashboard-client/dist/assets/WikiTab-hRcJPi_m.js +2 -0
  38. package/extensions/dashboard-client/dist/assets/{WikiTab-BVAnGp8_.js.map → WikiTab-hRcJPi_m.js.map} +1 -1
  39. package/extensions/dashboard-client/dist/assets/{button-CUrzhi2A.js → button-BdAtIbVu.js} +2 -2
  40. package/extensions/dashboard-client/dist/assets/{button-CUrzhi2A.js.map → button-BdAtIbVu.js.map} +1 -1
  41. package/extensions/dashboard-client/dist/assets/{card-BsfojHfp.js → card-BFYc_QrK.js} +2 -2
  42. package/extensions/dashboard-client/dist/assets/{card-BsfojHfp.js.map → card-BFYc_QrK.js.map} +1 -1
  43. package/extensions/dashboard-client/dist/assets/generateCategoricalChart-BwzyqytT.js +70 -0
  44. package/extensions/dashboard-client/dist/assets/generateCategoricalChart-BwzyqytT.js.map +1 -0
  45. package/extensions/dashboard-client/dist/assets/{index-OM3w-l2T.js → index-pmtEr3sb.js} +8 -8
  46. package/extensions/dashboard-client/dist/assets/index-pmtEr3sb.js.map +1 -0
  47. package/extensions/dashboard-client/dist/assets/index-za_gmEmQ.css +1 -0
  48. package/extensions/dashboard-client/dist/assets/{switch-qSz2Un3t.js → switch-DEyGlFwJ.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{switch-qSz2Un3t.js.map → switch-DEyGlFwJ.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{toggle-DyBKcLe-.js → toggle-pDMBQYQj.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{toggle-DyBKcLe-.js.map → toggle-pDMBQYQj.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{useSSE-DqVouqww.js → useSSE-BKaC4dao.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{useSSE-DqVouqww.js.map → useSSE-BKaC4dao.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/index.html +2 -2
  55. package/extensions/dashboard-client/package-lock.json +56 -0
  56. package/extensions/dashboard-client/package.json +3 -0
  57. package/extensions/dashboard-client/src/api/client.ts +11 -0
  58. package/extensions/dashboard-client/src/components/SessionContextGauges.tsx +14 -8
  59. package/extensions/dashboard-client/src/components/WidgetDetailModal.tsx +69 -0
  60. package/extensions/dashboard-client/src/components/charts/PerfBarChart.tsx +89 -0
  61. package/extensions/dashboard-client/src/components/charts/PerfLineChart.tsx +108 -0
  62. package/extensions/dashboard-client/src/components/layout/AppShell.tsx +1 -1
  63. package/extensions/dashboard-client/src/components/ui/SortableCard.tsx +75 -0
  64. package/extensions/dashboard-client/src/hooks/useCardPositions.ts +74 -0
  65. package/extensions/dashboard-client/src/styles/base.css +2 -4
  66. package/extensions/dashboard-client/src/styles/session-gauges.css +28 -15
  67. package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +310 -79
  68. package/extensions/dashboard-client/src/types/card.ts +40 -0
  69. package/extensions/dashboard-client/tailwind.config.js +1 -1
  70. package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +10 -0
  71. package/extensions/dashboard-server/api-contracts/endpoints/types.ts +36 -0
  72. package/extensions/dashboard-server/api-contracts/endpoints.ts +3 -0
  73. package/extensions/dashboard-server/api-contracts/index.ts +3 -0
  74. package/extensions/dashboard-server/routes-cache.ts +17 -5
  75. package/extensions/dashboard-server/routes-game.ts +72 -0
  76. package/extensions/dashboard-server/routes.ts +1 -0
  77. package/extensions/dashboard-server/server.ts +2 -0
  78. package/extensions/mega-runtime/capture-model.ts +14 -2
  79. package/extensions/mega-runtime/runtime-snapshot.ts +40 -0
  80. package/extensions/mega-runtime/runtime.ts +1 -0
  81. package/package.json +1 -1
  82. package/extensions/dashboard-client/dist/assets/AreaChart-C68tGr2J.js +0 -70
  83. package/extensions/dashboard-client/dist/assets/AreaChart-C68tGr2J.js.map +0 -1
  84. package/extensions/dashboard-client/dist/assets/MetricsTab-DP7Kmdu9.js +0 -2
  85. package/extensions/dashboard-client/dist/assets/MetricsTab-DP7Kmdu9.js.map +0 -1
  86. package/extensions/dashboard-client/dist/assets/OverviewTab-4CGP0djv.js +0 -2
  87. package/extensions/dashboard-client/dist/assets/OverviewTab-4CGP0djv.js.map +0 -1
  88. package/extensions/dashboard-client/dist/assets/SessionsTab-Cg-IsijD.js +0 -2
  89. package/extensions/dashboard-client/dist/assets/WikiTab-BVAnGp8_.js +0 -2
  90. package/extensions/dashboard-client/dist/assets/index-CUA6EDRF.css +0 -1
  91. package/extensions/dashboard-client/dist/assets/index-OM3w-l2T.js.map +0 -1
@@ -8,6 +8,9 @@
8
8
  "name": "pi-mega-compact-dashboard-client",
9
9
  "version": "0.8.9",
10
10
  "dependencies": {
11
+ "@dnd-kit/core": "^6.3.1",
12
+ "@dnd-kit/sortable": "^10.0.0",
13
+ "@dnd-kit/utilities": "^3.2.2",
11
14
  "@radix-ui/react-select": "^2.1.0",
12
15
  "@radix-ui/react-switch": "^1.1.0",
13
16
  "@radix-ui/react-tabs": "^1.1.0",
@@ -335,6 +338,59 @@
335
338
  "node": ">=6.9.0"
336
339
  }
337
340
  },
341
+ "node_modules/@dnd-kit/accessibility": {
342
+ "version": "3.1.1",
343
+ "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
344
+ "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
345
+ "license": "MIT",
346
+ "dependencies": {
347
+ "tslib": "^2.0.0"
348
+ },
349
+ "peerDependencies": {
350
+ "react": ">=16.8.0"
351
+ }
352
+ },
353
+ "node_modules/@dnd-kit/core": {
354
+ "version": "6.3.1",
355
+ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
356
+ "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
357
+ "license": "MIT",
358
+ "dependencies": {
359
+ "@dnd-kit/accessibility": "^3.1.1",
360
+ "@dnd-kit/utilities": "^3.2.2",
361
+ "tslib": "^2.0.0"
362
+ },
363
+ "peerDependencies": {
364
+ "react": ">=16.8.0",
365
+ "react-dom": ">=16.8.0"
366
+ }
367
+ },
368
+ "node_modules/@dnd-kit/sortable": {
369
+ "version": "10.0.0",
370
+ "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
371
+ "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
372
+ "license": "MIT",
373
+ "dependencies": {
374
+ "@dnd-kit/utilities": "^3.2.2",
375
+ "tslib": "^2.0.0"
376
+ },
377
+ "peerDependencies": {
378
+ "@dnd-kit/core": "^6.3.0",
379
+ "react": ">=16.8.0"
380
+ }
381
+ },
382
+ "node_modules/@dnd-kit/utilities": {
383
+ "version": "3.2.2",
384
+ "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
385
+ "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
386
+ "license": "MIT",
387
+ "dependencies": {
388
+ "tslib": "^2.0.0"
389
+ },
390
+ "peerDependencies": {
391
+ "react": ">=16.8.0"
392
+ }
393
+ },
338
394
  "node_modules/@esbuild/aix-ppc64": {
339
395
  "version": "0.21.5",
340
396
  "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -11,6 +11,9 @@
11
11
  "typecheck": "tsc --noEmit"
12
12
  },
13
13
  "dependencies": {
14
+ "@dnd-kit/core": "^6.3.1",
15
+ "@dnd-kit/sortable": "^10.0.0",
16
+ "@dnd-kit/utilities": "^3.2.2",
14
17
  "@radix-ui/react-select": "^2.1.0",
15
18
  "@radix-ui/react-switch": "^1.1.0",
16
19
  "@radix-ui/react-tabs": "^1.1.0",
@@ -25,6 +25,7 @@ import type {
25
25
  GameScoresQuery,
26
26
  PerfResponse,
27
27
  PerfQuery,
28
+ PerfSamplesResponse,
28
29
  AchievementRow,
29
30
  SessionsResponse,
30
31
  SessionTimeseriesResponse,
@@ -192,6 +193,16 @@ export function fetchPerf(params: PerfQuery = {}): Promise<PerfResponse> {
192
193
  );
193
194
  }
194
195
 
196
+ /** GET /api/perf/samples — raw perf samples for one kind (chart drill-down). */
197
+ export function fetchPerfSamples(
198
+ kind: string,
199
+ minutes?: number,
200
+ ): Promise<PerfSamplesResponse> {
201
+ return getJson<PerfSamplesResponse>(
202
+ `${ENDPOINTS.perfSamples.path}${query({ kind, minutes })}`,
203
+ );
204
+ }
205
+
195
206
  export function fetchAchievements(): Promise<AchievementRow[]> {
196
207
  return getJson<AchievementRow[]>(ENDPOINTS.achievements.path);
197
208
  }
@@ -78,7 +78,7 @@ interface GaugeData {
78
78
  isSelf: boolean;
79
79
  }
80
80
 
81
- /** A single compact gauge: header label, fill bar, tokens/window sublabel. */
81
+ /** A single readable gauge: header label, fill bar, tokens/window sublabel. */
82
82
  function SessionGauge({ data }: { data: GaugeData }): React.ReactElement {
83
83
  const pct = data.percent ?? 0;
84
84
  const fillWidth = Math.max(pct, 1);
@@ -89,7 +89,11 @@ function SessionGauge({ data }: { data: GaugeData }): React.ReactElement {
89
89
  return (
90
90
  <div className={`session-gauge ${data.isSelf ? "session-gauge-self" : ""}`}>
91
91
  <div className="session-gauge-head">
92
- <span className="session-gauge-label">{data.label}</span>
92
+ {/* Full repo/session name by default; truncate only as a last
93
+ * resort (ellipsis + hover title) when space is genuinely tight. */}
94
+ <span className="session-gauge-label truncate" title={data.label}>
95
+ {data.label}
96
+ </span>
93
97
  <span className="session-gauge-pct">{pct}%</span>
94
98
  </div>
95
99
  <div
@@ -103,7 +107,7 @@ function SessionGauge({ data }: { data: GaugeData }): React.ReactElement {
103
107
  <div className="gauge-fill" style={{ width: `${fillWidth}%` }} />
104
108
  </div>
105
109
  <p className="gauge-label session-gauge-sub">{sublabel}</p>
106
- {data.sublabel && <p className="session-gauge-meta">{data.sublabel}</p>}
110
+ {data.sublabel && <p className="session-gauge-meta truncate" title={data.sublabel}>{data.sublabel}</p>}
107
111
  </div>
108
112
  );
109
113
  }
@@ -154,7 +158,7 @@ export function SessionContextGauges({
154
158
  const showLoading = loading && !sessions;
155
159
 
156
160
  return (
157
- <Card className="electric-hover">
161
+ <Card className="electric-hover session-gauges-card w-full">
158
162
  <CardHeader>
159
163
  <CardTitle>Context per Session (live)</CardTitle>
160
164
  </CardHeader>
@@ -166,10 +170,12 @@ export function SessionContextGauges({
166
170
  ) : showLoading ? (
167
171
  <div className="text-sm text-muted-foreground">Loading sessions…</div>
168
172
  ) : (
169
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
170
- {gauges.map((g) => (
171
- <SessionGauge key={g.key} data={g} />
172
- ))}
173
+ <div className="session-gauges-scroll">
174
+ <div className="session-gauges-grid">
175
+ {gauges.map((g) => (
176
+ <SessionGauge key={g.key} data={g} />
177
+ ))}
178
+ </div>
173
179
  </div>
174
180
  )}
175
181
  </CardContent>
@@ -0,0 +1,69 @@
1
+ /**
2
+ * dashboard-client/src/components/WidgetDetailModal.tsx — reusable drill-down modal.
3
+ *
4
+ * Wraps the RepoDetailModal overlay pattern (fixed backdrop, escape-key close,
5
+ * click-backdrop close) into a generic container so Overview cards can open a
6
+ * bar/line chart over time. Children render inside a scrollable panel.
7
+ */
8
+
9
+ import type React from "react";
10
+ import { useEffect } from "react";
11
+ import { X } from "lucide-react";
12
+
13
+ export interface WidgetDetailModalProps {
14
+ /** Modal heading shown in the panel header. */
15
+ title: string;
16
+ /** Whether the modal is open (mounted). */
17
+ open: boolean;
18
+ /** Called when the user closes via Escape, close button, or backdrop click. */
19
+ onClose: () => void;
20
+ /** Modal body (e.g. a chart component). */
21
+ children: React.ReactNode;
22
+ }
23
+
24
+ export function WidgetDetailModal({
25
+ title,
26
+ open,
27
+ onClose,
28
+ children,
29
+ }: WidgetDetailModalProps): React.ReactElement {
30
+ // Close on Escape key while the panel is open.
31
+ useEffect(() => {
32
+ if (!open) return;
33
+ const handler = (e: KeyboardEvent): void => {
34
+ if (e.key === "Escape") onClose();
35
+ };
36
+ window.addEventListener("keydown", handler);
37
+ return () => window.removeEventListener("keydown", handler);
38
+ }, [open, onClose]);
39
+
40
+ if (!open) return <></>;
41
+
42
+ return (
43
+ <div
44
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
45
+ onClick={onClose}
46
+ role="dialog"
47
+ aria-modal="true"
48
+ aria-label={title}
49
+ >
50
+ <div
51
+ className="max-h-[80vh] w-full max-w-3xl overflow-y-auto rounded-lg border border-border bg-bg-card p-5"
52
+ onClick={(e) => e.stopPropagation()}
53
+ >
54
+ <header className="mb-4 flex items-center justify-between gap-4">
55
+ <h2 className="font-heading text-lg font-semibold">{title}</h2>
56
+ <button
57
+ type="button"
58
+ className="text-muted-foreground transition-colors hover:text-foreground"
59
+ onClick={onClose}
60
+ aria-label="Close"
61
+ >
62
+ <X size={22} strokeWidth={2} />
63
+ </button>
64
+ </header>
65
+ {children}
66
+ </div>
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * dashboard-client/src/components/charts/PerfBarChart.tsx — reusable perf bar chart.
3
+ *
4
+ * Recharts BarChart with the dashboard dark theme (AXIS "#8b949e",
5
+ * GRID "#30363d", dark tooltip). Renders `{label, value}` buckets.
6
+ * Handles empty data with a "No data available" message.
7
+ *
8
+ * PREVENT-PI-004: recharts is bundled into this localhost-served static
9
+ * dashboard bundle; it makes no runtime network calls.
10
+ */
11
+
12
+ import type React from "react";
13
+ import {
14
+ ResponsiveContainer,
15
+ BarChart,
16
+ Bar,
17
+ XAxis,
18
+ YAxis,
19
+ CartesianGrid,
20
+ Tooltip,
21
+ } from "recharts";
22
+
23
+ export interface PerfBarChartProps {
24
+ /** Buckets with a display label + numeric value. */
25
+ data: Array<{ label: string; value: number }>;
26
+ /** Bar color (default dashboard green). */
27
+ color?: string;
28
+ /** Chart height in px (default 280). */
29
+ height?: number;
30
+ /** Optional formatter for values (e.g. "$" prefix, thousands separators). */
31
+ valueFormatter?: (v: number) => string;
32
+ }
33
+
34
+ const AXIS_COLOR = "#8b949e";
35
+ const GRID_COLOR = "#30363d";
36
+ const TOOLTIP_STYLE = {
37
+ background: "#161b22",
38
+ border: "1px solid #30363d",
39
+ borderRadius: "6px",
40
+ color: "#e6edf3",
41
+ };
42
+
43
+ export function PerfBarChart({
44
+ data,
45
+ color = "#3fb950",
46
+ height = 280,
47
+ valueFormatter,
48
+ }: PerfBarChartProps): React.ReactElement {
49
+ const fmt = valueFormatter ?? ((v: number) => String(v));
50
+
51
+ if (!data || data.length === 0) {
52
+ return <p className="py-8 text-center text-sm text-muted-foreground">No data available</p>;
53
+ }
54
+
55
+ return (
56
+ <ResponsiveContainer width="100%" height={height}>
57
+ <BarChart data={data} margin={{ top: 8, right: 16, left: 0, bottom: 0 }}>
58
+ <CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} />
59
+ <XAxis
60
+ dataKey="label"
61
+ stroke={AXIS_COLOR}
62
+ tick={{ fontSize: 11 }}
63
+ interval={0}
64
+ angle={-20}
65
+ textAnchor="end"
66
+ height={56}
67
+ />
68
+ <YAxis
69
+ dataKey="value"
70
+ stroke={AXIS_COLOR}
71
+ tick={{ fontSize: 11 }}
72
+ width={56}
73
+ tickFormatter={(v: number) => fmt(v)}
74
+ />
75
+ <Tooltip
76
+ formatter={(value) => [fmt(value as number), "value"] as [string, string]}
77
+ contentStyle={TOOLTIP_STYLE}
78
+ labelStyle={{ color: AXIS_COLOR, fontSize: 12 }}
79
+ />
80
+ <Bar
81
+ dataKey="value"
82
+ fill={color}
83
+ radius={[4, 4, 0, 0]}
84
+ isAnimationActive={false}
85
+ />
86
+ </BarChart>
87
+ </ResponsiveContainer>
88
+ );
89
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * dashboard-client/src/components/charts/PerfLineChart.tsx — reusable perf line chart.
3
+ *
4
+ * Recharts LineChart with the dashboard dark theme (AXIS "#8b949e",
5
+ * GRID "#30363d", dark tooltip). Renders `{ts, value}` samples over time.
6
+ * Handles empty data with a "No data available" message.
7
+ *
8
+ * PREVENT-PI-004: recharts is bundled into this localhost-served static
9
+ * dashboard bundle; it makes no runtime network calls.
10
+ */
11
+
12
+ import type React from "react";
13
+ import {
14
+ ResponsiveContainer,
15
+ LineChart,
16
+ Line,
17
+ XAxis,
18
+ YAxis,
19
+ CartesianGrid,
20
+ Tooltip,
21
+ } from "recharts";
22
+
23
+ export interface PerfLineChartProps {
24
+ /** Time-series samples ascending by ts (epoch ms). */
25
+ data: Array<{ ts: number; value: number }>;
26
+ /** Optional series label shown in the tooltip. */
27
+ label?: string;
28
+ /** Line + gradient color (default dashboard blue). */
29
+ color?: string;
30
+ /** Chart height in px (default 280). */
31
+ height?: number;
32
+ }
33
+
34
+ const AXIS_COLOR = "#8b949e";
35
+ const GRID_COLOR = "#30363d";
36
+ const TOOLTIP_STYLE = {
37
+ background: "#161b22",
38
+ border: "1px solid #30363d",
39
+ borderRadius: "6px",
40
+ color: "#e6edf3",
41
+ };
42
+
43
+ function fmtTs(ts: number): string {
44
+ return new Date(ts).toLocaleTimeString();
45
+ }
46
+
47
+ function fmtVal(v: number): string {
48
+ return Number.isFinite(v) ? String(v) : String(v);
49
+ }
50
+
51
+ export function PerfLineChart({
52
+ data,
53
+ label = "value",
54
+ color = "#58a6ff",
55
+ height = 280,
56
+ }: PerfLineChartProps): React.ReactElement {
57
+ if (!data || data.length === 0) {
58
+ return <p className="py-8 text-center text-sm text-muted-foreground">No data available</p>;
59
+ }
60
+
61
+ const gradId = `perf-line-${color.replace(/[^a-zA-Z0-9]/g, "")}`;
62
+
63
+ return (
64
+ <ResponsiveContainer width="100%" height={height}>
65
+ <LineChart data={data} margin={{ top: 8, right: 16, left: 0, bottom: 0 }}>
66
+ <defs>
67
+ <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
68
+ <stop offset="5%" stopColor={color} stopOpacity={0.7} />
69
+ <stop offset="95%" stopColor={color} stopOpacity={0.15} />
70
+ </linearGradient>
71
+ </defs>
72
+ <CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} />
73
+ <XAxis
74
+ dataKey="ts"
75
+ type="number"
76
+ domain={["dataMin", "dataMax"]}
77
+ scale="time"
78
+ tickFormatter={(ts: number) => fmtTs(ts)}
79
+ stroke={AXIS_COLOR}
80
+ tick={{ fontSize: 11 }}
81
+ />
82
+ <YAxis
83
+ dataKey="value"
84
+ stroke={AXIS_COLOR}
85
+ tick={{ fontSize: 11 }}
86
+ width={56}
87
+ tickFormatter={(v: number) => fmtVal(v)}
88
+ />
89
+ <Tooltip
90
+ labelFormatter={(ts: number) => fmtTs(ts)}
91
+ formatter={(value) => [fmtVal(value as number), label] as [string, string]}
92
+ contentStyle={TOOLTIP_STYLE}
93
+ labelStyle={{ color: AXIS_COLOR, fontSize: 12 }}
94
+ />
95
+ <Line
96
+ type="monotone"
97
+ dataKey="value"
98
+ name={label}
99
+ stroke={color}
100
+ strokeWidth={2}
101
+ dot={false}
102
+ fill={`url(#${gradId})`}
103
+ isAnimationActive={false}
104
+ />
105
+ </LineChart>
106
+ </ResponsiveContainer>
107
+ );
108
+ }
@@ -17,7 +17,7 @@ export function AppShell({ active, onTabChange, snapshot, children }: AppShellPr
17
17
 
18
18
  return (
19
19
  <div className="min-h-screen bg-background text-foreground">
20
- <div className="mx-auto flex max-w-[1600px] gap-6 px-4 py-4">
20
+ <div className="mx-auto flex w-full gap-6 px-4 py-4 xl:px-6">
21
21
  <Sidebar active={active} onTabChange={onTabChange} />
22
22
  <div className="flex min-w-0 flex-1 flex-col">
23
23
  <header className="mb-4 flex items-center gap-3">
@@ -0,0 +1,75 @@
1
+ /**
2
+ * dashboard-client/src/components/ui/SortableCard.tsx — sortable wrapper for
3
+ * Overview grid cards.
4
+ *
5
+ * Drag isolation (Plexus PR #59 pattern): the sortable listeners live ONLY on
6
+ * the grip icon. The card surface is not directly draggable — it keeps its
7
+ * onClick (drill-down) and stays keyboard-accessible.
8
+ */
9
+ import type React from "react";
10
+ import { useSortable } from "@dnd-kit/sortable";
11
+ import { CSS } from "@dnd-kit/utilities";
12
+ import { GripVertical } from "lucide-react";
13
+ import type { CardId } from "../../types/card";
14
+
15
+ export interface SortableCardProps {
16
+ id: CardId;
17
+ children: React.ReactNode;
18
+ onClick?: () => void;
19
+ }
20
+
21
+ export function SortableCard({
22
+ id,
23
+ children,
24
+ onClick,
25
+ }: SortableCardProps): React.ReactElement {
26
+ const {
27
+ attributes,
28
+ listeners,
29
+ setNodeRef,
30
+ transform,
31
+ transition,
32
+ isDragging,
33
+ } = useSortable({ id });
34
+
35
+ const style: React.CSSProperties = {
36
+ transform: CSS.Transform.toString(transform),
37
+ transition,
38
+ opacity: isDragging ? 0.5 : 1,
39
+ zIndex: isDragging ? 10 : undefined,
40
+ };
41
+
42
+ const handleKeyDown = (e: React.KeyboardEvent) => {
43
+ if (onClick && (e.key === "Enter" || e.key === " ")) {
44
+ e.preventDefault();
45
+ onClick();
46
+ }
47
+ };
48
+
49
+ return (
50
+ <div ref={setNodeRef} style={style} className="relative">
51
+ <button
52
+ type="button"
53
+ aria-label={`Reorder ${id} card`}
54
+ className="absolute right-2 top-2 z-10 cursor-grab rounded p-1 text-muted-foreground/50 hover:bg-foreground/5 hover:text-muted-foreground active:cursor-grabbing"
55
+ {...attributes}
56
+ {...listeners}
57
+ >
58
+ <GripVertical size={16} />
59
+ </button>
60
+ {onClick ? (
61
+ <div
62
+ role="button"
63
+ tabIndex={0}
64
+ onClick={onClick}
65
+ onKeyDown={handleKeyDown}
66
+ className="cursor-pointer rounded-lg outline-none transition-all duration-200 hover:ring-1 hover:ring-primary/60 focus-visible:ring-1 focus-visible:ring-primary/60"
67
+ >
68
+ {children}
69
+ </div>
70
+ ) : (
71
+ <div>{children}</div>
72
+ )}
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * dashboard-client/src/hooks/useCardPositions.ts — persisted, deterministic
3
+ * card order for the Overview tab.
4
+ *
5
+ * The order is read from localStorage on mount, validated against
6
+ * DEFAULT_CARD_ORDER (must contain exactly the same CardIds — no missing,
7
+ * no extras — and no duplicate ids), and falls back to the default if invalid.
8
+ * Pure deterministic: no Date.now() / Math.random() anywhere.
9
+ */
10
+ import { useCallback, useState } from "react";
11
+ import type { CardId } from "../types/card";
12
+ import { DEFAULT_CARD_ORDER } from "../types/card";
13
+
14
+ const STORAGE_KEY = "mega-compact-card-order";
15
+
16
+ /** Returns true when `ids` is a valid permutation of DEFAULT_CARD_ORDER. */
17
+ function isValidOrder(ids: unknown): ids is CardId[] {
18
+ if (!Array.isArray(ids)) return false;
19
+ if (ids.length !== DEFAULT_CARD_ORDER.length) return false;
20
+
21
+ const expected = new Set<CardId>(DEFAULT_CARD_ORDER);
22
+ const seen = new Set<CardId>();
23
+
24
+ for (const id of ids) {
25
+ // every entry must be a known CardId
26
+ if (typeof id !== "string" || !expected.has(id as CardId)) return false;
27
+ // no duplicates
28
+ if (seen.has(id as CardId)) return false;
29
+ seen.add(id as CardId);
30
+ }
31
+ return true;
32
+ }
33
+
34
+ /** Loads a persisted order from localStorage, validating it first. */
35
+ function loadOrder(): CardId[] {
36
+ try {
37
+ const raw = window.localStorage.getItem(STORAGE_KEY);
38
+ if (raw == null) return DEFAULT_CARD_ORDER;
39
+ const parsed: unknown = JSON.parse(raw);
40
+ return isValidOrder(parsed) ? parsed : DEFAULT_CARD_ORDER;
41
+ } catch {
42
+ return DEFAULT_CARD_ORDER;
43
+ }
44
+ }
45
+
46
+ export interface UseCardPositionsResult {
47
+ order: CardId[];
48
+ moveCard: (id: CardId, overId: CardId) => void;
49
+ }
50
+
51
+ export function useCardPositions(): UseCardPositionsResult {
52
+ const [order, setOrder] = useState<CardId[]>(loadOrder);
53
+
54
+ const moveCard = useCallback((id: CardId, overId: CardId) => {
55
+ setOrder((prev) => {
56
+ const from = prev.indexOf(id);
57
+ const to = prev.indexOf(overId);
58
+ if (from === -1 || to === -1 || from === to) return prev;
59
+
60
+ const next = [...prev];
61
+ next.splice(from, 1);
62
+ next.splice(to, 0, id);
63
+ try {
64
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
65
+ } catch {
66
+ // storage unavailable (private mode / quota) — ignore; order
67
+ // still holds for the session
68
+ }
69
+ return next;
70
+ });
71
+ }, []);
72
+
73
+ return { order, moveCard };
74
+ }
@@ -30,7 +30,8 @@ body {
30
30
  }
31
31
 
32
32
  .dashboard-app {
33
- max-width: min(1600px, 96vw);
33
+ width: 100%;
34
+ max-width: 100vw;
34
35
  margin: 0 auto;
35
36
  padding: clamp(8px, 2vw, 24px);
36
37
  }
@@ -210,9 +211,6 @@ body {
210
211
  }
211
212
 
212
213
  @media (min-width: 1601px) {
213
- .dashboard-app {
214
- max-width: min(1800px, 96vw);
215
- }
216
214
  .card-grid {
217
215
  grid-template-columns: repeat(4, 1fr);
218
216
  }