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.
- Exportez vos données depuis votre outil actuel en CSV ou en tableur.
- Envoyez ce fichier et les instructions ci-dessous à un assistant IA pour créer un fichier JSON TimeRetain.
- 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.