architecture widgets and watch - fahadahmed/jamaal-app GitHub Wiki
How Jamaal appears outside the app: the v1 widgets and the v1.1 watch companion. The decision and its reasons are in ADR 0003. The frames are drawn (batches 8 and 9, WG- and WA- in the register); what they decided is in the reconciliation. Where this page and the frames differ on detail, the frames win and this page is updated to match.
| Widget | Sizes | Shows | Tap |
|---|---|---|---|
| Next Anchor | Small; Lock Screen circular, rectangular and inline | The current or next Anchor window: Dhuhr · until 15:32, School run opens 15:00 | Opens Today |
| Today | Small, medium, large | The headline (Four things, gently paced.), the capacity bar, the next two or three tasks (more on large) | Opens Today (a task opens its detail on a wide layout) |
On iPhone, iPad and Mac (desktop and Notification Center). Plan tomorrow (which opens Night Planning) appears from the planning time on the medium and large Today widget only, never on the Lock Screen, and is the only terracotta on any widget. Not in v1: interactive habit logging, Control Center controls, StandBy-specific designs, Live Activity.
Rules for what a widget shows: the same words and figures as the app (it reuses the engine's reads), no streaks, no red, no Wellbeing score, nothing that nags. A day that has nothing in it says so calmly.
What the frames decided (widgets) (WG-01 to WG-06; the numbered list is in the reconciliation):
- No checkboxes: v1 widgets aren't interactive, so a task shows a category dot (a primary-coloured dot when tinted), never a check circle.
-
Accent group:
widgetAccentable()goes on the Anchor name, Today's count, the italic headline line and the bar fills; everything else is primary. - Next Anchor states: upcoming; open; closing soon (keeps the accent colour; only the word and the bar's length change); none left today; needs attention (a window that closed unmarked: a neutral full bar and Not marked yet, neither accent nor error).
- Today states: normal; all done (the capacity bar is hidden; 4 of 4 done and the next Anchor); blank day (the next Anchor and no "add something" prompt); evening with Plan tomorrow; read-only (the same data, no Plan tomorrow, one quiet View only line, and no upgrade prompt on a widget).
- Dynamic Type at accessibility sizes: the medium drops its headline to the count and shows one task, truncated to one line.
- Habits stay off the large widget for now (provisional; owner to confirm).
- On the Mac the widget is full colour when the desktop is focused and vibrant with a window in front.
A pure, Codable, device-independent value in JamaalCore (no UI, no SwiftData objects), built from the engine's existing Today reads:
-
generatedAt, the logical day it describes, andisReadOnly(the trial ended without a subscription) - the headline count of tasks left, and the capacity (planned minutes, budget, level, load state)
- the count of tasks left and done, and the first N of what is left in Today's order (id, title, effort, category colour key), with how many more there are; the done tasks are only counted
- the next N Anchors still to attend, as windows: id, name, the rule's title when there is one, phase (
open,closingSoon,upcoming, orneedsAttentionfor a closed window nobody marked), start, end, progress. Attended, skipped and delegated ones are left out; open ones come first, then upcoming, then unmarked ones - the evening planning time and whether it has passed (the link is offered only when not read-only), and when the logical day ends
Built as WidgetSnapshot.make(in:now:access:) in JamaalCore. It only reads: it never marks a prompt as shown.
It is tested like any engine module. The phone's and iPad's widgets, the Mac's, and the watch all use it.
A widget does not tick. Its entries are computed ahead for the moments that change what it says: now; each Anchor window opening and closing in the next day; the evening planning time; the logical-day rollover. At most about a dozen entries, then the system asks again. The app reloads every timeline after a save (the same hook that re-plans notifications), when it comes to the foreground, when a sync brings changes, and when the access state changes. The system limits how often a widget may refresh, so nothing here depends on a per-minute update. After a sync a widget can be stale until the next refresh; it shows data as of the last one.
Widgets use a jamaal:// link: jamaal://today, jamaal://plan (Night Planning), jamaal://task/<id>. The app turns a link into the same route a notification tap uses, so there is one path in: JamaalLink (JamaalCore) parses the URL into a NotificationRoute (jamaal://plan is Night Planning for tomorrow; jamaal://task/<id> is the new .task route, which opens that task on Today), and anything unrecognised (a wrong host, a bad id, extra path) is ignored rather than guessed at. The app registers the jamaal scheme and handles onOpenURL. A link while read-only still works, and plan is refused by the same access check as the Plan tomorrow button. iOS shows its own "Open in Jamaal?" confirmation for links opened from outside the app (Safari, the simctl openurl command); a widget's own tap does not.
-
Task titles are marked privacy-sensitive, so they are hidden on the Lock Screen while the device is locked. Anchor titles (a prayer name, the school run) show on a locked Lock Screen, because the Next Anchor widget is useless without them;
WG-05draws it that way (owner to confirm). Redacted titles are drawn as a plain bar, on a locked Lock Screen, in StandBy and on an iPad Lock Screen. - Full-colour, tinted (accented) and Lock Screen (vibrant) rendering are all designed for. Fonts are bundled into the extension.
- A widget needs no network. Read-only users see the same data with no editing prompts.
The SwiftData store moves into an App Group container, with the CloudKit container set-up, in the first sync-enabled build, so no user ever migrates. The in-memory test and debug modes are unaffected. On the Mac the group identifier is team-prefixed.
How it is wired (#229). One build setting, APP_GROUP_ID, is group.dev.fhdamd.Jamaal on iOS and $(TeamIdentifierPrefix)dev.fhdamd.Jamaal on the Mac; it reaches Info.plist as JamaalAppGroup (the app reads it) and the entitlement files. AppGroup resolves the group's folder and the shared defaults; StoreLocation decides the store's URL (<group>/Library/Application Support/Jamaal.store) in one place, used by the container, by "reset this device's data" and, later, by the widget extension. With no group (the free team, tests, previews) it falls back to exactly where the store has always been, and the entitlement cache stays in the standard defaults, so nothing changes until the full entitlements are on. The preferences other than the entitlement cache (theme, suggestion cards, prompt state) stay in the standard defaults for v1. An existing store in the old location is not migrated (the only data there is the developer's own, before the first release).
The extension (#231). JamaalWidgets is a widget extension for iOS, iPadOS and the Mac, built on JamaalCore and ThreadsKit (the same fonts, registered in the provider). Its first widget is Next Anchor, small (frame WG-01): the open or closing window leads, then the next to open, then a window that closed unmarked; the five states and their words, clock time and bar come from NextAnchorDisplay and NextAnchorCopy in JamaalCore (tested), and the view only draws them (accent group on the name and the bar's fill, no checkboxes). The provider reads the store read-only through StoreLocation, never creates one, plans its entries with WidgetTimeline (one snapshot per entry date) and asks for a new timeline at the last. A tap opens jamaal://today. Until the App Group is on, the widget can't see the app's store (its own folder is empty), so it shows its quiet "Nothing more today" state; this is expected. Gaps: no medium size (no frame for it), "Fajr tomorrow, 05:12" under the empty state (the snapshot doesn't carry tomorrow's first Anchor), read-only ("View only") needs the entitlement cache in the shared defaults, and the Today widget and the Lock Screen accessories are the next slices. In debug builds, -JamaalWidgetGallery (add -JamaalWidgetGalleryDark for dark) draws the widget views in every state at the real 158 pt size, because a widget can't be placed on a Home Screen from a test.
The Today widget (#267). Small, medium and large (frames WG-02 and WG-03). The words come from TodayWidgetDisplay in JamaalCore (the headline and its mood: Four things, gently paced., One thing left, then rest. from the planning time, All done. The rest is yours., Nothing planned. An open day.; the capacity figures and state word; "and N more"; the Anchor line), so they match Today and are unit tested; the extension only draws them. Small shows no task titles; medium shows two tasks and the Plan tomorrow pill from the planning time; large shows five tasks and the Anchor line with its bar. Task titles are marked privacy-sensitive. Not yet: the View only note for a read-only widget is drawn and tested but cannot show, because the entitlement cache is in the app's own defaults, not a shared one; that, and the Lock Screen widgets, are next. -JamaalWidgetGallery -JamaalWidgetGalleryToday (debug) shows every state.
The Lock Screen widgets (#271). Next Anchor now also has the inline, circular and rectangular sizes (frame WG-02), worded by LockScreenAnchorDisplay in JamaalCore: Dhuhr · until 15:32; the circular ring fills as the window passes (dashed before it opens, neutral and full once it closed unmarked); the rectangular one adds Asr opens 15:33 or, in its last quarter, closes in 20 min; with nothing left it says Nothing more today and, when there is one, Fajr tomorrow, 05:12 (the snapshot now carries tomorrowAnchor). The system tints them, so the Anchor name and the fills are the accent group and everything else is primary or secondary. The circular one drops am/pm to fit. Anchor titles show while locked, as decided. -JamaalWidgetGallery -JamaalWidgetGalleryLock (debug) shows all five states.
Refreshing (#245). A widget plans its own updates (an Anchor opening or closing, the planning time, the day's end); it knows nothing about a task ticked, onboarding finished or a change arriving from iCloud unless the app says so. WidgetRefresher calls WidgetCenter.reloadAllTimelines() after every save (debounced to one reload per quiet second), when a remote change arrives on the store (NSPersistentStoreRemoteChangeNotification, which iCloud imports raise), and when the app becomes active or leaves the foreground. Without it, a widget added while the store was empty stayed on "Nothing more today" until the next planned moment, which could be hours. The system may still throttle reloads, so a widget can lag by a short while; opening the app always refreshes it.
Switch-on, once the paid programme is active: see the checklist in TestFlight readiness.
One widget extension target for iOS and macOS, built on JamaalCore and ThreadsKit (and the same fonts). It needs its own App ID, the App Groups capability on the app and the extension, and the paid programme.
-
Before the paid programme (no entitlements needed): these docs and the Design brief;
WidgetSnapshotand the timeline logic in JamaalCore with tests; thejamaal://routing in the app. - After it (about 14 October): the App Group, the store move and the CloudKit container in one change; the extension target; Next Anchor first, then Today, then the Lock Screen widgets, then the Mac.
- Then: apply Design's frames, check VoiceOver and Dynamic Type, and TestFlight with widgets.
- Today: the next task large; tap to mark it done (one soft haptic, as the design rules say); the Digital Crown scrolls the rest.
- Anchors: the current window and the time left, and a Smart Stack card that surfaces at the right time of day.
- Habits: Water counted with the crown; Held today and Slip for avoid habits; a tick for the rest; Begin on a timed habit.
- Focus timer: Begin, pause and finish, with the time in large digits and a soft haptic at the end.
-
Complications, in priority order:
- Next Anchor (the primary one): inline Dhuhr · until 15:32; rectangular, the Anchor and its window with the next one beneath; corner, curved text with a soft arc for how much of the window has passed; circular, a ring with the closing time in the middle. It shows the clock time the window closes; closes in 40 min appears only in the larger rectangular size. No draining ring and no urgent colour: nothing should demand attention.
- Today: circular, a ring of planned time against the day's budget with the number of tasks left inside; rectangular, the next task and its effort. Task titles are privacy-redacted while the watch is locked.
- Focus timer, only while a session runs: a count-up time that opens the focus screen, surfacing in the Smart Stack.
- A habit counter (Water 3 of 8 as a ring) is later: choosing the habit needs a configurable widget, which brings in App Intents.
- Not on a face: the Wellbeing score, anything streak-like, an overdue count. An open Anchor window is the natural moment for the Smart Stack to surface its card by itself.
- Notifications: the window-closing and evening reminders already mirror from the iPhone; Mark done can become an action.
Not on the watch: Night Planning, Wellbeing (a score on the wrist would feel judgemental), Settings, and adding a task (a dictated quick add is a later decision, because it needs defaults for effort and importance).
- Navigation: three vertical pages (Today · Anchors · Habits), at most one level of push, and Focus as a full-screen cover. No tab bar.
- Launch rule: a running session opens Focus; otherwise an open, unmarked Anchor window opens Anchors; otherwise Today. After an hour away the rule runs again.
- Task detail exists on the watch: Begin is the filled button and Done is glass; the checklist is read-only; More on your iPhone points the way to defer and drop.
- Finish offers the finish sheet's two choices, Done and Stop for now; defer, drop and the note line stay on the phone.
- Anchors: Attended is the filled action, and a long-press on a row gives Attended or Skipped. The app says the time left in words (40 min left); complications show the clock time.
- Counted habits are saved as you turn the crown, debounced into one queued action; past the target the ring stays full and the number keeps going.
- Pending state: a queued action shows a dashed ring and waiting for iPhone; an As of 14:20 footer appears only after 15 minutes without a snapshot.
- First run: before the first snapshot arrives, Today says Open Jamaal on your iPhone to begin. Read-only users see the pages with no action buttons.
- 40 mm: the primary button is 40 pt tall there, the only place below 44.
- Icon: the existing monogram layers with a circular mask, one appearance, and the small-size J at list and notification sizes.
- The iPhone owns the store. It sends the latest
WidgetSnapshotto the watch withupdateApplicationContext(latest wins). The watch app and its complications read it from the watch's own container and reload their timelines. - The watch sends actions back (complete a task, change a habit's amount, begin, pause or finish a focus session, attend an Anchor) with
transferUserInfo, so they queue while the phone is out of reach. Each carries an id and a time, so applying one twice is harmless. The phone applies them through the same engine functions the app uses, then sends a new snapshot. If the two disagree the phone wins, and the watch shows a pending mark until it hears back. - Focus on the watch: the timer uses a running-time text so it stays correct, and an extended-runtime session keeps the app alive while a session runs. A session started on the watch is created on the phone, so the one-live-session rule, the chip and (later) the Live Activity stay consistent. If the phone is out of reach the watch keeps its own start time and tells the phone when it reconnects.
An independent watch app (its own store, synced through CloudKit as a fourth device) is possible later and would need the engine, conflict handling and migrations on the watch.
The watchOS platform added to JamaalCore (its manifest lists iOS and macOS today; it imports only Foundation, SwiftData and the prayer-times library, so it should compile) and to ThreadsKit (iOS and macOS today; to be checked), a watchOS 27 simulator runtime (only 10.4 is installed), a watch app target embedded in the iOS app, a Design pass for the watch screens, and the owner's Apple Watch for testing (it takes a device slot on the paid programme).
Settled by the frames and the owner (see the reconciliation): Live Activity stays v1.1; the Next Anchor complication shows the closing clock time and not a draining ring.
Still owner sign-offs (the frames assume them):
- Anchor titles visible on a locked Lock Screen (proposed and drawn: yes).
- Habits off the large Today widget for now (provisional).