Files
Synapsis/deploy/maintenance-server.mjs
cyph3rasi 8935bdf5c5 Use the bundled Synapsis logo on maintenance pages without custom branding
Hop-State: A_06FP9KPYV5CQ0X9F467H58G
Hop-Proposal: R_06FP9KNN01X6RVC106905HG
Hop-Task: T_06FP9KBX6WGXFK0K5XYQSR0
Hop-Attempt: AT_06FP9KBX6Y81G7RGHF7RCK8
2026-07-15 01:05:06 -07:00

167 lines
7.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 appDir = process.env.MAINTENANCE_APP_DIR || process.cwd();
const brandingPath = join(dataDir, 'maintenance-branding.json');
const logoPath = join(dataDir, 'maintenance-logo');
const defaultLogoPath = join(appDir, 'public', 'logotext.svg');
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 hasDefaultLogo = existsSync(defaultLogoPath);
const logoMarkup = hasStoredLogo || externalLogoUrl || hasDefaultLogo
? '<img class="logo" src="/maintenance-logo" alt="Node logo">'
: '<div class="brand-fallback">Synapsis</div>';
const page = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#090909">
<title>Update in progress</title>
<style>
:root { --accent: ${accentColor}; color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; display: grid; place-items: center; overflow: hidden; background: #090909; color: #f5f5f5; }
body::before { content: ""; position: fixed; width: 34rem; height: 34rem; border-radius: 50%; background: color-mix(in srgb, var(--accent) 13%, transparent); filter: blur(110px); transform: translate(-38vw, -30vh); }
main { width: min(90vw, 34rem); padding: 3rem; text-align: center; border: 1px solid #292929; border-radius: 1.75rem; background: rgba(18, 18, 18, .92); box-shadow: 0 2rem 7rem rgba(0, 0, 0, .5); }
.logo { display: block; width: auto; max-width: min(18rem, 75%); height: auto; max-height: 6rem; margin: 0 auto 2rem; object-fit: contain; filter: drop-shadow(0 0 1.75rem color-mix(in srgb, var(--accent) 35%, transparent)); }
.brand-fallback { margin: 0 auto 2rem; color: var(--accent); font-size: 1.5rem; font-weight: 800; letter-spacing: -.04em; }
h1 { margin: 0; font-size: clamp(1.75rem, 6vw, 2.5rem); letter-spacing: -.045em; }
p { margin: 1rem auto 0; max-width: 25rem; color: #a6a6a6; font-size: 1.05rem; line-height: 1.6; }
.status { margin-top: 2rem; display: inline-flex; align-items: center; gap: .65rem; color: #d7d7d7; font-size: .9rem; }
.dot { width: .6rem; height: .6rem; border-radius: 50%; background: var(--accent); animation: pulse 1.4s ease-in-out infinite; }
@keyframes pulse { 50% { opacity: .3; transform: scale(.75); } }
@media (max-width: 520px) { main { padding: 2.25rem 1.5rem; } }
</style>
</head>
<body>
<main>
${logoMarkup}
<h1>Update in progress</h1>
<p>Were making Synapsis better. Well be right back!</p>
<div class="status"><span class="dot"></span>Checking for the node…</div>
</main>
<script>
setInterval(async () => {
try {
const response = await fetch('/api/health', { cache: 'no-store' });
if (response.ok) location.reload();
} catch {}
}, 5000);
</script>
</body>
</html>`;
const server = http.createServer((request, response) => {
if (request.url?.split('?')[0] === '/maintenance-logo') {
if (hasStoredLogo) {
const logo = readFileSync(logoPath);
response.writeHead(200, {
'Content-Type': logoContentType,
'Content-Length': logo.byteLength,
'Cache-Control': 'no-store, max-age=0',
});
response.end(logo);
return;
}
if (externalLogoUrl) {
response.writeHead(302, { Location: externalLogoUrl, 'Cache-Control': 'no-store, max-age=0' });
response.end();
return;
}
if (hasDefaultLogo) {
const logo = readFileSync(defaultLogoPath);
response.writeHead(200, {
'Content-Type': 'image/svg+xml',
'Content-Length': logo.byteLength,
'Cache-Control': 'no-store, max-age=0',
});
response.end(logo);
return;
}
}
response.writeHead(503, {
'Content-Type': 'text/html; charset=utf-8',
'Content-Length': Buffer.byteLength(page),
'Cache-Control': 'no-store, max-age=0',
'Retry-After': '30',
'X-Robots-Tag': 'noindex',
});
if (request.method === 'HEAD') response.end();
else response.end(page);
});
server.listen(port, host, () => {
console.log(`Synapsis maintenance page listening on http://${host}:${port}`);
});
const shutdown = () => server.close(() => process.exit(0));
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);