Zaimportuj dane do TimeRetain
TimeRetain importuje dane czasu z pliku JSON. Najszybciej uzyskasz taki plik, prosząc asystenta AI o przekształcenie dotychczasowych danych.
- Wyeksportuj dane z dotychczasowego narzędzia jako CSV lub arkusz.
- Wyślij plik wraz z poniższą instrukcją do asystenta AI, aby utworzyć plik JSON TimeRetain.
- Otwórz Dane i synchronizacja, wybierz Importuj i prześlij plik JSON.
Ponowny import jest bezpieczny: istniejące wpisy są aktualizowane, bez duplikatów.
Instrukcja dla asystenta
Przekształć załączony plik na JSON TimeRetain v0.12 zgodnie z poniższą specyfikacją.
Używaj deterministycznych identyfikatorów, które nie zmieniają się dla tych samych danych wejściowych.
Wyprowadź każdy identyfikator ze stabilnego klucza wiersza źródłowego.
Dzięki temu ponowny import aktualizuje istniejące wpisy zamiast tworzyć duplikaty.
Zaimportuj wynikowy plik przez Dane i synchronizacja → Importuj.
## Import JSON TimeRetain v0.12
Użyj tej specyfikacji do przekształcenia zewnętrznych danych o czasie w plik JSON TimeRetain.
## Sposób konwersji
Wykonaj następujące kroki:
1. Wybierz stabilny zewnętrzny klucz wiersza, na przykład oryginalny identyfikator. Jeśli go nie ma, zbuduj klucz ze stałych kolumn źródłowych, takich jak data, czas rozpoczęcia, czas zakończenia, klient, projekt, zadanie i opis.
2. Wyprowadź każdy identyfikator TimeRetain z `first 16 bytes of SHA-256(utf8(externalId))`. Zakoduj go jako bezpieczny dla URL Base64 bez dopełnienia. Dla powiązanych wierszy użyj odrębnych danych wejściowych do wyznaczania identyfikatorów. Przykłady: `stopwatch:<externalId>`, `interval:<externalId>`, `timeline:<client>/<project>`, `tag:<tagName>` i `tagStopwatch:<externalId>:<tagName>`.
3. Ustaw `version` w zewnętrznym obiekcie na `"0.12"`. Ten obiekt otacza `data`. Ustaw jego `timestamp` jako ciąg ISO-8601.
4. Umieść w `data` wszystkie siedem tablic w tej kolejności: `interval`, `preferences`, `stopwatch`, `tag`, `timeline`, `tagStopwatch`, `exportOptions`. Tablice mogą być puste oprócz `preferences` i `timeline`. `exportOptions` zwykle jest `[]`.
5. Przedstaw klientów, projekty, obszary robocze, konta oraz inne grupy rozliczeniowe i raportowe jako wiersze `timeline`. Powiązane osie czasu grupuj pod szerszymi osiami. Dla każdego stopera ustaw `timelineId` na najlepiej pasującą szczegółową oś.
6. Nazwy czynności, takich jak spotkania, programowanie, pisanie, administracja, badania i wsparcie, przedstaw jako płaskie wiersze `tag`. W v0.12 tagów nie można grupować.
7. Dla `interval.start` i `interval.end` użyj czasu Unix w milisekundach. Jeśli źródło zawiera lokalne daty lub godziny, przed konwersją zastosuj jego strefę czasową. Tylko dla trwających wpisów ustaw `interval.end` na `null`.
8. Każde pole `Bool` musi mieć wartość `0` lub `1`. Nie używaj wartości logicznych JSON.
9. Oblicz `checksum` jako skrót SHA-256 z `JSON.stringify(data)` w zapisie szesnastkowym z małymi literami. `data` to wewnętrzny obiekt z kluczami w podanej wyżej kolejności. Nie uwzględniaj pól obiektu zewnętrznego.
Import dodaje nowe wiersze lub aktualizuje istniejące na podstawie `id`.
Deterministyczne identyfikatory zapobiegają duplikatom podczas ponownego importu.
## Zasady dotyczące osi czasu
Stosuj poniższe zasady:
- Jeśli źródło nie ma pogrupowanych osi czasu klientów ani projektów, dodaj domyślny wiersz osi Personal.
- Oś czasu poza grupą jest już granicą rozliczenia i może być rozliczana samodzielnie. Ustaw `isBillingBoundary: 1` tylko wtedy, gdy pogrupowana oś także może być rozliczana samodzielnie.
- Ustaw `parentTimelineId` na `timeline.id` grupy nadrzędnej albo `null`, jeśli jej nie ma. Nie twórz cykli ani odwołań do nieistniejących osi czasu.
- Zarchiwizowane osie czasu zachowują historię, ale nie można wybierać ich dla nowych wpisów. W normalnym imporcie używaj `archivedAt: null`.
- Zarchiwizowane tagi zachowują historię, ale nie można wybierać ich dla nowych wpisów. W normalnym imporcie używaj `archivedAt: null`.
- Dla domyślnych stawek projektu lub klienta użyj `timeline.hourlyRate`. Stawkę określoną dla każdego wpisu umieść w `stopwatch.hourlyRate`.
- Gdy `stopwatch.hourlyRate` nie jest `null`, ustaw `rateCurrencyCodeAtWrite` na walutę obszaru roboczego w chwili importu. Jeśli wpis nie ma ustalonej stawki, ustaw oba pola na `null`.
- Użyj `timelineId: null` tylko wtedy, gdy wpis celowo nie należy do żadnej osi czasu.
- Ustaw `preferences.defaultTimelineId` i `preferences.lastUsedTimelineId` na istniejącą, niezarchiwizowaną oś czasu, zwykle Personal.
## Typy
Użyj następujących typów wartości:
- `Id` to 22-znakowy ciąg Base64 bez dopełnienia, bezpieczny dla URL. Używa znaków `A-Z`, `a-z`, `0-9`, `-` i `_` oraz koduje 16 bajtów.
- `Bool` to `0` albo `1`, nigdy `false` lub `true`.
- Znaczniki czasu wierszy (`interval.start`, `interval.end`) są czasem Unix w milisekundach. `timestamp` zewnętrznego obiektu jest ciągiem ISO-8601. Wszystkie liczby muszą być skończone.
## Zewnętrzny obiekt
```json
{
"version": "0.12",
"timestamp": "<ISO-8601 string>",
"checksum": "<sha256 hex of JSON.stringify(data)>",
"data": {
"interval": [],
"preferences": [],
"stopwatch": [],
"tag": [],
"timeline": [],
"tagStopwatch": [],
"exportOptions": []
}
}
```
Umieść w `data` siedem kluczy dokładnie w tej kolejności: `interval`, `preferences`, `stopwatch`, `tag`, `timeline`, `tagStopwatch`, `exportOptions`.
## Schemat
```ts
interval: {
id: Id;
stopwatchId: Id; // -> stopwatch.id
start: number; // czas Unix w ms
end: number | null; // czas Unix w ms lub null dla trwającego wpisu
}
preferences: {
// dokładnie jeden wiersz
id: Id;
useMilitaryTime: Bool;
mergeOverlappingStopwatches: Bool; // starsze pole, zwykle 0
useAdjust: Bool;
adjustDefault: number; // minuty, może być 0 lub wartość ujemna
backupReminderIntervalInDays: number;
currencyCode: string; // ISO 4217, na przykład "USD"
earningsTrackingEnabled: Bool;
defaultHourlyRate: number | null;
weekStartsOn: number; // 0 = niedziela ... 6 = sobota
alternativeColor: string | null; // token Open Color lub null dla szarości
defaultTimelineId: Id | null; // -> timeline.id
lastUsedTimelineId: Id | null; // -> timeline.id
forgottenRemindersMode: "both" | "start" | "stop" | "off";
}
stopwatch: {
id: Id;
description: string;
adjustMinutes: number; // liczba całkowita, może być ujemna
isActive: Bool;
isExpanded: Bool;
hourlyRate: number | null; // zapisana stawka tego stopera
rateCurrencyCodeAtWrite: string | null; // waluta obszaru roboczego w chwili zapisu stawki
isHourlyRateManual: Bool | null;
targetMinutes: number | null;
timelineId: Id | null; // -> timeline.id; null oznacza Brak osi czasu
}
tag: {
id: Id;
name: string;
hourlyRate: number | null;
color: string | null; // token Open Color, np. "blue.6", lub null
isPinned: Bool | null; // przypięty na liście tagów, null oznacza przypięcie
archivedAt: number | null; // czas archiwizacji Unix w ms, null zachowuje aktywny tag
}
timeline: {
id: Id;
name: string;
parentTimelineId: Id | null; // -> timeline.id osi nadrzędnej lub null, jeśli jej nie ma
hourlyRate: number | null;
isBillingBoundary: Bool;
archivedAt: number | null; // czas archiwizacji Unix w ms, null zachowuje aktywną oś czasu
}
tagStopwatch: {
id: Id;
tagId: Id; // -> tag.id
stopwatchId: Id; // -> stopwatch.id
}
exportOptions: {
// najwyżej jeden wiersz, większość importów używa pustej tablicy
// Preferuj id "4WnR17vMjH50XmwttC218Q" (jedyny obiekt tego typu w aplikacji).
// Import zmienia inne identyfikatory na tę wartość.
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";
}
```
## Suma kontrolna
`envelope.checksum` to skrót SHA-256 z `JSON.stringify(data)` w zapisie szesnastkowym z małymi literami.
`data` oznacza wewnętrzny obiekt, a nie całą otoczkę.
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()
```
## Minimalna struktura przykładu
```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": []
}
}
```
## Typowe błędy
Unikaj następujących błędów:
- Nie używaj `true` ani `false` w polach `Bool`. Używaj `0` lub `1`.
- Nie używaj sekund dla `interval.start` ani `interval.end`. Używaj milisekund.
- Nie używaj ciągów ISO dla znaczników czasu wierszy. Tylko zewnętrzny `timestamp` jest ciągiem ISO.
- Nie dodawaj pola `timezone` do zewnętrznego obiektu. Konwersja może wymagać strefy czasowej źródła, ale TimeRetain zapisuje czasy wierszy jako milisekundy Unix.
- Nie pomijaj sumy kontrolnej. Oblicz ją z wewnętrznego `data`, a nie z całego obiektu.
- Przed obliczeniem sumy nie serializuj `data` z wcięciami ani nie sortuj kluczy alfabetycznie. Zachowaj dokładną kolejność kluczy podaną wyżej.
- Nie pomijaj żadnej z siedmiu tablic `data`, nawet gdy są puste.
- Nie grupuj klientów ani projektów za pomocą tagów. Użyj wierszy `timeline` i powiązanych osi nadrzędnych.
- Nie dodawaj `tag.parentTagId`. Tagów nie można grupować.
- Nie twórz osi czasu z brakującą grupą nadrzędną ani cykli grupowania.
- Jeśli w źródle określono klienta, projekt, konto lub obszar roboczy, ustaw `stopwatch.timelineId`.
- Dla stoperów z tagami utwórz pasujące wiersze `tagStopwatch`.