@rubytech/create-maxy 1.0.781 → 1.0.782
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/package.json
CHANGED
|
@@ -38,11 +38,30 @@ const CONNECT_FIFO = args["connect-fifo"] || "/tmp/wifi-provision-connect";
|
|
|
38
38
|
const RESULT_FILE = args["result-file"] || "/tmp/wifi-provision-result";
|
|
39
39
|
const BRAND_JSON = args["brand-json"] || "";
|
|
40
40
|
const HOSTNAME = args.hostname || "maxy";
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const PORTAL_HOST = args["portal-host"] || "";
|
|
42
|
+
|
|
43
|
+
// ── Load brand config ─────────────────────────────────────────────────
|
|
44
|
+
// All visual surfaces (colour, font, logo) come from brand.json so the
|
|
45
|
+
// captive portal matches the rest of the product. Earlier this file
|
|
46
|
+
// hard-coded a generic dark navy + system font fallback that bore no
|
|
47
|
+
// relation to the brand's actual look — the operator-side complaint
|
|
48
|
+
// that surfaced this. Now: read the brand's `defaultColors`,
|
|
49
|
+
// `defaultFonts`, and `assets.logo`, fall through to neutral defaults
|
|
50
|
+
// only if a field is missing from brand.json.
|
|
43
51
|
let brandName = "Maxy";
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
const colors = {
|
|
53
|
+
primary: "#1a1a2e",
|
|
54
|
+
primaryHover: "#0f0f1f",
|
|
55
|
+
background: "#FAFAF8",
|
|
56
|
+
text: "#1a1a2e",
|
|
57
|
+
card: "rgba(0,0,0,0.04)",
|
|
58
|
+
cardBorder: "rgba(0,0,0,0.08)",
|
|
59
|
+
muted: "#6b6b6b",
|
|
60
|
+
};
|
|
61
|
+
const fonts = {
|
|
62
|
+
display: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
63
|
+
body: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
64
|
+
};
|
|
46
65
|
let logoBase64 = "";
|
|
47
66
|
|
|
48
67
|
try {
|
|
@@ -50,15 +69,29 @@ try {
|
|
|
50
69
|
const brand = JSON.parse(fs.readFileSync(BRAND_JSON, "utf-8"));
|
|
51
70
|
if (brand.productName) brandName = brand.productName;
|
|
52
71
|
|
|
53
|
-
|
|
72
|
+
if (brand.defaultColors) {
|
|
73
|
+
const c = brand.defaultColors;
|
|
74
|
+
if (c.primary) colors.primary = c.primary;
|
|
75
|
+
if (c.primaryHover) colors.primaryHover = c.primaryHover;
|
|
76
|
+
if (c.background) colors.background = c.background;
|
|
77
|
+
if (c.agentBubble) colors.card = c.agentBubble;
|
|
78
|
+
}
|
|
79
|
+
if (brand.defaultFonts) {
|
|
80
|
+
if (brand.defaultFonts.display) fonts.display = brand.defaultFonts.display;
|
|
81
|
+
if (brand.defaultFonts.body) fonts.body = brand.defaultFonts.body;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Load logo. Prefer assets.logo (the larger brand mark) and fall
|
|
85
|
+
// back to assets.icon.
|
|
54
86
|
const brandDir = path.dirname(BRAND_JSON);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
87
|
+
const logoFile = (brand.assets && (brand.assets.logo || brand.assets.icon)) || "";
|
|
88
|
+
if (logoFile) {
|
|
89
|
+
const logoPath = path.join(brandDir, "assets", logoFile);
|
|
90
|
+
if (fs.existsSync(logoPath)) {
|
|
91
|
+
const logoData = fs.readFileSync(logoPath);
|
|
92
|
+
const ext = path.extname(logoPath).slice(1);
|
|
60
93
|
const mime = ext === "png" ? "image/png" : ext === "ico" ? "image/x-icon" : "image/" + ext;
|
|
61
|
-
logoBase64 = `data:${mime};base64,${
|
|
94
|
+
logoBase64 = `data:${mime};base64,${logoData.toString("base64")}`;
|
|
62
95
|
}
|
|
63
96
|
}
|
|
64
97
|
}
|
|
@@ -95,25 +128,29 @@ function portalHTML() {
|
|
|
95
128
|
<style>
|
|
96
129
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
97
130
|
body {
|
|
98
|
-
font-family:
|
|
99
|
-
background: ${
|
|
100
|
-
color:
|
|
131
|
+
font-family: ${fonts.body};
|
|
132
|
+
background: ${colors.background};
|
|
133
|
+
color: ${colors.text};
|
|
101
134
|
min-height: 100vh;
|
|
102
135
|
display: flex;
|
|
103
136
|
flex-direction: column;
|
|
104
137
|
align-items: center;
|
|
105
138
|
padding: 24px 16px;
|
|
106
139
|
}
|
|
107
|
-
.logo { width:
|
|
140
|
+
.logo { width: 56px; height: 56px; margin-bottom: 12px; border-radius: 12px; }
|
|
108
141
|
.logo-text {
|
|
109
|
-
font-
|
|
110
|
-
|
|
142
|
+
font-family: ${fonts.display};
|
|
143
|
+
font-size: 28px; font-weight: 600; margin-bottom: 8px;
|
|
144
|
+
color: ${colors.primary};
|
|
145
|
+
}
|
|
146
|
+
h1 {
|
|
147
|
+
font-family: ${fonts.display};
|
|
148
|
+
font-size: 22px; font-weight: 500; margin-bottom: 4px;
|
|
111
149
|
}
|
|
112
|
-
|
|
113
|
-
.subtitle { font-size: 14px; color: #999; margin-bottom: 24px; }
|
|
150
|
+
.subtitle { font-size: 14px; color: ${colors.muted}; margin-bottom: 24px; }
|
|
114
151
|
.card {
|
|
115
|
-
background:
|
|
116
|
-
border: 1px solid
|
|
152
|
+
background: ${colors.card};
|
|
153
|
+
border: 1px solid ${colors.cardBorder};
|
|
117
154
|
border-radius: 12px;
|
|
118
155
|
width: 100%; max-width: 360px;
|
|
119
156
|
padding: 16px;
|
|
@@ -128,17 +165,17 @@ function portalHTML() {
|
|
|
128
165
|
transition: background 0.15s;
|
|
129
166
|
}
|
|
130
167
|
.network-item:hover, .network-item.selected {
|
|
131
|
-
background:
|
|
168
|
+
background: ${colors.cardBorder};
|
|
132
169
|
}
|
|
133
|
-
.network-item.selected { border: 1px solid ${
|
|
170
|
+
.network-item.selected { border: 1px solid ${colors.primary}; }
|
|
134
171
|
.network-name { font-size: 15px; font-weight: 500; }
|
|
135
|
-
.network-meta { font-size: 12px; color:
|
|
172
|
+
.network-meta { font-size: 12px; color: ${colors.muted}; }
|
|
136
173
|
.signal-bars { display: flex; gap: 2px; align-items: flex-end; height: 16px; }
|
|
137
174
|
.signal-bar {
|
|
138
175
|
width: 4px; border-radius: 1px;
|
|
139
|
-
background:
|
|
176
|
+
background: ${colors.cardBorder};
|
|
140
177
|
}
|
|
141
|
-
.signal-bar.active { background: ${
|
|
178
|
+
.signal-bar.active { background: ${colors.primary}; }
|
|
142
179
|
.password-group {
|
|
143
180
|
position: relative;
|
|
144
181
|
margin-top: 16px;
|
|
@@ -146,34 +183,35 @@ function portalHTML() {
|
|
|
146
183
|
.password-group input {
|
|
147
184
|
width: 100%;
|
|
148
185
|
padding: 12px 44px 12px 12px;
|
|
149
|
-
background:
|
|
150
|
-
border: 1px solid
|
|
186
|
+
background: ${colors.background};
|
|
187
|
+
border: 1px solid ${colors.cardBorder};
|
|
151
188
|
border-radius: 8px;
|
|
152
|
-
color:
|
|
189
|
+
color: ${colors.text};
|
|
153
190
|
font-size: 15px;
|
|
154
191
|
outline: none;
|
|
155
192
|
}
|
|
156
|
-
.password-group input:focus { border-color: ${
|
|
193
|
+
.password-group input:focus { border-color: ${colors.primary}; }
|
|
157
194
|
.toggle-pw {
|
|
158
195
|
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
|
|
159
|
-
background: none; border: none; color:
|
|
196
|
+
background: none; border: none; color: ${colors.muted}; cursor: pointer;
|
|
160
197
|
font-size: 13px; padding: 4px 8px;
|
|
161
198
|
}
|
|
162
199
|
.btn {
|
|
163
200
|
width: 100%; padding: 14px;
|
|
164
|
-
background: ${
|
|
201
|
+
background: ${colors.primary};
|
|
165
202
|
color: #fff; font-size: 16px; font-weight: 600;
|
|
166
203
|
border: none; border-radius: 8px;
|
|
167
204
|
cursor: pointer; margin-top: 16px;
|
|
168
|
-
transition:
|
|
205
|
+
transition: background 0.15s;
|
|
169
206
|
min-height: 48px;
|
|
170
207
|
}
|
|
171
|
-
.btn:disabled {
|
|
208
|
+
.btn:hover:not(:disabled) { background: ${colors.primaryHover}; }
|
|
209
|
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
172
210
|
.error {
|
|
173
|
-
color: #
|
|
211
|
+
color: #c0392b; font-size: 13px;
|
|
174
212
|
margin-top: 8px; display: none;
|
|
175
213
|
}
|
|
176
|
-
.empty { text-align: center; padding: 24px; color:
|
|
214
|
+
.empty { text-align: center; padding: 24px; color: ${colors.muted}; font-size: 14px; }
|
|
177
215
|
|
|
178
216
|
/* Progress / success screens */
|
|
179
217
|
.screen { display: none; width: 100%; max-width: 360px; text-align: center; }
|
|
@@ -181,28 +219,30 @@ function portalHTML() {
|
|
|
181
219
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
182
220
|
.spinner {
|
|
183
221
|
width: 40px; height: 40px;
|
|
184
|
-
border: 3px solid
|
|
185
|
-
border-top-color: ${
|
|
222
|
+
border: 3px solid ${colors.cardBorder};
|
|
223
|
+
border-top-color: ${colors.primary};
|
|
186
224
|
border-radius: 50%;
|
|
187
225
|
animation: spin 0.8s linear infinite;
|
|
188
226
|
margin: 24px auto;
|
|
189
227
|
}
|
|
190
228
|
.success-icon {
|
|
191
229
|
width: 48px; height: 48px; margin: 24px auto;
|
|
192
|
-
border-radius: 50%; background:
|
|
230
|
+
border-radius: 50%; background: ${colors.primary};
|
|
193
231
|
display: flex; align-items: center; justify-content: center;
|
|
194
232
|
font-size: 24px; color: #fff;
|
|
195
233
|
}
|
|
196
234
|
.address-box {
|
|
197
|
-
background:
|
|
198
|
-
border: 1px solid
|
|
235
|
+
background: ${colors.card};
|
|
236
|
+
border: 1px solid ${colors.cardBorder};
|
|
199
237
|
border-radius: 8px;
|
|
200
238
|
padding: 12px 16px;
|
|
201
239
|
margin: 16px auto;
|
|
202
240
|
display: inline-block;
|
|
241
|
+
max-width: 100%;
|
|
242
|
+
word-break: break-all;
|
|
203
243
|
}
|
|
204
244
|
.address-box a {
|
|
205
|
-
color: ${
|
|
245
|
+
color: ${colors.primary};
|
|
206
246
|
text-decoration: none;
|
|
207
247
|
font-size: 16px;
|
|
208
248
|
font-weight: 600;
|
|
@@ -245,7 +285,7 @@ function portalHTML() {
|
|
|
245
285
|
<div class="card" style="padding:32px 16px">
|
|
246
286
|
<div class="success-icon">✓</div>
|
|
247
287
|
<h1>Connected!</h1>
|
|
248
|
-
<p class="hint">${escapedBrandName} is now online. Redirecting in <span id="redirect-countdown">
|
|
288
|
+
<p class="hint">${escapedBrandName} is now online. Redirecting in <span id="redirect-countdown">20</span>s…</p>
|
|
249
289
|
<div class="address-box">
|
|
250
290
|
<a id="device-link" href="#" target="_blank"></a>
|
|
251
291
|
</div>
|
|
@@ -370,10 +410,15 @@ function portalHTML() {
|
|
|
370
410
|
? "http://" + data.hostname + ".local" + devicePort
|
|
371
411
|
: null;
|
|
372
412
|
var ipAddr = data.ip ? "http://" + data.ip + devicePort : null;
|
|
373
|
-
|
|
413
|
+
// Display: prefer the friendly .local URL.
|
|
414
|
+
// Auto-redirect target: the raw IP. On Android (notably Brave) the
|
|
415
|
+
// mDNS .local lookup fails and the auto-navigate would dead-end;
|
|
416
|
+
// the IP works on every platform.
|
|
417
|
+
var displayAddr = hostnameAddr || ipAddr;
|
|
418
|
+
var redirectAddr = ipAddr || hostnameAddr;
|
|
374
419
|
var link = document.getElementById("device-link");
|
|
375
|
-
link.href =
|
|
376
|
-
link.textContent =
|
|
420
|
+
link.href = displayAddr;
|
|
421
|
+
link.textContent = displayAddr;
|
|
377
422
|
if (hostnameAddr && ipAddr && hostnameAddr !== ipAddr) {
|
|
378
423
|
var ipLink = document.getElementById("device-link-ip");
|
|
379
424
|
ipLink.href = ipAddr;
|
|
@@ -382,16 +427,20 @@ function portalHTML() {
|
|
|
382
427
|
document.getElementById("ip-fallback-box").style.display = "block";
|
|
383
428
|
}
|
|
384
429
|
showScreen("success-screen");
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
430
|
+
// Countdown timing: the Pi keeps the AP up for ~10s after writing the
|
|
431
|
+
// success result (so this poll can land), then tears it down. The
|
|
432
|
+
// phone then needs ~5s to drop the AP SSID and auto-rejoin its home
|
|
433
|
+
// WiFi. A 20s countdown crosses both gates so the redirect lands
|
|
434
|
+
// when the phone is back on its home network and the device is
|
|
435
|
+
// routable on its new IP.
|
|
436
|
+
var remaining = 20;
|
|
388
437
|
var countdownEl = document.getElementById("redirect-countdown");
|
|
389
438
|
var ticker = setInterval(function() {
|
|
390
439
|
remaining -= 1;
|
|
391
440
|
if (countdownEl) countdownEl.textContent = String(remaining);
|
|
392
441
|
if (remaining <= 0) {
|
|
393
442
|
clearInterval(ticker);
|
|
394
|
-
window.location.href =
|
|
443
|
+
window.location.href = redirectAddr;
|
|
395
444
|
}
|
|
396
445
|
}, 1000);
|
|
397
446
|
}
|
|
@@ -500,7 +549,7 @@ function handleRequest(req, res) {
|
|
|
500
549
|
|
|
501
550
|
// Captive portal detection — iOS
|
|
502
551
|
if (pathname === "/hotspot-detect.html" || pathname === "/library/test/success.html") {
|
|
503
|
-
res.writeHead(302, { Location: `http://${AP_IP}/` });
|
|
552
|
+
res.writeHead(302, { Location: `http://${PORTAL_HOST || AP_IP}/` });
|
|
504
553
|
res.end();
|
|
505
554
|
return;
|
|
506
555
|
}
|
|
@@ -508,14 +557,14 @@ function handleRequest(req, res) {
|
|
|
508
557
|
// Captive portal detection — Android
|
|
509
558
|
if (pathname === "/generate_204" || pathname === "/gen_204" ||
|
|
510
559
|
pathname === "/connecttest.txt" || pathname === "/ncsi.txt") {
|
|
511
|
-
res.writeHead(302, { Location: `http://${AP_IP}/` });
|
|
560
|
+
res.writeHead(302, { Location: `http://${PORTAL_HOST || AP_IP}/` });
|
|
512
561
|
res.end();
|
|
513
562
|
return;
|
|
514
563
|
}
|
|
515
564
|
|
|
516
565
|
// Captive portal detection — Windows
|
|
517
566
|
if (pathname === "/redirect" || pathname === "/fwlink/") {
|
|
518
|
-
res.writeHead(302, { Location: `http://${AP_IP}/` });
|
|
567
|
+
res.writeHead(302, { Location: `http://${PORTAL_HOST || AP_IP}/` });
|
|
519
568
|
res.end();
|
|
520
569
|
return;
|
|
521
570
|
}
|
|
@@ -645,7 +694,17 @@ function handleRequest(req, res) {
|
|
|
645
694
|
return;
|
|
646
695
|
}
|
|
647
696
|
|
|
648
|
-
// Default: serve portal page
|
|
697
|
+
// Default: serve portal page. If the client hit the raw AP IP (e.g.
|
|
698
|
+
// 192.168.4.1) and we have a friendly portal host configured, redirect
|
|
699
|
+
// so the URL bar shows the brand-aware name (e.g. http://maxy.setup/).
|
|
700
|
+
// dnsmasq's wildcard DNS resolves PORTAL_HOST → AP_IP transparently
|
|
701
|
+
// while the AP is up.
|
|
702
|
+
const incomingHost = (req.headers.host || "").split(":")[0];
|
|
703
|
+
if (PORTAL_HOST && incomingHost === AP_IP) {
|
|
704
|
+
res.writeHead(302, { Location: `http://${PORTAL_HOST}/` });
|
|
705
|
+
res.end();
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
649
708
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
650
709
|
res.end(portalHTML());
|
|
651
710
|
}
|
|
@@ -61,8 +61,14 @@ LOG_FILE="${LOG_DIR}/wifi-provision.log"
|
|
|
61
61
|
mkdir -p "$LOG_DIR"
|
|
62
62
|
|
|
63
63
|
# ── Derived constants ────────────────────────────────────────────────
|
|
64
|
-
# SSID:
|
|
65
|
-
|
|
64
|
+
# SSID: lowercase the product name and replace spaces with hyphens — gives
|
|
65
|
+
# `maxy`, `real-agent`, etc. (kebab-case, no "-Setup" suffix). The user's
|
|
66
|
+
# phone-side WiFi list is the brand surface; an extra "-Setup" suffix is
|
|
67
|
+
# operator jargon, not customer language.
|
|
68
|
+
SSID="$(echo "$PRODUCT_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')"
|
|
69
|
+
# Captive-portal hostname served by dnsmasq's wildcard — gives the phone
|
|
70
|
+
# a friendly URL bar (`http://maxy.setup/`) instead of `http://192.168.4.1/`.
|
|
71
|
+
PORTAL_HOST="${SSID}.setup"
|
|
66
72
|
AP_IP="192.168.4.1"
|
|
67
73
|
AP_SUBNET="192.168.4.0/24"
|
|
68
74
|
DHCP_RANGE_START="192.168.4.2"
|
|
@@ -283,6 +289,7 @@ DNSMASQ_EOF
|
|
|
283
289
|
--result-file "$RESULT_FILE" \
|
|
284
290
|
--brand-json "$BRAND_JSON" \
|
|
285
291
|
--hostname "$HOSTNAME_NAME" \
|
|
292
|
+
--portal-host "$PORTAL_HOST" \
|
|
286
293
|
>> "$LOG_FILE" 2>&1 &
|
|
287
294
|
HTTP_SERVER_PID=$!
|
|
288
295
|
sleep 1
|