diff --git a/server.mjs b/server.mjs index fb8ec24..4ce822f 100644 --- a/server.mjs +++ b/server.mjs @@ -67,7 +67,7 @@ function resolvePath(url) { const publicCandidate = fileIfReadable(resolve(publicRoot, `.${requested}`)); if (publicCandidate && isInsideRoot(publicRoot, publicCandidate)) return publicCandidate; - return join(root, "index.html"); + return extname(requested) ? null : join(root, "index.html"); } const server = createServer((req, res) => { @@ -78,9 +78,15 @@ const server = createServer((req, res) => { return; } + const extension = extname(file); + const cacheControl = extension === ".html" + ? "no-store" + : extension === ".json" + ? "no-cache" + : "public, max-age=31536000, immutable"; res.writeHead(200, { "Content-Type": types[extname(file)] || "application/octet-stream", - "Cache-Control": "no-store" + "Cache-Control": cacheControl }); createReadStream(file).pipe(res); }); diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index 0e8f8e3..40b6463 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -200,11 +200,10 @@ test("toolbox module registry and modules expose expected behavior", async () => const tableUtils = await readFile("website/src/features/toolboxes/modules/tableUtils.js", "utf8"); const tableFormulaEngine = await readFile("website/src/features/toolboxes/modules/tableFormulaEngine.js", "utf8"); const timerModule = await readFile("website/src/features/toolboxes/modules/TimerModule.jsx", "utf8"); - const timerParts = await readFile("website/src/features/toolboxes/modules/TimerParts.jsx", "utf8"); const timerCountdown = await readFile("website/src/features/toolboxes/modules/TimerCountdown.jsx", "utf8"); const timerSharedControls = await readFile("website/src/features/toolboxes/modules/TimerSharedControls.jsx", "utf8"); const timerStopwatch = await readFile("website/src/features/toolboxes/modules/TimerStopwatch.jsx", "utf8"); - const timerBundle = `${timerModule}\n${timerParts}\n${timerCountdown}\n${timerSharedControls}\n${timerStopwatch}`; + const timerBundle = `${timerModule}\n${timerCountdown}\n${timerSharedControls}\n${timerStopwatch}`; const timerOptions = await readFile("website/src/features/toolboxes/modules/timerOptions.js", "utf8"); const calendarModule = await readFile("website/src/features/toolboxes/modules/CalendarModule.jsx", "utf8"); const calendarController = await readFile("website/src/features/toolboxes/modules/useCalendarController.js", "utf8"); @@ -221,14 +220,13 @@ test("toolbox module registry and modules expose expected behavior", async () => const calendarBundle = `${calendarModule}\n${calendarController}\n${calendarDraftControls}\n${calendarEventActions}\n${calendarLifecycle}\n${calendarModeChoice}\n${calendarContent}\n${calendarDraft}\n${calendarEventForm}\n${calendarEventControls}\n${calendarMonthDatePicker}\n${calendarViews}`; const calendarUtils = await readFile("website/src/features/toolboxes/modules/calendarUtils.js", "utf8"); const taskPlannerModule = await readFile("website/src/features/toolboxes/modules/TaskPlannerModule.jsx", "utf8"); - const taskPlannerItems = await readFile("website/src/features/toolboxes/modules/TaskPlannerItems.jsx", "utf8"); const taskPlannerBranch = await readFile("website/src/features/toolboxes/modules/TaskPlannerBranch.jsx", "utf8"); const taskPlannerItem = await readFile("website/src/features/toolboxes/modules/TaskPlannerItem.jsx", "utf8"); const taskPlannerItemPanels = await readFile("website/src/features/toolboxes/modules/TaskPlannerItemPanels.jsx", "utf8"); const taskPlannerReorder = await readFile("website/src/features/toolboxes/modules/useTaskPlannerReorder.js", "utf8"); const taskPlannerTopbar = await readFile("website/src/features/toolboxes/modules/TaskPlannerTopbar.jsx", "utf8"); const taskPlannerSettings = await readFile("website/src/features/toolboxes/modules/TaskPlannerSettings.jsx", "utf8"); - const taskPlannerBundle = `${taskPlannerItems}\n${taskPlannerBranch}\n${taskPlannerItem}\n${taskPlannerItemPanels}\n${taskPlannerReorder}\n${taskPlannerTopbar}\n${taskPlannerSettings}`; + const taskPlannerBundle = `${taskPlannerBranch}\n${taskPlannerItem}\n${taskPlannerItemPanels}\n${taskPlannerReorder}\n${taskPlannerTopbar}\n${taskPlannerSettings}`; const taskPlannerUtils = await readFile("website/src/features/toolboxes/modules/taskPlannerUtils.js", "utf8"); const goalTreeModule = await readFile("website/src/features/toolboxes/modules/GoalTreeModule.jsx", "utf8"); const goalTreeEditor = await readFile("website/src/features/toolboxes/modules/GoalTreeEditor.jsx", "utf8"); @@ -279,8 +277,6 @@ test("toolbox module registry and modules expose expected behavior", async () => ["CalendarModule.jsx", calendarModule], ["modules/index.jsx", moduleRegistry], ["GoalTreeParts.jsx", goalTreeParts], - ["TimerParts.jsx", timerParts], - ["TaskPlannerItems.jsx", taskPlannerItems], ["RandomizerModule.jsx", randomizerModule] ].forEach(([fileName, fileSource]) => { assert.ok(countLines(fileSource) <= 220, `${fileName} must remain a short orchestrator/barrel`); diff --git a/website/src/features/toolboxes/modules/RngSimulatorModule.jsx b/website/src/features/toolboxes/modules/RngSimulatorModule.jsx index 0c741cd..918acb2 100644 --- a/website/src/features/toolboxes/modules/RngSimulatorModule.jsx +++ b/website/src/features/toolboxes/modules/RngSimulatorModule.jsx @@ -146,7 +146,8 @@ function getToneFromDelta(value) { } export function RngSimulatorModule({ toolboxId, moduleId, context, editing = false, setEditing = () => {}, textExchangeOpen }) { - const data = context.normalizeRngSimulatorData(context.getModuleData(toolboxId, moduleId, {})); + const rawData = context.getModuleData(toolboxId, moduleId, {}); + const data = useMemo(() => context.normalizeRngSimulatorData(rawData), [context, rawData]); const textContent = context.moduleText?.rngSimulator || {}; const tabs = useMemo(() => [ { ...RNG_TABS[0], label: textContent.trackingTabLabel || RNG_TABS[0].label }, diff --git a/website/src/features/toolboxes/modules/TaskPlannerItems.jsx b/website/src/features/toolboxes/modules/TaskPlannerItems.jsx deleted file mode 100644 index 180d5c7..0000000 --- a/website/src/features/toolboxes/modules/TaskPlannerItems.jsx +++ /dev/null @@ -1,3 +0,0 @@ -// Rôle : conserve les exports publics historiques des sous-composants task planner. -export { TaskPlannerBranch } from "./TaskPlannerBranch.jsx"; -export { TaskPlannerItem } from "./TaskPlannerItem.jsx"; diff --git a/website/src/features/toolboxes/modules/TimerModule.jsx b/website/src/features/toolboxes/modules/TimerModule.jsx index fa18b52..b65b491 100644 --- a/website/src/features/toolboxes/modules/TimerModule.jsx +++ b/website/src/features/toolboxes/modules/TimerModule.jsx @@ -31,16 +31,20 @@ import { } from "./timerOptions.js"; export function TimerModule({ toolboxId, moduleId, context, textExchangeOpen }) { - const data = context.normalizeTimerData(context.getModuleData(toolboxId, moduleId, { activeTab: "stopwatch" })); + const rawData = context.getModuleData(toolboxId, moduleId, { activeTab: "stopwatch" }); + const data = useMemo(() => context.normalizeTimerData(rawData), [context, rawData]); const textContent = context.moduleText?.timer || {}; const activeTab = data.activeTab; const [nowMs, setNowMs] = useState(getNowMs); const countdownDraft = useDraftForm(DEFAULT_COUNTDOWN_FORM); + const clockActive = Boolean(data.stopwatch.startedAt || data.countdowns.some((countdown) => countdown.autoRefresh)); + useEffect(() => { + if (!clockActive) return undefined; const intervalId = window.setInterval(() => setNowMs(getNowMs()), 50); return () => window.clearInterval(intervalId); - }, []); + }, [clockActive]); const stopwatchElapsedMs = data.stopwatch.startedAt ? data.stopwatch.elapsedMs + Math.max(0, nowMs - data.stopwatch.startedAt) diff --git a/website/src/features/toolboxes/modules/TimerParts.jsx b/website/src/features/toolboxes/modules/TimerParts.jsx deleted file mode 100644 index 1e0b24c..0000000 --- a/website/src/features/toolboxes/modules/TimerParts.jsx +++ /dev/null @@ -1,4 +0,0 @@ -// Rôle : conserve les exports publics historiques des sous-composants timer. -export { CountdownControls, CountdownList } from "./TimerCountdown.jsx"; -export { EditableTimerLabel, TimerAlertModeSwitch, TimerValue, TimePartsInput } from "./TimerSharedControls.jsx"; -export { StopwatchControls, StopwatchLapList } from "./TimerStopwatch.jsx"; diff --git a/website/src/features/toolboxes/modules/calendarUtils.js b/website/src/features/toolboxes/modules/calendarUtils.js index 6e87aa4..8f1608c 100644 --- a/website/src/features/toolboxes/modules/calendarUtils.js +++ b/website/src/features/toolboxes/modules/calendarUtils.js @@ -191,11 +191,6 @@ export function getEventDurationMs(event) { return Number.isFinite(startMinutes) && Number.isFinite(endMinutes) && endMinutes > startMinutes ? (endMinutes - startMinutes) * MINUTE_MS : 0; } -export function getEventEndMs(event, startMs) { - const durationMs = getEventDurationMs(event); - return durationMs > 0 ? startMs + durationMs : startMs; -} - export function getCalendarAlertOffsets(event) { const rawOffsets = Array.isArray(event?.alertOffsetMinutes) ? event.alertOffsetMinutes : [event?.alertOffsetMinutes]; const offsets = [...new Set(rawOffsets.map(Number).filter((offset) => CALENDAR_ALERT_OFFSETS.includes(offset)))]; diff --git a/website/src/features/toolboxes/modules/useGoalTreeController.js b/website/src/features/toolboxes/modules/useGoalTreeController.js index f64b7d2..0024ea5 100644 --- a/website/src/features/toolboxes/modules/useGoalTreeController.js +++ b/website/src/features/toolboxes/modules/useGoalTreeController.js @@ -20,7 +20,8 @@ import { } from "./goalTreeUtils.js"; export function useGoalTreeController({ toolboxId, moduleId, context, editing, setEditing, openEditor }) { - const data = context.normalizeGoalTreeData(context.getModuleData(toolboxId, moduleId, { nodes: [] })); + const rawData = context.getModuleData(toolboxId, moduleId, { nodes: [] }); + const data = useMemo(() => context.normalizeGoalTreeData(rawData), [context, rawData]); const textContent = context.moduleText?.goalTree || {}; const treeOrientation = data.orientation || "horizontal"; const verticalView = treeOrientation === "vertical"; diff --git a/website/src/features/toolboxes/storage/modules/table.js b/website/src/features/toolboxes/storage/modules/table.js index 395733a..fae875b 100644 --- a/website/src/features/toolboxes/storage/modules/table.js +++ b/website/src/features/toolboxes/storage/modules/table.js @@ -1,20 +1,11 @@ // Rôle : normalise et compacte les cellules et libellés de l'outil table. +import { columnIndexToName } from "../../modules/tableFormulaEngine.js"; + function clampTableSize(value, fallback, max) { const parsed = Number.parseInt(value, 10); return Number.isInteger(parsed) ? Math.min(max, Math.max(1, parsed)) : fallback; } -function columnIndexToName(index) { - let value = Math.max(0, Number(index) || 0) + 1; - let name = ""; - while (value > 0) { - const remainder = (value - 1) % 26; - name = String.fromCharCode(65 + remainder) + name; - value = Math.floor((value - 1) / 26); - } - return name; -} - function tableCellAddress(rowIndex, columnIndex) { return `${columnIndexToName(columnIndex)}${rowIndex + 1}`; } diff --git a/website/src/hooks/useAppData.js b/website/src/hooks/useAppData.js index 0b66985..84a4e37 100644 --- a/website/src/hooks/useAppData.js +++ b/website/src/hooks/useAppData.js @@ -19,7 +19,10 @@ export function useAppData(route) { setSiteContentError(error.message); return null; }), - fetch("/data/games.json").then((response) => response.ok ? response.json() : { games: [] }).catch((error) => { + fetch("/data/games.json").then((response) => { + if (!response.ok) throw new Error("Impossible de charger la liste des jeux."); + return response.json(); + }).catch((error) => { setGamesError(error.message); return { games: [] }; }) diff --git a/website/src/hooks/useTimerAlerts.js b/website/src/hooks/useTimerAlerts.js deleted file mode 100644 index 899f85b..0000000 --- a/website/src/hooks/useTimerAlerts.js +++ /dev/null @@ -1,63 +0,0 @@ -// Rôle : surveille globalement les comptes à rebours actifs et déclenche les alertes internes autorisées. -import { useEffect, useRef } from "react"; -import { getCountdownTargetMs } from "../features/toolboxes/modules/timerUtils.js"; -import { moduleStorageKey } from "../features/toolboxes/storage/toolboxStorage.js"; - -function getVisibleToolboxIds(route, drawerGameId, links) { - const visibleIds = new Set(); - const toolboxMatch = route.match(/^\/toolbox\/([^/]+)/); - if (toolboxMatch?.[1]) visibleIds.add(toolboxMatch[1]); - const drawerToolboxId = drawerGameId ? links[drawerGameId] : ""; - if (drawerToolboxId) visibleIds.add(drawerToolboxId); - return visibleIds; -} - -function getAlertMessage(template, toolbox, countdown) { - return template - .replaceAll("{label}", countdown.label) - .replaceAll("{toolbox}", toolbox.name); -} - -export function useTimerAlerts({ toolboxes, moduleData, links, route, drawerGameId, normalizeTimerData, textContent, notify }) { - const previousTargetsRef = useRef(new Map()); - const lastTickRef = useRef(Date.now()); - - useEffect(() => { - const visibleToolboxIds = getVisibleToolboxIds(route, drawerGameId, links); - const alertMessageTemplate = textContent?.alertMessage || "Compte à rebours terminé : {label}"; - - function tick() { - const nowMs = Date.now(); - const lastTickMs = lastTickRef.current; - const nextTargets = new Map(); - - toolboxes.forEach((toolbox) => { - toolbox.modules.forEach((module) => { - if (module.type !== "timer") return; - - const data = normalizeTimerData(moduleData[moduleStorageKey(toolbox.id, module.id)]); - - data.countdowns.forEach((countdown) => { - if (countdown.alertMode === "off") return; - if (countdown.alertMode === "visible" && !visibleToolboxIds.has(toolbox.id)) return; - - const key = `${toolbox.id}:${module.id}:${countdown.id}`; - const targetMs = getCountdownTargetMs(countdown, nowMs); - const previousTargetMs = previousTargetsRef.current.get(key) || targetMs; - nextTargets.set(key, targetMs); - - if (!targetMs || previousTargetMs <= lastTickMs || previousTargetMs > nowMs) return; - notify(getAlertMessage(alertMessageTemplate, toolbox, countdown)); - }); - }); - }); - - previousTargetsRef.current = nextTargets; - lastTickRef.current = nowMs; - } - - tick(); - const intervalId = window.setInterval(tick, 500); - return () => window.clearInterval(intervalId); - }, [drawerGameId, links, moduleData, normalizeTimerData, notify, route, textContent, toolboxes]); -} diff --git a/website/src/router/RouteContent.jsx b/website/src/router/RouteContent.jsx index f07f595..76d39da 100644 --- a/website/src/router/RouteContent.jsx +++ b/website/src/router/RouteContent.jsx @@ -1,5 +1,5 @@ // Rôle : choisit la page à rendre selon la route active. -import { lazy, Suspense } from "react"; +import { lazy, Suspense, useEffect } from "react"; import { AboutPage } from "../pages/AboutPage.jsx"; import { HomePage } from "../pages/HomePage.jsx"; import { navigate } from "./hashRouter.js"; @@ -17,6 +17,13 @@ function RouteLoader() { return

Chargement...

; } +function UnknownRouteRedirect() { + useEffect(() => { + navigate("/"); + }, []); + return ; +} + export function RouteContent(props) { const { route } = props; if (route === "/") return ; @@ -27,6 +34,5 @@ export function RouteContent(props) { if (route.startsWith("/toolbox/")) return }>; if (route === "/games") return }>; if (route.startsWith("/games/")) return }>; - navigate("/"); - return null; + return ; }