librelad 31b73f9670 feat(webui): auto-discover features from folders, mirroring the theme system
Themes are already modular via folder discovery (GET /api/themes/list scans
themes/<name>/). This brings the SAME model to pages:
- backend/routes/features.js: public GET /api/features/list scans
  frontend/features/<id>/feature.json and returns the page manifest. The
  Node process reads its own bind-mounted /app/frontend — no runFileOp /
  regen / source-array plumbing needed (sidesteps the shell-generator gotchas).
- features/<id>/feature.json: each page now self-describes (id, routes,
  module, handler, navId, nav, order). 6 real features + 3 redirect-only
  (config/peers/ssh) so behaviour is preserved exactly.
- kernel loadManifest() prefers /api/features/list, falls back to the static
  features/manifest.dev.json when the endpoint isn't up yet.

Result: dropping a features/<id>/ folder registers a page; deleting it
removes it — zero central edits, exactly like dropping a theme folder.
(Backend route needs a Node restart to activate; the static-manifest
fallback keeps everything working until then.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 00:18:20 +01:00

59 lines
2.3 KiB
JavaScript

const express = require('express');
const fs = require('fs');
const path = require('path');
const router = express.Router();
const FEATURES_DIR = path.join(__dirname, '..', '..', 'frontend', 'features');
/* =========================
GET /api/features/list
Walks frontend/features/<id>/ and returns one entry per directory that
contains a feature.json — the WebUI's page manifest, discovered from the
folders themselves (exactly how /api/themes/list discovers themes). Drop a
features/<id>/ folder in and its page appears; delete it and the page is
gone — no central edit. The navigation kernel fetches this and falls back to
the checked-in features/manifest.dev.json if the API is unavailable.
Each feature.json declares: id, routes[], optional module (self-registering
index.js), optional handler (legacy fallback method), navId, nav{}, and order
(controls list + route-precedence ordering — e.g. apps before app-detail so
the '/apps*' wildcard wins over '/app*').
Public — the page list isn't sensitive and the kernel needs it before login
to render the right route (same rationale as the themes list).
========================= */
router.get('/list', (req, res) => {
const features = [];
try {
if (fs.existsSync(FEATURES_DIR)) {
for (const entry of fs.readdirSync(FEATURES_DIR, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const metaPath = path.join(FEATURES_DIR, entry.name, 'feature.json');
if (!fs.existsSync(metaPath)) continue;
try {
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
if (meta && meta.id && Array.isArray(meta.routes)) {
features.push(meta);
} else {
console.warn(`[features] ${entry.name}/feature.json missing id/routes — skipped`);
}
} catch (e) {
console.warn(`[features] ${entry.name}/feature.json is malformed — skipped:`, e.message);
}
}
}
} catch (err) {
console.error('Error scanning features directory:', err);
}
// Ascending `order` controls both nav order and route-registration order
// (the latter preserves wildcard precedence). Missing order sorts last.
features.sort((a, b) => ((a.order ?? 999) - (b.order ?? 999)));
res.json({ version: 1, source: 'scan', features });
});
module.exports = router;