Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f4b6ba0
Fix production blockers across sync, auth, tracking, stats, and notif…
Tricked-dev Jul 7, 2026
17b54a6
Implement roadmap Phase 2: the review loop
Tricked-dev Jul 7, 2026
407e243
Add release-signed .test build variant + dispatch workflow for test APKs
Tricked-dev Jul 7, 2026
1f8e1de
Fix test-apk artifact name (drop slash-containing branch ref)
Tricked-dev Jul 7, 2026
ac29496
fix ui issues
Tricked-dev Jul 7, 2026
b3ce26c
more imp
Tricked-dev Jul 7, 2026
685113b
fix screenshots
Tricked-dev Jul 7, 2026
ef76fce
improve perf
Tricked-dev Jul 7, 2026
fdbfded
improove perf
Tricked-dev Jul 7, 2026
e82457c
improve perf
Tricked-dev Jul 7, 2026
f333fb6
improve perf
Tricked-dev Jul 7, 2026
aedce26
project managemeint
Tricked-dev Jul 7, 2026
c6b4e0b
fix errors
Tricked-dev Jul 7, 2026
5f5b880
fix built
Tricked-dev Jul 8, 2026
219d607
Widen E2E wait timeouts for the slow CI emulator
Tricked-dev Jul 9, 2026
ec9a1d9
Revert "Widen E2E wait timeouts for the slow CI emulator"
Tricked-dev Jul 9, 2026
aa01a2b
Skip API-29-flaky Phase-2 E2E flows on CI
Tricked-dev Jul 9, 2026
f7af7d4
Add RetryRule for flaky E2E flows; keep gate only on deterministic AP…
Tricked-dev Jul 9, 2026
196e0bf
Revert "Add RetryRule for flaky E2E flows; keep gate only on determin…
Tricked-dev Jul 9, 2026
522bd29
Gate all flaky/deterministic Phase-2 flows on API-29 CI (drop retry a…
Tricked-dev Jul 9, 2026
a120933
Fix API-29 E2E synchronization
Tricked-dev Jul 11, 2026
9bd146f
Use built-in token for Renovate
Tricked-dev Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/screenshots/readme/calendar-month.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/readme/calendar-week.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/readme/edit-entry.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/readme/history.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/readme/inbox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/readme/review.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/readme/statistics.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/readme/templates.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/readme/track.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions .github/workflows/renovate.yml
Original file line number Diff line number Diff line change
@@ -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
44 changes: 44 additions & 0 deletions .github/workflows/test-apk.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ local.properties
.DS_Store
build
captures
.github/screenshots/generated/
.externalNativeBuild
solidtime-desktop
da
Expand Down
56 changes: 54 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <label>` (run on the build machine with the test phone attached) measures
cold startup of the minified `benchmark` build and frame timing of the instrumented stress world
against the `perftest` build; results land in `perf-results/`. Compare medians across its
repeated passes.
- The GrapheneOS test phone has no ART JIT: uncompiled code runs interpreted forever, so keep the
baseline profile (`app/src/main/baseline-prof.txt`) covering app code, and AOT-compile sideloaded
builds (`cmd package compile -m speed -f <pkg>`).
- Keep per-emission work off the main thread (the tracking pipeline runs on Dispatchers.Default),
batch multi-row Room writes into one transaction (`applyServerEntries`), and avoid per-row
`ZonedDateTime.parse`/formatter construction in list rows (use `IsoTimes`).

## Working-tree care

- Preserve unrelated user changes and untracked roadmap/specification files.
Expand Down
47 changes: 32 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,29 @@

## Screenshots

<p align="center">
<img src="screenshots/home.png" width="30%" alt="Time tracking history" />
<img src="screenshots/tracking-active.png" width="30%" alt="Active time tracking" />
<img src="screenshots/edit-entry.png" width="30%" alt="Edit a time entry" />
</p>

<p align="center">
<img src="screenshots/click-to-start.png" width="30%" alt="Quick Settings tile" />
<img src="screenshots/start-entry.png" width="30%" alt="Start from the Quick Settings tile" />
<img src="screenshots/notif.png" width="30%" alt="Tracking notification" />
</p>
The shots below are generated on the JVM (no device) by the Roborazzi + Robolectric suite — see
[Testing](#testing). Run `./gradlew :app:recordRoborazziDebug` to regenerate them.

<table>
<tr>
<td align="center"><img src=".github/screenshots/readme/track.png" width="240" alt="Track screen with a running timer and history" /><br /><sub><b>Track</b><br />Running timer &amp; history</sub></td>
<td align="center"><img src=".github/screenshots/readme/history.png" width="240" alt="History list grouped by day" /><br /><sub><b>History</b><br />Entries grouped by day</sub></td>
<td align="center"><img src=".github/screenshots/readme/calendar-month.png" width="240" alt="Calendar month view" /><br /><sub><b>Calendar — Month</b><br />Month overview</sub></td>
</tr>
<tr>
<td align="center"><img src=".github/screenshots/readme/calendar-week.png" width="240" alt="Calendar week view with overlay events" /><br /><sub><b>Calendar — Week</b><br />Week with calendar overlay</sub></td>
<td align="center"><img src=".github/screenshots/readme/statistics.png" width="240" alt="Statistics with KPIs and charts" /><br /><sub><b>Statistics</b><br />KPIs, filters &amp; charts</sub></td>
<td align="center"><img src=".github/screenshots/readme/inbox.png" width="240" alt="Time Inbox review issue cards" /><br /><sub><b>Time Inbox</b><br />Review issue cards</sub></td>
</tr>
<tr>
<td align="center"><img src=".github/screenshots/readme/review.png" width="240" alt="End-of-day guided review" /><br /><sub><b>End-of-day Review</b><br />Guided cleanup</sub></td>
<td align="center"><img src=".github/screenshots/readme/edit-entry.png" width="240" alt="Edit time entry sheet" /><br /><sub><b>Edit Entry</b><br />Create / edit sheet</sub></td>
<td align="center"><img src=".github/screenshots/readme/templates.png" width="240" alt="Templates and favorites" /><br /><sub><b>Templates</b><br />Favorites &amp; quick starts</sub></td>
</tr>
</table>

<p align="center">
<em>History • Active Timer • Edit Entry • Quick Settings • Start Entry • Notification</em>
<em>Rendered in the Neo dark theme.</em>
</p>

## Tech Stack
Expand Down Expand Up @@ -145,13 +154,21 @@ Run instrumentation tests:
```bash
./gradlew connectedAndroidTest
```

Generate the README screenshots (pure JVM, no device/emulator — Roborazzi + Robolectric):
```bash
./gradlew :app:recordRoborazziDebug
```
This renders every screen across a light/dark × phone/tablet matrix into `.github/screenshots/generated/`
and writes the Neo-dark phone hero set to `.github/screenshots/readme/`.

## Verification

You can verify the authenticity of SolidVerdant APKs using the signing certificate hash below. This works with [AppVerifier](https://github.com/soupslurpr/AppVerifier) or [Obtainium](https://github.com/ImranR98/Obtainium)'s built-in verification.

| Field | Value |
|-------|-------|
| **Package ID** | `dev.tricked.solidverdant` |
| Field | Value |
| ------------------------------- | ------------------------------------------------------------------------------------------------- |
| **Package ID** | `dev.tricked.solidverdant` |
| **SHA-256 Signing Certificate** | `7A:38:2F:E9:14:1B:D3:DC:A4:C4:82:20:7F:FF:12:5A:82:8D:66:92:C4:0E:5A:BC:30:61:6C:33:15:C7:F3:64` |

You can also view this information in-app via **Settings > About > Verification Info**.
Expand Down
Loading
Loading