Importe seus dados para o TimeRetain
O TimeRetain importa dados de tempo de um arquivo JSON. A forma mais rápida de obter um é pedir a um assistente de IA para converter seus dados.
- Exporte os dados da ferramenta atual como CSV ou planilha.
- Envie o arquivo com as instruções abaixo a um assistente de IA para criar um arquivo JSON do TimeRetain.
- Abra Dados e sincronização, escolha Importar e envie o arquivo JSON.
Importar de novo é seguro: os registros existentes são atualizados, sem duplicar.
Instruções para o assistente
Converta o arquivo anexado para JSON do TimeRetain v0.12 seguindo a especificação abaixo.
Use IDs determinísticos, que permanecem iguais para a mesma entrada.
Derive cada ID de uma chave estável da linha de origem.
Assim, importações repetidas atualizam os registros existentes em vez de duplicá-los.
Importe o arquivo resultante em Dados e sincronização → Importar.
## Importação de JSON do TimeRetain v0.12
Use esta especificação para converter dados externos de controle de tempo em um arquivo JSON do TimeRetain.
## Etapas de conversão
Siga estas etapas:
1. Escolha uma chave externa estável para cada linha, como o ID original. Se não houver uma, crie-a a partir de colunas estáveis da origem. Elas podem incluir data, hora de início, hora de término, cliente, projeto, tarefa e descrição.
2. Derive cada ID do TimeRetain de `first 16 bytes of SHA-256(utf8(externalId))`. Codifique-o em Base64 seguro para URL, sem preenchimento. Use valores iniciais diferentes para linhas relacionadas. Exemplos: `stopwatch:<externalId>`, `interval:<externalId>`, `timeline:<client>/<project>`, `tag:<tagName>` e `tagStopwatch:<externalId>:<tagName>`.
3. Defina `version` do objeto externo como `"0.12"`. Esse objeto envolve `data`. Defina seu `timestamp` como uma string ISO 8601.
4. Inclua os sete arrays dentro de `data` nesta ordem: `interval`, `preferences`, `stopwatch`, `tag`, `timeline`, `tagStopwatch`, `exportOptions`. Eles podem estar vazios, exceto `preferences` e `timeline`. `exportOptions` normalmente é `[]`.
5. Represente clientes, projetos, espaços de trabalho, contas e outros grupos de faturamento ou relatórios como linhas `timeline`. Agrupe linhas do tempo relacionadas sob linhas do tempo mais amplas. Defina o `timelineId` de cada cronômetro como a linha do tempo específica mais adequada.
6. Represente atividades como reuniões, programação, redação, administração, pesquisa e suporte como linhas `tag` sem hierarquia. As etiquetas não podem formar grupos na v0.12.
7. Use tempo Unix em milissegundos para `interval.start` e `interval.end`. Se a origem usa datas ou horários locais, aplique seu fuso horário antes da conversão. Use `null` para `interval.end` somente em registros ainda em andamento.
8. Use `0` ou `1` em todos os campos `Bool`. Não use valores booleanos de JSON.
9. Calcule `checksum` como SHA-256 de `JSON.stringify(data)` em hexadecimal minúsculo. Aqui, `data` é o objeto interno com a ordem de chaves acima. Não inclua campos do objeto externo na soma de verificação.
A importação insere novas linhas ou atualiza linhas existentes pelo campo `id`.
IDs determinísticos evitam duplicatas em importações repetidas.
## Regras para linhas do tempo
Aplique estas regras:
- Se a origem não tiver linhas do tempo agrupadas por cliente ou projeto, inclua uma linha padrão chamada Personal.
- Uma linha do tempo fora de um grupo já é um limite de faturamento, ou seja, um grupo que pode ser faturado separadamente. Defina `isBillingBoundary: 1` somente quando uma linha do tempo agrupada também puder ser faturada separadamente.
- Defina `parentTimelineId` como o `timeline.id` do grupo principal, ou `null` se não houver um. Não crie ciclos de agrupamento nem referências a linhas do tempo inexistentes.
- Linhas do tempo arquivadas continuam válidas para o histórico, mas não podem ser escolhidas para novos registros. Em importações normais, use `archivedAt: null`.
- Etiquetas arquivadas continuam válidas para o histórico, mas não podem ser escolhidas para novos registros. Em importações normais, use `archivedAt: null`.
- Para valores por hora padrão por projeto ou cliente, use `timeline.hourlyRate`. Coloque o valor por hora definido para cada registro em `stopwatch.hourlyRate`.
- Se `stopwatch.hourlyRate` não for `null`, defina `rateCurrencyCodeAtWrite` como a moeda do espaço de trabalho durante a importação. Se o registro não tiver valor por hora definido, defina ambos os campos como `null`.
- Use `timelineId: null` somente quando um registro pertencer de propósito a Sem linha do tempo.
- Defina `preferences.defaultTimelineId` e `preferences.lastUsedTimelineId` como uma linha do tempo existente e não arquivada, geralmente Personal.
## Tipos
Use estes tipos de valor:
- `Id` é uma string Base64 de 22 caracteres, segura para URL e sem preenchimento. Ela usa `A-Z`, `a-z`, `0-9`, `-` e `_`, e codifica 16 bytes.
- `Bool` é `0` ou `1`, nunca `false` ou `true`.
- Os horários das linhas (`interval.start`, `interval.end`) usam tempo Unix em milissegundos. O `timestamp` do objeto externo é uma string ISO 8601. Todos os números devem ser finitos.
## Objeto externo
```json
{
"version": "0.12",
"timestamp": "<ISO-8601 string>",
"checksum": "<sha256 hex of JSON.stringify(data)>",
"data": {
"interval": [],
"preferences": [],
"stopwatch": [],
"tag": [],
"timeline": [],
"tagStopwatch": [],
"exportOptions": []
}
}
```
Inclua as sete chaves dentro de `data` exatamente nesta ordem: `interval`, `preferences`, `stopwatch`, `tag`, `timeline`, `tagStopwatch`, `exportOptions`.
## Esquema
```ts
interval: {
id: Id;
stopwatchId: Id; // -> stopwatch.id
start: number; // Milissegundos Unix
end: number | null; // Milissegundos Unix ou null se estiver em andamento
}
preferences: {
// Exatamente uma linha
id: Id;
useMilitaryTime: Bool;
mergeOverlappingStopwatches: Bool; // Campo legado; geralmente 0
useAdjust: Bool;
adjustDefault: number; // Minutos; pode ser 0 ou negativo
backupReminderIntervalInDays: number;
currencyCode: string; // ISO 4217, por exemplo "USD"
earningsTrackingEnabled: Bool;
defaultHourlyRate: number | null;
weekStartsOn: number; // 0 = domingo ... 6 = sábado
alternativeColor: string | null; // Token do Open Color ou null para cinza
defaultTimelineId: Id | null; // -> timeline.id
lastUsedTimelineId: Id | null; // -> timeline.id
forgottenRemindersMode: "both" | "start" | "stop" | "off";
}
stopwatch: {
id: Id;
description: string;
adjustMinutes: number; // Número inteiro; pode ser negativo
isActive: Bool;
isExpanded: Bool;
hourlyRate: number | null; // Valor por hora salvo para este cronômetro
rateCurrencyCodeAtWrite: string | null; // Moeda do espaço de trabalho quando o valor por hora foi salvo
isHourlyRateManual: Bool | null;
targetMinutes: number | null;
timelineId: Id | null; // -> timeline.id; null significa Sem linha do tempo
}
tag: {
id: Id;
name: string;
hourlyRate: number | null;
color: string | null; // Token do Open Color, como "blue.6", ou null
isPinned: Bool | null; // Fixada na lista de etiquetas; null equivale a fixada
archivedAt: number | null; // Milissegundos Unix do arquivamento; null mantém a etiqueta ativa
}
timeline: {
id: Id;
name: string;
parentTimelineId: Id | null; // -> timeline.id da linha superior ou null se não estiver agrupada
hourlyRate: number | null;
isBillingBoundary: Bool;
archivedAt: number | null; // Milissegundos Unix do arquivamento; null mantém a linha do tempo ativa
}
tagStopwatch: {
id: Id;
tagId: Id; // -> tag.id
stopwatchId: Id; // -> stopwatch.id
}
exportOptions: {
// No máximo uma linha; a maioria das importações usa um array vazio
// Prefira o id "4WnR17vMjH50XmwttC218Q" (registro único do aplicativo).
// A importação substitui qualquer outro id por esse valor.
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";
}
```
## Soma de verificação
`envelope.checksum` é o hash SHA-256 de `JSON.stringify(data)` em hexadecimal minúsculo.
Aqui, `data` é o objeto interno, não o objeto externo inteiro.
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()
```
## Estrutura mínima de exemplo
```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": []
}
}
```
## Erros comuns
Evite estes erros:
- Não use `true` nem `false` nos campos `Bool`. Use `0` ou `1`.
- Não use segundos em `interval.start` nem `interval.end`. Use milissegundos.
- Não use strings ISO para os horários das linhas. Somente o `timestamp` do objeto externo é uma string ISO.
- Não adicione um campo `timezone` ao objeto externo. A conversão pode precisar do fuso horário da origem, mas o TimeRetain armazena os horários das linhas em milissegundos Unix.
- Não omita a soma de verificação. Calcule-a a partir do objeto interno `data`, não do objeto externo inteiro.
- Não acrescente espaços ou quebras de linha a `data` nem ordene suas chaves alfabeticamente antes de calcular a soma de verificação. Use exatamente a ordem de chaves acima.
- Não omita nenhum dos sete arrays de `data`, mesmo que esteja vazio.
- Não agrupe clientes ou projetos com etiquetas. Use linhas `timeline` para agrupá-los sob linhas do tempo relacionadas.
- Não adicione `tag.parentTagId`. Etiquetas não podem formar grupos.
- Não crie linhas do tempo com grupos principais inexistentes ou ciclos de agrupamento.
- Se a origem tiver um cliente, projeto, conta ou espaço de trabalho conhecido, defina `stopwatch.timelineId`.
- Para cronômetros com etiquetas, crie as linhas `tagStopwatch` correspondentes.