@rubytech/create-maxy 1.0.777 → 1.0.779
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
|
@@ -125,7 +125,7 @@ If the initial Cloudflare login fails during setup, {{productName}} will fall ba
|
|
|
125
125
|
|
|
126
126
|
Task 795 — `maxy-edge.service` (always-on front door) classifies upstream errors and serves a brand-aware response. There are two distinct user-visible shapes; the right one depends on what failed.
|
|
127
127
|
|
|
128
|
-
**Branded holding page (
|
|
128
|
+
**Branded holding page (brand logo + "Starting") for ~10 s during an upgrade — this is expected and self-healing.** The edge process binds the public port immediately, but `maxy.service` (the upstream UI) takes ~10 s after restart to apply the neo4j schema and mount its 11 routes. Any browser navigation that lands during that window gets a self-contained HTML holding page that polls `/api/health` and reloads automatically once the upstream binds. The page renders the brand logo (inlined as a base64 data URI at edge boot from `<install>/server/public/brand/<assets.logo>`) and the brand display/body fonts (loaded from fonts.googleapis.com) — both paths bypass the unavailable upstream so the page never makes a same-origin asset fetch. When `brand.logoContainsName` is true the logo replaces the productName text; otherwise the page falls back to "{{productName}} is starting". No operator action required. The diagnostic line in `~/.maxy/logs/edge.log` is `[edge] upstream http error path=… err=connect ECONNREFUSED 127.0.0.1:<UPSTREAM_PORT> err-class=econnrefused-coldstart upstream=…` and disappears as soon as upstream binds. Boot-time confirmation that the logo resolved: `[edge] brand=<name> holding-logo=inlined assets-dir=<path>` — `holding-logo=missing` means the logo file wasn't found at `assets-dir`, the page degrades to text-only.
|
|
129
129
|
|
|
130
130
|
**Branded plain-text 502 ("Bad Gateway ({{productName}} unavailable)") — real upstream failure, not cold-start.** Any error class other than `ECONNREFUSED` (timeouts, resets, host-unreachable) returns the existing 502 path. The diagnostic line carries `err-class=other`. Read the log with `tail -200 ~/.maxy/logs/edge.log | rg 'err-class=other'` and check `~/.maxy/logs/server.log` for upstream stack traces — the upstream itself is the source.
|
|
131
131
|
|
|
@@ -332,10 +332,28 @@ handle_connect_requests() {
|
|
|
332
332
|
nmcli device set wlan0 managed yes 2>/dev/null
|
|
333
333
|
sleep 2
|
|
334
334
|
|
|
335
|
+
# NM's wifi-list cache is empty immediately after wlan0 is handed back
|
|
336
|
+
# from hostapd, so `nmcli device wifi connect` reports "No network with
|
|
337
|
+
# SSID 'X' found" even when hostapd's pre-AP scan saw it. Trigger a
|
|
338
|
+
# rescan and poll until the target SSID appears in the cache (or 15s
|
|
339
|
+
# timeout) before attempting the connect.
|
|
340
|
+
nmcli device wifi rescan 2>/dev/null || true
|
|
341
|
+
local rescan_elapsed=0
|
|
342
|
+
while [ "$rescan_elapsed" -lt 15 ]; do
|
|
343
|
+
if nmcli -t -f SSID device wifi list 2>/dev/null | grep -Fxq "$target_ssid"; then
|
|
344
|
+
break
|
|
345
|
+
fi
|
|
346
|
+
sleep 1
|
|
347
|
+
rescan_elapsed=$((rescan_elapsed + 1))
|
|
348
|
+
done
|
|
349
|
+
log "rescan-before-connect: target=\"${target_ssid}\" elapsed=${rescan_elapsed}s"
|
|
350
|
+
|
|
335
351
|
# Attempt WiFi connection. Capture exit code before || true so we
|
|
336
352
|
# get nmcli's actual exit status, not the unconditional 0 from || true.
|
|
337
353
|
local connect_output connect_exit
|
|
338
|
-
|
|
354
|
+
# `--wait` / `-w` is a top-level nmcli option (must precede the
|
|
355
|
+
# subcommand), not an argument to `device wifi connect`.
|
|
356
|
+
connect_output=$(nmcli --wait 30 device wifi connect "$target_ssid" password "$target_password" 2>&1)
|
|
339
357
|
connect_exit=$?
|
|
340
358
|
|
|
341
359
|
if [ $connect_exit -eq 0 ] && wifi_is_connected; then
|
|
@@ -24,7 +24,7 @@ import { createServer, request as httpRequest } from "http";
|
|
|
24
24
|
import { createConnection as createConnection2 } from "net";
|
|
25
25
|
import { readFileSync as readFileSync4, existsSync as existsSync5, watchFile } from "fs";
|
|
26
26
|
import { homedir } from "os";
|
|
27
|
-
import { join as
|
|
27
|
+
import { join as join4 } from "path";
|
|
28
28
|
|
|
29
29
|
// server/ws-proxy.ts
|
|
30
30
|
import { createConnection } from "net";
|
|
@@ -668,14 +668,50 @@ function createEdgeAdminApp(opts) {
|
|
|
668
668
|
|
|
669
669
|
// server/edge-fallback.ts
|
|
670
670
|
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
671
|
+
import { extname, join as join3 } from "path";
|
|
672
|
+
var BRAND_DEFAULTS = {
|
|
673
|
+
configDir: ".maxy",
|
|
674
|
+
productName: "Maxy",
|
|
675
|
+
background: "#FAFAF8",
|
|
676
|
+
textColor: "#2A2A2A",
|
|
677
|
+
displayFont: "'Cormorant', Georgia, serif",
|
|
678
|
+
bodyFont: "'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif",
|
|
679
|
+
logoContainsName: false,
|
|
680
|
+
logoDataUri: null
|
|
681
|
+
};
|
|
682
|
+
var MIME_BY_EXT = {
|
|
683
|
+
".png": "image/png",
|
|
684
|
+
".jpg": "image/jpeg",
|
|
685
|
+
".jpeg": "image/jpeg",
|
|
686
|
+
".svg": "image/svg+xml",
|
|
687
|
+
".webp": "image/webp",
|
|
688
|
+
".gif": "image/gif"
|
|
689
|
+
};
|
|
690
|
+
function inlineAsset(filePath) {
|
|
691
|
+
if (!existsSync4(filePath)) return null;
|
|
692
|
+
const mime = MIME_BY_EXT[extname(filePath).toLowerCase()];
|
|
693
|
+
if (!mime) return null;
|
|
694
|
+
const bytes = readFileSync3(filePath);
|
|
695
|
+
return `data:${mime};base64,${bytes.toString("base64")}`;
|
|
696
|
+
}
|
|
697
|
+
function readString(value, fallback) {
|
|
698
|
+
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
699
|
+
}
|
|
700
|
+
function loadBrand(brandJsonPath2, assetsDir = "") {
|
|
701
|
+
if (!brandJsonPath2 || !existsSync4(brandJsonPath2)) return { ...BRAND_DEFAULTS };
|
|
674
702
|
try {
|
|
675
|
-
const parsed = JSON.parse(readFileSync3(
|
|
703
|
+
const parsed = JSON.parse(readFileSync3(brandJsonPath2, "utf-8"));
|
|
704
|
+
const logoFile = typeof parsed.assets?.logo === "string" ? parsed.assets.logo : null;
|
|
705
|
+
const logoDataUri = logoFile && assetsDir ? inlineAsset(join3(assetsDir, logoFile)) : null;
|
|
676
706
|
return {
|
|
677
|
-
configDir:
|
|
678
|
-
productName:
|
|
707
|
+
configDir: readString(parsed.configDir, BRAND_DEFAULTS.configDir),
|
|
708
|
+
productName: readString(parsed.productName, BRAND_DEFAULTS.productName),
|
|
709
|
+
background: readString(parsed.defaultColors?.background, BRAND_DEFAULTS.background),
|
|
710
|
+
textColor: BRAND_DEFAULTS.textColor,
|
|
711
|
+
displayFont: readString(parsed.defaultFonts?.display, BRAND_DEFAULTS.displayFont),
|
|
712
|
+
bodyFont: readString(parsed.defaultFonts?.body, BRAND_DEFAULTS.bodyFont),
|
|
713
|
+
logoContainsName: parsed.logoContainsName === true,
|
|
714
|
+
logoDataUri
|
|
679
715
|
};
|
|
680
716
|
} catch (err) {
|
|
681
717
|
console.error(`[edge] brand.json parse error: ${err.message}`);
|
|
@@ -689,40 +725,59 @@ var HTML_ESCAPES = { "<": "<", ">": ">", "&": "&", '"': """, "'":
|
|
|
689
725
|
function escapeHtml(s) {
|
|
690
726
|
return String(s).replace(/[<>&"']/g, (c) => HTML_ESCAPES[c] ?? c);
|
|
691
727
|
}
|
|
692
|
-
function
|
|
693
|
-
const
|
|
728
|
+
function firstFontFamily(stack) {
|
|
729
|
+
const match = stack.match(/^\s*['"]([^'"]+)['"]/);
|
|
730
|
+
return match ? match[1] : null;
|
|
731
|
+
}
|
|
732
|
+
function googleFontsHref(displayFont, bodyFont) {
|
|
733
|
+
const families = [firstFontFamily(displayFont), firstFontFamily(bodyFont)].filter((f) => f !== null);
|
|
734
|
+
if (families.length === 0) return null;
|
|
735
|
+
const params = families.map((name) => `family=${encodeURIComponent(name)}:wght@400;500;600`).join("&");
|
|
736
|
+
return `https://fonts.googleapis.com/css2?${params}&display=swap`;
|
|
737
|
+
}
|
|
738
|
+
function buildHoldingPage(brand) {
|
|
739
|
+
const safeName = escapeHtml(brand.productName);
|
|
740
|
+
const fontsHref = googleFontsHref(brand.displayFont, brand.bodyFont);
|
|
741
|
+
const fontsLink = fontsHref ? `<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link rel="stylesheet" href="${escapeHtml(fontsHref)}">` : "";
|
|
742
|
+
const logoBlock = brand.logoDataUri ? `<img class="logo" src="${brand.logoDataUri}" alt="${safeName}">` : "";
|
|
743
|
+
const headline = brand.logoContainsName && brand.logoDataUri ? '<p class="sub">Starting</p>' : `<p class="name">${safeName} is starting</p>`;
|
|
694
744
|
return `<!doctype html>
|
|
695
745
|
<html lang="en">
|
|
696
746
|
<head>
|
|
697
747
|
<meta charset="utf-8">
|
|
698
748
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
699
749
|
<title>${safeName}</title>
|
|
750
|
+
${fontsLink}
|
|
700
751
|
<style>
|
|
701
752
|
html, body { margin: 0; padding: 0; height: 100%; }
|
|
702
753
|
body {
|
|
703
|
-
font-family:
|
|
704
|
-
background:
|
|
705
|
-
color:
|
|
754
|
+
font-family: ${brand.bodyFont};
|
|
755
|
+
background: ${brand.background};
|
|
756
|
+
color: ${brand.textColor};
|
|
706
757
|
display: flex;
|
|
707
758
|
align-items: center;
|
|
708
759
|
justify-content: center;
|
|
709
760
|
flex-direction: column;
|
|
761
|
+
gap: 1.25rem;
|
|
710
762
|
}
|
|
711
|
-
.
|
|
712
|
-
.
|
|
763
|
+
.logo { display: block; max-width: 240px; max-height: 120px; width: auto; height: auto; }
|
|
764
|
+
.name { font-family: ${brand.displayFont}; font-size: 2rem; font-weight: 500; margin: 0; letter-spacing: -0.01em; }
|
|
765
|
+
.sub { font-family: ${brand.bodyFont}; font-size: 0.95rem; color: ${brand.textColor}; opacity: 0.55; margin: 0; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
766
|
+
.dots { display: flex; gap: 6px; align-items: center; }
|
|
713
767
|
.dot {
|
|
714
768
|
display: inline-block; width: 6px; height: 6px; border-radius: 50%;
|
|
715
|
-
background: currentColor;
|
|
769
|
+
background: currentColor; opacity: 0.3;
|
|
716
770
|
animation: pulse 1.4s infinite ease-in-out;
|
|
717
771
|
}
|
|
718
772
|
.dot:nth-child(2) { animation-delay: 0.2s; }
|
|
719
773
|
.dot:nth-child(3) { animation-delay: 0.4s; }
|
|
720
|
-
@keyframes pulse { 0%, 80%, 100% { opacity: 0.
|
|
774
|
+
@keyframes pulse { 0%, 80%, 100% { opacity: 0.25; } 40% { opacity: 0.85; } }
|
|
721
775
|
</style>
|
|
722
776
|
</head>
|
|
723
777
|
<body>
|
|
724
|
-
|
|
725
|
-
|
|
778
|
+
${logoBlock}
|
|
779
|
+
${headline}
|
|
780
|
+
<p class="dots"><span class="dot"></span><span class="dot"></span><span class="dot"></span></p>
|
|
726
781
|
<script>
|
|
727
782
|
(function () {
|
|
728
783
|
var attempts = 0;
|
|
@@ -743,9 +798,10 @@ function buildHoldingPage(productName) {
|
|
|
743
798
|
|
|
744
799
|
// server/edge.ts
|
|
745
800
|
var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT || "";
|
|
746
|
-
var BRAND_JSON_PATH = PLATFORM_ROOT2 ?
|
|
747
|
-
var
|
|
748
|
-
var
|
|
801
|
+
var BRAND_JSON_PATH = PLATFORM_ROOT2 ? join4(PLATFORM_ROOT2, "config", "brand.json") : "";
|
|
802
|
+
var BRAND_ASSETS_DIR = PLATFORM_ROOT2 ? join4(PLATFORM_ROOT2, "..", "server", "public", "brand") : "";
|
|
803
|
+
var BRAND = loadBrand(BRAND_JSON_PATH, BRAND_ASSETS_DIR);
|
|
804
|
+
var ALIAS_DOMAINS_PATH = join4(homedir(), BRAND.configDir, "alias-domains.json");
|
|
749
805
|
function loadAliasDomains() {
|
|
750
806
|
try {
|
|
751
807
|
if (!existsSync5(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
|
|
@@ -814,7 +870,7 @@ function forwardHttp(clientReq, clientRes) {
|
|
|
814
870
|
const isHtmlNavigation = errClass === "econnrefused-coldstart" && clientReq.method === "GET" && accept.includes("text/html");
|
|
815
871
|
if (isHtmlNavigation) {
|
|
816
872
|
clientRes.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
817
|
-
clientRes.end(buildHoldingPage(BRAND
|
|
873
|
+
clientRes.end(buildHoldingPage(BRAND));
|
|
818
874
|
} else {
|
|
819
875
|
clientRes.writeHead(502, { "content-type": "text/plain" });
|
|
820
876
|
clientRes.end(`Bad Gateway (${BRAND.productName} unavailable)`);
|
|
@@ -904,7 +960,7 @@ server.on("upgrade", (req, socket, head) => {
|
|
|
904
960
|
});
|
|
905
961
|
server.listen(EDGE_PORT, EDGE_HOSTNAME, () => {
|
|
906
962
|
console.log(`[edge] listening on http://${EDGE_HOSTNAME}:${EDGE_PORT}`);
|
|
907
|
-
console.log(`[edge] brand=${BRAND.productName}`);
|
|
963
|
+
console.log(`[edge] brand=${BRAND.productName} holding-logo=${BRAND.logoDataUri ? "inlined" : "missing"} assets-dir=${BRAND_ASSETS_DIR || "(none)"}`);
|
|
908
964
|
console.log(`[edge] /websockify \u2192 ${WEBSOCKIFY_HOST}:${WEBSOCKIFY_PORT}`);
|
|
909
965
|
console.log(`[edge] everything else \u2192 ${UPSTREAM_HOST}:${UPSTREAM_PORT}`);
|
|
910
966
|
});
|