@rubytech/create-maxy 1.0.777 → 1.0.778

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.777",
3
+ "version": "1.0.778",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -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 ("{{productName}} is 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. 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.
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
 
@@ -335,7 +335,10 @@ handle_connect_requests() {
335
335
  # Attempt WiFi connection. Capture exit code before || true so we
336
336
  # get nmcli's actual exit status, not the unconditional 0 from || true.
337
337
  local connect_output connect_exit
338
- connect_output=$(nmcli device wifi connect "$target_ssid" password "$target_password" --wait 30 2>&1)
338
+ # `--wait` / `-w` is a top-level nmcli option (must precede the
339
+ # subcommand), not an argument to `device wifi connect`. Putting it
340
+ # after the subcommand fails with "invalid extra argument '--wait'".
341
+ connect_output=$(nmcli --wait 30 device wifi connect "$target_ssid" password "$target_password" 2>&1)
339
342
  connect_exit=$?
340
343
 
341
344
  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 join3 } from "path";
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
- var BRAND_DEFAULTS = { configDir: ".maxy", productName: "Maxy" };
672
- function loadBrand(path) {
673
- if (!path || !existsSync4(path)) return { ...BRAND_DEFAULTS };
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(path, "utf-8"));
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: typeof parsed.configDir === "string" ? parsed.configDir : BRAND_DEFAULTS.configDir,
678
- productName: typeof parsed.productName === "string" ? parsed.productName : BRAND_DEFAULTS.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 = { "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;", "'":
689
725
  function escapeHtml(s) {
690
726
  return String(s).replace(/[<>&"']/g, (c) => HTML_ESCAPES[c] ?? c);
691
727
  }
692
- function buildHoldingPage(productName) {
693
- const safeName = escapeHtml(productName);
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: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
704
- background: #fafaf8;
705
- color: #2a2a2a;
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
- .name { font-size: 1.4rem; font-weight: 500; margin: 0 0 0.5rem; }
712
- .sub { font-size: 0.95rem; color: #888; margin: 0; }
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; margin: 0 2px; opacity: 0.3;
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.3; } 40% { opacity: 1; } }
774
+ @keyframes pulse { 0%, 80%, 100% { opacity: 0.25; } 40% { opacity: 0.85; } }
721
775
  </style>
722
776
  </head>
723
777
  <body>
724
- <p class="name">${safeName} is starting</p>
725
- <p class="sub"><span class="dot"></span><span class="dot"></span><span class="dot"></span></p>
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 ? join3(PLATFORM_ROOT2, "config", "brand.json") : "";
747
- var BRAND = loadBrand(BRAND_JSON_PATH);
748
- var ALIAS_DOMAINS_PATH = join3(homedir(), BRAND.configDir, "alias-domains.json");
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.productName));
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
  });