4bnode 4.1.0 → 4.1.1
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/README.md +25 -206
- package/add-crud.js +6 -106
- package/add-docs.js +91 -0
- package/add-email.js +63 -0
- package/add-security.js +143 -0
- package/index.js +76 -8
- package/lib/codegen.js +1223 -0
- package/lib/codegen.test.js +480 -0
- package/lib/indexFile.js +10 -12
- package/lib/names.js +4 -21
- package/lib/routeFile.js +9 -27
- package/lib/sync-skeleton.js +47 -0
- package/lib/ui.js +15 -2
- package/package.json +4 -2
- package/scripts/audit-dev-surface.mjs +153 -0
- package/scripts/reveal-passkey.mjs +35 -0
- package/skeleton/.4bnode/assets/js/App.js +31 -9
- package/skeleton/.4bnode/assets/js/Root.js +1 -5
- package/skeleton/.4bnode/assets/js/components/Sidebar.js +9 -5
- package/skeleton/.4bnode/assets/js/lib.js +18 -5
- package/skeleton/.4bnode/assets/js/pages/AiBuilderPage.js +268 -0
- package/skeleton/.4bnode/assets/js/pages/ApiKeysPage.js +164 -0
- package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +275 -9
- package/skeleton/.4bnode/assets/js/pages/DatabasePage.js +44 -205
- package/skeleton/.4bnode/assets/js/pages/DocsPage.js +142 -0
- package/skeleton/.4bnode/assets/js/pages/MailPage.js +134 -0
- package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +5 -266
- package/skeleton/.4bnode/assets/js/pages/SchemasPage.js +23 -213
- package/skeleton/.4bnode/assets/js/pages/SecurityPage.js +74 -0
- package/skeleton/.4bnode/assets/js/screens/PasskeyScreen.js +61 -61
- package/skeleton/.4bnode/codegen.js +1223 -0
- package/skeleton/.4bnode/dev-api.js +634 -728
- package/skeleton/.4bnode/ui.html +62 -40
- package/skeleton/_gitignore +7 -0
- package/skeleton/index.js +46 -5
- package/skeleton/package.json +4 -1
- package/skeleton/src/middleware/apiKey.js +36 -0
- package/skeleton/src/middleware/errorHandler.js +15 -0
- package/skeleton/src/middleware/roles.js +14 -0
- package/skeleton/src/middleware/validate.js +20 -0
- package/skeleton/.4bnode/assets/images/mysql.png +0 -0
- package/skeleton/.4bnode/assets/images/postgresql.png +0 -0
- package/skeleton/.4bnode/assets/images/sqlite.png +0 -0
- package/skeleton/.4bnode/assets/js/pages/ReportsPage.js +0 -667
- package/skeleton/.4bnode/assets/js/screens/SplashScreen.js +0 -66
package/lib/ui.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
1
3
|
import chalk from "chalk";
|
|
2
4
|
import gradient from "gradient-string";
|
|
3
5
|
import ora from "ora";
|
|
6
|
+
import { addDepsToPackageJson } from "./codegen.js";
|
|
4
7
|
|
|
5
8
|
// Brand gradients
|
|
6
9
|
const brand = gradient(["#00c6ff", "#0072ff", "#7b2ff7"]);
|
|
@@ -132,14 +135,24 @@ export function showNextSteps(steps) {
|
|
|
132
135
|
|
|
133
136
|
export async function installDeps(packages, opts = {}) {
|
|
134
137
|
const { execSync } = await import("child_process");
|
|
138
|
+
const { dev = false, ...execOpts } = opts;
|
|
135
139
|
const names = Array.isArray(packages) ? packages : [packages];
|
|
136
140
|
const label = names.join(", ");
|
|
141
|
+
// Record in package.json first, so the app is installable even if the live
|
|
142
|
+
// install fails (offline) — a later `npm install` then resolves everything.
|
|
143
|
+
try {
|
|
144
|
+
const pkgPath = path.join(execOpts.cwd || process.cwd(), "package.json");
|
|
145
|
+
if (fs.existsSync(pkgPath)) {
|
|
146
|
+
const { content, changed } = addDepsToPackageJson(fs.readFileSync(pkgPath, "utf8"), names, { dev });
|
|
147
|
+
if (changed) fs.writeFileSync(pkgPath, content, "utf8");
|
|
148
|
+
}
|
|
149
|
+
} catch {}
|
|
137
150
|
const spinner = createSpinner(`Installing ${label}...`);
|
|
138
151
|
spinner.start();
|
|
139
152
|
try {
|
|
140
|
-
execSync(`npm install ${names.join(" ")}`, {
|
|
153
|
+
execSync(`npm install ${dev ? "-D " : ""}${names.join(" ")}`, {
|
|
141
154
|
stdio: "pipe",
|
|
142
|
-
...
|
|
155
|
+
...execOpts,
|
|
143
156
|
});
|
|
144
157
|
spinner.succeed(success(`Installed ${label}`));
|
|
145
158
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "4bnode",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "4bnode is a CLI-powered backend development platform with a built-in visual dashboard to generate, manage, and test Node.js/Express APIs faster.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"4bnode": "index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "node lib/codegen.test.js",
|
|
12
|
+
"sync": "node lib/sync-skeleton.js",
|
|
13
|
+
"prepublishOnly": "npm run sync"
|
|
12
14
|
},
|
|
13
15
|
"repository": {
|
|
14
16
|
"type": "git",
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Audit the /_dev dashboard exposure across 4bnode apps (read-only).
|
|
3
|
+
//
|
|
4
|
+
// node scripts/audit-dev-surface.mjs <dir> [--since=2026-06-07]
|
|
5
|
+
//
|
|
6
|
+
// Scans <dir> recursively for 4bnode apps (any folder containing
|
|
7
|
+
// .4bnode/dev-api.js) and reports, per app:
|
|
8
|
+
// • whether the /_dev mount is gated on NODE_ENV (production-safe)
|
|
9
|
+
// • whether dev-api.js carries the loopback network guard
|
|
10
|
+
// • whether a .passkey exists and whether it is a known-weak PIN
|
|
11
|
+
// • route/source files modified on/after --since, and any that contain
|
|
12
|
+
// code-execution / cryptominer signatures (possible injected backdoor)
|
|
13
|
+
//
|
|
14
|
+
// Nothing is modified. Rotate weak/shared passkeys with `npx 4bnode passkey`.
|
|
15
|
+
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import crypto from 'crypto';
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const root = path.resolve(args.find((a) => !a.startsWith('--')) || '.');
|
|
22
|
+
const sinceArg = (args.find((a) => a.startsWith('--since=')) || '').split('=')[1];
|
|
23
|
+
const since = sinceArg ? new Date(sinceArg) : null;
|
|
24
|
+
|
|
25
|
+
const sha = (s) => crypto.createHash('sha256').update(s).digest('hex');
|
|
26
|
+
// Common weak PINs operators reuse. Hashes compared against .4bnode/.passkey.
|
|
27
|
+
const WEAK_PINS = ['123456', '000000', '111111', '121212', '123123', '654321',
|
|
28
|
+
'666666', '999999', '112233', '159753', '147258', '101010', '696969', '777777',
|
|
29
|
+
'123321', '789456', '000001', '222222', '555555', '888888', '123654', '098765'];
|
|
30
|
+
const WEAK_HASHES = new Map(WEAK_PINS.map((p) => [sha(p), p]));
|
|
31
|
+
|
|
32
|
+
// Signatures of code execution / miners that should never appear in generated routes.
|
|
33
|
+
const BACKDOOR_PATTERNS = [
|
|
34
|
+
[/child_process|\brequire\(\s*['"]child_process['"]\s*\)/, 'child_process import'],
|
|
35
|
+
[/\bexec(Sync)?\s*\(/, 'exec() call'],
|
|
36
|
+
[/\bspawn(Sync)?\s*\(/, 'spawn() call'],
|
|
37
|
+
[/\beval\s*\(/, 'eval()'],
|
|
38
|
+
[/new\s+Function\s*\(/, 'new Function()'],
|
|
39
|
+
[/Buffer\.from\([^)]*,\s*['"]base64['"]\)/, 'base64-decoded payload'],
|
|
40
|
+
[/(curl|wget)\s+[^\n|]*\|\s*(sh|bash)/, 'pipe-to-shell download'],
|
|
41
|
+
[/xmrig|minerd|stratum\+tcp|coinhive|cryptonight|nanopool|supportxmr|nicehash/i, 'cryptominer signature'],
|
|
42
|
+
[/\/dev\/tcp\//, 'reverse-shell /dev/tcp'],
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const C = { red: (s) => `\x1b[31m${s}\x1b[0m`, yel: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
46
|
+
grn: (s) => `\x1b[32m${s}\x1b[0m`, dim: (s) => `\x1b[2m${s}\x1b[0m`, bold: (s) => `\x1b[1m${s}\x1b[0m` };
|
|
47
|
+
|
|
48
|
+
function findApps(dir, depth = 0, acc = []) {
|
|
49
|
+
if (depth > 4) return acc;
|
|
50
|
+
let entries;
|
|
51
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return acc; }
|
|
52
|
+
if (entries.some((e) => e.isDirectory() && e.name === '.4bnode') &&
|
|
53
|
+
fs.existsSync(path.join(dir, '.4bnode', 'dev-api.js'))) {
|
|
54
|
+
acc.push(dir);
|
|
55
|
+
}
|
|
56
|
+
for (const e of entries) {
|
|
57
|
+
if (e.isDirectory() && e.name !== 'node_modules' && e.name !== '.git' && e.name !== '.4bnode') {
|
|
58
|
+
findApps(path.join(dir, e.name), depth + 1, acc);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return acc;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function scanSourceFiles(appDir) {
|
|
65
|
+
const targets = ['index.js', 'src'];
|
|
66
|
+
const files = [];
|
|
67
|
+
const walk = (p) => {
|
|
68
|
+
let st; try { st = fs.statSync(p); } catch { return; }
|
|
69
|
+
if (st.isDirectory()) {
|
|
70
|
+
if (path.basename(p) === 'node_modules' || path.basename(p) === '.git') return;
|
|
71
|
+
for (const c of fs.readdirSync(p)) walk(path.join(p, c));
|
|
72
|
+
} else if (p.endsWith('.js') || p.endsWith('.mjs') || p.endsWith('.cjs')) {
|
|
73
|
+
files.push(p);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
for (const t of targets) walk(path.join(appDir, t));
|
|
77
|
+
const findings = [];
|
|
78
|
+
for (const f of files) {
|
|
79
|
+
let content, mtime;
|
|
80
|
+
try { content = fs.readFileSync(f, 'utf8'); mtime = fs.statSync(f).mtime; } catch { continue; }
|
|
81
|
+
const rel = path.relative(appDir, f);
|
|
82
|
+
const hits = [];
|
|
83
|
+
const lines = content.split('\n');
|
|
84
|
+
for (const [re, label] of BACKDOOR_PATTERNS) {
|
|
85
|
+
for (let i = 0; i < lines.length; i++) {
|
|
86
|
+
if (re.test(lines[i])) hits.push(`${rel}:${i + 1} — ${label}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const recent = since && mtime >= since;
|
|
90
|
+
if (hits.length || recent) findings.push({ rel, mtime, hits, recent });
|
|
91
|
+
}
|
|
92
|
+
return findings;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function auditApp(appDir) {
|
|
96
|
+
const name = path.basename(appDir);
|
|
97
|
+
const issues = [];
|
|
98
|
+
// 1. Mount gating
|
|
99
|
+
const idx = path.join(appDir, 'index.js');
|
|
100
|
+
if (fs.existsSync(idx)) {
|
|
101
|
+
const c = fs.readFileSync(idx, 'utf8');
|
|
102
|
+
const mountIdx = Math.max(c.indexOf('app.use("/_dev"'), c.indexOf("app.use('/_dev'"));
|
|
103
|
+
if (mountIdx !== -1) {
|
|
104
|
+
const gateIdx = c.search(/NODE_ENV\s*!==\s*['"]production['"]/);
|
|
105
|
+
const gated = gateIdx !== -1 && gateIdx < mountIdx;
|
|
106
|
+
if (!gated) issues.push([C.red('CRITICAL'), '/_dev is mounted WITHOUT a NODE_ENV production gate']);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// 2. Loopback guard in dev-api.js
|
|
110
|
+
const devApi = path.join(appDir, '.4bnode', 'dev-api.js');
|
|
111
|
+
const devApiSrc = fs.existsSync(devApi) ? fs.readFileSync(devApi, 'utf8') : '';
|
|
112
|
+
if (devApiSrc && !devApiSrc.includes('isLoopbackRequest')) {
|
|
113
|
+
issues.push([C.red('CRITICAL'), 'dev-api.js has NO loopback network guard (reachable off-host)']);
|
|
114
|
+
}
|
|
115
|
+
// 3. Passkey
|
|
116
|
+
const pkFile = path.join(appDir, '.4bnode', '.passkey');
|
|
117
|
+
if (!fs.existsSync(pkFile)) {
|
|
118
|
+
issues.push([C.red('CRITICAL'), 'NO .passkey file — dashboard auth is open (anyone gets in)']);
|
|
119
|
+
} else {
|
|
120
|
+
const h = fs.readFileSync(pkFile, 'utf8').trim();
|
|
121
|
+
if (WEAK_HASHES.has(h)) {
|
|
122
|
+
issues.push([C.red('CRITICAL'), `weak/guessable passkey "${WEAK_HASHES.get(h)}" — rotate with \`npx 4bnode passkey\``]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// 4. Backdoor / recent-change scan
|
|
126
|
+
const findings = scanSourceFiles(appDir);
|
|
127
|
+
const backdoors = findings.filter((f) => f.hits.length);
|
|
128
|
+
const recents = findings.filter((f) => f.recent && !f.hits.length);
|
|
129
|
+
|
|
130
|
+
console.log(`\n${C.bold(name)} ${C.dim(appDir)}`);
|
|
131
|
+
if (!issues.length && !backdoors.length) {
|
|
132
|
+
console.log(` ${C.grn('✔')} no exposure issues found`);
|
|
133
|
+
}
|
|
134
|
+
for (const [sev, msg] of issues) console.log(` ${sev} ${msg}`);
|
|
135
|
+
for (const f of backdoors) {
|
|
136
|
+
console.log(` ${C.red('BACKDOOR?')} ${f.rel} ${C.dim('(modified ' + f.mtime.toISOString() + ')')}`);
|
|
137
|
+
for (const h of f.hits) console.log(` ${C.yel(h)}`);
|
|
138
|
+
}
|
|
139
|
+
if (recents.length) {
|
|
140
|
+
console.log(` ${C.yel('REVIEW')} ${recents.length} source file(s) modified since ${sinceArg}:`);
|
|
141
|
+
for (const f of recents) console.log(` ${f.rel} ${C.dim(f.mtime.toISOString())}`);
|
|
142
|
+
}
|
|
143
|
+
return { name, critical: issues.filter((i) => i[0].includes('CRITICAL')).length + backdoors.length };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(C.bold(`\n4bnode /_dev exposure audit — ${root}`));
|
|
147
|
+
if (since) console.log(C.dim(`Flagging source changed on/after ${sinceArg}`));
|
|
148
|
+
const apps = findApps(root);
|
|
149
|
+
if (!apps.length) { console.log(C.yel('\nNo 4bnode apps (.4bnode/dev-api.js) found under that path.')); process.exit(0); }
|
|
150
|
+
let totalCritical = 0;
|
|
151
|
+
for (const app of apps) totalCritical += auditApp(app).critical;
|
|
152
|
+
console.log(`\n${C.bold('Summary:')} ${apps.length} app(s) scanned, ${totalCritical ? C.red(totalCritical + ' critical finding(s)') : C.grn('0 critical findings')}.`);
|
|
153
|
+
process.exit(totalCritical ? 1 : 0);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Reveal the 6-digit dashboard PIN for a 4bnode app by reversing its hash.
|
|
3
|
+
//
|
|
4
|
+
// node scripts/reveal-passkey.mjs /path/to/app
|
|
5
|
+
//
|
|
6
|
+
// The PIN is stored as a SHA-256 hash in .4bnode/.passkey. A 6-digit PIN has
|
|
7
|
+
// only 1,000,000 possibilities, so we just hash them all and match. (This is
|
|
8
|
+
// exactly why a 6-digit PIN is weak — but it lets you recover/audit it.)
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import crypto from 'crypto';
|
|
13
|
+
|
|
14
|
+
const appDir = path.resolve(process.argv[2] || '.');
|
|
15
|
+
const pkFile = path.join(appDir, '.4bnode', '.passkey');
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(pkFile)) {
|
|
18
|
+
console.log(`No .passkey file in ${path.join(appDir, '.4bnode')} — the dashboard had NO password set (open).`);
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const target = fs.readFileSync(pkFile, 'utf8').trim();
|
|
23
|
+
const sha = (s) => crypto.createHash('sha256').update(s).digest('hex');
|
|
24
|
+
|
|
25
|
+
// Try all 6-digit PINs (000000–999999), including shorter numeric values.
|
|
26
|
+
for (let n = 0; n < 1_000_000; n++) {
|
|
27
|
+
const pin = String(n).padStart(6, '0');
|
|
28
|
+
if (sha(pin) === target) { console.log(`PIN: ${pin}`); process.exit(0); }
|
|
29
|
+
}
|
|
30
|
+
// Fallback: also try the value un-padded (e.g. "1234") in case it wasn't 6 digits.
|
|
31
|
+
for (let n = 0; n < 1_000_000; n++) {
|
|
32
|
+
if (sha(String(n)) === target) { console.log(`PIN: ${n}`); process.exit(0); }
|
|
33
|
+
}
|
|
34
|
+
console.log('Not a numeric PIN up to 6 digits — the passkey is something else (longer/alphanumeric).');
|
|
35
|
+
console.log(`Stored hash: ${target}`);
|
|
@@ -9,11 +9,15 @@ import { DashboardPage } from "./pages/DashboardPage.js";
|
|
|
9
9
|
import { DatabasePage } from "./pages/DatabasePage.js";
|
|
10
10
|
import { SchemasPage } from "./pages/SchemasPage.js";
|
|
11
11
|
import { RoutesPage } from "./pages/RoutesPage.js";
|
|
12
|
+
import { DocsPage } from "./pages/DocsPage.js";
|
|
13
|
+
import { AiBuilderPage } from "./pages/AiBuilderPage.js";
|
|
14
|
+
import { SecurityPage } from "./pages/SecurityPage.js";
|
|
15
|
+
import { ApiKeysPage } from "./pages/ApiKeysPage.js";
|
|
12
16
|
import { ApiTesterPage } from "./pages/ApiTesterPage.js";
|
|
13
17
|
import { RealtimePage } from "./pages/RealtimePage.js";
|
|
14
18
|
import { SerialPortPage } from "./pages/SerialPortPage.js";
|
|
19
|
+
import { MailPage } from "./pages/MailPage.js";
|
|
15
20
|
import { LogsPage } from "./pages/LogsPage.js";
|
|
16
|
-
import { ReportsPage } from "./pages/ReportsPage.js";
|
|
17
21
|
import { EnvironmentPage } from "./pages/EnvironmentPage.js";
|
|
18
22
|
|
|
19
23
|
function getHashPage() {
|
|
@@ -61,12 +65,27 @@ export function App() {
|
|
|
61
65
|
}, []);
|
|
62
66
|
|
|
63
67
|
const refresh = useCallback(async () => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// File-writing actions (create route, docs, security, ...) modify index.js,
|
|
69
|
+
// which triggers a nodemon restart. Since the dashboard is served by that same
|
|
70
|
+
// app, the refresh fetch fired right after can hit the restart window and fail,
|
|
71
|
+
// leaving the UI stale until a manual reload. Retry across the restart so new
|
|
72
|
+
// state shows up in real time.
|
|
73
|
+
let lastErr;
|
|
74
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
75
|
+
try {
|
|
76
|
+
const [dashData, statusData] = await Promise.all([
|
|
77
|
+
api("/dashboard"),
|
|
78
|
+
api("/status"),
|
|
79
|
+
]);
|
|
80
|
+
setData(dashData);
|
|
81
|
+
setStatus(statusData);
|
|
82
|
+
return;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
lastErr = err;
|
|
85
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw lastErr;
|
|
70
89
|
}, []);
|
|
71
90
|
|
|
72
91
|
useEffect(() => {
|
|
@@ -75,13 +94,17 @@ export function App() {
|
|
|
75
94
|
|
|
76
95
|
const pages = {
|
|
77
96
|
dashboard: html`<${DashboardPage} />`,
|
|
97
|
+
"ai-builder": html`<${AiBuilderPage} />`,
|
|
78
98
|
database: html`<${DatabasePage} />`,
|
|
79
99
|
schemas: html`<${SchemasPage} />`,
|
|
80
100
|
routes: html`<${RoutesPage} />`,
|
|
101
|
+
docs: html`<${DocsPage} />`,
|
|
102
|
+
security: html`<${SecurityPage} />`,
|
|
103
|
+
"api-keys": html`<${ApiKeysPage} />`,
|
|
81
104
|
"api-tester": html`<${ApiTesterPage} />`,
|
|
82
105
|
realtime: html`<${RealtimePage} />`,
|
|
83
106
|
serialport: html`<${SerialPortPage} />`,
|
|
84
|
-
|
|
107
|
+
mail: html`<${MailPage} />`,
|
|
85
108
|
logs: html`<${LogsPage} />`,
|
|
86
109
|
env: html`<${EnvironmentPage} />`,
|
|
87
110
|
};
|
|
@@ -93,7 +116,6 @@ export function App() {
|
|
|
93
116
|
<div className="main">
|
|
94
117
|
<div className="topbar">
|
|
95
118
|
<h2>${PAGE_TITLES[page] || "Dashboard"}</h2>
|
|
96
|
-
<span className="topbar-badge">Development</span>
|
|
97
119
|
</div>
|
|
98
120
|
<div className="content">
|
|
99
121
|
<${AnimatePresence} mode="wait">
|
|
@@ -3,11 +3,9 @@ import { AnimatePresence } from "framer-motion";
|
|
|
3
3
|
import { motion } from "framer-motion";
|
|
4
4
|
import { html, API, getStoredPasskey } from "./lib.js";
|
|
5
5
|
import { App } from "./App.js";
|
|
6
|
-
import { SplashScreen } from "./screens/SplashScreen.js";
|
|
7
6
|
import { PasskeyScreen } from "./screens/PasskeyScreen.js";
|
|
8
7
|
|
|
9
8
|
export function Root() {
|
|
10
|
-
const [showSplash, setShowSplash] = useState(true);
|
|
11
9
|
const [authed, setAuthed] = useState(false);
|
|
12
10
|
const [checking, setChecking] = useState(true);
|
|
13
11
|
|
|
@@ -29,9 +27,7 @@ export function Root() {
|
|
|
29
27
|
if (checking) return null;
|
|
30
28
|
|
|
31
29
|
return html`<${AnimatePresence} mode="wait">
|
|
32
|
-
${
|
|
33
|
-
? html`<${SplashScreen} key="splash" onDone=${() => setShowSplash(false)} />`
|
|
34
|
-
: !authed
|
|
30
|
+
${!authed
|
|
35
31
|
? html`<${PasskeyScreen} key="passkey" onSuccess=${() => setAuthed(true)} />`
|
|
36
32
|
: html`<${motion.div} key="app"
|
|
37
33
|
style=${{ display: 'flex', height: '100vh', width: '100%' }}
|
|
@@ -13,6 +13,8 @@ export function Sidebar() {
|
|
|
13
13
|
icon: Icons.dashboard,
|
|
14
14
|
section: "Overview",
|
|
15
15
|
},
|
|
16
|
+
// AI Builder — temporarily hidden from the menu (re-enable by uncommenting)
|
|
17
|
+
// { id: "ai-builder", label: "AI Builder", icon: Icons.ai },
|
|
16
18
|
{
|
|
17
19
|
id: "database",
|
|
18
20
|
label: "Database",
|
|
@@ -27,15 +29,18 @@ export function Sidebar() {
|
|
|
27
29
|
lockMsg: "Requires database",
|
|
28
30
|
},
|
|
29
31
|
{ id: "routes", label: "API Routes", icon: Icons.route },
|
|
32
|
+
{ id: "docs", label: "API Docs", icon: Icons.docs },
|
|
30
33
|
{ id: "api-tester", label: "API Tester", icon: Icons.apiTester },
|
|
31
34
|
{
|
|
32
|
-
id: "
|
|
33
|
-
label: "
|
|
34
|
-
icon: Icons.
|
|
35
|
+
id: "security",
|
|
36
|
+
label: "Security",
|
|
37
|
+
icon: Icons.shield,
|
|
35
38
|
section: "Features",
|
|
36
39
|
},
|
|
40
|
+
{ id: "api-keys", label: "API Keys", icon: Icons.apiKeys },
|
|
41
|
+
{ id: "realtime", label: "Real-time", icon: Icons.realtime },
|
|
37
42
|
{ id: "serialport", label: "SerialPort", icon: Icons.serial },
|
|
38
|
-
{ id: "
|
|
43
|
+
{ id: "mail", label: "Email", icon: Icons.mail },
|
|
39
44
|
{ id: "logs", label: "Logs", icon: Icons.logs, section: "Settings" },
|
|
40
45
|
{ id: "env", label: "Environment", icon: Icons.env },
|
|
41
46
|
];
|
|
@@ -44,7 +49,6 @@ export function Sidebar() {
|
|
|
44
49
|
return html`<div className="sidebar">
|
|
45
50
|
<div className="sidebar-brand">
|
|
46
51
|
<pre>${`██╗ ██╗██████╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗\n██║ ██║██╔══██╗████╗ ██║██╔═══██╗██╔══██╗██╔════╝\n███████║██████╔╝██╔██╗ ██║██║ ██║██║ ██║█████╗\n╚════██║██╔══██╗██║╚██╗██║██║ ██║██║ ██║██╔══╝\n ██║██████╔╝██║ ╚████║╚██████╔╝██████╔╝███████╗\n ╚═╝╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝`}</pre>
|
|
47
|
-
<span>Project Dashboard</span>
|
|
48
52
|
</div>
|
|
49
53
|
${navItems.map((item) => {
|
|
50
54
|
const sectionHeader =
|
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
LuSend,
|
|
21
21
|
LuSettings,
|
|
22
22
|
LuScrollText,
|
|
23
|
-
|
|
23
|
+
LuSparkles,
|
|
24
|
+
LuBookOpen,
|
|
25
|
+
LuMail,
|
|
26
|
+
LuKeyRound,
|
|
24
27
|
} from "react-icons/lu";
|
|
25
28
|
|
|
26
29
|
export const html = htm.bind(React.createElement);
|
|
@@ -61,8 +64,11 @@ export async function api(path, options = {}) {
|
|
|
61
64
|
if (res.ok) return { message: "Done" };
|
|
62
65
|
throw new Error("Server restarting, please wait...");
|
|
63
66
|
}
|
|
64
|
-
if (!res.ok)
|
|
65
|
-
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const e = new Error(data.error || data.message || "Request failed");
|
|
69
|
+
e.data = data; // expose extra fields (e.g. command output) to callers
|
|
70
|
+
throw e;
|
|
71
|
+
}
|
|
66
72
|
return data;
|
|
67
73
|
}
|
|
68
74
|
|
|
@@ -82,22 +88,29 @@ export const Icons = {
|
|
|
82
88
|
serial: html`<${LuCable} size=${16} />`,
|
|
83
89
|
zap: html`<${LuZap} size=${16} />`,
|
|
84
90
|
shield: html`<${LuShield} size=${16} />`,
|
|
91
|
+
apiKeys: html`<${LuKeyRound} size=${16} />`,
|
|
85
92
|
box: html`<${LuBox} size=${16} />`,
|
|
86
93
|
apiTester: html`<${LuFlaskConical} size=${16} />`,
|
|
87
94
|
send: html`<${LuSend} size=${16} />`,
|
|
88
95
|
logs: html`<${LuScrollText} size=${16} />`,
|
|
89
|
-
|
|
96
|
+
ai: html`<${LuSparkles} size=${16} />`,
|
|
97
|
+
docs: html`<${LuBookOpen} size=${16} />`,
|
|
98
|
+
mail: html`<${LuMail} size=${16} />`,
|
|
90
99
|
};
|
|
91
100
|
|
|
92
101
|
export const PAGE_TITLES = {
|
|
93
102
|
dashboard: "Dashboard",
|
|
103
|
+
"ai-builder": "AI Builder",
|
|
94
104
|
database: "Database",
|
|
95
105
|
schemas: "Schemas",
|
|
96
106
|
routes: "API Routes",
|
|
107
|
+
docs: "API Docs",
|
|
108
|
+
security: "Security",
|
|
109
|
+
"api-keys": "API Keys",
|
|
97
110
|
"api-tester": "API Tester",
|
|
98
111
|
realtime: "Real-time",
|
|
99
112
|
serialport: "SerialPort",
|
|
113
|
+
mail: "Email",
|
|
100
114
|
logs: "Logs",
|
|
101
|
-
reports: "Reports",
|
|
102
115
|
env: "Environment",
|
|
103
116
|
};
|