4bnode 4.0.0 → 4.0.2

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 (37) hide show
  1. package/README.md +10 -0
  2. package/add-login.js +2 -2
  3. package/index.js +57 -5
  4. package/lib/indexFile.js +60 -13
  5. package/package.json +1 -1
  6. package/skeleton/.4bnode/assets/js/App.js +115 -0
  7. package/skeleton/.4bnode/assets/js/Root.js +44 -0
  8. package/skeleton/.4bnode/assets/js/components/Modal.js +52 -0
  9. package/skeleton/.4bnode/assets/js/components/Sidebar.js +68 -0
  10. package/skeleton/.4bnode/assets/js/components/ToastContainer.js +15 -0
  11. package/skeleton/.4bnode/assets/js/context.js +7 -0
  12. package/skeleton/.4bnode/assets/js/lib.js +83 -0
  13. package/skeleton/.4bnode/assets/js/main.js +5 -0
  14. package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +1594 -0
  15. package/skeleton/.4bnode/assets/js/pages/DashboardPage.js +435 -0
  16. package/skeleton/.4bnode/assets/js/pages/DatabasePage.js +461 -0
  17. package/skeleton/.4bnode/assets/js/pages/EnvironmentPage.js +141 -0
  18. package/skeleton/.4bnode/assets/js/pages/RealtimePage.js +186 -0
  19. package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +1656 -0
  20. package/skeleton/.4bnode/assets/js/pages/SchemasPage.js +901 -0
  21. package/skeleton/.4bnode/assets/js/pages/SerialPortPage.js +170 -0
  22. package/skeleton/.4bnode/assets/js/screens/PasskeyScreen.js +105 -0
  23. package/skeleton/.4bnode/assets/js/screens/SplashScreen.js +66 -0
  24. package/skeleton/{dev-api.js → .4bnode/dev-api.js} +259 -114
  25. package/skeleton/.4bnode/ui.html +1556 -0
  26. package/skeleton/index.js +62 -16
  27. package/skeleton/package.json +1 -1
  28. package/skeleton/public/index.html +290 -7586
  29. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Bold.woff2 +0 -0
  30. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Light.woff2 +0 -0
  31. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Medium.woff2 +0 -0
  32. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Regular.woff2 +0 -0
  33. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-SemiBold.woff2 +0 -0
  34. /package/skeleton/{public → .4bnode/assets}/images/mongo.png +0 -0
  35. /package/skeleton/{public → .4bnode/assets}/images/mysql.png +0 -0
  36. /package/skeleton/{public → .4bnode/assets}/images/postgresql.png +0 -0
  37. /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/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,6 +99,12 @@ 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
110
  execFileSync("npm", ["install"], { stdio: "pipe", cwd: targetPath });
@@ -80,7 +118,7 @@ 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
 
@@ -210,6 +248,7 @@ function showHelp() {
210
248
  console.log(` ${c("schema")} ${d("[name]")} Manage schemas (create/view/edit)`);
211
249
  console.log(` ${c("list")} ${d("[filter]")} List models, routes & config status`);
212
250
  console.log(` ${c("ui")} Start dev server with visual dashboard`);
251
+ console.log(` ${c("passkey")} Change dashboard passkey`);
213
252
  console.log();
214
253
  console.log(h(" ADD FEATURES\n"));
215
254
  console.log(` ${c("add api")} ${d("[name]")} API endpoint`);
@@ -306,11 +345,24 @@ async function main() {
306
345
  }
307
346
  } else if (command === "list") {
308
347
  showList(subCommand);
348
+ } else if (command === "passkey") {
349
+ // Must be inside a 4bnode project
350
+ const projectRoot = process.cwd();
351
+ const passkeyFile = path.join(projectRoot, ".4bnode", ".passkey");
352
+ if (!fs.existsSync(path.join(projectRoot, ".4bnode"))) {
353
+ showError("Not inside a 4bnode project. Run 'npx 4bnode init <name>' first.");
354
+ process.exit(1);
355
+ }
356
+ showBanner();
357
+ console.log();
358
+ const newPasskey = await promptPasskey();
359
+ savePasskey(projectRoot, newPasskey);
360
+ console.log(chalk.hex("#00f260").bold("\n ✔ Dashboard passkey updated successfully\n"));
309
361
  } else if (command === "ui") {
310
- // Must be inside a 4bnode project (has package.json + dev-api.js)
362
+ // Must be inside a 4bnode project (has package.json + .4bnode/)
311
363
  const projectRoot = process.cwd();
312
364
  const hasPackageJson = fs.existsSync(path.join(projectRoot, "package.json"));
313
- const hasDevApi = fs.existsSync(path.join(projectRoot, "dev-api.js"));
365
+ const hasDevApi = fs.existsSync(path.join(projectRoot, ".4bnode", "dev-api.js")) || fs.existsSync(path.join(projectRoot, "dev-api.js"));
314
366
  if (!hasPackageJson || !hasDevApi) {
315
367
  showError("Not inside a 4bnode project. Run 'npx 4bnode init <name>' first, then cd into the project folder.");
316
368
  process.exit(1);
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "4bnode",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
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
+ };
@@ -0,0 +1,5 @@
1
+ import { createRoot } from "react-dom/client";
2
+ import { html } from "./lib.js";
3
+ import { Root } from "./Root.js";
4
+
5
+ createRoot(document.getElementById("root")).render(html`<${Root} />`);