# 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": "<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 `{ "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 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",
      "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.
- `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.
- `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`.
