Move the loose root-level site/ into a proper containers/weblibreportal app (mirrors getlibreportal): the Eleventy source + nginx serving ./data via publish.sh (npm run build -> docroot). Fix gen-data.mjs repoRoot (now ../../.. from containers/weblibreportal/scripts) so it still finds containers/ for the catalogue. Decouple the two hosts: - weblibreportal -> the website (libreportal.org) - getlibreportal -> downloads only (install.sh + signed release channels); its publish.sh no longer builds the site, and its config text updated to match. Both are dev-only project hosting and will move to a separate repo later; for now they live under containers/ as normal apps. ignores updated for their built docroots; dropped the dead 'site export-ignore'. Verified: gen-data builds the catalogue from the new location (33 apps), and weblibreportal/publish.sh produces a docroot with index.html. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
79 lines
3.4 KiB
JavaScript
79 lines
3.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* gen-data.mjs — single source of truth: the LibrePortal repo.
|
|
*
|
|
* Reads every containers/<app>/<app>.config METADATA block and emits
|
|
* src/_data/apps.json (array of apps, used by the app grid)
|
|
* src/_data/categories.json (categories present, with friendly names + counts)
|
|
*
|
|
* Run via `npm run data` (also runs automatically before `npm run build`).
|
|
* Add an app to the repo -> rebuild -> the website updates itself.
|
|
*/
|
|
import { readdirSync, readFileSync, existsSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join, resolve } from 'node:path';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = resolve(here, '..', '..', '..'); // containers/weblibreportal/scripts -> repo root
|
|
const containersDir = join(repoRoot, 'containers');
|
|
const dataDir = join(here, '..', 'src', '_data');
|
|
const iconsDir = join(here, '..', 'src', 'assets', 'apps');
|
|
|
|
// Only these metadata fields are pulled from each .config. LONG_DESCRIPTION must
|
|
// precede DESCRIPTION in the alternation so it wins the match.
|
|
const FIELD_RE = /^CFG_.+?_(LONG_DESCRIPTION|DESCRIPTION|CATEGORY|TITLE|URL)\s*=\s*"?(.*?)"?\s*$/;
|
|
|
|
const CATEGORY_NAMES = {
|
|
recommended: 'Recommended', communication: 'Communication', development: 'Development',
|
|
knowledge: 'Knowledge', media: 'Media', monitoring: 'Monitoring', storage: 'Storage',
|
|
network: 'Network', security: 'Security', productivity: 'Productivity', finance: 'Finance',
|
|
ai: 'AI', utilities: 'Utilities', privacy: 'Privacy', social: 'Social', system: 'System',
|
|
};
|
|
const titleCase = (s) => s.replace(/[-_]/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase());
|
|
|
|
function parseConfig(text) {
|
|
const out = {};
|
|
for (const raw of text.split(/\r?\n/)) {
|
|
const m = FIELD_RE.exec(raw.trim());
|
|
if (m && out[m[1]] === undefined) out[m[1]] = m[2];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const apps = [];
|
|
for (const slug of readdirSync(containersDir)) {
|
|
const dir = join(containersDir, slug);
|
|
if (!statSync(dir).isDirectory()) continue;
|
|
const cfg = join(dir, `${slug}.config`);
|
|
if (!existsSync(cfg)) continue;
|
|
const meta = parseConfig(readFileSync(cfg, 'utf8'));
|
|
if (!meta.TITLE) continue; // skip anything without a display title (infra without metadata)
|
|
const cats = (meta.CATEGORY || 'utilities').split(',').map((s) => s.trim()).filter(Boolean);
|
|
apps.push({
|
|
slug,
|
|
title: meta.TITLE,
|
|
category: cats[0], // primary category
|
|
categories: cats, // an app can belong to several
|
|
description: meta.DESCRIPTION || '',
|
|
longDescription: meta.LONG_DESCRIPTION || meta.DESCRIPTION || '',
|
|
url: meta.URL || '',
|
|
icon: existsSync(join(iconsDir, `${slug}.svg`)) ? `assets/apps/${slug}.svg` : 'assets/apps/default.svg',
|
|
});
|
|
}
|
|
apps.sort((a, b) => a.title.localeCompare(b.title));
|
|
|
|
const catMap = new Map();
|
|
for (const a of apps) {
|
|
for (const cat of a.categories) {
|
|
const c = catMap.get(cat) || { id: cat, name: CATEGORY_NAMES[cat] || titleCase(cat), count: 0 };
|
|
c.count++;
|
|
catMap.set(cat, c);
|
|
}
|
|
}
|
|
const categories = [...catMap.values()].sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
|
|
mkdirSync(dataDir, { recursive: true });
|
|
writeFileSync(join(dataDir, 'apps.json'), JSON.stringify(apps, null, 2) + '\n');
|
|
writeFileSync(join(dataDir, 'categories.json'), JSON.stringify(categories, null, 2) + '\n');
|
|
console.log(`✓ ${apps.length} apps across ${categories.length} categories → src/_data/{apps,categories}.json`);
|