meteo-exam/server/src/seed.ts
Mathis 246f989bdc v0.2.0 — config admin (météo forcée, flash info, édition classe) + PWA
- DB : table day_weather (override admin par jour), flash_message sur classes
- API : PUT/DELETE /api/config/day-weather, PUT /api/config/flash, PATCH /api/config/class
- SSE : day_weather.updated/deleted, flash.updated, class.updated
- Front : bandeau flash info, indicateur 'forcé' sur la frise, modale météo admin
- Onglet Plus : panneau admin (flash info + édition classe), tout admin modifiable
- PWA : manifest.webmanifest, service worker, meta Apple, icônes 192/512
- Web version accessible sur iOS via Safari (Ajouter à l'écran d'accueil)
2026-10-04 17:05:56 +02:00

130 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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, flash_message, created_at) VALUES (?, ?, ?, ?, ?)')
.run(CLASS_NAME, LEVEL, CLASS_CODE, 'Bon courage pour les révisions cette semaine ! 🌤️', 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}`)