4bnode 4.1.5 → 4.1.7

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 (68) hide show
  1. package/add-api.js +5 -3
  2. package/add-email.js +118 -28
  3. package/add-login.js +35 -16
  4. package/add-mongo-schema.js +13 -5
  5. package/lib/codegen.js +191 -57
  6. package/lib/codegen.test.js +87 -15
  7. package/lib/names.js +1 -1
  8. package/package.json +1 -1
  9. package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +54 -9
  10. package/skeleton/.4bnode/assets/js/pages/LogsPage.js +49 -16
  11. package/skeleton/.4bnode/assets/js/pages/MailPage.js +110 -50
  12. package/skeleton/.4bnode/codegen.js +191 -57
  13. package/skeleton/.4bnode/dev-api.js +284 -56
  14. package/skeleton/.env.example +17 -3
  15. package/skeleton/_gitignore +1 -0
  16. package/skeleton/index.js +27 -4
  17. package/skeleton/src/middleware/apiKey.js +14 -1
  18. package/skeleton/src/middleware/errorHandler.js +7 -2
  19. package/app-1/.4bnode/.passkey +0 -1
  20. package/app-1/.4bnode/assets/fonts/NotoSans-Bold.woff2 +0 -0
  21. package/app-1/.4bnode/assets/fonts/NotoSans-Light.woff2 +0 -0
  22. package/app-1/.4bnode/assets/fonts/NotoSans-Medium.woff2 +0 -0
  23. package/app-1/.4bnode/assets/fonts/NotoSans-Regular.woff2 +0 -0
  24. package/app-1/.4bnode/assets/fonts/NotoSans-SemiBold.woff2 +0 -0
  25. package/app-1/.4bnode/assets/images/mongo.png +0 -0
  26. package/app-1/.4bnode/assets/js/App.js +0 -139
  27. package/app-1/.4bnode/assets/js/Root.js +0 -40
  28. package/app-1/.4bnode/assets/js/components/Modal.js +0 -52
  29. package/app-1/.4bnode/assets/js/components/Sidebar.js +0 -73
  30. package/app-1/.4bnode/assets/js/components/ToastContainer.js +0 -15
  31. package/app-1/.4bnode/assets/js/context.js +0 -7
  32. package/app-1/.4bnode/assets/js/lib.js +0 -113
  33. package/app-1/.4bnode/assets/js/main.js +0 -5
  34. package/app-1/.4bnode/assets/js/pages/AiBuilderPage.js +0 -268
  35. package/app-1/.4bnode/assets/js/pages/ApiTesterPage.js +0 -1879
  36. package/app-1/.4bnode/assets/js/pages/DashboardPage.js +0 -435
  37. package/app-1/.4bnode/assets/js/pages/DatabasePage.js +0 -300
  38. package/app-1/.4bnode/assets/js/pages/DocsPage.js +0 -150
  39. package/app-1/.4bnode/assets/js/pages/EnvironmentPage.js +0 -103
  40. package/app-1/.4bnode/assets/js/pages/LogsPage.js +0 -205
  41. package/app-1/.4bnode/assets/js/pages/MailPage.js +0 -224
  42. package/app-1/.4bnode/assets/js/pages/RealtimePage.js +0 -186
  43. package/app-1/.4bnode/assets/js/pages/RoutesPage.js +0 -1485
  44. package/app-1/.4bnode/assets/js/pages/SchemasPage.js +0 -838
  45. package/app-1/.4bnode/assets/js/pages/SecurityPage.js +0 -74
  46. package/app-1/.4bnode/assets/js/pages/SerialPortPage.js +0 -170
  47. package/app-1/.4bnode/assets/js/screens/PasskeyScreen.js +0 -105
  48. package/app-1/.4bnode/codegen.js +0 -1440
  49. package/app-1/.4bnode/dev-api.js +0 -3747
  50. package/app-1/.4bnode/ui.html +0 -1578
  51. package/app-1/.env.example +0 -11
  52. package/app-1/README.md +0 -47
  53. package/app-1/ecosystem.config.cjs +0 -25
  54. package/app-1/index.js +0 -188
  55. package/app-1/package.json +0 -37
  56. package/app-1/public/index.html +0 -292
  57. package/app-1/src/db.js +0 -13
  58. package/app-1/src/docs.js +0 -84
  59. package/app-1/src/middleware/apiKey.js +0 -36
  60. package/app-1/src/middleware/auth.js +0 -17
  61. package/app-1/src/middleware/errorHandler.js +0 -15
  62. package/app-1/src/middleware/roles.js +0 -14
  63. package/app-1/src/middleware/validate.js +0 -20
  64. package/app-1/src/models/user.js +0 -11
  65. package/app-1/src/openapi.json +0 -80
  66. package/app-1/src/routes/upload-resume.js +0 -21
  67. package/app-1/src/services/mailer.js +0 -52
  68. package/app-1/src/validators/upload-resume.js +0 -8
@@ -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
- }