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

108
scripts/gen-icons.mjs Normal file
View file

@ -0,0 +1,108 @@
/**
* Génère des icônes PWA simples (dégradé bleu + pastille blanche).
* Usage : node scripts/gen-icons.mjs
*/
import { writeFileSync, mkdirSync } from 'node:fs'
import { resolve } from 'node:path'
import { deflateSync } from 'node:zlib'
const OUT = resolve(import.meta.dirname, '../web/public')
mkdirSync(OUT, { recursive: true })
function crc32(buf) {
const table = []
for (let n = 0; n < 256; n++) {
let c = n
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
table[n] = c >>> 0
}
let crc = 0xffffffff
for (const b of buf) crc = table[(crc ^ b) & 0xff] ^ (crc >>> 8)
return (crc ^ 0xffffffff) >>> 0
}
function chunk(type, data) {
const len = Buffer.alloc(4)
len.writeUInt32BE(data.length)
const body = Buffer.concat([Buffer.from(type, 'ascii'), data])
const crc = Buffer.alloc(4)
crc.writeUInt32BE(crc32(body))
return Buffer.concat([len, body, crc])
}
function encodePng(width, height, rgba) {
const raw = Buffer.alloc(height * (1 + width * 4))
for (let y = 0; y < height; y++) {
raw[y * (1 + width * 4)] = 0
rgba.copy(raw, y * (1 + width * 4) + 1, y * width * 4, (y + 1) * width * 4)
}
const ihdr = Buffer.alloc(13)
ihdr.writeUInt32BE(width, 0)
ihdr.writeUInt32BE(height, 4)
ihdr[8] = 8
ihdr[9] = 6
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk('IHDR', ihdr),
chunk('IDAT', deflateSync(raw)),
chunk('IEND', Buffer.alloc(0)),
])
}
function drawIcon(size) {
const buf = Buffer.alloc(size * size * 4)
const c0 = [56, 189, 248]
const c1 = [99, 102, 241]
const cx = size / 2
const cy = size / 2
const rInner = size * 0.3
const rPad = size * 0.16
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4
const dx = x - cx
const dy = y - cy
const dist = Math.sqrt(dx * dx + dy * dy)
// Fond arrondi : on coupe les coins
let transparent = false
if (x < rPad && y < rPad) transparent = Math.hypot(rPad - x, rPad - y) > rPad
else if (x >= size - rPad && y < rPad) transparent = Math.hypot(x - (size - rPad), rPad - y) > rPad
else if (x < rPad && y >= size - rPad) transparent = Math.hypot(rPad - x, y - (size - rPad)) > rPad
else if (x >= size - rPad && y >= size - rPad) transparent = Math.hypot(x - (size - rPad), y - (size - rPad)) > rPad
if (transparent) {
buf[i + 3] = 0
continue
}
const t = (x + y) / (size * 2)
let r = c0[0] + (c1[0] - c0[0]) * t
let g = c0[1] + (c1[1] - c0[1]) * t
let b = c0[2] + (c1[2] - c0[2]) * t
if (dist < rInner) {
r = 255
g = 255
b = 255
} else if (dist < rInner + size * 0.025) {
r = Math.min(255, r + 50)
g = Math.min(255, g + 50)
b = Math.min(255, b + 50)
}
buf[i] = Math.round(r)
buf[i + 1] = Math.round(g)
buf[i + 2] = Math.round(b)
buf[i + 3] = 255
}
}
return buf
}
for (const size of [192, 512]) {
const png = encodePng(size, size, drawIcon(size))
writeFileSync(resolve(OUT, `icon-${size}.png`), png)
console.log(`icon-${size}.png (${png.length} octets)`)
}