4bnode 4.1.4 → 4.1.6

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 (70) hide show
  1. package/add-api.js +5 -3
  2. package/add-docs.js +18 -5
  3. package/add-email.js +118 -28
  4. package/add-login.js +35 -16
  5. package/add-mongo-schema.js +13 -5
  6. package/lib/codegen.js +408 -104
  7. package/lib/codegen.test.js +122 -15
  8. package/lib/names.js +1 -1
  9. package/package.json +1 -1
  10. package/skeleton/.4bnode/assets/js/components/Sidebar.js +2 -2
  11. package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +54 -9
  12. package/skeleton/.4bnode/assets/js/pages/LogsPage.js +49 -16
  13. package/skeleton/.4bnode/assets/js/pages/MailPage.js +221 -71
  14. package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +51 -2
  15. package/skeleton/.4bnode/assets/js/pages/SecurityPage.js +1 -1
  16. package/skeleton/.4bnode/codegen.js +408 -104
  17. package/skeleton/.4bnode/dev-api.js +384 -30
  18. package/skeleton/.env +6 -0
  19. package/skeleton/.env.example +25 -0
  20. package/skeleton/_gitignore +1 -0
  21. package/skeleton/index.js +7 -2
  22. package/skeleton/src/middleware/apiKey.js +14 -1
  23. package/skeleton/src/middleware/errorHandler.js +7 -2
  24. package/app-1/.4bnode/.passkey +0 -1
  25. package/app-1/.4bnode/assets/fonts/NotoSans-Bold.woff2 +0 -0
  26. package/app-1/.4bnode/assets/fonts/NotoSans-Light.woff2 +0 -0
  27. package/app-1/.4bnode/assets/fonts/NotoSans-Medium.woff2 +0 -0
  28. package/app-1/.4bnode/assets/fonts/NotoSans-Regular.woff2 +0 -0
  29. package/app-1/.4bnode/assets/fonts/NotoSans-SemiBold.woff2 +0 -0
  30. package/app-1/.4bnode/assets/images/mongo.png +0 -0
  31. package/app-1/.4bnode/assets/js/App.js +0 -139
  32. package/app-1/.4bnode/assets/js/Root.js +0 -40
  33. package/app-1/.4bnode/assets/js/components/Modal.js +0 -52
  34. package/app-1/.4bnode/assets/js/components/Sidebar.js +0 -73
  35. package/app-1/.4bnode/assets/js/components/ToastContainer.js +0 -15
  36. package/app-1/.4bnode/assets/js/context.js +0 -7
  37. package/app-1/.4bnode/assets/js/lib.js +0 -113
  38. package/app-1/.4bnode/assets/js/main.js +0 -5
  39. package/app-1/.4bnode/assets/js/pages/AiBuilderPage.js +0 -268
  40. package/app-1/.4bnode/assets/js/pages/ApiTesterPage.js +0 -1879
  41. package/app-1/.4bnode/assets/js/pages/DashboardPage.js +0 -435
  42. package/app-1/.4bnode/assets/js/pages/DatabasePage.js +0 -300
  43. package/app-1/.4bnode/assets/js/pages/DocsPage.js +0 -150
  44. package/app-1/.4bnode/assets/js/pages/EnvironmentPage.js +0 -103
  45. package/app-1/.4bnode/assets/js/pages/LogsPage.js +0 -205
  46. package/app-1/.4bnode/assets/js/pages/MailPage.js +0 -134
  47. package/app-1/.4bnode/assets/js/pages/RealtimePage.js +0 -186
  48. package/app-1/.4bnode/assets/js/pages/RoutesPage.js +0 -1436
  49. package/app-1/.4bnode/assets/js/pages/SchemasPage.js +0 -838
  50. package/app-1/.4bnode/assets/js/pages/SecurityPage.js +0 -74
  51. package/app-1/.4bnode/assets/js/pages/SerialPortPage.js +0 -170
  52. package/app-1/.4bnode/assets/js/screens/PasskeyScreen.js +0 -105
  53. package/app-1/.4bnode/codegen.js +0 -1249
  54. package/app-1/.4bnode/dev-api.js +0 -3621
  55. package/app-1/.4bnode/ui.html +0 -1578
  56. package/app-1/README.md +0 -47
  57. package/app-1/ecosystem.config.cjs +0 -25
  58. package/app-1/index.js +0 -145
  59. package/app-1/package.json +0 -35
  60. package/app-1/public/index.html +0 -292
  61. package/app-1/src/db.js +0 -13
  62. package/app-1/src/docs.js +0 -84
  63. package/app-1/src/middleware/errorHandler.js +0 -15
  64. package/app-1/src/middleware/roles.js +0 -14
  65. package/app-1/src/middleware/validate.js +0 -20
  66. package/app-1/src/models/login.js +0 -9
  67. package/app-1/src/openapi.json +0 -62
  68. package/app-1/src/routes/products.js +0 -26
  69. package/app-1/src/validators/products.js +0 -7
  70. package/app-1/src/validators/user.js +0 -7
@@ -1,268 +0,0 @@
1
- import React, { useState, useEffect, useCallback } from "react";
2
- import { LuSparkles, LuCheck, LuChevronDown, LuChevronRight, LuKey } from "react-icons/lu";
3
- import { html, api } from "../lib.js";
4
- import { useApp } from "../context.js";
5
-
6
- const EXAMPLES = [
7
- "A blog with posts (title, body, author) and full CRUD",
8
- "A todo app: users and tasks linked to a user, with create/read/update/delete",
9
- "An e-commerce backend: products, categories, and orders",
10
- ];
11
-
12
- export function AiBuilderPage() {
13
- const { status, toast, refresh } = useApp();
14
-
15
- const [config, setConfig] = useState(null);
16
- const [loadError, setLoadError] = useState(null);
17
- const [selectedTab, setSelectedTab] = useState(null); // provider id currently viewed
18
- const [editingKey, setEditingKey] = useState(false);
19
- const [keyInput, setKeyInput] = useState("");
20
- const [savingKey, setSavingKey] = useState(false);
21
-
22
- const [prompt, setPrompt] = useState("");
23
- const [loading, setLoading] = useState(false);
24
- const [preview, setPreview] = useState(null);
25
- const [plan, setPlan] = useState(null);
26
- const [applying, setApplying] = useState(false);
27
-
28
- const loadConfig = useCallback(async () => {
29
- setLoadError(null);
30
- try {
31
- const c = await api("/ai/config");
32
- setConfig(c);
33
- setSelectedTab((cur) => cur || c.provider);
34
- } catch (err) {
35
- setLoadError(err.message);
36
- toast(err.message, "error");
37
- }
38
- }, [toast]);
39
-
40
- useEffect(() => { loadConfig(); }, [loadConfig]);
41
-
42
- const activeProvider = config?.provider;
43
- const providerById = (id) => (config?.providers || []).find((p) => p.id === id);
44
- const activeConfigured = !!providerById(activeProvider)?.configured;
45
- const sel = providerById(selectedTab);
46
- const showKeyForm = sel && (!sel.configured || editingKey);
47
-
48
- function selectTab(id) {
49
- setSelectedTab(id);
50
- setEditingKey(false);
51
- setKeyInput("");
52
- }
53
-
54
- async function saveKey(providerId) {
55
- if (!keyInput.trim()) return;
56
- setSavingKey(true);
57
- try {
58
- const r = await api("/ai/key", { method: "POST", body: { provider: providerId, key: keyInput.trim() } });
59
- toast(r.message, "success");
60
- setKeyInput("");
61
- setEditingKey(false);
62
- await loadConfig();
63
- refresh().catch(() => {});
64
- } catch (err) {
65
- toast(err.message, "error");
66
- }
67
- setSavingKey(false);
68
- }
69
-
70
- async function switchTo(providerId) {
71
- try {
72
- const r = await api("/ai/provider", { method: "POST", body: { provider: providerId } });
73
- toast(r.message, "success");
74
- await loadConfig();
75
- refresh().catch(() => {});
76
- } catch (err) {
77
- toast(err.message, "error");
78
- }
79
- }
80
-
81
- async function generate() {
82
- if (!prompt.trim()) { toast("Describe what you want to build", "error"); return; }
83
- setLoading(true);
84
- setPreview(null);
85
- setPlan(null);
86
- try {
87
- const r = await api("/ai/plan", { method: "POST", body: { prompt: prompt.trim() } });
88
- setPreview(r.preview);
89
- setPlan(r.plan);
90
- } catch (err) {
91
- toast(err.message, "error");
92
- }
93
- setLoading(false);
94
- }
95
-
96
- async function apply() {
97
- if (!plan) return;
98
- setApplying(true);
99
- try {
100
- const r = await api("/ai/apply", { method: "POST", body: { plan } });
101
- toast(r.message, "success");
102
- if (r.skipped && r.skipped.length) toast(`Skipped existing: ${r.skipped.join(", ")}`, "info");
103
- setPreview(null);
104
- setPlan(null);
105
- setPrompt("");
106
- refresh().catch(() => {});
107
- } catch (err) {
108
- toast(err.message, "error");
109
- }
110
- setApplying(false);
111
- }
112
-
113
- if (!config) {
114
- if (loadError) {
115
- return html`<div>
116
- <div className="card-title mb-1">AI Builder</div>
117
- <div className="card" style=${{ maxWidth: "560px", borderColor: "rgba(239,68,68,0.35)" }}>
118
- <div style=${{ fontWeight: 600, marginBottom: "6px" }}>Couldn't load AI settings</div>
119
- <div className="text-sm text-muted mb-4">${loadError}</div>
120
- <div className="text-sm text-muted mb-4">
121
- If you just updated 4bnode, restart the dev server — <span style=${{ fontFamily: "monospace" }}>npm run dev</span>
122
- does not hot-reload the <span style=${{ fontFamily: "monospace" }}>.4bnode/</span> dashboard.
123
- </div>
124
- <button className="btn btn-primary" onClick=${loadConfig}>Retry</button>
125
- </div>
126
- </div>`;
127
- }
128
- return html`<div className="text-muted" style=${{ padding: "16px" }}>Loading…</div>`;
129
- }
130
-
131
- function statusChip(p) {
132
- if (p.configured && p.id === activeProvider)
133
- return html`<span className="badge badge-success" style=${{ fontSize: "9px", marginLeft: "6px" }}>Active</span>`;
134
- if (p.configured)
135
- return html`<span className="badge" style=${{ fontSize: "9px", marginLeft: "6px" }}>Connected</span>`;
136
- return html`<span className="badge" style=${{ fontSize: "9px", marginLeft: "6px", opacity: 0.6 }}>No key</span>`;
137
- }
138
-
139
- return html`<div>
140
- <div className="flex items-center justify-between mb-4">
141
- <div>
142
- <div className="card-title">AI Builder</div>
143
- <div className="card-subtitle">Describe your backend — review the plan — generate</div>
144
- </div>
145
- </div>
146
-
147
- <div className="card mb-4" style=${{ padding: 0, overflow: "hidden" }}>
148
- <div className="api-tester-tabs">
149
- ${config.providers.map((p) => html`<button key=${p.id}
150
- className=${`api-tester-tab${selectedTab === p.id ? " active" : ""}`}
151
- onClick=${() => selectTab(p.id)}>
152
- ${p.label}${statusChip(p)}
153
- </button>`)}
154
- </div>
155
-
156
- ${sel && html`<div style=${{ padding: "16px" }}>
157
- ${showKeyForm
158
- ? html`<div>
159
- <div className="flex items-center gap-3 mb-3">
160
- <${LuKey} size=${16} color="var(--accent)" />
161
- <div className="text-sm text-muted">
162
- ${editingKey ? "Replace" : "Add"} the ${sel.label} API key — stored in your .env
163
- </div>
164
- </div>
165
- <div className="form-group">
166
- <input className="form-input" type="password" value=${keyInput}
167
- onInput=${(e) => setKeyInput(e.target.value)} placeholder=${sel.keyPlaceholder}
168
- style=${{ maxWidth: "420px" }} />
169
- </div>
170
- <div style=${{ display: "flex", gap: "8px", alignItems: "center" }}>
171
- <button className="btn btn-primary" onClick=${() => saveKey(sel.id)} disabled=${savingKey || !keyInput.trim()}>
172
- ${savingKey ? html`<span className="spinner" />` : html`<${LuCheck} size=${16} />`}
173
- ${savingKey ? "Saving..." : "Save key & use"}
174
- </button>
175
- ${editingKey && html`<button className="btn" onClick=${() => { setEditingKey(false); setKeyInput(""); }}>Cancel</button>`}
176
- </div>
177
- <div className="text-sm text-muted mt-4">Get a key at <span style=${{ color: "var(--accent)" }}>${sel.keysUrl}</span>.</div>
178
- </div>`
179
- : html`<div className="flex items-center gap-3" style=${{ flexWrap: "wrap" }}>
180
- <${LuCheck} size=${18} color="var(--success)" />
181
- <div style=${{ flex: 1, minWidth: "180px" }}>
182
- <div style=${{ fontWeight: 600 }}>
183
- ${sel.id === activeProvider ? "In use" : "Connected"}
184
- </div>
185
- <div className="text-sm text-muted">
186
- ${sel.id === activeProvider
187
- ? html`model <span style=${{ fontFamily: "monospace" }}>${config.model}</span>`
188
- : "Not the active provider"}
189
- </div>
190
- </div>
191
- ${sel.id !== activeProvider &&
192
- html`<button className="btn btn-primary" onClick=${() => switchTo(sel.id)}>Use this provider</button>`}
193
- <button className="btn" onClick=${() => { setEditingKey(true); setKeyInput(""); }}>Replace key</button>
194
- </div>`}
195
- </div>`}
196
- </div>
197
-
198
- ${!activeConfigured
199
- ? html`<div className="card text-sm text-muted">Add an API key above to start generating.</div>`
200
- : html`<div>
201
- ${!status?.mongoConfigured &&
202
- html`<div className="card mb-4 text-sm" style=${{ borderColor: "rgba(245,158,11,0.3)" }}>
203
- Configure MongoDB first — models and routes need a database to run.
204
- </div>`}
205
-
206
- <div className="card mb-4">
207
- <textarea className="form-input" rows=${3} value=${prompt}
208
- onInput=${(e) => setPrompt(e.target.value)}
209
- placeholder="e.g. A blog with posts and comments, authored by users, with full CRUD"
210
- style=${{ resize: "vertical", fontFamily: "inherit" }} />
211
- <div style=${{ display: "flex", flexWrap: "wrap", gap: "6px", margin: "10px 0" }}>
212
- ${EXAMPLES.map((ex) => html`<button key=${ex} className="badge"
213
- onClick=${() => setPrompt(ex)}
214
- style=${{ cursor: "pointer", border: "1px solid var(--border)", background: "var(--bg-input)", color: "var(--text-secondary)", fontWeight: 400 }}>${ex}</button>`)}
215
- </div>
216
- <button className="btn btn-primary" onClick=${generate} disabled=${loading || !prompt.trim()}>
217
- ${loading ? html`<span className="spinner" />` : html`<${LuSparkles} size=${16} />`}
218
- ${loading ? "Designing..." : "Generate plan"}
219
- </button>
220
- </div>
221
-
222
- ${preview &&
223
- html`<div className="card">
224
- <div className="flex items-center justify-between mb-2">
225
- <div className="card-title">Plan</div>
226
- <button className="btn btn-primary" onClick=${apply} disabled=${applying}>
227
- ${applying ? html`<span className="spinner" />` : html`<${LuCheck} size=${16} />`}
228
- ${applying ? "Generating..." : "Generate files"}
229
- </button>
230
- </div>
231
- ${preview.summary && html`<div className="text-sm text-muted mb-4">${preview.summary}</div>`}
232
-
233
- ${preview.models.length > 0 &&
234
- html`<div className="mb-4">
235
- <div style=${{ fontSize: "11px", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.5px", color: "var(--text-muted)", marginBottom: "8px" }}>
236
- Models (${preview.models.length})
237
- </div>
238
- ${preview.models.map((m) => html`<${CodeBlock} key=${m.name}
239
- title=${`src/models/${m.name}.js`} subtitle=${m.fields.map((f) => f.name).join(", ")} code=${m.code} />`)}
240
- </div>`}
241
-
242
- ${preview.routes.length > 0 &&
243
- html`<div>
244
- <div style=${{ fontSize: "11px", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.5px", color: "var(--text-muted)", marginBottom: "8px" }}>
245
- Routes (${preview.routes.length})
246
- </div>
247
- ${preview.routes.map((r) => html`<${CodeBlock} key=${r.name}
248
- title=${`/api/${r.name}`} subtitle=${`${r.model} · ${r.operations.join(", ")}`} code=${r.code} />`)}
249
- </div>`}
250
- </div>`}
251
- </div>`}
252
- </div>`;
253
- }
254
-
255
- function CodeBlock({ title, subtitle, code }) {
256
- const [open, setOpen] = useState(false);
257
- return html`<div style=${{ border: "1px solid var(--border)", borderRadius: "8px", marginBottom: "8px", overflow: "hidden" }}>
258
- <div onClick=${() => setOpen((o) => !o)}
259
- style=${{ display: "flex", alignItems: "center", gap: "8px", padding: "10px 12px", cursor: "pointer" }}>
260
- ${open ? html`<${LuChevronDown} size=${14} />` : html`<${LuChevronRight} size=${14} />`}
261
- <div style=${{ flex: 1, minWidth: 0 }}>
262
- <div style=${{ fontWeight: 600, fontSize: "13px" }}>${title}</div>
263
- <div className="text-sm text-muted" style=${{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>${subtitle}</div>
264
- </div>
265
- </div>
266
- ${open && html`<pre style=${{ margin: 0, padding: "12px", background: "var(--bg)", borderTop: "1px solid var(--border)", overflowX: "auto", fontSize: "12px", lineHeight: 1.5 }}>${code}</pre>`}
267
- </div>`;
268
- }