@cybermem/dashboard 0.9.12 → 0.13.4

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 (43) hide show
  1. package/Dockerfile +3 -3
  2. package/app/api/audit-logs/route.ts +12 -6
  3. package/app/api/health/route.ts +2 -1
  4. package/app/api/mcp-config/route.ts +128 -0
  5. package/app/api/metrics/route.ts +22 -70
  6. package/app/api/settings/route.ts +125 -30
  7. package/app/page.tsx +105 -127
  8. package/components/dashboard/{chart-card.tsx → charts/chart-card.tsx} +13 -19
  9. package/components/dashboard/{metrics-chart.tsx → charts/memory-chart.tsx} +1 -1
  10. package/components/dashboard/charts-section.tsx +3 -3
  11. package/components/dashboard/header.tsx +177 -176
  12. package/components/dashboard/{audit-log-table.tsx → logs/log-viewer.tsx} +12 -7
  13. package/components/dashboard/mcp/config-preview.tsx +246 -0
  14. package/components/dashboard/mcp/platform-selector.tsx +96 -0
  15. package/components/dashboard/mcp-config-modal.tsx +97 -503
  16. package/components/dashboard/{metric-card.tsx → metrics/stat-card.tsx} +4 -2
  17. package/components/dashboard/metrics-grid.tsx +10 -2
  18. package/components/dashboard/settings/access-token-section.tsx +131 -0
  19. package/components/dashboard/settings/data-management-section.tsx +122 -0
  20. package/components/dashboard/settings/system-info-section.tsx +98 -0
  21. package/components/dashboard/settings-modal.tsx +55 -299
  22. package/e2e/api.spec.ts +219 -0
  23. package/e2e/routing.spec.ts +39 -0
  24. package/e2e/ui.spec.ts +373 -0
  25. package/lib/data/dashboard-context.tsx +96 -29
  26. package/lib/data/types.ts +32 -38
  27. package/middleware.ts +31 -13
  28. package/package.json +6 -1
  29. package/playwright.config.ts +23 -58
  30. package/public/clients.json +5 -3
  31. package/release-reports/assets/local/1_dashboard.png +0 -0
  32. package/release-reports/assets/local/2_audit_logs.png +0 -0
  33. package/release-reports/assets/local/3_charts.png +0 -0
  34. package/release-reports/assets/local/4_mcp_modal.png +0 -0
  35. package/release-reports/assets/local/5_settings_modal.png +0 -0
  36. package/lib/data/demo-strategy.ts +0 -110
  37. package/lib/data/production-strategy.ts +0 -191
  38. package/lib/prometheus/client.ts +0 -58
  39. package/lib/prometheus/index.ts +0 -6
  40. package/lib/prometheus/metrics.ts +0 -234
  41. package/lib/prometheus/sparklines.ts +0 -71
  42. package/lib/prometheus/timeseries.ts +0 -305
  43. package/lib/prometheus/utils.ts +0 -176
package/middleware.ts CHANGED
@@ -11,25 +11,43 @@ import { NextResponse } from "next/server";
11
11
  export async function middleware(request: NextRequest) {
12
12
  const host = request.headers.get("host") || "";
13
13
 
14
- // LOCAL BYPASS: Skip auth for local development and trusted networks
14
+ // 1. LOCAL BYPASS: Skip auth for local development and trusted networks
15
+ const authMethod = request.headers.get("x-auth-method");
16
+ const userId = request.headers.get("x-user-id");
17
+
18
+ // Check if host is a private IP address (10.x.x.x)
19
+ const isPrivateIP = /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/.test(host);
20
+
15
21
  const isLocal =
16
22
  host.includes("localhost") ||
17
23
  host.includes("127.0.0.1") ||
18
- host.includes("raspberrypi.local");
24
+ host.includes("raspberrypi.local") ||
25
+ isPrivateIP ||
26
+ authMethod === "local" ||
27
+ userId === "local";
19
28
 
20
- if (!isLocal) {
21
- // REMOTE: Check cookie token
22
- const token = request.cookies.get("cybermem_token")?.value;
29
+ if (isLocal) {
30
+ const responseHeaders = new Headers(request.headers);
31
+ responseHeaders.set("X-User-Id", "local");
32
+ return NextResponse.next({
33
+ request: {
34
+ headers: responseHeaders,
35
+ },
36
+ });
37
+ }
23
38
 
24
- if (!token) {
25
- // Redirect to token auth page with error
26
- const authUrl = new URL("/api/auth/token", request.url);
27
- authUrl.searchParams.set("error", "no_token");
28
- return NextResponse.redirect(authUrl);
29
- }
39
+ // REMOTE: Check cookie token
40
+ const token = request.cookies.get("cybermem_token")?.value;
30
41
 
31
- // Note: Full token verification happens in the token endpoint
32
- // Cookie existence is enough for middleware (token was validated when set)
42
+ // We do NOT redirect here anymore to avoid 307 loops and respect Law #6.
43
+ // Unauthorized requests will simply not have the X-User-Id header,
44
+ // and the UI (app/page.tsx) will render the LoginModal with a 200 OK.
45
+ if (
46
+ !token &&
47
+ !userId &&
48
+ !request.nextUrl.pathname.startsWith("/api/auth")
49
+ ) {
50
+ console.log("MiddleWare: No token/userId found for remote request");
33
51
  }
34
52
 
35
53
  // CSRF Protection for mutating requests
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cybermem/dashboard",
3
- "version": "0.9.12",
3
+ "version": "0.13.4",
4
4
  "description": "CyberMem Monitoring Dashboard",
5
5
  "homepage": "https://cybermem.dev",
6
6
  "repository": {
@@ -15,6 +15,7 @@
15
15
  "scripts": {
16
16
  "build": "next build --webpack",
17
17
  "dev": "next dev --webpack",
18
+ "dev:turbo": "next dev --turbopack",
18
19
  "lint": "eslint .",
19
20
  "start": "next start",
20
21
  "test:e2e": "playwright test"
@@ -76,7 +77,9 @@
76
77
  "react-day-picker": "9.8.0",
77
78
  "react-dom": "19.2.0",
78
79
  "react-hook-form": "^7.60.0",
80
+ "react-remove-scroll-bar": "2.3.8",
79
81
  "react-resizable-panels": "^2.1.7",
82
+ "react-style-singleton": "2.2.3",
80
83
  "recharts": "2.15.4",
81
84
  "recharts-scale": "^0.4.5",
82
85
  "sonner": "^1.7.4",
@@ -85,6 +88,8 @@
85
88
  "ssh2": "^1.17.0",
86
89
  "tailwind-merge": "^3.3.1",
87
90
  "tailwindcss-animate": "^1.0.7",
91
+ "use-callback-ref": "1.3.3",
92
+ "use-sidecar": "1.1.3",
88
93
  "vaul": "^1.1.2",
89
94
  "zod": "3.25.76"
90
95
  },
@@ -1,70 +1,35 @@
1
1
  import { defineConfig, devices } from "@playwright/test";
2
- import fs from "fs";
3
- import path from "path";
4
-
5
- // Load clients to pick a random one for realistic testing
6
- // This ensures E2E tests generate traffic that looks like real clients (Claude, VS Code, etc.)
7
- const clientsPath = path.join(__dirname, "public", "clients.json");
8
- let randomClient = { name: "Playwright Desktop", match: "playwright" };
9
-
10
- try {
11
- if (fs.existsSync(clientsPath)) {
12
- const clients = JSON.parse(fs.readFileSync(clientsPath, "utf-8"));
13
- const validClients = clients.filter(
14
- (c: any) => c.match && c.match !== "other",
15
- );
16
- if (validClients.length > 0) {
17
- randomClient =
18
- validClients[Math.floor(Math.random() * validClients.length)];
19
- // Handle regex matchers (take first option)
20
- if (randomClient.match.includes("|")) {
21
- randomClient.match = randomClient.match.split("|")[0];
22
- }
23
- }
24
- }
25
- } catch (e) {
26
- console.warn("Failed to load clients.json, using default.");
27
- }
28
-
29
- console.log(
30
- `🤖 E2E Test Identity: ${randomClient.name} (UA: ${randomClient.match})`,
31
- );
32
2
 
33
3
  export default defineConfig({
34
4
  testDir: "./e2e",
35
- fullyParallel: true,
36
- forbidOnly: !!process.env.CI,
37
- retries: 3,
38
- timeout: 10000, // 10s max per test
39
- expect: {
40
- timeout: 5000, // 5s for assertions
41
- },
42
- workers: process.env.CI ? 1 : undefined,
5
+ outputDir: "./test-results",
43
6
  reporter: "html",
44
7
  use: {
45
- baseURL: process.env.BASE_URL || "http://127.0.0.1:3000",
46
- trace: "on-first-retry",
47
- ignoreHTTPSErrors: true,
48
- userAgent: `${randomClient.match}/1.0 (E2E Test)`,
49
- extraHTTPHeaders: {
50
- "X-Client-Name": randomClient.name,
51
- },
8
+ baseURL: process.env.DASHBOARD_URL || "http://localhost:3000",
9
+ screenshot: "only-on-failure",
52
10
  },
11
+ webServer: process.env.DASHBOARD_URL
12
+ ? undefined
13
+ : {
14
+ command: "npm run dev",
15
+ port: 3000,
16
+ reuseExistingServer: !process.env.CI,
17
+ stdout: "ignore",
18
+ stderr: "pipe",
19
+ },
53
20
  projects: [
54
21
  {
55
- name: "chromium",
56
- use: { ...devices["Desktop Chrome"] },
22
+ name: "api",
23
+ testMatch: ["api.spec.ts", "routing.spec.ts"],
24
+ },
25
+ {
26
+ name: "ui",
27
+ testMatch: "ui.spec.ts",
28
+ use: {
29
+ ...devices["Desktop Chrome"],
30
+ trace: "on",
31
+ screenshot: "on",
32
+ },
57
33
  },
58
34
  ],
59
- webServer: {
60
- // In CI: dashboard runs via docker-compose, no need to start new server
61
- // Locally: starts dev server if not already running
62
- command:
63
- "lsof -ti:3000 | xargs kill -9 2>/dev/null || true; npm run dev -- -p 3000 -H 127.0.0.1",
64
- url: "http://127.0.0.1:3000",
65
- reuseExistingServer: true, // Always reuse - docker-compose provides in CI
66
- stdout: "pipe",
67
- stderr: "pipe",
68
- timeout: 60000,
69
- },
70
35
  });
@@ -5,6 +5,7 @@
5
5
  "match": "claude|antigravity",
6
6
  "color": "#e65c40",
7
7
  "icon": "/icons/claude.png",
8
+ "filename": "claude_desktop_config.json",
8
9
  "description": "Configure Claude Desktop to use CyberMem for persistent memory across conversations.",
9
10
  "steps": [
10
11
  "Open Claude Desktop",
@@ -54,6 +55,7 @@
54
55
  "match": "windsurf",
55
56
  "color": "#3b82f6",
56
57
  "icon": "/icons/windsurf.png",
58
+ "filename": "mcp_config.json",
57
59
  "description": "Windsurf (Codeium) supports MCP via a dedicated config file.",
58
60
  "steps": [
59
61
  "Open the config file: `~/.codeium/windsurf/mcp_config.json`",
@@ -116,6 +118,7 @@
116
118
  "match": "codex",
117
119
  "color": "#6366f1",
118
120
  "icon": "/icons/codex.png",
121
+ "filename": "config.toml",
119
122
  "description": "Codex CLI uses TOML format for MCP configuration.",
120
123
  "steps": [
121
124
  "Edit `~/.codex/config.toml`",
@@ -165,9 +168,8 @@
165
168
  "icon": null,
166
169
  "description": "For any other MCP-compliant client, use the following connection details:",
167
170
  "steps": [
168
- "**Transport**: stdio (via npx command)",
169
- "**Command**: `npx @cybermem/mcp` with `--url {{ENDPOINT}}` and `--token {{TOKEN}}` args",
170
- "Refer to your client's documentation for config file location"
171
+ "Locate the configuration file for your client",
172
+ "Insert the following JSON configuration:"
171
173
  ],
172
174
  "configType": "json"
173
175
  }
@@ -1,110 +0,0 @@
1
-
2
- import { DashboardData, DataSourceStrategy, TimeSeriesData } from "./types"
3
-
4
- export class DemoDataSource implements DataSourceStrategy {
5
- async fetchGlobalStats(): Promise<DashboardData> {
6
- // 1. Generate full time-series history for sparklines
7
- const generateSeries = (start: number, count: number, variance: number) => {
8
- const series = [start]
9
- for (let i = 1; i < count; i++) {
10
- const change = (Math.random() - 0.5) * variance
11
- series.push(Math.max(0, series[i - 1] + change))
12
- }
13
- return series
14
- }
15
-
16
- const exactData = {
17
- memory: generateSeries(12000, 20, 500),
18
- clients: generateSeries(40, 20, 2),
19
- success: generateSeries(98, 20, 0.5).map((v) => Math.min(100, v)),
20
- requests: generateSeries(85000, 20, 1000),
21
- }
22
-
23
- // 2. Generate stable, rich Audit Log history
24
- const mockClients = ["Antigravity", "Claude", "Cursor", "ChatGPT", "Copilot"]
25
- const mockOps = ["Create", "Read", "Update", "Delete"]
26
- const demoLogs = Array.from({ length: 50 }).map((_, i) => ({
27
- id: i,
28
- date: new Date(Date.now() - i * 1000 * 60 * 5),
29
- client: mockClients[i % mockClients.length],
30
- operation: mockOps[i % mockOps.length],
31
- status: i % 10 === 0 ? "Error" : "Success",
32
- description:
33
- i % 10 === 0 ? "Rate limit exceeded" : "Operation completed successfully",
34
- timestamp: Date.now() - i * 1000 * 60 * 5,
35
- }))
36
-
37
- return {
38
- stats: {
39
- memoryRecords: Math.round(exactData.memory[exactData.memory.length - 1]),
40
- totalClients: Math.round(exactData.clients[exactData.clients.length - 1]),
41
- successRate: Number(
42
- exactData.success[exactData.success.length - 1].toFixed(1)
43
- ),
44
- totalRequests: Math.round(
45
- exactData.requests[exactData.requests.length - 1]
46
- ),
47
- topWriter: { name: "Antigravity", count: 4200 },
48
- topReader: { name: "Claude", count: 3100 },
49
- lastWriter: { name: "VS Code", timestamp: Date.now() - 1000 * 60 * 2 },
50
- lastReader: { name: "Cursor", timestamp: Date.now() - 1000 * 30 },
51
- },
52
- trends: {
53
- memory: {
54
- change: "+450",
55
- trend: "up",
56
- hasData: true,
57
- data: exactData.memory,
58
- },
59
- clients: {
60
- change: "+5",
61
- trend: "up",
62
- hasData: true,
63
- data: exactData.clients,
64
- },
65
- success: {
66
- change: "+0.5%",
67
- trend: "up",
68
- hasData: true,
69
- data: exactData.success,
70
- },
71
- requests: {
72
- change: "+1.2k",
73
- trend: "up",
74
- hasData: true,
75
- data: exactData.requests,
76
- },
77
- },
78
- logs: demoLogs,
79
- }
80
- }
81
-
82
- async getChartData(period: string): Promise<TimeSeriesData> {
83
- const clients = ["Antigravity", "Claude", "Cursor", "ChatGPT"]
84
- const now = Math.floor(Date.now() / 1000)
85
- // Generate 20 points
86
- const points = 20
87
- const interval = 300 // 5 mins
88
-
89
- const generateSeries = () => {
90
- return Array.from({ length: points }).map((_, i) => {
91
- const time = now - (points - 1 - i) * interval
92
- const point: any = { time }
93
- clients.forEach(c => {
94
- // Random value between 0 and 10
95
- point[c] = Math.floor(Math.random() * 10)
96
- })
97
- return point
98
- })
99
- }
100
-
101
- return {
102
- creates: generateSeries(),
103
- reads: generateSeries(),
104
- updates: generateSeries(),
105
- deletes: generateSeries(),
106
- // Metadata is now handled globally via clients.json, so we don't return partial overrides here
107
- metadata: {}
108
- }
109
- }
110
- }
@@ -1,191 +0,0 @@
1
- import { DashboardData, DataSourceStrategy, TimeSeriesData } from "./types";
2
-
3
- export class ProductionDataSource implements DataSourceStrategy {
4
- async fetchGlobalStats(): Promise<DashboardData> {
5
- const res = await fetch(`/api/metrics`);
6
- if (!res.ok) throw new Error("Failed to fetch metrics");
7
- const data = await res.json();
8
-
9
- const logsRes = await fetch(`/api/audit-logs`);
10
- const logsData = logsRes.ok ? await logsRes.json() : { logs: [] };
11
-
12
- // Helper to resolve logs
13
- const resolveOperation = (log: any) => {
14
- const normalizedOp = (log.operation || "").toString().toLowerCase();
15
- if (normalizedOp === "read") return "Read";
16
- if (normalizedOp === "write" || normalizedOp === "create") return "Write";
17
- if (normalizedOp === "update") return "Update";
18
- if (normalizedOp === "delete") return "Delete";
19
- const method = (log.method || "").toString().toUpperCase();
20
- if (method === "GET") return "Read";
21
- if (method === "DELETE") return "Delete";
22
- if (method === "PATCH" || method === "PUT") return "Update";
23
- return "Write";
24
- };
25
-
26
- const mappedLogs = (logsData.logs || []).map((log: any, index: number) => {
27
- const operation = resolveOperation(log);
28
- // Use rawStatus if available (from our API), otherwise fallback to status
29
- const statusCode = parseInt(log.rawStatus || log.status);
30
- let status = "Success";
31
- let description = "";
32
- if (statusCode >= 500) {
33
- status = "Error";
34
- description = "Server error";
35
- } else if (statusCode >= 400) {
36
- status = "Error";
37
- description =
38
- statusCode === 401
39
- ? "Unauthorized"
40
- : statusCode === 403
41
- ? "Forbidden"
42
- : "Client error";
43
- } else if (statusCode >= 300) {
44
- status = "Warning";
45
- description = "Redirect";
46
- } else if (log.status === "Error") {
47
- status = "Error";
48
- description = "Error";
49
- }
50
-
51
- return {
52
- id: index,
53
- date: new Date(log.timestamp),
54
- client: log.client || "Unknown",
55
- operation,
56
- status,
57
- description,
58
- timestamp: new Date(log.timestamp).getTime(),
59
- };
60
- });
61
-
62
- // Calculate Latest & Tops from logs if available
63
- const sortedByDate = [...mappedLogs].sort(
64
- (a, b) => b.timestamp - a.timestamp,
65
- );
66
-
67
- // Writers
68
- const wLog = sortedByDate.find((l) =>
69
- ["Write", "Update", "Delete", "Create"].includes(l.operation),
70
- );
71
- const lastWriter = wLog
72
- ? { name: wLog.client, timestamp: wLog.timestamp }
73
- : { name: "N/A", timestamp: 0 };
74
-
75
- // Readers
76
- const rLog = sortedByDate.find((l) => l.operation === "Read");
77
- const lastReader = rLog
78
- ? { name: rLog.client, timestamp: rLog.timestamp }
79
- : { name: "N/A", timestamp: 0 };
80
-
81
- // Tops
82
- const writerCounts: Record<string, number> = {};
83
- const readerCounts: Record<string, number> = {};
84
- mappedLogs.forEach((log: any) => {
85
- if (["Write", "Update", "Delete", "Create"].includes(log.operation)) {
86
- writerCounts[log.client] = (writerCounts[log.client] || 0) + 1;
87
- } else if (log.operation === "Read") {
88
- readerCounts[log.client] = (readerCounts[log.client] || 0) + 1;
89
- }
90
- });
91
- const getTop = (counts: Record<string, number>) => {
92
- const entries = Object.entries(counts);
93
- if (entries.length === 0) return { name: "N/A", count: 0 };
94
- entries.sort((a, b) => b[1] - a[1]);
95
- return { name: entries[0][0], count: entries[0][1] };
96
- };
97
-
98
- const topWriter = logsRes.ok
99
- ? getTop(writerCounts)
100
- : (data.stats.topWriter ?? { name: "N/A", count: 0 });
101
- const topReader = logsRes.ok
102
- ? getTop(readerCounts)
103
- : (data.stats.topReader ?? { name: "N/A", count: 0 });
104
-
105
- // Trends calculation
106
- const calculateTrend = (series: number[]) => {
107
- if (!series || series.length < 2)
108
- return {
109
- change: "0",
110
- trend: "neutral" as const,
111
- hasData: false,
112
- data: [],
113
- };
114
- const first = series[0];
115
- const last = series[series.length - 1];
116
- const diff = last - first;
117
- const prefix = diff > 0 ? "+" : "";
118
- return {
119
- change: `${prefix}${diff.toLocaleString()}`,
120
- trend: (diff > 0 ? "up" : diff < 0 ? "down" : "neutral") as
121
- | "up"
122
- | "down"
123
- | "neutral",
124
- hasData: true,
125
- data: series,
126
- };
127
- };
128
-
129
- // Success Rate Trend
130
- let successTrend = {
131
- change: "0%",
132
- trend: "neutral" as "neutral" | "up" | "down",
133
- hasData: false,
134
- data: [] as number[],
135
- };
136
- if (data.sparklines?.successRate) {
137
- const sData = data.sparklines.successRate;
138
- const sFirst = sData[0] || 0;
139
- const sLast = sData[sData.length - 1] || 0;
140
- const sDiff = sLast - sFirst;
141
- successTrend = {
142
- change: `${sDiff > 0 ? "+" : ""}${sDiff.toFixed(1)}%`,
143
- trend: sDiff >= 0 ? "up" : "down",
144
- hasData: true,
145
- data: sData,
146
- };
147
- }
148
-
149
- return {
150
- stats: {
151
- memoryRecords: data.stats.memoryRecords ?? 0,
152
- totalClients: data.stats.totalClients ?? 0,
153
- successRate: data.stats.successRate ?? 0,
154
- totalRequests: data.stats.totalRequests ?? 0,
155
- topWriter,
156
- topReader,
157
- lastWriter,
158
- lastReader,
159
- },
160
- trends: {
161
- memory: calculateTrend(data.sparklines?.memoryRecords || []),
162
- clients: calculateTrend(data.sparklines?.totalClients || []),
163
- success: successTrend,
164
- requests: calculateTrend(data.sparklines?.totalRequests || []),
165
- },
166
- logs: mappedLogs,
167
- };
168
- }
169
-
170
- async getChartData(period: string): Promise<TimeSeriesData> {
171
- const res = await fetch(`/api/metrics?period=${period}`);
172
- if (!res.ok) throw new Error("Failed to fetch chart data");
173
- const apiData = await res.json();
174
-
175
- // Fetch clients metadata separately or use what's in apiData
176
- // Ideally we merge them here
177
- let metadata = {};
178
- if (apiData.metadata) {
179
- metadata = apiData.metadata;
180
- }
181
-
182
- // apiData.timeSeries needs to be returned.
183
- return {
184
- creates: apiData.timeSeries?.creates || [],
185
- reads: apiData.timeSeries?.reads || [],
186
- updates: apiData.timeSeries?.updates || [],
187
- deletes: apiData.timeSeries?.deletes || [],
188
- metadata,
189
- };
190
- }
191
- }
@@ -1,58 +0,0 @@
1
- // Prefer explicit PROMETHEUS_URL, fall back to NEXT_PUBLIC, then local Prometheus default port (mapped to 9092 in docker-compose)
2
- export const PROMETHEUS_URL = process.env.PROMETHEUS_URL || process.env.NEXT_PUBLIC_PROMETHEUS_URL || 'http://localhost:9092'
3
-
4
- export interface PrometheusQueryResult {
5
- status: string
6
- data: {
7
- resultType: string
8
- result: Array<{
9
- metric: Record<string, string>
10
- value?: [number, string]
11
- values?: Array<[number, string]>
12
- }>
13
- }
14
- }
15
-
16
- export async function query(promql: string): Promise<PrometheusQueryResult> {
17
- try {
18
- const controller = new AbortController()
19
- const id = setTimeout(() => controller.abort(), 1500)
20
-
21
- const response = await fetch(`${PROMETHEUS_URL}/api/v1/query?query=${encodeURIComponent(promql)}`, {
22
- signal: controller.signal,
23
- cache: 'no-store'
24
- })
25
- clearTimeout(id)
26
-
27
- if (!response.ok) {
28
- throw new Error(`Prometheus query failed: ${response.statusText}`)
29
- }
30
- return response.json()
31
- } catch (error) {
32
- console.error('Prometheus query failed:', error)
33
- return { status: 'error', data: { resultType: 'vector', result: [] } }
34
- }
35
- }
36
-
37
- export async function queryRange(promql: string, start: number, end: number, step: string = '1m'): Promise<PrometheusQueryResult> {
38
- try {
39
- const url = `${PROMETHEUS_URL}/api/v1/query_range?query=${encodeURIComponent(promql)}&start=${start}&end=${end}&step=${step}`
40
-
41
- const controller = new AbortController()
42
- const id = setTimeout(() => controller.abort(), 1500)
43
-
44
- const response = await fetch(url, {
45
- signal: controller.signal,
46
- cache: 'no-store'
47
- })
48
- clearTimeout(id)
49
-
50
- if (!response.ok) {
51
- throw new Error(`Prometheus range query failed: ${response.statusText}`)
52
- }
53
- return response.json()
54
- } catch (error) {
55
- console.error('Prometheus range query failed:', error)
56
- return { status: 'error', data: { resultType: 'matrix', result: [] } }
57
- }
58
- }
@@ -1,6 +0,0 @@
1
- export * from './client'
2
- export * from './metrics'
3
- export * from './sparklines'
4
- export * from './timeseries'
5
- export * from './utils'
6
-