Importa tus datos a TimeRetain
TimeRetain importa datos de tiempo desde un archivo JSON. Un asistente de IA puede crear ese archivo rápidamente a partir de tus datos.
- Exporta los datos de tu herramienta actual como CSV u hoja de cálculo.
- Envía el archivo con las instrucciones de abajo a un asistente de IA para crear un archivo JSON de TimeRetain.
- Abre Datos y sincronización, elige Importar y sube el archivo JSON.
Volver a importar es seguro: las entradas existentes se actualizan y no se duplican.
Instrucciones para el asistente
Convierte el archivo adjunto a JSON de TimeRetain v0.12 según la especificación siguiente.
Usa ID deterministas que permanezcan iguales para los mismos datos de entrada.
Deriva cada ID de una clave estable de la fila de origen.
Así, las importaciones repetidas actualizan las entradas existentes en vez de duplicarlas.
Importa el archivo resultante desde Datos y sincronización → Importar.
## Importación JSON de TimeRetain v0.12
Usa esta especificación para convertir datos externos de registro de tiempo en un archivo JSON de TimeRetain.
## Pasos para la conversión
Sigue estos pasos:
1. Elige una clave externa estable para cada fila, como su ID original. Si no existe, créala a partir de columnas estables del origen, como fecha, hora de inicio, hora de fin, cliente, proyecto, tarea y descripción.
2. Deriva cada ID de TimeRetain de `first 16 bytes of SHA-256(utf8(externalId))`. Codifícalo en Base64 seguro para URL y sin relleno. Usa valores de partida distintos para las filas relacionadas. Algunos ejemplos son `stopwatch:<externalId>`, `interval:<externalId>`, `timeline:<client>/<project>`, `tag:<tagName>` y `tagStopwatch:<externalId>:<tagName>`.
3. Establece `version` del objeto contenedor en `"0.12"`. Este objeto contiene `data`. Establece su `timestamp` como una cadena ISO 8601.
4. Incluye los siete arrays dentro de `data` en este orden: `interval`, `preferences`, `stopwatch`, `tag`, `timeline`, `tagStopwatch`, `exportOptions`. Pueden estar vacíos excepto `preferences` y `timeline`. `exportOptions` suele ser `[]`.
5. Representa clientes, proyectos, espacios de trabajo, cuentas y otros grupos de facturación o informes mediante filas `timeline`. Agrupa las líneas de tiempo relacionadas bajo otras más amplias. Establece el `timelineId` de cada cronómetro en la línea de tiempo específica que mejor corresponda.
6. Representa actividades como reuniones, programación, redacción, administración, investigación y asistencia mediante filas `tag` sin jerarquía. Las etiquetas no pueden formar grupos en v0.12.
7. Usa tiempo Unix en milisegundos para `interval.start` y `interval.end`. Si el origen usa fechas u horas locales, aplica su zona horaria antes de convertirlas. Usa `null` para `interval.end` solo en entradas que siguen en curso.
8. Usa `0` o `1` en todos los campos `Bool`. No uses booleanos JSON.
9. Calcula `checksum` como el SHA-256 de `JSON.stringify(data)` en hexadecimal y minúsculas. Aquí, `data` es el objeto interno con el orden de claves indicado arriba. No incluyas los campos del objeto contenedor en la suma de comprobación.
La importación inserta filas nuevas o actualiza las existentes según su `id`.
Los ID deterministas evitan duplicados en importaciones repetidas.
## Reglas para las líneas de tiempo
Aplica estas reglas:
- Si el origen no tiene líneas de tiempo agrupadas por cliente o proyecto, incluye una fila predeterminada para la línea de tiempo Personal.
- Una línea de tiempo fuera de un grupo ya es un límite de facturación y puede facturarse por separado. Establece `isBillingBoundary: 1` solo si una línea de tiempo agrupada también puede facturarse por separado.
- Establece `parentTimelineId` en el `timeline.id` del grupo superior, o en `null` si no existe. No crees ciclos de agrupación ni referencias a líneas de tiempo inexistentes.
- Las líneas de tiempo archivadas siguen siendo válidas para el historial, pero no se pueden elegir para entradas nuevas. En importaciones normales, usa `archivedAt: null`.
- Las etiquetas archivadas siguen siendo válidas para el historial, pero no se pueden elegir para entradas nuevas. En importaciones normales, usa `archivedAt: null`.
- Para las tarifas predeterminadas de proyectos o clientes, usa `timeline.hourlyRate`. Guarda la tarifa determinada para cada entrada en `stopwatch.hourlyRate`.
- Si `stopwatch.hourlyRate` no es `null`, establece `rateCurrencyCodeAtWrite` en la moneda del espacio de trabajo durante la importación. Si la entrada no tiene una tarifa determinada, establece ambos campos en `null`.
- Usa `timelineId: null` solo cuando la entrada pertenezca expresamente a Sin línea de tiempo.
- Establece `preferences.defaultTimelineId` y `preferences.lastUsedTimelineId` en una línea de tiempo existente que no esté archivada, normalmente Personal.
## Tipos
Usa estos tipos de valores:
- `Id` es una cadena Base64 de 22 caracteres, segura para URL y sin relleno. Usa `A-Z`, `a-z`, `0-9`, `-` y `_`, y codifica 16 bytes.
- `Bool` es `0` o `1`, nunca `false` ni `true`.
- Las marcas de tiempo de las filas (`interval.start`, `interval.end`) usan tiempo Unix en milisegundos. El `timestamp` del objeto contenedor es una cadena ISO 8601. Todos los números deben ser finitos.
## Objeto contenedor
```json
{
"version": "0.12",
"timestamp": "<ISO-8601 string>",
"checksum": "<sha256 hex of JSON.stringify(data)>",
"data": {
"interval": [],
"preferences": [],
"stopwatch": [],
"tag": [],
"timeline": [],
"tagStopwatch": [],
"exportOptions": []
}
}
```
Incluye las siete claves en `data` exactamente en este orden: `interval`, `preferences`, `stopwatch`, `tag`, `timeline`, `tagStopwatch`, `exportOptions`.
## Esquema
```ts
interval: {
id: Id;
stopwatchId: Id; // -> stopwatch.id
start: number; // Unix ms
end: number | null; // Unix ms, or null if in progress
}
preferences: {
// exactly one row
id: Id;
useMilitaryTime: Bool;
mergeOverlappingStopwatches: Bool; // legacy field; usually 0
useAdjust: Bool;
adjustDefault: number; // minutes; may be 0 or negative
backupReminderIntervalInDays: number;
currencyCode: string; // ISO 4217, for example "USD"
earningsTrackingEnabled: Bool;
defaultHourlyRate: number | null;
weekStartsOn: number; // 0 = Sunday ... 6 = Saturday
alternativeColor: string | null; // Open Color token, or null for gray
defaultTimelineId: Id | null; // -> timeline.id
lastUsedTimelineId: Id | null; // -> timeline.id
forgottenRemindersMode: "both" | "start" | "stop" | "off";
}
stopwatch: {
id: Id;
description: string;
adjustMinutes: number; // integer; may be negative
isActive: Bool;
isExpanded: Bool;
hourlyRate: number | null; // stored rate for this stopwatch
rateCurrencyCodeAtWrite: string | null; // workspace currency when rate was stored
isHourlyRateManual: Bool | null;
targetMinutes: number | null;
timelineId: Id | null; // -> timeline.id; null means No timeline
}
tag: {
id: Id;
name: string;
hourlyRate: number | null;
color: string | null; // Open Color token like "blue.6", or null
isPinned: Bool | null; // pinned in the tag list; null is treated as pinned
archivedAt: number | null; // Unix ms when archived; null keeps the tag active
}
timeline: {
id: Id;
name: string;
parentTimelineId: Id | null; // -> timeline.id grouped under, or null when not grouped
hourlyRate: number | null;
isBillingBoundary: Bool;
archivedAt: number | null; // Unix ms when archived; null keeps the timeline active
}
tagStopwatch: {
id: Id;
tagId: Id; // -> tag.id
stopwatchId: Id; // -> stopwatch.id
}
exportOptions: {
// at most one row; most imports use an empty array instead
// Prefer id "4WnR17vMjH50XmwttC218Q" (the app singleton). Other ids are
// rewritten to that value on 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";
}
```
## Suma de comprobación
`envelope.checksum` es el hash SHA-256 de `JSON.stringify(data)` en hexadecimal y minúsculas.
Aquí, `data` es el objeto interno, no todo el objeto contenedor.
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()
```
## Estructura mínima de ejemplo
```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": []
}
}
```
## Errores frecuentes
Evita estos errores:
- No uses `true` ni `false` en campos `Bool`. Usa `0` o `1`.
- No uses segundos para `interval.start` ni `interval.end`. Usa milisegundos.
- No uses cadenas ISO para las marcas de tiempo de las filas. Solo el `timestamp` del objeto contenedor es una cadena ISO.
- No añadas un campo `timezone` al objeto contenedor. La conversión puede requerir la zona horaria de origen, pero TimeRetain guarda las horas de las filas en milisegundos Unix.
- No omitas la suma de comprobación. Calcúlala a partir del objeto interno `data`, no del contenedor completo.
- No reformatees `data` ni ordenes sus claves alfabéticamente antes de calcular la suma de comprobación. Usa exactamente el orden de claves anterior.
- No omitas ninguno de los siete arrays de `data`, aunque esté vacío.
- No agrupes clientes ni proyectos con etiquetas. Usa filas `timeline` para agruparlos bajo líneas de tiempo relacionadas.
- No añadas `tag.parentTagId`. Las etiquetas no pueden formar grupos.
- No crees líneas de tiempo con grupos superiores inexistentes ni ciclos de agrupación.
- Si el origen indica un cliente, proyecto, cuenta o espacio de trabajo conocido, establece `stopwatch.timelineId`.
- Para los cronómetros con etiquetas, crea las filas `tagStopwatch` correspondientes.