Zum Inhalt springen
TimeRetain
App öffnen

Importiere deine Daten in TimeRetain

TimeRetain importiert Zeitdaten aus einer JSON-Datei. Am schnellsten erstellt ein KI-Assistent diese Datei aus deinen vorhandenen Daten.

  1. Exportiere deine Daten aus dem aktuellen Werkzeug als CSV oder Tabelle.
  2. Sende die Datei mit der folgenden Anweisung an einen KI-Assistenten, damit er eine TimeRetain-JSON-Datei erstellt.
  3. Öffne Daten & Sync, wähle „Importieren“ und lade die JSON-Datei hoch.

Ein erneuter Import ist sicher: Bestehende Einträge werden aktualisiert, nicht doppelt angelegt.

Anweisung für den Assistenten
Wandle die angehängte Datei anhand der folgenden Spezifikation in TimeRetain-v0.12-JSON um.
Verwende deterministische IDs, die bei gleicher Eingabe gleich bleiben.
Leite jede ID aus einem stabilen Schlüssel der Quelldatenzeile ab.
Bei wiederholten Importen werden vorhandene Einträge dadurch aktualisiert statt verdoppelt.
Importiere die erzeugte Datei über Daten & Sync → Importieren.

## TimeRetain-v0.12-JSON-Import

Verwende diese Spezifikation, um externe Zeiterfassungsdaten in eine TimeRetain-JSON-Datei umzuwandeln.

## Anleitung zur Umwandlung

Gehe wie folgt vor:

1. Wähle einen stabilen externen Zeilenschlüssel, zum Beispiel die ursprüngliche Zeilen-ID. Wenn keiner vorhanden ist, bilde ihn aus stabilen Quellspalten. Dafür eignen sich Datum, Startzeit, Endzeit, Kunde, Projekt, Aufgabe und Beschreibung.
2. Leite jede TimeRetain-ID aus `first 16 bytes of SHA-256(utf8(externalId))` ab. Kodiere sie als URL-sicheres Base64 ohne Padding. Verwende unterschiedliche Ausgangswerte für zusammengehörige Zeilen. Beispiele sind `stopwatch:<externalId>`, `interval:<externalId>`, `timeline:<client>/<project>`, `tag:<tagName>` und `tagStopwatch:<externalId>:<tagName>`.
3. Setze `version` im äußeren Objekt auf `"0.12"`. Das äußere Objekt umschließt `data`. Setze sein `timestamp` auf eine ISO-8601-Zeichenfolge.
4. Füge alle sieben Arrays in dieser Reihenfolge in `data` ein: `interval`, `preferences`, `stopwatch`, `tag`, `timeline`, `tagStopwatch`, `exportOptions`. Außer `preferences` und `timeline` dürfen Arrays leer sein. `exportOptions` ist normalerweise `[]`.
5. Stelle Kunden, Projekte, Arbeitsbereiche, Konten und andere Abrechnungs- oder Berichtsgruppen als `timeline`-Zeilen dar. Ordne zusammengehörige Zeitleisten einer übergeordneten Zeitleiste zu. Setze `timelineId` jeder Stoppuhr auf die am besten passende konkrete Zeitleiste.
6. Stelle Tätigkeiten wie Besprechungen, Programmierung, Schreiben, Verwaltung, Recherche und Support als flache `tag`-Zeilen dar. Tags können in v0.12 keine Gruppen bilden.
7. Verwende für `interval.start` und `interval.end` Unix-Zeit in Millisekunden. Wenn die Quelle lokale Datumsangaben oder Uhrzeiten enthält, berücksichtige vor der Umwandlung ihre Zeitzone. Setze `interval.end` nur für noch laufende Einträge auf `null`.
8. Verwende für jedes `Bool`-Feld `0` oder `1`, keine JSON-Booleschen Werte.
9. Berechne `checksum` als SHA-256-Hash von `JSON.stringify(data)` in hexadezimalen Kleinbuchstaben. Dabei ist `data` das innere Objekt mit der oben genannten Schlüsselreihenfolge. Beziehe Felder des äußeren Objekts nicht in die Prüfsumme ein.

Der Import fügt neue Zeilen ein oder aktualisiert vorhandene Zeilen anhand ihrer `id`.
Deterministische IDs verhindern doppelte Einträge bei wiederholten Importen.

## Regeln für Zeitleisten

Wende diese Regeln an:

- Wenn die Quelle keine gruppierten Kunden- oder Projektzeitleisten hat, füge eine Standardzeile für die Zeitleiste Personal ein.
- Eine Zeitleiste außerhalb einer Gruppe ist bereits eine Abrechnungsgrenze und kann selbstständig abgerechnet werden. Setze `isBillingBoundary: 1` nur dann, wenn auch eine gruppierte Zeitleiste selbstständig abgerechnet werden kann.
- Setze `parentTimelineId` auf die `timeline.id` der übergeordneten Gruppe oder auf `null`, wenn es keine gibt. Erzeuge keine Gruppierungszyklen oder Verweise auf fehlende Zeitleisten.
- Archivierte Zeitleisten bleiben für frühere Einträge gültig, können aber nicht für neue Einträge ausgewählt werden. Verwende bei normalen Importen `archivedAt: null`.
- Archivierte Tags bleiben für frühere Einträge gültig, können aber nicht für neue Einträge ausgewählt werden. Verwende bei normalen Importen `archivedAt: null`.
- Verwende für Standardstundensätze eines Projekts oder Kunden `timeline.hourlyRate`. Trage den ermittelten Satz jedes Eintrags in `stopwatch.hourlyRate` ein.
- Wenn `stopwatch.hourlyRate` nicht `null` ist, setze `rateCurrencyCodeAtWrite` auf die Währung des Arbeitsbereichs beim Import. Wenn für den Eintrag kein Satz ermittelt wurde, setze beide Felder auf `null`.
- Verwende `timelineId: null` nur, wenn ein Eintrag ausdrücklich zu keiner Zeitleiste gehört.
- Setze `preferences.defaultTimelineId` und `preferences.lastUsedTimelineId` auf eine vorhandene, nicht archivierte Zeitleiste, normalerweise Personal.

## Typen

Verwende diese Werttypen:

- `Id` ist eine 22 Zeichen lange, URL-sichere Base64-Zeichenfolge ohne Padding. Sie verwendet `A-Z`, `a-z`, `0-9`, `-` und `_` und kodiert 16 Bytes.
- `Bool` ist `0` oder `1`, niemals `false` oder `true`.
- Zeitstempel von Zeilen (`interval.start`, `interval.end`) verwenden Unix-Zeit in Millisekunden. Das Feld `timestamp` des äußeren Objekts ist eine ISO-8601-Zeichenfolge. Alle Zahlen müssen endlich sein.

## Äußeres Objekt

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

Füge alle sieben Schlüssel in genau dieser Reihenfolge in `data` ein: `interval`, `preferences`, `stopwatch`, `tag`, `timeline`, `tagStopwatch`, `exportOptions`.

## Schema

```ts
interval: {
  id: Id;
  stopwatchId: Id; // -> stopwatch.id
  start: number; // Unix-Millisekunden
  end: number | null; // Unix-Millisekunden, oder null bei laufenden Einträgen
}

preferences: {
  // genau eine Zeile
  id: Id;
  useMilitaryTime: Bool;
  mergeOverlappingStopwatches: Bool; // veraltetes Feld; normalerweise 0
  useAdjust: Bool;
  adjustDefault: number; // Minuten; kann 0 oder negativ sein
  backupReminderIntervalInDays: number;
  currencyCode: string; // ISO 4217, zum Beispiel "USD"
  earningsTrackingEnabled: Bool;
  defaultHourlyRate: number | null;
  weekStartsOn: number; // 0 = Sonntag ... 6 = Samstag
  alternativeColor: string | null; // Open-Color-Wert, oder null für Grau
  defaultTimelineId: Id | null; // -> timeline.id
  lastUsedTimelineId: Id | null; // -> timeline.id
  forgottenRemindersMode: "both" | "start" | "stop" | "off";
}

stopwatch: {
  id: Id;
  description: string;
  adjustMinutes: number; // Ganzzahl; kann negativ sein
  isActive: Bool;
  isExpanded: Bool;
  hourlyRate: number | null; // gespeicherter Stundensatz dieser Stoppuhr
  rateCurrencyCodeAtWrite: string | null; // Währung des Arbeitsbereichs beim Speichern des Stundensatzes
  isHourlyRateManual: Bool | null;
  targetMinutes: number | null;
  timelineId: Id | null; // -> timeline.id; null bedeutet Keine Zeitleiste
}

tag: {
  id: Id;
  name: string;
  hourlyRate: number | null;
  color: string | null; // Open-Color-Wert wie "blue.6", oder null
  isPinned: Bool | null; // in der Tag-Liste angeheftet; null gilt als angeheftet
  archivedAt: number | null; // Unix-Millisekunden bei der Archivierung; null lässt den Tag aktiv
}

timeline: {
  id: Id;
  name: string;
  parentTimelineId: Id | null; // -> timeline.id der übergeordneten Zeitleiste, oder null ohne Gruppe
  hourlyRate: number | null;
  isBillingBoundary: Bool;
  archivedAt: number | null; // Unix-Millisekunden bei der Archivierung; null lässt die Zeitleiste aktiv
}

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

exportOptions: {
  // höchstens eine Zeile; die meisten Importe verwenden stattdessen ein leeres Array
  // Verwende bevorzugt die id "4WnR17vMjH50XmwttC218Q" (die feste App-Zeile). Andere IDs werden
  // beim Import durch diesen Wert ersetzt.
  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";
}
```

## Prüfsumme

`envelope.checksum` ist der SHA-256-Hash von `JSON.stringify(data)` in hexadezimalen Kleinbuchstaben.
Dabei ist `data` das innere Objekt, nicht das gesamte äußere Objekt.

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()
```

## Minimales Beispiel

```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": []
  }
}
```

## Häufige Fehler

Vermeide diese Fehler:

- Verwende für `Bool`-Felder nicht `true` oder `false`, sondern `0` oder `1`.
- Verwende für `interval.start` und `interval.end` keine Sekunden, sondern Millisekunden.
- Verwende für Zeitstempel von Zeilen keine ISO-Zeichenfolgen. Nur das Feld `timestamp` des äußeren Objekts ist eine ISO-Zeichenfolge.
- Füge dem äußeren Objekt kein Feld `timezone` hinzu. Für die Umwandlung kann die Zeitzone der Quelle nötig sein, TimeRetain speichert Zeilenzeiten aber als Unix-Millisekunden.
- Lasse die Prüfsumme nicht weg. Berechne sie aus dem inneren Objekt `data`, nicht aus dem gesamten äußeren Objekt.
- Verwende für `data` vor der Berechnung der Prüfsumme keine zusätzlichen Einrückungen oder Zeilenumbrüche und sortiere seine Schlüssel nicht alphabetisch. Verwende genau die oben genannte Schlüsselreihenfolge.
- Lasse keines der sieben `data`-Arrays weg, auch wenn es leer ist.
- Gruppiere Kunden oder Projekte nicht mit Tags. Verwende `timeline`-Zeilen, um sie unter zusammengehörigen Zeitleisten zu gruppieren.
- Füge `tag.parentTagId` nicht hinzu. Tags können keine Gruppen bilden.
- Erzeuge keine Zeitleisten mit fehlenden übergeordneten Gruppen oder Gruppierungszyklen.
- Wenn die Quelle einen bekannten Kunden, ein Projekt, ein Konto oder einen Arbeitsbereich enthält, setze `stopwatch.timelineId`.
- Erzeuge für Stoppuhren mit Tags passende `tagStopwatch`-Zeilen.