Error Toast

A failed action must be visible in a release build. Views that own the action show an alert (the record editor, the tag filter, a running timer’s Stop). Everything else used to set AppState.errorMessage, which only the DEBUG state view displays, so the failure was silent. Those actions now report through AppState.reportError, and the message comes up as a toast.

Appearance

  • A card with an error symbol, the message (wraps, never truncated) and an x button. The x glyph is about 7 points, so its hit area is padded by 10 points on each side (negative outer padding keeps the layout); without that a click landed beside it
  • Main window: at the bottom, above the floating play button and above any deletion toasts
  • Settings and the CSV export sheet: at the bottom of the sheet (.errorToasts()). A sheet covers the window under it, so each sheet in which an action can fail without an alert carries its own container
  • Mini timer window: a flat row under the header, part of the layout (the window hugs its content and can be shorter than a floating card)
  • Identifiers: errorToastMessage (the text), errorToastDismiss (the x button)

What reports here

ActionWhere it is triggered
Start a timerfloating play button, Cmd+N, tray, mini window play button
Stop all timersCmd+., tray. One toast for the command: the message of a single failure, or “2 timers could not be stopped.” followed by the first message
Drop a timerx button of a mini window row
Create, archive, unarchive a tagSettings, Add Tag sheet, tag filter
Undo of a deletedeletion toast
History loadContentView.loadHistoryRecords
CSV export from SettingsExportView
A failed loadDatalaunch, external change, pull to refresh

Not here, because the view shows an alert: editor Save, Stop and Delete, image add and remove in the editor (“Image Error”), tag delete, rename and merge (“Tag Error”), Stop of a single timer (“Stop Error”). Those paths still set errorMessage.

An unreadable month or tags file is not an error: the load succeeds without it, and the storage warning banner explains it.

Behavior

  • A toast disappears after 10 seconds (AppState.errorNoticeLifetime; the 1-second timer that expires deletion toasts calls expireErrorNotices(now:)), or when its x button is tapped
  • The same message again replaces the toast already on screen and restarts its time, so a repeated tap does not stack copies
  • At most 3 toasts (AppState.maxErrorNotices); the oldest goes first
  • A failed load is different: its toast has no expiry (ErrorNotice.expiresAt == nil) and stays until a load succeeds, since it is the reason the lists are stale. Loads repeat (every 60 seconds, on every external change), so a load failure the user dismissed is not shown again until a load has succeeded. Switching the storage folder drops it

Implementation

  • ErrorNotice (id, message, expiresAt), ErrorToastView, ErrorToastContainer and the errorToasts() modifier: Minuta/Sources/Views/Components/ErrorToastView.swift
  • AppState.errorNotices, reportError(_:) (a message or an Error), dismissError(_:), expireErrorNotices(now:)
  • Rule for new code: an action whose caller shows nothing itself calls reportError; a method that returns or throws the failure to a view with an alert sets only errorMessage

Limits

  • A toast is shown by the windows that are open. A tray command that fails while both the main window and the mini window are closed is gone after 10 seconds without having been seen
  • App Intents (Shortcuts, widget buttons) report through the intent’s own error, not here

Tests

  • Minuta/Tests/AppStateErrorNoticeTests.swift: a failed start, a repeated failure, expiry and dismissal, the cap, Stop All, a tag change, and a load overtaken by a storage-folder switch
  • Minuta/UITests/Flows/StorageWarningTests.swift testStartTimerErrorIsShown: the month file and its recovery file are both unreadable, the play button is tapped, the toast names the files