diff --git a/.github/screenshots/readme/calendar-month.png b/.github/screenshots/readme/calendar-month.png new file mode 100644 index 0000000..f6fe92a Binary files /dev/null and b/.github/screenshots/readme/calendar-month.png differ diff --git a/.github/screenshots/readme/calendar-week.png b/.github/screenshots/readme/calendar-week.png new file mode 100644 index 0000000..9f63241 Binary files /dev/null and b/.github/screenshots/readme/calendar-week.png differ diff --git a/.github/screenshots/readme/edit-entry.png b/.github/screenshots/readme/edit-entry.png new file mode 100644 index 0000000..bfd14a0 Binary files /dev/null and b/.github/screenshots/readme/edit-entry.png differ diff --git a/.github/screenshots/readme/history.png b/.github/screenshots/readme/history.png new file mode 100644 index 0000000..543d9db Binary files /dev/null and b/.github/screenshots/readme/history.png differ diff --git a/.github/screenshots/readme/inbox.png b/.github/screenshots/readme/inbox.png new file mode 100644 index 0000000..9c9664c Binary files /dev/null and b/.github/screenshots/readme/inbox.png differ diff --git a/.github/screenshots/readme/review.png b/.github/screenshots/readme/review.png new file mode 100644 index 0000000..007875a Binary files /dev/null and b/.github/screenshots/readme/review.png differ diff --git a/.github/screenshots/readme/statistics.png b/.github/screenshots/readme/statistics.png new file mode 100644 index 0000000..ab571c7 Binary files /dev/null and b/.github/screenshots/readme/statistics.png differ diff --git a/.github/screenshots/readme/templates.png b/.github/screenshots/readme/templates.png new file mode 100644 index 0000000..9f5d638 Binary files /dev/null and b/.github/screenshots/readme/templates.png differ diff --git a/.github/screenshots/readme/track.png b/.github/screenshots/readme/track.png new file mode 100644 index 0000000..efbf536 Binary files /dev/null and b/.github/screenshots/readme/track.png differ diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml new file mode 100644 index 0000000..0b56436 --- /dev/null +++ b/.github/workflows/renovate.yml @@ -0,0 +1,41 @@ +name: renovate + +on: + workflow_dispatch: + inputs: + logLevel: + description: Renovate log level + default: info + required: false + schedule: + # Daily, offset from the top of the hour to dodge GitHub's cron congestion. + - cron: "23 4 * * *" + +concurrency: + group: renovate + cancel-in-progress: false + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + renovate: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + + # The built-in token can create Renovate branches, dependency-dashboard issues, and PRs with + # the permissions above. GitHub may require a maintainer to approve CI runs created by it. + - name: Run Renovate + uses: renovatebot/github-action@v43.0.5 + with: + token: ${{ github.token }} + env: + LOG_LEVEL: ${{ inputs.logLevel || 'info' }} + RENOVATE_REPOSITORIES: ${{ github.repository }} + # The repo config (renovate.json) drives everything else. + RENOVATE_ONBOARDING: "false" + RENOVATE_REQUIRE_CONFIG: required diff --git a/.github/workflows/test-apk.yml b/.github/workflows/test-apk.yml new file mode 100644 index 0000000..be92989 --- /dev/null +++ b/.github/workflows/test-apk.yml @@ -0,0 +1,44 @@ +name: Test APK + +# Manually build a release-signed, separately-installable test APK +# (dev.tricked.solidverdant.test) from any branch. Uploads the APK as a build +# artifact only — it never touches the public "nightly" release. +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Decode and install keystore + run: | + echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > ${{ runner.temp }}/my-release-key.jks + + - name: Build test (.test) APK + run: ./gradlew app:assembleQa + env: + KEYSTORE_PATH: ${{ runner.temp }}/my-release-key.jks + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + + - name: Upload test APK artifact + uses: actions/upload-artifact@v4 + with: + name: solidverdant-test-apk + path: app/build/outputs/apk/qa/*.apk + if-no-files-found: error + retention-days: 14 diff --git a/.gitignore b/.gitignore index 13a4039..84e2dca 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ local.properties .DS_Store build captures +.github/screenshots/generated/ .externalNativeBuild solidtime-desktop da diff --git a/AGENTS.md b/AGENTS.md index eb99515..ddb000f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,22 +48,74 @@ If the intended behavior, product policy, or acceptance criteria needed for a co - Interactive controls need stable content descriptions and at least a 48 dp touch target. - Layouts must remain usable with large font/display scaling and keyboard or TalkBack navigation. +### Anti-drift rules for feature UI + +These keep feature screens consistent with the shared design system. They are enforced best-effort by detekt (`./gradlew detekt`, opt-in and outside the default build). + +- Compose from the shared kit. Build screens from the components in `ui/theme` and `ui/components`; do not fork bespoke variants of things the kit already provides. +- No hardcoded design values in feature code. Never write `Color(0x..)`, raw `fontSize = ..sp`, or magic `..dp` in feature packages. Pull spacing and sizing from `Dimens`, colors from `MaterialTheme.colorScheme` / `SemanticColors`, and type from `MaterialTheme.typography`. Raw literals belong only in `ui/theme`, where the tokens are defined. +- Reuse before you build. Check `ui/components` first; when a component genuinely does not exist, add the new shared piece to `ui/components` rather than inlining it in a screen. +- Progressive disclosure. The default, healthy state shows nothing extra — no always-on chrome, counters, or banners that bloat the screen. Power features live behind an affordance (icon, toggle, or sheet) and appear only when they apply. +- Use the shared status views. Render loading, empty, and error states with the shared `LoadingState`, `EmptyState`, and `ErrorState` from `ui/components` instead of re-inventing per-screen placeholders. + +## Formatting + +Spotless (ktlint + MPL license headers from `spotless/`, max line length 140) is the formatting +authority. Run `./gradlew spotlessCheck` to verify and `./gradlew spotlessApply` to fix before +committing. Composables are exempt from function-naming; everything else follows ktlint defaults. +Renovate (`.github/workflows/renovate.yml` + `renovate.json`) keeps dependencies current with the +built-in `GITHUB_TOKEN` and explicit contents/issues/pull-request write permissions. GitHub may +hold CI runs created by that token for maintainer approval; use an App or PAT token only when +fully automatic, no-approval CI triggering is required. + ## Verification Use the pinned development environment: ```sh -nix develop --command env -u LD_LIBRARY_PATH ./gradlew --no-daemon testDebugUnitTest lintDebug assembleDebug assembleDebugAndroidTest +nix develop --command env -u LD_LIBRARY_PATH ./gradlew --no-daemon spotlessCheck testDebugUnitTest lintDebug assembleDebug assembleDebugAndroidTest ``` +The flake pins JDK 21 plus SDK platforms 36/37 and build-tools 35, so the JVM test and Roborazzi +screenshot suites run on any machine with nix (on NixOS, point +`android.aapt2FromMavenOverride` in `~/.gradle/gradle.properties` at the nix SDK's aapt2). + For the connected development device, install `app-debug.apk`; its package is `dev.tricked.solidverdant.dev`. Run instrumentation with: ```sh -adb shell am instrument -w dev.tricked.solidverdant.dev.test/androidx.test.runner.AndroidJUnitRunner +adb shell am instrument -w dev.tricked.solidverdant.dev.test/dev.tricked.solidverdant.HiltTestRunner ``` When Hilt modules or database providers change, perform at least one clean build before device testing to avoid stale generated classes. +## On-device E2E tests + +- Flows live in `app/src/androidTest/.../e2e/flows`, built on `E2eRule` (mock solidtime backend + + seeded login + deterministic WorkManager), screen robots in `e2e/robots`, and stable testTags + re-exported through `e2e/TestTags` (tag constants are owned by production code, e.g. + `TrackingTestTags`). Match on tags or entry data, never on localized chrome; resolve localized + labels through resources when text matching is unavoidable (e.g. the undo snackbar action). +- Drive sync deterministically with `E2eRule.runPendingSync()` inside a `waitUntil`; never + `Thread.sleep`. `MockSolidtimeServer` honors `limit`/`offset` paging and records every request + (`callsMatching`) for payload assertions — assert what reached the server, not just what the UI + shows. +- New user-visible flows need an e2e test covering the happy path plus the failure/recovery edge + that AGENTS.md's feature-completeness rules promise (undo, offline, recreation, cross-device + state). + +## Performance + +- `./perf/run_perf.sh