# Storage Schema Ce document décrit les données persistées dans IndexedDB pour les toolboxes. Il doit être mis à jour à chaque ajout ou modification d'outil. ## IndexedDB Base : `sokkog` Object stores : - `kv` : données globales simples. - `modules` : données des outils, séparées par toolbox et par module. Clés principales dans `kv` : ```json { "toolboxes": [], "links": {}, "setting:toolboxOrder": [], "setting:storageBudgetBytes": 536870912, "setting:toastPosition": "right" } ``` Entrée type dans `modules` : ```json { "key": "tbx1:mod1", "data": {} } ``` ## Toolbox Les toolboxes sont stockées dans `kv/toolboxes`. ```json { "id": "tbx1", "name": "Monster Hunter Wilds", "gameId": "mhwilds", "icon": "/static/img/toolbox-icons/toolbox.png", "modules": [ { "id": "mod1", "type": "checklist", "title": "Armures" } ], "layout": "two", "updatedAt": "2026-07-25T12:00:00.000Z" } ``` Notes : - `gameId` est vide pour une toolbox libre. - `icon` est utilisé uniquement pour les toolboxes libres. - `layout` vaut généralement `one` ou `two`. - Les données lourdes des outils ne sont pas stockées dans la toolbox, mais dans `modules`. ## Liens Jeu / Toolbox Les associations entre pages jeux et toolboxes sont stockées dans `kv/links`. ```json { "mhwilds": "tbx1", "diablo4": "tbx2" } ``` ## Outil Bloc Notes Type : `notepad` ```json { "html": "

Notes rapides...

", "text": "Notes rapides...", "updatedAt": "2026-07-29T12:00:00.000Z", "drawingMode": "permanent", "drawings": { "strokes": [ { "id": "stroke1", "color": "#f6c453", "width": 4, "points": [ { "x": 12.5, "y": 48 }, { "x": 24, "y": 52.5 } ] } ] } } ``` Stockage compact : - l'ancien format `{ "text": "..." }` reste accepté et est converti en HTML riche au prochain enregistrement ; - `html` est nettoyé avec une allowlist de balises et de couleurs ; - `text` conserve une version texte pour les exports et les fallbacks ; - `updatedAt` est omis si vide ou invalide ; - `drawingMode` vaut `temporary` par défaut et est omis dans ce cas ; - `drawings` est stocké uniquement si `drawingMode` vaut `permanent` et qu'au moins un trait valide existe ; - les dessins temporaires du bloc-notes sont conservés dans `sessionStorage`, pas dans IndexedDB ; - si le texte, la date et les dessins permanents sont vides, l'entrée de module peut être supprimée. ## Outil Checklist Type : `checklist` Format simple, sans catégorie : ```json { "items": [ { "id": "item1", "label": "Potion", "qtyTarget": 10, "qtyCurrent": 3 } ] } ``` Format avec catégories : ```json { "hideCompletedSections": true, "hideCompletedSectionsFully": false, "sections": [ { "id": "section1", "title": "Rey Dau alpha", "hideWhenComplete": false, "collapsed": false, "items": [ { "id": "item1", "label": "Casque", "qtyTarget": 1, "qtyCurrent": 0 } ] } ] } ``` Stockage compact : - `qtyTarget` est omis si la valeur vaut `1`. - `qtyCurrent` est omis si la valeur vaut `0`. - `hideCompletedSections` est omis si `false`. - `hideCompletedSectionsFully` est omis si `false`. - `title` est omis si vide. - `hideWhenComplete` est omis si non défini. - `collapsed` est omis si `false`. - les sections sans item sont ignorées. ## Outil Planificateur D'équipements Type : `equipmentPlanner` ```json { "types": [ { "id": "type1", "title": "Armes", "icon": "sword", "collapsed": false } ], "equipments": [ { "id": "equipment1", "typeId": "type1", "name": "Épée runique", "icon": "sword", "active": true, "obtain": "Forge après le boss", "characteristics": [ { "id": "trait1", "category": "Stats", "icon": "sword", "name": "Force", "value": 12 } ], "socketItems": [ { "id": "socket1", "name": "Joyau attaque", "shape": "jewel", "color": "yellow", "bonuses": [ { "id": "bonus1", "category": "Compétences", "name": "Attaque", "value": 1 } ] } ], "socketLinks": [ { "id": "link1", "fromSocketItemId": "socket1", "toSocketItemId": "socket2" } ], "materials": [ { "id": "material1", "name": "Minerai rare", "qty": 4 } ], "categoryOrder": ["Stats", "Compétences"], "collapsedCategories": [] } ], "typeOrder": ["type1"] } ``` Stockage compact : - les types sans équipement sont ignorés ; - `icon` des types vaut `boot`, `chest-armor`, `helmet`, `glove`, `shield`, `sword`, `gun`, `earring`, `necklace` ou `ring` ; - `icon` des équipements utilise la même liste et pilote l'icône affichée sur la ligne de l'équipement ; - `active` est omis quand la valeur vaut `true` ; - `obtain`, `characteristics`, `socketItems`, `socketLinks`, `materials`, `categoryOrder` et `collapsedCategories` sont omis si vides ; - `collapsed` est omis si `false` ; - `qty` des matériaux est normalisé à minimum `1` ; - les caractéristiques et bonus sans nom sont ignorés ; - `icon` des caractéristiques directes vaut `sword` ou `shield` ; - `shape` vaut `ball` ou `jewel`, avec `jewel` par défaut ; - `color` vaut `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `teal`, `cyan`, `blue`, `indigo`, `violet`, `purple`, `pink`, `rose`, `white`, `gray` ou `black` ; - les liens de sertissage invalides, dupliqués ou hors équipement sont ignorés ; - seuls les équipements actifs contribuent au résumé. ## Outil Images Type : `images` ```json { "images": [ { "id": "image1", "label": "Map zone nord", "dataUrl": "data:image/webp;base64,..." } ] } ``` Stockage compact : - `label` est facultatif. - `label` est omis si vide. - `createdAt` n'est pas stocké. - Le nom de fichier original n'est pas stocké. ## Outil Annotation d'images Type : `imageAnnotation` ```json { "image": "data:image/webp;base64,...", "markers": [ { "id": "marker1", "x": 42.5, "y": 68, "label": "Entrée" } ], "drawings": { "strokes": [ { "id": "stroke1", "color": "#f6c453", "width": 4, "points": [ { "x": 18.5, "y": 32 }, { "x": 24, "y": 40.5 } ] } ] } } ``` Stockage compact : - un seul visuel par outil. - `x` et `y` sont des pourcentages entre `0` et `100`. - `label` est facultatif. - `label` est omis si vide. - `drawings` est facultatif et omis si aucun trait valide n'existe. - les points des dessins sont stockés en pourcentages entre `0` et `100` pour rester alignés en aperçu réduit et en plein écran. - les dessins sont éditables uniquement depuis la modale plein écran ; l'aperçu réduit les affiche en lecture seule. - le mode temporaire des dessins d'image utilise `sessionStorage` et ne modifie pas `drawings` dans IndexedDB tant que l'utilisateur ne repasse pas en stockage permanent. ## Outil Timer Type : `timer` Structure initiale : ```json { "activeTab": "stopwatch", "scrollResults": true, "sortResults": true, "stopwatch": { "elapsedMs": 12000, "startedAt": 1760000000000, "laps": [ { "id": "timer1", "label": "Phase 1", "elapsedMs": 9000 } ] }, "countdowns": [ { "id": "timer2", "label": "Boss", "type": "daily_time", "alertMode": "visible", "autoRefresh": true, "time": "12:03:00", "targetAt": 1760000000000 }, { "id": "timer3", "label": "Event", "type": "time_pattern", "pattern": "X:45:00" }, { "id": "timer4", "label": "Event", "type": "interval", "autoRefresh": true, "intervalMs": 1800000, "startMode": "time", "startTime": "06:00:00", "anchorAt": 1760000000000, "targetAt": 1760001800000 } ] } ``` Types de compte à rebours : - `duration` : durée ponctuelle, avec `durationMs` et `targetAt`. - `daily_time` : occurrence à une heure précise, avec `time` au format `HH:MM:SS`, `targetAt` et `autoRefresh` optionnel. - `time_pattern` : prochaine occurrence automatique correspondant à un pattern `HH:MM:SS`, où chaque segment peut valoir `X`. - `interval` : intervalle basé sur `intervalMs`, `anchorAt`, `targetAt`, `startMode`, `startTime` et `autoRefresh` optionnel. `startMode` vaut `now` ou `time`; `startTime` est présent uniquement avec `time`. `autoRefresh` n'est conservé que si l'intervalle dure au moins 5 minutes ; les anciens intervalles sans `targetAt` restent interprétés comme répétables. Pour `time_pattern`, un segment `X` situé après un segment plus large fixé cible le premier instant de l'occurrence. Exemple : `X:24:X` cible `hh:24:00` et se répète toutes les heures. Stockage compact : - `activeTab` est omis si la valeur vaut `stopwatch`. - `scrollResults` est omis si `false`. - `sortResults` est omis si `false`. - `stopwatch` est omis tant que le chrono est à zéro et qu'aucune étape n'est enregistrée. - `elapsedMs` est omis si `0`. - `startedAt` est omis si le chrono n'est pas lancé. - `laps` est omis tant qu'aucune étape n'est enregistrée. - `countdowns` est omis tant qu'aucun compte à rebours n'est configuré. - `countdowns[].alertMode` vaut `off`, `visible` ou `site`, et est omis si `off`. - `countdowns[].autoRefresh` est omis si `false`. - `countdowns[].startMode` et `countdowns[].startTime` sont omis pour les intervalles qui démarrent à la création. - L'activation d'une alerte est bloquée dans l'interface si la fréquence configurée est inférieure à 5 minutes pour les comptes à rebours récurrents (`time_pattern` et `interval`). Le blocage ne dépend pas du temps restant avant la prochaine échéance. ## Outil Task Planner Type : `taskPlanner` ```json { "weeklyResetDay": 1, "resetTime": "00:00", "lastResetAt": 1760000000000, "hideCompleted": true, "categoryOrder": ["Raid", "Farm"], "collapsedCategories": ["Farm"], "tasks": [ { "id": "task1", "title": "Quête 1", "description": "Préparer la route.", "category": "Raid", "type": "daily", "dailyResetTime": "06:30", "checked": true, "checkedAt": 1760000000000 }, { "id": "task2", "title": "Quête 2", "type": "weekly", "weeklyResetDay": 3 } ], "relations": [ { "id": "relation1", "fromTaskId": "task1", "toTaskId": "task2" }, { "id": "relation2", "fromTaskId": "task2", "toTaskId": "task1", "prerequisite": true } ] } ``` Notes : - `type` vaut `unique`, `daily` ou `weekly`. - `weeklyResetDay` suit les jours JavaScript : `0` dimanche, `1` lundi, ..., `6` samedi. - `resetTime` est global à l'outil et vaut `HH:MM`. - `hideCompleted` masque les tâches effectuées à l'affichage, ainsi que les catégories dont toutes les tâches sont effectuées. - `dailyResetTime` sur une tâche quotidienne surcharge l'heure globale. - `weeklyResetDay` sur une tâche hebdo surcharge le jour global. - `category` sur une tâche racine est facultatif. Les tâches enfants héritent de la catégorie effective de leur parent et ne stockent pas de catégorie propre. - Les tâches catégorisées sont regroupées par catégorie au niveau racine. - `categoryOrder` stocke l'ordre d'affichage des catégories utilisées. Les catégories absentes des tâches sont retirées à la normalisation. - `collapsedCategories` stocke les noms des catégories repliées. - `relations` stocke les parents directionnels. `fromTaskId` désigne la tâche enfant, `toTaskId` son parent. - une tâche ne conserve qu'un seul parent. - `prerequisite: true` marque aussi ce parent comme pré requis. - Les pré requis sont non bloquants : ils affichent un avertissement si le parent requis n'est pas effectué. - Le drag & drop conserve l'ordre manuel, et peut aussi ajouter une tâche racine à une catégorie existante ou la sortir vers les tâches sans catégorie. Stockage compact : - `weeklyResetDay` global est omis si la valeur vaut `1`. - `resetTime` est omis si la valeur vaut `00:00`. - `lastResetAt` est omis si la valeur vaut `0`. - `hideCompleted` est omis si `false`. - `description` est omis si vide. - `category` est omis si vide ou si la tâche a un parent. - `categoryOrder` est omis si aucune catégorie n'est utilisée. - `collapsedCategories` est omis si aucune catégorie n'est repliée. - `checked` et `checkedAt` sont omis si la tâche n'est pas effectuée. - `dailyResetTime` unitaire est omis si non défini ou identique à l'heure globale. - `weeklyResetDay` unitaire est omis si non défini. - `relations` est omis si vide. - `relations[].prerequisite` est omis si `false`. ## Outil Liens Type : `links` ```json { "links": [ { "id": "link1", "title": "Build rogue", "url": "https://example.com/build" } ] } ``` Stockage compact : - `title` est facultatif. - `title` est omis si vide. - `url` est obligatoire. ## Outil Compteurs Type : `counters` ```json { "counters": [ { "id": "counter1", "label": "Victoire", "value": 12 }, { "id": "counter2", "label": "Défaite", "value": -2 } ] } ``` Notes : - `value` est numérique. - Les valeurs négatives sont acceptées. - Les compteurs sans libellé ne sont pas conservés. ## Outil Combos Type : `combos` ```json { "device": "playstation", "categoryOrder": ["Neutral"], "collapsedCategories": ["Advanced"], "combos": [ { "id": "combo0", "name": "Anti air", "device": "playstation", "inputs": [ [ { "kind": "direction", "value": "down", "holdMs": 2000 }, { "kind": "button", "value": "triangle", "hold": true } ] ] }, { "id": "combo1", "name": "Hadoken", "category": "Neutral", "device": "playstation", "inputs": [ [{ "kind": "direction", "value": "down" }], [{ "kind": "direction", "value": "down-forward" }], [{ "kind": "button", "value": "cross" }] ] } ] } ``` Notes : - `device` vaut `playstation`, `xbox`, `switch`, `n64` ou `keyboardMouse`. - `combos[].device` peut figer le périphérique de rendu d'un combo, afin qu'il conserve ses couleurs si l'outil change de périphérique. - `combos[].category` est facultatif ; les catégories sont déduites automatiquement de cette valeur. - `inputs` est une liste d'étapes successives. - chaque étape contient une ou plusieurs entrées simultanées. - `kind` vaut `button`, `direction`, `key` ou `mouse`. - `hold` est facultatif et indique qu'une entrée doit être maintenue sans durée précise. - `holdMs` est facultatif et indique qu'une entrée doit être maintenue pendant cette durée en millisecondes ; l'interface limite la saisie à des secondes entières de `1` à `99`. - `categoryOrder` stocke l'ordre d'affichage des catégories utilisées. - `collapsedCategories` stocke les noms des catégories repliées. - les catégories restent locales à l'outil ; l'utilisateur peut ajouter plusieurs outils Combos pour séparer des usages. Stockage compact : - `device` est omis si la valeur vaut `playstation`. - `category` est omise si vide. - `hold` est omis si absent ou faux. - `holdMs` est omis si absent ou nul. - `categoryOrder` est omis si aucune catégorie n'est utilisée. - `collapsedCategories` est omis si aucune catégorie n'est repliée. - `combos` est omis si aucun combo valide n'existe. - les combos sans nom et sans touche sont supprimés. - les catégories et noms de combo sont limités à 80 caractères. - chaque combo conserve au maximum 64 étapes. ## Outil Calculateur Type : `calculator` ```json { "scrollResults": true, "entries": [ { "id": "calc1", "label": "Lingots de fer", "value": 100 }, { "id": "calc2", "parentId": "calc1", "label": "Minerais de fer", "value": 500 } ] } ``` Stockage compact : - `scrollResults` est omis si `false`. - `parentId` est omis si vide. - `label` peut être vide. - `value` est numérique. - Si un `parentId` ne pointe vers aucune entrée existante, il est réinitialisé à vide. ## Outil Tableau Type : `table` ```json { "rows": 12, "columns": 8, "cells": { "A1": "Texte libre", "B1": "100", "C1": "=A1+B1", "D1": "=(B1+20)/2" }, "rowLabels": { "0": "Boss", "1": "Phase 2" }, "columnLabels": { "0": "Item", "1": "Quantité" } } ``` Stockage compact : - `rows` est omis si la valeur vaut `10`. - `columns` est omis si la valeur vaut `6`. - `cells` conserve uniquement les cellules non vides et dans les limites du tableau. - les espaces saisis dans une cellule non vide sont conservés. - `rows` est limité entre `1` et `50`. - `columns` est limité entre `1` et `20`. - Les valeurs de cellules sont stockées sous forme de texte brut, formules incluses. - Les résultats calculés ne sont pas persistés. - `rowLabels` et `columnLabels` conservent uniquement les intitulés visuels personnalisés. - Les intitulés personnalisés n'impactent pas les références de formule, qui restent basées sur les adresses `A1`, `C2`, etc. ## Export L'export d'une toolbox remappe les identifiants pour produire des IDs courts et indépendants. Exemple : ```json { "toolbox": { "id": "t1", "name": "MHW", "modules": [ { "id": "m1", "type": "checklist", "title": "Armures" } ], "updatedAt": "2026-07-25T12:00:00.000Z" }, "modules": { "m1": { "items": [ { "id": "i1", "label": "Casque" } ] } } } ``` ## A maintenir à chaque update Quand un outil change de structure : - mettre à jour ce document ; - mettre à jour les normalisations dans `website/src/features/toolboxes/storage/toolboxStorage.js` ; - mettre à jour les textes dans `website/public/data/site.json` si nécessaire ; - mettre à jour la validation dans `tests/helpers/data-validation.mjs` si nécessaire ; - vérifier import/export ; - lancer `npm run check`.