- 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)
130 lines
6.6 KiB
TypeScript
130 lines
6.6 KiB
TypeScript
/**
|
||
* 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}`)
|