Files
AdrianoDev 6301f521fd Separazione: le aree riservate diventano un'app propria (apps/platforms)
Campus, Stress Index e Longevity escono dal sito e diventano
apps.insanitylab.it, con utenti, sessioni, login e database propri.
Deciso da Adriano il 03/09/2026: una repo sola, due applicazioni
autosufficienti, cosi' che ognuna resti estraibile.

Misura che ha guidato il taglio: le tre aree condividevano col sito
SOLO l'identita' - ~170 righe fra auth.ts, middleware.ts e safe-next.ts.
Layout, librerie e dati erano gia' disgiunti.

Lato piattaforme:
- ruoli piattaforme/cliente/trainer/admin, senza quelli redazionali
- canAccessPath con default CHIUSO (nel sito era aperto: li' le rotte non
  elencate erano di chiunque fosse loggato, qui ogni sezione e' di qualcuno)
- tutta l'app protetta, si elencano le eccezioni invece dei prefissi protetti
- /api risponde 401/403, non un redirect verso una pagina di login
- tre database distinti: utenti, dati clinici senza nomi, mappatura coi nomi

Lato sito:
- ROLES tornano i tre redazionali; via le regole e il login pubblico
- il login del pannello RESPINGE un account emigrato invece di aprirgli una
  sessione: senza, landingFor lo mandava su /admin, il middleware glielo
  negava, e si formava un ciclo di redirect che sembra un guasto del sito
- l'header non mostra piu' un menu delle piattaforme: un link che esce

Verificato: sito 20 file / 146 test, piattaforme 26 file / 197 test, entrambi
i build passano, e l'app girata davvero in locale - il ruolo piattaforme entra
in Campus e viene rimbalzato da Longevity, il cliente il contrario.

Non ancora in produzione: manca il record DNS di apps.insanitylab.it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 11:21:12 +00:00

121 lines
4.5 KiB
JavaScript

#!/usr/bin/env node
/**
* sync-campus.mjs — importa le guide di studio del corso Biohacking Campus
* nella sezione riservata /campus del sito.
*
* COSA FA
* Legge i file `<slug>.studio.md` dal progetto sorgente, li converte in
* frammenti HTML e produce in `campus-content/`:
* - `nav.json` → aree, capitoli, guide (ordine, titoli, prev/next)
* - `pages/<slug>.html` → corpo della guida
* - `assets/` → immagini delle slide, copiate dal sorgente
* Le pagine sono renderizzate a runtime da src/pages/campus/ con lo stile
* del sito InsanityLab; qui non si genera alcun layout.
*
* USO
* npm run sync-campus [-- <path-progetto-biohacking-campus>]
* Default: /home/adriano/Wasabi-Adp-Work/AI-OS/projects/personale/biohacking-campus
*
* IDEMPOTENTE
* Rigenera nav.json e le pagine, rimuovendo i frammenti orfani. Gli assets
* sono copiati solo se mancanti o più vecchi del sorgente.
*/
import { readFile, writeFile, mkdir, readdir, rm, copyFile, stat } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { CHAPTERS, CH_COLORS, AREAS, sourceDirFor } from './campus/structure.mjs';
import { parseGuide } from './campus/parse.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const OUT_DIR = resolve(HERE, '..', 'campus-content');
const DEFAULT_SRC = '/home/adriano/Wasabi-Adp-Work/AI-OS/projects/personale/biohacking-campus';
async function copyTree(src, dest) {
await mkdir(dest, { recursive: true });
let copied = 0;
for (const entry of await readdir(src, { withFileTypes: true })) {
const from = join(src, entry.name);
const to = join(dest, entry.name);
if (entry.isDirectory()) {
copied += await copyTree(from, to);
continue;
}
const [s, d] = [await stat(from), existsSync(to) ? await stat(to) : null];
if (!d || d.mtimeMs < s.mtimeMs || d.size !== s.size) {
await copyFile(from, to);
copied++;
}
}
return copied;
}
async function main() {
const srcRoot = resolve(process.argv[2] ?? DEFAULT_SRC);
if (!existsSync(srcRoot)) {
console.error(`Progetto sorgente non trovato: ${srcRoot}`);
process.exit(1);
}
const pagesDir = join(OUT_DIR, 'pages');
await mkdir(pagesDir, { recursive: true });
// 1. parsing di tutte le guide, capitolo per capitolo
const chapters = [];
const written = new Set();
for (const [num, title, sub, slugs] of CHAPTERS) {
const lessons = [];
for (const [i, slug] of slugs.entries()) {
const file = join(srcRoot, sourceDirFor(slug), `${slug}.studio.md`);
if (!existsSync(file)) {
console.warn(` guida mancante, salto: ${slug}`);
continue;
}
const g = parseGuide(await readFile(file, 'utf8'), slug, i + 1);
await writeFile(join(pagesDir, `${slug}.html`), g.html);
written.add(`${slug}.html`);
const entry = {
slug: g.slug, kind: g.kind, label: g.label, titleFull: g.titleFull,
titleShort: g.titleShort, relatore: g.relatore, concetti: g.concetti.slice(0, 6),
chapter: num,
};
lessons.push(entry);
}
chapters.push({ num, title, sub, color: CH_COLORS[num - 1], lessons });
}
const byNum = new Map(chapters.map((c) => [c.num, c]));
const areas = AREAS.map(([slug, title, sub, color, chapterNums]) => ({
slug, title, sub, color,
chapters: chapterNums.map((n) => byNum.get(n)).filter(Boolean),
}));
// 2. prev/next nell'ordine in cui l'utente naviga: aree → capitoli → guide,
// che non coincide con l'ordine dei capitoli (le aree li raggruppano).
const flat = [];
for (const area of areas)
for (const ch of area.chapters)
for (const l of ch.lessons) { l.area = area.slug; flat.push(l); }
const linkTo = (l) => (l ? { slug: l.slug, area: l.area, title: l.titleShort } : null);
flat.forEach((l, i) => {
l.prev = linkTo(flat[i - 1]);
l.next = linkTo(flat[i + 1]);
});
await writeFile(join(OUT_DIR, 'nav.json'), JSON.stringify({ areas, count: flat.length }, null, 2));
// 3. rimozione dei frammenti orfani
let removed = 0;
for (const f of await readdir(pagesDir)) {
if (!written.has(f)) { await rm(join(pagesDir, f)); removed++; }
}
// 4. assets delle slide
const assetsSrc = join(srcRoot, 'sito', 'assets');
const copied = existsSync(assetsSrc) ? await copyTree(assetsSrc, join(OUT_DIR, 'assets')) : 0;
console.log(`Campus: ${flat.length} guide, ${areas.length} aree, ${copied} asset copiati, ${removed} pagine orfane rimosse.`);
}
await main();