4bnode 4.0.0 → 4.0.3

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 (39) hide show
  1. package/README.md +10 -0
  2. package/add-login.js +2 -2
  3. package/add-serialport.js +1 -1
  4. package/index.js +60 -6
  5. package/lib/indexFile.js +60 -13
  6. package/lib/ui.js +1 -0
  7. package/package.json +1 -1
  8. package/skeleton/.4bnode/assets/js/App.js +115 -0
  9. package/skeleton/.4bnode/assets/js/Root.js +44 -0
  10. package/skeleton/.4bnode/assets/js/components/Modal.js +52 -0
  11. package/skeleton/.4bnode/assets/js/components/Sidebar.js +68 -0
  12. package/skeleton/.4bnode/assets/js/components/ToastContainer.js +15 -0
  13. package/skeleton/.4bnode/assets/js/context.js +7 -0
  14. package/skeleton/.4bnode/assets/js/lib.js +83 -0
  15. package/skeleton/.4bnode/assets/js/main.js +5 -0
  16. package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +1594 -0
  17. package/skeleton/.4bnode/assets/js/pages/DashboardPage.js +435 -0
  18. package/skeleton/.4bnode/assets/js/pages/DatabasePage.js +461 -0
  19. package/skeleton/.4bnode/assets/js/pages/EnvironmentPage.js +141 -0
  20. package/skeleton/.4bnode/assets/js/pages/RealtimePage.js +186 -0
  21. package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +1656 -0
  22. package/skeleton/.4bnode/assets/js/pages/SchemasPage.js +901 -0
  23. package/skeleton/.4bnode/assets/js/pages/SerialPortPage.js +170 -0
  24. package/skeleton/.4bnode/assets/js/screens/PasskeyScreen.js +105 -0
  25. package/skeleton/.4bnode/assets/js/screens/SplashScreen.js +66 -0
  26. package/skeleton/{dev-api.js → .4bnode/dev-api.js} +259 -114
  27. package/skeleton/.4bnode/ui.html +1556 -0
  28. package/skeleton/index.js +62 -16
  29. package/skeleton/package.json +1 -1
  30. package/skeleton/public/index.html +290 -7586
  31. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Bold.woff2 +0 -0
  32. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Light.woff2 +0 -0
  33. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Medium.woff2 +0 -0
  34. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Regular.woff2 +0 -0
  35. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-SemiBold.woff2 +0 -0
  36. /package/skeleton/{public → .4bnode/assets}/images/mongo.png +0 -0
  37. /package/skeleton/{public → .4bnode/assets}/images/mysql.png +0 -0
  38. /package/skeleton/{public → .4bnode/assets}/images/postgresql.png +0 -0
  39. /package/skeleton/{public → .4bnode/assets}/images/sqlite.png +0 -0
package/README.md CHANGED
@@ -92,6 +92,16 @@ Presets available: `user`, `product`, `post`
92
92
  4bnode list routes # List routes only
93
93
  ```
94
94
 
95
+ ### Dashboard Passkey
96
+
97
+ During `npx 4bnode init`, you'll be prompted to set a 6-digit passkey to protect the dev dashboard.
98
+
99
+ To change the passkey later, run inside your project:
100
+
101
+ ```sh
102
+ 4bnode passkey
103
+ ```
104
+
95
105
  ### Start Dev Dashboard
96
106
 
97
107
  ```sh
package/add-login.js CHANGED
@@ -168,13 +168,13 @@ router.post('/login', async (req, res) => {
168
168
  try {
169
169
  const user = await ${pascalName}.findOne({ ${selectedFields[0]}: ${selectedFields[0]} });
170
170
  if (!user) {
171
- return res.status(404).json({ message: 'User not found' });
171
+ return res.status(401).json({ message: 'Invalid credentials' });
172
172
  }
173
173
  ${
174
174
  selectedFields.includes("password")
175
175
  ? `const isMatch = await bcrypt.compare(password, user.password);
176
176
  if (!isMatch) {
177
- return res.status(400).json({ message: 'Invalid credentials' });
177
+ return res.status(401).json({ message: 'Invalid credentials' });
178
178
  }`
179
179
  : ""
180
180
  }
package/add-serialport.js CHANGED
@@ -18,7 +18,7 @@ async function main() {
18
18
 
19
19
  const serialPortPath = await input({
20
20
  message: "Serial port path:",
21
- default: "/dev/tty-usbserial1",
21
+ default: process.platform === "win32" ? "COM3" : "/dev/tty-usbserial1",
22
22
  });
23
23
 
24
24
  const baudRate = await input({
package/index.js CHANGED
@@ -2,10 +2,11 @@
2
2
 
3
3
  import fs from "fs";
4
4
  import path from "path";
5
+ import crypto from "crypto";
5
6
  import { execFileSync } from "child_process";
6
7
  import { fileURLToPath } from "url";
7
8
  import chalk from "chalk";
8
- import { input, select } from "@inquirer/prompts";
9
+ import { input, select, password } from "@inquirer/prompts";
9
10
  import { sanitizeName } from "./lib/names.js";
10
11
  import {
11
12
  checkMongoConfig,
@@ -53,7 +54,38 @@ function updatePackageName(destinationPath, projectName) {
53
54
  );
54
55
  }
55
56
 
56
- function createSkeletonApp(projectName) {
57
+ function hashPasskey(pin) {
58
+ return crypto.createHash("sha256").update(pin).digest("hex");
59
+ }
60
+
61
+ async function promptPasskey() {
62
+ const passkey = await password({
63
+ message: "Set a 6-digit passkey for the dev dashboard:",
64
+ mask: "*",
65
+ validate: (v) => {
66
+ if (!/^\d{6}$/.test(v)) return "Passkey must be exactly 6 digits";
67
+ return true;
68
+ },
69
+ });
70
+
71
+ const confirm = await password({
72
+ message: "Confirm passkey:",
73
+ mask: "*",
74
+ validate: (v) => {
75
+ if (v !== passkey) return "Passkeys do not match";
76
+ return true;
77
+ },
78
+ });
79
+
80
+ return passkey;
81
+ }
82
+
83
+ function savePasskey(projectPath, passkey) {
84
+ const hash = hashPasskey(passkey);
85
+ fs.writeFileSync(path.join(projectPath, ".4bnode", ".passkey"), hash, "utf8");
86
+ }
87
+
88
+ async function createSkeletonApp(projectName) {
57
89
  showBanner();
58
90
 
59
91
  const targetPath = path.resolve(projectName);
@@ -67,9 +99,15 @@ function createSkeletonApp(projectName) {
67
99
  updatePackageName(targetPath, projectName);
68
100
  scaffoldSpinner.succeed(chalk.hex("#00f260").bold("Project scaffolded"));
69
101
 
102
+ // Ask for dashboard passkey
103
+ console.log();
104
+ const passkey = await promptPasskey();
105
+ savePasskey(targetPath, passkey);
106
+ console.log(chalk.hex("#00f260").bold(" ✔ Dashboard passkey set"));
107
+
70
108
  const installSpinner = createSpinner("Installing dependencies...");
71
109
  installSpinner.start();
72
- execFileSync("npm", ["install"], { stdio: "pipe", cwd: targetPath });
110
+ execFileSync("npm", ["install"], { stdio: "pipe", cwd: targetPath, shell: true });
73
111
  installSpinner.succeed(chalk.hex("#00f260").bold("Dependencies installed"));
74
112
 
75
113
  showSuccess([
@@ -80,13 +118,14 @@ function createSkeletonApp(projectName) {
80
118
  showNextSteps([
81
119
  `cd ${projectName}`,
82
120
  "npm run dev",
83
- "Open http://localhost:3000",
121
+ "Open http://localhost:3000/_dev",
84
122
  ]);
85
123
  }
86
124
 
87
125
  function runCommand(script, args = []) {
88
126
  execFileSync("node", [path.join(__dirname, script), ...args], {
89
127
  stdio: "inherit",
128
+ shell: true,
90
129
  });
91
130
  }
92
131
 
@@ -210,6 +249,7 @@ function showHelp() {
210
249
  console.log(` ${c("schema")} ${d("[name]")} Manage schemas (create/view/edit)`);
211
250
  console.log(` ${c("list")} ${d("[filter]")} List models, routes & config status`);
212
251
  console.log(` ${c("ui")} Start dev server with visual dashboard`);
252
+ console.log(` ${c("passkey")} Change dashboard passkey`);
213
253
  console.log();
214
254
  console.log(h(" ADD FEATURES\n"));
215
255
  console.log(` ${c("add api")} ${d("[name]")} API endpoint`);
@@ -306,11 +346,24 @@ async function main() {
306
346
  }
307
347
  } else if (command === "list") {
308
348
  showList(subCommand);
349
+ } else if (command === "passkey") {
350
+ // Must be inside a 4bnode project
351
+ const projectRoot = process.cwd();
352
+ const passkeyFile = path.join(projectRoot, ".4bnode", ".passkey");
353
+ if (!fs.existsSync(path.join(projectRoot, ".4bnode"))) {
354
+ showError("Not inside a 4bnode project. Run 'npx 4bnode init <name>' first.");
355
+ process.exit(1);
356
+ }
357
+ showBanner();
358
+ console.log();
359
+ const newPasskey = await promptPasskey();
360
+ savePasskey(projectRoot, newPasskey);
361
+ console.log(chalk.hex("#00f260").bold("\n ✔ Dashboard passkey updated successfully\n"));
309
362
  } else if (command === "ui") {
310
- // Must be inside a 4bnode project (has package.json + dev-api.js)
363
+ // Must be inside a 4bnode project (has package.json + .4bnode/)
311
364
  const projectRoot = process.cwd();
312
365
  const hasPackageJson = fs.existsSync(path.join(projectRoot, "package.json"));
313
- const hasDevApi = fs.existsSync(path.join(projectRoot, "dev-api.js"));
366
+ const hasDevApi = fs.existsSync(path.join(projectRoot, ".4bnode", "dev-api.js")) || fs.existsSync(path.join(projectRoot, "dev-api.js"));
314
367
  if (!hasPackageJson || !hasDevApi) {
315
368
  showError("Not inside a 4bnode project. Run 'npx 4bnode init <name>' first, then cd into the project folder.");
316
369
  process.exit(1);
@@ -319,6 +372,7 @@ async function main() {
319
372
  execFileSync("npm", ["run", "dev"], {
320
373
  stdio: "inherit",
321
374
  cwd: projectRoot,
375
+ shell: true,
322
376
  });
323
377
  }
324
378
  // Legacy commands → route through handleAdd
package/lib/indexFile.js CHANGED
@@ -40,6 +40,11 @@ export function addRouteRegistration(importLine, useLine) {
40
40
  "server.listen(port",
41
41
  useLine.trim() + "\n" + "server.listen(port"
42
42
  );
43
+ } else if (content.match(/^startServer\(/m)) {
44
+ content = content.replace(
45
+ /^startServer\(/m,
46
+ useLine.trim() + "\n" + "startServer("
47
+ );
43
48
  }
44
49
  }
45
50
 
@@ -48,7 +53,7 @@ export function addRouteRegistration(importLine, useLine) {
48
53
 
49
54
  export function insertBeforeListen(codeBlock) {
50
55
  let content = readIndex();
51
- const listenMatch = content.match(/(app|server)\.listen\(port/);
56
+ const listenMatch = content.match(/(app|server)\.listen\(port/) || content.match(/^startServer\(/m);
52
57
  if (listenMatch) {
53
58
  content = content.replace(
54
59
  listenMatch[0],
@@ -62,25 +67,67 @@ export function insertBeforeListen(codeBlock) {
62
67
 
63
68
  export function replaceAppListen(newListenBlock) {
64
69
  let content = readIndex();
65
- content = content.replace(/app\.listen\(port[\s\S]*?\}\);?\n?/m, "");
66
- if (!content.includes("server.listen(port")) {
67
- content += "\n" + newListenBlock.trim() + "\n";
70
+
71
+ // If using startServer() pattern, just swap app.listen → server.listen inside it
72
+ if (content.includes("startServer(")) {
73
+ content = content.replace(
74
+ /const listener = app\.listen\(tryPort\)/,
75
+ "const listener = server.listen(tryPort)"
76
+ );
77
+ // Also swap standalone app.listen(tryPort) if no 'const listener' pattern
78
+ if (!content.includes("server.listen(tryPort)")) {
79
+ content = content.replace(
80
+ /app\.listen\(tryPort\)/,
81
+ "server.listen(tryPort)"
82
+ );
83
+ }
84
+ } else {
85
+ content = content.replace(/app\.listen\(port[\s\S]*?\}\);?\n?/m, "");
86
+ if (!content.includes("server.listen(port")) {
87
+ content += "\n" + newListenBlock.trim() + "\n";
88
+ }
68
89
  }
69
90
  writeIndex(content);
70
91
  }
71
92
 
72
93
  export function insertBeforeRoutes(codeBlock) {
73
94
  let content = readIndex();
74
- const routesStart = content.indexOf("app.use(");
75
- if (routesStart !== -1) {
76
- content =
77
- content.slice(0, routesStart) +
78
- codeBlock.trim() +
79
- "\n\n" +
80
- content.slice(routesStart);
81
- } else {
82
- content += "\n" + codeBlock.trim() + "\n";
95
+
96
+ // Insert before startServer() or the start server section
97
+ const markers = [
98
+ "// ── Start Server",
99
+ "// ── Helpers",
100
+ "startServer(",
101
+ ];
102
+ let inserted = false;
103
+ for (const marker of markers) {
104
+ const idx = content.indexOf(marker);
105
+ if (idx !== -1) {
106
+ content =
107
+ content.slice(0, idx) +
108
+ codeBlock.trim() +
109
+ "\n\n" +
110
+ content.slice(idx);
111
+ inserted = true;
112
+ break;
113
+ }
83
114
  }
115
+
116
+ if (!inserted) {
117
+ // Fallback: before first app.use that isn't middleware
118
+ const routeUseMatch = content.match(/app\.use\(['"]\/(?!_dev)/);
119
+ if (routeUseMatch) {
120
+ const idx = content.indexOf(routeUseMatch[0]);
121
+ content =
122
+ content.slice(0, idx) +
123
+ codeBlock.trim() +
124
+ "\n\n" +
125
+ content.slice(idx);
126
+ } else {
127
+ content += "\n" + codeBlock.trim() + "\n";
128
+ }
129
+ }
130
+
84
131
  writeIndex(content);
85
132
  }
86
133
 
package/lib/ui.js CHANGED
@@ -140,6 +140,7 @@ export async function installDeps(packages, opts = {}) {
140
140
  execSync(`npm install ${names.join(" ")}`, {
141
141
  stdio: "pipe",
142
142
  ...opts,
143
+ shell: true,
143
144
  });
144
145
  spinner.succeed(success(`Installed ${label}`));
145
146
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "4bnode",
3
- "version": "4.0.0",
3
+ "version": "4.0.3",
4
4
  "description": "A professional tool to generate a Node.js app by 4Brains Technologies",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,115 @@
1
+ import React, { useState, useEffect, useCallback } from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import { html, api, PAGE_TITLES } from "./lib.js";
4
+ import { AppContext } from "./context.js";
5
+ import { Sidebar } from "./components/Sidebar.js";
6
+ import { ToastContainer } from "./components/ToastContainer.js";
7
+ import { ConfirmDialog } from "./components/Modal.js";
8
+ import { DashboardPage } from "./pages/DashboardPage.js";
9
+ import { DatabasePage } from "./pages/DatabasePage.js";
10
+ import { SchemasPage } from "./pages/SchemasPage.js";
11
+ import { RoutesPage } from "./pages/RoutesPage.js";
12
+ import { ApiTesterPage } from "./pages/ApiTesterPage.js";
13
+ import { RealtimePage } from "./pages/RealtimePage.js";
14
+ import { SerialPortPage } from "./pages/SerialPortPage.js";
15
+ import { EnvironmentPage } from "./pages/EnvironmentPage.js";
16
+
17
+ function getHashPage() {
18
+ const hash = window.location.hash.replace("#/", "").replace("#", "");
19
+ return hash && PAGE_TITLES[hash] ? hash : "dashboard";
20
+ }
21
+
22
+ export function App() {
23
+ const [page, setPageState] = useState(getHashPage);
24
+ const [data, setData] = useState({
25
+ models: [],
26
+ routes: [],
27
+ mongoConfigured: false,
28
+ routesDirExists: false,
29
+ });
30
+ const [status, setStatus] = useState({});
31
+ const [toasts, setToasts] = useState([]);
32
+ const [confirmState, setConfirmState] = useState(null);
33
+
34
+ const showConfirm = useCallback((message, opts = {}) => {
35
+ return new Promise((resolve) => {
36
+ setConfirmState({ message, ...opts, resolve });
37
+ });
38
+ }, []);
39
+
40
+ const setPage = useCallback((p) => {
41
+ window.location.hash = "#/" + p;
42
+ }, []);
43
+
44
+ useEffect(() => {
45
+ function onHash() {
46
+ setPageState(getHashPage());
47
+ }
48
+ window.addEventListener("hashchange", onHash);
49
+ return () => window.removeEventListener("hashchange", onHash);
50
+ }, []);
51
+
52
+ const toast = useCallback((message, type = "info") => {
53
+ const id = Date.now();
54
+ setToasts((prev) => [...prev, { id, message, type }]);
55
+ setTimeout(
56
+ () => setToasts((prev) => prev.filter((t) => t.id !== id)),
57
+ 3500,
58
+ );
59
+ }, []);
60
+
61
+ const refresh = useCallback(async () => {
62
+ const [dashData, statusData] = await Promise.all([
63
+ api("/dashboard"),
64
+ api("/status"),
65
+ ]);
66
+ setData(dashData);
67
+ setStatus(statusData);
68
+ }, []);
69
+
70
+ useEffect(() => {
71
+ refresh().catch(() => {});
72
+ }, []);
73
+
74
+ const pages = {
75
+ dashboard: html`<${DashboardPage} />`,
76
+ database: html`<${DatabasePage} />`,
77
+ schemas: html`<${SchemasPage} />`,
78
+ routes: html`<${RoutesPage} />`,
79
+ "api-tester": html`<${ApiTesterPage} />`,
80
+ realtime: html`<${RealtimePage} />`,
81
+ serialport: html`<${SerialPortPage} />`,
82
+ env: html`<${EnvironmentPage} />`,
83
+ };
84
+
85
+ return html`<${AppContext.Provider}
86
+ value=${{ page, setPage, data, status, toasts, toast, refresh, showConfirm }}
87
+ >
88
+ <${Sidebar} />
89
+ <div className="main">
90
+ <div className="topbar">
91
+ <h2>${PAGE_TITLES[page] || "Dashboard"}</h2>
92
+ <span className="topbar-badge">Development</span>
93
+ </div>
94
+ <div className="content">
95
+ <${AnimatePresence} mode="wait">
96
+ <${motion.div} key=${page}
97
+ initial=${{ opacity: 0, y: 8 }}
98
+ animate=${{ opacity: 1, y: 0 }}
99
+ exit=${{ opacity: 0, y: -8 }}
100
+ transition=${{ duration: 0.15 }}>
101
+ ${pages[page] || pages.dashboard}
102
+ <//>
103
+ <//>
104
+ </div>
105
+ </div>
106
+ <${ToastContainer} />
107
+ ${confirmState && html`<${ConfirmDialog}
108
+ message=${confirmState.message}
109
+ confirmLabel=${confirmState.confirmLabel}
110
+ danger=${confirmState.danger}
111
+ onConfirm=${() => { confirmState.resolve(true); setConfirmState(null); }}
112
+ onCancel=${() => { confirmState.resolve(false); setConfirmState(null); }}
113
+ />`}
114
+ <//>`;
115
+ }
@@ -0,0 +1,44 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { AnimatePresence } from "framer-motion";
3
+ import { motion } from "framer-motion";
4
+ import { html, API, getStoredPasskey } from "./lib.js";
5
+ import { App } from "./App.js";
6
+ import { SplashScreen } from "./screens/SplashScreen.js";
7
+ import { PasskeyScreen } from "./screens/PasskeyScreen.js";
8
+
9
+ export function Root() {
10
+ const [showSplash, setShowSplash] = useState(true);
11
+ const [authed, setAuthed] = useState(false);
12
+ const [checking, setChecking] = useState(true);
13
+
14
+ useEffect(() => {
15
+ (async () => {
16
+ try {
17
+ const stored = getStoredPasskey();
18
+ const res = await fetch(API + '/auth', {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body: JSON.stringify({ passkey: stored }),
22
+ });
23
+ if (res.ok) setAuthed(true);
24
+ } catch {}
25
+ setChecking(false);
26
+ })();
27
+ }, []);
28
+
29
+ if (checking) return null;
30
+
31
+ return html`<${AnimatePresence} mode="wait">
32
+ ${showSplash
33
+ ? html`<${SplashScreen} key="splash" onDone=${() => setShowSplash(false)} />`
34
+ : !authed
35
+ ? html`<${PasskeyScreen} key="passkey" onSuccess=${() => setAuthed(true)} />`
36
+ : html`<${motion.div} key="app"
37
+ style=${{ display: 'flex', height: '100vh', width: '100%' }}
38
+ initial=${{ opacity: 0 }}
39
+ animate=${{ opacity: 1 }}
40
+ transition=${{ duration: 0.4 }}>
41
+ <${App} />
42
+ <//>`}
43
+ <//>`;
44
+ }
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import { LuTrash2, LuZap } from "react-icons/lu";
4
+ import { html } from "../lib.js";
5
+
6
+ export function Modal({ title, children, onClose, actions }) {
7
+ return html`<${AnimatePresence}><${motion.div}
8
+ className="modal-overlay"
9
+ onClick=${(e) => e.target === e.currentTarget && onClose()}
10
+ initial=${{ opacity: 0 }} animate=${{ opacity: 1 }} exit=${{ opacity: 0 }}
11
+ transition=${{ duration: 0.15 }}
12
+ >
13
+ <${motion.div} className="modal"
14
+ initial=${{ opacity: 0, scale: 0.95, y: 10 }}
15
+ animate=${{ opacity: 1, scale: 1, y: 0 }}
16
+ exit=${{ opacity: 0, scale: 0.95, y: 10 }}
17
+ transition=${{ duration: 0.15 }}>
18
+ <div className="modal-title">${title}</div>
19
+ ${children}
20
+ ${actions && html`<div className="modal-actions">${actions}</div>`}
21
+ <//>
22
+ <//><//>`;
23
+ }
24
+
25
+ export function ConfirmDialog({ message, onConfirm, onCancel, confirmLabel, danger }) {
26
+ return html`<${AnimatePresence}><${motion.div}
27
+ className="modal-overlay"
28
+ onClick=${onCancel}
29
+ initial=${{ opacity: 0 }} animate=${{ opacity: 1 }} exit=${{ opacity: 0 }}
30
+ transition=${{ duration: 0.15 }}
31
+ >
32
+ <${motion.div} className="modal" onClick=${e => e.stopPropagation()}
33
+ style=${{ minWidth: '380px', maxWidth: '440px' }}
34
+ initial=${{ opacity: 0, scale: 0.95, y: 10 }}
35
+ animate=${{ opacity: 1, scale: 1, y: 0 }}
36
+ exit=${{ opacity: 0, scale: 0.95, y: 10 }}
37
+ transition=${{ duration: 0.15 }}>
38
+ <div className="flex items-center gap-3" style=${{ marginBottom: '16px' }}>
39
+ <div style=${{ width: '40px', height: '40px', borderRadius: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
40
+ background: danger ? 'var(--error-glow)' : 'var(--warning-glow)' }}>
41
+ <${danger ? LuTrash2 : LuZap} size=${18} color=${danger ? 'var(--error)' : 'var(--warning)'} />
42
+ </div>
43
+ <div style=${{ fontSize: '15px', fontWeight: 600 }}>Confirm Action</div>
44
+ </div>
45
+ <div style=${{ fontSize: '13px', color: 'var(--text-secondary)', lineHeight: '1.6', marginBottom: '20px' }}>${message}</div>
46
+ <div className="modal-actions">
47
+ <button className="btn btn-ghost" onClick=${onCancel}>Cancel</button>
48
+ <button className=${`btn ${danger ? 'btn-danger' : 'btn-primary'}`} onClick=${onConfirm}>${confirmLabel || 'Confirm'}</button>
49
+ </div>
50
+ <//>
51
+ <//><//>`;
52
+ }
@@ -0,0 +1,68 @@
1
+ import React from "react";
2
+ import { html, Icons } from "../lib.js";
3
+ import { useApp } from "../context.js";
4
+
5
+ export function Sidebar() {
6
+ const { page, setPage, data } = useApp();
7
+ const dbOk = data.mongoConfigured;
8
+
9
+ const navItems = [
10
+ {
11
+ id: "dashboard",
12
+ label: "Dashboard",
13
+ icon: Icons.dashboard,
14
+ section: "Overview",
15
+ },
16
+ {
17
+ id: "database",
18
+ label: "Database",
19
+ icon: Icons.database,
20
+ section: "Setup",
21
+ },
22
+ {
23
+ id: "schemas",
24
+ label: "Schemas",
25
+ icon: Icons.schema,
26
+ locked: !dbOk,
27
+ lockMsg: "Requires database",
28
+ },
29
+ { id: "routes", label: "API Routes", icon: Icons.route },
30
+ { id: "api-tester", label: "API Tester", icon: Icons.apiTester },
31
+ {
32
+ id: "realtime",
33
+ label: "Real-time",
34
+ icon: Icons.realtime,
35
+ section: "Features",
36
+ },
37
+ { id: "serialport", label: "SerialPort", icon: Icons.serial },
38
+ { id: "env", label: "Environment", icon: Icons.env, section: "Settings" },
39
+ ];
40
+
41
+ let currentSection = null;
42
+ return html`<div className="sidebar">
43
+ <div className="sidebar-brand">
44
+ <pre>${`██╗ ██╗██████╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗\n██║ ██║██╔══██╗████╗ ██║██╔═══██╗██╔══██╗██╔════╝\n███████║██████╔╝██╔██╗ ██║██║ ██║██║ ██║█████╗\n╚════██║██╔══██╗██║╚██╗██║██║ ██║██║ ██║██╔══╝\n ██║██████╔╝██║ ╚████║╚██████╔╝██████╔╝███████╗\n ╚═╝╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝`}</pre>
45
+ <span>Project Dashboard</span>
46
+ </div>
47
+ ${navItems.map((item) => {
48
+ const sectionHeader =
49
+ item.section && item.section !== currentSection
50
+ ? ((currentSection = item.section),
51
+ html`<div className="sidebar-section">${item.section}</div>`)
52
+ : null;
53
+ return html`<${React.Fragment} key=${item.id}>
54
+ ${sectionHeader}
55
+ <div
56
+ className=${`sidebar-item ${page === item.id ? "active" : ""} ${item.locked ? "locked" : ""}`}
57
+ onClick=${() => !item.locked && setPage(item.id)}
58
+ title=${item.locked ? item.lockMsg : item.label}
59
+ >
60
+ <span className="sidebar-icon">${item.icon}</span>
61
+ ${item.label}
62
+ ${item.locked &&
63
+ html`<span className="lock-icon">${Icons.lock}</span>`}
64
+ </div>
65
+ <//>`;
66
+ })}
67
+ </div>`;
68
+ }
@@ -0,0 +1,15 @@
1
+ import { html } from "../lib.js";
2
+ import { useApp } from "../context.js";
3
+
4
+ export function ToastContainer() {
5
+ const { toasts } = useApp();
6
+ if (toasts.length === 0) return null;
7
+ return html`<div className="toast-container">
8
+ ${toasts.map(
9
+ (t) =>
10
+ html`<div key=${t.id} className=${`toast ${t.type}`}>
11
+ ${t.message}
12
+ </div>`,
13
+ )}
14
+ </div>`;
15
+ }
@@ -0,0 +1,7 @@
1
+ import React, { createContext, useContext } from "react";
2
+
3
+ export const AppContext = createContext();
4
+
5
+ export function useApp() {
6
+ return useContext(AppContext);
7
+ }
@@ -0,0 +1,83 @@
1
+ import React from "react";
2
+ import htm from "htm";
3
+ import {
4
+ LuLayoutDashboard,
5
+ LuDatabase,
6
+ LuFileText,
7
+ LuRadio,
8
+ LuPlus,
9
+ LuTrash2,
10
+ LuCheck,
11
+ LuLock,
12
+ LuArrowRight,
13
+ LuPencil,
14
+ LuCable,
15
+ LuActivity,
16
+ LuZap,
17
+ LuShield,
18
+ LuBox,
19
+ LuFlaskConical,
20
+ LuSend,
21
+ LuSettings,
22
+ } from "react-icons/lu";
23
+
24
+ export const html = htm.bind(React.createElement);
25
+ export const API = "/_dev/api";
26
+
27
+ export function getStoredPasskey() {
28
+ return sessionStorage.getItem("_dev_passkey") || "";
29
+ }
30
+
31
+ export async function api(path, options = {}) {
32
+ const passkey = getStoredPasskey();
33
+ const headers = { "Content-Type": "application/json" };
34
+ if (passkey) headers["x-passkey"] = passkey;
35
+ const res = await fetch(API + path, {
36
+ headers,
37
+ ...options,
38
+ body: options.body ? JSON.stringify(options.body) : undefined,
39
+ });
40
+ const text = await res.text();
41
+ let data;
42
+ try {
43
+ data = JSON.parse(text);
44
+ } catch {
45
+ if (res.ok) return { message: "Done" };
46
+ throw new Error("Server restarting, please wait...");
47
+ }
48
+ if (!res.ok)
49
+ throw new Error(data.error || data.message || "Request failed");
50
+ return data;
51
+ }
52
+
53
+ export const Icons = {
54
+ dashboard: html`<${LuLayoutDashboard} size=${16} />`,
55
+ database: html`<${LuDatabase} size=${16} />`,
56
+ schema: html`<${LuFileText} size=${16} />`,
57
+ route: html`<${LuRadio} size=${16} />`,
58
+ realtime: html`<${LuActivity} size=${16} />`,
59
+ env: html`<${LuSettings} size=${16} />`,
60
+ plus: html`<${LuPlus} size=${16} />`,
61
+ trash: html`<${LuTrash2} size=${16} />`,
62
+ check: html`<${LuCheck} size=${16} />`,
63
+ lock: html`<${LuLock} size=${12} />`,
64
+ arrow: html`<${LuArrowRight} size=${14} />`,
65
+ edit: html`<${LuPencil} size=${16} />`,
66
+ serial: html`<${LuCable} size=${16} />`,
67
+ zap: html`<${LuZap} size=${16} />`,
68
+ shield: html`<${LuShield} size=${16} />`,
69
+ box: html`<${LuBox} size=${16} />`,
70
+ apiTester: html`<${LuFlaskConical} size=${16} />`,
71
+ send: html`<${LuSend} size=${16} />`,
72
+ };
73
+
74
+ export const PAGE_TITLES = {
75
+ dashboard: "Dashboard",
76
+ database: "Database",
77
+ schemas: "Schemas",
78
+ routes: "API Routes",
79
+ "api-tester": "API Tester",
80
+ realtime: "Real-time",
81
+ serialport: "SerialPort",
82
+ env: "Environment",
83
+ };