Lint / JS (eslint) (pull_request) Successful in 10s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / JS Security (npm audit) (pull_request) Failing after 10s
Test / JS Tests (jest) (pull_request) Successful in 14s
Lint / JS (eslint) (push) Successful in 11s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / JS Security (npm audit) (push) Failing after 11s
Test / JS Tests (jest) (push) Successful in 10s
- Fix design-system class names that do not exist in base.css (.lt-alert--error, .lt-form-hint); add shared .lt-modal-lg, .lt-field-error and .is-invalid rules - Remove public/index.html and the public/base.js symlink - package.json: start script, correct main - README: vendored design system, Web UI section, new env vars, read-only dev mode Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HamVMDrA8RqhyxmUHgiqRp
101 lines
3.2 KiB
JavaScript
101 lines
3.2 KiB
JavaScript
// Two-step EJS rendering: views/pages/<view>.ejs -> `body` -> views/layout.ejs.
|
|
// No express-ejs-layouts; the vendored layout is a wrapper file that expects `body`.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const ejs = require('ejs');
|
|
const { navLinks } = require('./nav');
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
const VIEWS_DIR = path.join(ROOT, 'views');
|
|
const PAGES_DIR = path.join(VIEWS_DIR, 'pages');
|
|
const LAYOUT = path.join(VIEWS_DIR, 'layout.ejs');
|
|
const STUB = path.join(PAGES_DIR, '_stub.ejs');
|
|
|
|
const CACHE = process.env.NODE_ENV === 'production';
|
|
|
|
// Cache-busting token: <package version>-<web_template short sha>, computed once.
|
|
const assetVersion = (function computeAssetVersion() {
|
|
let version = '0.0.0';
|
|
try {
|
|
version = require(path.join(ROOT, 'package.json')).version || '0.0.0';
|
|
} catch (_) { /* keep default */ }
|
|
let sha = '';
|
|
try {
|
|
const raw = fs.readFileSync(path.join(ROOT, 'public/web_template/VERSION'), 'utf8').trim();
|
|
sha = (raw.split(/\s+/)[1] || '').trim();
|
|
} catch (_) { /* VERSION not vendored yet */ }
|
|
return sha ? `${version}-${sha}` : version;
|
|
})();
|
|
|
|
const APP_NAME = process.env.APP_NAME || 'PULSE';
|
|
const APP_SUBTITLE = process.env.APP_SUBTITLE || 'Worker Orchestration // LotusGuild';
|
|
|
|
function viewPath(view) {
|
|
const file = path.join(PAGES_DIR, `${view}.ejs`);
|
|
return fs.existsSync(file) ? file : STUB;
|
|
}
|
|
|
|
// Render a page view inside the shared layout.
|
|
async function renderPage(req, res, view, locals = {}) {
|
|
const data = Object.assign({
|
|
user: req.user || null,
|
|
nonce: res.locals && res.locals.nonce,
|
|
appName: APP_NAME,
|
|
appSubtitle: APP_SUBTITLE,
|
|
csrfToken: '',
|
|
navLinks,
|
|
pageTitle: '',
|
|
activeNav: '',
|
|
pageStyles: [],
|
|
pageScripts: [],
|
|
pageConfig: {},
|
|
assetVersion
|
|
}, locals);
|
|
|
|
const opts = { cache: CACHE, filename: viewPath(view) };
|
|
const body = await ejs.renderFile(viewPath(view), data, opts);
|
|
const html = await ejs.renderFile(LAYOUT, Object.assign({}, data, { body }), {
|
|
cache: CACHE,
|
|
filename: LAYOUT
|
|
});
|
|
|
|
res.set('Content-Type', 'text/html; charset=utf-8');
|
|
res.send(html);
|
|
return html;
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s).replace(/&/g, '&').replace(/</g, '<')
|
|
.replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
// Small self-contained themed page for 401/403/404/500.
|
|
function renderError(req, res, status, title, message) {
|
|
const html = `<!DOCTYPE html>
|
|
<html lang="en" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<title>${esc(status)} — ${esc(APP_NAME)}</title>
|
|
<link rel="stylesheet" href="/web_template/base.css?v=${esc(assetVersion)}">
|
|
</head>
|
|
<body>
|
|
<main class="lt-main lt-container">
|
|
<div class="lt-frame">
|
|
<div class="lt-alert lt-alert--error">
|
|
<strong>${esc(status)} — ${esc(title)}</strong>
|
|
<p>${esc(message)}</p>
|
|
</div>
|
|
<p><a class="lt-btn lt-btn-secondary lt-btn-sm" href="/">Return to dashboard</a></p>
|
|
</div>
|
|
</main>
|
|
</body>
|
|
</html>
|
|
`;
|
|
res.status(status).set('Content-Type', 'text/html; charset=utf-8').send(html);
|
|
}
|
|
|
|
module.exports = { renderPage, renderError, assetVersion };
|