@cybermem/dashboard 0.5.14 → 0.8.5
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.
- package/Dockerfile +2 -1
- package/app/api/audit-logs/route.ts +79 -56
- package/app/api/auth/token/route.ts +59 -0
- package/app/api/health/route.ts +49 -22
- package/app/api/metrics/route.ts +272 -86
- package/app/api/reset/route.ts +1 -1
- package/app/api/restore/route.ts +1 -1
- package/app/api/settings/route.ts +37 -26
- package/app/api/system/restart/route.ts +1 -1
- package/app/layout.tsx +25 -17
- package/app/page.tsx +135 -110
- package/components/dashboard/audit-log-table.tsx +3 -3
- package/components/dashboard/mcp-config-modal.tsx +356 -247
- package/components/dashboard/metric-card.tsx +4 -7
- package/components/dashboard/settings-modal.tsx +53 -58
- package/e2e/crud-happy-path.spec.ts +243 -173
- package/lib/auth.ts +50 -0
- package/lib/data/dashboard-context.tsx +117 -70
- package/lib/data/production-strategy.ts +124 -85
- package/middleware.ts +53 -31
- package/next.config.mjs +12 -20
- package/package.json +4 -1
- package/playwright.config.ts +50 -15
- package/public/clients.json +7 -23
package/app/api/metrics/route.ts
CHANGED
|
@@ -1,125 +1,311 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import { NextResponse } from
|
|
3
|
-
import path from
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import path from "path";
|
|
4
4
|
|
|
5
|
-
export const dynamic =
|
|
6
|
-
export const revalidate = 0
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
|
+
export const revalidate = 0;
|
|
7
7
|
|
|
8
8
|
// Use env var for db-exporter URL (Docker internal vs local dev)
|
|
9
|
-
const DB_EXPORTER_URL = process.env.DB_EXPORTER_URL ||
|
|
9
|
+
const DB_EXPORTER_URL = process.env.DB_EXPORTER_URL || "http://localhost:8000";
|
|
10
|
+
const PROMETHEUS_URL = process.env.PROMETHEUS_URL || "http://localhost:9092";
|
|
10
11
|
|
|
11
12
|
// Load clients config for name normalization
|
|
12
|
-
let clientsConfig: any[] = []
|
|
13
|
+
let clientsConfig: any[] = [];
|
|
13
14
|
try {
|
|
14
|
-
const configPath = path.join(process.cwd(),
|
|
15
|
-
clientsConfig = JSON.parse(fs.readFileSync(configPath,
|
|
15
|
+
const configPath = path.join(process.cwd(), "public", "clients.json");
|
|
16
|
+
clientsConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
16
17
|
} catch (e) {
|
|
17
|
-
console.error(
|
|
18
|
+
console.error("Failed to load clients.json:", e);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
// Normalize raw client name to friendly name
|
|
21
22
|
function normalizeClientName(rawName: string): string {
|
|
22
|
-
if (!rawName || rawName ===
|
|
23
|
-
const nameLower = rawName.toLowerCase()
|
|
23
|
+
if (!rawName || rawName === "N/A") return rawName;
|
|
24
|
+
const nameLower = rawName.toLowerCase();
|
|
24
25
|
const client = clientsConfig.find((c: any) => {
|
|
25
26
|
try {
|
|
26
|
-
return new RegExp(c.match,
|
|
27
|
+
return new RegExp(c.match, "i").test(nameLower);
|
|
27
28
|
} catch {
|
|
28
|
-
return nameLower.includes(c.match)
|
|
29
|
+
return nameLower.includes(c.match);
|
|
29
30
|
}
|
|
30
|
-
})
|
|
31
|
-
return client?.name || rawName
|
|
31
|
+
});
|
|
32
|
+
return client?.name || rawName;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// Normalize client names in time series data
|
|
35
36
|
function normalizeTimeSeries(data: any[]): any[] {
|
|
36
|
-
return data.map(point => {
|
|
37
|
-
const normalized: any = { time: point.time }
|
|
37
|
+
return data.map((point) => {
|
|
38
|
+
const normalized: any = { time: point.time };
|
|
38
39
|
for (const [key, value] of Object.entries(point)) {
|
|
39
|
-
if (key !==
|
|
40
|
-
|
|
40
|
+
if (key !== "time") {
|
|
41
|
+
const normalizedKey = normalizeClientName(key);
|
|
42
|
+
normalized[normalizedKey] = value;
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
|
-
return normalized
|
|
44
|
-
})
|
|
45
|
+
return normalized;
|
|
46
|
+
});
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export async function GET(request: Request) {
|
|
48
50
|
try {
|
|
49
|
-
const { searchParams } = new URL(request.url)
|
|
50
|
-
const period = searchParams.get(
|
|
51
|
-
|
|
52
|
-
const controller = new AbortController()
|
|
53
|
-
const timeoutId = setTimeout(() => controller.abort(), 5000)
|
|
54
|
-
|
|
55
|
-
// Fetch stats and timeseries from db-exporter (SQLite)
|
|
56
|
-
const [statsRes, timeseriesRes] = await Promise.all([
|
|
57
|
-
fetch(`${DB_EXPORTER_URL}/api/stats`, {
|
|
58
|
-
signal: controller.signal,
|
|
59
|
-
cache: 'no-store'
|
|
60
|
-
}),
|
|
61
|
-
fetch(`${DB_EXPORTER_URL}/api/timeseries?period=${period}`, {
|
|
62
|
-
signal: controller.signal,
|
|
63
|
-
cache: 'no-store'
|
|
64
|
-
})
|
|
65
|
-
])
|
|
66
|
-
|
|
67
|
-
clearTimeout(timeoutId)
|
|
68
|
-
|
|
69
|
-
if (!statsRes.ok) {
|
|
70
|
-
throw new Error(`Failed to fetch stats: ${statsRes.statusText}`)
|
|
71
|
-
}
|
|
51
|
+
const { searchParams } = new URL(request.url);
|
|
52
|
+
const period = searchParams.get("period") || "24h";
|
|
72
53
|
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
memoryRecords:
|
|
79
|
-
totalClients:
|
|
80
|
-
successRate:
|
|
81
|
-
totalRequests:
|
|
82
|
-
topWriter: {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
56
|
+
|
|
57
|
+
// --- PRIMARY DATA SOURCE: DIRECT SQLITE ---
|
|
58
|
+
let stats = {
|
|
59
|
+
memoryRecords: 0,
|
|
60
|
+
totalClients: 0,
|
|
61
|
+
successRate: 100,
|
|
62
|
+
totalRequests: 0,
|
|
63
|
+
topWriter: { name: "N/A", count: 0 },
|
|
64
|
+
topReader: { name: "N/A", count: 0 },
|
|
65
|
+
lastWriter: { name: "N/A", timestamp: 0 },
|
|
66
|
+
lastReader: { name: "N/A", timestamp: 0 },
|
|
67
|
+
};
|
|
68
|
+
let timeseries: Record<string, any[]> = {
|
|
69
|
+
creates: [],
|
|
70
|
+
reads: [],
|
|
71
|
+
updates: [],
|
|
72
|
+
deletes: [],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const homedir = process.env.HOME || process.env.USERPROFILE || "";
|
|
76
|
+
const dbPath =
|
|
77
|
+
process.env.OM_DB_PATH ||
|
|
78
|
+
path.resolve(homedir, ".cybermem/data/openmemory.sqlite");
|
|
79
|
+
|
|
80
|
+
if (fs.existsSync(dbPath)) {
|
|
81
|
+
console.error(`[STATS-API] Reading SQLite from ${dbPath}`);
|
|
82
|
+
try {
|
|
83
|
+
const sqlite3 = require("sqlite3").verbose();
|
|
84
|
+
const { open } = require("sqlite");
|
|
85
|
+
const db = await open({
|
|
86
|
+
filename: dbPath,
|
|
87
|
+
driver: sqlite3.Database,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Basic Stats
|
|
91
|
+
const memories = await db.get("SELECT COUNT(*) as count FROM memories");
|
|
92
|
+
const totalReqs = await db.get(
|
|
93
|
+
"SELECT COUNT(*) as count FROM cybermem_access_log",
|
|
94
|
+
);
|
|
95
|
+
const errReqs = await db.get(
|
|
96
|
+
"SELECT COUNT(*) as count FROM cybermem_access_log WHERE is_error = 1",
|
|
97
|
+
);
|
|
98
|
+
const lastWrite = await db.get(
|
|
99
|
+
"SELECT client_name, timestamp FROM cybermem_access_log WHERE operation = 'create' ORDER BY timestamp DESC LIMIT 1",
|
|
100
|
+
);
|
|
101
|
+
const lastRead = await db.get(
|
|
102
|
+
"SELECT client_name, timestamp FROM cybermem_access_log WHERE operation = 'read' ORDER BY timestamp DESC LIMIT 1",
|
|
103
|
+
);
|
|
104
|
+
const uniqueClients = await db.get(
|
|
105
|
+
"SELECT COUNT(DISTINCT client_name) as count FROM cybermem_access_log",
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
stats.memoryRecords = memories?.count || 0;
|
|
109
|
+
stats.totalRequests = totalReqs?.count || 0;
|
|
110
|
+
stats.totalClients = uniqueClients?.count || 0;
|
|
111
|
+
stats.successRate =
|
|
112
|
+
stats.totalRequests > 0
|
|
113
|
+
? ((stats.totalRequests - (errReqs?.count || 0)) /
|
|
114
|
+
stats.totalRequests) *
|
|
115
|
+
100
|
|
116
|
+
: 100;
|
|
117
|
+
|
|
118
|
+
console.error(
|
|
119
|
+
`[STATS-API] SQLite stats: ${stats.totalRequests} total requests, ${stats.memoryRecords} records`,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (lastWrite) {
|
|
123
|
+
stats.lastWriter = {
|
|
124
|
+
name: normalizeClientName(lastWrite.client_name),
|
|
125
|
+
timestamp: lastWrite.timestamp,
|
|
126
|
+
};
|
|
127
|
+
console.error(`[STATS-API] Last writer: ${stats.lastWriter.name}`);
|
|
128
|
+
}
|
|
129
|
+
if (lastRead) {
|
|
130
|
+
stats.lastReader = {
|
|
131
|
+
name: normalizeClientName(lastRead.client_name),
|
|
132
|
+
timestamp: lastRead.timestamp,
|
|
133
|
+
};
|
|
134
|
+
console.error(`[STATS-API] Last reader: ${stats.lastReader.name}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Top activity
|
|
138
|
+
const topWriter = await db.get(
|
|
139
|
+
"SELECT client_name, COUNT(*) as count FROM cybermem_access_log WHERE operation = 'create' GROUP BY client_name ORDER BY count DESC LIMIT 1",
|
|
140
|
+
);
|
|
141
|
+
const topReader = await db.get(
|
|
142
|
+
"SELECT client_name, COUNT(*) as count FROM cybermem_access_log WHERE operation = 'read' GROUP BY client_name ORDER BY count DESC LIMIT 1",
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (topWriter)
|
|
146
|
+
stats.topWriter = {
|
|
147
|
+
name: normalizeClientName(topWriter.client_name),
|
|
148
|
+
count: topWriter.count,
|
|
149
|
+
};
|
|
150
|
+
if (topReader)
|
|
151
|
+
stats.topReader = {
|
|
152
|
+
name: normalizeClientName(topReader.client_name),
|
|
153
|
+
count: topReader.count,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// --- TIME SERIES AGGREGATION (Robust Linear Sampling) ---
|
|
157
|
+
let periodMs = 24 * 60 * 60 * 1000; // Default 24h
|
|
158
|
+
if (period === "1h") periodMs = 60 * 60 * 1000;
|
|
159
|
+
else if (period === "7d") periodMs = 7 * 24 * 60 * 60 * 1000;
|
|
160
|
+
else if (period === "30d") periodMs = 30 * 24 * 60 * 60 * 1000;
|
|
161
|
+
else if (period === "90d") periodMs = 90 * 24 * 60 * 60 * 1000;
|
|
162
|
+
else if (period === "24h") periodMs = 24 * 60 * 60 * 1000;
|
|
163
|
+
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
const startTime = now - periodMs;
|
|
166
|
+
|
|
167
|
+
console.error(
|
|
168
|
+
`[STATS-API] Fetching cumulative chart data since ${new Date(startTime).toISOString()}`,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Get all logs in period
|
|
172
|
+
const allLogs = await db.all(
|
|
173
|
+
`
|
|
174
|
+
SELECT timestamp, operation, client_name
|
|
175
|
+
FROM cybermem_access_log
|
|
176
|
+
WHERE timestamp > ?
|
|
177
|
+
ORDER BY timestamp ASC
|
|
178
|
+
`,
|
|
179
|
+
[startTime],
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Get base counts (before startTime) to start the cumulative graph correctly
|
|
183
|
+
const baseCounts = await db.all(
|
|
184
|
+
`
|
|
185
|
+
SELECT operation, client_name, COUNT(*) as count
|
|
186
|
+
FROM cybermem_access_log
|
|
187
|
+
WHERE timestamp <= ?
|
|
188
|
+
GROUP BY 1, 2
|
|
189
|
+
`,
|
|
190
|
+
[startTime],
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const buildBeautifulSeries = (targetOp: string) => {
|
|
194
|
+
const clientTotals: Record<string, number> = {};
|
|
195
|
+
|
|
196
|
+
// 1. Initialize with base counts
|
|
197
|
+
baseCounts
|
|
198
|
+
.filter((b: any) => b.operation === targetOp)
|
|
199
|
+
.forEach((b: any) => {
|
|
200
|
+
clientTotals[b.client_name] = b.count;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const series: any[] = [];
|
|
204
|
+
const opLogs = allLogs.filter((l: any) => l.operation === targetOp);
|
|
205
|
+
|
|
206
|
+
// 2. Linear Sampling (60 points)
|
|
207
|
+
const SAMPLES = 60;
|
|
208
|
+
const interval = (now - startTime) / SAMPLES;
|
|
209
|
+
let currentLogIdx = 0;
|
|
210
|
+
|
|
211
|
+
for (let i = 0; i <= SAMPLES; i++) {
|
|
212
|
+
const timePoint = startTime + i * interval;
|
|
213
|
+
|
|
214
|
+
// Catch up logs that happened before this timePoint
|
|
215
|
+
while (
|
|
216
|
+
currentLogIdx < opLogs.length &&
|
|
217
|
+
opLogs[currentLogIdx].timestamp <= timePoint
|
|
218
|
+
) {
|
|
219
|
+
const log = opLogs[currentLogIdx];
|
|
220
|
+
clientTotals[log.client_name] =
|
|
221
|
+
(clientTotals[log.client_name] || 0) + 1;
|
|
222
|
+
currentLogIdx++;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Record state at this exact linear time point
|
|
226
|
+
series.push({
|
|
227
|
+
time: Math.floor(timePoint / 1000),
|
|
228
|
+
...clientTotals,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return series;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
timeseries.creates = buildBeautifulSeries("create");
|
|
236
|
+
timeseries.reads = buildBeautifulSeries("read");
|
|
237
|
+
timeseries.updates = buildBeautifulSeries("update");
|
|
238
|
+
timeseries.deletes = buildBeautifulSeries("delete");
|
|
239
|
+
|
|
240
|
+
console.error(`[STATS-API] SQLite Stats & Beautiful Charts processed.`);
|
|
241
|
+
|
|
242
|
+
await db.close();
|
|
243
|
+
} catch (dbErr) {
|
|
244
|
+
console.error(
|
|
245
|
+
"[STATS-API] Direct SQLite metrics fetch failed, trying db-exporter fallback:",
|
|
246
|
+
dbErr,
|
|
247
|
+
);
|
|
248
|
+
try {
|
|
249
|
+
const exporterRes = await fetch(`${DB_EXPORTER_URL}/metrics`, {
|
|
250
|
+
signal: controller.signal,
|
|
251
|
+
});
|
|
252
|
+
if (exporterRes.ok) {
|
|
253
|
+
const text = await exporterRes.text();
|
|
254
|
+
const getValue = (name: string) => {
|
|
255
|
+
const match = text.match(new RegExp(`${name}\\s+([\\d.]+)`));
|
|
256
|
+
return match ? parseFloat(match[1]) : 0;
|
|
257
|
+
};
|
|
258
|
+
stats.memoryRecords = getValue("openmemory_memories_total");
|
|
259
|
+
stats.totalRequests = getValue(
|
|
260
|
+
"openmemory_requests_aggregate_total",
|
|
261
|
+
);
|
|
262
|
+
stats.successRate = getValue("openmemory_success_rate_aggregate");
|
|
263
|
+
console.error(
|
|
264
|
+
`[STATS-API] Fallback stats from db-exporter: ${stats.totalRequests} total requests`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
} catch (exporterErr) {
|
|
268
|
+
console.error("[STATS-API] Fallback fetch failed:", exporterErr);
|
|
269
|
+
}
|
|
97
270
|
}
|
|
271
|
+
} else {
|
|
272
|
+
console.error(`[STATS-API] SQLite DB NOT FOUND at ${dbPath}`);
|
|
98
273
|
}
|
|
99
274
|
|
|
275
|
+
clearTimeout(timeoutId);
|
|
276
|
+
|
|
100
277
|
return NextResponse.json({
|
|
101
|
-
stats:
|
|
278
|
+
stats: {
|
|
279
|
+
...stats,
|
|
280
|
+
topWriter: {
|
|
281
|
+
name: normalizeClientName(stats.topWriter?.name || "N/A"),
|
|
282
|
+
count: stats.topWriter?.count || 0,
|
|
283
|
+
},
|
|
284
|
+
topReader: {
|
|
285
|
+
name: normalizeClientName(stats.topReader?.name || "N/A"),
|
|
286
|
+
count: stats.topReader?.count || 0,
|
|
287
|
+
},
|
|
288
|
+
lastWriter: {
|
|
289
|
+
name: normalizeClientName(stats.lastWriter?.name || "N/A"),
|
|
290
|
+
timestamp: stats.lastWriter?.timestamp || 0,
|
|
291
|
+
},
|
|
292
|
+
lastReader: {
|
|
293
|
+
name: normalizeClientName(stats.lastReader?.name || "N/A"),
|
|
294
|
+
timestamp: stats.lastReader?.timestamp || 0,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
102
297
|
timeSeries: {
|
|
103
298
|
creates: normalizeTimeSeries(timeseries.creates || []),
|
|
104
299
|
reads: normalizeTimeSeries(timeseries.reads || []),
|
|
105
300
|
updates: normalizeTimeSeries(timeseries.updates || []),
|
|
106
|
-
deletes: normalizeTimeSeries(timeseries.deletes || [])
|
|
301
|
+
deletes: normalizeTimeSeries(timeseries.deletes || []),
|
|
107
302
|
},
|
|
108
|
-
|
|
109
|
-
sparklines: {
|
|
110
|
-
memoryRecords: [],
|
|
111
|
-
totalRequests: [],
|
|
112
|
-
totalClients: [],
|
|
113
|
-
successRate: []
|
|
114
|
-
},
|
|
115
|
-
clientStats: {
|
|
116
|
-
reads: {},
|
|
117
|
-
writes: {},
|
|
118
|
-
successRate: {}
|
|
119
|
-
}
|
|
120
|
-
})
|
|
303
|
+
});
|
|
121
304
|
} catch (error) {
|
|
122
|
-
console.error(
|
|
123
|
-
return NextResponse.json(
|
|
305
|
+
console.error("Failed to fetch metrics:", error);
|
|
306
|
+
return NextResponse.json(
|
|
307
|
+
{ error: "Failed to fetch metrics" },
|
|
308
|
+
{ status: 500 },
|
|
309
|
+
);
|
|
124
310
|
}
|
|
125
311
|
}
|
package/app/api/reset/route.ts
CHANGED
|
@@ -50,7 +50,7 @@ export async function POST(request: NextRequest) {
|
|
|
50
50
|
message: `Deleted ${deletedCount} database files. Restart openmemory container to reinitialize.`,
|
|
51
51
|
deletedCount,
|
|
52
52
|
restartRequired: true,
|
|
53
|
-
restartCommand: 'docker restart cybermem-
|
|
53
|
+
restartCommand: 'docker restart cybermem-mcp'
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
} catch (fsError: any) {
|
package/app/api/restore/route.ts
CHANGED
|
@@ -59,7 +59,7 @@ export async function POST(request: NextRequest) {
|
|
|
59
59
|
success: true,
|
|
60
60
|
message: 'Database restored successfully. Restart openmemory container to apply.',
|
|
61
61
|
restartRequired: true,
|
|
62
|
-
restartCommand: 'docker restart cybermem-
|
|
62
|
+
restartCommand: 'docker restart cybermem-mcp'
|
|
63
63
|
})
|
|
64
64
|
|
|
65
65
|
} catch (restoreError: any) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { checkRateLimit, rateLimitResponse } from
|
|
2
|
-
import fs from
|
|
3
|
-
import { NextRequest, NextResponse } from
|
|
1
|
+
import { checkRateLimit, rateLimitResponse } from "@/lib/rate-limit";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
4
4
|
|
|
5
|
-
export const dynamic =
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
6
|
|
|
7
|
-
const CONFIG_PATH =
|
|
7
|
+
const CONFIG_PATH = "/data/config.json";
|
|
8
8
|
|
|
9
9
|
export async function GET(request: NextRequest) {
|
|
10
10
|
// Rate limiting check
|
|
@@ -13,39 +13,50 @@ export async function GET(request: NextRequest) {
|
|
|
13
13
|
return rateLimitResponse(rateLimit.resetIn);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
let apiKey = process.env.OM_API_KEY ||
|
|
16
|
+
let apiKey = process.env.OM_API_KEY || "not-set";
|
|
17
17
|
|
|
18
18
|
// Try to read from HttpOnly cookie first
|
|
19
|
-
const cookieKey = request.cookies.get(
|
|
19
|
+
const cookieKey = request.cookies.get("cybermem_api_key")?.value;
|
|
20
20
|
if (cookieKey) {
|
|
21
|
-
apiKey = cookieKey
|
|
21
|
+
apiKey = cookieKey;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// Fallback to config file
|
|
25
25
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
27
|
+
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
28
|
+
const conf = JSON.parse(raw);
|
|
29
|
+
if (conf.api_key && apiKey === "not-set") apiKey = conf.api_key;
|
|
30
|
+
}
|
|
31
31
|
} catch (e) {
|
|
32
|
-
|
|
32
|
+
// ignore
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Endpoint resolution:
|
|
36
36
|
// 1. Env var CYBERMEM_URL
|
|
37
37
|
// 2. Default to localhost:8088/memory (Managed Mode)
|
|
38
38
|
const rawEndpoint = process.env.CYBERMEM_URL;
|
|
39
|
-
const endpoint = rawEndpoint ||
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
const endpoint = rawEndpoint || "http://localhost:8626/memory";
|
|
40
|
+
// isManaged = Local Mode (No Auth). Only if NO URL and NO API KEY.
|
|
41
|
+
// If API Key is present (RPi), we are in "Secure/Legacy" mode, not Local.
|
|
42
|
+
// In local development, rawEndpoint might be unset, but we still want to not be "managed" if we want to test auth flows.
|
|
43
|
+
const isManaged =
|
|
44
|
+
!rawEndpoint &&
|
|
45
|
+
(!process.env.OM_API_KEY || process.env.OM_API_KEY === "dev-secret-key");
|
|
46
|
+
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{
|
|
49
|
+
token: apiKey,
|
|
50
|
+
apiKey: apiKey,
|
|
51
|
+
endpoint,
|
|
52
|
+
isManaged,
|
|
53
|
+
dashboardVersion: "v0.7.5",
|
|
54
|
+
mcpVersion: "v0.7.5",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
headers: {
|
|
58
|
+
"X-RateLimit-Remaining": String(rateLimit.remaining),
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
);
|
|
51
62
|
}
|
|
@@ -6,7 +6,7 @@ export const dynamic = 'force-dynamic'
|
|
|
6
6
|
export async function POST(req: Request) {
|
|
7
7
|
try {
|
|
8
8
|
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
|
|
9
|
-
const container = docker.getContainer('cybermem-
|
|
9
|
+
const container = docker.getContainer('cybermem-mcp');
|
|
10
10
|
|
|
11
11
|
await container.restart();
|
|
12
12
|
|
package/app/layout.tsx
CHANGED
|
@@ -1,37 +1,45 @@
|
|
|
1
|
-
import { DashboardProvider } from "@/lib/data/dashboard-context"
|
|
2
|
-
import { Analytics } from "@vercel/analytics/next"
|
|
3
|
-
import type { Metadata } from "next"
|
|
4
|
-
import { Exo_2, Geist, Geist_Mono } from "next/font/google"
|
|
5
|
-
import type React from "react"
|
|
6
|
-
import "./globals.css"
|
|
1
|
+
import { DashboardProvider } from "@/lib/data/dashboard-context";
|
|
2
|
+
import { Analytics } from "@vercel/analytics/next";
|
|
3
|
+
import type { Metadata } from "next";
|
|
4
|
+
import { Exo_2, Geist, Geist_Mono } from "next/font/google";
|
|
5
|
+
import type React from "react";
|
|
6
|
+
import "./globals.css";
|
|
7
7
|
|
|
8
|
-
const _geist = Geist({ subsets: ["latin"] })
|
|
9
|
-
const _geistMono = Geist_Mono({ subsets: ["latin"] })
|
|
10
|
-
const exo2 = Exo_2({ subsets: ["latin"], variable: "--font-exo2" })
|
|
8
|
+
const _geist = Geist({ subsets: ["latin"] });
|
|
9
|
+
const _geistMono = Geist_Mono({ subsets: ["latin"] });
|
|
10
|
+
const exo2 = Exo_2({ subsets: ["latin"], variable: "--font-exo2" });
|
|
11
11
|
|
|
12
12
|
export const metadata: Metadata = {
|
|
13
13
|
title: "CyberMem",
|
|
14
14
|
description: "Real-time memory operations dashboard",
|
|
15
15
|
icons: {
|
|
16
|
-
icon:
|
|
17
|
-
shortcut:
|
|
18
|
-
apple:
|
|
16
|
+
icon: "/favicon-dark.svg",
|
|
17
|
+
shortcut: "/favicon-dark.svg",
|
|
18
|
+
apple: "/favicon-dark.svg",
|
|
19
19
|
},
|
|
20
|
-
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
import { headers } from "next/headers";
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
// ... imports ...
|
|
25
|
+
|
|
26
|
+
export default async function RootLayout({
|
|
23
27
|
children,
|
|
24
28
|
}: Readonly<{
|
|
25
|
-
children: React.ReactNode
|
|
29
|
+
children: React.ReactNode;
|
|
26
30
|
}>) {
|
|
31
|
+
const headersList = await headers();
|
|
32
|
+
const userId = headersList.get("x-user-id");
|
|
33
|
+
const initialAuth = !!userId;
|
|
34
|
+
|
|
27
35
|
return (
|
|
28
36
|
<html lang="en" suppressHydrationWarning className="relative">
|
|
29
37
|
<body className={`font-sans antialiased relative z-10 ${exo2.variable}`}>
|
|
30
|
-
<DashboardProvider>
|
|
38
|
+
<DashboardProvider initialAuth={initialAuth}>
|
|
31
39
|
{children}
|
|
32
40
|
</DashboardProvider>
|
|
33
41
|
<Analytics />
|
|
34
42
|
</body>
|
|
35
43
|
</html>
|
|
36
|
-
)
|
|
44
|
+
);
|
|
37
45
|
}
|