Commit Graph
12 Commits
Author SHA1 Message Date
makiolaj ee29c9bca1 test(store): assert the whole reminder, not a bare minute count
First on-device run of the instrumented suite against the branch tip: 66
tests, one failure, and it was the test that was wrong.

`alarmsRoundTripAndReplaceRatherThanAccumulate` asserted
`source.alarms()[id] == 30`. The seam stopped returning a bare `Int` in
fcee1d1, when collapsing an alarm to a minute count turned out to be what
fired an imported START-referenced reminder off DUE — `alarms()` has returned
`Map<Long, TaskReminder>` ever since. The production value was correct
(`TaskReminder(minutesBefore=30, fromStart=false)`); only the expectation was
left behind.

It compiled the whole time because Truth's `isEqualTo` takes `Any?`, so an
`Int` compared against a `TaskReminder?` is a perfectly legal call that can
only ever be false. Nothing short of running it would have found this, which
is the argument for the ROADMAP item that asked for the run.

Now asserts the whole value, so the reference is part of the contract rather
than something the test is free to ignore.

66/66 green after the fix.
2026-09-22 08:53:16 +02:00
makiolaj 3150781376 docs: say what actually shipped, and what 1.0.0 actually is
The branch's documents describe a world where the vendored provider reached
users. It never did, and several claims follow from that mistake.

`ROADMAP.md`:
- Phase 5's "**Breaking:** the authority and both custom permissions no longer
  exist — anyone who pointed DAVx5 at that authority loses it, and the release
  notes have to say so" is wrong in the way that *removes* work: they were
  added and deleted inside this unreleased cycle, so nobody could have pointed
  anything at them. The release notes must not warn about losing something that
  never shipped. The per-locale release-notes item went with it.
- "Run the instrumented suite on a device … none has ever executed" was stale:
  52 tests, 0 failures, Pixel 10 / API 36, 13 Aug. What is genuinely open is a
  re-run against the tip, since the 4 Sep commits reworked the store and added
  instrumented cases that have never run. Both now say so, with the ARM64 aapt
  exit-code trap noted where someone will hit it.
- The device-verification item described upgrading from a v0.3.2 APK with
  seeded data, which cannot be the real path. Replaced with the four cases that
  matter, including the one that only exists on a device that side-loaded a dev
  build of this branch.
- M6's Glance item claimed "deps present in build.gradle.kts" — not any more.
  Translations and the language picker shipped in 0.4.0 and are marked done.

`OWN-STORE.md` gets a correction banner over "Migrating existing users" saying
the premise is wrong, and a section for the copy that replaces it.
`STORAGE-AND-SYNC.md`'s banner said the vendored-provider decision was "made,
shipped, and then costed properly" — built, not shipped.

`PRIVACY.md` had the opposite problem: it describes CalDAV sync, Nextcloud
Login Flow v2, RFC 6764 discovery and a Keystore-held password, none of which
exist in 1.0.0 — the app holds no `INTERNET` permission at all. The permissions
section listed six it does not declare. Since it is a legal document users are
sent to from Settings → About, section 4 is now marked as describing a planned
feature, section 9 lists exactly what the manifest declares (and says what is
*not* there), and the backup and crash-report sections no longer assume network
access or sync bookkeeping. Kept forward-looking rather than cut, so it does
not have to change underneath anyone when sync lands. **Worth a read before
merging** — it is the one change here with legal weight.

`fastlane/.../full_description.txt` still opened with "It works directly on an
existing tasks provider (OpenTasks / tasks.org) … no own account, no own sync"
as the app's premise. That is the F-Droid listing for a release whose headline
is that it needs nothing installed. Rewritten, with the feature list and the
no-internet-permission point that is now literally true.

`README.md` and `ExportWriter`'s "ships in eleven locales" (it is three) follow.
2026-09-21 13:38:23 +02:00
makiolaj ec2e2eb59d feat(settings): storage picker and export screen
The store picker and the export screen were the two frontend surfaces
the own-store work left unbuilt, so both backends shipped unreachable.
Settings gains a Storage section holding them: a full-screen picker over
Own / an installed external provider (dimmed when none is present, named
after the provider's own app), and an export screen with a per-list tick
and the two SAF destinations, a folder or a single zip. The picker asks
for the provider's runtime permission before writing the mode, so a
denial leaves the readable store in place instead of dropping the user on
the gate; a refusal is reported with a route to app settings.

Making the mode switchable at runtime had two consequences:

- reminders are armed off whichever store was active when they were
  scheduled, so a switch rebuilds the set. ReminderScheduler.sync() is
  now serialised — it is a read-modify-write over ScheduledReminderStore,
  and overlapping runs each wrote their own set as the whole truth
- the permission gate is the only screen an External user can reach once
  their provider app stops answering, so it offers the way back to our
  own store

ExportWriter no longer deletes a previous export before recreating it (a
failure in between lost both), lists the target directory once instead of
per document, and carries a typed ExportFailure so the screen can report
in the user's language rather than an exception message.
2026-09-04 15:37:48 +02:00
makiolaj fcee1d1736 feat(lists): manage lists in the app, and fix four store defects
Owning the store left a fresh install with no lists and no way to make
one, so no way to save a task. The seam gains updateList/deleteList
beside createLocalList on both paths — the External one addresses the row
as its own account's sync adapter, the only caller the provider lets
write tasklists. ListEditorSheet is the family's full-screen sheet: name
field, a 12-colour palette, and a destructive row behind a confirm when
editing. Entry points are a "New list" row under the home Lists section,
an empty state with a create button, and the home FAB switching to "New
list" while there are none. Deleting takes the list's tasks with it and
is offered only for device-only lists.

Four defects a review of the branch turned up:

- completing one occurrence closed the whole series — setCompleted wrote
  the master, the row TaskDao.tasks filters on. setCompletedInstance
  forks a RECURRENCE-ID override the way updateInstance does; phase 2
  always specified this, only the edit half had it
- the expansion ceiling was spent on the past, so a sub-daily series
  stopped expanding months before today and never reached Today or
  Upcoming
- an imported START-referenced reminder fired off DUE, because the seam
  collapsed alarms to a bare minute count. TaskReminder carries the
  anchor now
- registerObserver bound a live flow to whichever store was active at
  subscription, so a Settings store switch left every screen listening to
  the store it had stopped reading
2026-09-04 13:56:52 +02:00
makiolaj 1d4fe5b301 fix(reminders): arm reminders again in our own store
Regression from deleting the provider. sync() gated on
providerResolver.resolve() != null, and OWN resolves to no provider by
design — so from that commit no due reminder was ever armed in what had
just become the default mode, and clearAll() cancelled any that survived
the upgrade.

The gate is now ProviderResolver.canReadStore(): OWN is always readable,
and only EXTERNAL can fail, for the two reasons it ever could. Putting
the decision on the resolver rather than inside the scheduler is what
makes it testable at all — ReminderScheduler needs Context and
AlarmManager, which is why nothing caught this.

Also brings ARCHITECTURE.md and ROADMAP.md in line with the branch: one
module, OWN/EXTERNAL, the four Room tables, expansion at read time, the
import and startup gate, and the manifest surface that no longer declares
a provider or any permission of its own.
2026-08-13 16:46:31 +02:00
makiolajandClaude Opus 5 13cb27b2ab docs: decide to build our own store and delete the vendored provider
The vendored dmfs provider was kept on the grounds that it hands us the
sync bookkeeping for free. The phase-1 sync audit measured that
bookkeeping and found most of it broken, absent, or unusable: _DIRTY not
set on delete, no home for a per-collection sync token, read-only
collections inexpressible, ACCOUNT_TYPE write-once so enabling sync is a
full migration, and cleanUpLists able to delete a user's lists after a
backup restore. Sixteen findings are provider-imposed rather than
platform- or protocol-imposed.

Costing the alternative showed the swap is far smaller than assumed.
TasksDataSource is already a 14-method, domain-shaped interface;
exactly one file above the data layer references TasksContract. The
work is a second implementation behind an interface built for it, not a
rewrite. Against ~5 weeks to build, owning the store removes 2.5-4
weeks from the sync plan, and 8,200 of the vendored 14,555 lines are
things we would never write - 23 migrations from a 2013 schema, 798
lines of full-text search the app has zero call sites for, and 1,581
lines of a type-safe layer over ContentValues that Room deletes.

External mode (OpenTasks, tasks.org) is unaffected and keeps every
file that describes somebody else's schema.

STORAGE-DECISION.md is the reasoning; OWN-STORE.md is the architecture
and the six-phase plan. :provider stays in-tree until phase 5 so
recurrence parity can be tested against it before it goes.

Also corrected here: the provider's JVM test count (51 -> 56, measured
from the test-results XML) and a fourth site of the debunked "switching
sync on is never a migration" claim, in StorageMode.kt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:46:07 +02:00
makiolajandClaude Opus 5 98ed339346 docs: bring the docs in line with what shipped
STORAGE-AND-SYNC.md asked for a follow-up pass on ARCHITECTURE.md §7 and the
ProviderResolver KDoc, which still defined Posture B as "bundle OpenTasks and
find org.dmfs.tasks first" — the plan that was withdrawn as a dead end. That pass,
plus the status the doc left open.

ARCHITECTURE.md now describes the app as built: two modules, the storage-mode
table with the permission each needs, the autoMode rule and why it keys on
holding an external provider's permission, the two-not-three mode vocabulary, and
a manifest section that says what :provider contributes and what is deliberately
absent (GET_ACCOUNTS, INTERNET). §7 records squatting the dmfs authority as a
dead end rather than a road not yet taken, so it doesn't get re-proposed.

ROADMAP.md turns "Posture B, later" into what actually landed and lists what
didn't: the frontend surfaces, the DAVx5 issue, the sync adapter, and device
verification. Two open decisions resolved and struck through — the authority
choice, and recurrence-aware editing, which fix/provider-interaction-review made
stale.

STORAGE-AND-SYNC.md gets per-step status. Open question 3 ("does it work with no
account?") is answered, with the caveat that the test proving it is Robolectric
and skips on ARM64 — answered by construction, not yet on a device.

PLAN.md gets a banner. It's the original design document and still holds the
reasoning behind the layering, but two of its premises are overturned and it
should not be read as current.

README.md was telling users they need a tasks provider installed. They don't, and
that's the headline feature: a table of where tasks can live, that our provider
coexists with OpenTasks rather than replacing it, and that everything exports as
standard .ics.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 21:32:17 +02:00
makiolajandClaude Opus 4.8 76a0139fda Rename app Floret → Agendula
CI / ci (push) Successful in 8m33s
Floret is promoted to the family / design-language (shared-kit) name; the
tasks app itself becomes Agendula (de.jeanlucmakiola.agendula) — agenda
('things to be done') + Calendula's -ula, a twin of the Calendula name.

Renames the package, namespace, applicationId, rootProject.name, app_name,
FloretApp/FloretNavHost/FloretTransitions classes, theme, F-Droid metadata
dir, CI artifact name, and docs. The botanical word 'florets' is preserved in
the name-origin prose, which is rewritten to Agendula's etymology. Clean
build + unit tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:10:24 +02:00
makiolajandClaude Opus 4.8 d84ac60757 M5: reminders onboarding, Settings screen & per-list overrides
Reminders onboarding & polish, plus the Settings screen (landed early).

- One-time reminder onboarding gate after the provider grant
  (ReminderOnboardingScreen + OnboardingScaffold), requesting
  POST_NOTIFICATIONS on API 33+; choice recorded in prefs.
- Settings screen, structured after Calendula as a category hub with
  sliding sub-screens (CollapsingScaffold + grouped rows + full-screen
  OptionPicker): About card, Appearance (theme, dynamic colour), Task
  form (default edit-form fields, default list, add-a-subtask-row
  opt-out), Reminders, Language, and Report a problem. Reached via an
  expressive shaped action button (MaterialShapes.Cookie4Sided) on the
  lists overview. Back from a sub-screen returns to the hub.
- Reminders: master enable toggle (gates the whole engine; re-requests
  the notification permission), a default lead time with a Custom
  amount/unit editor, an Android-12 exact-alarm status row, and
  per-list overrides of the default (Inherit / None / custom lead),
  honoured by ReminderScheduler per task.
- Per-app language (AppLanguage + locales_config.xml + appcompat
  locales service); add-a-subtask-row opt-out wired into the task list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:03:26 +02:00
makiolajandClaude Opus 4.8 e0c56a73e2 M4: percent-complete, conflict-safe saves & subtask reparent
CI / ci (push) Successful in 6m49s
M3 detail/edit polish:
- "Progress" slider on the edit form writes Tasks.PERCENT_COMPLETE (clamped
  0-100, 5% detents); status stays owned by the complete toggle.
- Conflict-safe saves: updateTask re-checks the provider's last_modified against
  the value captured when the form loaded and throws TaskConflictException; the
  editor offers overwrite-or-cancel instead of clobbering an external change.

M4 subtasks (UI):
- Reparent: a "Parent task" picker files a task under any top-level task in its
  list (or "None" to promote it); candidates stay top-level to keep nesting one
  level deep. Switching list clears the now-invalid parent.
- Tapping a subtask opens its own detail (a new TaskDetail entry, own VM).

Tests: percent clamping, parent-id write, populatedFields reveal logic.
Docs: ROADMAP M2/M3/M4 reconciled to the codebase; CHANGELOG [Unreleased].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:15:15 +02:00
makiolajandClaude Opus 4.8 b196d9ebe0 M2: wire navigation host + base screen scaffolds
Replace the RootScreen nav stubs with a real NavHost. Each destination
binds its existing M1 ViewModel from route args.

- ui/navigation: Dest route table (string-based; no serialization plugin)
  + FloretNavHost (lists -> task list -> detail / edit).
- Base scaffolds for TaskListScreen, TaskDetailScreen, TaskEditScreen,
  wired to their ViewModels — skeletons for the rich M2/M3 UI.
- RootScreen READY branch now hands off to FloretNavHost.
- Add navigation-compose 2.9.0; supporting strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:44:18 +02:00
makiolajandClaude Opus 4.8 b784a77329 docs: add architecture, roadmap, releasing, contributing + docs index
CI / ci (push) Successful in 5m34s
Add a grounded doc set under docs/ plus a root CONTRIBUTING.md:
- docs/ARCHITECTURE.md: current code shape (layers, data seam, provider
  resolution, reminder engine, DI, build/tooling, manifest)
- docs/ROADMAP.md: status view (M0/M1 done, M2 in progress, open decisions)
- docs/RELEASING.md: tag-driven release flow, CI jobs, F-Droid repo, secrets
  (was referenced by build.gradle.kts and CHANGELOG but missing)
- docs/README.md: docs index and how the docs relate
- CONTRIBUTING.md: build/test/lint, layer rules, style, PR conventions

Also refresh the stale "M0 — skeleton" status note in README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 08:49:05 +02:00