6301f521fd
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
91 lines
3.4 KiB
TypeScript
91 lines
3.4 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { durata, dataBreve, dataOra, ultimi, kpiSport, perSettimana, riepilogoAtleti } from '../src/lib/stress-index/sport';
|
|
import type { Athlete, Session } from '../src/lib/stress-index/data';
|
|
|
|
const RIF = '2026-07-12';
|
|
|
|
const sess = (id: string, athleteId: string, startedAt: string, trimp: number): Session =>
|
|
({ id, athleteId, startedAt, durationSec: 1800, sport: null, hrAvg: 120, hrMax: 150, trimp, dfaA1: 1.2, rpe: 5 });
|
|
|
|
const athletes: Athlete[] = [
|
|
{ clientId: 'a', name: 'A. Uno', sport: 'Corsa' },
|
|
{ clientId: 'b', name: 'B. Due', sport: null },
|
|
];
|
|
|
|
// Ordinate dalla più recente, come le espone data.ts
|
|
const sessions: Session[] = [
|
|
sess('s1', 'a', '2026-07-12T09:00', 40),
|
|
sess('s2', 'a', '2026-07-08T09:00', 30),
|
|
sess('s3', 'a', '2026-07-02T09:00', 60), // settimana precedente
|
|
sess('s4', 'b', '2026-06-20T09:00', 20), // oltre i 30 giorni? no: 22 giorni prima
|
|
sess('s5', 'b', '2026-04-01T09:00', 15), // fuori da tutte le finestre
|
|
];
|
|
|
|
describe('formattazioni', () => {
|
|
it('durata: ore, minuti e secondi', () => {
|
|
expect(durata(4860)).toBe('1h 21m');
|
|
expect(durata(3275)).toBe('54m 35s');
|
|
expect(durata(5)).toBe('5s');
|
|
});
|
|
|
|
it('date in forma breve ed estesa', () => {
|
|
expect(dataBreve('2026-07-30T11:27')).toBe('30 lug 2026');
|
|
expect(dataOra('2026-07-30T11:27')).toBe('30 lug 2026 alle 11:27');
|
|
});
|
|
});
|
|
|
|
describe('finestre temporali', () => {
|
|
it('gli ultimi 7 giorni includono il giorno di riferimento ed escludono il settimo indietro', () => {
|
|
expect(ultimi(sessions, 7, RIF).map((s) => s.id)).toEqual(['s1', 's2']);
|
|
});
|
|
|
|
it('gli ultimi 30 giorni allargano a chi ha misurato nel mese', () => {
|
|
expect(ultimi(sessions, 30, RIF).map((s) => s.id)).toEqual(['s1', 's2', 's3', 's4']);
|
|
});
|
|
});
|
|
|
|
describe('kpi', () => {
|
|
it('conta sessioni, carico settimanale e atleti attivi nel mese', () => {
|
|
expect(kpiSport(sessions, RIF)).toEqual([
|
|
{ label: 'Sessioni totali', value: 5 },
|
|
{ label: 'Questa settimana', value: 2, note: 'ultimi 7gg' },
|
|
{ label: 'TRIMP medio', value: 70, note: 'settimanale' },
|
|
{ label: 'Atleti attivi', value: 2, note: 'ultimi 30gg' },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('sessioni per settimana', () => {
|
|
const righe = perSettimana(sessions, 4, RIF);
|
|
|
|
it('produce una riga per settimana, la più recente per ultima', () => {
|
|
expect(righe).toHaveLength(4);
|
|
// 2026-07-12 è una domenica: la sua settimana parte lunedì 06/07
|
|
expect(righe[righe.length - 1].settimana).toBe('06/07');
|
|
});
|
|
|
|
it('conta le sessioni nella settimana giusta', () => {
|
|
const perEtichetta = Object.fromEntries(righe.map((r) => [r.settimana, r.sessioni]));
|
|
expect(perEtichetta['06/07']).toBe(2); // s1 e s2
|
|
expect(perEtichetta['29/06']).toBe(1); // s3
|
|
});
|
|
});
|
|
|
|
describe('riepilogo atleti', () => {
|
|
const riepilogo = riepilogoAtleti(sessions, athletes, RIF);
|
|
|
|
it('somma il carico dei 7 giorni e riporta ultima sessione e conteggio mensile', () => {
|
|
expect(riepilogo[0]).toEqual({
|
|
clientId: 'a', name: 'A. Uno', sport: 'Corsa',
|
|
sessioni30: 3, trimp7: 70, ultimoTrimp: 40,
|
|
ultimaSessione: '2026-07-12T09:00', trend: 'stabile',
|
|
});
|
|
});
|
|
|
|
it('resta stabile quando lo scarto fra le due settimane è minimo', () => {
|
|
// b non ha nulla negli ultimi 7 giorni né nei 7 precedenti → nessuno scarto, resta stabile
|
|
expect(riepilogo[1].trend).toBe('stabile');
|
|
expect(riepilogo[1].sessioni30).toBe(1);
|
|
});
|
|
});
|