データをTimeRetainに取り込む
TimeRetainはJSONファイルから時間データを取り込みます。AIアシスタントを使うと、既存データからファイルをすばやく作れます。
- 今使っているツールからCSVまたは表計算ファイルとしてデータを出力します。
- そのファイルを以下の指示とともにAIアシスタントに渡し、TimeRetain用のJSONファイルを作ります。
- 次に、 データと同期を開き、「インポート」を選んでJSONファイルをアップロードします。
再度取り込んでも安全です。既存の記録が更新され、重複した記録は作られません。
AIアシスタントへの指示
添付ファイルを、以下の仕様に従ってTimeRetain v0.12のJSONに変換してください。
同じ入力から常に同じIDを生成する方法を使用してください。
各IDは、元データの行にある安定したキーから作成してください。
そうすれば、繰り返し取り込んでも重複せず、既存の記録が更新されます。
できたファイルは「データと同期」→「インポート」から取り込んでください。
## TimeRetain v0.12 JSONインポート
この仕様に従い、外部の時間記録データをTimeRetain用JSONファイルに変換してください。
## 変換手順
次の順番で作業してください。
1. 元データの行IDなど、変わらない外部行キーを選びます。なければ、日付、開始時刻、終了時刻、顧客、プロジェクト、作業、説明など、変わらない列の値から作ります。
2. 各TimeRetain IDは`first 16 bytes of SHA-256(utf8(externalId))`から作ります。パディングなしのURLセーフなBase64で符号化してください。関連する行には異なるシードを使います。例: `stopwatch:<externalId>`、`interval:<externalId>`、`timeline:<client>/<project>`、`tag:<tagName>`、`tagStopwatch:<externalId>:<tagName>`。
3. 外側のオブジェクトの`version`を`"0.12"`にします。外側のオブジェクトは`data`を包みます。その`timestamp`にはISO-8601形式の文字列を設定します。
4. `data`には7つの配列を次の順序ですべて含めます: `interval`、`preferences`、`stopwatch`、`tag`、`timeline`、`tagStopwatch`、`exportOptions`。`preferences`と`timeline`以外は空でも構いません。`exportOptions`は通常`[]`です。
5. 顧客、プロジェクト、ワークスペース、アカウント、その他の請求・報告単位は`timeline`行で表します。関連するタイムラインを上位のタイムラインでまとめます。各ストップウォッチの`timelineId`には、最もよく合う具体的なタイムラインを設定します。
6. 会議、開発、執筆、事務、調査、サポートなどの活動名は、階層のない`tag`行で表します。v0.12ではタグをグループ化できません。
7. `interval.start`と`interval.end`にはUnix時刻のミリ秒値を使います。元データが現地の日付や時刻なら、変換前にそのタイムゾーンを適用します。進行中の記録だけ、`interval.end`を`null`にします。
8. すべての`Bool`フィールドに`0`または`1`を使ってください。JSONの真偽値は使いません。
9. `JSON.stringify(data)`のSHA-256を小文字の16進数にして`checksum`を計算します。`data`は上記のキー順を持つ内側のオブジェクトです。外側のフィールドはチェックサムに含めません。
インポートでは、`id`を基準に新しい行を追加するか既存の行を更新します。
同じ入力から同じIDを生成することで、同じデータを再度取り込んでも重複しません。
## タイムラインの規則
次の規則を適用してください。
- 元データに顧客やプロジェクトのグループがない場合、標準のPersonalタイムライン行を含めます。
- グループ外のタイムラインは、それ自体が独立して請求できる単位です。グループ内のタイムラインも独立して請求できる場合にだけ、`isBillingBoundary: 1`を設定します。
- `parentTimelineId`には親グループの`timeline.id`を設定し、親がなければ`null`にします。循環や存在しないタイムラインへの参照を作らないでください。
- アーカイブ済みのタイムラインは履歴として有効ですが、新しい記録には選べません。通常のインポートでは`archivedAt: null`を使います。
- アーカイブ済みのタグも履歴として有効ですが、新しい記録には選べません。通常のインポートでは`archivedAt: null`を使います。
- プロジェクトや顧客の標準単価には`timeline.hourlyRate`を使います。各記録で決まった単価は`stopwatch.hourlyRate`に設定します。
- `stopwatch.hourlyRate`が`null`でない場合、`rateCurrencyCodeAtWrite`をインポート時のワークスペース通貨にします。単価が決まらない記録では、両方を`null`にします。
- 意図的に「タイムラインなし」に属する記録だけ、`timelineId: null`を使います。
- `preferences.defaultTimelineId`と`preferences.lastUsedTimelineId`には、存在する未アーカイブのタイムライン、通常はPersonalを設定します。
## 型
次の値の型を使用してください。
- `Id`はパディングなし、22文字のURLセーフなBase64文字列です。`A-Z`、`a-z`、`0-9`、`-`、`_`を使い、16バイトを表します。
- `Bool`は`0`または`1`であり、`false`や`true`ではありません。
- 行の時刻(`interval.start`、`interval.end`)はUnix時刻のミリ秒値です。外側の`timestamp`はISO-8601文字列です。すべての数値は有限でなければなりません。
## 外側のオブジェクト
```json
{
"version": "0.12",
"timestamp": "<ISO-8601 string>",
"checksum": "<sha256 hex of JSON.stringify(data)>",
"data": {
"interval": [],
"preferences": [],
"stopwatch": [],
"tag": [],
"timeline": [],
"tagStopwatch": [],
"exportOptions": []
}
}
```
`data`には、`interval`、`preferences`、`stopwatch`、`tag`、`timeline`、`tagStopwatch`、`exportOptions`の7つのキーを、この順序で含めてください。
## スキーマ
```ts
interval: {
id: Id;
stopwatchId: Id; // -> stopwatch.id
start: number; // Unix時刻のミリ秒値
end: number | null; // Unix時刻のミリ秒値。計測中ならnull
}
preferences: {
// 必ず1行
id: Id;
useMilitaryTime: Bool;
mergeOverlappingStopwatches: Bool; // 旧形式のフィールド。通常は0
useAdjust: Bool;
adjustDefault: number; // 分単位。0や負の値も使用可能
backupReminderIntervalInDays: number;
currencyCode: string; // ISO 4217。例: "USD"
earningsTrackingEnabled: Bool;
defaultHourlyRate: number | null;
weekStartsOn: number; // 0 = 日曜日 ... 6 = 土曜日
alternativeColor: string | null; // Open Colorのトークン。グレーならnull
defaultTimelineId: Id | null; // -> timeline.id
lastUsedTimelineId: Id | null; // -> timeline.id
forgottenRemindersMode: "both" | "start" | "stop" | "off";
}
stopwatch: {
id: Id;
description: string;
adjustMinutes: number; // 整数。負の値も使用可能
isActive: Bool;
isExpanded: Bool;
hourlyRate: number | null; // このストップウォッチに保存する単価
rateCurrencyCodeAtWrite: string | null; // 単価を保存した時点のワークスペースの通貨
isHourlyRateManual: Bool | null;
targetMinutes: number | null;
timelineId: Id | null; // -> timeline.id。nullは「タイムラインなし」
}
tag: {
id: Id;
name: string;
hourlyRate: number | null;
color: string | null; // "blue.6"などのOpen Colorのトークン、またはnull
isPinned: Bool | null; // タグ一覧にピン留めするかどうか。nullはピン留めとして扱う
archivedAt: number | null; // アーカイブ時点のUnix時刻のミリ秒値。nullならタグは有効
}
timeline: {
id: Id;
name: string;
parentTimelineId: Id | null; // -> 親のtimeline.id。親がなければnull
hourlyRate: number | null;
isBillingBoundary: Bool;
archivedAt: number | null; // アーカイブ時点のUnix時刻のミリ秒値。nullならタイムラインは有効
}
tagStopwatch: {
id: Id;
tagId: Id; // -> tag.id
stopwatchId: Id; // -> stopwatch.id
}
exportOptions: {
// 最大1行。通常のインポートでは空の配列を使う
// idにはアプリで共通の"4WnR17vMjH50XmwttC218Q"を推奨。
// ほかのIDはインポート時にこの値へ書き換えられる。
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";
}
```
## チェックサム
`envelope.checksum`は`JSON.stringify(data)`のSHA-256ダイジェストを小文字の16進数にしたものです。
ここで`data`は外側全体ではなく、内側のオブジェクトです。
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()
```
## 最小構成の例
```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": []
}
}
```
## よくある間違い
次の間違いを避けてください。
- `Bool`フィールドに`true`や`false`を使わず、`0`または`1`を使ってください。
- `interval.start`や`interval.end`に秒を使わず、ミリ秒を使ってください。
- 行の時刻にISO文字列を使わないでください。ISO文字列を使うのは外側の`timestamp`だけです。
- 外側のオブジェクトに`timezone`フィールドを追加しないでください。変換には元データのタイムゾーンが必要な場合がありますが、TimeRetainは行の時刻をUnixミリ秒で保存します。
- チェックサムを省略しないでください。外側全体ではなく、内側の`data`から計算します。
- チェックサムを計算する前に`data`を整形したり、キーをアルファベット順に並べたりしないでください。上記のキー順をそのまま使います。
- 7つの`data`配列は、空でも省略しないでください。
- 顧客やプロジェクトをタグでグループ化しないでください。関連するタイムラインの下に`timeline`行でまとめます。
- `tag.parentTagId`を追加しないでください。タグはグループ化できません。
- 親グループが存在しないタイムラインや、親子関係が循環するタイムラインを作らないでください。
- 元データに顧客、プロジェクト、アカウント、ワークスペースが分かっている場合は、`stopwatch.timelineId`を設定します。
- タグ付きストップウォッチには、対応する`tagStopwatch`行を作ります。