4bnode 4.1.1 → 4.1.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.
- package/add-security.js +0 -18
- package/app-1/.4bnode/.passkey +1 -0
- package/app-1/.4bnode/assets/fonts/NotoSans-Bold.woff2 +0 -0
- package/app-1/.4bnode/assets/fonts/NotoSans-Light.woff2 +0 -0
- package/app-1/.4bnode/assets/fonts/NotoSans-Medium.woff2 +0 -0
- package/app-1/.4bnode/assets/fonts/NotoSans-Regular.woff2 +0 -0
- package/app-1/.4bnode/assets/fonts/NotoSans-SemiBold.woff2 +0 -0
- package/app-1/.4bnode/assets/images/mongo.png +0 -0
- package/app-1/.4bnode/assets/js/App.js +139 -0
- package/app-1/.4bnode/assets/js/Root.js +40 -0
- package/app-1/.4bnode/assets/js/components/Modal.js +52 -0
- package/app-1/.4bnode/assets/js/components/Sidebar.js +73 -0
- package/app-1/.4bnode/assets/js/components/ToastContainer.js +15 -0
- package/app-1/.4bnode/assets/js/context.js +7 -0
- package/app-1/.4bnode/assets/js/lib.js +113 -0
- package/app-1/.4bnode/assets/js/main.js +5 -0
- package/app-1/.4bnode/assets/js/pages/AiBuilderPage.js +268 -0
- package/app-1/.4bnode/assets/js/pages/ApiTesterPage.js +1879 -0
- package/app-1/.4bnode/assets/js/pages/DashboardPage.js +435 -0
- package/app-1/.4bnode/assets/js/pages/DatabasePage.js +300 -0
- package/app-1/.4bnode/assets/js/pages/DocsPage.js +150 -0
- package/app-1/.4bnode/assets/js/pages/EnvironmentPage.js +103 -0
- package/app-1/.4bnode/assets/js/pages/LogsPage.js +205 -0
- package/app-1/.4bnode/assets/js/pages/MailPage.js +134 -0
- package/app-1/.4bnode/assets/js/pages/RealtimePage.js +186 -0
- package/app-1/.4bnode/assets/js/pages/RoutesPage.js +1436 -0
- package/app-1/.4bnode/assets/js/pages/SchemasPage.js +838 -0
- package/app-1/.4bnode/assets/js/pages/SecurityPage.js +74 -0
- package/app-1/.4bnode/assets/js/pages/SerialPortPage.js +170 -0
- package/app-1/.4bnode/assets/js/screens/PasskeyScreen.js +105 -0
- package/app-1/.4bnode/codegen.js +1249 -0
- package/app-1/.4bnode/dev-api.js +3621 -0
- package/app-1/.4bnode/ui.html +1578 -0
- package/app-1/README.md +47 -0
- package/app-1/ecosystem.config.cjs +25 -0
- package/app-1/index.js +145 -0
- package/app-1/package.json +35 -0
- package/app-1/public/index.html +292 -0
- package/app-1/src/db.js +13 -0
- package/app-1/src/docs.js +84 -0
- package/app-1/src/middleware/errorHandler.js +15 -0
- package/app-1/src/middleware/roles.js +14 -0
- package/app-1/src/middleware/validate.js +20 -0
- package/app-1/src/models/login.js +9 -0
- package/app-1/src/openapi.json +62 -0
- package/app-1/src/routes/products.js +26 -0
- package/app-1/src/validators/products.js +7 -0
- package/app-1/src/validators/user.js +7 -0
- package/index.js +2 -17
- package/lib/codegen.js +69 -43
- package/lib/codegen.test.js +21 -2
- package/package.json +1 -1
- package/skeleton/.4bnode/assets/js/App.js +0 -2
- package/skeleton/.4bnode/assets/js/components/Sidebar.js +2 -3
- package/skeleton/.4bnode/assets/js/lib.js +0 -3
- package/skeleton/.4bnode/assets/js/pages/DocsPage.js +29 -21
- package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +12 -0
- package/skeleton/.4bnode/codegen.js +69 -43
- package/skeleton/.4bnode/dev-api.js +173 -87
- package/skeleton/_gitignore +0 -1
- package/skeleton/ecosystem.config.cjs +25 -0
- package/skeleton/index.js +4 -8
- package/skeleton/.4bnode/assets/js/pages/ApiKeysPage.js +0 -164
package/add-security.js
CHANGED
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import crypto from "crypto";
|
|
6
5
|
import { checkbox, confirm } from "@inquirer/prompts";
|
|
7
6
|
import { getProjectRoot, listModels, getModelSchemaDetailed } from "./lib/project.js";
|
|
8
|
-
import { updateEnvFile } from "./lib/env.js";
|
|
9
7
|
import { addImport, insertBeforeRoutes, insertBeforeListen } from "./lib/indexFile.js";
|
|
10
8
|
import {
|
|
11
9
|
showHeader,
|
|
@@ -18,7 +16,6 @@ import {
|
|
|
18
16
|
import {
|
|
19
17
|
buildErrorHandlerFile,
|
|
20
18
|
buildRolesGuardFile,
|
|
21
|
-
buildApiKeyFile,
|
|
22
19
|
buildValidateFile,
|
|
23
20
|
buildZodSchemaFile,
|
|
24
21
|
securityWiring,
|
|
@@ -46,7 +43,6 @@ async function main() {
|
|
|
46
43
|
{ name: "Rate limiting Throttle requests per IP (express-rate-limit)", value: "rateLimit", checked: true },
|
|
47
44
|
{ name: "Error handler Central 404 + error JSON responses", value: "errorHandler", checked: true },
|
|
48
45
|
{ name: "RBAC roles roles('admin') route guard", value: "roles" },
|
|
49
|
-
{ name: "API keys x-api-key header auth", value: "apiKey" },
|
|
50
46
|
{ name: "Validation Zod request validation middleware", value: "validation" },
|
|
51
47
|
],
|
|
52
48
|
validate: (v) => (v.length > 0 ? true : "Select at least one protection."),
|
|
@@ -54,7 +50,6 @@ async function main() {
|
|
|
54
50
|
|
|
55
51
|
console.log();
|
|
56
52
|
|
|
57
|
-
const projectRoot = getProjectRoot();
|
|
58
53
|
const deps = new Set();
|
|
59
54
|
const summary = [];
|
|
60
55
|
|
|
@@ -79,19 +74,6 @@ async function main() {
|
|
|
79
74
|
writeFileSafe("src/middleware/roles.js", buildRolesGuardFile());
|
|
80
75
|
summary.push("RBAC roles() guard");
|
|
81
76
|
}
|
|
82
|
-
if (features.includes("apiKey")) {
|
|
83
|
-
writeFileSafe("src/middleware/apiKey.js", buildApiKeyFile());
|
|
84
|
-
const envPath = path.join(projectRoot, ".env");
|
|
85
|
-
const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
|
|
86
|
-
if (!/^API_KEYS=/m.test(existing)) {
|
|
87
|
-
const key = crypto.randomBytes(24).toString("hex");
|
|
88
|
-
updateEnvFile(envPath, "API_KEYS", key);
|
|
89
|
-
showFileAction("updated", ".env");
|
|
90
|
-
showInfo(`Generated API key: ${key}`);
|
|
91
|
-
}
|
|
92
|
-
summary.push("API key auth (x-api-key)");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
77
|
// ── Validation (Zod) ──
|
|
96
78
|
if (features.includes("validation")) {
|
|
97
79
|
deps.add("zod");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
scrypt$af7f6048a1c565afcc6f06de246f8b18$673c63bd180dc82eb74265fba34699ad45d1ae27353595e5c1c492351c66aafbd80e18d712b1e6f40e2343e45137b96cb4ed376d0c4ac5b54e8ea9d0a2fd2f73
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,139 @@
|
|
|
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 { DocsPage } from "./pages/DocsPage.js";
|
|
13
|
+
import { AiBuilderPage } from "./pages/AiBuilderPage.js";
|
|
14
|
+
import { SecurityPage } from "./pages/SecurityPage.js";
|
|
15
|
+
import { ApiTesterPage } from "./pages/ApiTesterPage.js";
|
|
16
|
+
import { RealtimePage } from "./pages/RealtimePage.js";
|
|
17
|
+
import { SerialPortPage } from "./pages/SerialPortPage.js";
|
|
18
|
+
import { MailPage } from "./pages/MailPage.js";
|
|
19
|
+
import { LogsPage } from "./pages/LogsPage.js";
|
|
20
|
+
import { EnvironmentPage } from "./pages/EnvironmentPage.js";
|
|
21
|
+
|
|
22
|
+
function getHashPage() {
|
|
23
|
+
const hash = window.location.hash.replace("#/", "").replace("#", "");
|
|
24
|
+
return hash && PAGE_TITLES[hash] ? hash : "dashboard";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function App() {
|
|
28
|
+
const [page, setPageState] = useState(getHashPage);
|
|
29
|
+
const [data, setData] = useState({
|
|
30
|
+
models: [],
|
|
31
|
+
routes: [],
|
|
32
|
+
mongoConfigured: false,
|
|
33
|
+
routesDirExists: false,
|
|
34
|
+
});
|
|
35
|
+
const [status, setStatus] = useState({});
|
|
36
|
+
const [toasts, setToasts] = useState([]);
|
|
37
|
+
const [confirmState, setConfirmState] = useState(null);
|
|
38
|
+
|
|
39
|
+
const showConfirm = useCallback((message, opts = {}) => {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
setConfirmState({ message, ...opts, resolve });
|
|
42
|
+
});
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const setPage = useCallback((p) => {
|
|
46
|
+
window.location.hash = "#/" + p;
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
function onHash() {
|
|
51
|
+
setPageState(getHashPage());
|
|
52
|
+
}
|
|
53
|
+
window.addEventListener("hashchange", onHash);
|
|
54
|
+
return () => window.removeEventListener("hashchange", onHash);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const toast = useCallback((message, type = "info") => {
|
|
58
|
+
const id = Date.now();
|
|
59
|
+
setToasts((prev) => [...prev, { id, message, type }]);
|
|
60
|
+
setTimeout(
|
|
61
|
+
() => setToasts((prev) => prev.filter((t) => t.id !== id)),
|
|
62
|
+
3500,
|
|
63
|
+
);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const refresh = useCallback(async () => {
|
|
67
|
+
// File-writing actions (create route, docs, security, ...) modify index.js,
|
|
68
|
+
// which triggers a nodemon restart. Since the dashboard is served by that same
|
|
69
|
+
// app, the refresh fetch fired right after can hit the restart window and fail,
|
|
70
|
+
// leaving the UI stale until a manual reload. Retry across the restart so new
|
|
71
|
+
// state shows up in real time.
|
|
72
|
+
let lastErr;
|
|
73
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
74
|
+
try {
|
|
75
|
+
const [dashData, statusData] = await Promise.all([
|
|
76
|
+
api("/dashboard"),
|
|
77
|
+
api("/status"),
|
|
78
|
+
]);
|
|
79
|
+
setData(dashData);
|
|
80
|
+
setStatus(statusData);
|
|
81
|
+
return;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
lastErr = err;
|
|
84
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw lastErr;
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
refresh().catch(() => {});
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const pages = {
|
|
95
|
+
dashboard: html`<${DashboardPage} />`,
|
|
96
|
+
"ai-builder": html`<${AiBuilderPage} />`,
|
|
97
|
+
database: html`<${DatabasePage} />`,
|
|
98
|
+
schemas: html`<${SchemasPage} />`,
|
|
99
|
+
routes: html`<${RoutesPage} />`,
|
|
100
|
+
docs: html`<${DocsPage} />`,
|
|
101
|
+
security: html`<${SecurityPage} />`,
|
|
102
|
+
"api-tester": html`<${ApiTesterPage} />`,
|
|
103
|
+
realtime: html`<${RealtimePage} />`,
|
|
104
|
+
serialport: html`<${SerialPortPage} />`,
|
|
105
|
+
mail: html`<${MailPage} />`,
|
|
106
|
+
logs: html`<${LogsPage} />`,
|
|
107
|
+
env: html`<${EnvironmentPage} />`,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return html`<${AppContext.Provider}
|
|
111
|
+
value=${{ page, setPage, data, status, toasts, toast, refresh, showConfirm }}
|
|
112
|
+
>
|
|
113
|
+
<${Sidebar} />
|
|
114
|
+
<div className="main">
|
|
115
|
+
<div className="topbar">
|
|
116
|
+
<h2>${PAGE_TITLES[page] || "Dashboard"}</h2>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="content">
|
|
119
|
+
<${AnimatePresence} mode="wait">
|
|
120
|
+
<${motion.div} key=${page}
|
|
121
|
+
initial=${{ opacity: 0, y: 8 }}
|
|
122
|
+
animate=${{ opacity: 1, y: 0 }}
|
|
123
|
+
exit=${{ opacity: 0, y: -8 }}
|
|
124
|
+
transition=${{ duration: 0.15 }}>
|
|
125
|
+
${pages[page] || pages.dashboard}
|
|
126
|
+
<//>
|
|
127
|
+
<//>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<${ToastContainer} />
|
|
131
|
+
${confirmState && html`<${ConfirmDialog}
|
|
132
|
+
message=${confirmState.message}
|
|
133
|
+
confirmLabel=${confirmState.confirmLabel}
|
|
134
|
+
danger=${confirmState.danger}
|
|
135
|
+
onConfirm=${() => { confirmState.resolve(true); setConfirmState(null); }}
|
|
136
|
+
onCancel=${() => { confirmState.resolve(false); setConfirmState(null); }}
|
|
137
|
+
/>`}
|
|
138
|
+
<//>`;
|
|
139
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { PasskeyScreen } from "./screens/PasskeyScreen.js";
|
|
7
|
+
|
|
8
|
+
export function Root() {
|
|
9
|
+
const [authed, setAuthed] = useState(false);
|
|
10
|
+
const [checking, setChecking] = useState(true);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
(async () => {
|
|
14
|
+
try {
|
|
15
|
+
const stored = getStoredPasskey();
|
|
16
|
+
const res = await fetch(API + '/auth', {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
body: JSON.stringify({ passkey: stored }),
|
|
20
|
+
});
|
|
21
|
+
if (res.ok) setAuthed(true);
|
|
22
|
+
} catch {}
|
|
23
|
+
setChecking(false);
|
|
24
|
+
})();
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
if (checking) return null;
|
|
28
|
+
|
|
29
|
+
return html`<${AnimatePresence} mode="wait">
|
|
30
|
+
${!authed
|
|
31
|
+
? html`<${PasskeyScreen} key="passkey" onSuccess=${() => setAuthed(true)} />`
|
|
32
|
+
: html`<${motion.div} key="app"
|
|
33
|
+
style=${{ display: 'flex', height: '100vh', width: '100%' }}
|
|
34
|
+
initial=${{ opacity: 0 }}
|
|
35
|
+
animate=${{ opacity: 1 }}
|
|
36
|
+
transition=${{ duration: 0.4 }}>
|
|
37
|
+
<${App} />
|
|
38
|
+
<//>`}
|
|
39
|
+
<//>`;
|
|
40
|
+
}
|
|
@@ -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,73 @@
|
|
|
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
|
+
// AI Builder — temporarily hidden from the menu (re-enable by uncommenting)
|
|
17
|
+
// { id: "ai-builder", label: "AI Builder", icon: Icons.ai },
|
|
18
|
+
{
|
|
19
|
+
id: "database",
|
|
20
|
+
label: "Database",
|
|
21
|
+
icon: Icons.database,
|
|
22
|
+
section: "Setup",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "schemas",
|
|
26
|
+
label: "Schemas",
|
|
27
|
+
icon: Icons.schema,
|
|
28
|
+
locked: !dbOk,
|
|
29
|
+
lockMsg: "Requires database",
|
|
30
|
+
},
|
|
31
|
+
{ id: "routes", label: "API Routes", icon: Icons.route },
|
|
32
|
+
// { id: "docs", label: "API Docs", icon: Icons.docs }, // hidden for now
|
|
33
|
+
{ id: "api-tester", label: "API Tester", icon: Icons.apiTester },
|
|
34
|
+
{
|
|
35
|
+
id: "security",
|
|
36
|
+
label: "Security",
|
|
37
|
+
icon: Icons.shield,
|
|
38
|
+
section: "Features",
|
|
39
|
+
},
|
|
40
|
+
{ id: "realtime", label: "Real-time", icon: Icons.realtime },
|
|
41
|
+
{ id: "serialport", label: "SerialPort", icon: Icons.serial },
|
|
42
|
+
// { id: "mail", label: "Email", icon: Icons.mail }, // hidden for now
|
|
43
|
+
{ id: "logs", label: "Logs", icon: Icons.logs, section: "Settings" },
|
|
44
|
+
{ id: "env", label: "Environment", icon: Icons.env },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
let currentSection = null;
|
|
48
|
+
return html`<div className="sidebar">
|
|
49
|
+
<div className="sidebar-brand">
|
|
50
|
+
<pre>${`██╗ ██╗██████╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗\n██║ ██║██╔══██╗████╗ ██║██╔═══██╗██╔══██╗██╔════╝\n███████║██████╔╝██╔██╗ ██║██║ ██║██║ ██║█████╗\n╚════██║██╔══██╗██║╚██╗██║██║ ██║██║ ██║██╔══╝\n ██║██████╔╝██║ ╚████║╚██████╔╝██████╔╝███████╗\n ╚═╝╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝`}</pre>
|
|
51
|
+
</div>
|
|
52
|
+
${navItems.map((item) => {
|
|
53
|
+
const sectionHeader =
|
|
54
|
+
item.section && item.section !== currentSection
|
|
55
|
+
? ((currentSection = item.section),
|
|
56
|
+
html`<div className="sidebar-section">${item.section}</div>`)
|
|
57
|
+
: null;
|
|
58
|
+
return html`<${React.Fragment} key=${item.id}>
|
|
59
|
+
${sectionHeader}
|
|
60
|
+
<div
|
|
61
|
+
className=${`sidebar-item ${page === item.id ? "active" : ""} ${item.locked ? "locked" : ""}`}
|
|
62
|
+
onClick=${() => !item.locked && setPage(item.id)}
|
|
63
|
+
title=${item.locked ? item.lockMsg : item.label}
|
|
64
|
+
>
|
|
65
|
+
<span className="sidebar-icon">${item.icon}</span>
|
|
66
|
+
${item.label}
|
|
67
|
+
${item.locked &&
|
|
68
|
+
html`<span className="lock-icon">${Icons.lock}</span>`}
|
|
69
|
+
</div>
|
|
70
|
+
<//>`;
|
|
71
|
+
})}
|
|
72
|
+
</div>`;
|
|
73
|
+
}
|
|
@@ -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,113 @@
|
|
|
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
|
+
LuScrollText,
|
|
23
|
+
LuSparkles,
|
|
24
|
+
LuBookOpen,
|
|
25
|
+
LuMail,
|
|
26
|
+
} from "react-icons/lu";
|
|
27
|
+
|
|
28
|
+
export const html = htm.bind(React.createElement);
|
|
29
|
+
export const API = "/_dev/api";
|
|
30
|
+
|
|
31
|
+
export function getStoredPasskey() {
|
|
32
|
+
return sessionStorage.getItem("_dev_passkey") || "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function copyToClipboard(text) {
|
|
36
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
37
|
+
return navigator.clipboard.writeText(text);
|
|
38
|
+
}
|
|
39
|
+
const ta = document.createElement("textarea");
|
|
40
|
+
ta.value = text;
|
|
41
|
+
ta.style.cssText = "position:fixed;opacity:0";
|
|
42
|
+
document.body.appendChild(ta);
|
|
43
|
+
ta.select();
|
|
44
|
+
document.execCommand("copy");
|
|
45
|
+
document.body.removeChild(ta);
|
|
46
|
+
return Promise.resolve();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function api(path, options = {}) {
|
|
50
|
+
const passkey = getStoredPasskey();
|
|
51
|
+
const headers = { "Content-Type": "application/json" };
|
|
52
|
+
if (passkey) headers["x-passkey"] = passkey;
|
|
53
|
+
const res = await fetch(API + path, {
|
|
54
|
+
headers,
|
|
55
|
+
...options,
|
|
56
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
57
|
+
});
|
|
58
|
+
const text = await res.text();
|
|
59
|
+
let data;
|
|
60
|
+
try {
|
|
61
|
+
data = JSON.parse(text);
|
|
62
|
+
} catch {
|
|
63
|
+
if (res.ok) return { message: "Done" };
|
|
64
|
+
throw new Error("Server restarting, please wait...");
|
|
65
|
+
}
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const e = new Error(data.error || data.message || "Request failed");
|
|
68
|
+
e.data = data; // expose extra fields (e.g. command output) to callers
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const Icons = {
|
|
75
|
+
dashboard: html`<${LuLayoutDashboard} size=${16} />`,
|
|
76
|
+
database: html`<${LuDatabase} size=${16} />`,
|
|
77
|
+
schema: html`<${LuFileText} size=${16} />`,
|
|
78
|
+
route: html`<${LuRadio} size=${16} />`,
|
|
79
|
+
realtime: html`<${LuActivity} size=${16} />`,
|
|
80
|
+
env: html`<${LuSettings} size=${16} />`,
|
|
81
|
+
plus: html`<${LuPlus} size=${16} />`,
|
|
82
|
+
trash: html`<${LuTrash2} size=${16} />`,
|
|
83
|
+
check: html`<${LuCheck} size=${16} />`,
|
|
84
|
+
lock: html`<${LuLock} size=${12} />`,
|
|
85
|
+
arrow: html`<${LuArrowRight} size=${14} />`,
|
|
86
|
+
edit: html`<${LuPencil} size=${16} />`,
|
|
87
|
+
serial: html`<${LuCable} size=${16} />`,
|
|
88
|
+
zap: html`<${LuZap} size=${16} />`,
|
|
89
|
+
shield: html`<${LuShield} size=${16} />`,
|
|
90
|
+
box: html`<${LuBox} size=${16} />`,
|
|
91
|
+
apiTester: html`<${LuFlaskConical} size=${16} />`,
|
|
92
|
+
send: html`<${LuSend} size=${16} />`,
|
|
93
|
+
logs: html`<${LuScrollText} size=${16} />`,
|
|
94
|
+
ai: html`<${LuSparkles} size=${16} />`,
|
|
95
|
+
docs: html`<${LuBookOpen} size=${16} />`,
|
|
96
|
+
mail: html`<${LuMail} size=${16} />`,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const PAGE_TITLES = {
|
|
100
|
+
dashboard: "Dashboard",
|
|
101
|
+
"ai-builder": "AI Builder",
|
|
102
|
+
database: "Database",
|
|
103
|
+
schemas: "Schemas",
|
|
104
|
+
routes: "API Routes",
|
|
105
|
+
docs: "API Docs",
|
|
106
|
+
security: "Security",
|
|
107
|
+
"api-tester": "API Tester",
|
|
108
|
+
realtime: "Real-time",
|
|
109
|
+
serialport: "SerialPort",
|
|
110
|
+
mail: "Email",
|
|
111
|
+
logs: "Logs",
|
|
112
|
+
env: "Environment",
|
|
113
|
+
};
|