@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.781",
3
+ "version": "1.0.782",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -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
- // ── Load brand config ────────────────────────────────────────────���──
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
- let brandColor = "#1a1a2e";
45
- let brandAccent = "#4a90d9";
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
- // Load logo if available (brand assets are alongside brand.json)
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
- if (brand.assets && brand.assets.icon) {
56
- const iconPath = path.join(brandDir, "assets", brand.assets.icon);
57
- if (fs.existsSync(iconPath)) {
58
- const iconData = fs.readFileSync(iconPath);
59
- const ext = path.extname(iconPath).slice(1);
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,${iconData.toString("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: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
99
- background: ${brandColor};
100
- color: #e8e8e8;
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: 48px; height: 48px; margin-bottom: 8px; border-radius: 12px; }
140
+ .logo { width: 56px; height: 56px; margin-bottom: 12px; border-radius: 12px; }
108
141
  .logo-text {
109
- font-size: 24px; font-weight: 700; margin-bottom: 8px;
110
- color: ${brandAccent};
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
- h1 { font-size: 20px; font-weight: 600; margin-bottom: 4px; }
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: rgba(255,255,255,0.06);
116
- border: 1px solid rgba(255,255,255,0.1);
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: rgba(255,255,255,0.1);
168
+ background: ${colors.cardBorder};
132
169
  }
133
- .network-item.selected { border: 1px solid ${brandAccent}; }
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: #888; }
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: rgba(255,255,255,0.2);
176
+ background: ${colors.cardBorder};
140
177
  }
141
- .signal-bar.active { background: ${brandAccent}; }
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: rgba(255,255,255,0.08);
150
- border: 1px solid rgba(255,255,255,0.15);
186
+ background: ${colors.background};
187
+ border: 1px solid ${colors.cardBorder};
151
188
  border-radius: 8px;
152
- color: #e8e8e8;
189
+ color: ${colors.text};
153
190
  font-size: 15px;
154
191
  outline: none;
155
192
  }
156
- .password-group input:focus { border-color: ${brandAccent}; }
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: #888; cursor: pointer;
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: ${brandAccent};
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: opacity 0.15s;
205
+ transition: background 0.15s;
169
206
  min-height: 48px;
170
207
  }
171
- .btn:disabled { opacity: 0.5; cursor: not-allowed; }
208
+ .btn:hover:not(:disabled) { background: ${colors.primaryHover}; }
209
+ .btn:disabled { opacity: 0.4; cursor: not-allowed; }
172
210
  .error {
173
- color: #ff6b6b; font-size: 13px;
211
+ color: #c0392b; font-size: 13px;
174
212
  margin-top: 8px; display: none;
175
213
  }
176
- .empty { text-align: center; padding: 24px; color: #888; font-size: 14px; }
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 rgba(255,255,255,0.15);
185
- border-top-color: ${brandAccent};
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: #2ecc71;
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: rgba(255,255,255,0.08);
198
- border: 1px solid rgba(255,255,255,0.15);
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: ${brandAccent};
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">&#10003;</div>
247
287
  <h1>Connected!</h1>
248
- <p class="hint">${escapedBrandName} is now online. Redirecting in <span id="redirect-countdown">5</span>s…</p>
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
- var primary = hostnameAddr || ipAddr;
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 = primary;
376
- link.textContent = primary;
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
- // Auto-redirect after 5s gives the phone time to drop the AP and
386
- // rejoin home WiFi before navigating to the .local / IP address.
387
- var remaining = 5;
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 = primary;
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: remove spaces from product name, append -Setup
65
- SSID="${PRODUCT_NAME// /}-Setup"
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