201 lines
6.6 KiB
JavaScript
201 lines
6.6 KiB
JavaScript
const fs = require('fs');
|
|
const express = require('express');
|
|
const path = require('path');
|
|
const { scanProjects } = require('./lib/scanner');
|
|
const pm = require('./lib/process-manager');
|
|
const { takeScreenshot } = require('./lib/screenshotter');
|
|
const centro = require('./lib/centrifugo');
|
|
|
|
const app = express();
|
|
const PORT = 3000;
|
|
|
|
// Scan projects on startup
|
|
let projects = scanProjects();
|
|
const projectsBySlug = new Map(projects.map(p => [p.slug, p]));
|
|
|
|
console.log(`Found ${projects.length} projects:`);
|
|
projects.forEach(p => {
|
|
const fe = p.frontend ? ` + frontend(${p.frontend.dir} :${p.frontend.port} ${p.frontend.packageManager})` : '';
|
|
console.log(` ${p.slug} :${p.port} (${p.portSource || 'assigned'})${fe}`);
|
|
});
|
|
|
|
// Publish status changes to Centrifugo
|
|
pm.events.on('status', (slug, subtype, status) => {
|
|
centro.publishStatus(slug, status, { subtype });
|
|
});
|
|
|
|
// Static files
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
app.use(express.json());
|
|
|
|
// Serve project docs folders with directory listing
|
|
app.use('/docs/:slug', (req, res, next) => {
|
|
const project = projectsBySlug.get(req.params.slug);
|
|
if (!project) return res.status(404).send('Project not found');
|
|
|
|
const docsRoot = path.join(project.path, 'docs');
|
|
const reqPath = decodeURIComponent(req.path).replace(/\.\./g, '');
|
|
const fullPath = path.join(docsRoot, reqPath);
|
|
|
|
if (!fullPath.startsWith(docsRoot)) return res.status(403).send('Forbidden');
|
|
|
|
let stat;
|
|
try { stat = fs.statSync(fullPath); } catch { return res.status(404).send('Not found'); }
|
|
|
|
if (stat.isFile()) return res.sendFile(fullPath);
|
|
|
|
// Directory listing
|
|
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
const basePath = `/docs/${project.slug}${reqPath}`.replace(/\/$/, '');
|
|
const dirs = entries.filter(e => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
|
|
const files = entries.filter(e => e.isFile()).sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
const items = [
|
|
...dirs.map(e => `<li><a href="${basePath}/${e.name}/">${e.name}/</a></li>`),
|
|
...files.map(e => `<li><a href="${basePath}/${e.name}">${e.name}</a></li>`),
|
|
].join('\n');
|
|
|
|
res.send(`<!DOCTYPE html><html><head><meta charset="utf-8">
|
|
<title>docs/${project.slug}${reqPath}</title>
|
|
<style>body{font-family:system-ui;background:#0f1117;color:#e4e6ed;padding:2rem}
|
|
a{color:#6c8cff;text-decoration:none}a:hover{text-decoration:underline}
|
|
li{margin:4px 0;font-size:15px}h2{font-weight:500;margin-bottom:1rem}</style>
|
|
</head><body><h2>docs/${project.slug}${reqPath}/</h2>
|
|
${reqPath !== '/' && reqPath !== '' ? '<a href="../">← back</a>' : ''}
|
|
<ul>${items}</ul></body></html>`);
|
|
});
|
|
|
|
// Centrifugo connection token
|
|
app.get('/api/centrifugo/token', (req, res) => {
|
|
const token = centro.createJWT('projects-browser');
|
|
res.json({ token });
|
|
});
|
|
|
|
// List all projects with live status
|
|
app.get('/api/projects', (req, res) => {
|
|
const statuses = pm.getAllStatuses();
|
|
const result = projects.map(p => {
|
|
const s = statuses[p.slug];
|
|
const hasFrontend = !!p.frontend;
|
|
return {
|
|
...p,
|
|
status: s ? pm.combinedStatus(p.slug, hasFrontend) : 'stopped',
|
|
apiStatus: s?.api?.status || 'stopped',
|
|
frontendStatus: hasFrontend ? (s?.frontend?.status || 'stopped') : null,
|
|
pid: s?.api?.pid || null,
|
|
};
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
// Start a project
|
|
app.post('/api/projects/:slug/start', (req, res) => {
|
|
const project = projectsBySlug.get(req.params.slug);
|
|
if (!project) return res.status(404).json({ error: 'Project not found' });
|
|
|
|
const result = pm.start(project.slug, project.path, project.port, project.frontend || null);
|
|
res.json(result);
|
|
});
|
|
|
|
// Stop a project
|
|
app.post('/api/projects/:slug/stop', async (req, res) => {
|
|
const project = projectsBySlug.get(req.params.slug);
|
|
if (!project) return res.status(404).json({ error: 'Project not found' });
|
|
|
|
const result = await pm.stop(project.slug);
|
|
res.json(result);
|
|
});
|
|
|
|
// Screenshot a project
|
|
app.post('/api/projects/:slug/screenshot', async (req, res) => {
|
|
const project = projectsBySlug.get(req.params.slug);
|
|
if (!project) return res.status(404).json({ error: 'Project not found' });
|
|
|
|
const s = pm.getStatus(project.slug);
|
|
const hasFrontend = !!project.frontend;
|
|
|
|
// For frontend projects, prefer frontend being up; otherwise check API
|
|
if (hasFrontend) {
|
|
if (s.frontend?.status !== 'running') {
|
|
return res.status(400).json({ error: 'Frontend must be running to take screenshot' });
|
|
}
|
|
} else {
|
|
if (s.api.status !== 'running') {
|
|
return res.status(400).json({ error: 'Project must be running to take screenshot' });
|
|
}
|
|
}
|
|
|
|
// Screenshot the frontend port if available, otherwise API port
|
|
const screenshotPort = hasFrontend ? project.frontend.port : project.port;
|
|
|
|
try {
|
|
const imgPath = await takeScreenshot(project.slug, screenshotPort, hasFrontend);
|
|
centro.publishScreenshot(project.slug);
|
|
res.json({ screenshot: imgPath });
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Screenshot all running projects
|
|
app.post('/api/screenshot-all', async (req, res) => {
|
|
const statuses = pm.getAllStatuses();
|
|
const results = {};
|
|
|
|
for (const p of projects) {
|
|
const s = statuses[p.slug];
|
|
if (!s) continue;
|
|
|
|
const hasFrontend = !!p.frontend;
|
|
const combined = pm.combinedStatus(p.slug, hasFrontend);
|
|
if (combined !== 'running') continue;
|
|
|
|
const screenshotPort = hasFrontend ? p.frontend.port : p.port;
|
|
try {
|
|
results[p.slug] = await takeScreenshot(p.slug, screenshotPort, hasFrontend);
|
|
centro.publishScreenshot(p.slug);
|
|
} catch (err) {
|
|
results[p.slug] = { error: err.message };
|
|
}
|
|
}
|
|
res.json(results);
|
|
});
|
|
|
|
// Get logs for a project
|
|
app.get('/api/projects/:slug/logs', (req, res) => {
|
|
const project = projectsBySlug.get(req.params.slug);
|
|
if (!project) return res.status(404).json({ error: 'Project not found' });
|
|
|
|
const s = pm.getStatus(project.slug);
|
|
res.json({
|
|
api: s.api.logs,
|
|
frontend: s.frontend?.logs || [],
|
|
});
|
|
});
|
|
|
|
// Rescan projects
|
|
app.post('/api/rescan', (req, res) => {
|
|
projects = scanProjects();
|
|
projectsBySlug.clear();
|
|
projects.forEach(p => projectsBySlug.set(p.slug, p));
|
|
res.json({ count: projects.length });
|
|
});
|
|
|
|
// Graceful shutdown
|
|
let shuttingDown = false;
|
|
function shutdown() {
|
|
if (shuttingDown) return;
|
|
shuttingDown = true;
|
|
console.log('\nShutting down, stopping all child processes...');
|
|
pm.killAllSync();
|
|
console.log('All processes killed.');
|
|
process.exit(0);
|
|
}
|
|
|
|
process.on('SIGINT', shutdown);
|
|
process.on('SIGTERM', shutdown);
|
|
|
|
app.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`Projects Browser running at http://0.0.0.0:${PORT}`);
|
|
});
|