Files
Synapsis/deploy/maintenance-server.mjs
T
cyph3rasi f435e62220 Show NSFW-node members unblurred media and brand maintenance screens per node
Hop-State: A_06FP9J7X3NRW462MRQP41Z0
Hop-Proposal: R_06FP9J6Q5X4KY348B7CJS28
Hop-Task: T_06FP9GSP35DQFZ5RXET13S0
Hop-Attempt: AT_06FP9GSP348ZKDWV5HCQYPG
2026-07-15 00:58:41 -07:00

154 lines
6.5 KiB
JavaScript
Raw 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 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
? '<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;
}
}
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);