* fix(task-repeat): show from-completion toggle for no-rrule completion cfgs A completion-relative cfg with no rrule and a kept-preset quickSetting (any pre-RRULE or imported cfg, since migration is lazy/save-only) opened under its preset label, hiding the "from completion" toggle, then silently reset repeatFromCompletionDate to false on the next save — flipping the task from "repeat after completion" to "repeat from start date". Hoist the repeatFromCompletionDate check above the if(cfg.rrule) guard and migrate no-rrule completion cfgs into the builder, so the toggle is always visible and quickSetting === 'RRULE' skips the preset reset. * fix(task-repeat): reject never-firing rrules without freezing the main thread isRRuleValid set valid=true whenever the rule merely constructed, even when the probe yielded no occurrence — so a contradictory rule (e.g. FREQ=DAILY;BYMONTH=13) counted as valid and the engine deferred to it, bypassing the legacy fallback and silently never creating a task. Worse, rrule.js walks day-by-day to its year-275760 ceiling (~3.8s main-thread freeze) before returning null, and its UNTIL/before bounds only apply to ACCEPTED occurrences, so they cannot cap a never-firing rule. Pre-screen contradictory BY-constraints (out-of-range values, impossible BYMONTH+BYMONTHDAY combos) before iterating — conservatively, only when an entire constraint is unsatisfiable, so a sound rule is never mis-dropped to legacy. Require the probe to actually return an occurrence. Strip UNTIL and COUNT for the probe: they are anchor-relative end conditions, and a rule whose window already closed still has a sound pattern that the engine applies per cfg — keeping them would resurrect a finished rule forever via the UNTIL-less legacy fallback. * fix(task-repeat): make startDate-less legacy→rrule migration deterministic _parseStart fell back to new Date() when a cfg had no startDate, encoding today's day-of-month into the migrated rrule (vs the legacy engine's day-1 semantics) and making the result depend on WHEN the dialog was opened — two devices migrating the same legacy cfg on different days would produce diverging rrules. Fall back to a fixed 1970-01-01 epoch (month 1, day 1) instead. UI paths always set startDate, so the fallback is only a safety net. * feat(task-repeat): gate RRULE engine behind a local per-device flag The experimental RRULE recurrence engine is now OFF by default behind a per-device flag stored in localStorage (NOT synced), mirroring the TaskWidgetSettings pattern. While off, the legacy repeatCycle engine stays authoritative for every occurrence calculation, and clients that never opt in — including older / mobile clients — are unaffected; enabling it on one device never propagates the half-built engine to another over sync. - isRRuleEngineEnabled() reads localStorage live (no module cache) so it is free of cross-spec state and runtime-toggleable. The three occurrence routing guards check cfg.rrule FIRST, so a device with no RRULE configs never touches storage. - RRuleFeatureFlagService plus a local config section (Tasks tab) provide the opt-in toggle, routed through the service like taskWidget so the value never reaches the sync wire. - The advanced 'RRULE' quick-setting is hidden from the repeat dialog and the add-task-bar menu while off — except for a config already in RRULE mode, so an existing rule stays editable (the builder is its only editor). Lets the epic merge to master without the half-state problem and gives opt-in testers a way to exercise it across devices before it ships default-on in a later release. * docs(task-repeat): add RRULE epic review watchlist Distil every critique from the PR #7948 review thread (9 rounds) into a single reference catalogued by risk class, with status (done / deferred / open) and the phase each item bubbles up in. The recurring themes — synced-field forward-compat, JSON-undefined clears not propagating, legacy↔RRULE fallback fidelity, engine robustness, log redaction — recur in almost every round, so the doc leads with an "Always-verify" checklist to run on any PR touching synced repeat state. Linked from the implementation plan. * docs(task-repeat): add RRULE epic roadmap + branch model Standing-PR epic model (feat/rrule-epic -> master) with a branch per phase (feat/rrule-epic-pN-<slug> — hyphen, since feat/rrule-epic/<phase> is a git file/directory ref conflict with the feat/rrule-epic branch). Documents the fork-based contribution flow, the issue wiring (#4020 needs reopen after the #7948 revert, #4931 parent, #7239 Phase 7), the off-by-default flag, and folds in the full feature-comparison table. Linked from the implementation plan. * docs(task-repeat): render epic phases as a task list Replace the wide phase table with a GitHub-native checklist (interactive checkboxes + progress bar, branch inline per row) so it renders cleanly when pasted into the standing PR body. No content change. * test(task-repeat): cover rrule re-anchor in the reschedule effect `rrule` is in SCHEDULE_AFFECTING_FIELDS so editing only the rule re-anchors the live instance; if it were dropped, an RRULE edit would not re-enter the effect and the stale lastTaskCreationDay would silently suppress every later occurrence. Add a regression test (engine flag on, rrule-only change) asserting the re-anchor — the field was previously unreferenced in the effects spec. * docs(task-repeat): assign persist-boundary guard to its ingestion phases The §5 persist-boundary guard (sub-daily / repeatCycle wire-safety, COUNT+ completion) has near-zero reachability until a non-dialog write path exists, so defer it to the phases that introduce those paths rather than add a speculative sanitizer now: untrusted-ingestion guard → Phase 7 (REST/import), sub-daily handling → Phase 12, COUNT+completion → Phase 5. Also note the rrule re-anchor test under Phase 1 follow-ups. * docs(task-repeat): correct Phase 1 branch attribution Core was built on feat/cron-recurring-schedules (PR #7948), merged then reverted, and feat/rrule-epic was created from that state — so Phase 1 is the BASE of the integration branch, not a merge into it. Cite the real source branch + commits, and separate Core from the follow-ups committed directly on feat/rrule-epic. * docs(task-repeat): render epic phases as a compact table Switch the phase checklist back to a table (✓/phase/branch/scope/status) with short cells + numbered footnotes for the nuance, so it renders cleanly — the earlier table was cramped by a long scope column. * feat(task-repeat): Phase 2 — live RRULE calendar preview + completion simulation Adds a calendar (heatmap) preview to the RRULE builder, driven by the in-progress rule so it works while authoring a brand-new recurrence (no saved cfg needed): - generic `heatmap` UI gains a `dayClick` output plus `isProjected`/`isCompleted` cell states; week-grid + projection layout extracted to `build-heatmap-data.util`. - the dialog's result block gets a "Calendar preview" toggle showing the next 365 days of projected occurrences; clicking a day simulates completing it there and, for a repeat-from-completion schedule, re-anchors the rest of the series (a fixed-calendar schedule is unaffected, by design). - `repeat-task-heatmap` (the saved-cfg history view) overlays upcoming occurrences via `getRRuleOccurrencesInRange`. All projection/simulation is naturally behind the RRULE engine flag (RRULE-mode UI). Per-instance overrides (moves/RDATE) are Phase 8 — only EXDATE skips (deletedInstanceDates) apply here. Specs for the new util and the dialog gating/projection/simulation. * docs(task-repeat): mark Phase 2 in progress in the epic roadmap Bump the Phase-2 (heatmap + simulation) row to in-progress now that the live calendar preview + completion simulation has landed on feat/rrule-epic-p2-heatmap. * feat(ui): month-grouped heatmap layout + month calendar view with year/month toggle Reshape the heatmap surfaces around months: - HeatmapComponent gains a `groupByMonth` layout — each month its own mini day-grid, spaced, with a label and per-month total beneath (hours for activity/history, occurrence count for projections); built by buildHeatmapMonths in build-heatmap-data.util. - New HeatmapMonthCalendarComponent: single-month calendar with numbered, level-coloured day cells, prev/next navigation bounded to the data range, weekday headers and a legend (Low→High gradient for relative activity coloring, projected/completed swatches for projections). - New HeatmapSwitcherComponent wraps both behind a Year/Month toggle; wired into the RRULE builder calendar preview (clickable, simulation-aware), the repeat history heatmap and the metric activity heatmap. - Heatmap day tooltips and legend labels now go through T/translate. The metric share-canvas export intentionally keeps the continuous weeks strip. Specs for the month grouping, total formatters and calendar navigation. * fix(task-repeat): correct calendar-preview review findings Five fixes from the Phase-2 review pass: - The month calendar compared wall-clock "now" against the noon-anchored rangeStart, so opening the preview before noon defaulted the view to the month of rangeEnd — a year ahead. Compare calendar days instead. - A navigated month is discarded when it falls outside the current data range (e.g. the metric year select swaps the range under a mounted calendar), instead of stranding the user on an all-empty month with nav disabled. - Other-month spill-over cells no longer emit dayClick: they render greyed with no level/completed styling, so consumer actions fired with zero visual feedback. - Simulation clicks are ignored for fixed-calendar schedules (nothing re-anchors on completion), the sim hint only shows for repeat-from-completion rules, and an active simulation is cleared when the rule or schedule type is edited — previously a stale sim silently distorted the preview of the newly authored rule and fixed schedules rendered a false "completed" day that inflated the month's occurrence count. - The 365-day projection no longer rebuilds on every keystroke in unrelated dialog fields: the heavy computed now hangs off a value-equal memo of the four schedule-relevant fields (formly emits a cloned model per keystroke). getEffectiveRepeatStartDate's parameter is narrowed to the fields it reads. * feat(ui): heatmap feedback round — year row, labels, legend, persistence, year nav Address review feedback on the heatmap surfaces: - Year view renders Jan–Dec in a single row (horizontal scroll when narrow) with the weekday labels (Sun–Sat) pinned back on the left, outside the scroll area. - Year view gets a legend again via a legendMode input on HeatmapComponent: Low→High intensity gradient for activity data, projected/completed swatches for projections; the switcher forwards each consumer's mode. - The Year/Month toggle is persisted per consumer (heatmap-switcher persistKey → localStorage; a per-device UI preference, not synced). - Metrics year filter is prev/next chevron navigation over the years with data instead of a dropdown. - The recurring-task history heatmap gains the same year filter; its range is now one calendar year (upcoming occurrences overlay the selected year's remaining days). This also removes the 16-month view's duplicate month labels and keeps the weeks grid within the 54-week layout cap. * refactor(ui): resolve remaining heatmap review findings - Replace hardcoded #fff day-number colors in the month calendar with the theming contract: translucent levels inherit the theme text color, solid primary cells use var(--c-contrast) — readable in light and dark themes. - Drop the ::ng-deep .mat-button-toggle-button override in the shared switcher for Material's --mat-button-toggle-label-text-size token (no .mat-* piercing, per the styling rules). - Finish the half-migration in activity-heatmap: delete its private _buildWeeksGrid copy (incl. two hardcoded English month arrays) and dead getDayClass/getDayTitle/weeks members; the shared buildHeatmapWeeks (with localized month names) now feeds the share-canvas export. - Delete HeatmapComponent's unreachable legacy strip layout and its dead showLegend/scrollToEnd/groupByMonth inputs, scroll effect and styles — the component renders the month-grouped layout only; weeks/monthLabels stay in HeatmapData purely as export data. - Rename legendMode values to what they render: 'intensity' | 'projection' | 'none' (was 'hours'/'occurrences', named after a discarded absolute-buckets design). - Export HeatmapViewData and use it in the switcher input and both explicit consumer computeds instead of three hand-rolled intersection types. * feat(ui): heatmap feedback round 2 — in-card year nav, wider metrics, activity setting - Year navigation (‹ 2026 ›) moves INSIDE the heatmap card, styled like the month calendar's ‹ June 2026 › header: new navLabel/canNavPrev/canNavNext inputs + navPrev/navNext outputs on HeatmapComponent, passed through the switcher. The metrics page's external year row is gone, and the recurring task history heatmap now always shows its year filter (chevrons disable at the data bounds). - The metrics heatmap section widens (880px → 1200px) so the 12 month blocks use the page's spare width instead of scrolling. - The recurring-cfg dialog keeps its normal width by design — the year strip scrolls horizontally inside the card; the heatmap's old 500px cap is gone so it fills the dialog. - New setting under Settings → Tasks: "Show recurring task activity when editing a recurring task config" (TasksConfig.isExpandActivityForRepeatEdit, optional synced field — old clients ignore it). When on, the dialog's Activity section starts expanded. * fix(heatmap): center year strip and cap month calendar width Year strip left all leftover card width right of December; the month calendar's 1fr/aspect-ratio cells scaled with the page and forced vertical scrolling on the metrics view. Center the strip, legend and repeat-dialog time summary; cap the calendar at 360px. * fix(task-repeat-cfg): keep calendar preview visible during rule edits The preview vanished with no way back in two states: a rule with no occurrence in the 365-day window rendered nothing while the toggle stayed open, and a mid-edit unparseable rule removed the whole result strip including the toggle button. Keep the strip rendered in builder mode and show an explanatory hint in both empty states. * fix(task-repeat-cfg): offer custom recurring config with engine off The RRULE builder replaced the legacy Custom UI, but its dropdown entry was gated behind the per-device engine flag - flag-off devices were left with fixed presets only. Always offer it: saves write the legacy mirror fields the flag-off engine schedules from, the same fallback older sync clients use. * test(task-repeat-cfg): scope rrule engine flag hooks to their suites Top-level beforeEach/afterEach attach to Jasmine's root suite, and Karma bundles all specs into one env — so four occurrence spec files were forcing the rrule engine flag ON around every test in the app. Move the hooks inside each file's describe so default-off specs stay bundle-order-independent. * fix(heatmap): tighten projection trust and polish review findings - Project future occurrences only when the cfg has a valid rrule (mirrors the routing utils' gate): the legacy-cfg converter diverges from the legacy engine for WEEKLY interval>=2 and zero-weekday cfgs, so those overlays showed days the device would never fire on. - Skip zero-value timeSpentOnDay entries in availableYears — a zero-time year rendered no heatmap, stranding the user on an empty view with the year nav removed. - Year-stamp the first month block of each year in buildHeatmapMonths so a rolling 13-month window can't show two identical month labels. - Clear an active completion simulation when startDate or excluded days change (schedule-slice effect) — a sim belongs to the exact schedule it was clicked on. - Localize heatmap day labels via DateAdapter like the month view. - Add an opt-in interactive mode to heatmap day cells (role, tabindex, enter/space) now that click-to-simulate is a real interaction. - Extract the duplicated prev/next year-nav quartet into a shared year-nav util; fix the stale getRRuleOccurrencesInRange comment. * test(task-repeat-cfg): scope rrule engine flag hooks to their suites Top-level beforeEach/afterEach attach to Jasmine's root suite, and Karma bundles all specs into one env — so four occurrence spec files were forcing the rrule engine flag ON around every test in the app. Move the hooks inside each file's describe so default-off specs stay bundle-order-independent. * fix(task-repeat-cfg): pre-screen impossible BYMONTH x BYYEARDAY/BYWEEKNO rules _canNeverFire only paired BYMONTH with positive BYMONTHDAY, so contradictions like FREQ=DAILY;BYWEEKNO=53;BYMONTH=2 slipped past the pre-screen into the validity probe, which walks rrule.js day-by-day to its iteration ceiling (~7-10s measured) before resolving false - a one-time main-thread freeze on the save click that first sees the rule. Extend the pre-screen with month-possibility checks for positive year-days (both leap layouts) and positive week numbers (a conservative day-span superset that covers WKST shifts and year-boundary spill - ISO week 1 can include late December and week 53 early January, which rrule.js honors via ISO week-year semantics; verified empirically). Negative values count from the year's end, so they skip the check. Conservative by construction: satisfiable combos are pinned by spec to stay valid. Also corrects the spec comment that described the probe as a bounded .between() (it is _canNeverFire + an unbounded memoised .after()), replaces the timing assertion that only ever exercised the O(1) fast-reject path, and adds a save-path spec proving a never-firing raw override is rejected by the isRRuleValid gate before the startDate probe can walk. * fix(task-repeat-cfg): derive RRULE quick-setting availability live On the task/repeatCfgId edit path the cfg loads async, after _initializeFormConfig captured includeRRule in the buildOptions closure - so on a flag-off device a completion cfg migrated to builder mode left the quick-setting select holding 'RRULE' with no matching option. Read the flag/mode per evaluation instead and key the options memo on it so the async migration rebuilds the list. Also flip buildRepeatQuickSettingOptions' includeRRule default to false: a future caller that forgets the param now fails safe (no advanced option on flag-off devices) instead of silently offering rules the local engine would ignore. Both existing call sites pass explicitly. * test(task-repeat-cfg): use withContext for combo validity assertions Jasmine's toBe() takes no message argument - the rule name passed as a second param was silently ignored. * fix(heatmap): keep current-year month labels plain Only stamp the year onto a month block when it differs from the current year - 'Jun', not 'Jun 2026', for the implied default year; the next year's stamp still marks the boundary in a rolling window. * fix(heatmap): drop current-year from month header and hide dead year arrows - Month-calendar header reads 'June' for the current year; other years keep the year suffix so cross-year navigation stays unambiguous. - The year strip's prev/next arrows render only when at least one direction is navigable - a single-year series showed two permanently disabled buttons. * chore: ignore local run-verification screenshots * feat(task-repeat-cfg): unbounded year/month navigation for the calendar preview The preview was a fixed next-365-days window. Now: - The year strip gets in-card arrows that shift the window by whole years, unlimited in both directions; the title shows the window's year span (e.g. 2026 - 2027). The projection is computed per window, so any year is reachable, including past ones (full pattern shown). - The month calendar gains a boundless mode (new heatmap-month-calendar input + viewMonthChange output, passed through the switcher): month arrows never hit a wall, and when the shown month leaves the window the dialog shifts the window a year so data follows. - The window is padded to full calendar months so the month view never renders a half-covered month; in the home window projection still starts at today (no projected marks on days already past). - A navigated window with no occurrences stays rendered (returning null would tear down the calendar and its nav mid-navigation); only the home window keeps the hide-when-empty behavior. * fix(heatmap): unreachable history, display-only clicks, and projection titles - A repeat cfg whose only tracked history is in older years opened on an empty current year with heatmapData() null - no heatmap and no year nav, so the history was unreachable. selectedYear now defaults to the newest year that renders something; the current year is offered only when the projection overlay can mark it; and an empty navigated year stays rendered (empty grid + nav) instead of tearing down the way back. Pinned by a new component spec. - Click handlers and pointer/hover affordances are now gated on the interactive input like the ARIA role/tabindex already were - in display-only heatmaps mouse clicks no longer emit dayClick while keyboard users can't activate the same cells. - In a projection calendar, empty days no longer report fake activity ('0 tasks, 0m'): interactive ones describe the simulation click (new G.HEATMAP_SIMULATE_DAY string), display-only ones just carry the date. * fix(task-repeat-cfg): reject sub-daily FREQs at the engine validity gate save() already blocks FREQ=HOURLY/MINUTELY/SECONDLY, but a synced, imported or REST-ingested rule never passes through the dialog - it routed into the day-granular engine and silently collapsed to ~daily firing at local noon. isRRuleValid now rejects non-day-granular FREQs, dropping such rules to the legacy repeatCycle fallback like any other invalid rule. The persist-boundary ingestion guard (strip/reject at the write path) still lands with Phase 7; roadmap + watchlist updated to say so instead of overclaiming. * docs: document the repeat-edit activity setting in the settings wiki isExpandActivityForRepeatEdit shipped in the Tasks settings form without the wiki entry the documentation guide requires. * feat(task-repeat-cfg): fullscreen toggle for the recurring-task dialog A fullscreen/fullscreen_exit icon button sits in the dialog title bar. Opening the calendar preview auto-expands the dialog (the year strip needs the room on short screens, where it used to push the action buttons out of view); closing the preview shrinks it back ONLY when the preview caused the expansion - a manual toggle takes ownership either way. Sizing rides on a new reusable .dialog-fullscreen overlay panel class in the shared Material overwrite layer. * fix(task-repeat-cfg): need-based fullscreen and no redundant upcoming line - The calendar preview only auto-expands to fullscreen when it actually doesn't fit: a post-render measurement checks whether the calendar is clipped vertically or the year strip is forced to scroll sideways (and fullscreen would buy it width). On a screen where everything fits, the dialog stays its normal size. - The textual 'Upcoming: ...' line hides while the calendar is open - it shows the same occurrences the calendar already shows spatially. * fix(task-repeat-cfg): render the fullscreen toggle as a Material icon button MatIconButton was missing from the dialog's imports, so the mat-icon-button attribute did nothing and the toggle rendered as an unstyled native button. * feat(task-repeat-cfg): show the assembled rrule beside the raw override The raw rrule string moves out of the sticky result band (which keeps the humanized reading, upcoming dates and calendar toggle) into the builder's Advanced section, right beneath the raw-override input - so reading the assembled string and overriding it happen in one place - with a copy-to-clipboard button. E2E selectors updated to the new location (the readout sits inside the Advanced collapsible now). * fix(heatmap): instant day-cell tooltips via matTooltip The cells used the native title attribute, whose browser-fixed ~1-2s hover delay made the heatmap feel unresponsive. MatTooltip's default 0ms show delay matches the instant feel of GitHub's contribution heatmap (measured ~55ms hover-to-visible), positioned above the cell and styled with the app theme. * feat(task-repeat-cfg): hide off-schedule days in the repeat-task heatmap Every day of the year rendered a grey level-0 cell, so a weekly task showed six 'missed-looking' cells per week - reading like a broken streak. With the engine flag on and a valid rrule, the schedule is now computed for the whole selected year and days the task is NOT scheduled on (and that carry no tracked time) are dropped from the day map; the grid renders them as transparent placeholders in the same position. Visible cells now read as the actual streak: filled = tracked, grey = genuinely missed occurrence, dashed = upcoming. Off-schedule days WITH tracked time stay visible, and legacy/flag-off cfgs keep the previous full-grid view (the schedule isn't reliably knowable there). * feat(task-repeat-cfg): say what the rule actually does while the engine is off With the per-device advanced-recurrence flag off, a saved rule only drives scheduling through its simplified legacy mirror - yet the builder described full engine behavior, including things that silently don't happen (end conditions foremost: UNTIL/COUNT have no legacy equivalent, so the series repeats forever). The builder now reads the flag and tells the truth: - a notice at the top explains that scheduling runs from a simplified version of the saved rule until the engine is enabled - the Ends row warns that end conditions only take effect with the engine on (shown only when one is selected) - the Advanced section notes its selections are saved but not scheduled All three disappear the moment the flag is on. * fix(task-repeat-cfg): never strand the empty home window; open future windows at their start - A valid rule with no occurrence in the next 365 days (multi-year intervals, a far-future start) nulled out the home preview entirely, taking the year nav with it - the same stranding already fixed for navigated windows. The empty window now renders with its nav alive plus a 'No occurrences in this window' hint instead of a bare grid. - The month calendar's out-of-range fallback is direction-aware: a window entirely in the future (year-jumped projection) opens at its START month; past windows (history years) keep opening at their END. * feat(task-repeat-cfg): write never-fires legacy fallback for unrepresentable rules Decided contract for the legacy mirror fields (the wire format for old clients and the schedule for flag-off devices): either they fire on the same days as the rrule, or they are the LEGACY_NEVER_FIRES_FALLBACK sentinel (repeatCycle WEEKLY with every weekday flag false, which is deterministically dead on every released legacy engine and fully wire-stable). Rules outside legacy expressiveness - COUNT/UNTIL, seasonal BYMONTH, BYWEEKNO/BYYEARDAY, multi-day lists, out-of-union ordinals, yearly weekday modes - previously persisted a nearest-pattern approximation, so old and flag-off clients created tasks on wrong days that synced back to every device; absent tasks beat fabricated ones. Also in this change: - interval-1 FREQ=DAILY;BYDAY=... now maps losslessly onto WEEKLY flags instead of approximating as plain DAILY - the dialog warns at authoring time (isRRuleLegacyRepresentable -> RRULE_LEGACY_INCOMPAT) and the engine-off builder notices now say "creates no tasks" instead of "simplified rhythm" for these rules - the live rrule preview is gated on the memoised isRRuleValid like the calendar preview already was; previously a never-firing raw override (e.g. FREQ=DAILY;BYWEEKNO=53;BYMONTH=2) froze the main thread for multiple seconds per keystroke via the ungated rule.after() walk - the engine-flag settings help now tells multi-device accounts to enable the flag everywhere or nowhere: engines diverging on a day produce different rpt_<cfgId>_<dueDay> ids, i.e. duplicate tasks on every device * build(deps): pin rrule to exact 2.8.1 Occurrence streams are downstream of a third-party parser with known quirks; a caret upgrade that changes parsing on some devices mid-account re-creates duplicate-task mechanics between identical app versions (different rpt_<cfgId>_<dueDay> ids for the same cfg). Bump only deliberately - the engine invariants/differential specs double as the upgrade tripwire. * docs(task-repeat-cfg): write down the dual-engine endgame, risk model and flip gates Roadmap additions (phase scope unchanged): - the governing risk model: engine divergence on a multi-device account means duplicate tasks (per-device recomputation deduped only by rpt_<cfgId>_<dueDay>) plus lastTaskCreationDay flip-flop - not merely shifted dates; the mixed window opens when ONE device opts in - the legacy-fallback contract (faithful-or-sentinel, never silent approximation) as a documented policy - the dual-engine endgame: lazy-migrate on edit -> lossless-only (interval=1) backfill at flag default-on -> soak -> remove flag -> delete legacy engine one release later; legacy dual-write stays forever as the wire format; post-legacy invalid-rrule fallback is pause + repair prompt (isPaused exists), decided now - flip gates before default-on: mixed-version convergence simulation, differential fuzz across all three calculators (divergences pinned as expected output, not excluded), typia wire round-trip per producer - recorded why a bounded between/until probe CANNOT replace the _canNeverFire heuristics: rrule.js checks until only against emitted occurrences; measured 10.1 s walk with UNTIL set Watchlist: sentinel contract entries, monthlyWeekOfMonth out-of-range resolved via sentinel, MONTHLY_ANCHOR_RESET scope sharpened (applies to the whole flag-off installed base, not just PRE-rrule versions), rrule exact-pin note. Model comment: the stale-anchor analysis claimed a stale anchor is inert once rrule is set - true only for flag-ON clients; while the flag defaults off every remote client routes the legacy engine, so the gap covers the whole installed base until default-on. Comment now says so and notes the sentinel path is unaffected (cycle switch to WEEKLY is wire-stable). * feat(task-repeat-cfg): align engine-off notices with the never-fires sentinel The old texts described the pre-sentinel contract (simplified rhythm; series repeats without end). With the sentinel, a rule beyond legacy expressiveness creates no tasks while the engine is off - say exactly that. * feat(task-repeat-cfg): wip heatmap calendar + extract heatmap constants * feat(task-repeat-cfg): TickTick-style frequency picker in repeat dialog Restructures the repeat-config dialog's frequency selection from a dropdown to a TickTick-style chip group (repeat-freq-picker), while keeping every existing recurring feature: all quick-setting presets, the full RRULE custom builder (intervals/multiples, multi-weekday, month-day grid + custom CSV, nth-weekday rows, BYSETPOS, BYMONTH, ends, advanced), tags, estimate, time/remind, notes, subtasks, wait-for-completion, skip-overdue, preview + heatmap. quickSetting is no longer a formly field — the chip picker drives it via onQuickSettingSelect (mirroring the old select change handler, incl. the #5806 reference-date behaviour) with options from quickSettingOptions(). The RRULE engine, presets and sync-safe persistence are unchanged. Scoped entirely to task-repeat-cfg/. Specs updated; build + dialog/const/tz specs green. * feat(task-repeat-cfg): restyle repeat dialog to TickTick recurrence flow Reworks the repeat-config dialog's look + flow to match TickTick's recurrence editor, on the existing RRULE engine, keeping every SP feature: - round weekday chips, quiet section labels, segmented pills (schedule type), cleaner inputs and a calendar-style day-of-month grid; - the live recurrence summary moves in-sheet under the builder as an accent-edged card (was a pinned bottom band); - seasonal BYMONTH moves into the builder's Advanced collapsible so the main flow stays clean (TickTick has no seasonal months up front); - presets (chip row), full custom builder (interval/multiples, multi-weekday, month-day grid + CSV, nth-weekday rows, BYSETPOS, ends, from-completion), WKST/BYWEEKNO/BYYEARDAY/raw override, and all task-default extras unchanged. Pure presentation + template moves; engine/persistence untouched. Build green; dialog/const/tz specs green (59/10/2). * feat(task-repeat-cfg): polish repeat dialog toward TickTick (curated presets, cleaner flow) - Repeat chips now show 6 curated common presets + a 'More options' toggle that reveals the full set; 'Custom' is rendered last as an accent-outlined chip. No preset removed — just progressive disclosure. - Rename labels for clarity: section 'Recurring Config' -> 'Repeat'; collapsibles 'Advanced options' -> 'Advanced rule' (rule internals) and 'Advanced configuration' -> 'Defaults for new tasks' (task template fields); dialog title 'Add/Edit Recurring Task Config' -> 'Set up repeat' / 'Edit repeat'. - Move the task Title field out of the top into 'Defaults for new tasks' so the Repeat picker is the first thing (TickTick-clean). - Shorten the wordy schedule-type description; add a divider under the picker. Presentation/labels + a field move only; RRULE engine, presets and sync-safe persistence unchanged. Build green; dialog/const/tz specs green. * feat(task-repeat-cfg): rebuild custom recurrence as a TickTick-style settings sheet Reworks the RRULE builder's look + flow to match TickTick's custom recurrence: - 'Repeat every' is a label-left row with a − N + stepper; - frequency (Day/Week/Month/Year), Ends (Never/On date/After) and Schedule type are iOS/TickTick segmented controls (pill-on-track), replacing the raw native selects in the main flow; - weekday selectors are centered circular chips; - monthly/yearly pattern stay as selects but themed with a custom chevron; - shortened the Ends option labels for the segmented control. All controls bind to the existing builder methods — RRULE assembly, presets and sync-safe persistence are unchanged. Build green; builder/dialog/const/tz specs green (27/59/10/2). * feat(task-repeat-cfg): polish recurring dialog — dropdown picker, calendar day grid, centered layout - Repeat selector is now a custom CDK-overlay dropdown; "More options" expands the long tail in the open panel instead of closing it. Order: every day, weekly, monthly, yearly, every weekday, Custom, More. - "Custom recurring config" option renamed to "Custom". - Interval row reads as one inline phrase "Every [n] [unit]"; unit is a dropdown that pluralizes with the interval (Day/Days …). - Monthly/Yearly mode labels shortened to "On" with concise options (Day of month, Nth weekday, Selected weekdays, Specific date, …). - Day-of-month picker laid out as a 7-column calendar grid with borderless circular cells (deadline-modal look), fits a 375px phone. - Builder content centered into one consistent-width column; segmented bars stretch uniformly. - Evaluated-rule summary pinned above the action buttons, outside the scroll area. * fix(task-repeat-cfg): bound the rrule validity probe to a decade The validity probe used an unbounded `.after()`: an exotic never-firing rule that slipped past the `_canNeverFire` O(1) pre-screen (e.g. a BYSETPOS past its occurrence set) made rrule.js walk day-by-day to its year-275760 ceiling — a multi-second main-thread freeze that the live preview hit on every keystroke. Replace it with a bounded `between(anchor, anchor + 10y)` that early-exits on the first occurrence: the window caps the work for any never-fire rule the heuristic doesn't model, and the early-exit keeps a firing rule at one occurrence. `_canNeverFire` stays as the O(1) pre-screen for the realistic contradictory rules (notably the FREQ=DAILY+BYWEEKNO class, whose per-day expansion is too slow to probe even over a short window). "Fires within a decade" is a deliberate product rule. Addresses the dual-engine review. * feat(task-repeat-cfg): widen the recurring dialog dynamically The dialog sized to its content and was pinned to a fixed narrow column, so it never used the available width. Add a `dialog-recurring` panel class that sizes the surface to `min(95vw, 600px)` and lifts Material's 80vw default — the modal now widens with the viewport and fills the width on a phone. The form column keeps a 600px cap so controls stay readable, and the pinned result band (calendar preview) drops the cap in fullscreen so the expand actually buys it room — previously the 420px content cap defeated it. * feat(task-repeat-cfg): hide the day-list free-text field behind a "custom…" toggle The free-text day list ("1,15,-5") sat permanently under the day-of-month grid (monthly) and the specific-date grid (yearly), duplicating what the grid already shows. Tuck it behind a "custom…" button; the grid + last-day chips cover the common cases. The field binds `monthDays`, so it opens pre-filled with the days already selected. An existing rule whose days the grid can't represent (e.g. a typed "1,15,-5") auto-reveals the field so it stays editable. * fix(heatmap): single shared tooltip to stop overlay stacking in the preview Per-cell matTooltip on the dense calendar grid trailed a stack of overlays on a fast cursor sweep (the show-delay only reduced it). Replace it with one shared readout per heatmap that follows the hovered/focused cell — there is never more than one tooltip element, in the year strip, the month calendar, and the recurrence preview alike. Positioned relative to the non-scrolling root so a horizontal scroll can't strand a stale tip; pointer-events:none so it can't re-trigger a hover; shown on focus too for keyboard parity. Drops the now-unused matTooltip + HEATMAP_TOOLTIP_SHOW_DELAY. * feat(task-repeat-cfg): richer recurrence preview — next spotlight, rhythm stats, reveal, calendar realism Four additive enhancements to the dialog's calendar preview: - Next-occurrence spotlight: the next upcoming cell pulses, and a "Next: <date> · in N days" chip sits under the summary. - Rhythm stats ribbon above the calendar: count in view · ~every N days · ends after N / until <date> / runs forever (from the rule's COUNT/UNTIL). - Staggered reveal: occurrence cells fade/scale in on open and on view switch (fill-mode backwards, so a keystroke that reuses cells never re-animates). - Calendar realism: today ring, weekend tint, and a richer tooltip ("occurrence #N · in N days"). All gated so the shared Activity heatmap + canvas export are untouched: new DayData flags (isNext/isToday/revealIndex) are only set by the dialog, and the new heatmap/switcher inputs (animateReveal/showWeekends) default off. Spotlight is derived from the projection's own occurrence set, not rrulePreview, so an unrelated field keystroke still doesn't rebuild the projection. Honors prefers-reduced-motion. * fix(task-repeat-cfg): bound never-firing rrule probes against MAXYEAR walk rrule.js cannot stop a never-firing walk early: when no day passes the BY-filters its iterator callback never fires, so it spins period-by-period to MAXYEAR (9999). A 2020-anchored validity probe walked ~8000 years of periods on contradictory rules (e.g. FREQ=DAILY;BYYEARDAY=60;BYWEEKNO=53) — a 7-11s main-thread freeze reachable from a dialog keystroke or the first scan of a synced/imported rule. - isRRuleValid: anchor the probe at 9620 (= 2020 + 19*400). The Gregorian calendar repeats exactly every 400 years, so [9620,9630] is calendar- identical to [2020,2030] (verdicts unchanged across a 700-rule battery) while bounding the never-fire walk to ~380y of periods. - _firesFromStart: the occurrence queries anchor at the cfg's real start, where an INTERVAL>1 phase can miss a positional constraint (fires from the canonical anchor yet never from the real start). Gate _buildRuleSet with a bounded phase- and calendar-preserving probe shifted near 9999, so such a rule resolves to "no occurrence" instead of walking to 9999. Pure/deterministic (fixed anchors); identical results for firing rules, never-fire resolves bounded. Adds timing + phase regression specs. * refactor(task-repeat-cfg): P2 review fixes + drop heatmap reveal animation Curator P2 review follow-ups: - dialog: restore quickSettingOptions memoization (value-equal {refDateStr, locale} key; build read untracked) so it stops rebuilding ~16 translate.instant + 4 toLocaleDateString per keystroke. - dialog: re-derive the legacy never-fires sentinel on every schedule- touched save (no longer gated on a present startDate), so a non- representable rule cannot persist stale legacy fields. - legacy-cfg-to-rrule: assertNever exhaustiveness guard on switch(cycle) (new shared util src/app/util/assert-never.ts). - repeat-task-heatmap: extract one _usesRRuleEngine predicate shared by the two dual-engine routing sites. - rrule-builder: replace the two hand-rolled .rb-seg segmented controls with the shared segmented-button-group. - repeat-freq-picker: document why the raw cdkConnectedOverlay deviates from mat-select/mat-menu (in-panel expand + trigger-width match). Remove the heatmap preview reveal animation (staggered cell entrance): drop animateReveal through heatmap/month-calendar/switcher, the is-animate-reveal CSS + keyframes, and the --reveal-index plumbing. Keep the occurrence-order data for the "occurrence #N" tooltip, renamed revealIndex -> occurrenceIndex (uncapped now the animation-delay cap is gone). * fix(task-repeat-cfg): restore listbox keyboard a11y to repeat-freq-picker The custom dropdown replaced a native <select> but dropped its keyboard support: the panel declared role="listbox" while the option children were plain buttons (no role="option", no aria-selected, no roving tabindex/arrow keys, and focus never entered the panel on open — only Escape was handled). - options now carry role="option" + [attr.aria-selected]; the listbox holds only option children (the More toggle is a sibling). - roving tabindex + Arrow/Home/End navigation across the panel rows, with a visible :focus-visible ring. - focus moves into the panel on open (and stays on the More toggle when it expands the long tail in place), mirroring the native select. Also dedupe the 'RRULE' quick-setting magic string: a shared RRULE_QUICK_SETTING const (model) used by the picker and the options builder so they can't drift. * refactor(task-repeat-cfg): P2 review polish — tokens, parse memo, orphan key, probe spec - rrule-builder SCSS: restore var(--card-border-radius) (was literal 9px) and swap the native-select chevron's baked-in #888 fill for a per-theme variant (.isDarkTheme), so it tracks light/dark like the native datepicker indicator. - safeParseRRuleOptions: add a small string-keyed parse cache (the dialog's preview/validity computeds re-parse the same rule ~4x per keystroke, and the engine re-parses on every scan). Result is treated read-only by all callers. - remove the orphaned RRULE_YEARLY_MODE_DESCRIPTION i18n key (lost its last template reference in the restyle) from t.const.ts + en.json. - pin the isRRuleValid "fires within a decade" probe boundary with a spec: FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=29;BYDAY=MO first fires >10y past the anchor -> invalid (deliberate product rule); plain Feb-29 fires in-window -> valid. Guards against a future window/anchor edit silently flipping a real rule. * fix(task-repeat-cfg): close _canNeverFire gaps + correct stale probe comments P2 review follow-up. The reviewed range predates the 9620-anchor probe fix, which already bounds every never-firing class to sub-second (DAILY;BYSETPOS=2 6764ms->341ms, BYWEEKNO=10;BYYEARDAY=300 11257ms->527ms). This makes the named classes O(1) and fixes comments that still described the pre-anchor behavior. - _canNeverFire: flag BYSETPOS beyond the per-period set for DAILY (1 slot) and WEEKLY (BYDAY count) — e.g. FREQ=DAILY;BYSETPOS=2; and BYWEEKNO x BYYEARDAY contradictions independent of BYMONTH (the month-intersection checks were skipped when bymonth was empty) — e.g. BYWEEKNO=10 x BYYEARDAY=300. Both reuse the conservative month-superset helpers / provable set-size bounds, so no sound rule is mis-flagged (verified: in-range BYSETPOS and satisfiable weekno x yearday stay valid). - correct the comments in _canNeverFire + the complex spec: rrule.js MAXYEAR is 9999 (not 275760); the validity probe does NOT bound never-firing rules via its window — it is bounded by anchoring near 9999 (already shipped); and _canNeverFire is an O(1) optimization, not the termination guarantee. - add a repeat-freq-picker unit spec (preset ordering, More toggle, Custom, and the listbox roving-focus keyboard navigation). * style(task-repeat-cfg): compact the Ends / Schedule-type segmented controls The shared segmented-button-group's default (lg) treatment — a 3px border plus a scale-up + elevation on the active segment — was too heavy in the recurring dialog and made the selection appear to "bounce" as it moved. - segmented-button-group: flesh out the md (compact) size — 1px border, no zoom/lift, a subtle tinted active fill, and nowrap labels so a two-word label ("On date") stays on one line. Only the rrule-builder uses md, so the lg consumers (focus-mode) are unaffected. - rrule-builder: stack .rb-ends as a column so the Ends control keeps a stable full width above its conditional COUNT/UNTIL input, instead of resizing (and re-wrapping its labels) as that input appeared/disappeared. * feat(task-repeat-cfg): unified preview calendar for the recurring dialog Merge the start-date picker, occurrence projection and activity history into one calendar in the Recur dialog (RRULE epic P2): - Click a day to set the start; drag to pan the 365-day strip; double-click to simulate a completion. Start is also editable via M/D/Y fields kept in sync with the calendar. - Green tracked-time overlay merged into the projection (toggleable, with a total / this-week / this-month summary), surfaced via a look-back window so past activity shows alongside upcoming occurrences. - Today drawn as a robust white inset ring (survives the start fill); legend gains start / today / next-occurrence / projected and a green tracked-time colour range. - Layout: Repeat -> Every/On -> calendar -> Ends / Schedule type; the calendar is projected into the rrule-builder between the On and Ends sections, and those two rows are single-line. - Rename user-facing "weekday(s)" -> "days of the week" (day-of-week sense) and "working day(s)" (Mon-Fri sense). - Drop the dead Calendar-preview toggle / auto-fullscreen and align the specs. * feat(task-repeat-cfg): calendar as direct-manipulation rrule editor Turn the recurrence calendar into a direct editor: clicking a day, a weekday header or the month label opens a contextual menu that edits the same rule the builder edits (new pure rrule-calendar-ops helpers). - Weekday menu lists every weekday-targeting variant (weekly / monthly / yearly, plain + nth); identical labels are differentiated by a grey "switches to ..." hint. - Ends + Schedule type are nested at the top of a renamed "Advanced" section (Custom-only), with the frequency-specific "On ..." day selection moved in beneath them - the calendar is now the primary day picker. - Month menu gains "Remove all month limits (...)" listing the limited months. - Changing frequency or monthly/yearly mode resets the day selection to none, so a stale selection from the previous mode can't silently narrow or dead-end the new rule. - Setting the start date jumps the month view / scrolls the year strip to it, and re-anchors a running "after I complete it" schedule (drops the stale lastTaskCreationDay) so the new start actually takes effect. - Heatmap gains interactive menus, per-weekday annotations, an end-day marker, tracked-time in tooltips, and limited-month chips, all kept optional so the metrics Activity heatmap is unchanged. * fix(task-repeat-cfg): correct BYSETPOS never-fire false positives The _canNeverFire BYSETPOS pre-screen wrongly flagged sound rules, which silently drops a cfg to the legacy repeatCycle fallback and reschedules it - so real recurring rules were being broken. Two false positives (each FIRES in rrule.js 2.8.1): - Negative BYSETPOS. `Math.abs(p) > slots` flagged e.g. DAILY;BYSETPOS=-2 and WEEKLY;BYDAY=MO;BYSETPOS=-2, but rrule.js clamps an out-of-range negative and still fires. Flag only positive overshoot (`p > slots`); negatives now short-circuit, matching how the other combo checks already skip on any negative value. - WEEKLY without BYDAY. _maxSetPosSlots returned the BYDAY count (1 when absent), but rrule.js treats BYMONTHDAY / BYYEARDAY / BYWEEKNO as EXPANDERS within the week, so the per-week set exceeds it - e.g. WEEKLY;BYMONTHDAY=1,2,3;BYSETPOS=2 fires. Bound WEEKLY only when the sole positional part is BYDAY; otherwise return null (probe handles it). Adds the regressing vectors to the "no false positives" suite and drops two fragile wall-clock timing asserts (the memoised validity cache makes the timed block a cache hit; the surviving sibling keeps a generous <1000ms regression guard). De-dups the negative-value guards into hasNegYearDay / hasNegWeekNo to mirror the existing hasNegDay. * fix(heatmap): distinguish the recurrence end-day from the next-occurrence Both markers were 2px rings differing only by --c-warn vs --c-accent, which are near-identical reddish hues in the default theme - and the static legend can't show the next-occurrence pulse, so the two swatches read as the same. Make the end (UNTIL) day a SOLID warn block instead of a ring: solid-vs-hollow is unmistakable regardless of theme colours. Applied to both the year strip and the month calendar, cells and legend. * feat(task-repeat-cfg): group frequency-switch menu options under icon buttons The calendar context menus listed every weekday/day option flat, with a grey "(switches to …)" hint on the ones that change the frequency. Bury those switch options instead: only the CURRENT frequency's options stay at the top of the menu; each other frequency gets one icon button in a bottom row that opens a deeper sub-menu of its options. - Weekday-header menu: native Selected-days (+ Nth for monthly/yearly) at top; a bottom row of week / month / year icons, each opening that frequency's {Selected days of the week, Nth day of the week ▸ ordinal}. - Day menu: native "On this day of the month/year" for the current freq at top; the other under a single month/year switch icon. - Switch triggers are mat-menu-items (restyled into a compact icon row) so the nested sub-menu opens and the parent stays open - a plain icon-button trigger inside a menu closes it instead. Icons: calendar_view_week / calendar_view_month / calendar_month; the old CAL_MENU_SWITCHES_* strings are repurposed as the icons' tooltips. Drops the inline switch hints and their computeds. * fix(task-repeat-cfg): match calendar-menu switch icons to app style + tooltips The frequency-switch icon buttons in the calendar menus used a native `title` attribute and a cramped square cell. Use Material `matTooltip` (responsive / touch-aware, like the rest of the app) and render them as round 40px icon buttons with the themed hover state-layer and ripple (mat-menu-item provides those once the cell is made round). * fix(task-repeat-cfg): stop the weekday menu focusing a switch icon on open In Yearly mode, opening the weekday menu painted a stray grey state-layer circle on the "switch to weekly" icon. MatMenu focuses its first registered item on open, and items projected via *ngTemplateOutlet register AFTER the static switch-icon buttons — so the focus landed on an icon. Inline the native weekday options (static, declared before the switch row) so the first item is the expected text option; the switch sub-menus keep using the shared templates. * feat(task-repeat-cfg): toggle-aware calendar menu labels (set vs remove) When the clicked day / weekday already carries a setting, the menu item now reads "Remove …" and the action toggles it off — mirroring the month menu's Limit/Unlimit. Covers every settable calendar action: - Day: Ends on this date <-> Remove end date (the only one whose handler needed a branch; the rest already add-or-remove); On this day of the month/year <-> Remove …; Simulate completing here <-> Stop simulating. - Weekday: Selected days of the week <-> Remove from selected days; each nth ordinal <-> Remove <ordinal>. "Set as start date" stays (a rule's anchor can't be unset), and the frequency-switch sub-menu variants always add (the target weekday/day can't already be set in another frequency). Active-state derived from the parsed model via computeds; new CAL_MENU_REMOVE_* strings. * style(heatmap): fold v18.10.0 palette + dark contrast into calendar & 365 Match upstream v18.10.0's "improved heatmap / recurrent task calendar design" while keeping all of our preview features (start / next / end / today / projected / activity / simulate markers, glyphs, menus). - Hoist the cell palette into themeable vars on the container (--heatmap-container-bg / -cell-border / -hover-outline / -level-0..4-bg) and drive cells + legend from them; add an inset cell-border hook (transparent by default). - Dark theme: re-tune via the vars only — level-0 becomes an ink overlay (rgba(var(--ink-on-channel), 0.16)) and levels mix primary at 38/55/72% so the activity cells stay distinct on dark backgrounds. - Apply the same palette to the month "Calendar" view (was 25/45/65%) so it and the 365 strip share one look. Light theme is unchanged (identical level values); the visible win is the dark-mode contrast and one shared, themeable palette. * feat(task-repeat-cfg): add leading icons to calendar context-menu items Every named item in the day / weekday / month context menus (and their sub-menus) now leads with a Material icon for faster scanning: - Day: flag (start), event_busy (end), calendar_view_month / calendar_month (this day of the month / year), science (simulate). - Weekday: calendar_view_week (selected days), format_list_numbered (nth); the nth ordinals get looks_one…looks_4 / last_page. - Month: filter_alt (limit) / filter_alt_off (clear all limits). Day-of-X and selected-days reuse their frequency's switch-icon glyph so the item and its switch button read as the same thing. The icon-only frequency-switch buttons are unchanged. * fix(heatmap): make calendar text + markers track the active theme The recurring calendar didn't fully honour the picked theme (e.g. Cybr: day numbers rendered white instead of the theme's red), and several markers shared a colour. - Day numbers now use the theme's --text-color explicitly. Inside a mat-dialog the cells were inheriting Material's on-surface colour (white), ignoring themes that recolour their text; the busiest day also hard-coded white. Removed both. - Distinct, theme-derived markers (no two share a hue by default): start = primary, next = accent (pulsing ring), end = error (was --c-warn; red "stop", falls back to --c-warn), completed = success (green "done", was accent — which made it identical to next), activity = success ramp, today = a neutral --ink ring (was hard-coded #fff). Legend swatches realigned to match (completed swatch had even been the wrong colour). Swept all 14 themes in their light/dark modes: numbers now theme-coloured everywhere; markers distinct except Everforest, whose own --c-accent and --c-success are the same olive (next vs completed there stay distinct by shape/animation). start follows --c-primary, which a few themes (Cybr) choose not to recolour. * fix(task-repeat-cfg): click-toggle switch options, distinct start, pulsing next-legend - Bottom "switch to …" icons no longer hover-open a sub-menu. They're now plain icon-button toggles: click expands that frequency's options INLINE in the same menu, click again collapses; the active icon is highlighted. This also kills the stray focus-circle that appeared on the first switch icon when a weekday was clicked in Daily mode (a non-menu-item button isn't auto-focused), and the parent menu stays open throughout. - start day now carries a contrasting ink ring (not a same-colour halo) so it stays distinct from the green tracked-time cells even when the theme's primary is close to that green. - The "next occurrence" legend swatch now pulses like its cell marker. Verified end-to-end in a clean profile: Daily-mode weekday menu opens with no circle, hover does nothing, click expands inline (parent stays), click again collapses. * feat(heatmap): hover tooltips spelling out weekday + month statuses Hovering a weekday header (Mon–Sun) that has something set now shows a tooltip listing it — e.g. "Nth day of the week: 2nd, last · Selected days of the week · Days of the week in months" — instead of just the terse glyphs. Hovering a month label/title that's BYMONTH-limited shows the limited-month list ("Limited to Jun, Jul"). The dialog derives both from the live rule (weekdayAnnotations) and passes them through the switcher to the month + year views as optional inputs, so the metrics Activity heatmap (which passes neither) is unaffected. * fix(heatmap): amber simulate-completion + visible today/next legend swatches - Simulated "completed here" day is now warm amber (var(--c-warning, #e8a13a)) instead of green — it was --c-success, the same family as the green tracked-time cells, so the two read too alike. - The today and next-occurrence legend swatches were drawn with an inset box-shadow ring on a tiny transparent box, which rendered as nothing in some themes. Switched both to an `outline` ring (like the dotted projected swatch, which always showed), so they're reliably visible; the next swatch keeps its pulse via an outline-colour keyframe. Verified in a clean profile: today shows an ink ring, next shows a pulsing accent ring. * feat(task-repeat-cfg): concise recurrence preset labels Rename the common quick-setting presets to a tighter "Freq (detail)" form: - Every day → Daily - Every week on Monday → Weekly (Mon) (short weekday) - Every month on day 15 → Monthly (15th) (ordinal day) - Every year on the 6/15 → Yearly (Jun 15) (short month + day) - Every Monday–Friday → Every weekday (Mon - Fri) Each option passes its own interpolation params, so the new compact forms (short weekday / ordinal day / "MMM d") are scoped to these labels and don't affect the verbose presets (biweekly, quarterly, nth-weekday, etc.) that still use the long/numeric values. en.json only; other locales keep their existing templates until translated. * feat(task-repeat-cfg): tighten the remaining recurrence preset labels Apply the concise "Freq (detail)" style to the rest of the presets: - Every other day → Every 2 days - Every other week on Monday → Every 2 weeks (Mon) - Every month on the first day → Monthly (1st) - Every month on the last day → Monthly (last day) - Every month on the 3rd Mon → Monthly (third Mon) - Every month on the last Mon → Monthly (last Mon) - Every 3 months on the day 15 → Every 3 months (15th) - Every 6 months on the day 15 → Every 6 months (15th) - Every 2 years on the 6/15 → Every 2 years (Jun 15) Weekday → short, day-of-month → ordinal, day/month → "MMM d", matching the common presets. The nth-weekday ordinal keeps its _NTH (dative) variant for non-English grammar. The verbose long-form date params are now unused and removed. en.json only. * test(task-repeat-cfg): update preset-label param expectations to concise forms The dialog spec asserted the old quick-setting params (numeric day, numeric day/month). They now carry the concise forms - ordinal day ("15th") and short month+day ("May 1") - so the expectations are updated to match (via a shared ordinalDay helper). Fixes the 3 pre-push test failures. * feat(task-repeat-cfg): show only the selected schedule-type description The schedule-type hint always showed both halves ("Fixed: … After I complete it: …"). Show only the one matching the current selection: - Fixed dates → "Keep a set calendar rhythm from the start date." - After I complete it → "Count the interval from each completion (e.g. every 3 days = 3 days after you finish)." Split into two keys driven by scheduleSelectedId(); the old combined key is left for other locales until translated. * feat(task-repeat-cfg): simulate-completion flips schedule type and respects the start date A simulated completion only re-anchors a "from completion" schedule, so "Simulate completing here" now turns that mode on when you pick a day — otherwise the what-if preview wouldn't actually shift. - Gate the simulate menu item: only offer it on/after the start date (a completion can't happen before the rule begins); the "Stop simulating" variant stays reachable for an active sim. - Moving the start date past an existing simulation drops that sim (it no longer makes sense before the new start). - Fix the self-defeating interaction with the sim-watcher effect: flipping the schedule type changes the schedule slice, which the watcher treats as an edit and clears the sim. Promote its tracked slice to a field and advance it in lockstep from menuSimulate so the freshly-set sim survives the flip it triggered. Unit tests flush the effect so they actually guard the survive-the-flip behavior; verified live across all four cases. * fix(task-repeat-cfg): keep the simulation when a new start date precedes it Moving the start to BEFORE a simulated completion left the completion valid (it still sits on/after the start), so the sim should stay — but the sim-watcher effect fired on the startDate slice change and cleared it. _applyStartDate now only drops the sim when the new start moves PAST it (a completion can't precede the rule's start); when the sim still sits on/after the new start it advances the watcher's tracked slice in lockstep so the kept sim survives the start change. Spec flushes the effect so it genuinely guards both the keep and the drop paths. * fix(task-repeat-cfg): make simulate a pure preview, drop the schedule-type flip menuSimulate flipped repeatFromCompletionDate -> true unconditionally to make the calendar re-anchor. That mutation of persisted state caused three problems: - A start-anchored COUNT rule got forced to from-completion, which save rejects with RRULE_COUNT_WITH_COMPLETION - blocking save on a setting the user never knowingly changed (and re-anchoring COUNT renders up to ~2x COUNT marks). - Toggling the sim off cleared simulatedCompletion but left the flag true, a one-way start-anchored -> from-completion conversion persisted on save. - Presets (always start-anchored, no from-completion control) showed a preview the saved cfg would not have. Simulation is now a pure preview that never touches the persisted schedule type. It is offered only on/after the start of a from-completion, non-COUNT schedule (menuDaySimAllowed), so presets and start-anchored rules no longer expose it and resultHeatmapData re-anchors off the existing flag. menuSimulate gains a defensive guard mirroring the template. Restores the documented contract that simulation only exists for repeat-from-completion schedules. |
||
|---|---|---|
| .air | ||
| .devcontainer | ||
| .github | ||
| .husky | ||
| .signpath/policies/super-productivity | ||
| .vscode | ||
| android | ||
| build | ||
| docs | ||
| e2e | ||
| electron | ||
| eslint-local-rules | ||
| fastlane | ||
| ios | ||
| nginx | ||
| packages | ||
| scripts | ||
| snap/hooks | ||
| src | ||
| tools | ||
| .browserslistrc | ||
| .dockerignore | ||
| .editorconfig | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| .gitmodules | ||
| .gitpod.yml | ||
| .npmrc | ||
| .nvmrc | ||
| .prettierignore | ||
| .prettierrc.json | ||
| .stylelintrc.mjs | ||
| AGENTS.md | ||
| angular.json | ||
| ARCHITECTURE-DECISIONS.md | ||
| capacitor.config.ts | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| docker-compose.e2e.fast.yaml | ||
| docker-compose.e2e.yaml | ||
| docker-compose.supersync.yaml | ||
| docker-compose.yaml | ||
| docker-entrypoint.sh | ||
| Dockerfile | ||
| Dockerfile.e2e.dev | ||
| Dockerfile.e2e.dev.fast | ||
| electron-builder.yaml | ||
| eslint.config.js | ||
| funding.json | ||
| Gemfile | ||
| Gemfile.lock | ||
| LICENSE | ||
| ngsw-config.json | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| SECURITY.md | ||
| tsconfig.base.json | ||
| tsconfig.json | ||
| webdav.yaml | ||
An advanced todo list app with timeboxing & time tracking capabilities that supports importing tasks from your calendar, Jira, GitHub and others
🌐 Open Web App or 💻 Download
💻 Downloads & Install
For all current downloads, package links, and platform-specific notes:
check the wiki
✔️ Features
- Keep organized and focused! Plan and categorize your tasks using sub-tasks, projects and tags and color code them as needed.
- Use timeboxing and track your time. Create time sheets and work summaries in a breeze to easily export them to your company's time tracking system.
- Helps you to establish healthy & productive habits:
- A break reminder reminds you when it's time to step away.
- The anti-procrastination feature helps you gain perspective when you really need to.
- Need some extra focus? A Pomodoro timer is also always at hand.
- Collect personal metrics to see, which of your work routines need adjustments.
- Integrate with Jira, Trello, GitHub, GitLab, Gitea, OpenProject, Linear, ClickUp and Azure DevOps. Auto import tasks assigned to you, plan the details locally, automatically create work logs, and get notified immediately, when something changes.
- Basic CalDAV integration.
- Back up and synchronize your data across multiple devices with Dropbox and WebDAV support
- Attach context information to tasks and projects. Create notes, attach files or create project-level bookmarks for links, files, and even commands.
- Super Productivity respects your privacy and does NOT collect any data and there are no user accounts or registration. You decide where you store your data!
- It's free and open source and always will be.
And much more!
Note
The web version has some limitations: See the Web App vs Desktop comparison for more details.
📖 Documentation and Guides
Getting Started
- Getting started guide (article)
- Video walkthrough (YouTube)
- Eat the frog prioritizing scheme
Starting Point in Wiki:
First steps •
Reference •
How-To
Productivity Tips:
Keyboard Shortcuts •
Short Syntax
Need Help?
Visit the discussions page
See the bottom of the README for more information on the documentation.
Advanced Topics
Here are some other topics covered in the official wiki:
Development:
Run dev server •
Package the app •
Build for Android •
Run with Docker
Data Management:
User Data •
Issue Providers •
Sync Providers
Customization:
Plugins •
Themes
APIs:
Sync Server •
Plugins •
REST
Community
The development of Super Productivity is driven by a wonderful community of users and contributors. Thank you all so much for your support!
👀 Check out our awesome curated list of community-created resources about Super Productivity
♥️ Contributing
If you want to get involved, please check out the CONTRIBUTING.md
There are several ways to help.
-
Spread the word: More users mean more people testing and contributing to the app which in turn means better stability and possibly more and better features. You can vote for Super Productivity on Slant, Product Hunt, Softpedia or on AlternativeTo, you can tweet about it, share it on LinkedIn, reddit or any of your favorite social media platforms. Every little bit helps!
-
Provide a Pull Request: Here is a list of the most popular community requests and here some info on how to run the development build (wiki). Please make sure that you're following the commit message format and to also include the issue number in your commit message, if you're fixing a particular issue (e.g.:
feat: add nice feature #31). -
Answer questions: You know the answer to another user's problem? Share your knowledge!
-
Provide your opinion: Some community suggestions are controversial. Your input might be helpful and if it is just an up- or down-vote.
-
Provide a more refined UI spec for existing feature requests
-
Make a feature or improvement request: Something can be done better? Something essential missing? Let us know!
-
Translations, Icons, etc.: You don't have to be a programmer to help; learn how to contribute translations!
-
Create custom plugins or custom themes
Special Thanks to our Sponsors!!!
Recently support for Super Productivity has been growing! A big thank you to all our sponsors!
(If you are, intend to or have been a sponsor and want to be shown here, please let me know!)
Code Signing
Windows binaries are signed. Free code signing is provided by SignPath.io, certificate by SignPath Foundation.
Documentation: Manual versus Automated
There are two wikis: the official one hosted in by GitHub autonomously generated variant using DeepWiki.com. The manually curated version is a more stable and approachable resource designed to help you understand the app from a more human-focused perspective whereas DeepWiki is optimized for explaining the code itself with little regard for context beyond that.
Official Wiki
It is preferable to maintain local documentation rather than rely on an external service. It also preferable that the documentation is updated in tandem with the code changes as demonstrated in this commit.
Changes to files within ./docs/wiki are linted in CI before being automatically
sync'd to the repository's official Wiki hosted by GitHub.
Migrating to Docusaurus is a long-term goal once the content and structure of the wiki has matured and the remaining "legacy docs" have either been reworked or removed. There are some automations in development to help reduce the difference between the published docs and the state of the code while retaining a human-in-the-loop.
DeepWiki.com
If you have very specific questions about how the code works or why a bug might be producing
a particular message it might be useful to
. It can help "cite your sources" when discussing functionality and code that you don't fully
understand as part of feature requests or bug reports.
This automated reference does come with some significant drawbacks:
- Intent: Describes what code does, not why decisions or tradeoffs were made.
- Staleness: Will *always* lag behind the code.
- Code-Focused: Does not provide guides or conceptual explanations.
- Cost: Potential future cost and higher resource usage than static docs.

