本文へ移動
TimeRetain
アプリを開く

データをTimeRetainに取り込む

TimeRetainはJSONファイルから時間データを取り込みます。AIアシスタントを使うと、既存データからファイルをすばやく作れます。

  1. 今使っているツールからCSVまたは表計算ファイルとしてデータを出力します。
  2. そのファイルを以下の指示とともにAIアシスタントに渡し、TimeRetain用のJSONファイルを作ります。
  3. 次に、 データと同期を開き、「インポート」を選んで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`行を作ります。