← back to Age Theme Slider

server.js

54 lines

// Age Theme Slider — standalone app
// Serves a theme slider that morphs through 8 age bands (3-95).
// Bands data canonical in this project's bands.json (deploy-portable).
const express = require('express');
const path = require('path');
const fs = require('fs');

const app = express();
const PORT = process.env.PORT || 9717;

const BANDS_PATH = path.join(__dirname, 'bands.json');
let bandsCache = null;
let bandsCacheMtime = 0;

function loadBands() {
  const st = fs.statSync(BANDS_PATH);
  if (!bandsCache || st.mtimeMs !== bandsCacheMtime) {
    bandsCache = JSON.parse(fs.readFileSync(BANDS_PATH, 'utf8'));
    bandsCacheMtime = st.mtimeMs;
  }
  return bandsCache;
}

app.use((req, res, next) => {
  res.setHeader('Cache-Control', 'no-store, must-revalidate');
  res.setHeader('X-Frame-Options', 'SAMEORIGIN');
  // Allow iframe embedding from anywhere — this app is meant to be embedded.
  res.removeHeader('X-Frame-Options');
  res.setHeader('Content-Security-Policy', "frame-ancestors *");
  next();
});

app.get('/api/bands', (_req, res) => {
  try { res.json(loadBands()); }
  catch (e) { res.status(500).json({ error: String(e.message || e) }); }
});

// Snapshot-file 404 guard — never serve .bak / .bak.* / .pre-* paths from
// the static root, even if one slips into public/ by accident.
app.use((req, res, next) => {
  if (/\.(bak|pre-)/i.test(req.path) || /(^|\/)\.pre-/i.test(req.path)) {
    return res.status(404).type('text/plain').send('Not Found');
  }
  next();
});

app.use(express.static(path.join(__dirname, 'public'), { extensions: ['html'] }));

app.get('/health', (_req, res) => res.json({ ok: true, port: PORT, bands_version: loadBands().version }));

app.listen(PORT, '0.0.0.0', () => {
  console.log(`age-theme-slider listening on http://127.0.0.1:${PORT}`);
});