Ir para o conteúdo
TimeRetain
Abrir aplicativo

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.

  1. Exporte os dados da ferramenta atual como CSV ou planilha.
  2. Envie o arquivo com as instruções abaixo a um assistente de IA para criar um arquivo JSON do TimeRetain.
  3. 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.