Météo Exam v0.1.0 — calendrier des évaluations en bulletin météo

- API Express + SQLite : classes, évaluations, SSE, auth Bearer
- Front Vite + TS + Tailwind : frise 7 jours, Exam Cards, modales
- Capacitor Android : APK debug, CapacitorHttp natif
- Seed : classe Terminale S2 (code TERMS2)
This commit is contained in:
Mathis 2026-10-02 19:51:13 +02:00
commit 6d154ed5fb
92 changed files with 9574 additions and 0 deletions

130
server/src/seed.ts Normal file
View file

@ -0,0 +1,130 @@
/**
* Jeu de données de démonstration : la classe Terminale S2 avec deux semaines
* d'évaluations. Idempotent — relancer `npm run seed` ne duplique rien.
* Usage : npm run seed [-- --reset]
*/
import { existsSync, rmSync } from 'node:fs'
import { resolve } from 'node:path'
import { db, nowIso } from './db.js'
import { newToken } from './auth.js'
import type { WeatherKind } from '@meteo-exam/shared'
const DB_PATH = process.env['METEO_DB'] ?? resolve(process.cwd(), 'data/meteo-exam.db')
if (process.argv.includes('--reset')) {
for (const suffix of ['', '-wal', '-shm']) {
const file = `${DB_PATH}${suffix}`
if (existsSync(file)) rmSync(file)
}
console.log('Base effacée. Relancez `npm run seed`.')
process.exit(0)
}
const existing = db.prepare<[], { n: number }>('SELECT COUNT(*) AS n FROM classes').get()!
if (existing.n > 0) {
console.log('La base contient déjà des classes — seed ignoré. Utilisez --reset pour repartir de zéro.')
process.exit(0)
}
const CLASS_NAME = 'Terminale S2'
const LEVEL = 'Lycée'
const ADMIN_NICK = 'Thomas S.'
const CLASS_CODE = 'TERMS2'
const DAY_MS = 86_400_000
const isWeekend = (d: Date): boolean => d.getDay() === 0 || d.getDay() === 6
const midnight = new Date()
midnight.setHours(0, 0, 0, 0)
/**
* `dayOffset` compte les jours **scolaires** : 0 = aujourd'hui (même si c'est
* samedi, le seed reste lisible), puis on saute les weekends.
*/
function schoolDay(offset: number): Date {
const date = new Date(midnight)
let counted = 0
for (let step = 0; step < 90; step++) {
if (counted === offset) break
date.setTime(date.getTime() + DAY_MS)
if (!isWeekend(date)) counted++
}
return date
}
interface SeedExam {
dayOffset: number
hour: number
subject: string
teacher: string | null
room: string | null
chapter: string | null
durationMin: number
weight: number
kind: WeatherKind
note: string | null
}
const exams: SeedExam[] = [
{ dayOffset: 0, hour: 8, subject: 'Mathématiques', teacher: 'M. Roussel', room: 'B204', chapter: 'Intégrales, primitives et dérivation', durationMin: 120, weight: 4, kind: 'tempete', note: 'Calculatrice autorisée, calculatrice graphique non.' },
{ dayOffset: 0, hour: 14, subject: 'Histoire-Géographie', teacher: 'Mme Ferrand', room: 'A112', chapter: null, durationMin: 55, weight: 1, kind: 'vent', note: 'Interro surprise probable en début de cours.' },
{ dayOffset: 1, hour: 9, subject: 'Physique-Chimie', teacher: 'Mme Nguyen', room: 'Labo 3', chapter: 'Mécanique : second principe', durationMin: 60, weight: 2, kind: 'orage', note: 'Paquet de calculatrices fourni.' },
{ dayOffset: 1, hour: 15, subject: 'Anglais', teacher: 'M. Whitfield', room: 'C007', chapter: 'Compréhension orale — Unit 4', durationMin: 30, weight: 1, kind: 'vent', note: 'Rumeur d’une dictée surprise.' },
{ dayOffset: 2, hour: 8, subject: 'SVT', teacher: 'Mme Perrin', room: 'S215', chapter: 'Génétique des populations', durationMin: 90, weight: 3, kind: 'orage', note: 'TP suivi d’une synthèse écrite.' },
{ dayOffset: 3, hour: 10, subject: 'Philosophie', teacher: 'M. Aubert', room: 'A008', chapter: 'La raison et le doute', durationMin: 120, weight: 3, kind: 'orage', note: 'Dissertation, sujet au choix.' },
{ dayOffset: 3, hour: 16, subject: 'EPS', teacher: 'Mme Costa', room: 'Gymnase', chapter: null, durationMin: 60, weight: 1, kind: 'soleil', note: 'Basket obligatoire.' },
{ dayOffset: 4, hour: 9, subject: 'Mathématiques', teacher: 'M. Roussel', room: 'B204', chapter: 'Probabilités conditionnelles', durationMin: 60, weight: 2, kind: 'orage', note: null },
{ dayOffset: 5, hour: 10, subject: 'Français', teacher: 'Mme Dumas', room: 'A101', chapter: 'Guerre froide — contexte', durationMin: 60, weight: 1, kind: 'vent', note: 'Lecture suivie d’une étude de texte.' },
{ dayOffset: 6, hour: 8, subject: 'Examen blanc', teacher: null, room: 'Amphi A', chapter: 'Toutes matières', durationMin: 180, weight: 5, kind: 'tempete', note: 'Blanc général, convocation à 8h précises.' },
{ dayOffset: 7, hour: 9, subject: 'SVT', teacher: 'Mme Perrin', room: 'S215', chapter: 'Écosystèmes et bilan de matière', durationMin: 45, weight: 1, kind: 'brouillard', note: 'Brouillard : TP évalué, date encore floue.' },
{ dayOffset: 8, hour: 14, subject: 'Anglais', teacher: 'M. Whitfield', room: 'C007', chapter: 'Expression écrite — sujet d’invention', durationMin: 60, weight: 2, kind: 'orage', note: null },
{ dayOffset: 9, hour: 8, subject: 'Histoire-Géographie', teacher: 'Mme Ferrand', room: 'A112', chapter: 'La décolonisation', durationMin: 60, weight: 2, kind: 'orage', note: null },
{ dayOffset: 10, hour: 10, subject: 'Philosophie', teacher: 'M. Aubert', room: 'A008', chapter: 'Le sujet et l’autre', durationMin: 60, weight: 2, kind: 'orage', note: 'Plan imposé.' },
]
const seed = db.transaction(() => {
db.prepare('INSERT INTO classes (name, level, code, created_at) VALUES (?, ?, ?, ?)')
.run(CLASS_NAME, LEVEL, CLASS_CODE, nowIso())
const classId = db.prepare<[string], { id: number }>('SELECT id FROM classes WHERE code = ?')
.get(CLASS_CODE)!.id
const adminToken = newToken()
const adminId = db
.prepare('INSERT INTO members (class_id, nickname, role, token, created_at) VALUES (?, ?, ?, ?, ?)')
.run(classId, ADMIN_NICK, 'admin', adminToken, nowIso()).lastInsertRowid as number
// Quelques élèves : ils donneront du volume aux votes du radar plus tard.
for (const nick of ['Léa M.', 'Karim B.', 'Chloé D.', 'Inès R.', 'Lucas P.', 'Mehdi A.']) {
db.prepare('INSERT INTO members (class_id, nickname, role, token, created_at) VALUES (?, ?, ?, ?, ?)')
.run(classId, nick, 'student', newToken(), nowIso())
}
const insert = db.prepare(
`INSERT INTO exams
(class_id, subject, teacher, room, chapter, starts_at, duration_min, weight, kind, note, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
for (const e of exams) {
const date = schoolDay(e.dayOffset)
date.setHours(e.hour, 0, 0, 0)
const now = nowIso()
insert.run(
classId, e.subject, e.teacher, e.room, e.chapter,
date.toISOString(), e.durationMin, e.weight, e.kind, e.note,
adminId, now, now,
)
}
return { classId, adminToken }
})
const { adminToken } = seed()
console.log('Seed terminé.')
console.log(` Classe : ${CLASS_NAME} (${LEVEL})`)
console.log(` Code invite : ${CLASS_CODE}`)
console.log(` Admin : ${ADMIN_NICK}`)
console.log(` Évaluations : ${exams.length}`)
console.log(` Jeton admin : ${adminToken}`)