leedab 0.1.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.
- package/LICENSE +6 -0
- package/README.md +85 -0
- package/bin/leedab.js +626 -0
- package/dist/analytics.d.ts +20 -0
- package/dist/analytics.js +57 -0
- package/dist/audit.d.ts +15 -0
- package/dist/audit.js +46 -0
- package/dist/brand.d.ts +9 -0
- package/dist/brand.js +57 -0
- package/dist/channels/index.d.ts +5 -0
- package/dist/channels/index.js +47 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +49 -0
- package/dist/config/schema.d.ts +58 -0
- package/dist/config/schema.js +21 -0
- package/dist/dashboard/routes.d.ts +5 -0
- package/dist/dashboard/routes.js +410 -0
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +80 -0
- package/dist/dashboard/static/app.js +351 -0
- package/dist/dashboard/static/console.html +252 -0
- package/dist/dashboard/static/favicon.png +0 -0
- package/dist/dashboard/static/index.html +815 -0
- package/dist/dashboard/static/logo-dark.png +0 -0
- package/dist/dashboard/static/logo-light.png +0 -0
- package/dist/dashboard/static/sessions.html +182 -0
- package/dist/dashboard/static/settings.html +274 -0
- package/dist/dashboard/static/style.css +493 -0
- package/dist/dashboard/static/team.html +215 -0
- package/dist/gateway.d.ts +8 -0
- package/dist/gateway.js +213 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/license.d.ts +27 -0
- package/dist/license.js +92 -0
- package/dist/memory/index.d.ts +9 -0
- package/dist/memory/index.js +41 -0
- package/dist/onboard/index.d.ts +4 -0
- package/dist/onboard/index.js +263 -0
- package/dist/onboard/oauth-server.d.ts +13 -0
- package/dist/onboard/oauth-server.js +73 -0
- package/dist/onboard/steps/google.d.ts +12 -0
- package/dist/onboard/steps/google.js +178 -0
- package/dist/onboard/steps/provider.d.ts +10 -0
- package/dist/onboard/steps/provider.js +292 -0
- package/dist/onboard/steps/teams.d.ts +5 -0
- package/dist/onboard/steps/teams.js +51 -0
- package/dist/onboard/steps/telegram.d.ts +6 -0
- package/dist/onboard/steps/telegram.js +88 -0
- package/dist/onboard/steps/welcome.d.ts +1 -0
- package/dist/onboard/steps/welcome.js +10 -0
- package/dist/onboard/steps/whatsapp.d.ts +2 -0
- package/dist/onboard/steps/whatsapp.js +76 -0
- package/dist/openclaw.d.ts +9 -0
- package/dist/openclaw.js +20 -0
- package/dist/team.d.ts +13 -0
- package/dist/team.js +49 -0
- package/dist/templates/verticals/supply-chain/HEARTBEAT.md +12 -0
- package/dist/templates/verticals/supply-chain/SOUL.md +49 -0
- package/dist/templates/verticals/supply-chain/WORKFLOWS.md +148 -0
- package/dist/templates/verticals/supply-chain/vault-template.json +18 -0
- package/dist/templates/workspace/AGENTS.md +181 -0
- package/dist/templates/workspace/BOOTSTRAP.md +32 -0
- package/dist/templates/workspace/HEARTBEAT.md +9 -0
- package/dist/templates/workspace/IDENTITY.md +14 -0
- package/dist/templates/workspace/SOUL.md +32 -0
- package/dist/templates/workspace/TOOLS.md +40 -0
- package/dist/templates/workspace/USER.md +26 -0
- package/dist/vault.d.ts +24 -0
- package/dist/vault.js +123 -0
- package/package.json +58 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { resolveOpenClawBin, openclawEnv } from "../openclaw.js";
|
|
6
|
+
import { addEntry, removeEntry, listEntries } from "../vault.js";
|
|
7
|
+
import { readAuditLog } from "../audit.js";
|
|
8
|
+
import { getAnalytics } from "../analytics.js";
|
|
9
|
+
import { loadTeam, addMember, removeMember, updateRole } from "../team.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
async function restartGatewayQuietly(stateDir) {
|
|
12
|
+
try {
|
|
13
|
+
const bin = resolveOpenClawBin();
|
|
14
|
+
const env = openclawEnv(stateDir);
|
|
15
|
+
await execFileAsync(bin, ["gateway", "health"], { env, timeout: 3000 });
|
|
16
|
+
await execFileAsync(bin, ["gateway", "restart"], { env, timeout: 10000 });
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Gateway not running, no restart needed
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function createRoutes(config) {
|
|
23
|
+
const bin = resolveOpenClawBin();
|
|
24
|
+
const stateDir = resolve(".leedab");
|
|
25
|
+
return {
|
|
26
|
+
/**
|
|
27
|
+
* POST /api/chat — send a message to the agent, get a response
|
|
28
|
+
*/
|
|
29
|
+
"POST /api/chat": async (req, res) => {
|
|
30
|
+
const body = await readBody(req);
|
|
31
|
+
const { message, session } = JSON.parse(body);
|
|
32
|
+
if (!message) {
|
|
33
|
+
json(res, { error: "message is required" }, 400);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const args = [
|
|
38
|
+
"agent",
|
|
39
|
+
"--message", message,
|
|
40
|
+
"--session-id", session ?? "console",
|
|
41
|
+
"--json",
|
|
42
|
+
];
|
|
43
|
+
const { stdout, stderr } = await execFileAsync(bin, args, {
|
|
44
|
+
env: openclawEnv(stateDir),
|
|
45
|
+
timeout: 120000,
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
const result = JSON.parse(stdout);
|
|
49
|
+
// Extract text from various response shapes
|
|
50
|
+
const reply = result.result?.payloads?.[0]?.text ??
|
|
51
|
+
result.reply ??
|
|
52
|
+
result.text ??
|
|
53
|
+
result.content ??
|
|
54
|
+
stdout.trim();
|
|
55
|
+
json(res, { reply, session: session ?? "console" });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
json(res, {
|
|
59
|
+
reply: stdout.trim() || stderr.trim() || "No response",
|
|
60
|
+
session: session ?? "console",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
json(res, { error: err.message }, 500);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* GET /api/chat/history — get conversation history
|
|
70
|
+
*/
|
|
71
|
+
"GET /api/chat/history": async (_req, res, url) => {
|
|
72
|
+
const session = url.searchParams.get("session") ?? "console";
|
|
73
|
+
try {
|
|
74
|
+
const { stdout } = await execFileAsync(bin, ["sessions", "history", "--session-id", session, "--json"], { env: openclawEnv(stateDir) });
|
|
75
|
+
json(res, JSON.parse(stdout));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
json(res, []);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
/**
|
|
82
|
+
* GET /api/status — channel health status
|
|
83
|
+
*/
|
|
84
|
+
"GET /api/status": async (_req, res) => {
|
|
85
|
+
let secrets = {};
|
|
86
|
+
try {
|
|
87
|
+
const raw = await readFile(resolve(stateDir, "secrets.json"), "utf-8");
|
|
88
|
+
secrets = JSON.parse(raw);
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
const status = {
|
|
92
|
+
whatsapp: {
|
|
93
|
+
connected: !!secrets.whatsapp?.connected || !!secrets.whatsapp?.accessToken,
|
|
94
|
+
label: "WhatsApp",
|
|
95
|
+
},
|
|
96
|
+
telegram: {
|
|
97
|
+
connected: !!secrets.telegram?.token,
|
|
98
|
+
label: "Telegram",
|
|
99
|
+
},
|
|
100
|
+
teams: {
|
|
101
|
+
connected: !!secrets.teams?.appSecret,
|
|
102
|
+
label: "Microsoft Teams",
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
json(res, status);
|
|
106
|
+
},
|
|
107
|
+
/**
|
|
108
|
+
* POST /api/whatsapp/connect — register channel and start QR pairing
|
|
109
|
+
*/
|
|
110
|
+
"POST /api/whatsapp/connect": async (_req, res) => {
|
|
111
|
+
try {
|
|
112
|
+
// Register channel if not already added
|
|
113
|
+
await execFileAsync(bin, ["channels", "add", "--channel", "whatsapp"], {
|
|
114
|
+
env: openclawEnv(stateDir),
|
|
115
|
+
}).catch(() => { });
|
|
116
|
+
// Spawn login and capture output (including QR)
|
|
117
|
+
const login = spawn(bin, ["channels", "login", "--channel", "whatsapp"], {
|
|
118
|
+
env: {
|
|
119
|
+
...openclawEnv(stateDir),
|
|
120
|
+
NO_COLOR: "1",
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
let output = "";
|
|
124
|
+
const qrReady = new Promise((resolve, reject) => {
|
|
125
|
+
const timeout = setTimeout(() => {
|
|
126
|
+
resolve(output);
|
|
127
|
+
}, 10000);
|
|
128
|
+
const onData = (chunk) => {
|
|
129
|
+
output += chunk.toString();
|
|
130
|
+
if (output.includes("▄▄██")) {
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
resolve(output);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
login.stdout?.on("data", onData);
|
|
136
|
+
login.stderr?.on("data", onData);
|
|
137
|
+
login.on("error", reject);
|
|
138
|
+
});
|
|
139
|
+
const qrOutput = await qrReady;
|
|
140
|
+
// Mark connected when login completes
|
|
141
|
+
login.on("close", async (code) => {
|
|
142
|
+
if (code === 0) {
|
|
143
|
+
await mkdir(stateDir, { recursive: true });
|
|
144
|
+
let secrets = {};
|
|
145
|
+
try {
|
|
146
|
+
const raw = await readFile(resolve(stateDir, "secrets.json"), "utf-8");
|
|
147
|
+
secrets = JSON.parse(raw);
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
secrets.whatsapp = { connected: true };
|
|
151
|
+
await writeFile(resolve(stateDir, "secrets.json"), JSON.stringify(secrets, null, 2) + "\n");
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
json(res, { status: "pairing", qr: qrOutput });
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
json(res, { error: err.message }, 500);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
/**
|
|
161
|
+
* GET /api/sessions — list all conversation sessions
|
|
162
|
+
*/
|
|
163
|
+
"GET /api/sessions": async (_req, res) => {
|
|
164
|
+
try {
|
|
165
|
+
const { stdout } = await execFileAsync(bin, ["sessions", "--json"], { env: openclawEnv(stateDir), timeout: 10000 });
|
|
166
|
+
const parsed = JSON.parse(stdout);
|
|
167
|
+
const sessions = parsed.sessions ?? (Array.isArray(parsed) ? parsed : []);
|
|
168
|
+
json(res, sessions);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
json(res, []);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
/**
|
|
175
|
+
* GET /api/audit — recent audit log entries
|
|
176
|
+
*/
|
|
177
|
+
"GET /api/audit": async (_req, res, url) => {
|
|
178
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
|
|
179
|
+
const channel = url.searchParams.get("channel") ?? undefined;
|
|
180
|
+
const sinceParam = url.searchParams.get("since");
|
|
181
|
+
const since = sinceParam ? new Date(sinceParam) : undefined;
|
|
182
|
+
const entries = await readAuditLog({ limit, channel, since });
|
|
183
|
+
json(res, entries);
|
|
184
|
+
},
|
|
185
|
+
/**
|
|
186
|
+
* GET /api/analytics — usage analytics summary
|
|
187
|
+
*/
|
|
188
|
+
"GET /api/analytics": async (_req, res, url) => {
|
|
189
|
+
const days = parseInt(url.searchParams.get("days") ?? "30", 10);
|
|
190
|
+
const summary = await getAnalytics(days);
|
|
191
|
+
json(res, summary);
|
|
192
|
+
},
|
|
193
|
+
/**
|
|
194
|
+
* GET /api/vault — list stored credentials (no passwords)
|
|
195
|
+
*/
|
|
196
|
+
"GET /api/vault": async (_req, res) => {
|
|
197
|
+
const entries = await listEntries();
|
|
198
|
+
json(res, entries);
|
|
199
|
+
},
|
|
200
|
+
/**
|
|
201
|
+
* POST /api/vault — add a credential to the vault
|
|
202
|
+
*/
|
|
203
|
+
"POST /api/vault": async (req, res) => {
|
|
204
|
+
const body = await readBody(req);
|
|
205
|
+
const { service, url, username, password, notes } = JSON.parse(body);
|
|
206
|
+
if (!service || typeof service !== "string") {
|
|
207
|
+
json(res, { error: "service name is required" }, 400);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
await addEntry(service.trim(), {
|
|
211
|
+
url: url || undefined,
|
|
212
|
+
username: username || undefined,
|
|
213
|
+
password: password || undefined,
|
|
214
|
+
notes: notes || undefined,
|
|
215
|
+
});
|
|
216
|
+
json(res, { ok: true, service: service.trim() });
|
|
217
|
+
},
|
|
218
|
+
/**
|
|
219
|
+
* POST /api/telegram/connect — validate token and save
|
|
220
|
+
*/
|
|
221
|
+
"POST /api/telegram/connect": async (req, res) => {
|
|
222
|
+
const body = await readBody(req);
|
|
223
|
+
const { token } = JSON.parse(body);
|
|
224
|
+
if (!token || typeof token !== "string") {
|
|
225
|
+
json(res, { error: "token is required" }, 400);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Validate token via Telegram API
|
|
229
|
+
try {
|
|
230
|
+
const tgRes = await fetch(`https://api.telegram.org/bot${token.trim()}/getMe`);
|
|
231
|
+
const data = await tgRes.json();
|
|
232
|
+
if (!data.ok) {
|
|
233
|
+
json(res, { error: "Invalid bot token" }, 400);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// Save to secrets
|
|
237
|
+
await mkdir(stateDir, { recursive: true });
|
|
238
|
+
let secrets = {};
|
|
239
|
+
try {
|
|
240
|
+
const raw = await readFile(resolve(stateDir, "secrets.json"), "utf-8");
|
|
241
|
+
secrets = JSON.parse(raw);
|
|
242
|
+
}
|
|
243
|
+
catch { }
|
|
244
|
+
secrets.telegram = { token: token.trim() };
|
|
245
|
+
await writeFile(resolve(stateDir, "secrets.json"), JSON.stringify(secrets, null, 2) + "\n");
|
|
246
|
+
json(res, { connected: true, username: data.result.username });
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
json(res, { error: "Could not reach Telegram API" }, 500);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
/**
|
|
253
|
+
* GET /api/allowlist — get allowlist for a channel
|
|
254
|
+
* Expects ?channel=<name> query param
|
|
255
|
+
*/
|
|
256
|
+
"GET /api/allowlist": async (_req, res, url) => {
|
|
257
|
+
const channel = url.searchParams.get("channel");
|
|
258
|
+
if (!channel) {
|
|
259
|
+
json(res, { error: "channel query param is required" }, 400);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const raw = await readFile(resolve(stateDir, "openclaw.json"), "utf-8");
|
|
264
|
+
const config = JSON.parse(raw);
|
|
265
|
+
const allowFrom = config.channels?.[channel]?.allowFrom ?? [];
|
|
266
|
+
const dmPolicy = config.channels?.[channel]?.dmPolicy ?? "pairing";
|
|
267
|
+
json(res, { channel, dmPolicy, allowFrom });
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
json(res, { channel, dmPolicy: "pairing", allowFrom: [] });
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
/**
|
|
274
|
+
* POST /api/allowlist — add a user to a channel's allowlist
|
|
275
|
+
*/
|
|
276
|
+
"POST /api/allowlist": async (req, res) => {
|
|
277
|
+
const body = await readBody(req);
|
|
278
|
+
const { channel, userId } = JSON.parse(body);
|
|
279
|
+
if (!channel || !userId) {
|
|
280
|
+
json(res, { error: "channel and userId are required" }, 400);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const configPath = resolve(stateDir, "openclaw.json");
|
|
285
|
+
const config = JSON.parse(await readFile(configPath, "utf-8"));
|
|
286
|
+
if (!config.channels?.[channel]) {
|
|
287
|
+
json(res, { error: `Channel "${channel}" not configured` }, 400);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!config.channels[channel].allowFrom) {
|
|
291
|
+
config.channels[channel].allowFrom = [];
|
|
292
|
+
}
|
|
293
|
+
if (!config.channels[channel].allowFrom.includes(userId)) {
|
|
294
|
+
config.channels[channel].allowFrom.push(userId);
|
|
295
|
+
}
|
|
296
|
+
config.channels[channel].dmPolicy = "allowlist";
|
|
297
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
298
|
+
await restartGatewayQuietly(stateDir);
|
|
299
|
+
json(res, { ok: true });
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
json(res, { error: err.message }, 500);
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
/**
|
|
306
|
+
* DELETE /api/allowlist — remove a user from a channel's allowlist
|
|
307
|
+
* Expects ?channel=<name>&userId=<id> query params
|
|
308
|
+
*/
|
|
309
|
+
"DELETE /api/allowlist": async (_req, res, url) => {
|
|
310
|
+
const channel = url.searchParams.get("channel");
|
|
311
|
+
const userId = url.searchParams.get("userId");
|
|
312
|
+
if (!channel || !userId) {
|
|
313
|
+
json(res, { error: "channel and userId query params are required" }, 400);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
const configPath = resolve(stateDir, "openclaw.json");
|
|
318
|
+
const config = JSON.parse(await readFile(configPath, "utf-8"));
|
|
319
|
+
const list = config.channels?.[channel]?.allowFrom;
|
|
320
|
+
if (!list) {
|
|
321
|
+
json(res, { ok: false, error: "No allowlist for this channel" }, 400);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
config.channels[channel].allowFrom = list.filter((id) => id !== userId);
|
|
325
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
326
|
+
await restartGatewayQuietly(stateDir);
|
|
327
|
+
json(res, { ok: true });
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
json(res, { error: err.message }, 500);
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
/**
|
|
334
|
+
* DELETE /api/vault — remove a credential from the vault
|
|
335
|
+
* Expects ?service=<name> query param
|
|
336
|
+
*/
|
|
337
|
+
"DELETE /api/vault": async (_req, res, url) => {
|
|
338
|
+
const service = url.searchParams.get("service");
|
|
339
|
+
if (!service) {
|
|
340
|
+
json(res, { error: "service query param is required" }, 400);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const removed = await removeEntry(service);
|
|
344
|
+
json(res, { ok: removed, service });
|
|
345
|
+
},
|
|
346
|
+
/**
|
|
347
|
+
* GET /api/team — list team members
|
|
348
|
+
*/
|
|
349
|
+
"GET /api/team": async (_req, res) => {
|
|
350
|
+
const team = await loadTeam();
|
|
351
|
+
json(res, team);
|
|
352
|
+
},
|
|
353
|
+
/**
|
|
354
|
+
* POST /api/team — add a team member
|
|
355
|
+
*/
|
|
356
|
+
"POST /api/team": async (req, res) => {
|
|
357
|
+
const body = await readBody(req);
|
|
358
|
+
const { name, email, role, channels } = JSON.parse(body);
|
|
359
|
+
if (!name || typeof name !== "string") {
|
|
360
|
+
json(res, { error: "name is required" }, 400);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const member = await addMember({
|
|
364
|
+
name: name.trim(),
|
|
365
|
+
email: email || undefined,
|
|
366
|
+
role: role || "operator",
|
|
367
|
+
channels: channels || [],
|
|
368
|
+
});
|
|
369
|
+
json(res, member, 201);
|
|
370
|
+
},
|
|
371
|
+
/**
|
|
372
|
+
* DELETE /api/team — remove a team member
|
|
373
|
+
* Expects ?id=<uuid> query param
|
|
374
|
+
*/
|
|
375
|
+
"DELETE /api/team": async (_req, res, url) => {
|
|
376
|
+
const id = url.searchParams.get("id");
|
|
377
|
+
if (!id) {
|
|
378
|
+
json(res, { error: "id query param is required" }, 400);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const removed = await removeMember(id);
|
|
382
|
+
json(res, { ok: removed });
|
|
383
|
+
},
|
|
384
|
+
/**
|
|
385
|
+
* PUT /api/team/role — update a member's role
|
|
386
|
+
*/
|
|
387
|
+
"PUT /api/team/role": async (req, res) => {
|
|
388
|
+
const body = await readBody(req);
|
|
389
|
+
const { id, role } = JSON.parse(body);
|
|
390
|
+
if (!id || !role) {
|
|
391
|
+
json(res, { error: "id and role are required" }, 400);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const updated = await updateRole(id, role);
|
|
395
|
+
json(res, { ok: updated });
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function json(res, data, status = 200) {
|
|
400
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
401
|
+
res.end(JSON.stringify(data));
|
|
402
|
+
}
|
|
403
|
+
function readBody(req) {
|
|
404
|
+
return new Promise((resolve, reject) => {
|
|
405
|
+
let body = "";
|
|
406
|
+
req.on("data", (chunk) => (body += chunk));
|
|
407
|
+
req.on("end", () => resolve(body));
|
|
408
|
+
req.on("error", reject);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve, extname } from "node:path";
|
|
4
|
+
import { createRoutes } from "./routes.js";
|
|
5
|
+
const MIME_TYPES = {
|
|
6
|
+
".html": "text/html",
|
|
7
|
+
".css": "text/css",
|
|
8
|
+
".js": "application/javascript",
|
|
9
|
+
".json": "application/json",
|
|
10
|
+
".svg": "image/svg+xml",
|
|
11
|
+
".png": "image/png",
|
|
12
|
+
};
|
|
13
|
+
export async function startDashboard(config, port = 3000) {
|
|
14
|
+
const routes = createRoutes(config);
|
|
15
|
+
const staticDir = resolve(import.meta.dirname, "static");
|
|
16
|
+
const server = createServer(async (req, res) => {
|
|
17
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
18
|
+
const method = req.method ?? "GET";
|
|
19
|
+
// API routes
|
|
20
|
+
if (url.pathname.startsWith("/api/")) {
|
|
21
|
+
const handler = routes[`${method} ${url.pathname}`];
|
|
22
|
+
if (handler) {
|
|
23
|
+
try {
|
|
24
|
+
await handler(req, res, url);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
28
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
33
|
+
res.end(JSON.stringify({ error: "not found" }));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Static files — try .html extension for clean URLs (/console -> /console.html)
|
|
37
|
+
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
38
|
+
if (!extname(filePath))
|
|
39
|
+
filePath += ".html";
|
|
40
|
+
try {
|
|
41
|
+
const fullPath = resolve(staticDir, filePath.slice(1));
|
|
42
|
+
const content = await readFile(fullPath);
|
|
43
|
+
const ext = extname(fullPath);
|
|
44
|
+
res.writeHead(200, { "Content-Type": MIME_TYPES[ext] ?? "text/plain" });
|
|
45
|
+
res.end(content);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// SPA fallback
|
|
49
|
+
try {
|
|
50
|
+
const index = await readFile(resolve(staticDir, "index.html"));
|
|
51
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
52
|
+
res.end(index);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
res.writeHead(404);
|
|
56
|
+
res.end("Not found");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
server.on("error", (err) => {
|
|
62
|
+
if (err.code === "EADDRINUSE") {
|
|
63
|
+
// Try next port
|
|
64
|
+
const nextPort = port + 1;
|
|
65
|
+
console.log(` Port ${port} in use, trying ${nextPort}...`);
|
|
66
|
+
server.listen(nextPort, "127.0.0.1");
|
|
67
|
+
server.once("listening", () => {
|
|
68
|
+
console.log(` Dashboard: http://localhost:${nextPort}`);
|
|
69
|
+
resolve();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
reject(err);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
server.listen(port, "127.0.0.1", () => {
|
|
77
|
+
resolve();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|