* fix(issue): prevent crash from orphan issueProviderId (#7135) The Jira image-headers effect in task-detail-panel subscribed to selectIssueProviderById without an error handler, so a task with an issueProviderId pointing at a deleted provider (e.g. after sync convergence where taskIdsToUnlink didn't cover all local tasks) propagated the selector throw to Zone.js as a crash dialog. Wrap the inner selector observable in catchError that logs and falls back to of(null); the downstream jiraCfg?.isEnabled guard handles the fallback. Also drop IssueLog.log(issueProviderKey, issueProvider) from the throwing variant of the selector: providers may carry credentials (host, token, apiKey) and IssueLog history is exportable. * fix(focus-mode): sync tray countdown with in-app timer during breaks Tray title was rebuilt from a cached currentFocusSessionTime that only refreshed when CURRENT_TASK_UPDATED fired. addTimeSpent is gated on an active current task, so during focus-mode breaks or task-less focus sessions the cache froze while the in-app timer kept ticking. Add the tick action to taskChangeElectron$ so the cache refreshes every second whenever the focus timer is running. Fixes #7278 * fix(ci): restore GitHub Actions SHA pins undone by0e9218bd68Commit0e9218bd68silently reverted PR #7212 (github-actions-minor group bump) along with its stated sync/client-id work. This restores the 15 workflow files to their pre-revert state. Actions restored to newer pinned SHAs: - actions/upload-artifact v7.0.0 -> v7.0.1 - step-security/harden-runner v2.16.1 -> v2.17.0 - softprops/action-gh-release v2.6.1 -> v3.0.0 - signpath/github-action-submit-signing-request v2.0 -> v2.1 - anthropics/claude-code-action v1.0.89 -> v1.0.93 - docker/build-push-action v7.0.0 -> v7.1.0 - easingthemes/ssh-deploy v5.1.1 -> v6.0.3 * fix: restore i18n, UI, and docs work undone by0e9218bd68Commit0e9218bd68silently reverted the following work alongside its stated sync/client-id changes. Files where later master commits (fec7b25f23, etc.) already re-applied the reverted work are intentionally left untouched. Restored: - #7232 docs/long-term-plans/location-based-reminders.md (513 lines) - #7199 Romanian i18n phase 3 (ro.json + ro-md.json, ~1168 lines) - #7049 Polish translation improvements - #7143 planner component styling (4 scss files) - #7211 add-task-bar preserve time estimate when typing title - #7208 task.reducer roll-up estimates for added subtasks - #6767 focus-mode pomodoro reset button - #7205 plugin-dev github-issue-provider TOKEN description - #7231 mobile-bottom-nav FAB fix -a4fe03272iOS keyboard accessory bar (global-theme + dialog-fullscreen-markdown) -309670db3ShortSyntaxEffects undefined guard -667a7986fDropbox PKCE auth comment/behavior * fix(electron): restore electron + e2e work undone by0e9218bd68Commit0e9218bd68silently reverted the following electron/e2e work. Files already re-fixed by later master commits are left as-is: - e2e/tests/sync/supersync-archive-conflict.spec.ts (de33234976+191d129ff3) Restored: -e8a3e156ebfix(electron): Linux autostart IDB backing-store recovery (re-adds electron/clear-stale-idb-locks.ts + start-app.ts wiring) -5ce78a5b63fix(electron): macOS Cmd+Q / Dock > Quit hang (setIsQuiting + before-quit delegate to close-handler) -46e0fa2d01fix(sync): FILE_SYNC_LIST_FILES IPC contract (electronAPI.d.ts + local-file-sync + preload + ipc-events) -ea1ef16307fix(android): session-only SAF permissions on OEM devices -8865dc0a50test(e2e): supersync parallel-worker stampede guard (SUPERSYNC_SERVER_HEALTHY env-var fallback + goto retry loop) -af7c7687e2test(e2e): block WS-triggered downloads in non-WS specs -265b44db5dtest(e2e): premature waitForURL on daily-summary (this is literally the fix the bad commit's message claimed to add) Conflict resolutions: - e2e/utils/supersync-helpers.ts: kept the refined getDoneTaskElement checks fromd64014d086(later thanc558bcab5e) while restoring the goto retry loop from8865dc0a50. - electron/start-app.ts: unioned imports (setIsQuiting + clearStaleLevelDbLocks from theirs, fs from ours). * fix(sync): restore sync-core work undone by0e9218bd68Commit0e9218bd68silently reverted parts of several sync fixes. Most sync-core reverts have already been re-addressed differently on master by later commits (1f5184f6e7,05cd875dd6,09f5ced2c9,7df43358ab,d9158d6adb,32dbc95ed9,8c3b08e016,f89fe1ebc3) — those files are intentionally left untouched to avoid reverting master's newer work. This PR restores only the pieces that are genuinely still missing: -e8a3e156ebfix(electron): IDB backing-store autoreload (in-app piece) operation-log-hydrator.service.ts + .spec.ts (the electron/clear- stale-idb-locks.ts piece was restored in the prior commit) Plus three low-risk documentation/cleanup restorations: - operation-sync.util.ts — add "Nextcloud" to isFileBasedProvider JSDoc - dropbox.ts — restore improved _getRedirectUri JSDoc (667a7986fb) - dialog-get-and-enter-auth-code.component.ts — restore isNativePlatform comment explaining why manual code entry flow is used (667a7986fb) - file-adapter.interface.ts — remove stray "// NEW" comment (46e0fa2d01) Intentionally NOT restored (master's newer work covers or supersedes): - sync-trigger.service.ts / sync.effects.ts (05cd875dd6) - sync-wrapper.service.ts (1f5184f6e7) - sync-errors.ts (1f5184f6e7re-added LegacySyncFormatDetectedError) - file-based-sync-adapter.service.ts + spec (1f5184f6e7+d9158d6adb) - file-based-sync.types.ts (1f5184f6e7) - operation-log.const.ts (32dbc95ed9bumped IDB_OPEN_RETRIES to 5) - dialog-sync-initial-cfg.component.ts (f89fe1ebc3)
19 KiB
Location-Based Reminders — Design Document
Status: Planned (Brainstorm)
Overview
Add location-based reminders to Super Productivity. Users can attach a saved location to a task and receive a notification when they arrive at that place. This is primarily a mobile feature (Android/iOS via Capacitor), with passive location display on desktop/web.
Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Primary platform | Mobile (Android/iOS) | Geofencing requires GPS + background location. Desktop/web show location info but don't trigger. |
| Trigger type | Arrive only (v1) | Simplest. Leave triggers can be added later. |
| Location management | Saved locations entity | Users revisit the same places. Follows existing Tag entity pattern. |
| Location picker (v1) | "Use current location" + label | No map needed for v1. Map picker is a v2 enhancement. |
| Geofencing approach | Custom native Capacitor plugin | @capacitor/geolocation only does point-in-time reads, not geofencing. Need native GeofencingClient (Android) / CLLocationManager (iOS). |
| Sync | Sync by default | Location data treated like any other entity. Each device manages its own geofences locally after sync. |
| Feature toggle | isLocationRemindersEnabled in AppFeaturesConfig |
Opt-in, default false. |
1. Data Model
1.1 SavedLocation Entity
New file: src/app/features/saved-location/saved-location.model.ts
import { EntityState } from '@ngrx/entity';
export interface SavedLocationCopy {
id: string;
title: string; // "Office", "Grocery Store", "Gym"
lat: number; // latitude
lng: number; // longitude
radius: number; // geofence radius in meters (default 200)
icon?: string | null; // material icon name, e.g. 'home', 'work', 'shopping_cart'
created: number; // creation timestamp
modified?: number; // last update timestamp
}
export type SavedLocation = Readonly<SavedLocationCopy>;
export type SavedLocationState = EntityState<SavedLocation>;
1.2 Task Model Changes
File: src/app/features/tasks/task.model.ts — add to TaskCopy:
/** ID of a SavedLocation. When set, a geofence reminder is active for this task. */
locationReminderId?: string | null;
1.3 Config Changes
File: src/app/features/config/global-config.model.ts — add to AppFeaturesConfig:
isLocationRemindersEnabled: boolean; // default false
File: src/app/features/config/default-global-config.const.ts — set default:
isLocationRemindersEnabled: false,
1.4 Platform Capabilities
File: src/app/core/platform/platform-capabilities.model.ts — add:
/** Whether the platform supports native geofencing (background location monitoring). */
readonly geofencing: boolean;
| Platform | geofencing |
|---|---|
Android (ANDROID_CAPABILITIES) |
true |
iOS (IOS_CAPABILITIES) |
true |
Electron (ELECTRON_CAPABILITIES) |
false |
Web (WEB_CAPABILITIES) |
false |
2. Sync & Persistence Registration
All registration steps required for the new entity, following existing patterns:
2.1 Entity Type
File: packages/shared-schema/src/entity-types.ts
Add 'SAVED_LOCATION' to the ENTITY_TYPES array.
2.2 Action Types Enum
File: src/app/op-log/core/action-types.enum.ts
// SavedLocation actions
SAVED_LOCATION_ADD = '[SavedLocation] Add SavedLocation',
SAVED_LOCATION_UPDATE = '[SavedLocation] Update SavedLocation',
SAVED_LOCATION_DELETE = '[SavedLocation] Delete SavedLocation',
These string values are immutable once deployed — they are used for encoding/decoding operations in IndexedDB and sync between clients.
2.3 Entity Registry
File: src/app/op-log/core/entity-registry.ts
Add to ENTITY_CONFIGS:
SAVED_LOCATION: {
storagePattern: 'adapter',
featureName: SAVED_LOCATION_FEATURE_NAME,
payloadKey: 'savedLocation',
adapter: savedLocationAdapter,
selectEntities: createSelector(
selectSavedLocationFeatureState,
selectSavedLocationEntitiesFromAdapter,
),
selectById: selectSavedLocationById,
},
2.4 Model Config
File: src/app/op-log/model/model-config.ts
Add to AllModelConfig type:
savedLocation: ModelCfg<SavedLocationState>;
Add to MODEL_CONFIGS:
savedLocation: {
defaultData: initialSavedLocationState,
isMainFileModel: true,
repair: fixEntityStateConsistency,
},
2.5 Root State
File: src/app/root-store/root-state.ts
[SAVED_LOCATION_FEATURE_NAME]: SavedLocationState;
2.6 Feature Store Registration
File: src/app/root-store/feature-stores.module.ts
StoreModule.forFeature(SAVED_LOCATION_FEATURE_NAME, savedLocationReducer),
2.7 Cascading Delete Meta-Reducer
File: src/app/root-store/meta/task-shared-meta-reducers/saved-location-shared.reducer.ts
When a SavedLocation is deleted, clear locationReminderId on all tasks that reference it. This must be a meta-reducer (not an effect) to ensure atomicity — one operation in the sync log.
Register in src/app/root-store/meta/meta-reducer-registry.ts in Phase 5 (Entity-Specific Cascades), alongside tagSharedMetaReducer, projectSharedMetaReducer, etc.
3. NgRx Store
3.1 Actions
File: src/app/features/saved-location/store/saved-location.actions.ts
Following the Tag action pattern with PersistentActionMeta:
| Action | OpType | Payload |
|---|---|---|
addSavedLocation |
Create |
{ savedLocation: SavedLocation } |
updateSavedLocation |
Update |
{ savedLocation: Update<SavedLocation> } |
deleteSavedLocation |
Delete |
{ id: string } |
All include meta: { isPersistent: true, entityType: 'SAVED_LOCATION', entityId, opType } satisfies PersistentActionMeta.
3.2 Reducer
File: src/app/features/saved-location/store/saved-location.reducer.ts
Standard @ngrx/entity adapter:
export const SAVED_LOCATION_FEATURE_NAME = 'savedLocation';
export const savedLocationAdapter = createEntityAdapter<SavedLocation>({
sortComparer: (a, b) => a.title.localeCompare(b.title),
});
export const initialSavedLocationState = savedLocationAdapter.getInitialState();
3.3 Selectors
File: src/app/features/saved-location/store/saved-location.selectors.ts
| Selector | Returns |
|---|---|
selectSavedLocationFeatureState |
Feature state |
selectAllSavedLocations |
SavedLocation[] |
selectSavedLocationById |
SavedLocation | undefined |
selectSavedLocationEntities |
Dictionary<SavedLocation> |
Task-side selector (in task selectors or a cross-feature selector):
| Selector | Returns |
|---|---|
selectTasksWithLocationReminder |
All undone tasks that have locationReminderId set |
3.4 Service
File: src/app/features/saved-location/saved-location.service.ts
Thin wrapper dispatching actions to the store. Methods: addSavedLocation(), updateSavedLocation(), deleteSavedLocation(), getById$().
4. Geofencing Service
4.1 Architecture
New file: src/app/features/saved-location/geofence.service.ts
Store (undone tasks with locationReminderId)
→ GeofenceService watches selector (distinctUntilChanged)
→ Computes which locations need active geofences
→ Registers/unregisters via custom Capacitor plugin
→ Receives geofence enter events
→ Emits to ReminderService for dialog/notification
4.2 Why a Custom Capacitor Plugin
@capacitor/geolocation only provides one-time and continuous position reads — it does not support geofencing. Native geofencing requires:
- Android:
com.google.android.gms.location.GeofencingClient(Google Play Services). Supports up to 100 geofences. FiresBroadcastReceiveron enter/exit. - iOS:
CLLocationManager.startMonitoring(for: CLCircularRegion). Supports up to 20 monitored regions. Fires delegate callbacks on enter/exit.
Implementation: Create GeofencePlugin.kt (Android) and GeofencePlugin.swift (iOS) extending Capacitor's Plugin class. Register in CapacitorMainActivity.onCreate() following the pattern of SafBridgePlugin, WebDavHttpPlugin, etc.
4.3 Lifecycle Rules
Register geofence when:
- Feature is enabled + a task gets
locationReminderIdassigned - App starts with existing location-reminded tasks
Unregister geofence when:
- Task is completed, deleted, or
locationReminderIdcleared - SavedLocation is deleted
- No more undone tasks reference that location
- Feature is disabled
iOS 20-region limit: Only register geofences for the 20 locations with the most active tasks. Re-evaluate when tasks change.
4.4 Effects
Effects MUST use inject(LOCAL_ACTIONS) — geofence registration should never happen during remote sync replay. Each device manages its own geofences based on local state after sync.
4.5 Background Behavior
When geofence fires while app is in background:
- Android:
BroadcastReceivershows native notification directly (same pattern asReminderAlarmReceiver). Tapping opens app + reminder dialog. - iOS:
CLLocationManagerdelegate fires local notification. Tapping opens app + reminder dialog.
4.6 Permission Flow
On first use:
- Check
CapacitorPlatformService.hasCapability('geofencing') - Request
ACCESS_FINE_LOCATION+ACCESS_BACKGROUND_LOCATION(Android) or "Always" location access (iOS) - If denied: feature degrades to display-only, show explanation
5. Reminder Integration
5.1 Integration Point
The existing ReminderService.onRemindersActive$ is a derived observable — external code cannot emit to it directly. Two approaches:
Option A (recommended): Add a new public subject on ReminderService:
// In ReminderService
private _onLocationReminders$ = new Subject<TaskWithReminderData[]>();
locationReminders$ = this._onLocationReminders$.asObservable();
Then merge in ReminderModule:
merge(
this._reminderService.onRemindersActive$,
this._reminderService.locationReminders$,
).subscribe(reminders => /* existing dialog handling */);
Option B: Add a public method emitLocationReminders() that pushes to the private _onRemindersActive$ subject.
5.2 Data Shape
Location-triggered reminders must satisfy TaskWithReminderData:
interface TaskWithReminderData extends Task {
readonly reminderData: { remindAt: number }; // use trigger timestamp
readonly parentData?: Task;
readonly isDeadlineReminder?: boolean; // false for location
}
Consider adding isLocationReminder?: boolean for UI differentiation.
5.3 Dialog UX Changes
The existing DialogViewTaskRemindersComponent works as-is with minor additions:
- Show location name/icon in header (e.g., "At: Grocery Store")
- Snooze = suppress this location reminder for 1 hour (temporary geofence pause)
- Dismiss = clear
locationReminderIdon the task - Done = mark task complete (same as today)
- Hide "Edit Reminder" time picker for location-triggered reminders
5.4 Native Notifications (Background)
Reuse CapacitorReminderService patterns:
- Android: native notification via
ReminderNotificationHelpertriggered from geofenceBroadcastReceiver - iOS:
LocalNotifications.schedule()triggered fromCLLocationManagerdelegate - Both support Done/Snooze action buttons
6. UI Components
6.1 New Components
| Component | Path | Purpose |
|---|---|---|
saved-location-settings |
src/app/features/saved-location/saved-location-settings/ |
CRUD list in settings page |
location-picker-dialog |
src/app/features/saved-location/location-picker-dialog/ |
Assign location to task (dropdown + "Use current location") |
6.2 Modified Components
| Component | Change |
|---|---|
| Task detail panel | Add "Location" field showing assigned location |
| Task schedule dialog | Add location option alongside time-based reminder |
| Settings page | Add "Locations" section |
| Reminder dialog | Show location name when triggered by geofence |
6.3 Location Picker UX
┌─────────────────────────────────┐
│ Set Location Reminder │
├─────────────────────────────────┤
│ │
│ 📍 Use Current Location │ ← gets GPS, prompts for label
│ │
│ ─── Saved Locations ────────── │
│ │
│ 🏠 Home │
│ 🏢 Office │
│ 🛒 Grocery Store │
│ │
│ [ Remove Location ] [ Cancel ] │
└─────────────────────────────────┘
6.4 Settings Section
┌─────────────────────────────────┐
│ Location Reminders │
├─────────────────────────────────┤
│ [Toggle] Enable location │
│ reminders │
│ │
│ Saved Locations: │
│ ┌─────────────────────────┐ │
│ │ 🏠 Home 200m [✏️][🗑]│ │
│ │ 🏢 Office 150m [✏️][🗑]│ │
│ │ 🛒 Grocery 200m [✏️][🗑]│ │
│ └─────────────────────────┘ │
│ [ + Add Location ] │
└─────────────────────────────────┘
7. Cross-Platform Behavior
| Platform | Geofencing | Location Display | Notifications |
|---|---|---|---|
| Android | Native GeofencingClient, up to 100 fences, background |
Yes | Native via BroadcastReceiver |
| iOS | Native CLLocationManager, up to 20 regions, background |
Yes | Native via LocalNotifications |
| Electron | None | Yes (label only) | None |
| Web | None | Yes (label only) | None |
8. File Structure
src/app/features/saved-location/
├── saved-location.model.ts
├── saved-location.const.ts # DEFAULT_RADIUS = 200
├── saved-location.service.ts
├── geofence.service.ts # Capacitor geofencing bridge
├── store/
│ ├── saved-location.actions.ts
│ ├── saved-location.reducer.ts
│ └── saved-location.selectors.ts
├── saved-location-settings/
│ ├── saved-location-settings.component.ts
│ └── saved-location-settings.component.html
└── location-picker-dialog/
├── location-picker-dialog.component.ts
└── location-picker-dialog.component.html
src/app/root-store/meta/task-shared-meta-reducers/
└── saved-location-shared.reducer.ts # Cascading delete
android/app/src/main/java/.../
└── GeofencePlugin.kt # Native Android geofencing
GeofenceBroadcastReceiver.kt # Handles fence enter events
ios/App/App/
└── GeofencePlugin.swift # Native iOS geofencing
9. All Files to Touch
New Files
| File | Purpose |
|---|---|
src/app/features/saved-location/saved-location.model.ts |
Entity interface |
src/app/features/saved-location/saved-location.const.ts |
Defaults |
src/app/features/saved-location/saved-location.service.ts |
Service |
src/app/features/saved-location/geofence.service.ts |
Capacitor bridge |
src/app/features/saved-location/store/saved-location.actions.ts |
Actions |
src/app/features/saved-location/store/saved-location.reducer.ts |
Reducer + adapter |
src/app/features/saved-location/store/saved-location.selectors.ts |
Selectors |
src/app/features/saved-location/saved-location-settings/* |
Settings UI |
src/app/features/saved-location/location-picker-dialog/* |
Picker dialog |
src/app/root-store/meta/task-shared-meta-reducers/saved-location-shared.reducer.ts |
Cascading deletes |
android/.../GeofencePlugin.kt |
Android native geofencing |
android/.../GeofenceBroadcastReceiver.kt |
Android fence event handler |
ios/App/App/GeofencePlugin.swift |
iOS native geofencing |
Modified Files
| File | Change |
|---|---|
packages/shared-schema/src/entity-types.ts |
Add 'SAVED_LOCATION' |
src/app/op-log/core/action-types.enum.ts |
Add SAVED_LOCATION_ADD/UPDATE/DELETE |
src/app/op-log/core/entity-registry.ts |
Add SAVED_LOCATION config |
src/app/op-log/model/model-config.ts |
Add to AllModelConfig + MODEL_CONFIGS |
src/app/root-store/root-state.ts |
Add to RootState |
src/app/root-store/feature-stores.module.ts |
Register feature store |
src/app/root-store/meta/meta-reducer-registry.ts |
Register cascading delete in Phase 5 |
src/app/features/tasks/task.model.ts |
Add locationReminderId field |
src/app/features/config/global-config.model.ts |
Add isLocationRemindersEnabled |
src/app/features/config/default-global-config.const.ts |
Set default false |
src/app/core/platform/platform-capabilities.model.ts |
Add geofencing capability |
src/app/features/reminder/reminder.service.ts |
Add locationReminders$ subject |
src/app/features/reminder/reminder.module.ts |
Merge location reminders into dialog flow |
android/app/src/main/AndroidManifest.xml |
Add location permissions |
android/.../CapacitorMainActivity.kt |
Register GeofencePlugin |
10. Future Enhancements (Out of Scope)
- Map picker with OpenStreetMap tiles
- Address search / geocoding (Nominatim)
- Leave triggers ("remind when I leave the office")
- Time + location combos ("at the store, but only after 5 PM")
- Project default locations
- Location-based task views ("show tasks for where I am now")
- WiFi-based triggers (alternative to GPS, works indoors)
- Bluetooth beacon triggers
11. Verification Plan
- Unit tests: SavedLocation reducer, selectors, service, cascading delete meta-reducer
- Manual mobile testing:
- Create location from "Use current location"
- Assign to a task, verify geofence registration
- Move in/out of geofence, verify notification fires
- Complete task, verify geofence unregistered
- Delete location, verify
locationReminderIdcleared on tasks
- Desktop/web: Verify location label displays on tasks, no geofencing attempted
- Sync: Create location on device A, verify it appears on device B
- Lint/format:
npm run lint,npm run prettier,npm test