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
@@ -0,0 +1,170 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { LuCable } from "react-icons/lu";
3
+ import { html, api, Icons } from "../lib.js";
4
+ import { useApp } from "../context.js";
5
+
6
+ export function SerialPortPage() {
7
+ const { toast, refresh, status, showConfirm } = useApp();
8
+ const [ports, setPorts] = useState([]);
9
+ const [selected, setSelected] = useState(null);
10
+ const [showAdd, setShowAdd] = useState(false);
11
+ const [portName, setPortName] = useState("serialPort");
12
+ const [portPath, setPortPath] = useState("/dev/ttyUSB0");
13
+ const [baudRate, setBaudRate] = useState("9600");
14
+ const [loading, setLoading] = useState(false);
15
+
16
+ useEffect(() => {
17
+ api("/serialports").then(setPorts).catch(() => {});
18
+ }, [status]);
19
+
20
+ async function handleAdd() {
21
+ setLoading(true);
22
+ try {
23
+ const result = await api("/add-serialport", {
24
+ method: "POST",
25
+ body: { name: portName, portPath, baudRate: parseInt(baudRate) },
26
+ });
27
+ toast(result.message, "success");
28
+ setShowAdd(false);
29
+ setPortName("serialPort" + ((ports?.length || 0) + 1));
30
+ setPortPath("/dev/ttyUSB0");
31
+ setBaudRate("9600");
32
+ refresh();
33
+ api("/serialports").then(setPorts).catch(() => {});
34
+ } catch (err) { toast(err.message, "error"); }
35
+ setLoading(false);
36
+ }
37
+
38
+ async function handleDelete(name) {
39
+ const ok = await showConfirm('Remove serial port "' + name + '"? This will remove its code from index.js.', { danger: true, confirmLabel: 'Remove' });
40
+ if (!ok) return;
41
+ try {
42
+ await api("/serialports/" + name, { method: "DELETE" });
43
+ toast('Serial port "' + name + '" removed', "success");
44
+ setSelected(null);
45
+ refresh();
46
+ api("/serialports").then(setPorts).catch(() => {});
47
+ } catch (err) { toast(err.message, "error"); }
48
+ }
49
+
50
+ const selPort = ports.find(p => p.name === selected);
51
+ const hasNoPorts = !ports || ports.length === 0;
52
+
53
+ if (hasNoPorts && !showAdd) {
54
+ return html`<div>
55
+ <div className="flex items-center justify-between mb-4">
56
+ <div>
57
+ <div className="card-title">SerialPort</div>
58
+ <div className="card-subtitle">0 connections</div>
59
+ </div>
60
+ <button className="btn btn-primary" onClick=${() => setShowAdd(true)}>
61
+ <span className="sidebar-icon">${Icons.plus}</span> Add Port
62
+ </button>
63
+ </div>
64
+ <div className="empty-state">
65
+ <div className="empty-icon"><${LuCable} size=${36} /></div>
66
+ <h3>No serial ports</h3>
67
+ <p>Add a serial port connection to communicate with hardware devices</p>
68
+ <button className="btn btn-primary" onClick=${() => setShowAdd(true)}>Add Port</button>
69
+ </div>
70
+ </div>`;
71
+ }
72
+
73
+ return html`<div>
74
+ <div className="flex items-center justify-between mb-4">
75
+ <div>
76
+ <div className="card-title">SerialPort</div>
77
+ <div className="card-subtitle">${ports.length} connection${ports.length !== 1 ? 's' : ''}</div>
78
+ </div>
79
+ <button className=${`btn ${showAdd ? 'btn-ghost' : 'btn-primary'}`}
80
+ onClick=${() => {
81
+ if (showAdd) { setShowAdd(false); if (ports.length) setSelected(ports[0].name); }
82
+ else { setShowAdd(true); setSelected(null); setPortName("serialPort" + (ports.length + 1)); }
83
+ }}>
84
+ ${showAdd ? 'Cancel' : html`<span className="sidebar-icon">${Icons.plus}</span> Add Port`}
85
+ </button>
86
+ </div>
87
+
88
+ <div className="routes-layout">
89
+ <div className="card" style=${{ padding: 0, overflow: 'hidden' }}>
90
+ <div style=${{ padding: '14px 16px 10px', borderBottom: '1px solid var(--border)' }}>
91
+ <span style=${{ fontWeight: 600, fontSize: '12px', textTransform: 'uppercase', letterSpacing: '0.5px', color: 'var(--text-muted)' }}>Connections</span>
92
+ </div>
93
+ <div className="routes-list">
94
+ ${ports.map(p => html`<div key=${p.name}
95
+ className=${`route-item ${selected === p.name && !showAdd ? 'active' : ''}`}
96
+ onClick=${() => { setSelected(p.name); setShowAdd(false); }}>
97
+ <div style=${{ display: 'flex', flexDirection: 'column', gap: '4px', flex: 1, minWidth: 0 }}>
98
+ <span className="ri-name">${p.name}</span>
99
+ <span className="ri-file">${p.path} @ ${p.baudRate}</span>
100
+ </div>
101
+ <span className="badge badge-success" style=${{ fontSize: '10px' }}>Active</span>
102
+ </div>`)}
103
+ <div className=${`route-item ${showAdd ? 'active' : ''}`}
104
+ onClick=${() => { setShowAdd(true); setSelected(null); setPortName("serialPort" + (ports.length + 1)); }}
105
+ style=${{ borderTop: ports.length ? '1px solid var(--border)' : 'none' }}>
106
+ <span className="sidebar-icon" style=${{ color: 'var(--accent-light)' }}>${Icons.plus}</span>
107
+ <span className="ri-name" style=${{ color: 'var(--accent-light)' }}>Add Port</span>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ <div className="routes-detail">
113
+ ${showAdd && html`<div className="card">
114
+ <div className="card-title mb-2">Add Serial Port</div>
115
+ <div className="card-subtitle mb-4">Configure a new serial port connection</div>
116
+ <div className="form-group">
117
+ <label className="form-label">Variable Name</label>
118
+ <input className="form-input" value=${portName} onInput=${e => setPortName(e.target.value)} placeholder="serialPort1" />
119
+ </div>
120
+ <div className="grid-2">
121
+ <div className="form-group">
122
+ <label className="form-label">Port Path</label>
123
+ <input className="form-input" value=${portPath} onInput=${e => setPortPath(e.target.value)} placeholder="/dev/ttyUSB0" />
124
+ </div>
125
+ <div className="form-group">
126
+ <label className="form-label">Baud Rate</label>
127
+ <select className="form-select" value=${baudRate} onChange=${e => setBaudRate(e.target.value)}>
128
+ ${['9600', '14400', '19200', '38400', '57600', '115200'].map(r =>
129
+ html`<option key=${r} value=${r}>${r}</option>`
130
+ )}
131
+ </select>
132
+ </div>
133
+ </div>
134
+ <button className="btn btn-primary" onClick=${handleAdd} disabled=${loading}>
135
+ ${loading ? html`<span className="spinner" />` : null}
136
+ ${loading ? 'Adding...' : 'Add Port'}
137
+ </button>
138
+ </div>`}
139
+
140
+ ${!showAdd && selPort && html`<div>
141
+ <div className="card mb-4">
142
+ <div className="flex items-center justify-between mb-3">
143
+ <div>
144
+ <div style=${{ fontWeight: 600, fontSize: '16px', marginBottom: '2px' }}>${selPort.name}</div>
145
+ <div className="text-sm text-muted" style=${{ fontFamily: "'SF Mono', monospace" }}>${selPort.path} @ ${selPort.baudRate} baud</div>
146
+ </div>
147
+ <button className="btn btn-danger btn-sm btn-icon" onClick=${() => handleDelete(selPort.name)}>
148
+ <span className="sidebar-icon">${Icons.trash}</span>
149
+ </button>
150
+ </div>
151
+ <div style=${{ fontSize: '11px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px', color: 'var(--text-muted)', marginBottom: '8px' }}>Configuration</div>
152
+ <div style=${{ display: 'grid', gridTemplateColumns: '100px 1fr', gap: '6px 12px', fontSize: '13px' }}>
153
+ <span className="text-muted">Variable</span><span>${selPort.name}</span>
154
+ <span className="text-muted">Path</span><span style=${{ fontFamily: 'monospace' }}>${selPort.path}</span>
155
+ <span className="text-muted">Baud Rate</span><span>${selPort.baudRate}</span>
156
+ <span className="text-muted">Parser</span><span>ReadlineParser (\\r\\n)</span>
157
+ </div>
158
+ </div>
159
+ </div>`}
160
+
161
+ ${!showAdd && !selPort && html`<div className="card">
162
+ <div className="empty-state" style=${{ padding: '40px' }}>
163
+ <p>Select a port to view details</p>
164
+ </div>
165
+ </div>`}
166
+ </div>
167
+ </div>
168
+ </div>`;
169
+ }
170
+
@@ -0,0 +1,105 @@
1
+ import React, { useState, useEffect, useRef } from "react";
2
+ import { motion } from "framer-motion";
3
+ import { LuLock } from "react-icons/lu";
4
+ import { html, API, getStoredPasskey } from "../lib.js";
5
+
6
+ export function PasskeyScreen({ onSuccess }) {
7
+ const [digits, setDigits] = useState(['', '', '', '', '', '']);
8
+ const [error, setError] = useState('');
9
+ const [loading, setLoading] = useState(false);
10
+ const inputRefs = useRef([]);
11
+
12
+ const handleChange = (index, value) => {
13
+ if (!/^\d?$/.test(value)) return;
14
+ const next = [...digits];
15
+ next[index] = value;
16
+ setDigits(next);
17
+ setError('');
18
+ if (value && index < 5) {
19
+ inputRefs.current[index + 1]?.focus();
20
+ }
21
+ };
22
+
23
+ const handleKeyDown = (index, e) => {
24
+ if (e.key === 'Backspace' && !digits[index] && index > 0) {
25
+ inputRefs.current[index - 1]?.focus();
26
+ }
27
+ if (e.key === 'Enter') handleSubmit();
28
+ };
29
+
30
+ const handlePaste = (e) => {
31
+ e.preventDefault();
32
+ const pasted = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, 6);
33
+ if (pasted.length === 0) return;
34
+ const next = [...digits];
35
+ for (let i = 0; i < 6; i++) next[i] = pasted[i] || '';
36
+ setDigits(next);
37
+ const focusIdx = Math.min(pasted.length, 5);
38
+ inputRefs.current[focusIdx]?.focus();
39
+ };
40
+
41
+ const handleSubmit = async () => {
42
+ const pin = digits.join('');
43
+ if (pin.length !== 6) { setError('Enter all 6 digits'); return; }
44
+ setLoading(true);
45
+ try {
46
+ const res = await fetch(API + '/auth', {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({ passkey: pin }),
50
+ });
51
+ const data = await res.json();
52
+ if (!res.ok) {
53
+ setError(data.error || 'Invalid passkey');
54
+ setDigits(['', '', '', '', '', '']);
55
+ inputRefs.current[0]?.focus();
56
+ } else {
57
+ sessionStorage.setItem('_dev_passkey', pin);
58
+ onSuccess();
59
+ }
60
+ } catch {
61
+ setError('Connection failed');
62
+ } finally {
63
+ setLoading(false);
64
+ }
65
+ };
66
+
67
+ useEffect(() => { inputRefs.current[0]?.focus(); }, []);
68
+
69
+ return html`<${motion.div}
70
+ className="passkey-screen"
71
+ initial=${{ opacity: 0 }}
72
+ animate=${{ opacity: 1 }}
73
+ exit=${{ opacity: 0 }}
74
+ transition=${{ duration: 0.3 }}
75
+ >
76
+ <div className="splash-glow" />
77
+ <${motion.div}
78
+ className="passkey-card"
79
+ initial=${{ opacity: 0, scale: 0.95, y: 20 }}
80
+ animate=${{ opacity: 1, scale: 1, y: 0 }}
81
+ transition=${{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
82
+ >
83
+ <${LuLock} size=${28} color="#0072ff" />
84
+ <h2>Dashboard Passkey</h2>
85
+ <p>Enter your 6-digit passkey to continue</p>
86
+ <div className="passkey-inputs" onPaste=${handlePaste}>
87
+ ${digits.map((d, i) => html`<input
88
+ key=${i}
89
+ ref=${(el) => { inputRefs.current[i] = el; }}
90
+ type="text"
91
+ inputMode="numeric"
92
+ maxLength="1"
93
+ value=${d}
94
+ className=${error ? 'error' : ''}
95
+ onInput=${(e) => handleChange(i, e.target.value)}
96
+ onKeyDown=${(e) => handleKeyDown(i, e)}
97
+ />`)}
98
+ </div>
99
+ <div className="passkey-error">${error}</div>
100
+ <button className="passkey-btn" onClick=${handleSubmit} disabled=${loading}>
101
+ ${loading ? 'Verifying...' : 'Unlock Dashboard'}
102
+ </button>
103
+ <//>
104
+ <//>`;
105
+ }
@@ -0,0 +1,66 @@
1
+ import React, { useEffect } from "react";
2
+ import { motion } from "framer-motion";
3
+ import { html } from "../lib.js";
4
+
5
+ const LOGO_TEXT = `██╗ ██╗██████╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗
6
+ ██║ ██║██╔══██╗████╗ ██║██╔═══██╗██╔══██╗██╔════╝
7
+ ███████║██████╔╝██╔██╗ ██║██║ ██║██║ ██║█████╗
8
+ ╚════██║██╔══██╗██║╚██╗██║██║ ██║██║ ██║██╔══╝
9
+ ██║██████╔╝██║ ╚████║╚██████╔╝██████╔╝███████╗
10
+ ╚═╝╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝`;
11
+
12
+ export function SplashScreen({ onDone }) {
13
+ useEffect(() => {
14
+ const timer = setTimeout(onDone, 2000);
15
+ return () => clearTimeout(timer);
16
+ }, []);
17
+
18
+ return html`<${motion.div}
19
+ className="splash"
20
+ initial=${{ opacity: 1 }}
21
+ exit=${{ opacity: 0 }}
22
+ transition=${{ duration: 0.5, ease: 'easeInOut' }}
23
+ >
24
+ <div className="splash-glow" />
25
+
26
+ <${motion.div}
27
+ className="splash-logo"
28
+ initial=${{ opacity: 0, scale: 0.8, y: 20 }}
29
+ animate=${{ opacity: 1, scale: 1, y: 0 }}
30
+ transition=${{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
31
+ >
32
+ ${LOGO_TEXT}
33
+ <//>
34
+
35
+ <${motion.div}
36
+ className="splash-tagline"
37
+ initial=${{ opacity: 0, y: 15 }}
38
+ animate=${{ opacity: 1, y: 0 }}
39
+ transition=${{ duration: 0.6, delay: 0.6, ease: 'easeOut' }}
40
+ >
41
+ Supercharge Your Node.js Setup
42
+ <//>
43
+
44
+ <${motion.div}
45
+ initial=${{ opacity: 0 }}
46
+ animate=${{ opacity: 1 }}
47
+ transition=${{ duration: 0.5, delay: 1.2 }}
48
+ >
49
+ <${motion.div}
50
+ style=${{ marginTop: '28px', width: '120px', height: '2px', borderRadius: '1px', background: 'linear-gradient(90deg, transparent, #0072ff, transparent)' }}
51
+ initial=${{ scaleX: 0 }}
52
+ animate=${{ scaleX: 1 }}
53
+ transition=${{ duration: 1, delay: 1.4, ease: 'easeInOut' }}
54
+ />
55
+ <//>
56
+
57
+ <${motion.div}
58
+ className="splash-company"
59
+ initial=${{ opacity: 0 }}
60
+ animate=${{ opacity: 1 }}
61
+ transition=${{ duration: 0.5, delay: 1.0 }}
62
+ >
63
+ a product of <span>4Brains Technologies</span>
64
+ <//>
65
+ <//>`;
66
+ }