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
metadatakey, 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 besiderecords/YYYY/MM.automerge; it holds only images) - Naming:
{record-id}_{index}.{ext}- Example:
550e8400-e29b-41d4-a716-446655440000_0.jpg
- Example:
- 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
Related
Data
- 201-tag - Tag reference for categorization
Services
- 301-file-storage - CRUD and image operations
- 303-time-tracking - Timer start/stop, record updates
UI
- 406-record-editor - Edit record fields and images
- 407-history-section - Record listing with filtering
- 412-deletion-toast - Soft delete with undo
Reports
- 306-report - ReportRecordItem, ReportCommentItem
- 309-comments-section - Comment and image display