docs: add architecture, roadmap, releasing, contributing + docs index
All checks were successful
CI / ci (push) Successful in 5m34s
All checks were successful
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>
This commit is contained in:
92
CONTRIBUTING.md
Normal file
92
CONTRIBUTING.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Contributing to Floret
|
||||
|
||||
Thanks for your interest in Floret — a Material 3 Expressive task app that's a
|
||||
pure front-end over the OpenTasks `TaskContract` provider, with no own database
|
||||
or sync stack. Before diving in, skim [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
|
||||
(how it's built), [`docs/ROADMAP.md`](docs/ROADMAP.md) (what's next), and
|
||||
[`docs/PLAN.md`](docs/PLAN.md) (the design rationale). This file covers the
|
||||
practical how.
|
||||
|
||||
## The one architectural rule
|
||||
|
||||
Everything above the data layer talks to `TasksRepository` and sees only domain
|
||||
types and Flows. **Provider column names, `TaskContract`, `ContentResolver`, and
|
||||
the authority string never leak above `data/tasks/`.** This is what keeps
|
||||
"Posture B" (bundling the provider later) an additive change instead of a
|
||||
rewrite — see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) §7. If a change
|
||||
would expose provider details to a ViewModel or the UI, it's in the wrong layer.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- JDK 17
|
||||
- Android SDK: compileSdk 37, build-tools 36.0.0 (the Gradle wrapper handles AGP/Kotlin)
|
||||
- A device or emulator with **OpenTasks** or **tasks.org** installed for
|
||||
anything touching the read/write paths (ideally with DAVx5 syncing a CalDAV
|
||||
task list, so there's real data). Debug builds fall back to `DemoSeeder` for
|
||||
sample data.
|
||||
|
||||
## Build, test, lint
|
||||
|
||||
```sh
|
||||
./gradlew :app:assembleDebug # build the debug APK
|
||||
./gradlew :app:testDebugUnitTest # JVM unit tests (JUnit5 + Truth + Turbine)
|
||||
./gradlew lintDebug # Android lint (CI runs this on every push)
|
||||
```
|
||||
|
||||
CI (`.gitea/workflows/ci.yaml`) runs lint → unit tests → debug build on every
|
||||
push, so run these locally before opening a PR. Keep CI green.
|
||||
|
||||
## Where to put code
|
||||
|
||||
| Layer | Lives in | Rule of thumb |
|
||||
|---|---|---|
|
||||
| Pure logic (models, filtering, sorting, form validation, date maths) | `domain/` | No Android imports — must be JVM-unit-testable. |
|
||||
| Provider access | `data/tasks/` | The only place that knows about the provider. New provider work goes through `TasksDataSource`. |
|
||||
| Reminders, prefs, DI, demo data | `data/reminders/`, `data/prefs/`, `data/di/`, `data/demo/` | |
|
||||
| Screens | `ui/<area>/` | One ViewModel + immutable `UiState` per area; Compose for the screen. |
|
||||
|
||||
## Code style & conventions
|
||||
|
||||
- **Kotlin**, 4-space indent, LF line endings, final newline, no trailing
|
||||
whitespace — all enforced by `.editorconfig` (2-space for yaml/toml/json/md).
|
||||
Match the surrounding code.
|
||||
- **Material 3 Expressive** for all UI: use `MaterialExpressiveTheme`, the
|
||||
colour-scheme tokens (never hardcoded colours), and canonical M3 components
|
||||
(e.g. `ListItem` for rows). Consult the `material-3` skill before designing a
|
||||
new screen or component.
|
||||
- Prefer the domain layer for anything testable; keep `AndroidTasksDataSource`
|
||||
the only Android-coupled data implementation so the rest stays JVM-testable.
|
||||
|
||||
## Tests
|
||||
|
||||
- New domain logic (mappers, filters, sorting, forms, value mapping) **must**
|
||||
come with JVM unit tests under `app/src/test/`. The data source is the
|
||||
JVM-testable seam — mock or fake it rather than reaching for instrumentation.
|
||||
- Add an instrumented test only when a path genuinely needs a real
|
||||
`ContentResolver`.
|
||||
|
||||
## Commits & PRs
|
||||
|
||||
- Write focused commits with clear messages (the existing history uses short,
|
||||
scoped subjects like `UI: ...` / `M1 ...`).
|
||||
- Update [`CHANGELOG.md`](CHANGELOG.md) under `[Unreleased]` for any
|
||||
user-visible change — its sections feed the release notes and F-Droid "What's
|
||||
New" (see [`docs/RELEASING.md`](docs/RELEASING.md)).
|
||||
- If your change shifts the architecture or completes a milestone, update
|
||||
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) / [`docs/ROADMAP.md`](docs/ROADMAP.md)
|
||||
in the same PR.
|
||||
- Don't bump `versionName` / `versionCode` by hand — the git tag drives those at
|
||||
release time.
|
||||
|
||||
## Scope
|
||||
|
||||
Floret stays true to its thesis: a front-end over **open** task backends
|
||||
(CalDAV / iCalendar / DecSync via the OpenTasks provider). Proprietary backends
|
||||
(Google Tasks, Microsoft To Do) are out of scope by design — they'd mean owning
|
||||
a sync stack. v1 targets the OpenTasks contract (OpenTasks + tasks.org); jtx's
|
||||
richer contract is a possible later addition.
|
||||
|
||||
## License
|
||||
|
||||
By contributing you agree your contributions are licensed under the project's
|
||||
[MIT License](LICENSE).
|
||||
11
README.md
11
README.md
@@ -22,10 +22,13 @@ database, no reinvented sync.
|
||||
A Calendula flower head is botanically made of many small *florets* — the
|
||||
individual items that make up the bloom. Floret is those items: your tasks.
|
||||
|
||||
> **Status: M0 — skeleton.** Builds and shows a themed placeholder. The
|
||||
> `TaskContract` data layer, task screens, and reminder engine are the next
|
||||
> milestones. See [`docs/PLAN.md`](docs/PLAN.md) for the full roadmap and the
|
||||
> A-now-B-later architecture (front-end first; bundle the provider later).
|
||||
> **Status: data layer done, UI in progress.** The full non-visual stack over
|
||||
> the `TaskContract` provider — provider resolution, live-updating reads,
|
||||
> writes, smart-list filtering, and a self-scheduled reminder engine — is built
|
||||
> and unit-tested. The Material 3 Expressive screens are now being built on top,
|
||||
> one at a time. See [`docs/ROADMAP.md`](docs/ROADMAP.md) for status,
|
||||
> [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for how it's built, and
|
||||
> [`docs/PLAN.md`](docs/PLAN.md) for the A-now-B-later design rationale.
|
||||
|
||||
## Sync sources (by design)
|
||||
|
||||
|
||||
263
docs/ARCHITECTURE.md
Normal file
263
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# Floret — architecture
|
||||
|
||||
This document describes how Floret is built **as it stands today**. For the
|
||||
*why* behind the big decisions and the long-term plan, see
|
||||
[`PLAN.md`](PLAN.md); for status and what's next, see [`ROADMAP.md`](ROADMAP.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. The thesis in one sentence
|
||||
|
||||
Floret is a Material 3 Expressive **front-end** over the OpenTasks
|
||||
`TaskContract` provider — it reads, writes, and reminds on top of a tasks store
|
||||
that some other app (DAVx5, SmoothSync, DecSync CC, tasks.org, …) syncs over
|
||||
CalDAV. **Floret owns no database and no sync stack.** It is the task-list
|
||||
sibling to [Calendula](https://gitea.jeanlucmakiola.de/makiolaj/calendula),
|
||||
which does the same thing for `CalendarContract`.
|
||||
|
||||
The whole design hangs off one rule:
|
||||
|
||||
> The entire app talks to a `TasksRepository`. Only the data layer knows there
|
||||
> is a `ContentResolver`, a `TaskContract`, or an authority string behind it.
|
||||
> **Provider column names and the authority string never leak above the data
|
||||
> layer.**
|
||||
|
||||
This is what lets "Posture A" (front-end over an installed provider) become
|
||||
"Posture B" (bundle the Apache-2.0 provider, be self-contained) without touching
|
||||
the UI, the ViewModels, or the domain. See §7.
|
||||
|
||||
---
|
||||
|
||||
## 2. Layers
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
UI │ Compose screens + ViewModels (ui/*) │
|
||||
│ RootScreen → permission gate → ListsScreen │
|
||||
└───────────────┬──────────────────────────────┘
|
||||
│ domain models + Flows only
|
||||
┌───────────────▼──────────────────────────────┐
|
||||
Domain │ Models, TaskForm, TaskFilter, TaskSorting, │
|
||||
│ DayWindow (pure Kotlin, no Android) │
|
||||
└───────────────┬──────────────────────────────┘
|
||||
│ TasksRepository (interface)
|
||||
┌───────────────▼──────────────────────────────┐
|
||||
Data │ TasksRepositoryImpl │
|
||||
│ └ TasksDataSource (interface) │
|
||||
│ └ AndroidTasksDataSource │
|
||||
│ └ ContentResolver / TaskContract / │
|
||||
│ ProviderResolver / ContentObserver│
|
||||
│ reminders/ prefs/ di/ demo/ │
|
||||
└───────────────┬──────────────────────────────┘
|
||||
│ content:// + dangerous perms
|
||||
┌───────────────▼──────────────────────────────┐
|
||||
External │ OpenTasks provider ←sync← DAVx5 / DecSync… │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The seam that matters is the pair of interfaces in the data layer:
|
||||
|
||||
- **`TasksRepository`** — the only type the UI sees. Flow-based reads, suspend
|
||||
writes. (`data/tasks/TasksRepository.kt`)
|
||||
- **`TasksDataSource`** — the JVM-testable interface that does the actual
|
||||
provider work; `AndroidTasksDataSource` is the only Android-coupled
|
||||
implementation.
|
||||
|
||||
Both are bound in Hilt in `data/di/DataModule.kt`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Module & package layout
|
||||
|
||||
Single `:app` module (Posture A). Package root `de.jeanlucmakiola.floret`.
|
||||
|
||||
| Package | Contents |
|
||||
|---|---|
|
||||
| `domain/` | `Models` (TaskList, Task, TaskDetail, enums + pure iCal↔domain value mappers), `TaskForm` (validated create/edit), `TaskFilter` + `TaskFiltering` (smart lists), `TaskSorting`, `DayWindow` (local-midnight maths). No Android imports. |
|
||||
| `data/tasks/` | `TasksContract` (vendored subset), `ProviderResolver` (the A/B seam), `TaskProjections`, `ColumnReader`, `TaskMapper` (cursor→domain), `TaskWriteMapper` (form→`ContentValues`), `TasksDataSource` + `AndroidTasksDataSource`, `TasksRepository` + `Impl`, `Failures`. |
|
||||
| `data/reminders/` | `ReminderScheduler` (the self-scheduled engine), `DueReminderReceiver`, `BootReceiver`, `ProviderChangeReceiver`, `ScheduledReminderStore`, `TaskNotifier`. |
|
||||
| `data/prefs/` | `SettingsPrefs` (DataStore). |
|
||||
| `data/di/` | `DataModule` (binds + provides), `Qualifiers` (`@IoDispatcher`). |
|
||||
| `data/demo/` | `DemoSeeder` (debug-only sample data). |
|
||||
| `ui/` | `theme/`, `common/` (GroupedList, ListChip), `lists/`, `tasklist/`, `detail/`, `edit/`, `settings/`, `permission/` (each a ViewModel + UiState; `lists` also has its screen), `RootScreen`. |
|
||||
| root | `FloretApp` (Hilt app), `MainActivity`. |
|
||||
|
||||
---
|
||||
|
||||
## 4. The data layer (the heart)
|
||||
|
||||
### 4.1 Provider targeting — `ProviderResolver`
|
||||
|
||||
`ProviderResolver.resolve()` walks a preference-ordered candidate list and
|
||||
returns the first provider actually installed (via
|
||||
`PackageManager.resolveContentProvider`), or `null` if none is. Each candidate
|
||||
is a `TaskProvider(authority, readPermission, writePermission, packageName)`.
|
||||
|
||||
| Provider | Authority | Permissions |
|
||||
|---|---|---|
|
||||
| OpenTasks | `org.dmfs.tasks` | `org.dmfs.permission.READ_TASKS` / `WRITE_TASKS` |
|
||||
| tasks.org | `org.tasks.opentasks` | `org.tasks.permission.READ_TASKS` / `WRITE_TASKS` |
|
||||
|
||||
Both are backed by the same dmfs `TaskProvider`, so the **same `TaskContract`
|
||||
columns apply** regardless of which is present. `null` from `resolve()` drives
|
||||
the "install a tasks provider" onboarding gate. `hasPermission()` checks both
|
||||
runtime perms for the active provider.
|
||||
|
||||
### 4.2 `TasksContract`
|
||||
|
||||
A vendored subset of the Apache-2.0 OpenTasks `TaskContract` — column names,
|
||||
table paths, status/priority constants, the local-account type. Floret does
|
||||
**not** take a runtime dependency on OpenTasks; the authority is injected from
|
||||
`ProviderResolver`, never hardcoded in the contract.
|
||||
|
||||
### 4.3 Reads — Instances + `ContentObserver`
|
||||
|
||||
`AndroidTasksDataSource` queries the denormalized **instances** view (so each
|
||||
occurrence is a row with the joined list colour, account, etc.), maps each
|
||||
cursor row through `ColumnReader` → `TaskMapper` → domain `Task`, and exposes
|
||||
the result as a `Flow`. A `ContentObserver` on the active authority's
|
||||
Tasks/TaskLists URIs bridges into the Flow via `callbackFlow`, so **any change
|
||||
re-emits** — Floret's own writes *and* external sync (DAVx5 pulling new tasks)
|
||||
update the UI live, and multiple sync sources coexist in one list.
|
||||
|
||||
### 4.4 Writes — repository API
|
||||
|
||||
```kotlin
|
||||
interface TasksRepository {
|
||||
fun taskLists(): Flow<List<TaskList>>
|
||||
fun tasks(filter: TaskFilter): Flow<List<Task>>
|
||||
fun taskDetail(taskId: Long): Flow<TaskDetail?>
|
||||
|
||||
suspend fun createTask(form: TaskForm): Long
|
||||
suspend fun updateTask(taskId: Long, form: TaskForm)
|
||||
suspend fun setCompleted(taskId: Long, completed: Boolean) // the core gesture
|
||||
suspend fun deleteTask(taskId: Long)
|
||||
suspend fun createLocalList(name: String, color: Int): Long
|
||||
|
||||
fun providerStatus(): ProviderStatus // READY | NEEDS_PERMISSION | NO_PROVIDER
|
||||
}
|
||||
```
|
||||
|
||||
`TaskWriteMapper` turns a validated `TaskForm` into `ContentValues`. Completion
|
||||
sets `STATUS = COMPLETED` (+ percent/completed timestamp); DAVx5 syncs that back
|
||||
out as a normal VTODO status change. Writes to local/unsynced lists use the
|
||||
sync-adapter URI form where the provider requires it.
|
||||
|
||||
### 4.5 Domain model notes
|
||||
|
||||
- `Task.id` is the **instance** row id; `Task.taskId` is the underlying
|
||||
`tasks._id` and the stable target for edits/completion.
|
||||
- Subtasks are carried via `parentId` (`RELATED-TO` / `RELATION_TYPE_PARENT`);
|
||||
`TaskDetail` bundles a task with its direct children.
|
||||
- `effectiveColor` = the task's own colour, else the list colour.
|
||||
- iCal priority is bucketed to `NONE/LOW/MEDIUM/HIGH`; status maps to a
|
||||
4-value enum. Both mappings are pure functions in `Models.kt`, unit-tested.
|
||||
|
||||
---
|
||||
|
||||
## 5. Smart lists, filtering, sorting
|
||||
|
||||
`TaskFilter` is either `OfList(listId)` or `Smart(SmartList)`. The smart lists —
|
||||
`ALL, TODAY, UPCOMING, OVERDUE, NO_DATE, COMPLETED` — are computed from due
|
||||
dates, not membership. `TaskFiltering.matches()` is a **pure predicate** taking
|
||||
`todayStart`/`todayEnd` (local-midnight bounds from `DayWindow`), so it
|
||||
unit-tests with a fixed clock. `TaskSorting` orders within a list (due /
|
||||
priority / etc.). None of this touches Android, which is why it's all in
|
||||
`domain/`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Reminders — the one subsystem that does NOT mirror Calendula
|
||||
|
||||
Calendula relies on the calendar provider broadcasting `EVENT_REMINDER`. **Tasks
|
||||
providers broadcast nothing**, so Floret schedules its own (`data/reminders/`):
|
||||
|
||||
- **`ReminderScheduler.sync()`** reads upcoming, non-closed, due-dated tasks
|
||||
within a rolling **30-day window**, computes each trigger as `due − lead`
|
||||
(lead from `SettingsPrefs`), and **diffs against `ScheduledReminderStore`** so
|
||||
only changed alarms move. It bails and clears everything if the provider is
|
||||
absent or unpermissioned.
|
||||
- Alarms are exact where allowed (`setExactAndAllowWhileIdle`, falling back to
|
||||
`set` when `canScheduleExactAlarms()` is false), keyed by `taskId`.
|
||||
- **`DueReminderReceiver`** fires → posts via `TaskNotifier` (channel,
|
||||
`POST_NOTIFICATIONS` gate, dedupe-by-tag).
|
||||
- **Re-sync triggers:** app start, **`BootReceiver`** (re-arm after reboot), and
|
||||
**`ProviderChangeReceiver`** (`PROVIDER_CHANGED` on both authorities → external
|
||||
sync changed the data). The store lets each run diff like Calendula diffs
|
||||
reminder rows.
|
||||
|
||||
This is the single largest piece of genuinely-new code in Floret.
|
||||
|
||||
---
|
||||
|
||||
## 7. The A / B seam (why the layering is shaped this way)
|
||||
|
||||
- **Posture A (today):** front-end over whatever provider is installed. Ships
|
||||
fast; requires a provider app present (the "needs DAVx5/OpenTasks" onboarding
|
||||
moment).
|
||||
- **Posture B (later):** add a `:provider` module bundling the Apache-2.0
|
||||
`opentasks-provider`. `ProviderResolver` then finds **our own** `org.dmfs.tasks`
|
||||
first; external CalDAV engines sync directly into it. **The UI, ViewModels,
|
||||
domain, and `TasksRepository` do not change** — only the resolver's default and
|
||||
some manifest perms.
|
||||
|
||||
Bundling the provider bundles **storage, not sync** — Floret stays a pure
|
||||
front-end over open backends either way.
|
||||
|
||||
---
|
||||
|
||||
## 8. UI
|
||||
|
||||
Compose + Material 3 **Expressive** (`MaterialExpressiveTheme`,
|
||||
`MotionScheme.standard()`, dynamic colour with a hand-tuned warm-mauve
|
||||
fallback in `ui/theme/`). Each screen area (`lists`, `tasklist`, `detail`,
|
||||
`edit`, `settings`, `permission`) has a ViewModel + immutable `UiState`;
|
||||
`ListsScreen` is the first rendered surface.
|
||||
|
||||
`RootScreen` is the entry composable: it gates on `ProviderStatus`
|
||||
(`NO_PROVIDER` / `NEEDS_PERMISSION` → onboarding `Gate`; `READY` →
|
||||
`ListsScreen`). The remaining screens are being built one at a time — their
|
||||
ViewModels exist and are tested against the real data layer; navigation
|
||||
callbacks are currently stubs (see [`ROADMAP.md`](ROADMAP.md)). Follow the
|
||||
`material-3` skill for component choices (M3 `ListItem` rows, expressive
|
||||
checkbox/FAB/swipe motion).
|
||||
|
||||
---
|
||||
|
||||
## 9. Dependency injection
|
||||
|
||||
Hilt, `SingletonComponent`. `DataModule` has a `@Binds` module
|
||||
(`TasksDataSource` → `AndroidTasksDataSource`, `TasksRepository` →
|
||||
`TasksRepositoryImpl`) and a `@Provides` module (the `floret_prefs` DataStore,
|
||||
the `@IoDispatcher`). `FloretApp` is the `@HiltAndroidApp` entry point;
|
||||
`MainActivity` is `@AndroidEntryPoint`. ViewModels get the repository injected.
|
||||
|
||||
---
|
||||
|
||||
## 10. Build & tooling
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Build | AGP 9.2.1, Kotlin 2.3.21, KSP, Hilt 2.59.2, Java 17 |
|
||||
| SDK | compileSdk 37, minSdk 29 (Android 10), targetSdk 36 |
|
||||
| UI | Compose BOM 2026.05.01, Material3 `1.5.0-alpha21` (Expressive APIs), Glance 1.1.1 (widget, later) |
|
||||
| Other | DataStore, kotlinx-datetime, kotlinx-coroutines |
|
||||
| Tests | JUnit5 (Jupiter) + Truth + Turbine + coroutines-test; the data source is the JVM-testable seam |
|
||||
| Versioning | git tag is the source of truth; `versionCode = MAJOR*10000 + MINOR*100 + PATCH`, derived in CI at release. See [`RELEASING.md`](RELEASING.md). |
|
||||
| CI | Gitea workflows (`.gitea/workflows/ci.yaml`, `release.yaml`) |
|
||||
| Distribution | F-Droid (`fdroid-metadata/`) |
|
||||
|
||||
---
|
||||
|
||||
## 11. Manifest surface
|
||||
|
||||
- **Permissions:** both `org.dmfs.*` and `org.tasks.*` read/write tasks perms
|
||||
declared statically (the active set is requested at runtime);
|
||||
`POST_NOTIFICATIONS`, `RECEIVE_BOOT_COMPLETED`, exact-alarm
|
||||
(`USE_EXACT_ALARM` on 33+, `SCHEDULE_EXACT_ALARM` ≤32).
|
||||
- **`<queries>`** for package visibility: both provider authorities + a LAUNCHER
|
||||
intent (so `resolveContentProvider` works and onboarding can open the
|
||||
provider / a store listing).
|
||||
- **Receivers:** `DueReminderReceiver` (not exported), `BootReceiver`,
|
||||
`ProviderChangeReceiver` (both authorities). No `EVENT_REMINDER` receiver —
|
||||
that's a Calendula thing that doesn't apply here.
|
||||
27
docs/README.md
Normal file
27
docs/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Floret — documentation
|
||||
|
||||
Floret is a Material 3 Expressive **task** app for Android: a pure front-end over
|
||||
the OpenTasks `TaskContract` provider (synced by DAVx5 / SmoothSync / DecSync
|
||||
over CalDAV), with no own database or sync stack. Sibling to
|
||||
[Calendula](https://gitea.jeanlucmakiola.de/makiolaj/calendula). See the
|
||||
top-level [`../README.md`](../README.md) for the project pitch.
|
||||
|
||||
## Index
|
||||
|
||||
| Doc | What it covers |
|
||||
|---|---|
|
||||
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | How Floret is built **today** — layers, the data seam, provider resolution, the reminder engine, DI, build/tooling, manifest. Start here to work on the code. |
|
||||
| [`ROADMAP.md`](ROADMAP.md) | **Status** and what's next — milestones (M0–M6 + Posture B), what's done, open decisions, how to build/verify. |
|
||||
| [`PLAN.md`](PLAN.md) | The original implementation plan and **design rationale** — the A-now-B-later thesis, what transfers from Calendula, the locked decisions. The "why". |
|
||||
| [`RELEASING.md`](RELEASING.md) | How to cut a release — the git-tag-as-source-of-truth flow, CI jobs, F-Droid repo, required secrets. |
|
||||
|
||||
Also: [`../CHANGELOG.md`](../CHANGELOG.md) (Keep a Changelog format; tag sections
|
||||
feed the release notes).
|
||||
|
||||
## How the docs relate
|
||||
|
||||
- **PLAN** is the design decisions (mostly stable; the "why").
|
||||
- **ARCHITECTURE** is the current shape of the code (kept in sync with the
|
||||
source as it grows).
|
||||
- **ROADMAP** is the moving status layer (update as milestones land).
|
||||
- **RELEASING** is the operational runbook.
|
||||
101
docs/RELEASING.md
Normal file
101
docs/RELEASING.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Floret — releasing
|
||||
|
||||
Floret is distributed through a **self-hosted F-Droid repo** (on Hetzner) with a
|
||||
human-readable **Gitea release** per tag. Both are produced automatically by
|
||||
`.gitea/workflows/release.yaml` when you push a tag. There are no APK assets on
|
||||
the Gitea release itself — distribution lives in the F-Droid repo; the release is
|
||||
the changelog of record.
|
||||
|
||||
---
|
||||
|
||||
## The one source of truth: the git tag
|
||||
|
||||
The git tag drives the version. You do **not** hand-edit version numbers for a
|
||||
release — CI substitutes them from the tag:
|
||||
|
||||
- `versionName` = the tag without a leading `v` (e.g. `v0.2.0` → `0.2.0`).
|
||||
- `versionCode` = `MAJOR*10000 + MINOR*100 + PATCH` (e.g. `0.2.0` → `200`,
|
||||
`1.3.4` → `10304`).
|
||||
|
||||
The values committed in `app/build.gradle.kts` are just the local/dev default;
|
||||
keep them roughly matching the latest released tag, but the tag wins at release
|
||||
time.
|
||||
|
||||
---
|
||||
|
||||
## Cutting a release
|
||||
|
||||
1. **Update `CHANGELOG.md`.** Move items out of `[Unreleased]` into a new
|
||||
`## [X.Y.Z]` section. The release pipeline extracts everything between
|
||||
`## [X.Y.Z]` and the next `## [` heading and uses it verbatim as both the
|
||||
Gitea release notes and the F-Droid per-version "What's New"
|
||||
(`changelogs/<versionCode>.txt`). If no matching section exists, a fallback
|
||||
line is used — so the heading **must** match the tag's version exactly.
|
||||
2. **Commit** the changelog on `main`.
|
||||
3. **Tag and push:**
|
||||
```sh
|
||||
git tag v0.2.0
|
||||
git push origin v0.2.0
|
||||
```
|
||||
4. CI takes over (see below). Watch the run in Gitea Actions.
|
||||
|
||||
---
|
||||
|
||||
## What CI does on a tag
|
||||
|
||||
`release.yaml` runs three jobs:
|
||||
|
||||
| Job | Purpose |
|
||||
|---|---|
|
||||
| `ci` | Sanity gate: unit tests + a debug build (catches version-substitution drift). The other jobs depend on this. |
|
||||
| `build-and-deploy` | Substitute version from the tag → build signed release APK → fetch the existing F-Droid repo from Hetzner → add the new APK + per-version changelog → `fdroid update -c` → upload `repo/` + `metadata/` back. Also attaches the R8 `mapping.txt.gz` to the Gitea release (best-effort) so crash stacktraces stay deobfuscatable. |
|
||||
| `gitea-release` | Create/update the Gitea release for the tag, body = the extracted CHANGELOG section. Gated on `ci` only (not deploy), so notes still publish if the F-Droid upload hiccups. |
|
||||
|
||||
Both deploy and release steps are **re-run safe** (idempotent upserts), so a
|
||||
failed run can be retried.
|
||||
|
||||
---
|
||||
|
||||
## Required CI secrets
|
||||
|
||||
Configured in the Gitea repo settings; the workflow fails loudly if the F-Droid
|
||||
ones are missing (it will **never** auto-generate a repo key — that would rotate
|
||||
the repo fingerprint and break every user's pinned repo).
|
||||
|
||||
| Secret | Used for |
|
||||
|---|---|
|
||||
| `KEYSTORE_BASE64`, `KEY_PASSWORD`, `KEY_ALIAS` | App signing keystore (the APK). |
|
||||
| `FDROID_KEYSTORE_BASE64`, `FDROID_CONFIG_BASE64` | The F-Droid **repo** signing key + `config.yml`. Never uploaded to the server. |
|
||||
| `HETZNER_HOST`, `HETZNER_USER`, `HETZNER_PASS` | SFTP target for the published `repo/` + `metadata/`. |
|
||||
| `GITHUB_TOKEN` | Gitea API (release create/patch, asset upload). |
|
||||
|
||||
The repo signing key and `config.yml` come from secrets at build time and are
|
||||
**never** pulled from or pushed back to the server, so they can't leak into the
|
||||
web-served tree (nginx serves only `repo/`).
|
||||
|
||||
---
|
||||
|
||||
## Key rotation / repo recovery
|
||||
|
||||
A manual `workflow_dispatch` run (ref = a branch, not a tag) skips all the
|
||||
tag-only build steps: it just re-signs the existing index with the configured
|
||||
repo key and re-uploads. Use this to recover the repo or rotate infrastructure
|
||||
**without** publishing a new APK.
|
||||
|
||||
---
|
||||
|
||||
## Push CI (non-tag)
|
||||
|
||||
Every push to any branch runs `.gitea/workflows/ci.yaml`: `lintDebug` →
|
||||
`testDebugUnitTest` → `assembleDebug`, plus a Trivy filesystem scan on `main`.
|
||||
Keep this green before tagging.
|
||||
|
||||
---
|
||||
|
||||
## F-Droid metadata
|
||||
|
||||
App store listing lives in `fdroid-metadata/` (`de.jeanlucmakiola.floret.yml`
|
||||
plus `en-US/` summary/description). Per-version changelogs are generated into the
|
||||
repo's `metadata/.../en-US/changelogs/<versionCode>.txt` from `CHANGELOG.md` at
|
||||
release time; metadata is uploaded alongside `repo/` so changelog history
|
||||
survives across releases.
|
||||
105
docs/ROADMAP.md
Normal file
105
docs/ROADMAP.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Floret — roadmap
|
||||
|
||||
Where the project is and where it's going. This is the **status** view; the
|
||||
design rationale behind each milestone lives in [`PLAN.md`](PLAN.md), and how the
|
||||
pieces fit together is in [`ARCHITECTURE.md`](ARCHITECTURE.md).
|
||||
|
||||
Status legend: ✅ done · 🚧 in progress · ⬜ not started
|
||||
|
||||
---
|
||||
|
||||
## Current state (one line)
|
||||
|
||||
The full non-visual stack ("backoffice") over the OpenTasks `TaskContract`
|
||||
provider is **done and unit-tested**; the Material 3 Expressive UI is being
|
||||
built screen by screen on top of it. App builds, gates on provider/permission,
|
||||
and renders the lists overview.
|
||||
|
||||
---
|
||||
|
||||
## Milestones
|
||||
|
||||
### ✅ M0 — Skeleton
|
||||
Project scaffolding copied from Calendula (Gradle, version catalog, Hilt,
|
||||
Material 3 Expressive theme, Gitea CI/release workflows, F-Droid metadata),
|
||||
reseeded to a warm-mauve fallback palette. Builds, shows a themed placeholder.
|
||||
|
||||
### ✅ M1 — Read path + logic (the "backoffice")
|
||||
The complete non-visual stack:
|
||||
- Vendored `TaskContract` subset, `ProviderResolver` (runtime authority
|
||||
detection), `ColumnReader`, mappers, `AndroidTasksDataSource` (Instances
|
||||
query + `ContentObserver`), and `TasksRepository` exposing live Flows of
|
||||
lists / tasks / detail.
|
||||
- Domain models, smart-list filtering (Today / Upcoming / Overdue / No-date /
|
||||
All / Completed), sorting, form validation, subtasks via `parentId`.
|
||||
- Self-scheduled due-reminder engine (AlarmManager + boot / provider-change
|
||||
re-sync), notifications, DataStore prefs.
|
||||
- Render-only ViewModels + UiState for **every** screen, so the UI is build-only
|
||||
from here.
|
||||
- JVM unit tests across mappers, filtering, sorting, form, value mapping, and
|
||||
day windows.
|
||||
|
||||
### 🚧 M2 — Screens: lists, task list, complete & CRUD
|
||||
Replace the functional scaffold with the real Material 3 Expressive UI, screen
|
||||
by screen, against the already-built ViewModels.
|
||||
- ✅ Provider/permission onboarding gate (`RootScreen`).
|
||||
- ✅ Lists overview (`ListsScreen`) — smart lists + user lists grouped by account.
|
||||
- 🚧 Navigation host wiring (the `onOpenFilter` / `onNewTask` callbacks in
|
||||
`RootScreen` are currently stubs).
|
||||
- ⬜ Task list screen — checkbox rows, swipe-to-complete / swipe-to-delete,
|
||||
inline add field, section headers for smart lists, FAB.
|
||||
- ⬜ Toggle complete (swipe + checkbox), create / edit / delete, local-list
|
||||
creation wired to the UI.
|
||||
|
||||
### ⬜ M3 — Detail / edit polish
|
||||
Task detail and edit screens: due/start date-time pickers, priority,
|
||||
percent-complete, conflict-safe saves (re-check before overwrite), smart-list
|
||||
section presentation.
|
||||
|
||||
### ⬜ M4 — Subtasks (UI)
|
||||
`RELATED-TO` hierarchy in the UI: indentation, create subtask, reparent. The
|
||||
data layer already reads `parentId`; this is the trickiest UI piece.
|
||||
|
||||
### ⬜ M5 — Reminders onboarding & polish
|
||||
The engine exists (M1). Remaining: the `POST_NOTIFICATIONS` + exact-alarm
|
||||
onboarding flow, per-task / default reminder-offset settings UI, and
|
||||
end-to-end verification on device.
|
||||
|
||||
### ⬜ M6 — Settings, widget, i18n, release
|
||||
Settings screen (theme / dynamic-color / language, default list, default
|
||||
reminder), Glance task-list widget, translations, finalize F-Droid metadata,
|
||||
confirm CI release flow.
|
||||
|
||||
### ⬜ Posture B (separate track, later)
|
||||
Add a `:provider` module bundling the Apache-2.0 `opentasks-provider`;
|
||||
`ProviderResolver` defaults to our own `org.dmfs.tasks`; add sync-adapter
|
||||
permissions; ship self-contained. UI / repository / domain untouched — see
|
||||
[`ARCHITECTURE.md`](ARCHITECTURE.md) §7.
|
||||
|
||||
---
|
||||
|
||||
## Open decisions / to verify
|
||||
|
||||
These carry over from [`PLAN.md`](PLAN.md) §9; resolved ones are struck through.
|
||||
|
||||
1. ~~**Name** — `Floret`~~ confirmed (appId `de.jeanlucmakiola.floret`).
|
||||
2. ~~**tasks.org provider authority**~~ verified on device:
|
||||
`org.tasks.opentasks` + `org.tasks.permission.*`.
|
||||
3. **jtx Board** — support its richer contract later, or stay OpenTasks-only?
|
||||
(Not in the candidate list today.)
|
||||
4. **Posture B authority choice** — bundling `org.dmfs.tasks` makes Floret a
|
||||
*replacement* for OpenTasks (one authority owner per device). Intended, but a
|
||||
conscious choice.
|
||||
5. **Recurring tasks** — read as occurrences today (`isRecurring` flag exists);
|
||||
recurrence-aware editing is out of scope for v1.
|
||||
|
||||
---
|
||||
|
||||
## How to contribute / verify
|
||||
|
||||
- Build: `./gradlew :app:assembleDebug`
|
||||
- Unit tests: `./gradlew :app:testDebugUnitTest`
|
||||
- Run on a device/emulator that has **OpenTasks** or **tasks.org** installed (and
|
||||
ideally DAVx5 syncing a CalDAV task list) so the read/write paths have real
|
||||
data. Debug builds use `DemoSeeder` for sample data when no provider data is
|
||||
present.
|
||||
Reference in New Issue
Block a user