4bnode 4.1.9 → 4.2.1
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-bonjour.js +29 -1
- package/add-email.js +33 -15
- package/add-serialport.js +133 -41
- package/index.js +3 -3
- package/lib/codegen.js +1442 -36
- package/lib/codegen.test.js +233 -13
- package/package.json +1 -1
- package/skeleton/.4bnode/assets/js/App.js +116 -7
- package/skeleton/.4bnode/assets/js/components/CommandPalette.js +165 -0
- package/skeleton/.4bnode/assets/js/components/SerialWorkspace.js +634 -0
- package/skeleton/.4bnode/assets/js/components/Sidebar.js +64 -64
- package/skeleton/.4bnode/assets/js/lib.js +76 -0
- package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +113 -53
- package/skeleton/.4bnode/assets/js/pages/DashboardPage.js +240 -403
- package/skeleton/.4bnode/assets/js/pages/DiscoveryPage.js +86 -8
- package/skeleton/.4bnode/assets/js/pages/MailPage.js +37 -19
- package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +1 -1
- package/skeleton/.4bnode/assets/js/pages/SchemasPage.js +5 -5
- package/skeleton/.4bnode/assets/js/pages/SecurityPage.js +154 -3
- package/skeleton/.4bnode/assets/js/pages/SerialPortPage.js +455 -139
- package/skeleton/.4bnode/codegen.js +1442 -36
- package/skeleton/.4bnode/dev-api.js +568 -95
- package/skeleton/.4bnode/ui.html +1241 -240
- package/skeleton/.env.example +23 -3
- package/skeleton/_gitignore +1 -0
- package/skeleton/index.js +8 -0
- package/skeleton/public/index.html +345 -237
- package/skeleton/src/discovery.js +128 -6
- package/skeleton/src/middleware/apiKey.js +63 -20
|
@@ -2,291 +2,399 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
+
<meta name="color-scheme" content="dark light">
|
|
7
|
+
<title>App is running</title>
|
|
8
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%235b8cff'/%3E%3Ctext x='16' y='21' font-family='system-ui,sans-serif' font-size='13' font-weight='700' fill='white' text-anchor='middle'%3E4b%3C/text%3E%3C/svg%3E">
|
|
7
9
|
<style>
|
|
10
|
+
/* Self-contained on purpose: no external fonts, scripts or images, so the
|
|
11
|
+
page renders the same in production and under helmet's CSP. */
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #07070a;
|
|
14
|
+
--surface: rgba(20, 20, 27, 0.66);
|
|
15
|
+
--border: rgba(255, 255, 255, 0.09);
|
|
16
|
+
--text: #f1f1f4;
|
|
17
|
+
--muted: #9b9ba8;
|
|
18
|
+
--faint: #5e5e6a;
|
|
19
|
+
--a1: #8fb0ff;
|
|
20
|
+
--a2: #5b8cff;
|
|
21
|
+
--a3: #9d7bff;
|
|
22
|
+
--ok: #3ecf8e;
|
|
23
|
+
--warn: #f5a524;
|
|
24
|
+
--net: 143, 176, 255;
|
|
25
|
+
}
|
|
26
|
+
@media (prefers-color-scheme: light) {
|
|
27
|
+
:root {
|
|
28
|
+
--bg: #f6f7fb;
|
|
29
|
+
--surface: rgba(255, 255, 255, 0.75);
|
|
30
|
+
--border: rgba(0, 0, 0, 0.08);
|
|
31
|
+
--text: #15151a;
|
|
32
|
+
--muted: #5b5b66;
|
|
33
|
+
--faint: #9a9aa4;
|
|
34
|
+
--a1: #5b8cff;
|
|
35
|
+
--a2: #3a6ff0;
|
|
36
|
+
--a3: #7b5cf0;
|
|
37
|
+
--ok: #0f9d63;
|
|
38
|
+
--warn: #b7791f;
|
|
39
|
+
--net: 58, 111, 240;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
8
43
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
44
|
+
[hidden] { display: none !important; }
|
|
45
|
+
html, body { height: 100%; overflow: hidden; }
|
|
46
|
+
body {
|
|
47
|
+
font-family: "Inter", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
48
|
+
background: var(--bg);
|
|
49
|
+
color: var(--text);
|
|
50
|
+
-webkit-font-smoothing: antialiased;
|
|
51
|
+
}
|
|
52
|
+
.mono { font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; }
|
|
9
53
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
54
|
+
/* ── Background ── */
|
|
55
|
+
#net { position: fixed; inset: 0; width: 100%; height: 100%; }
|
|
56
|
+
.aurora { position: fixed; inset: 0; pointer-events: none; filter: blur(70px); }
|
|
57
|
+
.aurora span { position: absolute; border-radius: 50%; animation: drift 16s ease-in-out infinite alternate; }
|
|
58
|
+
.aurora span:nth-child(1) { width: 50vmax; height: 50vmax; left: -8vmax; top: -12vmax; background: radial-gradient(circle, rgba(91, 140, 255, 0.30), transparent 62%); }
|
|
59
|
+
.aurora span:nth-child(2) { width: 46vmax; height: 46vmax; right: -10vmax; bottom: -14vmax; background: radial-gradient(circle, rgba(157, 123, 255, 0.26), transparent 62%); animation-duration: 20s; animation-delay: -7s; }
|
|
60
|
+
.aurora span:nth-child(3) { width: 30vmax; height: 30vmax; left: 50%; top: 50%; margin: -15vmax 0 0 -15vmax; background: radial-gradient(circle, rgba(62, 207, 142, 0.12), transparent 62%); animation-duration: 24s; animation-delay: -12s; }
|
|
61
|
+
@media (prefers-color-scheme: light) { .aurora span { opacity: 0.55; } }
|
|
62
|
+
.vignette { position: fixed; inset: 0; pointer-events: none; background: radial-gradient(ellipse 75% 70% at 50% 50%, transparent 35%, var(--bg) 100%); }
|
|
15
63
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
64
|
+
/* ── Stage: one screen, centered ── */
|
|
65
|
+
.stage {
|
|
66
|
+
position: relative;
|
|
67
|
+
z-index: 1;
|
|
68
|
+
height: 100vh;
|
|
69
|
+
height: 100dvh;
|
|
70
|
+
display: grid;
|
|
71
|
+
grid-template-rows: 1fr auto;
|
|
72
|
+
padding: env(safe-area-inset-top) 16px env(safe-area-inset-bottom);
|
|
73
|
+
}
|
|
74
|
+
.center {
|
|
21
75
|
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
22
77
|
align-items: center;
|
|
23
78
|
justify-content: center;
|
|
24
|
-
|
|
79
|
+
text-align: center;
|
|
80
|
+
min-height: 0;
|
|
25
81
|
}
|
|
26
82
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
83
|
+
/* Core: logo, spinning gradient rings, orbiting dots, heartbeat ripples */
|
|
84
|
+
.core {
|
|
85
|
+
--size: clamp(150px, 26vmin, 230px);
|
|
86
|
+
position: relative;
|
|
87
|
+
width: var(--size);
|
|
88
|
+
height: var(--size);
|
|
89
|
+
display: grid;
|
|
90
|
+
place-items: center;
|
|
91
|
+
margin-bottom: clamp(20px, 4.5vmin, 40px);
|
|
92
|
+
animation: pop 1s cubic-bezier(0.2, 0.9, 0.2, 1.2) both;
|
|
31
93
|
}
|
|
32
|
-
|
|
33
|
-
/* Animated grid floor */
|
|
34
|
-
.grid {
|
|
94
|
+
.ring {
|
|
35
95
|
position: absolute;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
linear-gradient(rgba(0,114,255,0.08) 1px, transparent 1px),
|
|
43
|
-
linear-gradient(90deg, rgba(0,114,255,0.08) 1px, transparent 1px);
|
|
44
|
-
background-size: 60px 60px;
|
|
45
|
-
animation: gridScroll 12s linear infinite;
|
|
46
|
-
mask-image: linear-gradient(to top, rgba(0,0,0,0.4) 0%, transparent 80%);
|
|
47
|
-
-webkit-mask-image: linear-gradient(to top, rgba(0,0,0,0.4) 0%, transparent 80%);
|
|
96
|
+
inset: 0;
|
|
97
|
+
border-radius: 50%;
|
|
98
|
+
background: conic-gradient(from 0deg, transparent 0deg, var(--a2) 80deg, var(--a3) 160deg, transparent 230deg, transparent 360deg);
|
|
99
|
+
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
|
|
100
|
+
mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
|
|
101
|
+
animation: spin 3.2s linear infinite;
|
|
48
102
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
103
|
+
.ring.r2 {
|
|
104
|
+
inset: 14%;
|
|
105
|
+
background: conic-gradient(from 180deg, transparent 0deg, var(--a1) 70deg, transparent 150deg, transparent 360deg);
|
|
106
|
+
animation-duration: 5s;
|
|
107
|
+
animation-direction: reverse;
|
|
108
|
+
opacity: 0.8;
|
|
53
109
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.
|
|
110
|
+
.track { position: absolute; inset: 0; border-radius: 50%; border: 1px solid var(--border); }
|
|
111
|
+
.track.t2 { inset: 14%; }
|
|
112
|
+
.orbit { position: absolute; inset: 0; animation: spin 9s linear infinite; }
|
|
113
|
+
.orbit.o2 { inset: 14%; animation-duration: 6s; animation-direction: reverse; }
|
|
114
|
+
.orbit i {
|
|
57
115
|
position: absolute;
|
|
116
|
+
left: 50%;
|
|
117
|
+
top: -4px;
|
|
118
|
+
width: 8px;
|
|
119
|
+
height: 8px;
|
|
120
|
+
margin-left: -4px;
|
|
58
121
|
border-radius: 50%;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
animation: float 8s ease-in-out infinite;
|
|
62
|
-
}
|
|
63
|
-
.orb-1 {
|
|
64
|
-
width: 400px; height: 400px;
|
|
65
|
-
background: radial-gradient(circle, #0072ff, transparent 70%);
|
|
66
|
-
top: 10%; left: 15%;
|
|
67
|
-
animation-delay: 0s;
|
|
68
|
-
}
|
|
69
|
-
.orb-2 {
|
|
70
|
-
width: 350px; height: 350px;
|
|
71
|
-
background: radial-gradient(circle, #7b2ff7, transparent 70%);
|
|
72
|
-
top: 30%; right: 10%;
|
|
73
|
-
animation-delay: -3s;
|
|
74
|
-
animation-duration: 10s;
|
|
75
|
-
}
|
|
76
|
-
.orb-3 {
|
|
77
|
-
width: 300px; height: 300px;
|
|
78
|
-
background: radial-gradient(circle, #00c6ff, transparent 70%);
|
|
79
|
-
bottom: 15%; left: 40%;
|
|
80
|
-
animation-delay: -5s;
|
|
81
|
-
animation-duration: 12s;
|
|
122
|
+
background: var(--a1);
|
|
123
|
+
box-shadow: 0 0 12px 2px var(--a2);
|
|
82
124
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
125
|
+
.orbit i:nth-child(2) { top: auto; bottom: -4px; background: var(--a3); box-shadow: 0 0 12px 2px var(--a3); }
|
|
126
|
+
.orbit.o2 i { width: 6px; height: 6px; margin-left: -3px; top: -3px; }
|
|
127
|
+
.glow {
|
|
128
|
+
position: absolute;
|
|
129
|
+
inset: 26%;
|
|
130
|
+
border-radius: 50%;
|
|
131
|
+
background: radial-gradient(circle, rgba(91, 140, 255, 0.55), transparent 70%);
|
|
132
|
+
filter: blur(18px);
|
|
133
|
+
animation: breathe 3.2s ease-in-out infinite;
|
|
88
134
|
}
|
|
89
|
-
|
|
90
|
-
/* Content */
|
|
91
|
-
.container {
|
|
135
|
+
.logo {
|
|
92
136
|
position: relative;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
/* Animated node SVG */
|
|
105
|
-
.node-graphic {
|
|
106
|
-
width: 120px;
|
|
107
|
-
height: 120px;
|
|
108
|
-
margin: 0 auto 32px;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.node-ring {
|
|
112
|
-
fill: none;
|
|
113
|
-
stroke: url(#ringGrad);
|
|
114
|
-
stroke-width: 1.5;
|
|
115
|
-
stroke-dasharray: 8 6;
|
|
116
|
-
transform-origin: 60px 60px;
|
|
117
|
-
animation: spin 20s linear infinite;
|
|
118
|
-
}
|
|
119
|
-
.node-ring-2 {
|
|
120
|
-
animation: spin 15s linear infinite reverse;
|
|
121
|
-
stroke-dasharray: 12 8;
|
|
137
|
+
width: 34%;
|
|
138
|
+
aspect-ratio: 1;
|
|
139
|
+
border-radius: 26%;
|
|
140
|
+
display: grid;
|
|
141
|
+
place-items: center;
|
|
142
|
+
font-size: calc(var(--size) * 0.13);
|
|
143
|
+
font-weight: 750;
|
|
144
|
+
letter-spacing: -0.04em;
|
|
145
|
+
color: #fff;
|
|
146
|
+
background: linear-gradient(140deg, #9fbcff 0%, #5b8cff 45%, #3a5fd9 100%);
|
|
147
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35), 0 14px 40px -10px rgba(91, 140, 255, 0.9);
|
|
122
148
|
}
|
|
123
|
-
.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
.node-dot {
|
|
133
|
-
fill: #0072ff;
|
|
134
|
-
animation: pulse 2s ease-in-out infinite;
|
|
135
|
-
}
|
|
136
|
-
.node-dot:nth-child(2) { animation-delay: 0.3s; fill: #7b2ff7; }
|
|
137
|
-
.node-dot:nth-child(3) { animation-delay: 0.6s; fill: #00c6ff; }
|
|
138
|
-
.node-dot:nth-child(4) { animation-delay: 0.9s; fill: #0072ff; }
|
|
139
|
-
.node-dot:nth-child(5) { animation-delay: 1.2s; fill: #7b2ff7; }
|
|
140
|
-
.node-dot:nth-child(6) { animation-delay: 1.5s; fill: #00c6ff; }
|
|
141
|
-
|
|
142
|
-
@keyframes pulse {
|
|
143
|
-
0%, 100% { opacity: 0.5; r: 3; }
|
|
144
|
-
50% { opacity: 1; r: 5; }
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.node-line {
|
|
148
|
-
stroke: url(#lineGrad);
|
|
149
|
-
stroke-width: 0.8;
|
|
150
|
-
opacity: 0.4;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.node-center {
|
|
154
|
-
fill: url(#centerGrad);
|
|
155
|
-
filter: drop-shadow(0 0 8px rgba(0,114,255,0.4));
|
|
149
|
+
.ripple {
|
|
150
|
+
position: absolute;
|
|
151
|
+
inset: 33%;
|
|
152
|
+
border-radius: 50%;
|
|
153
|
+
border: 2px solid var(--ok);
|
|
154
|
+
pointer-events: none;
|
|
155
|
+
animation: ripple 1.6s cubic-bezier(0.2, 0, 0, 1) forwards;
|
|
156
156
|
}
|
|
157
|
+
.ripple.bad { border-color: var(--warn); }
|
|
157
158
|
|
|
158
159
|
h1 {
|
|
159
|
-
font-size:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
font-size: clamp(34px, 7.4vmin, 76px);
|
|
161
|
+
line-height: 1.04;
|
|
162
|
+
font-weight: 720;
|
|
163
|
+
letter-spacing: -0.045em;
|
|
164
|
+
}
|
|
165
|
+
h1 .w { display: inline-block; animation: word 0.9s cubic-bezier(0.2, 0, 0, 1) both; }
|
|
166
|
+
h1 .grad {
|
|
167
|
+
background: linear-gradient(100deg, var(--a1) 0%, var(--a2) 35%, var(--a3) 65%, var(--a1) 100%);
|
|
168
|
+
background-size: 220% auto;
|
|
164
169
|
-webkit-background-clip: text;
|
|
170
|
+
background-clip: text;
|
|
165
171
|
-webkit-text-fill-color: transparent;
|
|
172
|
+
animation: word 0.9s cubic-bezier(0.2, 0, 0, 1) both, shine 5s linear 1s infinite;
|
|
166
173
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
font-size:
|
|
170
|
-
color:
|
|
171
|
-
|
|
172
|
-
letter-spacing: 0.3px;
|
|
173
|
-
margin-bottom: 32px;
|
|
174
|
+
.sub {
|
|
175
|
+
margin-top: clamp(10px, 2vmin, 16px);
|
|
176
|
+
font-size: clamp(14px, 2.2vmin, 17px);
|
|
177
|
+
color: var(--muted);
|
|
178
|
+
animation: rise 0.9s cubic-bezier(0.2, 0, 0, 1) 0.55s both;
|
|
174
179
|
}
|
|
175
180
|
|
|
176
|
-
.
|
|
177
|
-
|
|
181
|
+
.row {
|
|
182
|
+
margin-top: clamp(20px, 4vmin, 32px);
|
|
183
|
+
display: flex;
|
|
184
|
+
flex-wrap: wrap;
|
|
178
185
|
align-items: center;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
background: rgba(16, 185, 129, 0.08);
|
|
183
|
-
border: 1px solid rgba(16, 185, 129, 0.2);
|
|
184
|
-
font-size: 13px;
|
|
185
|
-
color: #10b981;
|
|
186
|
-
font-weight: 500;
|
|
187
|
-
animation: fadeUp 1s ease-out 0.3s both;
|
|
186
|
+
justify-content: center;
|
|
187
|
+
gap: 10px;
|
|
188
|
+
animation: rise 0.9s cubic-bezier(0.2, 0, 0, 1) 0.75s both;
|
|
188
189
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
190
|
+
.pill {
|
|
191
|
+
display: inline-flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
gap: 10px;
|
|
194
|
+
height: 38px;
|
|
195
|
+
padding: 0 16px 0 14px;
|
|
196
|
+
border-radius: 999px;
|
|
197
|
+
border: 1px solid var(--border);
|
|
198
|
+
background: var(--surface);
|
|
199
|
+
backdrop-filter: blur(12px);
|
|
200
|
+
-webkit-backdrop-filter: blur(12px);
|
|
201
|
+
font-size: 13.5px;
|
|
202
|
+
font-weight: 550;
|
|
197
203
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
204
|
+
.dot { position: relative; width: 8px; height: 8px; border-radius: 50%; background: var(--faint); }
|
|
205
|
+
.ok .dot { background: var(--ok); }
|
|
206
|
+
.warn .dot { background: var(--warn); }
|
|
207
|
+
.ok .dot::after { content: ""; position: absolute; inset: -4px; border-radius: 50%; border: 2px solid var(--ok); animation: ping 2s cubic-bezier(0, 0, 0.2, 1) infinite; }
|
|
208
|
+
.ms { color: var(--muted); font-size: 12.5px; padding-left: 10px; border-left: 1px solid var(--border); font-variant-numeric: tabular-nums; }
|
|
209
|
+
.ms:empty { display: none; }
|
|
210
|
+
.link {
|
|
211
|
+
display: inline-flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
gap: 7px;
|
|
214
|
+
height: 38px;
|
|
215
|
+
padding: 0 15px;
|
|
216
|
+
border-radius: 999px;
|
|
217
|
+
border: 1px solid var(--border);
|
|
218
|
+
background: var(--surface);
|
|
219
|
+
backdrop-filter: blur(12px);
|
|
220
|
+
-webkit-backdrop-filter: blur(12px);
|
|
221
|
+
color: var(--text);
|
|
222
|
+
text-decoration: none;
|
|
223
|
+
font-size: 13.5px;
|
|
224
|
+
font-weight: 550;
|
|
225
|
+
transition: transform 0.15s, border-color 0.15s;
|
|
202
226
|
}
|
|
227
|
+
.link:hover { transform: translateY(-1px); border-color: var(--a2); }
|
|
228
|
+
.link:focus-visible { outline: 2px solid var(--a2); outline-offset: 2px; }
|
|
229
|
+
.link svg { width: 15px; height: 15px; color: var(--muted); }
|
|
203
230
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
bottom: 28px;
|
|
207
|
-
left: 0;
|
|
208
|
-
right: 0;
|
|
231
|
+
footer {
|
|
232
|
+
padding: 0 0 clamp(16px, 3vmin, 28px);
|
|
209
233
|
text-align: center;
|
|
210
234
|
font-size: 12px;
|
|
211
|
-
color:
|
|
212
|
-
|
|
213
|
-
animation: fadeUp 1s ease-out 0.6s both;
|
|
235
|
+
color: var(--faint);
|
|
236
|
+
animation: rise 0.9s cubic-bezier(0.2, 0, 0, 1) 1s both;
|
|
214
237
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
footer a { color: var(--muted); font-weight: 600; text-decoration: none; }
|
|
239
|
+
footer a:hover { color: var(--text); }
|
|
240
|
+
|
|
241
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
242
|
+
@keyframes breathe { 0%, 100% { opacity: 0.6; transform: scale(0.92); } 50% { opacity: 1; transform: scale(1.08); } }
|
|
243
|
+
@keyframes ripple { from { opacity: 0.85; transform: scale(1); } to { opacity: 0; transform: scale(3.4); } }
|
|
244
|
+
@keyframes ping { 0% { transform: scale(0.6); opacity: 0.8; } 80%, 100% { transform: scale(1.9); opacity: 0; } }
|
|
245
|
+
@keyframes pop { from { opacity: 0; transform: scale(0.6) rotate(-20deg); } to { opacity: 1; transform: none; } }
|
|
246
|
+
@keyframes word { from { opacity: 0; transform: translateY(0.45em); filter: blur(8px); } to { opacity: 1; transform: none; filter: blur(0); } }
|
|
247
|
+
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
|
|
248
|
+
@keyframes shine { to { background-position: 220% center; } }
|
|
249
|
+
@keyframes drift { to { transform: translate(5vmax, 4vmax) scale(1.12); } }
|
|
250
|
+
|
|
251
|
+
@media (max-height: 520px) {
|
|
252
|
+
.core { --size: 110px; margin-bottom: 14px; }
|
|
253
|
+
.row { margin-top: 14px; }
|
|
224
254
|
}
|
|
225
|
-
|
|
226
|
-
|
|
255
|
+
@media (prefers-reduced-motion: reduce) {
|
|
256
|
+
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
257
|
+
.ripple { display: none; }
|
|
227
258
|
}
|
|
228
259
|
</style>
|
|
229
260
|
</head>
|
|
230
261
|
<body>
|
|
262
|
+
<canvas id="net" aria-hidden="true"></canvas>
|
|
263
|
+
<div class="aurora" aria-hidden="true"><span></span><span></span><span></span></div>
|
|
264
|
+
<div class="vignette" aria-hidden="true"></div>
|
|
231
265
|
|
|
232
|
-
<div class="
|
|
233
|
-
<
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
266
|
+
<div class="stage">
|
|
267
|
+
<main class="center">
|
|
268
|
+
<div class="core" id="core" aria-hidden="true">
|
|
269
|
+
<div class="glow"></div>
|
|
270
|
+
<div class="track"></div>
|
|
271
|
+
<div class="track t2"></div>
|
|
272
|
+
<div class="ring"></div>
|
|
273
|
+
<div class="ring r2"></div>
|
|
274
|
+
<div class="orbit"><i></i><i></i></div>
|
|
275
|
+
<div class="orbit o2"><i></i></div>
|
|
276
|
+
<div class="logo">4b</div>
|
|
277
|
+
</div>
|
|
238
278
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
<linearGradient id="lineGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
247
|
-
<stop offset="0%" stop-color="#0072ff" stop-opacity="0.6" />
|
|
248
|
-
<stop offset="100%" stop-color="#7b2ff7" stop-opacity="0.2" />
|
|
249
|
-
</linearGradient>
|
|
250
|
-
<radialGradient id="centerGrad" cx="50%" cy="50%" r="50%">
|
|
251
|
-
<stop offset="0%" stop-color="#0072ff" />
|
|
252
|
-
<stop offset="100%" stop-color="#7b2ff7" />
|
|
253
|
-
</radialGradient>
|
|
254
|
-
</defs>
|
|
279
|
+
<h1>
|
|
280
|
+
<span class="w" style="animation-delay:.15s">Your</span>
|
|
281
|
+
<span class="w" style="animation-delay:.25s">app</span>
|
|
282
|
+
<span class="w" style="animation-delay:.35s">is</span>
|
|
283
|
+
<span class="w grad" style="animation-delay:.45s">running</span>
|
|
284
|
+
</h1>
|
|
285
|
+
<p class="sub">Live and ready to accept requests.</p>
|
|
255
286
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
287
|
+
<div class="row">
|
|
288
|
+
<span class="pill" id="pill" role="status" aria-live="polite">
|
|
289
|
+
<span class="dot"></span><span id="pill-text">Checking…</span><span class="ms mono" id="ms"></span>
|
|
290
|
+
</span>
|
|
291
|
+
<a class="link" href="/docs" data-probe="/docs" hidden>
|
|
292
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
|
|
293
|
+
API docs
|
|
294
|
+
</a>
|
|
295
|
+
<a class="link" href="/_dev/" data-probe="/_dev/" hidden>
|
|
296
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/></svg>
|
|
297
|
+
Dashboard
|
|
298
|
+
</a>
|
|
299
|
+
</div>
|
|
300
|
+
</main>
|
|
263
301
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
<circle class="node-ring node-ring-2" cx="60" cy="60" r="38" />
|
|
267
|
-
<circle class="node-ring node-ring-3" cx="60" cy="60" r="26" />
|
|
302
|
+
<footer>Powered by <a href="https://www.npmjs.com/package/4bnode" target="_blank" rel="noopener noreferrer">4bnode</a></footer>
|
|
303
|
+
</div>
|
|
268
304
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
<circle class="node-dot" cx="60" cy="106" r="4" />
|
|
274
|
-
<circle class="node-dot" cx="20" cy="83" r="4" />
|
|
275
|
-
<circle class="node-dot" cx="20" cy="37" r="4" />
|
|
305
|
+
<script>
|
|
306
|
+
(function () {
|
|
307
|
+
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
308
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
276
309
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
310
|
+
// ── Background: drifting node network, pulled toward the cursor ──
|
|
311
|
+
var canvas = $("net"), ctx = canvas.getContext("2d");
|
|
312
|
+
var nodes = [], W = 0, H = 0, DPR = Math.min(window.devicePixelRatio || 1, 2);
|
|
313
|
+
var mouse = { x: -9999, y: -9999 };
|
|
314
|
+
function netColor() { return getComputedStyle(document.documentElement).getPropertyValue("--net").trim() || "143,176,255"; }
|
|
315
|
+
var color = netColor();
|
|
316
|
+
function resize() {
|
|
317
|
+
W = window.innerWidth; H = window.innerHeight;
|
|
318
|
+
canvas.width = W * DPR; canvas.height = H * DPR;
|
|
319
|
+
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
320
|
+
var n = Math.round(Math.min(110, (W * H) / 13000));
|
|
321
|
+
nodes = [];
|
|
322
|
+
for (var i = 0; i < n; i++) {
|
|
323
|
+
nodes.push({ x: Math.random() * W, y: Math.random() * H, vx: (Math.random() - 0.5) * 0.3, vy: (Math.random() - 0.5) * 0.3, r: Math.random() * 1.4 + 0.6 });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function draw() {
|
|
327
|
+
ctx.clearRect(0, 0, W, H);
|
|
328
|
+
var max = 140;
|
|
329
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
330
|
+
var a = nodes[i];
|
|
331
|
+
if (!reduce) {
|
|
332
|
+
a.x += a.vx; a.y += a.vy;
|
|
333
|
+
if (a.x < -20) a.x = W + 20; else if (a.x > W + 20) a.x = -20;
|
|
334
|
+
if (a.y < -20) a.y = H + 20; else if (a.y > H + 20) a.y = -20;
|
|
335
|
+
var mx = mouse.x - a.x, my = mouse.y - a.y, md = Math.sqrt(mx * mx + my * my);
|
|
336
|
+
if (md < 190 && md > 1) { a.x += (mx / md) * 0.4; a.y += (my / md) * 0.4; }
|
|
337
|
+
}
|
|
338
|
+
for (var j = i + 1; j < nodes.length; j++) {
|
|
339
|
+
var b = nodes[j], dx = a.x - b.x, dy = a.y - b.y, d = dx * dx + dy * dy;
|
|
340
|
+
if (d < max * max) {
|
|
341
|
+
ctx.strokeStyle = "rgba(" + color + "," + (0.17 * (1 - Math.sqrt(d) / max)).toFixed(3) + ")";
|
|
342
|
+
ctx.lineWidth = 1;
|
|
343
|
+
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
var near = (mouse.x - a.x) * (mouse.x - a.x) + (mouse.y - a.y) * (mouse.y - a.y) < 190 * 190;
|
|
347
|
+
ctx.fillStyle = "rgba(" + color + "," + (near ? 0.95 : 0.45) + ")";
|
|
348
|
+
ctx.beginPath(); ctx.arc(a.x, a.y, near ? a.r + 0.9 : a.r, 0, Math.PI * 2); ctx.fill();
|
|
349
|
+
}
|
|
350
|
+
if (!reduce) requestAnimationFrame(draw);
|
|
351
|
+
}
|
|
352
|
+
resize(); draw();
|
|
353
|
+
window.addEventListener("resize", function () { resize(); if (reduce) draw(); });
|
|
354
|
+
window.addEventListener("pointermove", function (e) { mouse.x = e.clientX; mouse.y = e.clientY; }, { passive: true });
|
|
355
|
+
document.addEventListener("pointerleave", function () { mouse.x = mouse.y = -9999; });
|
|
356
|
+
window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", function () { color = netColor(); if (reduce) draw(); });
|
|
280
357
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
358
|
+
// ── Heartbeat: a ripple from the core on every real ping ──
|
|
359
|
+
var core = $("core");
|
|
360
|
+
function beat(ok) {
|
|
361
|
+
if (reduce) return;
|
|
362
|
+
var r = document.createElement("span");
|
|
363
|
+
r.className = "ripple" + (ok ? "" : " bad");
|
|
364
|
+
core.appendChild(r);
|
|
365
|
+
setTimeout(function () { r.remove(); }, 1700);
|
|
366
|
+
}
|
|
288
367
|
|
|
289
|
-
|
|
368
|
+
// ── Live status (HEAD to this page) ──
|
|
369
|
+
function check() {
|
|
370
|
+
if (document.visibilityState === "hidden") return;
|
|
371
|
+
var t0 = performance.now();
|
|
372
|
+
fetch(location.pathname, { method: "HEAD", cache: "no-store" })
|
|
373
|
+
.then(function (r) {
|
|
374
|
+
var ms = Math.max(1, Math.round(performance.now() - t0));
|
|
375
|
+
$("pill").className = "pill " + (r.ok ? "ok" : "warn");
|
|
376
|
+
$("pill-text").textContent = r.ok ? "All systems operational" : "Server answered " + r.status;
|
|
377
|
+
$("ms").textContent = r.ok ? ms + " ms" : "";
|
|
378
|
+
beat(r.ok);
|
|
379
|
+
})
|
|
380
|
+
.catch(function () {
|
|
381
|
+
$("pill").className = "pill warn";
|
|
382
|
+
$("pill-text").textContent = "Server unreachable";
|
|
383
|
+
$("ms").textContent = "";
|
|
384
|
+
beat(false);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
setTimeout(check, 900); // after the intro animation
|
|
388
|
+
setInterval(check, 3000);
|
|
389
|
+
document.addEventListener("visibilitychange", check);
|
|
290
390
|
|
|
391
|
+
// ── Links appear only if they answer (docs once set up; /_dev only in dev) ──
|
|
392
|
+
document.querySelectorAll("[data-probe]").forEach(function (el) {
|
|
393
|
+
fetch(el.getAttribute("data-probe"), { method: "HEAD", cache: "no-store" })
|
|
394
|
+
.then(function (r) { if (r.ok) el.hidden = false; })
|
|
395
|
+
.catch(function () {});
|
|
396
|
+
});
|
|
397
|
+
})();
|
|
398
|
+
</script>
|
|
291
399
|
</body>
|
|
292
400
|
</html>
|