Backlog
For completed tasks, see 911-completed.
Priority Legend
[P1]- High priority: bugs, blockers, quality issues affecting UX[P2]- Medium priority: improvements, polish, technical debt[P3]- Low priority: future enhancements, ideas, nice-to-have
Bugs [P1]
Record editor opens again after a save moves the record to another month in History (2026-10-15)
- Location:
HistoryGrid.swift(HistoryMonthRow) - Root cause: each month row presented the sheet when the edited id was among its records. Save publishes the moved record before the editor clears
editingRecordId; when the record stays in the History range (it overlaps it), the new month’s row opened the editor again (iPhone, full height). Seen as the flakyRecordEditingTests.testMoveRecordToPreviousMonth(“Editor should close after Save”), failing on 1.0.8 CI and in about half of local runs, on 1.0.7 code too - Fix: the row that held the record at tap time keeps it (
presentedRecord) and is the only one that presents the sheet while the id is set
- Location:
Fix progressive loading not working (load more broken) (2026-01-02)
- Location:
HistoryGrid.swift:287-324(LoadMoreTrigger) - Root cause: SwiftUI
Gridis not lazy, soonAppearfires immediately on render, not on scroll - Fix: Replaced
onAppearwith GeometryReader viewport detection - Result: Grid height dropped from 3904px to 1986px (50% reduction)
- Related: 803-load-more-bug
- Location:
Fix cache race condition in FileStorageService (2026-01-03)
- Location:
FileStorageService.swift:384-428 - Root cause:
updateRecord()deleted old file before writing new, causing data loss on error - Fix: Write new file first, then delete old file, then update cache atomically
- Location:
Fix AppState tag mutation inconsistency (2026-01-03)
- Location:
MinutaApp.swift:379-397,447-469 - Root cause:
loadData()had optimization that skipped updates if tag count/IDs matched, missing property changes - Fix: Always update tags and rebuild dictionary on load; use direct assignment instead of optional chaining
- Location:
Fix concurrent image operations (2026-01-03)
- Location:
RecordEditorViews.swift:453-509 - Root cause: Rapid image adds could race on
currentRecord - Fix: Added
isImageOperationInProgressguard to prevent concurrent add/delete operations
- Location:
Fix crash when opening editor for a record without tag and comment (2026-07-02)
- Repro: start timer with no tag/comment, stop it, tap the completed record — app crashed on Mac Catalyst (SIGSEGV from an uncaught exception during sheet window creation,
UINSSheetManagerscene hosting path) - Root cause: with an empty tag input,
TagSelectorshows all tags, and its horizontal chipsScrollViewreports its full content width (~3400pt for 34 tags) toMinWidthFittedSizing’ssizeThatFits(.unspecified)— the editor sheet took that width, blowing past the screen and crashing AppKit’s sheet-open animation. Same blowout risk applied to the image gallery. - Fix: replaced
MinWidthFittedSizingwithWidthClampedFittedSizing— clamps sheet width to 640–800pt and re-measures height at the clamped width (HistoryGrid.swift) - Regression test:
BareRecordEditorTests.testOpenEditorForBareRecordwith newbare-recordfixture (34 tags + one untagged, comment-less, 11-second record); asserts the editor opens and Save/Cancel stay hittable. Passes on iPhone sim and Mac Catalyst.
- Repro: start timer with no tag/comment, stop it, tap the completed record — app crashed on Mac Catalyst (SIGSEGV from an uncaught exception during sheet window creation,
UI test runs overwrite the CLI storage pointer (2026-07-03)
- The app wrote
storage-path.txton every launch, including test launches with--test-storage=, so running UI tests pointed the CLI at a (soon deleted) temp dir until the real app was launched again - Only Mac Catalyst runs clobbered the real pointer: the unsandboxed Catalyst app resolves
getpwuid()to the real home, while simulator processes resolve it inside the simulator container - Fix:
publishResolvedStoragePath()skips the pointer write whenisTestMode(StorageLocationManager.swift); the.minuta-versionmarker is still written into the test folder - Regression test:
StoragePointerTests.testTestStorageLaunchDoesNotOverwriteCLIPointer— captures the pointer before launch, asserts it is unchanged after a--test-storage=launch. Verified failing before the fix (Catalyst) and passing after (Catalyst + iPhone sim)
- The app wrote
Debug OKLCH colors
- Investigate hue distribution across alphabets
- Check color consistency between views
Fix view of tags in edit form (2026-07-28)
- Tags display incorrectly in RecordEditor
- Location: RecordEditorViews.swift
- Resolved by
WidthClampedFittedSizing(see the bare-record editor crash entry above): the tag chipsScrollViewreported its full content width to the sheet’s sizing, so the editor stretched past the screen and the tag row rendered wrong. Clamping sheet width to 640–800pt and re-measuring height fixed the display.
Handle floating/undocked virtual keyboard (iPad)
- Floating play button positioned wrong when the iPad keyboard is undocked or in floating (minimized) mode
- The undocked/floating keyboard is not part of the safe area, so keyboard-avoidance offsets are computed from a full-height keyboard that isn’t there
- Also check editor sheets and the tag/comment fields — content can be pushed or clipped by the phantom inset
- Fix: read the actual keyboard frame (
UIResponder.keyboardWillChangeFrameNotification) and intersect it with the window instead of relying on SwiftUI’s default keyboard avoidance
Fix window not hiding immediately on app close (Mac) (2026-07-28)
- Window stays visible briefly after closing the app
- Should hide immediately when user closes
Investigate empty block above running timers (2026-03-24)
- Unexplained empty space appearing over working timers section
- Need to identify the cause and remove
Test Coverage [P1]
Current: 619 unit tests passing (Shared package, 2026-10-15) + 120 app unit tests (MinutaTests target, iPhone simulator, 2026-10-15)
Add unit tests for StorageLocationManager (2026-07-03)
- Wired the previously dormant
Minuta/Tests/into a newMinutaTestsunit-test target (hosted in the app,project.yml), added to the scheme and a CI step - Added injection seams to
StorageLocationManager.init(UserDefaults suite, launch arguments,StoragePathPublishing) — production defaults unchanged - 8 tests:
--test-storage=parsing (valid/empty), default-location fallback, failed-bookmark-restore reset,useDefaultLocationbookkeeping, publish rules (test mode skips the CLI pointer but writes the marker), accessibility checks - Security-scoped bookmark round-trips intentionally untested — those APIs don’t work meaningfully in a unit-test host; covered by real app usage
- Wired the previously dormant
Consolidate UI tests into multi-check journeys per app launch (2026-07-09, done 2026-07-10)
- Problem: 52 UI test methods, each relaunching the app in
setUpWithError(launch + fixture copy + settle is the dominant cost; full suite ~25 min per platform, run twice for both platforms) - Done: merged compatible checks into journey methods, one per flow — Filter 10→2, RecordEditing 6→2, Settings 5→2, ReportFlow 7→1, ReportWithImages 3→1, TagManagement 3→1, TimerFlow 2→1, DocumentPicker 2→1, UpdatePrompt 5→4 (the two default-launch checks merged; the relaunch-based ones own their launch by design). 52 methods → 23.
- Each scenario runs inside
XCTContext.runActivityfor readable failure reports; scenarios restore the state they change (filter selections, calendar presets, sheet dismissal) so later scenarios start from the shared baseline - Kept isolated (own their launch): UpdatePromptTests relaunch tests (launch-time pill/defaults state), StoragePointerTests (launch-time pointer behavior), PerformanceTests (
measureblocks), BareRecordEditorTests (dedicated fixture), andtestEditSheetMinWidthOnMac(leaves the editor sheet open for measurement) - All
snapshot()names preserved — the website gallery (TestClass--name.png) gets the same file set - Verified green on both platforms 2026-07-10 (Mac Catalyst 24/24, iPhone sim all passing)
- Problem: 52 UI test methods, each relaunching the app in
Add UI test for image attachment (2026-07-12)
ImageAttachmentTests.testAddImageToRecord(empty fixture): creates a record via the timer, opens its editor, taps Add Photo, picks a stock photo through PHPicker, asserts the editor’s Images gallery appears, saves, asserts the thumbnail shows in the record row; snapshots each step- PHPicker automation notes (it is a remote view controller, but its elements are exposed through the host app’s a11y tree): grid cells are Images with identifier
PXGGridLayout-Infoand labels likePhoto, 30 March 2018, 12:14— match “Photo,” with the comma to skip the Photos-app icon in the privacy banner; cells report not-hittable, so tap via coordinate; first library load on a fresh simulator can exceed 20s - Query evaluation around the picker can take ~30s per attempt, so waits are 60s; enumerating the standard fixture’s history alongside the picker’s remote hierarchy made queries time out entirely, hence the empty fixture
- Record-row thumbnails (
CompactImagePreview, HistoryGrid.swift) now carry arecordRowImageThumbnailaccessibility identifier for the row assert - iPhone sim only: on Catalyst, Add Photo opens the machine’s real Photos library (not isolated, not reliably populated), so the test body is
#if !targetEnvironment(macCatalyst)
Fix flaky launch wait in DocumentPickerStabilityTests under full-suite load (observed 2026-07-13, fixed 2026-07-14: bumped to 15s)
- Failed in the first fully unattended Catalyst full-suite run (after
DevToolsSecurity -enable+automationmodetool enable-automationmode-without-authentication); passes rerun in isolation (409s wall time) — load-related timing flake, not a regression DocumentPickerStabilityTests.swift:23:openBrowseFilesButton()assertsfloatingPlayButton.waitForExistence(timeout: 5)right after launch. The 5s budget covers Catalyst app launch, fixture-to-Automerge conversion, initial data load, and the runner’s first accessibility snapshot — fine on an idle machine, but marginal mid-suite when everything crawls (the failure surfaced at t=26s because even the query evaluation runs slow under load; it still resolved to “not found” within the app’s slow startup). Bump to 15s like other launch waits.
- Failed in the first fully unattended Catalyst full-suite run (after
Fix flaky PNG/SVG preview waits in ReportFlowTests under full-suite load (observed 2026-07-13, fixed 2026-07-14: both scenarios use
tapPreviewAndWaitForDone(timeout: 60))- Failed in the same full-suite run as the DocumentPickerStabilityTests flake above; passes rerun in isolation (522s wall time) — load-related timing flake, not a regression
ReportFlowTests.swift:90“PNG preview should open”: PNG (and SVG, line 71) previews wait only 15s for the Done button. PDF hit the same flake on 2026-05-14 and got 60s + a one-shot retry viatapPreviewAndWaitForDone(timeout: 60); extend that helper to the PNG and SVG scenarios (CSV at 5s has never flaked — no WebKit render pass).
Fix FilterTests.testCalendarJourney “Current Month” ambiguous query (observed 2026-08-06, both platforms)
FilterTests.swift:349:app.buttons["Current Month"]matches the history section header (identifierhistorySection, label “Current Month”) instead of the calendar’s Current Month preset button; the header is not hittable at that point and the tap fails after scroll-to-visible retries- Pre-existing, not a regression: fails identically on commit 78a48d7 (verified via worktree build); date-dependent — last green in July, so the ambiguity or the header’s label/hittability shifts with the current month/date layout
- Fix: scope the preset query to the calendar container or give the preset buttons their own accessibility identifiers (
calendarPresetCurrentMonthetc.), matching howhistorySectionalready carries an identifier
Make the Mac UI suite pass in the 400x600pt test window (window changed 2026-10-15, done 2026-10-16)
- The Catalyst test window shrank from 783x971pt to 400x600pt (
testWindowSize520x779 UIKit points inMinutaApp.swiftSceneDelegate).RecordEditingTests.isMacchecks the platform at compile time instead ofwindow.frame.width > 500 - App bug found: in a window narrower than 640pt the record editor sheet was laid out at 640pt and cut off at both sides, Save included.
RecordEditorSheetnow clamps the sheet to the main window’s width;testEditSheetMinWidthOnMacasserts Save and Delete lie inside the sheet (failed before the fix) FilterTests.testCalendarJourneyreads the total throughreadTotal()(scrolls it into view and back);StorageWarningTestsscrolls with the newMinutaUITests.scrollMainList(down:)(wheel events on Catalyst, where drags do not scroll a list)- Result: Mac 37/37, iPhone 25 passed + 1 skipped (Mac-only)
- The Catalyst test window shrank from 783x971pt to 400x600pt (
Add App Intents/Shortcuts integration tests
- StartTimerIntent, StopTimerIntent have no coverage of their own; since 2026-10-15 their logic is
WidgetIntentService.start(tagName:comment:)/stopAll(), whichWidgetIntentServiceTestscovers.perform()itself (result strings) is untested - Location: AppIntents.swift
- StartTimerIntent, StopTimerIntent have no coverage of their own; since 2026-10-15 their logic is
Add view tests for multiplatform project
- Location:
Minuta/Tests/(unit-test targetMinutaTestsexists since 2026-07-03) - ViewInspector for SwiftUI testing
- ContentView, SettingsSheet tests
- Location:
SVGReportRenderer internal-method coverage via public API (2026-05-14)
- Added 6 tests exercising
truncateText(long tag names in legend, short names not truncated) andwrapText/wrapTextWithNewlines(long comments, explicit newlines, single overlong word, empty comment) through the publicSVGReportRenderer.render(_:)entry point.renderTimeSeriesChartalready covered bytestSVGRendererIncludesTimeSeriesChart. - Result: Shared package tests now 450 (was 444).
- Added 6 tests exercising
Audit Findings (2026-01-03) [P1]
Dead Code:
Remove unused
dayCellSizeconstant (2026-01-03)- Removed from CalendarDatePicker.swift
Remove unused
UIKitimport from ContentView (2026-01-03)- Location:
ContentView.swift:2 - No UIKit types used in file
- Location:
Remove unused(2026-01-03)UIKitimport from SettingsSheet- False positive:
UIPasteboardis used for copy-to-clipboard functionality
- False positive:
Remove unused DateRangePickerView.swift (2026-01-03)
- Location:
Views/History/DateRangePickerView.swift - 114-line file never referenced - CalendarDatePicker is used instead
- Location:
Remove unused Color(oklch:) initializer (2026-01-03)
- Location:
Color+Hex.swift:30-40 - Never called; only Color(hex:) is used
- Also removed unused contrastingTextColor(for: OKLCH) overload
- Location:
Maintainability:
Extract layout constants in CalendarDatePicker (2026-01-03)
- Location:
CalendarDatePicker.swift:75-91 - Created
LayoutMetricsenum with named constants for cellSize, fonts, padding, opacity, etc.
- Location:
Refactor duplicated date range logic in CalendarDatePicker (2026-01-03)
- Added
endOfDay()andsetDateRange(from:to:)helper methods - Reduced duplicate “add day, subtract second” pattern from 5 occurrences to 1
- Added
Clean up verbose comments in CalendarDatePicker (2026-01-03)
- Removed redundant section labels and implementation detail comments
- Kept useful explanatory comments (corner radius logic, week selection criteria)
UI Tests:
UI test failures - History section not found, Export sheet not opening, Missing fixture data, Tag management flow(2026-05-14)- The “16 failures” list from 2026-04-15 is stale; targeted re-run on 2026-05-14 (iPhone 17 sim, iOS 18) showed all those tests passing.
Fix PDF preview flake on standard fixture (2026-05-14)
- Affected:
ReportFlowTests.testPDFReportPreview,ReportFlowTests.testExportSheetStaysDuringGeneration - Root cause:
Donebutton wait usedtimeout: 15, but PDF generation throughWKWebView.pdf()over the 138-record standard fixture runs ~16–60s on the iOS 18 simulator with high variance (PNG/SVG/CSV complete in ~5–10s; PDF-with-images on a 2-record fixture is unaffected). - Fix: bumped both
Done-button waits totimeout: 60and added a one-shot retry path (tapPreviewAndWaitForDone(timeout:)helper) that dismisses any “Export Error” alert and re-tapspreviewButton. Verified: when the first 60s wait misses, the retry succeeds (134s total).
- Affected:
Mac Catalyst UI tests: pre-existing failures (discovered 2026-05-14; resolved 2026-07-10)
- Update 2026-07-10: full Catalyst suite is green (24/24 including all PerformanceTests) after the journey consolidation plus two Catalyst-specific fixes:
historyDurationRecord()regex-matches labels in Swift instead of an NSPredicateMATCHESquery (numeric labels in the Catalyst a11y snapshot made the predicate throwNSInvalidArgumentException), and the settings sheet is closed via its Done button (swipe-down on the main window does not dismiss a Catalyst sheet). Test launches also pin the window to 783x971pt (SceneDelegatesizeRestrictions) so runs no longer inherit the user’s last window size. - Update 2026-07-03: fixes for clusters 1 and 2 have landed since the re-run — toolbar bar buttons now carry
accessibilityIdentifiers plus identifier-or-label test predicates (b4df387), section headers matched case-insensitively viasectionHeader(), coordinate scrolls window-anchored viadragScroll(), document picker flakes and the TCC prompt fixed (db416f2,a00971f). Needs a fresh full Catalyst run to confirm what remains — likely just cluster 4 (PerformanceTests). - Update 2026-07-02: cluster 3 below is resolved —
testEditSheetMinWidthOnMacpasses sinceWidthClampedFittedSizing(commit58c27bf). Remaining failures are clusters 1, 2, 4 plusDocumentPickerStabilityTests(2) andTagSelectorTests.testTappingChipCommitsFullNameToField;testViewExistingRecordsfails with a coordinate-scrollNSInternalInconsistencyException(point.x != INFINITY). - Failures cluster around:
- Catalyst NSToolbar a11y missing —
app.buttons["settingsButton"]/["pinButton"]not found. The SwiftUI toolbar (ContentView.swift:62-73) is wrapped in#if !targetEnvironment(macCatalyst); the Catalyst replacement isSceneDelegate’sNSToolbar(MinutaApp.swift:140+) which setsitem.label = "Settings"/“Pin” but never sets an accessibility identifier. Tests must queryapp.buttons["Settings"]/["Pin"]on Catalyst — or NSToolbarItems should expose the identifiers used by the iOS path. - “Running” header not found after
floatingPlayButton.tap()on Mac — affectsRecordEditingTests.testAddCommentToTimer,testTapToEditRecord,TimerFlowTests.testStartAndStopTimer, etc. Needs screenshot inspection during failure (Mac may render the running section differently or below the fold). — resolved 2026-07-02 bytestEditSheetMinWidthOnMacregressed (611pt vs 640pt min)WidthClampedFittedSizing(640–800pt clamp with height re-measure, commit58c27bf); passes in the full-suite run.- 3 PerformanceTests fail at ~24s (testExportSheetPerformance, testReportGenerationPerformance, testTagFilterPerformance) — needs investigation.
- Catalyst NSToolbar a11y missing —
- Note: iPhone-sim tests pass for the same flows because iOS uses the SwiftUI toolbar path which does have identifiers wired up. The Mac CI lane has likely been red since the
7992a0a add mac pin button (wip - not working)commit.
- Update 2026-07-10: full Catalyst suite is green (24/24 including all PerformanceTests) after the journey consolidation plus two Catalyst-specific fixes:
Audit Findings (2026-10-02)
Full-project audit (8 auditors). Test runs that day: Shared 476/476 passed, iPhone UI tests 18 passed + 1 expected Mac-only skip, MinutaTests 36/36 passed, Mac Catalyst UI tests did not build (first entry under “Build, tests, CI”). Each entry carries its own priority tag. “Reproduced” means run against a temp storage folder with the built CLI; “by reading” means traced in code and not executed.
Data integrity:
Record edit across a month boundary duplicates the record [P1] (reproduced; fixed 2026-10-02)
- Fixed:
AutomergeStorageServicekeeps an id-to-months index that survives cache eviction;updateRecordanddeleteRecordrefresh it under the lock from changed mtimes, write the target month, move the image files, then remove the id from the other months.deleteRecordalso handles a stale start time.minuta doctorgot aduplicate recordscheck (warning) - Existing duplicates are not collapsed on read (no reliable “newer copy” signal): saving one copy removes the others, deleting removes only that month’s copy
- Tests: 10 unit tests in
AutomergeStorageServiceTests/AutomergeStorageConcurrencyTests, CLItestRecordsEditStartAcrossMonths_noDuplicateandtestDoctor_reportsRecordInTwoMonths, UIRecordEditingTests.testMoveRecordToPreviousMonth(both platforms; verified failing with the move disabled) - Not done, see follow-ups below: fixed-calendar bucketing and the timezone edge
- Location:
AutomergeStorageService.swift:263-287;updateRecordissaveRecord, which writes into the month document of the newstartTimeand never deletes the key from the old month document - Repro:
minuta records add --start 2026-04-22..., thenrecords edit <id> --start 2026-03-22...: the same ID is in03.automergeand04.automerge,records listshows it twice. Reachable from the editor (RecordEditorViews.swift) and the CLI (RecordsCommand.swift:263,292); a timezone change near a month boundary has the same effect - Images are keyed by start month too (
imageURL,:331), so attachments stay in the oldrecords/YYYY/MM/folder - Fix:
updateRecordlooks up the old month (or takes the previous record), deletes from the old document and moves the images under the same lock; consider bucketing by a fixed calendar instead ofCalendar.current - Tests first:
AutomergeStorageServiceTests.testUpdateRecordAcrossMonthMovesRecord(plus year boundary) and a UI test inRecordEditingTests; the legacy store already hastestUpdateRecordWithDateChangeMovesFile - Also decide how to repair folders that already contain duplicates (
minuta doctorcheck)
- Fixed:
External-change probe is always true with 13+ month files [P1] (confirmed by a failing test; fixed 2026-10-02)
- Fixed: eviction no longer drops
recordDocumentMtimes;saveRecordDocumenttakes the document explicitly (it used to skip the write silently when the document had been evicted); a failed save discards the mutated cached document - Tests:
testHasExternalChanges_falseAfterLoadAllWith13Months,..._detectsWriteToEvictedMonth,..._detectsDeletedEvictedMonthFile,testFailedSaveRecord_unsavedRecordNotServedLater,testFailedDeleteRecord_recordStillServedLater - Location:
AutomergeStorageService.swift:209-217(eviction also dropsrecordDocumentMtimes) and:383-389(hasExternalChangestreats a month file without an mtime entry as new) loadAllRecords()always leaves at least one month evicted once there are more than 12, so the 5s probe fires invalidate + conflict resolution + full reload forever- Fix: track observed mtimes separately from the document cache and never evict them; fix the stale comment at
:13(“12 covers ~2 weeks of daily records”, documents are per month) - Test: unit test with 13 month files asserting
hasExternalChanges()is false after a full load; add the “cached month file deleted” case
- Fixed: eviction no longer drops
Conflict resolution writes outside the storage lock [P1] (fixed 2026-10-02)
- Fixed: each conflict group is merged inside
StorageFileLock.withExclusiveLock; the primary is written before merged copies are deleted (the old order lost a merged copy when a later sibling was corrupt); a corrupt copy stays on disk and is reported inerrors; the scan takes no lock - Tests:
testValidCopiesMergedDespiteCorruptSibling,testResolveConflictsWaitsForStorageLock,testNoConflicts_doesNotTakeLock - Location:
ConflictResolutionService.swift:106-119;mergeConflictFilesreads, merges, deletes conflict copies and rewrites the primary withoutStorageFileLock.withExclusiveLock - Runs on every scene activation and external-change reload (
MinutaApp.swift:669,830,866-880) and fromminuta conflicts resolve, so it can clobber a concurrent app/CLI write - Test: hold the lock on a second fd and assert
resolveConflictsblocks, or race it againstsaveRecord
- Fixed: each conflict group is merged inside
CSV export does not escape tag names [P1] (reproduced; fixed 2026-10-02)
- Fixed: every field goes through
escapeField; columns followExportOptions.Column.allCases, so the header isDate,Start Time,End Time,Duration,Tag,Comment(it was alphabetical:Comment,Date,Duration,End Time,Start Time,Tag); duplicate tag ids no longer trap inExportServiceandExportCommand - Tests:
testExportEscapesCommaAndQuoteInTagName,testExportHeaderExactOrder,testExportSelectedColumnsKeepCanonicalOrder,testExportRecordSpanningMidnight,testExportDuplicateTagIdsDoesNotCrash, CLItestExportCSV_duplicateTagIdsInStorage_doesNotCrash - Location:
ExportService.swift:88-93; only the comment goes throughcsvParser.escapeField, a tag namedA, Bshifts the columns - Same pass: pin the header with an exact-order test (code emits
Start Time/End Time, CLAUDE.md documentsStart/End; the audit also reported the columns come out sorted by raw value), and guardDictionary(uniqueKeysWithValues:)at:16against duplicate tag IDs - Tests:
testExportEscapesCommaAndQuoteInTagName, exact header order, record spanning midnight, running record row
- Fixed: every field goes through
Undo delete of a record with images [P1] (reproduced by a UI test; fixed 2026-10-02)
- Fixed: soft delete removes the record but keeps its image files (
TimeTrackingService.deleteRecord(_:keepingImages:),purgeImages(of:)); files are purged on expiry, swipe-dismiss or storage switch;undoDeleteis a no-op once the entry is gone and re-inserts it when the re-save fails; a second delete of the same record while one is pending is ignored; operations in flight across a storage switch no longer touch the new folder - Also fixed on review:
deleteRecorddeletes image files only after the document write succeeded; the editor writes the saved value back tocurrentRecord(a start-date change followed by adding a photo used to revert the date) - Tests: UI
RecordDeletionTests.testDeleteUndoJourney(both platforms), 10 app-target tests inAppStateSoftDeleteTests, Shared tests inTimeTrackingServiceIntegrationTests - The UI test fixture conversion now moves image files along with the date shift (
with-imagesnever showed real images before) softDeleteRecorddeletes from storage immediately (MinutaApp.swift:1092-1108) anddeleteRecordremoves the image files (AutomergeStorageService.swift:290); undo re-saves the record but the files are gone- Reproduce with a UI test first; then either defer the image deletion until the toast expires or restore the files on undo
- Fixed: soft delete removes the record but keeps its image files (
HybridStorageService.invalidateCache()returns before invalidating [P2] (fixed 2026-10-02: nowasync, awaits both stores)- Location:
HybridStorageService.swift:115-120wraps the work in an unstructuredTask;AppState.reloadFromExternalChangesawaits it and reloads straight away (MinutaApp.swift:827-832,872) - Fix: make it
asyncand await both inner calls
- Location:
Coalesce
AppState.loadDataand fix stale history after external edits [P2] (by reading) (done 2026-10-15)- Was: History reloaded only on the date range,
todayRecords,historyRevision(undo of a delete) andstorageWarnings, so a record of an earlier day added, edited or deleted by the CLI or a sync stayed stale until the range changed (reproduced by the UI test below, which failed before) - Now: every published load bumps
historyRevision;ContentViewreloads History through one.task(id:)keyed on the range, revision, today’s records, warnings and whether an editor or the share sheet is open (closing it reloads what was skipped), and a newer reload cancels an older one. Lists are published only by a load not older than the last published one and started after the state’s last local change (noteLocalChange()), otherwise it loads again; a failure is reported only by such a load;isLoadingcounts loads in flight; running timers come from the sameloadAllRecords()pass as the tag usage (one full scan per load instead of two) - Tests: UI
ExternalChangeTests.testHistoryFollowsExternalChanges(failed before),AppStateLoadTests.testLoad_bumpsHistoryRevision(failed before),AppStateLoadTests.testLoadOvertakenByStartAndStop_keepsTheChange(a guard: the stale publish needs a load’s reads to straddle a write in storage and did not reproduce on the old code) - Left: a seam to inject storage into
AppState, to reproduce the stale publish deterministically loadData(MinutaApp.swift:882-917) is entered from at least six places with no coalescing or generation token: a slow older load can overwrite optimistic state or write the previous folder’s data afterreinitializeStorage;isLoadingflips false while another load runs; each load scans the full history twice (tagLastUsedAt+getRunningTimers)ContentView.swift:106-113reloads history only on date range ortodayRecordschange, so an external edit to an older record or a tag merge leaves history stale; the two dateonChangehandlers spawn unordered Tasks- Fix: load-generation counter or single in-flight task;
dataRevisionbumped inloadDatadriving one.task(id:)keyed on (start, end, revision)
- Was: History reloaded only on the date range,
mtime cache trusts an unchanged mtime on write paths [P2] (by reading) (done 2026-10-15)
- Was:
saveRecord/updateRecord/deleteRecordreused the cached month document when the stamp (mtime and size) was equal and wrote it over the file; on exFAT, SMB or sync folders with 1s granularity an external write in the same tick with the same size was lost. The backlog’s test idea (restore the mtime) alone did not reproduce it: the size changed too - Now: every record write and the consolidation in
refreshIndexLockedread the month from disk under the lock (workingDocument(fresh: true)). Tests:AutomergeStorageConcurrencyTests.test{Save,Update,Delete}AfterSameStampExternalWrite_keepsTheExternalRecord(a same-size file with the same mtime; all three failed before) - Left: the id-to-months index is still refreshed by stamp, so a record another process moved to another month within the same tick and size is not cleaned from the old month (a duplicate, reported by
minuta doctor)
- Was:
Widget publish path is not isolated in test mode or on Catalyst [P2] (by reading) (done 2026-10-15)
- Was:
AppState.syncWidgetDataran under--test-storage=and on Catalyst and reached the real App Group container and suite (WidgetDataService.swift,RecentTagsStore.swift); app unit tests wrote their tags into the real recent-tags list - Now:
WidgetDataService.forApp(testLaunch:)publishes nothing for a test launch and on Catalyst,RecentTagsStore.forApp(testLaunch:)uses its own suite for a test launch; used byAppStateandWidgetIntentService.live(). Tests:WidgetIntentServiceTests.testTestLaunch_publishesNothingAndKeepsRecentTagsOutOfTheAppGroup,AppStateEditorSaveTests.testTestLaunch_doesNotTouchTheAppGroupRecentTags(failed before)
- Was:
Build, tests, CI:
Mac Catalyst UI tests do not build:
MinutaWidgetssigning [P1] (fixed 2026-10-02)- Fixed: removed
MinutaWidgets: allfrom theMinutascheme’s build targets; the widget still builds on iOS through the app’splatformFilter: iOSdependency (the simulator build still embeds the.appex), and Catalyst builds of the scheme no longer build or sign it. Also fixesscripts/run-mac.sh; the ReleaseMinutaArchiveCatalyst build used byrelease-brew.shwas verified to build without the widget NSFontPanelAttributescleared; full Mac suite 32/32 on 2026-10-02./scripts/run-uitests.sh --macstops before any test:No profiles for 'tools.minuta.app.dev.widgets' were found ... (in target 'MinutaWidgets')- Cause: the
Minutascheme buildsMinutaWidgets(project.yml:199); under the Catalyst destination the iOS-only target resolves to theiphoneosSDK with the Debug bundle ID overridetools.minuta.app.dev.widgets(project.yml:107), for which no development profile exists (only the holder’s four AppStore/MacAppStore profiles are installed) - Not run since:
MiniTimerTests,DocumentPickerStabilityTests,UpdatePromptTests,SettingsTests.testPinButtonToggle,testEditSheetMinWidthOnMac, all Mac screenshots - Fix options: keep the widget out of Catalyst test builds (separate test scheme without
MinutaWidgets, or a destination filter on the dependency), same root cause as thescripts/run-mac.shitem in “Streamline the build, test and deploy process”; do not paper over it with-allowProvisioningUpdates - After the build is fixed:
defaults read tools.minuta.app NSFontPanelAttributesis"1, 8", so the phantom Fonts panel is primed; clear it before the first full Mac run
- Fixed: removed
Run Catalyst in CI [P1] (workflow updated 2026-10-02; verified by running the same commands locally, not yet by a CI run)
.github/workflows/build.ymlgot three Catalyst steps (build,MinutaTests,MinutaUITests) with ad-hoc signing:CODE_SIGN_IDENTITY=-(an unsigned Catalyst runner is blocked by Gatekeeper) andENABLE_HARDENED_RUNTIME=NO(library validation rejects an ad-hoc signed test bundle).TrayIconManagerTestsnow runs (48 app unit tests on Catalyst)- Open questions are in the follow-ups below
.github/workflows/build.ymlcovers iPhone only, which is how the break above went unnoticed; add the Catalyst UI suite (or at least a Catalyst build) andMinutaTestson Catalyst, sinceTrayIconManagerTestsis#if targetEnvironment(macCatalyst)and currently runs nowhere- Related: “Fix GitHub Actions” under Infrastructure
Harden
Minuta/scripts/run-uitests.sh[P2] (done 2026-10-19)set -eonly and the xcodebuild pipeline ends in|| true; pass/fail is decided by grepping/tmp/uitest-output.log, shared by iPhone and Mac runs- Seen 2026-10-19:
run-uitests.sh StorageWarningTests ...appended a secondTests, matched nothing, ran 0 tests and printed “All tests passed!” - Done:
set -euo pipefail; exit status is xcodebuild’s (PIPESTATUS), and 1 when the result bundle counts no test; the class name takesTestsor not; log per platform/tmp/uitest-output-{iphone,mac}.log(UITEST_LOG, whichtest-gate.shpoints into the release logs); simulator fromMINUTA_SIMULATOR;build.ymlnames it once (IOS_DESTINATION); usage examples name real methods - Same day:
StorageWarningTests.testUnreadableMonthJourneyfailed on CI after release 1.2.0. The on-screen keyboard of the new timer’s tag field covered the lower half of the screen and the scroll drags typed on it; whether it shows depends on the simulator’s hardware keyboard state.scrollMainList(down:)now closes it first (MinutaUITests.dismissKeyboard()) - Use
set -euo pipefail, capture${PIPESTATUS[0]}, per-platform log, simulator name from one env-overridable variable (also hardcoded three times inbuild.yml); the usage example at:7names atestStartTimermethod that does not exist
CLISmokeTestsmoves the realstorage-path.txtaside [P2]- Location:
Shared/Tests/MinutaCLITests/CLIPlaceholderTests.swift:33-54; a crash mid-test loses the pointer and a running app/CLI sees it missing - Make
CLIConfigService.configDirectory()injectable; this also unlocks theStorageResolverpointer tests below
- Location:
PerformanceTests cannot fail on performance [P2]
PerformanceTests.swiftdocuments targets (:35under 1.0s,:102,:164,:213) but asserts none; the 2026-10-02 run measured launch 1.96s and export sheet 5.7s and passedtestTagFilterPerformance(:103-160) printed “No tag filter buttons found” and passed, so it measures nothing- Its 13
snapshot()screenshots are published to the website; decide whether they belong there
FilterTests.tapPresethides a missed reset [P2]FilterTests.swift:71-91, 318-328(introduced in7957e14): returns false when the preset is not hittable and the “Current Month” resets discard the result;03-today-selected/04-current-week-selectedsnapshots have never existed in git- Fix together with “Fix FilterTests.testCalendarJourney Current Month ambiguous query” above
Tests that pin CSVParser bugs as expected behaviour [P2] (done 2026-10-17)
CSVParserTests.swift:98-105(escaped""quotes stripped: export then import loses quotes) and:166-170(a CRLF comment is not quoted, producing a broken row)- Fixed with the RFC 4180 parser:
""is one literal quote, quoted content is kept verbatim (only unquoted fields and whitespace outside the quotes are trimmed), a leading UTF-8 BOM is dropped;escapeFieldchecks scalars (CRLF is quoted) and quotes leading/trailing spaces. The pinning tests now assert the correct behaviour, plus BOM and round-trip tests
Replace placeholder tests with real ones [P2]
Minuta/Tests/MinutaTests.swift:5-8isXCTAssertTrue(true);MinutaSharedTests.swift:6assertsMinutaShared.versionequals itself (the constant is a stale"1.0.0")- Give them real assertions (see the AppState coverage entry below); do not delete
Coverage: storage, locking, conflicts [P2]
- True cross-process lock test: spawn N parallel
minuta records add/tags addon one folder, assert nothing is lost;openFailedon a read-only folder (StorageFileLock.swift:42-58) - Conflict tests on real
AutomergeTimeRecord/AutomergeTagdocuments (edited on A + deleted on B;endTimeset on both), corrupted primary, group with only conflict copies, false positives from the iCloud pattern\s+\d+$(ConflictResolutionService.swift:10-15, 94-104) ExternalChangeMonitor.swift:50-80(cadence switching, in-flight coalescing,checkNow); UI test writing a record into the--test-storagefolder while the app runs
- True cross-process lock test: spawn N parallel
Coverage: CLI [P2]
StorageResolver.swift:35-45: pointer to an accessible folder, pointer to an inaccessible path exits 2, empty flag/env falls through,~expansion, env end to end- No integration test for:
records image export,stop --id(invalid UUID exits 3, not running exits 1),records showby 8-char prefix and ambiguous prefix,records edit --start/--clear-tag/--clear-comment/--clear-end,records listfilters other than--today,tags list --archived,tags add --color,tags renameonto an existing name,tags recolorwith bad hex,export csv -o,export report --tags/--title,status --json, fish completion DoctorCommand.swift:29-165failure paths: unwritable folder, corrupttags.automerge, orphan images, conflict files, unreadable marker, held lockCLIIntegrationTests.swift:165-169redeclaresCLIExitprivately and can drift from the real values
Coverage: dates, DST, import [P2]
- No DST or non-UTC tests anywhere:
WidgetTimelineBuilder.swift:54-68,WidgetData.swift:73-79,AutomergeStorageService.swift:410-423,RecordFilters.swift:51-107(zero unit tests; needs an injected calendar) - HEY import (
ImportService.swift:29-83,CSVParser.swift:9-27): end before start (the CLI aborts mid-import atImportCommand.swift:94after writing some records), UTF-8 BOM, quoted field with a newline, DST-gap times,--resumeafter a tag rename,--json
- No DST or non-UTC tests anywhere:
Coverage: app target [P2]
AppState(MinutaApp.swift:692-1218):renameTagintomergeTag,softDeleteRecord/undoDelete,resolveTagId,stopAllTimers,reloadFromExternalChanges- Widget extension has no tests:
WidgetDataReader.readfallback,MinutaEntry.isStale, timeline provider (MinutaWidgets.swift:8-99);WidgetDataService.swift:93-120nil URL / write failure,:72record started yesterday and finished today not counted;WidgetIntents.swift:56-106invalid UUID and nil host UpdateCheckService: fetcher non-200 and bad JSON (:25-37),isVersionwith suffixes (1.0.5-betaparses to 1.0.0,:213-222),setEnabledtoggling bypasses the 24h throttle (:160-167)- Pure UI logic:
CalendarSectionHeader.swift:17-150,HistoryVisibility.swift:11-48,Color+Hex.swift:12-58 - Related: “Add App Intents/Shortcuts integration tests” above
Tag hues exceed 360 for Indic and Thai alphabets [P2] (by reading)
TagColorGenerator.swift:138-185vs the sizes at:33-39: code-point ranges are wider than the declaredsize(Devanagari, Bengali, Telugu, Kannada, Malayalam, Tamil: 53 code points vs 46/43/41/41/41/35; Thai 46 vs 44), so late letters wrap and collide- Test: parameterised over every alphabet’s first and last letter,
0 <= hue < 360and monotonic order; Bengali, Tamil, Telugu, Kannada, Malayalam have no tests today - Related: “Debug OKLCH colors” under Bugs
UI test hygiene [P3]
- 94
Thread.sleepcalls across UI flows (FilterTests 22, PerformanceTests 17, RecordEditingTests 13): replace with waits - Leftover
print("=== A11Y DUMP ===")block inSettingsTests.swift:21-25; fix-history comments inRecordEditingTests.swift:63,117-118 - Rename
CLIPlaceholderTests.swift(containsCLISmokeTests),Phase2IntegrationTests.swift/Phase3IntegrationTests.swift(named after plan phases);TagManagementTests.swiftholds a second classTagSelectorTests
- 94
Release and scripts:
Site would advertise 1.0.4 while only 1.0.3 artifacts exist [P1] (resolved 2026-10-03 by releasing 1.0.5 (7) / CLI 0.1.3 everywhere: notarized Homebrew artifacts, tap
ed3711d, site deployed withversion.json1.0.5, TestFlight iOS and Mac App Store uploads; the release orchestrator now enforces artifacts before the site)7ee2710bumped all three targets to 1.0.4 (6) for the Mac App Store upload;minuta.tools/src/lib/appVersions.js:15feeds the firstMARKETING_VERSIONinto/downloads/version.jsonand the download link,dist/and the server holdMinuta-1.0.3.zip- Until fixed, do not run
deploy.shfrom HEAD: Homebrew users would see “Update available” pointing at a missing file - Fix: either ship 1.0.4 through
release-brew.sh(artifacts, tap, then site), or give the site its own “latest Homebrew release” source instead ofproject.yml
release-brew.shfallback path and pipefail traps [P2] (fixed 2026-10-03 with the release orchestrator: empty-array-safeXCARGS, capturedsecurityoutput, no| headinside substitutions):134-140: underset -u,"${XCARGS[@]}"on an empty array aborts on macOS bash 3.2 (unbound variable), so the documented path withoutDeveloperID.xcconfigor withMINUTA_NOTARIZE=0dies before building:50-57(also:33-36):SIGN_ID="$(security ... | grep ... | head -1 | sed ...)"exits 1 when nothing matches,set -ekills the script and the friendly error at:53-57is unreachable; the script already documents this trap at:147-148
Script safety and duplication [P2] (
deploy.shpart done 2026-10-03:set -euo pipefailandcdto its own directory)scripts/generate-perf-fixtures.sh:9,14andscripts/generate-standard-fixture.sh:16,20:rm -rf "$OUTPUT_DIR"on a path relative to the caller’s cwd; resolve the repo root from$0and useset -euo pipefailminuta.tools/deploy.shhas nocd "$(dirname "$0")"beforersync --delete build/; the remote host is duplicated inscripts/upload-brew-artifacts.sh:11scripts/run-ipad.shis byte-identical torun-iphone.sh;MARKETING_VERSIONis repeated three times inproject.ymlwhilerelease-brew.sh:35readshead -1generate-standard-fixture.sh(260 lines) is referenced nowhere: document it or drop it
Personal data and large files in the repo and on the site [P2]
- The holder’s full name is in the tracked
Minuta/ExportOptions-mac.plist:22(required there) and repeated in docs that are served publicly (702-testflight:246,919-plan-mac-distribution:159, this backlog); the next deploy publishes it on 702. Refer to the plist instead of repeating the identity string ImageAttachmentTests/03-photo-picker.png(1.8 MB) is stored twice (snapshots + site screenshots); it is a system photo-picker shot, the same class as the Finder panels banned on Mac- The four
DocumentPickerStabilityTestsFinder-panel screenshots removed ind43bac2are still in history; matters only if the repo goes public
- The holder’s full name is in the tracked
Build config leftovers and stale config comments [P3]
Minuta/TestFlight/MinutaWidgets.macOS.entitlementsis unused:TestFlight-mac.xcconfigmaps the widget toTestFlight/MinutaWidgets.entitlementsproject.yml:205-207and CLAUDE.md describeMinutaArchiveas “without MinutaTray”, butrelease-brew.sh:132-133,156relies on it building and signing Tray as an implicit dependencyTrayIconManager.swift:59-60says the bundle is embedded under Resources (project.ymlusescopy: plugins);project.ymlsays Catalyst avoids thegroup.*app group whileMinuta.macOS.entitlementsincludes it.gitignore:58-61, 69-70deadreport-generatorentries;--keep-animationsdebug flag (MinutaApp.swift:217-221) is undocumented- Keep version bumps in their own commits (1.0.3 and 1.0.4 bumps rode in feature commits)
CLI:
-q/--quietand-v/--verboseare advertised but not implemented [P2] (done 2026-10-15)- Was: declared in
Minuta.swiftand the help; read only bydoctorand the unreadable-file warning - Now: informational stderr goes through
CLIGlobalOptions.info(silenced by-q;error:lines stay),-vprints the storage folder, its source and the.minuta-versionstate throughCLIGlobalOptions.debug. Tests:MarkerAndOutputIntegrationTests(-qontags add,records add,export csv; errors kept;-voutput)
- Was: declared in
Exit code 5 can never be returned [P2] (reproduced) (done 2026-10-15)
- Was:
requireVersionMarkerForWritesalways wrote the marker (garbage and newer versions such as2overwritten with1);strictRequireVersionMarker, the only thrower of code 5, had no callers - Decided: absent marker is created (new folder), older upgraded; newer version or not a number refuses the write with exit code 5, marker and data untouched; reads work.
StorageVersionMarker.writeCurrent(app too) never downgrades or overwrites such a marker;doctorFAILs on it.CLIExit.versionMarkerAbsentrenamedversionMarkerIncompatible,strictRequireVersionMarkerremoved. Tests:MarkerAndOutputIntegrationTests(fresh folder, newer, garbage, reads, doctor; failed before), 3 inStorageSafetyTests - Left: the app itself does not refuse to write into a folder marked with a newer version
- Was:
stophandles IDs differently from every other command [P2]StopCommand.swift:13,30takes--idas an option and requires a full UUID;records show/edit/delete/imagetake a positional ID and accept an 8-char prefix (RecordsCommand.swift:352-376);statusprints only 8-char prefixes, so its output cannot be fed tostop --id--all(:16-17) is declared and never read
CLI help text contradicts behaviour [P2]
RecordsCommand.swift:231“Pass an empty string to reopen the timer” vs:268-272rejecting it (only--clear-endworks)RecordFilters.swift:9mentions a--tzflag that does not existExportCommand.swift:90-93says--from/--todefault to the current month,:193-198requires both if either is given
Unify range filters across CLI commands [P2]
export report --prev-month(ExportCommand.swift:96) vs--last-month(RecordFilters.swift:43);export reportre-declares--from/--to/--tags/--untaggedinstead of using the sharedRecordFiltersgroup, so it lacks--today,--this-weekand the rest
Regularize
--jsonoutput shapes [P2]- Tag mutations return
{"tags":[one]}, record mutations{"record":{...}};import heyemits"records": <count>where elsewhererecordsis an array; delete payloads differ between records and tags - The JSON writer is copy-pasted seven times while
RecordOutput.writePayloadis private; document the shapes in311-cli
- Tag mutations return
Align app and CLI behaviour for the same operations [P2]
- Rename collision: the app merges into the existing tag (
MinutaApp.swift:1037-1039), the CLI rejects it (TagsCommand.swift:198-202) - Colour validation:
tags recolorvalidates hex,tags add --colorstores anything (TagsCommand.swift:155) - HEY import: the app creates records through
updateRecordinside the view (SettingsSheet.swift:298-334), the CLI usescreateManualRecordwith--resumededupe (ImportCommand.swift:81-104); move “apply parsed import” intoImportServiceand call it from both TimeTrackingService:createTagvalidates but does not dedupe,getOrCreateTagdedupes but does not validate or trim and throwsTagError.emptyNamefor “not found after create” (:41-67); use[Tag].findByNameinstead of the eight inline case-insensitive compares
- Rename collision: the app merges into the existing tag (
CLI plain-mode output conventions [P3]
- stdout carries
started <uuid>, a bare uuid, orstopped <uuid> (dur)depending on the command;records editand tag rename print nothing; “no running timers” goes to stderr instopand stdout instatus;StartCommandis the only command without theexecute()split - Stale comments:
CLIExit.swift:4points atdocs/300-services/304-cli/(it is311-cli);StorageResolver.swift:50-53says it normalises viagetpwuidbut usesexpandingTildeInPath
- stdout carries
Code quality:
Remove leftover image-debug trace logging [P2]
- About 60
info-level ENTER/EXIT step logs:RecordEditorViews.swift:467-513,MinutaApp.swift:1138-1158,FileStorageService.swift:402-525(the read-only legacy store),RecordImageViews.swift:302-332,TimeTrackingService.swift:170-191 - Collapse to one line per operation at
.debug; unify message prefixes while there
- About 60
Collapse
HybridStorageServiceand retire the migration scaffolding [P2]checkAndMigrate()only setsmigrationComplete = true; all six call sites call it first, so the legacy merge branches (HybridStorageService.swift:27-75) are unreachable;migration(:7,15) is never read;AppState.migrationChecked(MinutaApp.swift:698,777,885-891) can never fire- Legacy calls still made:
legacy.deleteRecord(:87) logs a spurious “file not found” on every delete;legacy.loadImage/deleteImageresolve to the same path as Automerge;legacy.invalidateCacheclears an empty cache MigrationService.swift(140 lines) is reachable only fromMigrationServiceTests; removing it takes those tests with it, which needs an explicit decision. Keep theLocalFileStorageServicereader:MinutaUITests.swift:214converts JSON fixtures with it- Update CLAUDE.md “Architecture” and “Data Storage” afterwards
Route Shortcuts intents through
WidgetIntentService[P2] (done 2026-10-15)- Was:
AppIntents.swiftre-implemented start / stop-all inline, never calledRecentTagsStore.track, never published the widget snapshot (relied on a liveAppStatehearing.minutaDataChanged), and silently started untagged for an unknown tag name while the CLI and UI create the tag. A stop by a Shortcut or a widget button did not reachAppStatebefore its reload, so an editor’s pending auto-save reopened the timer (reproduced inAppStateEditorSaveTests) - Now: both intents call
WidgetIntentService(start(tagName:comment:)creates an unknown tag,stopAll()); stops post.minutaTimersStopping/.minutaTimersStopped, andAppStatedrops those timers fromrunningTimersuntil a load that started after the stop. Tests: 3 inWidgetIntentServiceTests, 3 inAppStateEditorSaveTests - Left:
AppState.startTimer/stopAllTimersis still a separate copy of start and stop (it updates its own lists without a reload)
- Was:
Split
MinutaApp.swift[P2]- 1218 lines holding
MinutaApp,TimerCommands,AppDelegate, a 350-lineSceneDelegate,WindowPinManager,NSWindowBridge,AppContainer,RootView,PendingDeletionand a 530-lineAppState; split intoAppState.swift,SceneDelegate.swift,WindowPinManager+NSWindowBridge.swiftwith no behaviour change - Extends “Split AppState into smaller services” below
- 1218 lines holding
Centralize storage layout, test-mode detection and AppKit bridging [P2]
StorageLayoutin MinutaShared:"tags.automerge","records", the"%04d-%02d"key and the year/month walk are repeated inAutomergeStorageService,DoctorCommand,ConflictResolutionService,AutomergeExplorerView,FileStorageService; the defaultDocuments/Minutapath is built in four placesLaunchEnvironment.isUITest:"--test-storage="is re-parsed in six files with different#if DEBUGgating (MinutaApp.swift:170,185,203,257,MiniWindowManager.swift:80,TrayIconManager.swift:21,UpdateCheckService.swift:101,StorageLocationManager.swift:88-89);WindowPinManagerstill writes the realwindowPinnedkey in test launchesNSWindowBridge: two copies of the AppKit-via-KVC code (MiniWindowManager.swift:112-160vsMinutaApp.swift:535-582), raw style-mask numbers and button types
Widget views: dedupe and consistent stale hint [P2]
MinutaWidgets.swift:162-450, 638-662: small and medium views duplicate the running/idle column, the Start/Stop capsule styling is copy-pasted about seven times,Color(hex:)duplicatesViews/Color+Hex.swift:12isStaleis honoured only in the small widget (:211,285) while205-widget-data:78says small and medium; decide and alignWidgetData.swift:12hardcodes “up to 4” next tomaxRecentTags
RecordEditor debounce and structure [P2]
RecordEditorViews.swift:12-397: one ~390-line view with three display modes and 12@Stateproperties; the 300ms debounce is a literal (:415) and nothing visibly cancels or flushessaveTaskon dismiss (verify)- Extends “Split RecordEditorViews” below
Pass environment to sheets explicitly [P3]
- Project rule; only
ContentView.swift:78-86follows it. Missing atSettingsSheet.swift:247(ExportView),:260(AddTagSheet),TodaySectionView.swift:38andHistoryGrid.swift:229(RecordEditorSheet)
- Project rule; only
Remove dead code [P2]
Shared/Tests/MinutaSharedTests/Mocks/MockTimeTrackingService.swift(171 lines, never used)OKLCH.swift:13-51(BrandHue,BrandVariation,brandColor,purpleVivid,purpleDeep,orangeVivid,blueVivid)AutomergeUtilities.swift:150-154unusedAutomergeErrorcases;HistoryGrid.swift:20-24GridDebugInfo;WidgetData.swift:66-69runningTimersCount;CalendarDatePicker.swift:87presetButtonWidth;AutomergeExplorerView.swift:126modifiedDateStorageLocationManager.swift:6duplicateappGroupIdentifier;DateFormatters.swift:52-53dayOfMonthalias;MigrationService.swift:125cleanupJSONFiles- Unused imports (heuristic):
OSLoginHistoryGrid.swift:3,PhotosUIinRecordEditorViews.swift:2;.gitkeepinShared/Sources/MinutaShared/Utilities/andMinuta/Sources/Resources/ - Needs a decision: grid geometry preferences with no reader (
GridDebugPreferenceKey,GridCellDebugInfo,HistoryGridFramePreferenceKey,HistoryGrid.swift:14-45; kept deliberately per the HistoryGrid entry, but nothing callsonPreferenceChange); single-conformer protocols never used as a type (ExportServiceProtocol,ImportServiceProtocol,StorageLocationManagerProtocol,TimeTrackingServiceProtocol); accessibility identifiers no test queries (cancelTimerButton,miniTimerWindow,saveFileButton,useDefaultLocationButton)
Comment and naming cleanup [P3]
WebKitReportService.swift: the same narrating comments over an identical 8-line block three times (:25-37, 50-65, 78-90),:202“simple polling approach” above delegate-continuation code- Misleading “legacy” labels on live code:
ReportData.swift:3,89,ReportService.swift:78(chartDatais read bySVGReportRenderer.swift:154) - Roughly 100 narrating “what” comments in sources (e.g.
MinutaApp.swift:598,779,1069,ReportService.swift:96,119,122,MinutaWidgets.swift:518,529,WidgetDataService.swift:59,75) and numbered step narration in tests - Naming drift:
StopTimerIntentstops all timers;WidgetIntentService.start/stop/stopAllvs the protocol’sstartTimer/...; notification"miniWindowStateChanged"vs"Minuta..."-prefixed siblings;minutaTrayButtonvspinButton/pipButton;let helper = RangeDescriptionHelper(...);storageURLisolation differs between storage actors
Site template leftovers [P3]
- Check
~/Projects/sveletonfirst, removal diverges from the template:minuta.tools/200.htmlplaceholder,scripts/{pug-to-html,remove-lang-ts,fix-title-shorthand,fix-allowfullscreen}.js, nestedminuta.tools/.github/workflows/ci.yml(never runs), dev dependenciesflowbite,flowbite-svelte,@zerodevx/svelte-img,fast-glob,@sveltejs/adapter-node - Unused component graph (~1,470 lines) rooted at
src/lib/publication/Publication.svelte, plusAnchorHeading,CallToAction-black,SharePostLink,LinkBlock,Nav/navSection.svelte.js;readNextPostsin+layout.server.js:10is computed for every page and read only by the unusedReadNext.svelte src/lib/shaders/+ thethreedependency (288 lines, zero importers)minuta.tools/.claude/settings.local.jsonis tracked;minuta.tools/README.md:9-10says Node 21 / pnpm 9 (it is 24 / 10)
- Check
Documentation:
Fix the record storage layout everywhere [P1] (fixed 2026-10-02 in all listed files)
- Docs say
records/YYYY/MM/YYYY-MM-DD_HHmmss.automerge, one file per record; the code stores one document per month,records/YYYY/MM.automerge, and theMM/directory holds only images (AutomergeStorageService.swift:33-37) - Wrong in:
CLAUDE.md:169,191,README.md:3,66-67,PROJECT_SPEC.md:7,505-automerge-storage:17,56-63,202-time-record:24,40,43,101-overview:55-62,301-file-storage:45,47;312-automerge-storage-serviceand506-cross-process-safetyare correct
- Docs say
Fix the TestFlight guides [P1] (fixed 2026-10-02)
- Left open: a “What to Test” text for builds after 1.0.2 (4);
919:99still shows the short installer cert name in the template; checklist item 2 (delete bundle IDs from the personal team) contradicts section 1 in both guides TESTFLIGHT-ORG.md:77and702-testflight:84showsubpath: "$(CONTENTS_FOLDER_PATH)/PlugIns", which breaks codesign;project.yml:44-49hasdestination: pluginswith no subpathTESTFLIGHT-ORG.md:252still says widgets are disabled; newest “What to Test” text is for 1.0.2 (4)- Mac widget entitlements described as
MinutaWidgets.macOS.entitlements(702:228, ORG:262) while the snippets andTestFlight-mac.xcconfig:16useMinutaWidgets.entitlements;919:99quotes the short installer cert name - Dangling references to deleted
TESTFLIGHT-HOLDER.mdsections:TESTFLIGHT-ORG.md:265,283,305, 702:231,249,271,919:24, this backlog - 702
:280and ORG:314nameExportOptions.plist(it isExportOptions-ios.plist)
- Left open: a “What to Test” text for builds after 1.0.2 (4);
Fix the CLI reference [P1] (fixed 2026-10-02)
- Left for the CLI entries above, since the code is what is wrong:
311-clistill describes-v/-qas working and exit code 5 as returned;--prev-monthvs--last-month 311-cli:105,217documents--tagforrecords list,export csv,export report; the flag is--tags(RecordFilters.swift:19-20,ExportCommand.swift:99-100)311-cli:314listssourcestrings that differ fromStorageResolver.swift:8-11in three of four values;311:240calls--with-imageswith PNG “irrelevant”, it is a hard exit-3 error- Sandbox caveat missing from
311-cli:26,47,305-storage-location,506-cross-process-safety:129-137: a sandboxed Mac build never writesstorage-path.txt - Undocumented:
stop --all, full-UUID requirement of--id, default subcommands, accepted date formats,--jsonshapes beyondrecords list/tags list
- Left for the CLI entries above, since the code is what is wrong:
Bring CLAUDE.md in line with the code [P2]
AppStateis@Observable, not anObservableObject(:218)- “Auto-refresh data every 60 seconds” (
:371) no longer exists; onlyExternalChangeMonitor - Migration:
:173-175and:211-213say auto-migrated / orchestrates,:177says complete and disabled - “No network requests - app is fully offline” (
:193) vs the carve-out at:373 - CSV export header (
:342); iOS Simulator run command (:112) installs from a path the build never produces - UI Structure: “floating editor overlay” (
:272) vs sheet (:287); “collapsible DisclosureGroup” (:269), noDisclosureGroupin Sources src/routes/assets/is not stale (:163):app-icon.pngis tracked, written bygenerate-app-icons.sh:20and imported by+page.svelte:13- Fixtures list (
:47, five exist); structure tree omitsSources/Services/,MinutaCLI,Automerge/,scripts/; CI section omits theMinutaTestsstep; “Tag creation happens on timer start or record save” omitsAddTagSheet, HEY import,tags add
Privacy policy claims no network requests of any kind [P2] (done 2026-10-15)
- Was:
minuta.tools/src/routes/privacy/+page.svelte(dated April 14, 2025) and the landing page (“no network requests”) predated the opt-in update check (UpdateCheckService.swift) - Now: the policy has a “Network requests” section describing the single opt-in request (Mac builds from the site and Homebrew, off until enabled, at most daily,
version.json, nothing but a standard request; iOS, Mac App Store, TestFlight and the CLI make none), dated October 15, 2026; the landing page says the only request is an update check you can turn on
- Was:
Retire or rewrite
PROJECT_SPEC.md[P2]:22-237describe the pre-implementation design (TimeTracker/target,tags.json, per-record JSON, CloudKit sync,SyncService, tab bar, Touch Bar, Live Activities);README.md:59links it as the product spec
Rewrite stale UI and service pages [P2]
403-app-state:10,14-25,70-77(ObservableObject,@Published,runningRecords, 60s refresh); same error in the docs index:135,101-overview:16,402-ios:28404-storage-locationand305-storage-locationlistselectStorageLocation(),resetToDefault(), a “Reset to Default” button andLocalFileStorageService(baseURL:), none of which exist412-deletion-toast:23,39-40says deletion is deferred and mentions swipe to delete; deletion is immediate and undo re-saves408-date-range-picker:12-38(six presets, range arrows),407-history-section:66-68,93(inline editor, context menu; grid borders not mentioned),413-mac-titlebar-design:12-24(says no native toolbar),405-settings-sheet:16-42(omits Updates, Menu bar, Debug, Reset Colors rows)802-scroll-profilingand803-load-more-bug:278-335present removed load-more code as the final state;102-mvvm-analysisdiagram still showsLocalFileStorageServiceand JSON files315-update-check:24and theproject.ymlcomment above line 64 sayMANUAL_DISTRIBUTIONis on in every configuration; both TestFlight xcconfigs blank it
Rewrite the testing and CI docs [P2]
603-ui-testing-implementation:84-90uses schemeTimeTrackerand methodtestStartAndStopTimer(real:Minuta,testTimerJourney);:149-159advises matching by label, tests do the opposite;602-ui-automation-planstill argues for swift-snapshot-testing and has 17TimeTrackerhits (69 across 10 pages)601-testing-guide: 253 unit / 17 UI tests (now about 476 and 31), test tree lists 8 of about 25 files; no page lists the 15 UI test classes, thebare-record/with-images/performancefixtures, the fixture JSON-to-Automerge conversion orMinutaTests703-ci:14-19describes two jobs, “398 tests”, “UI tests are not yet part of CI”;build.ymlis one job with four steps- The “Current: 458 unit tests” line under Test Coverage in this backlog
Document undocumented features [P3]
- Services with no 300-services page:
WidgetIntentService,WidgetDataService,RecentTagsStore,HybridStorageService,StorageFileLock,StorageVersionMarker,CLIConfigService,StorageLockError - UI: Reset Colors and Automerge Explorer rows, the current calendar picker (pill, week toggles, drag-select), tag filter collapse and drag multi-select, tag selector keyboard navigation, pull-to-refresh, inline image thumbnails, toast swipe-down dismiss, Cmd+N and Cmd+. shortcuts
- Services with no 300-services page:
Docs hygiene [P3]
- About 20 of 81 pages have a garbage auto-derived
description(table rows,Date: ..., code, wiki-link residue), which feeds<meta description>and the OG image - Point-in-time AI review reports published as docs (
913-repository-review,912-audit-2025-12-25,102-mvvm-analysis,912-execution-plan): archive or mark as historical; log pages 901-904 and 912-audit are dated 2024, the first commit is 2025-12-09 - Duplicate numbering:
100-architecture/500-architecture,500-architecture/500-reports, two912-*, two913-*;401-macosis a 16-line stub the index links as “400-ui”; boilerplate section descriptions - Index omissions: 300-services (312-315), 200-models (205), 700-integrations (703), root index (315, 415, 915-919)
- Broken link
505-automerge-storage:67(should be/docs/500-architecture/archive/505-automerge-migration-plan/);801-optimization-plan:22namesgenerate-perf-fixtures.swift(it is.sh) - Stale entries in this backlog: “Automerge for conflict-free sync” and “Add drag-to-select for tag filters” are shipped; the Mac distribution entry still lists “Blocked on the holder”;
914-plan-cli-toolhas no “shipped” status and points to304-cli;701-deelhas no “not implemented” status
- About 20 of 81 pages have a garbage auto-derived
Follow-ups from the P1 fixes (found 2026-10-02):
Restored record lands in Today regardless of its date [P2] (done 2026-10-13)
undoDelete(MinutaApp.swift) inserts the restored record intotodayRecordsunconditionally (it also counts toward Today’s total), and nothing re-adds it toHistorySection’s local list until the next reload; visible in theRecordDeletionTests04-restoredsnapshot- Now: the record returns to
todayRecordsonly if it starts today, andAppState.historyRevisionmakesContentViewreload History. Tests:AppStateSoftDeleteTests.testUndoOfOlderRecord_notInsertedIntoToday,testUndoOfTodayRecord_backInToday, UIRecordDeletionTests.testUndoRestoresOlderRecordToHistoryOnly(skipped on the 25th and 26th, when a fixture record is of today)
Duplicate tag ids crash the app and reports [P2] (done 2026-10-07)
- Was: document keys that differ only in letter case parse to the same UUID, so duplicates are reachable from storage, and
Dictionary(uniqueKeysWithValues:)trapped inAppState.rebuildTagsByID(the app crashed on load),ReportData.build(export report),stop,import hey(by id with--resumeand by lowercased name), the two report image providers and the legacyFileStorageServicecache - Now: every lookup is built with
uniquingKeysWith(first wins), asExportServicealready was; nouniqueKeysWithValuesis left in the sources. Tests:AppStateDuplicateTagTests,ReportServiceTests.testBuildWithDuplicateTagIdsDoesNotCrash, three inCLIIntegrationTests - Left: storage still hands out both tags (the app lists the twin as a second tag); whether to normalize key case on load is open
- Was: document keys that differ only in letter case parse to the same UUID, so duplicates are reachable from storage, and
Unparseable month file [P2] (done 2026-10-05, with the tags file included)
- Was: one unreadable
MM.automergemadeloadAllRecordsthrow (nothing loaded in the app, most CLI commands exited 1),hasExternalChanges()stayed true forever (full reload every 5s), nothing could be saved into that month, and a zero-byte file was read as an empty document and overwritten by the next save. A release build showed none of it:errorMessageis only visible in the Debug view - Now: an unreadable or zero-byte file is never overwritten, renamed or deleted. Reads skip it; new records for that month go to
MM.recovery.automerge, which is merged into the month file and removed once the file reads again (merge plus a fix-up for records that exist as two independent objects). The per-file stamp is (mtime, size); files that could not be opened are retried every 60s. Edits and deletes of records hidden inside an unreadable file are refused with a named error instead of being queued tags.automergeunreadable: no recovery file; the app keeps its last known tags, tag edits are refused, entries keep their tags. Also fixed on the way: the editor erased a record’s tag on any save while tags were missing, a timer with a tag that could not be created started untagged, the in-app HEY import imported untagged- UI: banner under the toolbar plus a “Storage problems” section in Settings; alerts for refused save, stop, delete and tag operations. CLI: one stderr warning on commands that read records;
doctornames each unreadable file, keepsduplicate recordsandimage integritymeaningful, adds arecovery filesrow and anunreadableJSON array;export,records list --csv,import hey --resumeandtags deleterefuse when an unreadable month is in range ConflictResolutionService: no longer overwrites a zero-byte base file; a lone conflict copy with no base file is promoted instead of being ignored- Docs: 312-automerge-storage-service, 506-cross-process-safety, 416-storage-warnings, 311-cli
- Not done, see “Follow-ups from the unreadable-file work” below
- Was: one unreadable
Soft-delete leftovers [P2]
- Quitting (or iOS killing the app) inside the 60s undo window leaves the deleted record’s image files as orphans; only
minuta doctorreports them. Consider purging on termination or a launch-time sweep limited to ids the app itself deleted - Deleting a record another process moved to a different month within the last 5s keeps a stale start time in the pending entry: the purge looks in the old month folder, and undo restores the record into the old month while its files sit in the new one
softDeletesInFlightis not cleared on a storage switch; the reinitialize purge has no test
- Quitting (or iOS killing the app) inside the 60s undo window leaves the deleted record’s image files as orphans; only
Record editor save edge cases [P2] (done 2026-10-13)
- Done 2026-10-05:
onRecordUpdatedfires only after a successful save; a failed save or stop shows an alert and keeps the editor open; buttons are disabled while an action is awaited - Was: a save and an image add/remove could overwrite each other in storage, since each writes the whole record from its own snapshot. Now they run one at a time through
SerialWriteQueueand read the record at their turn - Was: the debounced auto-save was not cancelled on Stop or on the delete of a running timer, so it wrote the stopped timer back as running or re-created the deleted one (both reproduced at
AppStatelevel). Now Delete, Save and Stop cancel it, and it goes throughAppState.autoSaveRunningTimer, which refuses a timer that leftrunningTimersor is being stopped or deleted (covers a stop from the mini window, Stop All and the tray) - Tests:
AppStateEditorSaveTests(9). No UI test: XCUITest takes about 4 seconds between a keystroke and the next tap, far outside the 300 ms window, and nothing can slow storage down for the image case; the editor’s use of the queue is covered only by the existing editor and image UI tests - Left:
a stop through Shortcuts or the widget does not go through(fixed 2026-10-15, see “Route Shortcuts intents throughAppState, so until the next reload an auto-save can still reopen that timerWidgetIntentService”); a stop from the mini window uses its own copy of the record and can drop a keystroke typed in the main window in the last 300 ms
- Done 2026-10-05:
Storage move leftovers [P3]
- Timezone:
loadRecordsopens only the months of the range under the current calendar, so a boundary record bucketed in another timezone can be missed; widenmonthsBetweenby a day on each side or bucket by a fixed calendar - A month document emptied by a move stays on disk as an empty file;
deleteRecordfor an id found nowhere still writes the start-month document;deleteRecordover several months can fail after an earlier month’s copy is already gone - Every
updateRecord/deleteRecordwalks the records directory under the lock; negligible per call but multiplied in tag merge and the in-app HEY import (which callsupdateRecordfor brand-new records,SettingsSheet.swift:329)
- Timezone:
Catalyst CI open questions [P2]
- Done 2026-10-13: the Catalyst UI-test step runs only on a manual run (
workflow_dispatch,gh workflow run build.yml); on every push it took over the interactive session of the runner, which is the development machine. Not verified in Actions until the change is pushed - Ad-hoc signing is unverified on a machine without an Xcode account for the team; each ad-hoc build is a new code identity, so macOS privacy grants do not persist between runs
/private/tmp/MinutaUITests/is left behind between runs
- Done 2026-10-13: the Catalyst UI-test step runs only on a manual run (
UI test findings [P2]
FilterTests“Navigate previous month” is a silent no-op: it looks for buttons labelledchevron.leftunderif leftButtons.count >= 2, but the chevrons are labelledBack/Forwardand the month toggle is not a button on Catalyst- The
historySectionidentifier on the HistorySectionoverrides its descendants’ identifiers (shareButton,recordRowImageThumbnail,historyRecordDuration) dragScrollatdx: 0.5over the expanded calendar selects a date range instead of scrolling (its doc comment claims otherwise); thehistoryDurationRecord()comment is outdated (Today rows carry the identifier too)AppStateSoftDeleteTestsfixture record starts at now minus 2h, so onetodayRecordsassertion depends on the clock between 00:00 and 02:00BareRecordEditorTests.testOpenEditorForBareRecordfailed once on Mac Catalyst as the first test of a full run (2026-10-07): “Failed to get matching snapshot” for theTotal:label right afterwaitForExistence; passed 4 of 4 alone- Fixed 2026-10-13:
StorageWarningTests.testStartTimerErrorIsShownfailed on Mac Catalyst on every run that day (the click on the error toast’s x did nothing), also on the sources that passed on 2026-10-07. The x had a 7 by 7 point hit area; it is now padded. Why the same click hit it a week earlier is not established
Small leftovers [P3]
scripts/release-brew.sh:128-130comment still says theMinutascheme buildsMinutaWidgetson Catalyst- Docs:
301-file-storage“Record Caching” section describes the legacy cache as current;403-app-statesnippet still shows@Published var pendingDeletion;412-deletion-toastRelated points at402-iosfor the toast container (it is inContentView);311-clisynopsisminuta doctor [--json]omits-v, and its duckdb example sums theH:MMDurationstring;305-storage-location:64still calls the Mac App Store build “Planned”;914-plan-cli-tool:272,309shows--tagas the filter flag
Follow-ups from the unreadable-file work (found 2026-10-05):
Errors that are still invisible in release builds [P2] (done 2026-10-07)
- Was:
AppState.errorMessageis shown only in the DEBUGDebugStateView, so every action without an alert of its own failed silently - Now: those actions call
AppState.reportErrorand the message shows as an error toast (417-error-toast) in the main window, Settings, the CSV export sheet and the mini window: start (play button, Cmd+N, tray, mini window), Stop All (Cmd+., tray), the mini-window drop button, tag create/archive/unarchive, Reset Colors, undo of a delete,loadHistoryRecords, the Settings CSV export, a failedloadData(stays until a load succeeds). Image add/remove in the editor shows an “Image Error” alert like the editor’s other failures - Left: a tray command that fails while no window is open is gone after 10 seconds unseen; App Intents report through the intent’s own error
- Was:
Phantom Fonts panel keeps coming back in Mac UI tests [P2] (found 2026-10-05, fixed 2026-10-06)
- Cause: with a non-Latin keyboard layout active (Russian), XCUITest
typeTextreaches the app as Command chords, so typing a comment started timers (Cmd+N), minimized the window (Cmd+M) and opened Fonts (Cmd+T); tests kill the app, so AppKit window restoration brought the panel and the minimized window back on every later launch. TheNSFontPanelAttributesdefault was a red herring - Fix: the UI test base class switches to an ASCII-capable layout per test on Catalyst and restores it, and launches with
-ApplePersistenceIgnoreState YES; the app’s Format menu is removed inAppDelegate.buildMenu
- Cause: with a non-Latin keyboard layout active (Russian), XCUITest
Unreadable-file leftovers [P3]
- No in-app action to give up on a lost original; the user moves or deletes it in Finder or Files and the recovery file takes its place on the next write
- A stale update of a record that lives only in an unreadable recovery file next to a readable original is still accepted;
saveRecordwith an existing id whose copy sits in an unreadable month leaves a duplicate after repair;deleteRecordwith a stale start time pointing at a readable month while the record sits in an unreadable one is a silent no-op - Lone-copy promotion in
ConflictResolutionServiceapplies to any.automergefile in the folder whose name matches a conflict pattern WidgetIntentService.publish()still loads tags, so with unreadable tags and no live app a widget start does not refresh the widget snapshot- CLI:
--this-month-style ranges end at the next month’s first instant, so an unreadable next-month file also blocks an export; with unreadable tagsrecords show --jsongives"tag": nullwith only the stderr line as a signal; a defaultrecords listwith no date filter walks every month from 1970 to 2100
loadDataacross a storage-folder switch [P2] (done 2026-10-07)- Was: it read
trackingServicefromselfafter its first await whilestoragewas captured before it, so a switch during the load read tags and records from the new folder and tag usage from the old one;tags,todayRecords,runningTimersandtagLastUsedAtwere assigned without the folder guard thatstorageWarningshad - Now: both services are captured together and nothing is published once the folder has changed (
migrationChecked,isLoadingand the load error included);reinitializeStoragealso clearstagLastUsedAt. Test:AppStateErrorNoticeTests.testLoadOvertakenByStorageSwitch_doesNotPublishOldFolder
- Was: it read
NewTimerSheetis unreachable [P3]showingNewTimeris never set to true; either wire the sheet up or remove it
Home-directory path in published screenshots [P2] (done 2026-10-07)
- Confirmed: the iPhone snapshots of Settings (
SettingsTests02-settings-open,TagManagementTests02-settings-open) showed the simulator path under the developer’s home directory in the “Data Folder” row - Now: simulator test storage lives in
/private/tmp/MinutaUITests/, the folder Mac Catalyst already used, so the row shows that path on both platforms - Left: the old images are in git history, and the live site keeps them until the next deploy
- Confirmed: the iPhone snapshots of Settings (
Code Quality [P2]
Remove debug cellBorder() from HistoryGrid (2026-09-07)
- Was only wrapped in
#if DEBUG, so Debug builds (and every screenshot) showed gray cell outlines that Release/TestFlight builds did not. Removed the graycellBorder()and its six call sites for good; the coloredgridBorder()outlines (green around a month’s days grid, orange around a day’s records) lost their#if DEBUGand are part of the design in all builds
- Was only wrapped in
Split AppState into smaller services
- Location:
MinutaApp.swift(320+ lines) - Handles: state, tags, records, deletion undo, tag merging
- Consider: TagManagementService, RecordManagementService
- Location:
Split RecordEditorViews
- Location:
RecordEditorViews.swift:1-450(450+ lines) - Mixed concerns: display, editing, formatting, image operations
- Extract image handling to separate component
- Location:
Consolidate history reload triggers (2026-01-03)
- Merged two
.taskmodifiers into one for initial load - Added comments explaining each reload trigger
- Location:
ContentView.swift:113-126
- Merged two
Performance [P2]
Layout optimization (2026-07-12)
- Covered by the HistoryGrid work below: row-per-month restructure (List cell reuse), sticky labels via
onGeometryChange, removed dead DEBUGdebugInfostate that re-evaluated the whole page every scroll frame - Scroll confirmed smooth after the fixes; further ideas tracked in the HistoryGrid entry below
- Covered by the HistoryGrid work below: row-per-month restructure (List cell reuse), sticky labels via
Improve HistoryGrid performance (2026-07-09)
- Root cause of year-range slowness: the whole grid was a single List row containing eager nested
Grids, so a year selection built every record view upfront (the progressive-loading LoadMoreTrigger had been removed by then) - Fix:
HistoryGridrestructured intoHistoryMonthRow— HistorySection emits one List row per month, so the List’s native cell reuse only builds on-screen months. Columns still align across months via fixedHistoryColumnSizeswidths. Editor sheet moved into the month row owning the edited record. - Grid geometry reporting (
GridDebugPreferenceKeyand friends) kept in release — part of the design, not debug-only - Follow-up (scroll felt janky after the restructure): sticky labels moved from GeometryReader+onChange to
onGeometryChange; removed dead DEBUGdebugInfostate in ContentView that re-evaluated the whole page on every scroll frame (its preference emission in the grid stays — part of the design) - Remaining ideas if profiling still shows hotspots: one List row per day instead of per month (bounds cell-build cost — the likely fix if big months still hitch on scroll-in), incremental re-grouping on single-record edits (
updateFilteredRecords()re-groups everything), debounce filter changes - Related: 801-optimization-plan Phase 6, 802-scroll-profiling, 803-load-more-bug
- Root cause of year-range slowness: the whole grid was a single List row containing eager nested
Investigate slow year report generation
- Year range report is noticeably slow (about 500 records)
- Profile SVGReportRenderer and WebKitReportService
Add pagination for large record sets
- Location:
FileStorageService.swift:218-256 - All records loaded into memory - problematic for years of data
- Location:
UI/UX Improvements [P2]
- Add 300ms debounce to record saving while editing (2026-06-01)
- Already implemented for running timers:
scheduleSave()cancels the priorsaveTaskand sleeps 300ms beforesaveChanges(createTag: false)(RecordEditorViews.swift:412-419), wired totagInput/editedComment/editedStartTimeonChange handlers (:386-394).createTag: falseavoids creating tags mid-typing (tags created on timer stop). - Completed records intentionally keep explicit Save/Cancel (Cancel discards). Extending auto-save there is a separate behavior change (Cancel/tag-creation semantics) — not part of this item.
- Already implemented for running timers:
Edit record sheet — remaining polish (verified done 2026-07-03)
- Both sub-items were already implemented:
TabEscapingTextViewcaps growth atmaxHeight: 200and switches to internal scrolling past the cap;TagSelectorauto-focuses the text field in itsonAppear(two runloop hops to survive Catalyst’s UIFocusSystem race) - Location:
TabEscapingTextView.swift,TagComboBox.swift(TagSelector)
- Both sub-items were already implemented:
Better controls for date/time selection in record editor
- Current text fields are bare; need proper date/time pickers
- Consider inline pickers or popovers for start/end time editing
Keyboard accessibility
- Arrow keys for tag navigation, Tab for all controls
- Mac: Shift-Tab for tab character, Tab for navigation
Arrow key navigation in CalendarDatePicker
- Navigate between year/month/week/day/preset buttons with arrow keys
- Enter/Space to select
- Location:
CalendarDatePicker.swift
Tab focus on tag filter buttons
- Tags in TagFilterView should be focusable via Tab key
- Location:
TagFilterView.swift
Highlight records spanning over 1 day
- Records whose start and end fall on different days should be visually distinguished in the history list
Support drag and drop for image attachments
- Drop images onto RecordEditor from Finder, Photos, browsers
Apply color theme to whole project
- Consistent theming, design tokens, dark mode consistency
Save everything on Cmd+S
Cleanup settings from dev info
- Settings sheet exposes developer-facing details that shouldn’t be in a user-facing screen; move them under Debug (DEBUG builds only) or remove
- Location:
SettingsSheet.swift
Create app logo (2026-06-01)
- Hand-drawn stopwatch (
assets/logo-source.png) over the brand gradient. Sourceassets/app-icon-source.svg; generated via./scripts/generate-app-icons.sh(app icon, in-app stopwatch button, web hero). Favicon built from the same SVG.
- Hand-drawn stopwatch (
Infrastructure [P2]
Fix GitHub Actions
- Figure out good way to run and fix setup
Monorepo structure (turborepo or bazel)
- Build and screenshots cached, triggered on code changes
- Use code hash as cache key
Publishing [P2]
App autoupdate (done 2026-07-03)
- Implemented as check + notify only (no in-app install, no Sparkle):
UpdateCheckServicefetcheshttps://minuta.tools/downloads/version.json(prerendered at site build fromproject.yml), compares versions, and the toolbar pill / Settings About row link to the downloads page - Settings “Check for updates” toggle off by default; 7-day discovery pill
( enable updates | x )next to the pin button,xdismisses forever; zero network before opt-in (single carve-out from the “no network requests ever” rule) - Gated by
MANUAL_DISTRIBUTIONcompilation condition (Catalyst UI only; an App Store build would drop the flag — currently on in all configs since no App Store pipeline exists) - Covered by
UpdateCheckServiceTests(unit, both platforms) andUpdatePromptTests(UI, Catalyst-only, not run in CI) - Docs: 315-update-check
- Implemented as check + notify only (no in-app install, no Sparkle):
Mac distribution under the holder’s team: Developer ID (Homebrew) + Mac App Store (TestFlight) (planned 2026-09-08; Homebrew part shipped 2026-10-02 as 1.0.3 / CLI 0.1.2, iOS 1.0.3 (5) with widgets in TestFlight; Mac App Store build 1.0.4 (6) uploaded 2026-10-02 after the Mac Installer
.p12arrived; remaining: verify the macOS build in TestFlight)- Plan: 919-plan-mac-distribution (supersedes 917-plan-mac-testflight); holder request in
TESTFLIGHT-HOLDER.md - Homebrew build today is signed with the personal team’s Apple Development cert and carries
get-task-allow;spctlrejects it, the cask hides it by stripping quarantine. Daler’s Apple Distribution cert cannot fix this (App Store only) - Blocked on the holder: Developer ID Application
.p12, App Store Connect API access (notarytool key), Mac Catalyst capability on the App IDs, Mac App Store profiletools.minuta.app MacAppStore, Mac Installer Distribution certificate; macOS platform added to the App Store Connect record - Done (2026-09-08):
release-brew.shsigns with Developer ID + notarizes + staples when the untrackedMinuta/DeveloperID.xcconfigand theminuta-notarykeychain profile exist, loud warning + old path otherwise;Minuta/TestFlight/Minuta.macOS.entitlementsandMinutaWidgets.macOS.entitlements; sandbox guard for the CLI pointer inStorageLocationManager.publishResolvedStoragePath()(+ unit test);Minuta/ExportOptions-mac.plist; xcconfig templates in the plan; guide section 8 rewritten (App Group staysgroup.*) - 2026-10-02: certs + profiles imported,
minuta-notaryis an app-specific-password profile (Team Keys page errors for the Admin account), both xcconfigs created, first notarized release shipped (caskpostflightdropped, tapc840b23), Mac archive verified, export fails on the missingMac Installer Distributionidentity; execution log in the plan - Follow-up: CLI discovery pointer under App Sandbox, now its own entry below
- Plan: 919-plan-mac-distribution (supersedes 917-plan-mac-testflight); holder request in
CLI discovery pointer from the sandboxed Mac App Store build (added 2026-10-02)
- Today
StorageLocationManager.publishResolvedStoragePath()skips the pointer whenAPP_SANDBOX_CONTAINER_IDis set, because<realHome>/Library/Application Support/minuta/storage-path.txtis outside the container; the Homebrew CLI then falls back to~/Documents/Minutainstead of the folder picked in the App Store app - Write the pointer into the App Group container instead:
FileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.tools.minuta.apps")/storage-path.txt(~/Library/Group Containers/group.tools.minuta.apps/on macOS, allowed under sandbox with the existingapplication-groupsentitlement);CLIConfigServicegets a second writer, the sandbox guard inStorageLocationManagerpicks the group path rather than skipping StorageResolver(Shared/Sources/MinutaCLI/Runtime/StorageResolver.swift): add the group-container pointer after the Application Support one (--storage->MINUTA_STORAGE-> App Support pointer -> Group Containers pointer ->~/Documents/Minuta);minuta doctorreports which one resolved- Caveat to document: on macOS 15 a Terminal process reading another app’s Group Container may trigger the one-time “wants to access data from other apps” prompt for Terminal; acceptable for a CLI, no entitlement exceptions
- Tests:
StorageLocationManagerTests(sandboxed launch writes the group pointer), CLIStorageResolvertests with a fake group pointer - Docs: 506-cross-process-safety, 311-cli discovery order, CLAUDE.md
- Today
CLI install hint in Settings for the App Store build (added 2026-10-02)
- The Mac App Store build cannot ship the CLI: every executable in the bundle must be sandboxed, and a sandboxed binary launched from Terminal gets its own container and cannot read the user-picked storage folder. The CLI stays on Homebrew / direct download (notarized, unsandboxed), as other App Store apps do
- Add a “Command-line tool” row to Settings > Data (or About) in the Catalyst build: one-line
brew install ramil-k/minuta/minuta(plusbrew trust ramil-k/minutafor Homebrew 7, see the entry above) with a Copy button and a link to the downloads page; detect an installed CLI viastorage-path.txtbeing read is not possible, so keep it static - Show it in both Mac distributions (Homebrew users may not know the CLI exists); hide on iOS
- With the group-container pointer above, the Homebrew CLI finds the App Store app’s folder automatically, which is what makes this a complete “ships with CLI” story
- UI test (Catalyst): the row is present and the copy action puts the command on the pasteboard
Streamline the build, test and deploy process (added 2026-10-02, done 2026-10-03)
- Done:
scripts/release.sh --app X.Y.Z [--cli X.Y.Z]runs preflight, the test gate (all tests including UI by default), version bump +release X.Y.Zcommit, Homebrew build/upload/tap, TestFlight iOS, Mac App Store, site deploy and push, with--only/--skip,--no-ui-tests/--no-tests,--no-push,--dry-run,--resume; step scripts inscripts/release/; notarized CLI cache; CLI rebuilt only when its version changes; docs at 704-release. The Catalyst CI items andrun-mac.shwere fixed earlier the same week - First real run: 1.0.6 (8) / CLI 0.1.4 on 2026-10-06, all four channels, 1h19m in total (tests 49 min, site build 15 min). The test gate stopped the first attempt before anything was uploaded (Mac UI tests, keyboard layout issue), and
--resumecontinued after the fix - Open:
release-brew.shregeneratesscripts/cli-completions, so the completions land in the tree after the release commit instead of inside it - Today a release is a hand-run sequence spread over
release-brew.sh,upload-brew-artifacts.sh, the tap clone,deploy.sh, twoxcodebuild archive+-exportArchiveinvocations (iOS, Mac) with three untracked xcconfigs, version bumps in three places (project.ymlx3 targets,Minuta.swift), and manual checks; the 1.0.3/1.0.4 run on 2026-10-02 took four archive reruns and threerelease-brew.shreruns, each re-notarizing the CLI - One entry point, e.g.
scripts/release.sh <channel> [--version X.Y.Z --build N]with channelsbrew,ios,mac: bump versions in every place from one flag (and fail ifproject.ymltargets disagree), run the gate (swift test,MinutaTests, iOS + Catalyst builds) once, then archive/export/notarize per channel;--dry-runstops before upload/push/deploy - Make reruns cheap: cache the notarized CLI tarball per source hash so an app-side retry does not resubmit the CLI; archive to per-channel paths so iOS/Mac runs do not
cleaneach other’s DerivedData (bit the MinutaTests run on 2026-10-02) - Tap bump as a step: clone or reuse
~/Projects/homebrew-minuta, rewriteversion/sha256/urlfromdist/SHA256SUMS,brew audit, commit, push; thendeploy.sh; enforce the order artifacts -> tap -> site - Fold the TestFlight uploads in (
release-testflight.shfrom the entry below) once the API key exists, so no Xcode login is needed; until then keep the Xcode-login path - CI: run the gate on the self-hosted runner for every push (the Shared tests,
MinutaTests, iOS build already exist inbuild.yml; add the CatalystMinutaArchivebuild), release stays manual - Fix
scripts/run-mac.sh(uses theMinutascheme, which now dragsMinutaWidgetsin and fails on the personal team’s missing widget profile; switch toMinutaArchiveor filter the dependency) - Docs: one “Release” page with the end-to-end checklist replacing the scattered steps in CLAUDE.md,
TESTFLIGHT-ORG.md, 702-testflight and 919-plan-mac-distribution
- Done:
Describe and automate the publishing process (mostly done 2026-10-03:
scripts/release/testflight.sh ios|macarchives, checks signing and uploads; release docs at 704-release; left: upload and external-tester distribution through an App Store Connect API key, see the API key entry)- One script (
scripts/release-testflight.sh) for iOS and Mac: archiveMinutaArchivewith the right xcconfig, generateExportOptions.plist,codesignchecks, build-number bump, upload - Upload without an Xcode login via an App Store Connect API key (Team Keys page errors for the Admin account as of 2026-10-02;
notarytoolruns on an app-specific password meanwhile, see 919-plan-mac-distribution) - A single docs page with the end-to-end checklist (holder materials, project state, archive, TestFlight groups, Test Information texts); today this is split across
TESTFLIGHT-ORG.md,TESTFLIGHT-HOLDER.mdand 702-testflight - Lessons from the 1.0.2 release run (2026-09-07), to bake into the script: check each command’s own exit code instead of piping xcodebuild through
grep(with-quietthere are no “Test Suite” lines andpipefailfails the step);xcodebuild -exportArchiveupload works with the Xcode Apple ID login; the Homebrew Catalyst build must use theMinutaArchivescheme (theMinutascheme drags inMinutaWidgets, which the personal team cannot sign in Release); nevercodesign ... | grep -qunderpipefaileither, grep’s early exit SIGPIPEs codesign and the check fails on a correct signature (bitrelease-brew.shon 2026-10-02); only re-upload the CLI tarball when the CLI version changed, or the formula sha256 breaks; the tap clone needs an SSH remote for a non-interactive push; a full UI-test run regenerates every site screenshot, so the following site build recomputes the AVIF cache (~25 min)
- One script (
Document
brew trustfor Homebrew 7 (found 2026-10-02)- Homebrew 7 refuses to load formulae/casks from a third-party tap until
brew trust ramil-k/minutais run once (“Refusing to load cask ramil-k/minuta/minuta from untrusted tap”) - Add the command to the install snippets on the downloads page, the CLI page, 311-cli, the tap README and CLAUDE.md; mention it in the TestFlight/Homebrew release checklist
- Homebrew 7 refuses to load formulae/casks from a third-party tap until
App Store Connect API key for notarytool and uploads (blocked 2026-10-02)
- Users and Access > Integrations > App Store Connect API > Team Keys shows “An error has occurred. Try again later.” for the Admin account even though the holder enabled API access (Issuer ID
8b60b2ec-8676-41fa-92ca-820560cf3044) - Meanwhile
minuta-notaryis an app-specific-password keychain profile and-exportArchiveuploads use the Xcode Apple ID login - To unblock: retry the page later / from another browser, or the holder creates the key (
Minuta notary, role Developer) and sends.p8+ Key ID; thenxcrun notarytool store-credentials minuta-notary --key ... --key-id ... --issuer ...and add-authenticationKey*flags to the export commands in the guides
- Users and Access > Integrations > App Store Connect API > Team Keys shows “An error has occurred. Try again later.” for the Admin account even though the holder enabled API access (Issuer ID
Mac App Store export: Mac Installer Distribution
.p12(blocked 2026-10-02, resolved the same day:.p12imported, export needs the full identity name ininstallerSigningCertificate, 1.0.4 (6) uploaded)xcodebuild -exportArchivewithExportOptions-mac.plistfails:No certificate for team 'G86VRDBMDK' matching 'Mac Installer Distribution' found; the holder’s bundle had only the publicmac_installer.cer- To unblock: the holder exports the Mac installer identity (the
installerSigningCertificatevalue inMinuta/ExportOptions-mac.plist) with its private key (TESTFLIGHT-HOLDER.md), import it, rerun the export from the already verified Mac archive, add the macOS platform to the App Store Connect record
Research iOS app publishing from Uzbekistan
- App Store Connect requirements, payments, tax implications
Trademark protection for “Minuta”
- Research registration in software category
- Consider jurisdiction (US, EU, local)
macOS Enhancements [P3]
Minified/PiP mode (Mac only) (2026-08-06)
- Plan: 916-plan-pip-mini-timer — supersedes the same-window sketch below with a companion-window design; shipped as the mini timer window (pip toolbar button + tray icon), docs at 400-ui/415-mini-timer-window
- Compact floating view showing only running timers
- Toggle via
pipicon in title bar toolbar (next to pin/settings) - Layout: small window with just timer list + play button
- Behavior:
- Remembers minified state separately from main window size
- Works with existing pin (always-on-top) feature
- Hides History section, Today section, settings button
- Shows: running timers, floating play button, expand button
- Expand button (
pip.exit) returns to full view
- Implementation:
- Add
isMinifiedstate to WindowPinManager (rename to WindowStateManager?) - Add pip toolbar button in SceneDelegate
- Conditionally render sections in ContentView based on minified state
- Animate window resize transition
- Store minified window frame separately in UserDefaults
- Add
- Related: pairs well with pin button for “mini always-on-top timer”
Menu Bar app
- Mostly shipped 2026-08-06 via 916-plan-pip-mini-timer: tray icon with play symbol, summon/hide mini window, quick start/stop menu
- Remaining (deferred there): per-timer rows with ticking elapsed in the right-click menu, elapsed text next to the icon — needs timer snapshots across the tray bridge
Always-on-top widget (superseded by Minified mode above)
- Floating window with current timer state
- Minimal, non-intrusive design
Widgets [P3]
Home Screen / Desktop widgets (WidgetKit) (2026-01-05)
- Small: running timer with stop button, or today summary with start button
- Medium: running timer + quick start tags with interactive buttons
- Large: running timers list with stop buttons, today summary, quick start tags grid
- App Groups for shared data (
group.tools.minuta.apps) - Timeline provider with 1-minute updates for running timers
- Interactive buttons using App Intents (start/stop timers)
- Note: Widget extension enabled 2026-08-29, iOS only (
platformFilter: iOS); Catalyst builds ship without widgets
Time by Tag widget (2026-10-13)
- Home screen widget, medium and large: hours per tag for a period chosen in Edit Widget (Today, This Week, This Month), total in the header, the rest folded into “Other”
- The snapshot carries per-day totals by tag; the widget sums its period per timeline entry, so it rolls over without the app. Docs: 205-widget-data
- Checked in the simulator’s widget gallery and through the published snapshot; the configured widget on a home screen is unverified until a TestFlight build (the simulator’s SpringBoard crashes when any widget is placed)
- Not done: more periods (last month, last 7 or 30 days, custom dates), a small size, share bars, a Mac version
Lock Screen widgets (iOS) (2026-01-05)
- Circular: gauge showing running timer progress or today’s hours
- Rectangular: running timer with tag name and duration, or today summary
- Inline: compact text showing timer or today’s total
Enable widget extension (2026-08-29)
- App Group
group.tools.minuta.appsand bundle IDtools.minuta.app.widgetsregistered under the TestFlight publishing team - Widget target enabled in
project.yml(iOS only) - Committed entitlements stay empty (personal free team cannot hold the App Group); TestFlight builds swap in
Minuta/TestFlight/*.entitlementsvia xcconfig, see Publishing to TestFlight
- App Group
Re-enable widgets in TestFlight (done 2026-10-02)
- Was blocked 2026-09-06: the holder’s widget profile was issued for App ID
tools.minuta.app.widget(missing trailing “s”); the widget dependency and itsMinutaArchiveentry were commented out so the first TestFlight build shipped app-only - 2026-10-01: Daler registered
tools.minuta.app.widgets(App Groupgroup.tools.minuta.apps) and reissuedtools.minuta.app.widgets AppStore(plus a reissuedtools.minuta.app AppStore); profiles installed, dependency + scheme entry restored,ExportOptions-ios.plistcommitted with both profiles; see 919-plan-mac-distribution
- Was blocked 2026-09-06: the holder’s widget profile was issued for App ID
Rework the widget data flow (added 2026-10-02, done 2026-10-02; two corrections to the original sketch below:
Text(style: .timer)cannot hide seconds, so elapsed isH:MMfrom per-minute precomputed entries with.atEnd; and an intent defined in the app target does not launch a killed app, the app-side copies conform toForegroundContinuableIntentinstead, see 205-widget-data)- Today: the app writes a JSON snapshot (
WidgetDataService->widget-data.jsonin the App Group) and callsreloadAllTimelines(); the widget’sStartTimerFromWidgetIntent/StopTimerFromWidgetIntentonly droppending-timer.jsoninto the group container, whichAppState.processPendingWidgetRequests()consumes the next time the app becomes active, so a tap on the widget does nothing until the app is opened; running durations are rendered as static text from a single timeline entry with.after(60s), which WidgetKit throttles (40-70 refreshes/day budget), so the elapsed time goes stale - Intents: move the widget buttons to the app-target intents (
StartTimerIntent/StopTimerIntentinAppIntents.swift, exposed to the extension through a sharedAppIntentsPackage); the system then runsperform()in the app process in the background with full storage access (bookmarks,AutomergeStorageService, write lock), the intent writes the record, refreshes the snapshot and callsWidgetCenter.shared.reloadTimelines(ofKind:); deletepending-timer.jsonandprocessPendingWidgetRequests() - Rendering: show elapsed time with
Text(timer.startTime, style: .timer)(ticks without timeline refreshes) and today’s total as a static value; build the timeline with a few entries (now, next midnight for the “today” rollover) andpolicy: .atEnd, no per-minute reloads - Reload triggers: only on data changes — after start/stop/edit/delete in
AppState, afterExternalChangeMonitorreloads (CLI / sync drops), on scene activation; usereloadTimelines(ofKind:)for the affected widget kinds instead ofreloadAllTimelines()everywhere - Snapshot: keep
widget-data.jsonas the read model for the extension (it cannot resolve the app’s security-scoped bookmark), includelastUpdatedand the storage folder display name so a stale snapshot can show “open Minuta to refresh” - Tests: unit tests for the intent path through an injected storage (both targets), a UI test that taps the widget intent in the simulator is not possible, so cover the
AppStateside
- Today: the app writes a JSON snapshot (
iPhone nightstand mode widget
Watch App [P3]
- Apple Watch companion
- Start/stop timers, show current state
- Complication for quick access
- WatchConnectivity for sync
Integrations [P3]
Default folder visible in Files on iOS (2026-09-06)
- The container
Documents/Minutawas invisible on the phone: no way to find, back up or move the data. AddedUIFileSharingEnabled+LSSupportsOpeningDocumentsInPlacetoMinuta/Sources/Info.plist; the folder now shows in Files > On My iPhone > Minuta and in Finder file sharing. Still local-only, see the next item - Shipped in 1.0.2: TestFlight build 1.0.2 (4) and Homebrew cask 1.0.2 (2026-09-07)
- The container
iCloud Drive as the default folder (planned 2026-09-06)
- Plan: 918-plan-icloud-default
- Blocked on the holder: iCloud capability + container
iCloud.tools.minuta.appon the App ID, regeneratedAppStoreprofile, and an Apple Development certificate + development profile for device iteration (iCloud is not available on the free personal team) - Ours: iCloud entitlements in
Minuta/TestFlight/Minuta.entitlements,NSUbiquitousContainersin Info.plist (public scope, nameMinuta), ubiquity container resolver with local fallback,.icloudplaceholder download viaNSMetadataQuery,NSFileCoordinatoraround reads/writes,NSFileVersionconflict merge through Automerge, one-time “Move to iCloud Drive” prompt for existing local-default users, CLI discovery of~/Library/Mobile Documents/iCloud~tools~minuta~app/Documents
Automerge for conflict-free sync
- CRDT library (automerge-swift v0.6.1)
- Decision: Use Automerge over custom CRDT implementation
- See 505-automerge-migration-plan for 11-phase implementation plan
Deel timesheet sync
- See 701-deel
Toggl Track import/export (done 2026-10-17)
- Import Toggl CSV exports; export records in Toggl’s 13-column layout
Projectalone becomes the tag name (decided 2026-10-16); TogglTagsgoes to metadata. Zero-duration rows import as 1-second records. Toggl export lives in Settings only: a format dropdown (CSV by default, Toggl) before the export button- Remaining columns (client, task, billable, project, tags) kept verbatim in a new
TimeRecord.metadata: [String: String]— inert for now, so a later migration can promote them without a re-import - Done:
CSVParserRFC 4180 fix,TimeRecord.metadata(Automerge map, per-key merge),TogglImportService, CLIimport togglandexport csv --layout toggl, Settings “Import from Toggl” + Export Format menu. The reference file (547 rows) imports in full and exports back identical except User/Email and the 3 zero-length rows - Open: Toggl’s own importer may need
Emailfilled in (the export leaves it empty); not tried against Toggl - See 915-plan-toggl-import-export for the full plan
Tags with a long shared prefix get the same color [P3] (observed 2026-10-17, done 2026-10-19)
- The Toggl reference file’s projects
avito-pii,avito-general,avito-onboarding,avito-edu,avito-hrall got#AC5463fromTagColorGenerator.color(for:): the hue is subdivided by the first letters only, so names sharing a 6-character prefix collide - Same for HEY categories and tags created by hand; “Reset Colors” does not help
- Done: hues are spread around the circle in name order. A new tag takes the middle of the hue gap between its neighbours (
TagColorGenerator.color(for:among:)), Reset Colors places all tags at 360/N steps (spreadColors). See 204-color-palette - 2026-10-20, after 1.2.1: colors are computed, not stored. Active tags are always at 360/N steps in name order, archived ones between their active neighbours, the stored color is ignored (
TagColorGenerator.colored, applied ingetAllTagsandAppState.publishTags). Reset Colors, CLItags recolorandtags add --colorare gone
- The Toggl reference file’s projects
Image and PDF previews in Quick Look [P3] (done 2026-10-22)
- Record images and the PDF/PNG export previews opened in small sheets (on the Mac a sheet from a sheet ignores
presentationSizing) with hand-written zoom gestures - Done: the system Quick Look viewer (
QuickLookPresenter): on the Mac the Quick Look panel over the dimmed window, on iPhone and iPad full screen; zoom, pan, paging between a record’s images, share. The viewer has no Delete; it stays in the thumbnail’s context menu. See 414-record-images
- Record images and the PDF/PNG export previews opened in small sheets (on the Mac a sheet from a sheet ignores
Billable flag on records [P3]
BoolonTimeRecord+ checkbox in the record editor; surface in reports/export- Migration reads
toggl.billablefrommetadatafor already-imported records - Depends on the
metadatafield from Toggl import/export above
Client/Project as tag concepts [P3]
- Decide the structure: tag groups, hierarchy, or a second tag axis on records
- Migration source is
toggl.client/toggl.projectinmetadata - Depends on the
metadatafield from Toggl import/export above
Google Calendar import
- Convert calendar events to time records
System calendar auto-tracking
- Auto-create records from meetings
Architecture [P3]
Introduce view model layer
- Reduce direct AppState coupling from views
Implement repository pattern
- Abstract storage implementation details
Stop storing tag colors
- Tag colors are deterministically derived from tag names via
TagColorGenerator(OKLCH-based) - Remove
colorfield from the storedTagmodel; compute on read instead - Drop migration logic that carries forward legacy stored colors
- Location:
Shared/Sources/MinutaShared/Models/Tag.swift, call sites in views
- Tag colors are deterministically derived from tag names via
Create shared reports library
- Extract report generation (SVG/PDF/PNG/CSV) into a reusable module consumable by both app and CLI
- Location:
Shared/Sources/MinutaShared/Services/(ReportService, SVGReportRenderer, WebKitReportService)
Websites [P3]
Add downloads page to the website (2026-07-03)
- Public page at
/downloads/(src/routes/downloads/) with Homebrew instructions and direct links to the release artifacts served from the same path; linked from header and footer nav - Versions read at prerender time from
Minuta/project.ymlandShared/Sources/MinutaCLI/Minuta.swift(+page.server.js), so links track releases on each site deploy - Fixed
deploy.sh: rsync--deletewas wiping the server’sdownloads/artifacts on every site deploy (confirmed: the directory was gone on the server); added--filter='P downloads/*'and re-uploaded artifacts viascripts/upload-brew-artifacts.sh
- Public page at
Docs and promo sites
- SvelteKit + IDS styles
- Static generation with backlinks component
- Prepare scaffold project with Daler
CalendarDatePicker [P3]
Use full month title instead of abbreviated (2026-01-03)
- Changed from
shortMonthName()(MMM) tomonthName()(MMMM)
- Changed from
Round borders only for range start/end days (2026-01-03)
- Added
RoundedCornersshape for selective corner rounding - Start day: left corners rounded, End day: right corners rounded
- Middle days: no corners, Single day: all corners
- Added
Use pressable button style for navigation (2026-01-03)
- Added
PressableButtonStylewith scale and opacity animation - Applied to all interactive elements
- Added
Use pressable button style for presets (2026-01-03)
- Applied
PressableButtonStyleto Today, Week, Month preset buttons
- Applied
Add drag-to-select date range (2026-01-03)
- Drag across calendar days to select a range
- Handles dragging in both directions (forward and backward)
Toggle visual state for all selectable elements (2026-01-04)
- Year/month/week labels and presets show “selected” visual when their range matches
- Single
rangeBinding(from:to:)factory method creates bindings for any date range - Added
SelectableLabelToggleStyle,PresetToggleStyle,WeekNumberToggleStyle - All styles now use
LayoutMetricsconstants andPressableButtonStyle
Fix calendar control layout
- Add folded variant (collapsed view)
- Make year and month labels wider
- Set default state to today (not expanded full calendar)
- Fix overall layout alignment and spacing issues
Replace calendar toggler with simple button (2026-07-13)
- Collapsed state: calendar-icon pill + underlined period label (link style), opens calendar on tap
- Expanded state: same calendar-icon pill at top left (next to the From/To date pickers) collapses it
- Removed
FilterToggleButton; location:HistorySection.swift,CalendarDatePicker.swift
Keep calendar grid height stable across months (2026-07-13)
calendarGridpads with empty rows up toLayoutMetrics.maxWeekRows(6), so 4- and 5-week months render as tall as 6-week ones
Pressable toggle style for the calendar pill button (2026-07-13)
- Moved
PressableButtonStyletoComponents/PressableButtonStyle.swift(was private inCalendarDatePicker.swift) and applied it to the pill inHistorySection.swift
- Moved
Keep the calendar button the same view across collapsed/expanded states (2026-07-13)
HistorySectionnow owns a single persistent pill button that toggles the picker; next to it the period label (collapsed) or the From/To date pickers (expanded)CalendarDatePickeris grid-only now (dropped the header row andonDismiss)
Profile slow first open of the calendar and fix (2026-07-28)
- First expand takes visibly long; subsequent opens are fast
- Suspected: compact
DatePicker(UIDatePicker/UICalendarView) lazy first-init cost on Catalyst; List section relayout secondary - Profile with Instruments (Time Profiler / SwiftUI), then fix (e.g. pre-warm a hidden DatePicker at startup or keep the picker mounted)
Animate calendar expand/collapse (2026-07-14)
- Calendar grid is its own List row under
if isCalendarExpanded; expand/collapse rides List’s native row insertion/removal animation (0.3s easeOut), the only row-height change List animates smoothly - Animating the height inside a row was a dead end: self-sizing UICollectionView cells resize discretely, so per-frame
animatableDatare-measure jitters (SwiftUI draws the interpolated height while the cell applies it a beat later); Apple DTS confirms there is no supported knob - Pill row keeps constant height (period label / date pickers swap in an always-mounted ZStack, flipped instantly via
.transaction { $0.animation = nil }), so the pill never moves
- Calendar grid is its own List row under
TagFilterView [P3]
Animate tags filter expand/collapse
- Add animation to the tag filter DisclosureGroup, similar to the calendar expand/collapse animation
- Location:
TagFilterView.swift,HistorySection.swift
Rework the filter overall
- Rethink the whole filter area (calendar + tag filters + summary) — layout, interaction, visual design
- Subsumes/relates to “Rearrange filter components” below
All tags unselected by default
- Currently all tags are selected on first open; change default to no tags selected (show all records unfiltered)
Rearrange filter components
- Review layout and order of CalendarDatePicker, tag filters, and summary controls
- Improve visual hierarchy and discoverability
Add drag-to-select for tag filters
- Press and drag across tags to toggle selection
- Similar to calendar drag-to-select behavior
Inbox
Unsorted ideas. Review periodically.
- Play button background gradient in OKLCH with hours marks
- Logo: play button with gradient and hours/minutes marks, 3D watch concept
- Sort new timers consistent with file system
- Heatmap view for records (monthly calendar visualization)
- Auto start/stop based on app activity (macOS, accessibility permissions)
- Better timeline navigation (swipe gestures, scrubber)
- Native Apple Charts in history view (iOS 16+ / macOS 13+)
- Not sure if we need to show Today section when it falls within the selected date span — experiment with hiding it
- Record a short demo video of real usage to show people after installation (user feedback); could live on the website / downloads page
Related
- 911-completed - Completed tasks archive
- 912-audit-2025-12-25 - Audit report
- 901-2025-12-08-init - Project initialization
- 902-2025-12-09-editor - Editor implementation