Includes fixes found while reviewing the app: - Results: open each result by a type+agent+id key, so a video and a material draft with the same id no longer open each other's popup - AuthGate: clear the query cache during render, before pages build their queries, and remount pages on organization switch (pages no longer stay on skeletons for up to a minute after sign-in or refresh) - Instruction: keep only failed rules after a partial approve, so retrying does not create the successful schedules twice; drop the monthly note once count or frequency is edited - Parser/schedules: never schedule a count below one - Audio: ignore the end event of a cancelled utterance when changing speed - Downloads: revoke the object URL later so Safari/Firefox can read it - Results: refetch SCORM content when any part's revision changes; show a failed video as failed instead of "still rendering" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
53 lines
2.4 KiB
JavaScript
53 lines
2.4 KiB
JavaScript
// Static host for Learning Lab Express. Proxies /api to the learning-lab engine
|
|
// so the browser stays same-origin (no CORS setup needed on the engine).
|
|
import http from 'node:http';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { extname, join, normalize } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ROOT = fileURLToPath(new URL('.', import.meta.url));
|
|
const PORT = Number(process.env.PORT || 3000);
|
|
const ENGINE = new URL(process.env.ENGINE_URL || 'http://localhost:8000');
|
|
const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css', '.svg': 'image/svg+xml' };
|
|
|
|
function proxy(req, res) {
|
|
const upstream = http.request({
|
|
host: ENGINE.hostname, port: ENGINE.port, path: req.url, method: req.method,
|
|
headers: { ...req.headers, host: ENGINE.host },
|
|
}, up => { res.writeHead(up.statusCode, up.headers); up.pipe(res); });
|
|
upstream.on('error', () => {
|
|
res.writeHead(502, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ detail: `Engine learning-lab tidak dapat dihubungi di ${ENGINE.origin}` }));
|
|
});
|
|
req.pipe(upstream);
|
|
}
|
|
|
|
// Clerk publishable keys are public; default to the engine's own dev key so both share one Clerk instance.
|
|
async function clerkKey() {
|
|
if (process.env.CLERK_PUBLISHABLE_KEY) return process.env.CLERK_PUBLISHABLE_KEY;
|
|
try {
|
|
const env = await readFile(join(ROOT, '../../learning-lab/backend/.env'), 'utf8');
|
|
return env.match(/^VITE_CLERK_PUBLISHABLE_KEY=["']?([^"'\s]+)/m)?.[1] || '';
|
|
} catch { return ''; }
|
|
}
|
|
|
|
async function serve(req, res) {
|
|
const path = new URL(req.url, 'http://x').pathname;
|
|
if (path === '/config.js') {
|
|
res.writeHead(200, { 'content-type': TYPES['.js'] });
|
|
return res.end(`window.LLE_CONFIG=${JSON.stringify({ clerkKey: await clerkKey() })};`);
|
|
}
|
|
const name = path === '/' ? 'index.html' : normalize(path).replace(/^([/\\])+/, '');
|
|
if (name.startsWith('..')) { res.writeHead(403); return res.end(); }
|
|
try {
|
|
const body = await readFile(join(ROOT, name));
|
|
res.writeHead(200, { 'content-type': TYPES[extname(name)] || 'application/octet-stream' });
|
|
res.end(body);
|
|
} catch {
|
|
res.writeHead(404); res.end('Not found');
|
|
}
|
|
}
|
|
|
|
http.createServer((req, res) => (req.url.startsWith('/api/') ? proxy(req, res) : serve(req, res)))
|
|
.listen(PORT, () => console.log(`Learning Lab Express: http://localhost:${PORT} → engine ${ENGINE.origin}`));
|