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)
This commit is contained in:
Mathis 2026-10-04 17:05:56 +02:00
parent 6d154ed5fb
commit 246f989bdc
16 changed files with 782 additions and 86 deletions

BIN
web/public/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
web/public/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,31 @@
{
"name": "Météo Exam",
"short_name": "Météo Exam",
"description": "Le calendrier des évaluations scolaires, en bulletin météo interactif.",
"start_url": "/",
"display": "standalone",
"background_color": "#020617",
"theme_color": "#020617",
"orientation": "portrait",
"lang": "fr",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}

39
web/public/sw.js Normal file
View file

@ -0,0 +1,39 @@
/**
* Service worker Météo Exam.
* - Cache les assets statiques (app shell) pour un démarrage hors ligne.
* - Les appels API passent en network-first : les données restent fraîches.
*/
const CACHE_NAME = 'meteo-exam-v2'
const STATIC_ASSETS = ['/', '/index.html', '/manifest.webmanifest']
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)),
)
self.skipWaiting()
})
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))),
),
)
self.clients.claim()
})
self.addEventListener('fetch', (event) => {
const { request } = event
const url = new URL(request.url)
// API et flux SSE : toujours réseau, pas de cache.
if (url.pathname.startsWith('/api/')) return
// Assets statiques : cache d'abord, réseau en secours.
if (request.method === 'GET' && (url.origin === self.location.origin || url.hostname === 'localhost')) {
event.respondWith(
caches.match(request).then((cached) => cached ?? fetch(request).catch(() => caches.match('/index.html'))),
)
}
})