Initial commit: EventTracker desktop app + Android companion
Desktop: GTK3 + matplotlib app tracking arbitrary events (button -> timestamp -> CSV), with per-event stats, trend charts, settings (rename/add events, relocate data file), and a reset-with-backup flow. Portable desktop launcher via a .desktop.in template + install script, no machine-specific paths baked in. Android: minimal Kivy v1 companion reading/writing the same CSV format, plus buildozer build tooling isolated in a venv and a local p4a recipe override for kivy (see CLAUDE.md for why). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
cd9f9456e9
17 changed files with 1722 additions and 0 deletions
226
CLAUDE.md
Normal file
226
CLAUDE.md
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
# CLAUDE.md
|
||||
|
||||
Context for future Claude sessions working on this repo.
|
||||
|
||||
## Current status (2026-08-22)
|
||||
|
||||
- Desktop app (`event_tracker.py`) is feature-complete and working on the
|
||||
user's machine: record buttons, stats popup, settings popup, reset,
|
||||
relocatable data path, taskbar icon, "Time Since" column.
|
||||
- Repo is pushed to `repo.tas-tech.net/tas-tech.net/event-tracker`
|
||||
(private — will go public once there's a tagged release).
|
||||
- Android build (`android/`) is in progress. Native recipe compilation
|
||||
(SDL2, OpenSSL, sqlite3, pyjnius, hostpython3) succeeds. The last
|
||||
blocker was a p4a upstream bug in the `kivy` recipe's `python_depends`
|
||||
— see "Android companion — design decisions" below for the fix
|
||||
(`android/p4a-recipes/kivy`). Not yet confirmed working end-to-end —
|
||||
next session should check whether the user has run
|
||||
`./build.sh android debug` since that fix landed and, if not, ask for
|
||||
the latest log tail before touching anything else Android-related.
|
||||
- **Outstanding, agreed but not implemented**: split `config.json` into
|
||||
two files — event list (safe to sync between machines) vs. data file
|
||||
path (must stay local per-device, same principle as the Android app's
|
||||
own config). User explicitly agreed to this; it just hasn't been
|
||||
built yet. Don't re-ask, just do it when picking this up.
|
||||
- **Outstanding**: LICENSE file. User has decided on GPL-3.0 (want to
|
||||
give simple apps away free via F-Droid/Aurora, charge for the same
|
||||
thing on Play Store — GPL permits charging for build/distribution,
|
||||
doesn't force pricing). Not yet added as a file; add it before the
|
||||
first public/tagged release if nobody's done it by then.
|
||||
- Standing rule, unchanged: **never `git push` without asking the user
|
||||
first**, even though this repo now has a remote and history — treat
|
||||
every future push as needing fresh confirmation, not a standing
|
||||
yes.
|
||||
|
||||
## What this is
|
||||
|
||||
Single-file GTK3 + matplotlib desktop app (`event_tracker.py`,
|
||||
despite the filename it's now multi-event and the event list is user
|
||||
editable). One button per entry in `self.events` records a timestamp to
|
||||
a CSV file; the main window shows a total/avg-hour/avg-day row per event
|
||||
and a line graph of today's counts by hour. A stats popup adds
|
||||
streaks/trends and by-hour/week/month charts; a settings popup renames
|
||||
or adds events and relocates the data file.
|
||||
|
||||
## Design decisions
|
||||
|
||||
- **CSV, not sqlite** — dataset is tiny (a handful of rows/day), and a
|
||||
flat file lets the user hand-edit a bad entry without tooling.
|
||||
- **No inline magic numbers** — all environment-specific values
|
||||
(`DEFAULT_EVENTS`, `COLOR_PALETTE`, `DEFAULT_DATA_PATH`, `CONFIG_PATH`,
|
||||
`WEEKLY_WEEKS`, `MONTHLY_MONTHS`, `TIMESTAMP_FORMAT`) live in a config
|
||||
block at the top of the file. Change those, not the logic below.
|
||||
- **GTK3 over Tkinter/Qt** — native widget set on the target desktop
|
||||
(EndeavourOS/GNOME-ish), no pip-installed GUI runtime to drag around.
|
||||
- **Malformed CSV rows are skipped, not fatal** — `load_entries()`
|
||||
tolerates a hand-edited file rather than crashing the app on startup.
|
||||
- **One CSV, `event` + `timestamp` columns** — chosen over one file per
|
||||
event so stats/graph code doesn't duplicate per event. `ensure_csv()`
|
||||
auto-migrates the original single-column format on first run
|
||||
(existing rows relabelled to the first configured event).
|
||||
- **Reset backs up, never deletes outright** — `reset_log()` copies the
|
||||
current CSV to a timestamped `.bak-*` file before truncating. The
|
||||
confirmation dialog in `TrackerWindow.on_reset` is the only path to
|
||||
this, and always warns before calling it.
|
||||
- **Integer y-axis ticks** — `MaxNLocator(integer=True)` on every graph;
|
||||
event counts are always whole numbers, so fractional ticks (e.g. 0.2)
|
||||
would be misleading.
|
||||
- **`compute_stats()`/`weekly_counts()`/`monthly_counts()`/`format_elapsed()`
|
||||
are pure** — take a sorted list of datetimes (or a timedelta) for one
|
||||
event, return plain dicts/tuples/strings, no GTK/matplotlib
|
||||
references. Keeps the date math testable without a display.
|
||||
- **"Time Since" ticks live, not just on action** — `GLib.timeout_add_seconds(60, ...)`
|
||||
in `TrackerWindow.__init__` calls `refresh()` every 60s so the elapsed
|
||||
time keeps counting up without a new event or button click. This is
|
||||
the one thing in the UI that changes purely with wall-clock time.
|
||||
- **Event list is config, not data** — stored separately in
|
||||
`config.json` (via `load_config()`/`save_config()`), not in the CSV.
|
||||
`self.events` on `TrackerWindow` is the live, mutable copy; renaming
|
||||
in Settings calls `rename_event_in_csv()` so historical rows follow
|
||||
the rename rather than becoming orphaned. Colors are derived from an
|
||||
event's position in the list (`event_color()`), not stored per-event,
|
||||
so there's nothing to keep in sync when the list changes.
|
||||
- **Data file location is also config, and is itself relocatable** —
|
||||
every CSV function (`ensure_csv`, `load_entries`, `append_entry`,
|
||||
`rename_event_in_csv`, `reset_log`) takes `csv_path` as an explicit
|
||||
argument rather than reading a module constant, because it can change
|
||||
at runtime via Settings. `self.data_path` on `TrackerWindow` is the
|
||||
live value; `CONFIG_PATH` itself (where that value is recorded) stays
|
||||
fixed — it's a small pointer file, not the user's data, so it isn't
|
||||
exposed as something to relocate. `move_data_file()` backs up
|
||||
whatever's already at a destination before overwriting it, same
|
||||
belt-and-suspenders approach as `reset_log()`.
|
||||
- **Widget rebuild over dynamic-only display** — `rebuild_event_widgets()`
|
||||
destroys and recreates the record buttons and stats-grid rows when the
|
||||
event list changes, rather than trying to diff them. Simpler and this
|
||||
only runs on an explicit Settings save, not per-frame.
|
||||
- **App icon requires both a window icon and WM_CLASS matching** — GTK's
|
||||
window icon alone isn't enough for KDE/GNOME taskbars to pick the
|
||||
right icon; without a WM_CLASS matching the `.desktop` file's
|
||||
`StartupWMClass`, the taskbar can fall back to an unrelated running
|
||||
app's icon. `main()` calls `GLib.set_prgname(WM_CLASS)` before creating
|
||||
any window, and `WM_CLASS` must stay identical to `StartupWMClass` in
|
||||
`event-tracker.desktop.in`.
|
||||
- **Icon is PNG, not SVG, on purpose** — `event-tracker.svg` still ships
|
||||
(source of truth for the design, regenerate the PNG from it if the
|
||||
icon changes), but `ICON_FILE` points at `event-tracker.png` and the
|
||||
`.desktop` file's `Icon=` does too. SVG decoding depends on the
|
||||
system's gdk-pixbuf loader modules (librsvg's loader specifically),
|
||||
which isn't guaranteed present/registered even when librsvg itself is
|
||||
installed — confirmed missing on the dev machine via
|
||||
`gdk-pixbuf-query-loaders | grep svg` returning nothing despite
|
||||
librsvg being installed. PNG decoding is core gdk-pixbuf, no optional
|
||||
loader involved, so it isn't a system-dependent single point of
|
||||
failure. Both files are loaded by absolute path — no icon-theme
|
||||
install/cache-refresh step required either way.
|
||||
|
||||
## Repo layout
|
||||
|
||||
```
|
||||
event_tracker.py # the desktop GTK app
|
||||
event-tracker.desktop.in # launcher template (no machine-specific path baked in)
|
||||
install-launcher.sh # generates event-tracker.desktop for this clone's actual path and installs it
|
||||
event-tracker.svg # icon source (edit this if the design changes)
|
||||
event-tracker.png # icon actually referenced by the app + launcher, regenerate from the .svg
|
||||
README.md # install/run instructions for the desktop app
|
||||
CLAUDE.md # this file
|
||||
android/
|
||||
main.py # minimal Kivy touchscreen app, v1 — see its own README for scope
|
||||
buildozer.spec # Android build config
|
||||
README.md # build/install/permission instructions specific to Android
|
||||
setup-build-env.sh # creates the isolated venv for buildozer/cython
|
||||
build.sh # runs buildozer through that venv (sets PATH + VIRTUAL_ENV by hand)
|
||||
p4a-recipes/kivy/ # local override of p4a's built-in kivy recipe — see design decisions below
|
||||
```
|
||||
|
||||
## Android companion — design decisions
|
||||
|
||||
- **Same CSV format, deliberately zero coupling otherwise** — the
|
||||
Android app only knows the `event,timestamp` column format and
|
||||
`TIMESTAMP_FORMAT`; it has no code path that talks to the desktop app
|
||||
or assumes anything about it. The two apps stay in sync purely because
|
||||
they read/write the same file, which some third-party sync tool
|
||||
(FolderSync/DAVx5) mirrors between devices — that sync is out of scope
|
||||
for both apps and is the user's own infrastructure.
|
||||
- **v1 is intentionally thin** (record buttons, today's totals, one bar)
|
||||
— Android build tooling (buildozer/SDK/NDK) is the actual risk on a
|
||||
first build, not app logic. Shipping the smallest thing that proves
|
||||
the pipeline works, before porting the stats popup / settings / rename
|
||||
/ charts from the desktop app, was a deliberate choice — see
|
||||
`android/README.md`.
|
||||
- **No matplotlib on Android** — unreliable through Kivy's garden
|
||||
packaging. `BarRow` in `main.py` hand-draws a proportional bar with
|
||||
Kivy's own `Color`/`Rectangle` canvas primitives instead.
|
||||
- **Config path is local-only, same principle as desktop's `CONFIG_PATH`**
|
||||
— each device's app-private storage holds only that device's CSV
|
||||
path; it is never itself synced. Only the CSV — the actual data — goes
|
||||
through the user's sync tool.
|
||||
- **`MANAGE_EXTERNAL_STORAGE` permission** — the CSV can live in an
|
||||
arbitrary user-chosen folder (wherever the sync tool puts it), so
|
||||
scoped storage / SAF alone isn't enough; broad file access is required
|
||||
and must additionally be granted manually in Android settings post-install
|
||||
(see `android/README.md` step 5) — Android doesn't allow silently
|
||||
requesting this one like the legacy permissions.
|
||||
- **The APK is built by the user, on their own machine** — this was
|
||||
never attempted in the cloud sandbox that authored the code: no
|
||||
reliable way to fetch a multi-GB Android SDK/NDK there, and "run
|
||||
`buildozer android debug` on your own box" is the correct place for
|
||||
this regardless.
|
||||
- **Known gap, not silently glossed over**: whole-file sync tools don't
|
||||
merge concurrent CSV writes from two devices — logging on both within
|
||||
the same sync window risks a conflicted duplicate file rather than
|
||||
merged rows. Documented in `android/README.md`, not solved in code.
|
||||
- **Build tooling lives in a venv, never `--break-system-packages`** —
|
||||
`setup-build-env.sh` creates a plain `python3 -m venv` (no
|
||||
`--system-site-packages` — that was tried and reverted, see next
|
||||
point) at `android/build/venv` and installs `buildozer`/`cython` into
|
||||
it. `build.sh` runs buildozer by absolute path rather than
|
||||
`source`-ing the venv, so it manually replicates the two things
|
||||
activation would do: puts `venv/bin` on `PATH` (buildozer shells out
|
||||
to find `cython`) and sets `VIRTUAL_ENV` (buildozer's own
|
||||
`targets/android.py` checks this env var to decide whether its
|
||||
bootstrap `pip install` needs `--user`; without it, it wrongly assumes
|
||||
it's not in a venv, uses `--user`, and pip refuses since there's no
|
||||
user site-packages inside an isolated venv).
|
||||
- **`p4a-recipes/kivy` — local override, not a workaround** — p4a's own
|
||||
built-in `kivy` recipe declares
|
||||
`python_depends = ['certifi', 'chardet', 'idna', 'requests', 'urllib3', 'filetype']`
|
||||
(for `kivy.network.UrlRequest`, which this app never calls).
|
||||
`requests` transitively needs `charset-normalizer`, whose newest
|
||||
release is the first to ship a prebuilt Android-tagged wheel (new,
|
||||
tied to CPython 3.14's official Android build target). p4a's own
|
||||
dependency resolver (`process_python_modules` in
|
||||
`pythonforandroid/build.py`) finds that wheel correctly during its
|
||||
`pip --dry-run --platform=android_24_arm64_v8a` pass and pins the
|
||||
exact URL into a generated `requirements.txt` — but the *next* stage
|
||||
(`run_pymodules_install`) installs from that file with a plain host
|
||||
pip and no `--platform` override, so it rejects its own pinned wheel
|
||||
as "not a supported wheel on this platform." That's a real upstream
|
||||
p4a bug, confirmed by reading `pythonforandroid/build.py` directly
|
||||
(not guessed) — and `python_depends` isn't something `buildozer.spec`
|
||||
can override, so `p4a-recipes/kivy/__init__.py` is a full copy of the
|
||||
upstream recipe (patches included) with just `python_depends = []`.
|
||||
`buildozer.spec` points at it via `p4a.local_recipes = p4a-recipes`.
|
||||
If a future Kivy release actually needs one of those network extras,
|
||||
re-diff against p4a's current `pythonforandroid/recipes/kivy/__init__.py`
|
||||
before blindly re-adding them.
|
||||
- **Previously-tried and reverted**: `--system-site-packages` on the
|
||||
build venv. It fixed the `--user`/pip-in-venv error above, but leaked
|
||||
the real system Python's installed packages into what p4a decided the
|
||||
app needed to bundle (confirmed via `pip show requests` showing a
|
||||
system-wide install), which is a worse, harder-to-diagnose failure
|
||||
mode than the one it fixed. Don't reintroduce it as a quick fix for a
|
||||
future build error without checking whether it's actually leaking
|
||||
something first.
|
||||
|
||||
## Conventions for changes
|
||||
|
||||
- Keep the desktop app single-file unless it grows enough to justify
|
||||
splitting (e.g. a separate stats module) — ask before restructuring.
|
||||
- Any new tunable value goes in the config block at the top of whichever
|
||||
file it belongs to, never buried in a function body.
|
||||
- Keep the CSV format (`event,timestamp`, `TIMESTAMP_FORMAT`) identical
|
||||
across `event_tracker.py` and `android/main.py` — changing one
|
||||
without the other breaks cross-device sync silently.
|
||||
- Git remote is `repo.tas-tech.net` (Forgejo), auth via `~/.netrc`,
|
||||
user `AI_Assistant` — **never push without asking the user first.**
|
||||
Loading…
Reference in a new issue