TimeRecord Model

Definition

struct TimeRecord: Identifiable, Codable {
    let id: UUID
    var startTime: Date
    var endTime: Date?      // nil = running timer
    var tagId: UUID?        // nil = untagged
    var comment: String?
    var images: [String]    // filenames of attached images
    var metadata: [String: String]  // importer source columns, inert
}

Storage Format

Records are persisted in binary Automerge documents (CRDT), one document per month. Each record is a map inside its month’s document, keyed by the record UUID. The fields above map directly to Automerge map keys, so concurrent edits from multiple devices merge automatically (last-writer-wins per field). Legacy JSON records (.json) are read-only and auto-migrated to Automerge.

Conceptually, a record holds:

id        : uuid
startTime : 2024-01-15T09:30:00Z
endTime   : 2024-01-15T11:45:00Z   // absent = running timer
tagId     : uuid                   // absent = untagged
comment   : "Working on feature X"
images    : ["uuid-string_0.jpg", "uuid-string_1.jpg"]
metadata  : { "source": "toggl", "toggl.project": "avito-pii" }   // absent = none

Note: The images field is optional for backward compatibility. Old records without the field decode with an empty array.

Metadata

metadata holds source columns an importer keeps verbatim, under namespaced keys (source, toggl.project, toggl.tags, toggl.billable, toggl.client, toggl.task, toggl.<column>). It is inert: not shown in the editor, not filtered, reported or searched. It exists so a later feature (a billable flag, client/project concepts) can promote the values of already imported records without a re-import. The Toggl export reads it back (915-plan-toggl-import-export).

  • Automerge: a nested map under the metadata key, written only when non-empty. The setter (AutomergeProperty<[String: String]>.set) updates keys in place, so concurrent edits of different keys merge; recreating the map would drop them.
  • Older builds keep it when they edit a record (they write field by field and leave unknown keys alone). An older build that moves a record to another month re-creates it without the map.
  • A record without the key, and a JSON record without the field, read as [:].

File Naming

There is no per-record file. A record is stored in the month document selected by its startTime: MM.automerge inside the YYYY directory. The YYYY-MM-DD_HHmmss.json naming (TimeRecord.filename) is used only by the legacy read-only JSON store.

Storage Path

records/YYYY/MM.automerge

Business Rules

  • Multiple concurrent running timers allowed
  • Overlapping records are valid
  • tagId is optional (untagged records valid)
  • endTime is optional (nil = running)
  • images array is optional (old records without images field decode with empty array)
  • metadata map is optional (absent reads as empty)

Image Storage

Images are stored in a per-month directory next to the month document:

  • Path: records/YYYY/MM/ (a directory beside records/YYYY/MM.automerge; it holds only images)
  • Naming: {record-id}_{index}.{ext}
    • Example: 550e8400-e29b-41d4-a716-446655440000_0.jpg
  • Supported formats: JPEG, PNG, HEIF
  • Processing: Resized to max 1920px width/height, compressed to 80% JPEG quality
  • When a record is deleted, all its images are automatically deleted

Data

  • 201-tag - Tag reference for categorization

Services

UI

Reports