sokko-g/docs/STORAGE_SCHEMA.md
Shinuwa 09a2d4b71a
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
Protect toolbox data after IndexedDB startup failures
2026-09-12 23:05:16 +02:00

1110 lines
37 KiB
Markdown

# 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
### Ordre et confirmation des écritures
Le démarrage attend la lecture complète du registre, des liens, des modules et
de la position des notifications avant d'ouvrir l'interface. Un échec laisse
le stockage non prêt : les méthodes d'écriture refusent les opérations, et la
création, l'import et l'export ne peuvent pas utiliser un état vide incomplet.
Le bouton de reprise relance uniquement les lectures, sans supprimer de données.
Une ouverture IndexedDB échouée n'est pas conservée dans le cache de connexion.
Les erreurs de lecture sont distinctes des erreurs d'écriture et ne sont pas
présentées systématiquement comme un dépassement de quota.
Dans une instance de l'application, le registre, les liens, les modules et la
position des notifications partagent une file d'écritures. L'interface présente
les modifications en attente ; en cas d'échec, elle est reconstruite depuis le
dernier état confirmé, puis les opérations restantes sont réappliquées.
La création, l'association, l'import et la suppression d'une toolbox utilisent
une transaction commune aux stores `kv` et `modules`. Les imports ajoutent leur
contenu à l'état courant au moment de l'exécution et annoncent leur succès après
la fin de transaction. Une erreur synchrone pendant sa préparation déclenche
l'abandon de la transaction. Les écritures ordinaires d'un module restent ciblées
sur sa clé et ne réécrivent pas les images des autres outils.
Cette coordination est locale à l'instance React ; elle ne synchronise pas
l'édition entre plusieurs onglets.
Les estimations de quota demandées après sauvegarde sont regroupées sur une
fenêtre de 250 ms, avec une seule estimation active. Une écriture intervenant
pendant le calcul déclenche une nouvelle estimation. Les sauvegardes ne sont
pas retardées ; en cas d'estimation indisponible, le dernier quota connu est
conservé.
Les toolboxes sont stockées dans `kv/toolboxes`.
```json
{
"id": "tbx1",
"name": "Monster Hunter Wilds",
"gameId": "mhwilds",
"icon": "/static/img/toolbox-icons/toolbox.png",
"customCover": "data:image/webp;base64,...",
"modules": [
{
"id": "mod1",
"type": "checklist",
"title": "Armures"
}
],
"moduleColumns": 2,
"moduleOrder": {
"one": ["mod1"],
"two": [["mod1"], []]
},
"tutorial": true,
"updatedAt": "2026-07-25T12:00:00.000Z"
}
```
Notes :
- `gameId` est vide pour une toolbox libre.
- `icon` est utilisé uniquement pour les toolboxes libres.
- `customCover` est une image rectangulaire optionnelle utilisée comme cover de card pour les toolboxes libres. Elle est stockée en data URL image après recadrage, sans conserver le fichier original.
- `moduleColumns` vaut `1` ou `2` et contrôle l'affichage de la page toolbox complète ; le panneau latéral utilise toujours l'ordre une colonne.
- `moduleOrder.one` stocke l'ordre du mode une colonne.
- `moduleOrder.two` stocke l'ordre explicite des colonnes gauche et droite en mode deux colonnes.
- Chaque identifiant d'outil présent dans `modules` doit apparaître exactement une fois dans `moduleOrder.one` et exactement une fois dans l'une des deux colonnes de `moduleOrder.two`.
- Si une ancienne toolbox n'a pas encore `moduleOrder.two`, les outils sont répartis automatiquement depuis l'ordre une colonne avec une moitié en colonne gauche et une moitié en colonne droite.
- Les données lourdes des outils ne sont pas stockées dans la toolbox, mais dans `modules`.
- `tutorial` est un indicateur temporaire réservé aux toolboxes créées depuis la prise en main. Il déclenche les premiers repères à l'ouverture, puis il est retiré automatiquement du stockage.
## Liens Jeu / Toolbox
Les associations entre pages jeux et toolboxes sont stockées dans `kv/links`.
```json
{
"mhwilds": "tbx1",
"diablo4": "tbx2"
}
```
## Outil Bloc Note
Type : `notepad`
```json
{
"activePageId": "page1",
"pages": [
{
"id": "page1",
"title": "Page 1",
"html": "<p>Notes <strong>rapides</strong>...</p>",
"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 mono-page `{ "html": "...", "text": "..." }` reste accepté et est converti en `pages[0]` au prochain enregistrement ;
- `activePageId` est omis si la première page stockée est active ;
- `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-note sont conservés dans `sessionStorage` par toolbox, outil et page, pas dans IndexedDB ;
- si toutes les pages sont vides et que seule la page par défaut reste présente, l'entrée de module peut être supprimée.
## Outil Mémos
Type : `memos`
```json
{
"tickerEnabled": true,
"items": [
{
"id": "memo1",
"text": "Penser à sauvegarder avant le boss",
"color": "#f6c453",
"updatedAt": "2026-08-21T12:00:00.000Z"
}
]
}
```
Stockage compact :
- `tickerEnabled` vaut `true` par défaut et est omis dans ce cas ;
- `updatedAt` est omis si vide ou invalide ;
- les mémos sans texte sont ignorés ;
- si aucun mémo n'est valide et que la bande est active par défaut, 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"]
}
```
## Outil Arbre D'objectifs
Type : `goalTree`
```json
{
"lastResetAt": 1786226400000,
"orientation": "horizontal",
"nodes": [
{
"id": "goal1",
"parentId": "",
"title": "Breuvage de rage",
"kind": "step",
"icon": "cube",
"color": "gold",
"qtyTarget": 10,
"qtyCurrent": 0,
"formula": "",
"progressMode": "auto",
"resetType": "none",
"resetTime": "00:00",
"weeklyResetDay": 1,
"checkedAt": 0,
"collapsed": false
},
{
"id": "goal2",
"parentId": "goal1",
"parentIds": ["goal1"],
"title": "Élixir de destruction",
"kind": "step",
"icon": "flask",
"color": "cyan",
"qtyTarget": 1,
"qtyCurrent": 0,
"formula": "base * 3",
"resetType": "weekly",
"resetTime": "05:00",
"weeklyResetDay": 1,
"checkedAt": 0,
"collapsed": false
},
{
"id": "goal3",
"parentId": "goal1",
"title": "Liste de farm",
"kind": "checklist",
"items": [
{
"id": "item1",
"label": "Élément",
"formula": "1",
"qtyTarget": 1,
"qtyCurrent": 1
},
{
"id": "item2",
"label": "Faire cela",
"formula": "base",
"qtyTarget": 5,
"qtyCurrent": 2
}
],
"resetType": "none",
"resetTime": "00:00",
"weeklyResetDay": 1,
"checkedAt": 0,
"collapsed": false
}
]
}
```
Stockage compact :
- `lastResetAt` est omis si vide.
- `orientation` est omis si la valeur vaut `horizontal`.
- `orientation` vaut `horizontal` ou `vertical`.
- `parentId` est omis pour les racines.
- `parentId` reste le parent principal de placement dans le schéma.
- `parentIds` contient les parents/prérequis pris en compte pour les liaisons de convergence ; il est omis si aucun prérequis supplémentaire n'existe.
- les calculs `base`, le placement, la suppression de branche et l'agrégation de progression restent basés sur `parentId`.
- un objectif peut donc être placé sous un parent principal tout en ayant d'autres prérequis, afin de représenter une convergence de branches.
- `kind` est omis si la valeur vaut `step`.
- `kind` vaut `step`, `text` ou `checklist`.
- `icon` est omis si la valeur vaut `cube`.
- `icon` vaut `cube`, `fire`, `flask`, `grass`, `leaf`, `meat`, `powder`, `tear` ou `ball`.
- `color` est omis si la valeur vaut `gold`.
- `color` vaut `gold`, `cyan`, `violet`, `pink`, `green`, `gray`, `red`, `orange`, `yellow`, `blue` ou `indigo`.
- `qtyTarget` est omis si la valeur vaut `1`.
- `qtyCurrent` est omis si la valeur vaut `0`.
- `formula` est omis si vide.
- `progressMode` est utilisé uniquement par les objectifs `step`; il vaut `auto` ou `manual`.
- `progressMode` est omis tant qu'aucun choix explicite n'a été fait; par défaut, une étape avec enfants calcule sa progression automatiquement, sauf si tous ses enfants directs ont une cible de `1`, auquel cas elle se comporte en mise à jour manuelle.
- en `auto`, une étape avec enfants calcule sa progression depuis ses enfants; en `manual`, elle utilise sa propre `qtyCurrent`.
- `items` est omis si vide ou si `kind` vaut `step`.
- `items[].label` contient la ligne affichee dans un noeud `text` ou `checklist`; il peut etre vide pendant l'edition.
- `items[].formula` est utilise uniquement par `checklist` pour calculer le total de la ligne ; la variable `base` reference la cible calculee du parent direct.
- `items[].qtyTarget` est utilise uniquement par `checklist` et omis si la valeur vaut `1`.
- `items[].qtyCurrent` est utilise uniquement par `checklist` et omis si la valeur vaut `0`.
- `resetType` est omis si la valeur vaut `none`.
- `resetType` vaut `none`, `daily` ou `weekly`.
- `resetTime` est omis si la valeur vaut `00:00`.
- `weeklyResetDay` est omis si la valeur vaut `1`.
- `checkedAt` est omis si vide.
- `collapsed` est omis si `false`.
- Les parents invalides ou cycliques sont ramenés à la racine pendant la normalisation.
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 Calendrier
Type : `calendar`
```json
{
"initialized": true,
"viewMode": "month",
"weekStartsOn": 1,
"events": [
{
"id": "calendar1",
"title": "Raid",
"startDate": "2026-08-14",
"endDate": "2026-08-16",
"repeat": "monthly",
"monthlyRepeatBy": "weekday",
"excludedOccurrences": ["2026-09-11"],
"alertMode": "site",
"alertOffsetMinutes": [10, 5, 0],
"color": "violet"
}
]
}
```
Stockage compact :
- `viewMode` vaut `week` ou `month` ; `week` est omis dans ce cas.
- `initialized` indique que le choix du type de calendrier a été validé dans l'outil ; il est conservé pour garder un calendrier vide utilisable.
- `weekStartsOn` vaut `1` par défaut et est omis dans ce cas.
- `events` est omis tant qu'aucun événement valide n'est configuré.
- les événements sans titre ou sans cible de jour/date valide sont supprimés ; `startMinutes` reste requis en hebdomadaire et vaut `0` par défaut en mensuel.
- `dayIndex` vaut `0` pour lundi, `1` pour mardi, jusqu'à `6` pour dimanche.
- en calendrier mensuel, `startDate` et `endDate` sont des dates locales `YYYY-MM-DD` ; `endDate` est omis si la plage dure un seul jour.
- en calendrier mensuel, `monthlyRepeatBy` vaut `monthDay` ou `weekday` ; `monthDay` est la valeur par défaut et est omise dans ce cas.
- en calendrier mensuel, `repeat` vaut `none`, `weekly` ou `monthly` ; `monthly` est la valeur par défaut et est omise dans ce cas.
- `repeat=weekly` est réservé aux entrées mensuelles par `weekday` ; si une entrée par `monthDay` demande `weekly`, elle est normalisée en `none`.
- une entrée mensuelle répétée par `monthDay` n'a pas d'occurrence dans un mois où le jour demandé n'existe pas.
- une entrée mensuelle répétée par `weekday` et `monthly` se répète sur le même rang de jour de semaine que `startDate` ; par exemple le premier lundi du mois.
- `excludedOccurrences` contient des dates locales `YYYY-MM-DD` correspondant aux occurrences supprimées individuellement.
- `startMinutes` et `endMinutes` sont des minutes depuis `00:00` dans la journée ; en hebdomadaire, l'interface saisit les heures au pas de 5 minutes tout en gardant une grille visuelle de 30 minutes ; en mensuel, les champs horaires ne sont pas exposés et `startMinutes` est omis s'il vaut `0`.
- `endMinutes` est conservé s'il produit une fin postérieure au début ; en mensuel, il peut être inférieur à `startMinutes` si la fin est sur un jour ultérieur.
- en hebdomadaire, `repeat` n'est pas stocké dans le format cible : il est déduit de `repeatable` (`weekly` par défaut, `none` si `repeatable=false`).
- en hebdomadaire, `repeatable` vaut `true` par défaut et est omis dans ce cas ; si `false`, l'entrée est supprimée quand son échéance de la semaine courante est terminée.
- `alertMode` vaut `off`, `visible` ou `site`, et est omis si `off`.
- `alertOffsetMinutes` est une liste composée de `10`, `5` ou `0` (`0` = au début et pendant l'événement pour les reprises de session) ; il est omis si l'alerte est désactivée ou si la liste vaut `[10]`.
- `color` vaut `violet`, `gold`, `cyan`, `pink`, `green` ou `gray`, et est omis si `violet`.
Import/export texte :
- le format texte commence par `@viewMode: week` ou `@viewMode: month`, puis une ligne `event | ...` par entrée ;
- en hebdomadaire, les champs `day`, `start` et `end` décrivent le jour de semaine et les heures locales (`HH:mm`) ;
- en mensuel, `repeatBy=monthDay` ou `repeatBy=weekday` utilise `startDate`/`endDate` et `repeat=none|weekly|monthly`, par exemple `repeatBy=weekday | repeat=monthly | startDate=2026-08-03 | endDate=2026-08-05` ;
- l'import texte remappe toujours les identifiants d'événements.
## 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
{
"categoryOrder": ["Builds", "Guides"],
"collapsedCategories": ["Guides"],
"links": [
{
"id": "link1",
"title": "Build rogue",
"category": "Builds",
"url": "https://example.com/build"
}
]
}
```
Stockage compact :
- `title` est facultatif.
- `title` est omis si vide.
- `category` est facultatif et sert à l'autocomplete de création.
- `category` est omis si vide.
- `url` est obligatoire.
- `categoryOrder` stocke l'ordre d'affichage des catégories, y compris les catégories vides.
- `collapsedCategories` stocke les noms des catégories repliées.
## 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 Randomizer
Type : `randomizer`
```json
{
"activeTab": "wheel",
"dice": {
"faces": 6,
"rolls": 3,
"results": [2, 5, 1],
"history": [
{
"id": "random1",
"at": "2026-08-30T12:00:00.000Z",
"results": [2, 5, 1]
}
]
},
"wheel": {
"draws": 2,
"entries": [
{
"id": "entry1",
"label": "Route farm",
"weight": 3
}
],
"results": ["Route farm"],
"history": []
},
"teams": {
"teamCount": 2,
"playerSelectionMode": "all",
"playerSelectionCount": 2,
"players": [
{
"id": "player1",
"name": "Alice",
"score": 10,
"active": true
}
],
"exclusions": [
{
"id": "exclusion1",
"playerAId": "player1",
"playerBIds": ["player2", "player3"],
"mode": "source"
}
],
"teams": [],
"history": []
}
}
```
Stockage compact :
- `activeTab` est omis quand l'onglet actif est `dice`.
- `dice`, `wheel` et `teams` sont omis quand ils ne contiennent que leurs valeurs par défaut et aucun résultat.
- l'historique de chaque onglet est limité aux 20 derniers tirages.
- les entrées de roue sans libellé sont ignorées, et `weight` est borné de 1 à 999.
- les joueurs sans nom sont ignorés, et `score` est borné de 0 à 9999.
- `active` vaut `true` par défaut sur un joueur.
- `playerSelectionMode` vaut `all` ou `count`; en mode `count`, `playerSelectionCount` indique combien de joueurs actifs sont choisis aléatoirement avant la répartition.
- chaque exclusion décrit un nom principal `playerAId`, une ou plusieurs cibles `playerBIds` et un `mode`.
- `mode: "source"` interdit uniquement `playerAId` avec chaque cible; les cibles peuvent rester ensemble.
- `mode: "group"` interdit chaque nom de la combinaison avec tous les autres noms de cette combinaison.
- les exclusions invalides, dupliquées ou qui référencent un joueur absent sont ignorées.
## Outil Simulateur de chance
Type : `rngSimulator`
```json
{
"activeRateId": "rate1",
"rates": [
{
"id": "rate1",
"label": "Drop rare",
"rate": "2.500",
"costPerDraw": "150",
"multiplier": 50,
"occurrenceValue": 30,
"occurrenceUnit": "perHour",
"realDraws": 120
},
{
"id": "rate2",
"label": "Ultra rare",
"rate": "0.000004"
}
],
"settings": {
"simulationDraws": 1000
}
}
```
Stockage compact :
- `rates` contient les taux enregistrés du module; un taux sans libellé est ignoré.
- `rate` est une chaîne en pourcentage afin de conserver la précision saisie, par exemple `"2.500"` ou `"0.000004"`.
- un taux doit être strictement supérieur à `0` et inférieur ou égal à `100`; les valeurs supérieures sont bornées à `"100"`.
- `costPerDraw` est une chaîne décimale optionnelle, associée au taux, utilisée comme unité libre pour les calculs de coût.
- `activeRateId` référence un taux existant; sinon le premier taux est sélectionné.
- `rates[].multiplier` est un bonus ou malus en pourcentage associé au taux, vaut 0 par défaut et le taux effectif est borné entre 0% et 100%.
- `rates[].occurrenceValue` est numérique, positif ou nul, associé au taux, et vaut 0 par défaut.
- `rates[].occurrenceUnit` vaut `perHour` ou `perDay`, et vaut `perHour` par défaut.
- les anciennes données `occurrenceUnit: "perMonth"` sont converties en `perDay`, avec `occurrenceValue` divisé par 30.
- `rates[].realDraws` est le total persistant de tirages réellement effectués pour ce taux, borné de 0 à 1 000 000 000.
- `settings.simulationDraws` est borné de 1 à 1 000 000 et vaut 1000 par défaut.
- `settings` est omis si `simulationDraws` vaut sa valeur par défaut.
- `rates[].costPerDraw` est omis si vide ou nul.
- `rates[].multiplier` est omis si sa valeur vaut 0.
- `rates[].occurrenceValue` et `rates[].occurrenceUnit` sont omis si `occurrenceValue` vaut 0.
- `rates[].realDraws` est omis si sa valeur vaut 0.
- l'historique des simulations n'est pas persisté en IndexedDB; il est limité aux 20 dernières entrées en `sessionStorage`.
Import/export texte :
```text
@activeRateId: rate1
[rates]
- Drop rare | rate=2.500 | active=true | costPerDraw=150 | multiplier=50 | occurrenceValue=30 | occurrenceUnit=perHour | realDraws=120
[settings]
simulationDraws: 1000
```
## 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,
"formula": "100"
},
{
"id": "calc2",
"parentId": "calc1",
"label": "Minerais de fer",
"value": 500,
"formula": "base*5"
}
]
}
```
Stockage compact :
- `scrollResults` est omis si `false`.
- `parentId` est omis si vide.
- `label` peut être vide.
- `value` est numérique.
- `formula` est omis si vide ; dans un calcul enfant, le mot `base` référence la valeur recalculée du parent.
- 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"
}
]
}
}
}
```
## Import / Export texte des outils
Les outils compatibles proposent aussi un échange par copier-coller depuis le toggle import/export de leur header. Ce format est un format UI distinct du stockage IndexedDB et de l'export JSON global.
Règles communes :
- l'import valide tout le texte avant de sauvegarder ;
- une erreur de format ne modifie pas les données existantes ;
- les IDs internes sont recréés à l'import ;
- l'identité de contenu est évaluée après normalisation ;
- les états de progression ne sont pas repris : checklist à `qtyCurrent: 0`, tâches non effectuées et `checkedAt: 0`.
Formats :
- `notepad` : format structuré `@notepad`, blocs `@page: Titre`, `@html` / `@endhtml` pour le HTML riche nettoyé, meta `@drawingMode: permanent` et `@drawings: {...}` pour les dessins permanents. Un collage texte simple reste accepté et devient une page sans mise en forme.
- `memos` : une ligne par mémo, avec préfixe `-` optionnel et suffixe couleur `| color=#f6c453` ; les anciens préfixes `*` sont acceptés puis normalisés.
- `checklist` : sections `# Catégorie`, lignes `Item: quantité cible`.
- `links` : sections `# Catégorie`, lignes `Titre: https://...` ou URL seule; le suffixe optionnel `| category=Catégorie` reste accepté à l'import.
- `counters` : lignes `Libellé: valeur`, valeurs négatives acceptées.
- `calculator` : lignes `Libellé: valeur | formula=base*5`, indentation de deux espaces pour les enfants, meta `@scrollResults: true`. Le suffixe `formula=...` est optionnel et les anciens exports `Libellé: valeur` restent acceptés.
- `table` : TSV pur accepté ; l'export peut ajouter `@size`, `@columns`, `@rows` puis `@tsv` pour préserver dimensions et intitulés.
- `combos` : sections `# Catégorie`, lignes `Nom | device=... | kind:value > kind:value+kind:value`, avec suffixes `[hold]` ou `[2s]`.
- `taskPlanner` : sections `# Catégorie`, lignes indentées `- daily Titre`, `- weekly Titre` ou `- unique Titre`, metas `@description`, `@dailyResetTime`, `@weeklyResetDay`, `@prerequisite`.
- `equipmentPlanner` : types `# Type | icon=...`, équipements `## Nom | icon=... | active=true`, sous-sections `Stats`, `Sockets`, `Craft`.
- `images` : blocs `# Libellé` puis `data:image/...;base64,...`.
- `imageAnnotation` : image base64, lignes `@marker: x,y,label` et `@drawings: {...}` pour les dessins permanents.
- `timer` : metas globales, lignes `lap | ...` et `countdown | ...`; les échéances temporelles sont recalculées à l'import.
## 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`.