Update in progress
We’re making Synapsis better. We’ll be right back!
diff --git a/README.md b/README.md
index 87282ea..6ea5bd6 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ The installer creates:
Every node checks `origin/main` about once per minute. When a new commit is available, Synapsis fast-forwards the checkout, replaces the single `backups/latest` database snapshot, installs dependencies, runs migrations, builds, and restarts automatically. The repository commit count is shown in the Network Info card; `/api/version` exposes both that number and the full deployed commit hash.
-While an update is being installed, `synapsis-maintenance.service` temporarily serves a branded maintenance page on the node's configured `PORT`. Existing reverse proxies continue receiving an HTTP response instead of showing a gateway error, and browsers automatically reload when Synapsis is ready.
+While an update is being installed, `synapsis-maintenance.service` temporarily serves a maintenance page on the node's configured `PORT`, using that node's logo and accent color. Existing reverse proxies continue receiving an HTTP response instead of showing a gateway error, and browsers automatically reload when Synapsis is ready.
For a node installed before automatic updates existed, bootstrap the timer once with:
diff --git a/deploy/maintenance-server.mjs b/deploy/maintenance-server.mjs
index 264a535..298ab3e 100644
--- a/deploy/maintenance-server.mjs
+++ b/deploy/maintenance-server.mjs
@@ -1,7 +1,77 @@
import http from 'node:http';
+import { dirname, join } from 'node:path';
+import { existsSync, readFileSync } from 'node:fs';
+import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
const host = process.env.MAINTENANCE_HOST || '127.0.0.1';
const port = Number.parseInt(process.env.PORT || '43821', 10);
+const dataDir = process.env.MAINTENANCE_DATA_DIR
+ || dirname(process.env.DATABASE_PATH || '/var/lib/synapsis/synapsis.db');
+const brandingPath = join(dataDir, 'maintenance-branding.json');
+const logoPath = join(dataDir, 'maintenance-logo');
+
+function validAccent(value) {
+ return typeof value === 'string' && /^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(value)
+ ? value
+ : '#9fdf5f';
+}
+
+async function captureBranding() {
+ const baseUrl = `http://127.0.0.1:${port}`;
+ await mkdir(dataDir, { recursive: true });
+ const nodeResponse = await fetch(`${baseUrl}/api/node`, { cache: 'no-store' });
+ if (!nodeResponse.ok) throw new Error(`Node branding returned HTTP ${nodeResponse.status}`);
+
+ const node = await nodeResponse.json();
+ const branding = {
+ accentColor: validAccent(node.accentColor),
+ externalLogoUrl: typeof node.logoUrl === 'string' && /^https?:\/\//i.test(node.logoUrl)
+ ? node.logoUrl
+ : null,
+ logoContentType: null,
+ };
+
+ const logoResponse = await fetch(`${baseUrl}/api/node/logo`, { cache: 'no-store' });
+ if (logoResponse.ok) {
+ const contentType = logoResponse.headers.get('content-type')?.split(';')[0];
+ if (contentType?.startsWith('image/')) {
+ const logoTempPath = `${logoPath}.tmp`;
+ await writeFile(logoTempPath, Buffer.from(await logoResponse.arrayBuffer()));
+ await rename(logoTempPath, logoPath);
+ branding.logoContentType = contentType;
+ } else {
+ await rm(logoPath, { force: true });
+ }
+ } else {
+ await rm(logoPath, { force: true });
+ }
+
+ const brandingTempPath = `${brandingPath}.tmp`;
+ await writeFile(brandingTempPath, `${JSON.stringify(branding)}\n`, { mode: 0o640 });
+ await rename(brandingTempPath, brandingPath);
+}
+
+if (process.argv.includes('--capture-branding')) {
+ await captureBranding();
+ process.exit(0);
+}
+
+let branding = {};
+try {
+ branding = JSON.parse(readFileSync(brandingPath, 'utf8'));
+} catch {}
+
+const accentColor = validAccent(branding.accentColor);
+const logoContentType = typeof branding.logoContentType === 'string' && branding.logoContentType.startsWith('image/')
+ ? branding.logoContentType
+ : 'application/octet-stream';
+const externalLogoUrl = typeof branding.externalLogoUrl === 'string' && /^https?:\/\//i.test(branding.externalLogoUrl)
+ ? branding.externalLogoUrl
+ : null;
+const hasStoredLogo = existsSync(logoPath);
+const logoMarkup = hasStoredLogo || externalLogoUrl
+ ? ''
+ : '
We’re making Synapsis better. We’ll be right back!