Aller au contenu
TimeRetain
Ouvrir l’application

Importez vos données dans TimeRetain

TimeRetain importe des données de temps depuis un fichier JSON. Un assistant IA peut créer ce fichier rapidement à partir de vos données.

  1. Exportez vos données depuis votre outil actuel en CSV ou en tableur.
  2. Envoyez ce fichier et les instructions ci-dessous à un assistant IA pour créer un fichier JSON TimeRetain.
  3. Ouvrez Données et synchronisation, choisissez Importer, puis téléversez le fichier JSON.

Vous pouvez recommencer l'import : les entrées existantes sont mises à jour sans être dupliquées.

Instructions pour l'assistant
Convertissez le fichier joint en JSON TimeRetain v0.12 selon la spécification ci-dessous.
Utilisez des ID déterministes, identiques pour une même entrée.
Déduisez chaque ID d’une clé de ligne stable dans la source.
Les imports répétés mettront ainsi à jour les entrées existantes au lieu de les dupliquer.
Importez le fichier obtenu dans Données et synchronisation → Importer.

## Import JSON TimeRetain v0.12

Utilisez cette spécification pour convertir des données de suivi du temps externes en fichier JSON TimeRetain.

## Procédure de conversion

Suivez ces étapes :

1. Choisissez une clé de ligne externe stable, comme l’ID de la ligne d’origine. S’il n’existe pas, créez-en une à partir de colonnes stables de la source : date, heure de début, heure de fin, client, projet, tâche et description, par exemple.
2. Déduisez chaque ID TimeRetain de `first 16 bytes of SHA-256(utf8(externalId))`. Encodez-le en Base64 compatible avec les URL, sans remplissage. Utilisez des préfixes distincts pour les lignes liées. Exemples : `stopwatch:<externalId>`, `interval:<externalId>`, `timeline:<client>/<project>`, `tag:<tagName>` et `tagStopwatch:<externalId>:<tagName>`.
3. Définissez `version` de l’enveloppe sur `"0.12"`. L’enveloppe est l’objet externe autour de `data`. Définissez son `timestamp` comme une chaîne ISO 8601.
4. Incluez les sept tableaux dans `data` dans cet ordre : `interval`, `preferences`, `stopwatch`, `tag`, `timeline`, `tagStopwatch`, `exportOptions`. Ils peuvent être vides, sauf `preferences` et `timeline`. `exportOptions` vaut généralement `[]`.
5. Représentez les clients, projets, espaces de travail, comptes et autres groupes de facturation ou de rapport par des lignes `timeline`. Placez les chronologies liées sous des chronologies plus générales. Définissez le `timelineId` de chaque chronomètre sur la chronologie précise qui lui correspond le mieux.
6. Représentez les activités comme les réunions, le code, la rédaction, l’administration, la recherche et l’assistance par des lignes `tag` sans hiérarchie. Les étiquettes ne peuvent pas former de groupes dans la v0.12.
7. Utilisez l’heure Unix en millisecondes pour `interval.start` et `interval.end`. Si la source contient des dates ou heures locales, appliquez son fuseau horaire avant la conversion. Utilisez `null` pour `interval.end` uniquement si l’entrée est encore en cours.
8. Utilisez `0` ou `1` pour chaque champ `Bool`. N’utilisez pas les booléens JSON.
9. Calculez `checksum` comme le SHA-256 de `JSON.stringify(data)` en hexadécimal minuscule. `data` désigne l’objet interne avec l’ordre des clés ci-dessus. N’incluez pas les champs de l’enveloppe dans la somme de contrôle.

L’import ajoute les nouvelles lignes ou met à jour les lignes existantes selon leur `id`.
Les ID déterministes évitent les doublons lors d’imports répétés.

## Règles des chronologies

Appliquez les règles suivantes :

- Si la source ne contient pas de chronologies groupées par client ou projet, ajoutez une ligne de chronologie Personal par défaut.
- Une chronologie hors d’un groupe est déjà une limite de facturation et peut être facturée indépendamment. Définissez `isBillingBoundary: 1` uniquement si une chronologie groupée peut elle aussi être facturée indépendamment.
- Définissez `parentTimelineId` sur le `timeline.id` du groupe parent, ou sur `null` s’il n’y en a pas. Ne créez ni cycles de regroupement ni références à des chronologies absentes.
- Les chronologies archivées restent valables pour l’historique, mais ne peuvent pas être choisies pour de nouvelles entrées. Pour un import normal, utilisez `archivedAt: null`.
- Les étiquettes archivées restent valables pour l’historique, mais ne peuvent pas être choisies pour de nouvelles entrées. Pour un import normal, utilisez `archivedAt: null`.
- Pour les tarifs par défaut d’un projet ou client, utilisez `timeline.hourlyRate`. Inscrivez le tarif déterminé pour chaque entrée dans `stopwatch.hourlyRate`.
- Si `stopwatch.hourlyRate` n’est pas `null`, définissez `rateCurrencyCodeAtWrite` sur la devise de l’espace de travail lors de l’import. Si aucun tarif n’est déterminé pour l’entrée, définissez les deux champs sur `null`.
- Utilisez `timelineId: null` uniquement si l’entrée appartient volontairement à Aucune chronologie.
- Définissez `preferences.defaultTimelineId` et `preferences.lastUsedTimelineId` sur une chronologie existante et non archivée, généralement Personal.

## Types

Utilisez les types de valeurs suivants :

- `Id` est une chaîne Base64 de 22 caractères, compatible avec les URL et sans remplissage. Elle utilise `A-Z`, `a-z`, `0-9`, `-` et `_`, et encode 16 octets.
- `Bool` vaut `0` ou `1`, jamais `false` ou `true`.
- Les horodatages des lignes (`interval.start`, `interval.end`) utilisent l’heure Unix en millisecondes. Le `timestamp` de l’enveloppe est une chaîne ISO 8601. Tous les nombres doivent être finis.

## Enveloppe

```json
{
  "version": "0.12",
  "timestamp": "<ISO-8601 string>",
  "checksum": "<sha256 hex of JSON.stringify(data)>",
  "data": {
    "interval": [],
    "preferences": [],
    "stopwatch": [],
    "tag": [],
    "timeline": [],
    "tagStopwatch": [],
    "exportOptions": []
  }
}
```

Incluez les sept clés dans `data` exactement dans cet ordre : `interval`, `preferences`, `stopwatch`, `tag`, `timeline`, `tagStopwatch`, `exportOptions`.

## Schéma

```ts
interval: {
  id: Id;
  stopwatchId: Id; // -> stopwatch.id
  start: number; // millisecondes Unix
  end: number | null; // millisecondes Unix, ou null si l’entrée est en cours
}

preferences: {
  // exactement une ligne
  id: Id;
  useMilitaryTime: Bool;
  mergeOverlappingStopwatches: Bool; // ancien champ ; généralement 0
  useAdjust: Bool;
  adjustDefault: number; // minutes ; peut valoir 0 ou être négatif
  backupReminderIntervalInDays: number;
  currencyCode: string; // ISO 4217, par exemple "USD"
  earningsTrackingEnabled: Bool;
  defaultHourlyRate: number | null;
  weekStartsOn: number; // 0 = dimanche ... 6 = samedi
  alternativeColor: string | null; // valeur Open Color, ou null pour le gris
  defaultTimelineId: Id | null; // -> timeline.id
  lastUsedTimelineId: Id | null; // -> timeline.id
  forgottenRemindersMode: "both" | "start" | "stop" | "off";
}

stopwatch: {
  id: Id;
  description: string;
  adjustMinutes: number; // entier ; peut être négatif
  isActive: Bool;
  isExpanded: Bool;
  hourlyRate: number | null; // tarif enregistré pour ce chronomètre
  rateCurrencyCodeAtWrite: string | null; // devise de l’espace de travail à l’enregistrement du tarif
  isHourlyRateManual: Bool | null;
  targetMinutes: number | null;
  timelineId: Id | null; // -> timeline.id ; null signifie Aucune chronologie
}

tag: {
  id: Id;
  name: string;
  hourlyRate: number | null;
  color: string | null; // valeur Open Color comme "blue.6", ou null
  isPinned: Bool | null; // épinglée dans la liste des étiquettes ; null équivaut à épinglée
  archivedAt: number | null; // millisecondes Unix à l’archivage ; null garde l’étiquette active
}

timeline: {
  id: Id;
  name: string;
  parentTimelineId: Id | null; // -> timeline.id de la chronologie parente, ou null sans groupe
  hourlyRate: number | null;
  isBillingBoundary: Bool;
  archivedAt: number | null; // millisecondes Unix à l’archivage ; null garde la chronologie active
}

tagStopwatch: {
  id: Id;
  tagId: Id; // -> tag.id
  stopwatchId: Id; // -> stopwatch.id
}

exportOptions: {
  // au plus une ligne ; la plupart des imports utilisent un tableau vide
  // Utilisez de préférence l’id "4WnR17vMjH50XmwttC218Q" (la ligne unique de l’application). Les autres ID sont
  // remplacés par cette valeur lors de l’import.
  id: Id;
  format: "csv";
  explodeIntervals: 0 | 1;
  explodeTags: 0 | 1;
  durationBreakdown: 0 | 1;
  roundTarget: "off" | "interval" | "total";
  roundIncrement: "5" | "6" | "10" | "15" | "30" | "60";
  roundDirection: "nearest" | "up" | "down";
  timeColumns: "utc" | "split";
}
```

## Somme de contrôle

`envelope.checksum` est l’empreinte SHA-256 de `JSON.stringify(data)` en hexadécimal minuscule.
Ici, `data` désigne l’objet interne, pas toute l’enveloppe.

JavaScript :

```js
import { createHash } from "node:crypto";

const checksum = createHash("sha256")
  .update(JSON.stringify(data))
  .digest("hex");
```

Python :

```python
import hashlib
import json

payload = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
checksum = hashlib.sha256(payload.encode("utf-8")).hexdigest()
```

## Structure minimale d’exemple

```json
{
  "version": "0.12",
  "timestamp": "2026-04-17T12:00:00.000Z",
  "checksum": "3149c7d5240911d6fe1112839e7511b43d4b8e93500a3173c309771769c83f3a",
  "data": {
    "interval": [
      {
        "id": "YpC0F378hvhs9cwPrPZqjA",
        "stopwatchId": "uHDqdpI_ehZ1fWTWH7I1dw",
        "start": 1763884800000,
        "end": 1763890200000
      }
    ],
    "preferences": [
      {
        "id": "NymWULO4G6GjGMp6_e5MOA",
        "useMilitaryTime": 0,
        "mergeOverlappingStopwatches": 0,
        "useAdjust": 0,
        "adjustDefault": 0,
        "backupReminderIntervalInDays": 7,
        "currencyCode": "USD",
        "earningsTrackingEnabled": 1,
        "defaultHourlyRate": null,
        "weekStartsOn": 1,
        "alternativeColor": null,
        "defaultTimelineId": "nM-FuMhKJ-Qc34ihF_5GAQ",
        "lastUsedTimelineId": "nM-FuMhKJ-Qc34ihF_5GAQ",
        "forgottenRemindersMode": "both"
      }
    ],
    "stopwatch": [
      {
        "id": "uHDqdpI_ehZ1fWTWH7I1dw",
        "description": "Client work",
        "adjustMinutes": 0,
        "isActive": 0,
        "isExpanded": 0,
        "hourlyRate": 125,
        "rateCurrencyCodeAtWrite": "USD",
        "isHourlyRateManual": 0,
        "targetMinutes": null,
        "timelineId": "C_MCKuU1uvIkXqlM2cuvvA"
      }
    ],
    "tag": [
      {
        "id": "-PpIeZugec_a_57XR7os3A",
        "name": "writing",
        "hourlyRate": null,
        "color": null,
        "isPinned": null,
        "archivedAt": null
      }
    ],
    "timeline": [
      {
        "id": "nM-FuMhKJ-Qc34ihF_5GAQ",
        "name": "Personal",
        "parentTimelineId": null,
        "hourlyRate": null,
        "isBillingBoundary": 0,
        "archivedAt": null
      },
      {
        "id": "C_MCKuU1uvIkXqlM2cuvvA",
        "name": "Client A",
        "parentTimelineId": null,
        "hourlyRate": 125,
        "isBillingBoundary": 0,
        "archivedAt": null
      }
    ],
    "tagStopwatch": [
      {
        "id": "Z5XfUVYDOcXMuFH02iHMxQ",
        "tagId": "-PpIeZugec_a_57XR7os3A",
        "stopwatchId": "uHDqdpI_ehZ1fWTWH7I1dw"
      }
    ],
    "exportOptions": []
  }
}
```

## Erreurs fréquentes

Évitez les erreurs suivantes :

- N’utilisez pas `true` ou `false` pour les champs `Bool`. Utilisez `0` ou `1`.
- N’utilisez pas les secondes pour `interval.start` ou `interval.end`. Utilisez les millisecondes.
- N’utilisez pas de chaînes ISO pour les horodatages des lignes. Seul le `timestamp` de l’enveloppe est une chaîne ISO.
- N’ajoutez pas de champ `timezone` à l’enveloppe. La conversion peut nécessiter le fuseau horaire de la source, mais TimeRetain stocke les heures des lignes en millisecondes Unix.
- N’omettez pas la somme de contrôle. Calculez-la à partir de l’objet interne `data`, pas de toute l’enveloppe.
- Ne reformatez pas `data` et ne triez pas ses clés par ordre alphabétique avant de calculer la somme de contrôle. Respectez exactement l’ordre des clés ci-dessus.
- N’omettez aucun des sept tableaux de `data`, même s’ils sont vides.
- Ne regroupez pas les clients ou les projets avec des étiquettes. Utilisez des lignes `timeline` pour les placer sous des chronologies liées.
- N’ajoutez pas `tag.parentTagId`. Les étiquettes ne peuvent pas former de groupes.
- Ne créez pas de chronologies dont le groupe parent est absent ou qui forment un cycle.
- Si la source indique un client, projet, compte ou espace de travail connu, définissez `stopwatch.timelineId`.
- Pour les chronomètres associés à des étiquettes, créez des lignes `tagStopwatch` correspondantes.