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
This commit is contained in:
@@ -1,115 +0,0 @@
|
||||
// Parsing delle guide di studio (*.studio.md) del corso Biohacking Campus.
|
||||
// Le intestazioni H1 sono eterogenee per tipo di guida: ogni regex qui sotto
|
||||
// riconosce una famiglia e ne estrae titolo, relatore ed eventuale numero.
|
||||
|
||||
import { marked } from 'marked';
|
||||
|
||||
const H1_RE = /^#\s+Lezione\s+([0-9]+)\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
|
||||
const ACCEL_RE = /^#\s+Accelerator\s+([0-9]+)\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
|
||||
const LIVE_RE = /^#\s+Lezione\s+live\s+([0-9]+)\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
|
||||
const SLIDE_RE = /^#\s+Slide\s+—\s+(.*?)\s+—\s+Guida/u;
|
||||
const SPEAKER_RE = /^#\s+Intervento\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
|
||||
const CONSUL_RE = /^#\s+Consulenza\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
|
||||
const MASTER_RE = /^#\s+Masterclass\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
|
||||
// I titoli Q&A sono eterogenei: cattura tutto fino a "— Guida allo studio".
|
||||
const QA_RE = /^#\s+(.+?)\s+—\s+Guida allo studio\s*$/u;
|
||||
const BOLD_RE = /\*\*(.+?)\*\*/u;
|
||||
const PARTE_RE = /\((Parte\s+\d+)\)/u;
|
||||
|
||||
export function kindOf(slug) {
|
||||
if (slug.startsWith('accelerator')) return 'accelerator';
|
||||
if (slug.startsWith('lezione-live')) return 'live';
|
||||
if (slug.startsWith('slide-')) return 'slide';
|
||||
if (slug.startsWith('speaker-')) return 'speaker';
|
||||
if (slug.startsWith('consulenza-')) return 'consulenza';
|
||||
if (slug.startsWith('masterclass-')) return 'masterclass';
|
||||
if (slug.startsWith('qa-')) return 'qa';
|
||||
return 'lezione';
|
||||
}
|
||||
|
||||
const LABEL_PREFIX = {
|
||||
accelerator: 'A', live: 'LV', slide: 'SL', speaker: 'SP',
|
||||
consulenza: 'C', masterclass: 'MC', qa: 'QA',
|
||||
};
|
||||
|
||||
// Le immagini nei sorgenti puntano a "assets/..." relativo alla cartella del
|
||||
// sito statico; nella sezione campus sono servite dalla route protetta.
|
||||
export function rewriteAssets(html) {
|
||||
return html.replaceAll('src="assets/', 'src="/campus/assets/');
|
||||
}
|
||||
|
||||
function extractConcetti(bodyLines) {
|
||||
const out = [];
|
||||
let inSection = false;
|
||||
for (const ln of bodyLines) {
|
||||
if (ln.startsWith('## ')) {
|
||||
inSection = ln.trim().toLowerCase().startsWith('## concetti chiave');
|
||||
continue;
|
||||
}
|
||||
if (!inSection || !ln.trimStart().startsWith('- ')) continue;
|
||||
const m = BOLD_RE.exec(ln);
|
||||
if (m) out.push(m[1].trim().replace(/[.:;,]+$/, ''));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsa una guida.
|
||||
* @param {string} raw contenuto del file .studio.md
|
||||
* @param {string} slug nome file senza estensione
|
||||
* @param {number} pos posizione della guida nel capitolo (1-based)
|
||||
* @returns {{slug,kind,num,titleFull,titleShort,relatore,parte,label,concetti,html}}
|
||||
*/
|
||||
export function parseGuide(raw, slug, pos) {
|
||||
const lines = raw.split('\n');
|
||||
const h1 = lines[0];
|
||||
const kind = kindOf(slug);
|
||||
|
||||
let num = pos, titleFull, titleShort, relatore, parte = '';
|
||||
let m;
|
||||
if (kind === 'qa' && (m = QA_RE.exec(h1))) {
|
||||
titleFull = titleShort = m[1].trim();
|
||||
relatore = 'Q&A del corso';
|
||||
} else if (kind === 'masterclass' && (m = MASTER_RE.exec(h1))) {
|
||||
titleFull = titleShort = m[1].trim();
|
||||
relatore = m[2].trim();
|
||||
} else if (kind === 'consulenza' && (m = CONSUL_RE.exec(h1))) {
|
||||
titleFull = titleShort = m[1].trim();
|
||||
relatore = m[2].trim();
|
||||
} else if (kind === 'speaker' && (m = SPEAKER_RE.exec(h1))) {
|
||||
titleFull = titleShort = m[1].trim();
|
||||
relatore = m[2].trim();
|
||||
} else if (kind === 'slide' && (m = SLIDE_RE.exec(h1))) {
|
||||
titleFull = titleShort = m[1].trim();
|
||||
relatore = 'Slide ufficiali del corso';
|
||||
} else if (kind === 'live' && (m = LIVE_RE.exec(h1))) {
|
||||
num = Number(m[1]);
|
||||
const date = m[2].trim();
|
||||
titleFull = `Lezione live ${num} — ${date}`;
|
||||
titleShort = `Lezione live ${num}`;
|
||||
relatore = `${m[3]} · ${date}`;
|
||||
} else if (kind === 'accelerator' && (m = ACCEL_RE.exec(h1))) {
|
||||
num = Number(m[1]);
|
||||
const date = m[2].trim();
|
||||
titleFull = `Accelerator ${num} — ${date}`;
|
||||
titleShort = `Accelerator ${num}`;
|
||||
relatore = `${m[3]} · ${date}`;
|
||||
} else if (kind === 'lezione' && (m = H1_RE.exec(h1))) {
|
||||
num = Number(m[1]);
|
||||
titleFull = m[2];
|
||||
relatore = m[3];
|
||||
parte = PARTE_RE.exec(titleFull)?.[1] ?? '';
|
||||
titleShort = titleFull.replace(/\s*\(Parte\s+\d+\)/u, '').trim();
|
||||
} else {
|
||||
throw new Error(`H1 non riconosciuto in ${slug}: ${JSON.stringify(h1)}`);
|
||||
}
|
||||
|
||||
const bodyLines = lines.slice(1).filter((ln) => !ln.startsWith('> Sintesi di studio basata'));
|
||||
const html = rewriteAssets(marked.parse(bodyLines.join('\n').trim(), { async: false }));
|
||||
|
||||
const label = kind === 'lezione'
|
||||
? `L${num}${parte ? ` · ${parte}` : ''}`
|
||||
: `${LABEL_PREFIX[kind]}${num}`;
|
||||
|
||||
return { slug, kind, num, titleFull, titleShort, relatore, parte, label, concetti: extractConcetti(bodyLines), html };
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
// Struttura del corso Biohacking Campus: capitoli, colori e macro-aree.
|
||||
// Portata dal generatore statico del progetto sorgente (sito/genera_sito.py).
|
||||
// Quando si aggiungono guide, inserirle nel capitolo corrispondente.
|
||||
|
||||
export const CHAPTERS = [
|
||||
[1, 'Sonno e ritmi', 'Il fondamento: dormire e vivere in fase con la luce', [
|
||||
'lezione-01-ottimizzazione-del-sonno',
|
||||
]],
|
||||
[2, 'Movimento, peso, proteine', 'Corpo in movimento e composizione corporea', [
|
||||
'lezione-02-movimento-fitness-parte1',
|
||||
'lezione-02-movimento-fitness-parte2',
|
||||
'lezione-03-protocollo-fitness-longevita',
|
||||
'lezione-04-peso-corporeo',
|
||||
'lezione-05-proteine-creatina',
|
||||
]],
|
||||
[3, 'Freddo e respirazione', 'Stress ormetico e controllo del respiro', [
|
||||
'lezione-06-il-freddo',
|
||||
'lezione-07-la-respirazione',
|
||||
'lezione-08-respirazione-pianificazione',
|
||||
]],
|
||||
[4, 'Mente e HRV', 'Allenamento mentale e variabilità cardiaca', [
|
||||
'lezione-09-allenamento-mentale-parte1',
|
||||
'lezione-09-allenamento-mentale-parte2',
|
||||
'lezione-10-hrv',
|
||||
]],
|
||||
[5, 'Luce e recupero', 'Fotobiomodulazione, recupero attivo e dati', [
|
||||
'lezione-11-luce-fotobiomodulazione-parte1',
|
||||
'lezione-11-luce-fotobiomodulazione-parte2',
|
||||
'lezione-12-recupero-attivo',
|
||||
'lezione-13-monitoraggio-dati',
|
||||
]],
|
||||
[6, 'Consulenza e obiettivi', 'Dalla prima consulenza alla definizione degli obiettivi', [
|
||||
'lezione-14-prima-consulenza-cliente',
|
||||
'lezione-15-arte-obiettivo',
|
||||
]],
|
||||
[7, 'DNA, microbiota, epigenetica', 'Le basi molecolari della personalizzazione', [
|
||||
'lezione-16-introduzione-dna',
|
||||
'lezione-17-microbiota-metagenomica',
|
||||
'lezione-18-epigenetica-ormoni',
|
||||
]],
|
||||
[8, 'Longevità: analisi e alimentazione', 'Interpretare le analisi e mangiare per durare', [
|
||||
'lezione-19-analisi-longevita-parte1',
|
||||
'lezione-19-analisi-longevita-parte2',
|
||||
'lezione-20-alimentazione-longevita',
|
||||
]],
|
||||
[9, 'Performance cognitiva', 'Memoria, focus e nootropi', [
|
||||
'lezione-21-performance-cognitiva-parte1',
|
||||
'lezione-21-performance-cognitiva-parte2',
|
||||
]],
|
||||
[10, 'Funghi, integrazione, cervello', 'Micoterapia, integrazione e cervello sano', [
|
||||
'lezione-22-funghi-terapeutici',
|
||||
'lezione-23-integrazione-annuale-giornaliera',
|
||||
'lezione-24-longevita-cervello-sano',
|
||||
'lezione-25-focus-microbiota-plus',
|
||||
]],
|
||||
[11, 'Accelerator', 'Business e pratica professionale del biohacker (live mensili, Massimo Filippi)', [
|
||||
'accelerator-1-2025-11-11',
|
||||
'accelerator-2-2025-12-20',
|
||||
'accelerator-3-2026-01-28',
|
||||
'accelerator-4-2026-02-21',
|
||||
'accelerator-5-2026-03-28',
|
||||
'accelerator-6-2026-05-09',
|
||||
'accelerator-7-2026-06-06',
|
||||
]],
|
||||
[12, 'Lezioni live', 'Incontri live di approfondimento (Massimo Filippi)', [
|
||||
'lezione-live-1-2026-02-24',
|
||||
'lezione-live-2-2026-03-31',
|
||||
'lezione-live-3-2026-05-26',
|
||||
'lezione-live-4-2026-06-30',
|
||||
]],
|
||||
[13, 'Slide ufficiali', 'Deck del Master Monitoraggio Biometrico e focus tematici', [
|
||||
'slide-hrv',
|
||||
'slide-dna',
|
||||
'slide-microbiota-metagenomica',
|
||||
'slide-epigenetica-ormoni',
|
||||
'slide-nutrizione-precisione',
|
||||
'slide-focus-ormoni',
|
||||
'slide-focus-microbiota-plus',
|
||||
]],
|
||||
[14, 'Interventi speaker', 'Ospiti e focus tematici: genetica, ambiente, etica, consulenza, marketing, ormoni', [
|
||||
'speaker-dna-focus-moduli',
|
||||
'speaker-seipel-campi-elettromagnetici',
|
||||
'speaker-delnero-etica-integrita',
|
||||
'speaker-manfron-consulenza',
|
||||
'speaker-resta-marketing',
|
||||
'speaker-smeazzetto-focus-ormoni',
|
||||
]],
|
||||
[15, 'Consulenze', 'Casi studio reali: posizionamento e progetto professionale del biohacker (Massimo Filippi)', [
|
||||
'consulenza-2025-02-04-cristina-ruggeri',
|
||||
'consulenza-2025-02-10-umberto-riad',
|
||||
'consulenza-2025-02-11-stefano-pignatelli',
|
||||
'consulenza-2025-02-18-annalisa-di-lello',
|
||||
'consulenza-2025-02-24-barbara-fila-manenti',
|
||||
'consulenza-2025-02-27-eva-guerini',
|
||||
'consulenza-2025-03-03-luca-patella',
|
||||
'consulenza-2025-03-04-marco-gilberti',
|
||||
'consulenza-2025-06-10-guglielmo-cornelli',
|
||||
'consulenza-2025-06-16-liga-briviba',
|
||||
'consulenza-2025-07-02-lucia-griglio',
|
||||
'consulenza-2025-07-03-monia-gaudenzi',
|
||||
'consulenza-2025-07-08-simone-holl',
|
||||
'consulenza-2025-07-09-isabella-biagina-gioia',
|
||||
]],
|
||||
[16, 'Masterclass', 'Lezioni formative per professionisti: comunicazione e coaching (Luca Ruggeri), psicosomatica e benessere (Laura Pirotta)', [
|
||||
'masterclass-2025-10-08-luca-ruggeri',
|
||||
'masterclass-2025-11-08-laura-pirotta',
|
||||
'masterclass-2025-11-12-luca-ruggeri',
|
||||
'masterclass-2025-11-29-laura-pirotta',
|
||||
'masterclass-2025-12-10-luca-ruggeri',
|
||||
'masterclass-2026-01-10-laura-pirotta',
|
||||
'masterclass-2026-01-14-luca-ruggeri',
|
||||
'masterclass-2026-02-07-laura-pirotta',
|
||||
'masterclass-2026-02-11-luca-ruggeri',
|
||||
'masterclass-2026-03-07-laura-pirotta',
|
||||
'masterclass-2026-03-25-luca-ruggeri',
|
||||
'masterclass-2026-04-27-laura-pirotta',
|
||||
'masterclass-2026-05-18-laura-pirotta',
|
||||
'masterclass-2026-06-08-laura-pirotta',
|
||||
'masterclass-2026-06-23-laura-pirotta',
|
||||
]],
|
||||
[17, 'Q&A', "L'appuntamento ricorrente del corso: risposte agli argomenti della University e casi studio reali (Tony Manfron, Massimo Filippi, Daniela Nuti)", [
|
||||
'qa-01-tony-04',
|
||||
'qa-02-tony-05',
|
||||
'qa-03-tony-06',
|
||||
'qa-04-tony-07',
|
||||
'qa-05-tony-08',
|
||||
'qa-06-tony-09',
|
||||
'qa-07-tony-10',
|
||||
'qa-08-tony-11',
|
||||
'qa-09-tony-12',
|
||||
'qa-10-tony-13',
|
||||
'qa-11-tony-n14',
|
||||
'qa-12-tony-n15-ketosi-e-sport',
|
||||
'qa-13-massimo-27-02',
|
||||
'qa-14-massimo-6-03',
|
||||
'qa-15-simulazione-caso-studio-1-cliente-skinny-fat',
|
||||
'qa-16-tony-n15-sonno-su-cliente-56-anni-e-lettura',
|
||||
'qa-17-tony-n16-ottimizzazione-sonno-uomo-donna-ipe',
|
||||
'qa-18-tony-n17-idratazione-sali-caffeina-teina',
|
||||
'qa-19-tony-n-18-caso-calciatore-caso-perimenopausa',
|
||||
'qa-20-tony-n19-quando-il-caso-non-e-di-nostra-comp',
|
||||
'qa-21-simulazione-di-esame-caso-studio-calciatore',
|
||||
'qa-22-tony-n20-domande-generiche-su-sonno-e-strate',
|
||||
'qa-23-tony-n21-sirtuine-restrizione-calorica-caso',
|
||||
'qa-24-tony-n21-uso-dei-dispositivi-di-monitoraggio',
|
||||
'qa-25-tony-n22-organizzare-la-routine-del-cliente',
|
||||
'qa-26-business-13-06-24-massimo-filippi',
|
||||
'qa-27-daniela-nuti-sedentarieta-donna-e-ritenzione',
|
||||
'qa-28-tony-n23-lettura-dati-wearable-sonno-come-im',
|
||||
'qa-29-tony-n24-organizzare-un-ritiro-massaggi-d-i',
|
||||
'qa-30-daniela-15-07-2024',
|
||||
'qa-31-tony-n25-dispositivi-per-stress-concetti-sul',
|
||||
'qa-32-tony-n26-biohacking-uomo-sportivo-con-protes',
|
||||
'qa-33-tony-n27-perdita-di-peso-inestetismi-deontol',
|
||||
'qa-34-tony-n28-come-organizzare-la-tua-attivita-a',
|
||||
'qa-35-tony-n29-lavoro-approfondito-su-caso-studio',
|
||||
'qa-36-tony-n30-presentazione-docente-neurobiohacki',
|
||||
'qa-37-info-esame-sessione-settembre-2024',
|
||||
'qa-38-tony-n31-uso-del-monitoraggio-con-i-clienti',
|
||||
'qa-39-tony-n32-caso-studio-donna-69-anni-lavorato',
|
||||
'qa-40-tony-n33-discussione-su-integratori-caso-stu',
|
||||
'qa-41-tony-n34-caso-studio-post-trauma-emotivo-dis',
|
||||
'qa-42-tony-n35-strategie-di-digiuno-intermittente',
|
||||
'qa-43-tony-n36-prodotti-da-network-marketing-esper',
|
||||
'qa-44-tony-n37-approfondimento-sonno-per-ottimizza',
|
||||
'qa-45-tony-n38-considerazioni-sul-bw24-caso-studio',
|
||||
'qa-46-massimo-n39-caso-studio-da-risolvere-e-tecni',
|
||||
'qa-47-tony-n40-togliere-invece-che-aggiungere',
|
||||
'qa-48-tony-n41-come-monitorare-senza-dispositivi-i',
|
||||
'qa-49-tony-n42-caso-studio-performance-lavorativa',
|
||||
'qa-50-tony-n43-ore-di-sonno-inverno-estate-dieta-d',
|
||||
'qa-51-tony-n44-abbassamento-fc-2-casi-studio-su-ot',
|
||||
'qa-52-tony-n45-caso-studio-approfondito-cliente-no',
|
||||
'qa-53-daniela-n-46-integrazione',
|
||||
'qa-54-tony-n47-caso-studio-entropico-tecniche-di-r',
|
||||
'qa-55-tony-n48-caso-studio-uomo-iperattivo-imprend',
|
||||
'qa-56-tony-n49-caso-studio-donna-menopausa-precoce',
|
||||
'qa-57-tony-n50-caso-studio-donna-iperattiva-caso-s',
|
||||
'qa-58-tony-n51-caso-studio-senza-autorevolezza-lav',
|
||||
'qa-59-tony-n52-caso-uomo-75-anni-considerazioni-su',
|
||||
'qa-60-tony-n53-caso-studio-persona-standard-e-caso',
|
||||
'qa-61-daniela-nuti-n54',
|
||||
'qa-62-daniela-nuti-n55-19-03-2025',
|
||||
'qa-63-daniela-nuti-modalita-esame-27-marzo-2025',
|
||||
'qa-64-tony-n56-caso-studio-donna-sovrappeso-dibatt',
|
||||
'qa-65-tony-n57-interpretazione-dati-dispositivi-ca',
|
||||
'qa-66-tony-n58-aspetti-di-impostazione-del-busines',
|
||||
'qa-67-tony-n59-3-casi-studio-uomo-imprenditore-tes',
|
||||
'qa-68-tony-n60-carboidrati-e-proteine-piante-adatt',
|
||||
'qa-69-tony-n61-monitoraggio-whoop-caso-studio-atle',
|
||||
'qa-70-tony-n62-caso-studio-operatore-turnista',
|
||||
'qa-71-tony-n63-camionista-sonno-e-routine-approcci',
|
||||
'qa-72-daniela-nuti-n64-11-06-2025',
|
||||
'qa-73-tony-n65-ipertrofia-soggetto-giovane-strips',
|
||||
'qa-74-tony-n66-caso-studio-donna-lifestyle-mindset',
|
||||
'qa-75-tony-n67-caso-studio-donna-pre-menopausa-ges',
|
||||
'qa-76-tony-n68-concetti-di-fotobiomodulazione-caso',
|
||||
'qa-77-tony-n69-caso-studio-atleta-amatore-focus-co',
|
||||
'qa-78-tony-n-70-caso-studio-ex-olimpica-ora-impren',
|
||||
'qa-79-tony-n71-fare-del-biohacking-il-proprio-lavo',
|
||||
'qa-82-tony-n74-caso-studio-insonnia-da-stress-lavo',
|
||||
'qa-83-tony-n75-ragionamenti-di-marketing-e-posizio',
|
||||
'qa-84-tony-n76-caso-studio-uomo-broker-stressato-c',
|
||||
'qa-85-tony-n77-domande-varie-cortisolo-basso-ashwa',
|
||||
'qa-86-tony-n79-operaio-turnista-con-3-turni-divers',
|
||||
]],
|
||||
];
|
||||
|
||||
// Un tono per capitolo, indicizzato per numero di capitolo - 1.
|
||||
export const CH_COLORS = [
|
||||
'#6b8e6b', '#7a8b5a', '#8a8a4e', '#a08a56', '#a67c52',
|
||||
'#9c6b58', '#8a6f7d', '#6f7d8a', '#5f8a82', '#6d8a6a',
|
||||
'#5a6b8c', '#7c6b9c', '#8a7d5a', '#7a8a86', '#a67c8a',
|
||||
'#5f8a7d', '#b07a4e',
|
||||
];
|
||||
|
||||
// Macro-aree: raggruppano i capitoli nelle pagine indice della sezione.
|
||||
export const AREAS = [
|
||||
['corso-base', 'Corso base',
|
||||
'Le 25 lezioni fondamentali del corso: sonno e ritmi, movimento e peso, freddo e respiro, mente e HRV, luce e recupero, consulenza, basi molecolari, longevità, performance cognitiva, integrazione.',
|
||||
'#6b8e6b', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]],
|
||||
['slide', 'Slide ufficiali',
|
||||
'I deck del Master in Monitoraggio Biometrico e i focus tematici, con i grafici, le tabelle e i referti originali.',
|
||||
'#8a7d5a', [13]],
|
||||
['live-e-ospiti', 'Live e ospiti',
|
||||
'Gli incontri live mensili (Accelerator), le lezioni live di approfondimento clinico e gli interventi degli ospiti del corso.',
|
||||
'#7c6b9c', [11, 12, 14]],
|
||||
['consulenze-masterclass', 'Consulenze e Masterclass',
|
||||
'Casi studio reali di consulenza e le masterclass formative per professionisti: comunicazione, coaching, psicosomatica.',
|
||||
'#a67c8a', [15, 16]],
|
||||
['qa', 'Q&A',
|
||||
"L'appuntamento ricorrente del corso: risposte agli argomenti della University e casi studio reali portati dagli studenti.",
|
||||
'#b07a4e', [17]],
|
||||
];
|
||||
|
||||
// Sottocartella del progetto sorgente in cui cercare il file <slug>.studio.md.
|
||||
export function sourceDirFor(slug) {
|
||||
if (slug.startsWith('accelerator')) return 'accelerator';
|
||||
if (slug.startsWith('lezione-live')) return 'lezioni-live';
|
||||
if (slug.startsWith('slide-')) return 'slide';
|
||||
if (slug.startsWith('speaker-')) return 'speaker';
|
||||
if (slug.startsWith('consulenza-')) return 'consulenze';
|
||||
if (slug.startsWith('masterclass-')) return 'masterclass';
|
||||
if (slug.startsWith('qa-')) return 'qa';
|
||||
return 'studio';
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user