Initial commit: Arbeitsorte-Logger + data-safety/robustness/input work
Existing app (single-file Tkinter/ttkbootstrap desktop tool for logging blood-drive work assignments and mapping them) plus the first round of improvements: - Data safety: atomic CSV writes (tmp + fsync + os.replace), rotating backups in data/backups/ (startup + before every change, keep 20), fallback to empty/backup on missing/empty/corrupt orte.csv. - Geocoder robustness: per-item try/except so the worker thread survives failures; GeocodingUnavailable + one-time "service unreachable" dialog. - Input: DateEntry calendar picker with parse_date() validation; manual lat/lon fields in the edit dialog; Tab 2 highlights/filters rows without coordinates and adds a right-click "Koordinaten suchen". - Logging to data/app.log; shared autocomplete helpers; config constants; map fit_bounds. docs/ describes current state, architecture, data model, setup (Linux Mint), and the full improvement roadmap. data/orte.csv is gitignored for now. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
venv/
|
||||
*.egg-info/
|
||||
|
||||
# Generated map (build artefact, regenerated on demand)
|
||||
data/karte.html
|
||||
|
||||
# Real data — kept out of version control for now (see docs/data-model.md)
|
||||
data/orte.csv
|
||||
|
||||
# Local backups and logs
|
||||
data/backups/
|
||||
data/app.log
|
||||
data/app.log.*
|
||||
|
||||
# Editor / OS
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
@@ -0,0 +1,34 @@
|
||||
# DRK Blutspende – Arbeitsorte: Dokumentation
|
||||
|
||||
Developer/maintainer documentation for the **Arbeitsorte-Logger**, a small
|
||||
desktop app that records blood-drive work assignments (date + city + postal
|
||||
code) and plots them on an interactive map.
|
||||
|
||||
> Context: built as a personal tool for a single user, running locally on a
|
||||
> **Linux Mint laptop**. Ease of use for a non-technical user is the primary
|
||||
> design constraint. There is no server, no multi-user story, and no network
|
||||
> dependency beyond geocoding and the map tiles.
|
||||
|
||||
## Documents
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| [overview.md](overview.md) | What the app does, feature by feature |
|
||||
| [architecture.md](architecture.md) | Code structure, threading model, data flow |
|
||||
| [data-model.md](data-model.md) | The `orte.csv` schema and how it is read/written |
|
||||
| [setup.md](setup.md) | Install & run on a fresh Linux Mint machine, plus a desktop launcher |
|
||||
| [improvements.md](improvements.md) | Prioritized review findings and suggested changes |
|
||||
| [changelog.md](changelog.md) | What has changed, newest first |
|
||||
| [dev-notes.md](dev-notes.md) | Assumptions that turned out wrong — read before editing |
|
||||
|
||||
## At a glance
|
||||
|
||||
- **Language / stack:** Python 3, Tkinter via [`ttkbootstrap`](https://ttkbootstrap.readthedocs.io/)
|
||||
- **Single file:** [`app.py`](../app.py) (~760 lines)
|
||||
- **Storage:** one CSV file, `data/orte.csv`
|
||||
- **Map:** generated on demand as `data/karte.html` with [Folium](https://python-visualization.github.io/folium/) (Leaflet), opened in the default browser
|
||||
- **Geocoding:** OpenStreetMap Nominatim via [`geopy`](https://geopy.readthedocs.io/), rate-limited to 1 request/second on a background thread
|
||||
- **UI language:** German
|
||||
- **Persistence:** atomic CSV writes + rotating backups in `data/backups/`; log at `data/app.log`
|
||||
- **Tests:** non-GUI smoke checks only (not yet a committed `tests/` suite)
|
||||
- **Version control:** git initialized 2026-09-07
|
||||
@@ -0,0 +1,89 @@
|
||||
# Architecture
|
||||
|
||||
Everything lives in [`app.py`](../app.py). There is no package structure.
|
||||
|
||||
## Components
|
||||
|
||||
| Class / function | Responsibility |
|
||||
|------------------|----------------|
|
||||
| `DataStore` | Owns `orte.csv`. Loads it into a pandas `DataFrame`, exposes read helpers (`get_map_data`, `get_autocomplete_strings`, `total_count`, `find_duplicates`) and write helpers (`append_rows`, `update_row`, `delete_rows`). All writes go through `_save_csv` → rotating backup + atomic write. All columns are read as strings. Falls back to an empty frame (or the newest backup) if `orte.csv` is missing/empty/corrupt. |
|
||||
| `geocode_city()` | Pure function: `(city, plz) -> (lat, lon) | None`. Tries several Nominatim queries with a configurable regional bias (`GEOCODE_REGIONS`). Raises `GeocodingUnavailable` if *every* query failed on a service/network error (vs. simply not finding the place). |
|
||||
| `GeocoderWorker` | A `threading.Thread` (daemon). Holds a FIFO list of `(row_id, city, plz, callback)` items guarded by a lock, woken by an `Event`. Processes one item per second, calls `geocode_city`, then invokes `callback` **from the worker thread**. Each item is wrapped in `try/except` so one failure never kills the thread; after `SERVICE_ERROR_THRESHOLD` consecutive service errors it fires the optional `on_service_error` callback. |
|
||||
| `parse_date()` / `parse_coord()` / helpers | Module-level pure functions for normalizing user input (date → ISO, coordinate strings → float, `"City (PLZ)"` splitting). Shared by the entry tab and `EditDialog`. |
|
||||
| `generate_map()` | Builds `karte.html` from `DataStore.get_map_data()` with Folium. |
|
||||
| `EditDialog` | `tk.Toplevel` modal dialog for editing one row. Returns its result via `self.result`. |
|
||||
| `App` | `ttkbootstrap.Window`. Builds the UI, owns the `DataStore`, the `GeocoderWorker`, and the in-memory `_queue` list for Tab 1. |
|
||||
|
||||
## Data flow
|
||||
|
||||
### Adding entries
|
||||
```
|
||||
user types -> _on_add_row()
|
||||
-> append dict to self._queue
|
||||
-> insert row into Tab-1 Treeview (coords = "⏳ wird gesucht…")
|
||||
-> geocoder.enqueue(row_id, city, plz, self._geocode_done)
|
||||
|
||||
GeocoderWorker thread (1/sec):
|
||||
coords = geocode_city(...)
|
||||
-> self._geocode_done(row_id, coords) [worker thread]
|
||||
-> self.after(0, self._apply_geocode_result, ...) [hop to UI thread]
|
||||
-> mutate self._queue entry + update Treeview cell
|
||||
|
||||
user clicks "Alle speichern" -> _on_save_all()
|
||||
-> DataStore.find_duplicates() (status-bar hint only)
|
||||
-> DataStore.append_rows() -> CSV append + in-memory concat
|
||||
-> clear queue, refresh Tab 2, update autocomplete
|
||||
```
|
||||
|
||||
### Editing / deleting
|
||||
`EditDialog` -> `DataStore.update_row(df_idx, values)` or
|
||||
`DataStore.delete_rows(indices)` -> full `df.to_csv()` rewrite -> refresh.
|
||||
|
||||
## Threading model
|
||||
|
||||
- Tkinter is single-threaded; all widget access must happen on the main thread.
|
||||
- The geocoder runs off-thread so the UI never blocks for the 1-second-per-item
|
||||
rate limit.
|
||||
- The **callback is invoked on the worker thread**, and every callback in
|
||||
`App` immediately does `self.after(0, ...)` to marshal back onto the UI
|
||||
thread. This is the load-bearing convention — any new callback must follow it.
|
||||
(`on_service_error` does the same.)
|
||||
- The worker catches every exception per item, so a bug in a callback or a
|
||||
network error logs a traceback but never stops the queue.
|
||||
- On window close, `_on_close()` calls `geocoder.stop()` (sets a flag + wakes
|
||||
the event) and then `destroy()`. The thread is a daemon, so a missed stop
|
||||
won't hang the process.
|
||||
|
||||
## Persistence model
|
||||
|
||||
- `orte.csv` is the single source of truth. It is read once at startup and then
|
||||
kept in sync in memory.
|
||||
- **Every** write (append, update, delete) rebuilds the full `DataFrame` and
|
||||
goes through `DataStore._save_csv`:
|
||||
1. `_make_backup()` – copy the current file to
|
||||
`data/backups/orte-YYYYMMDD-HHMMSS.csv` (skipped if byte-identical to the
|
||||
newest backup); prune to `MAX_BACKUPS` (20).
|
||||
2. `_write_atomic()` – write to `orte.csv.tmp`, `fsync`, then `os.replace()`
|
||||
onto `orte.csv` (atomic on POSIX). A crash mid-write leaves the old file
|
||||
intact.
|
||||
- A backup is also taken once at startup.
|
||||
- Still **no file locking**: if the CSV is edited in another program while the
|
||||
app is open, the next in-app save overwrites those changes (but the pre-write
|
||||
backup captures them).
|
||||
|
||||
## Logging
|
||||
|
||||
`_setup_logging()` (called from `__main__`) attaches a `RotatingFileHandler` to
|
||||
`data/app.log` (512 KB × 3). Logger name `arbeitsorte`, module-level `log`.
|
||||
Records saves, map generation, geocoding failures, and any uncaught exception.
|
||||
This is the file to ask for when diagnosing a problem on the user's laptop.
|
||||
|
||||
## External dependencies at runtime
|
||||
|
||||
| Dependency | Needed for | Offline behaviour |
|
||||
|------------|-----------|-------------------|
|
||||
| Nominatim (OSM) | geocoding new/edited entries | entries save with blank coords, shown as `⚠ nicht gefunden` |
|
||||
| CDN (jsdelivr, jquery, cloudflare) | rendering `karte.html` | map page loads blank / unstyled |
|
||||
| CartoDB tiles | map background | grey tiles |
|
||||
|
||||
Existing entries that already have coordinates do **not** need the network.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Changelog
|
||||
|
||||
Notable changes to the app. Newest first.
|
||||
|
||||
## 2026-09-07 — data safety, robustness, easier input
|
||||
|
||||
Addressed the top three findings from [improvements.md](improvements.md).
|
||||
|
||||
### Data safety (`DataStore`)
|
||||
- All writes (append / update / delete) now rebuild the full DataFrame and go
|
||||
through `_save_csv`: **rotating backup** to `data/backups/` (startup + before
|
||||
every change, keep 20, skip if unchanged) then **atomic write**
|
||||
(`tmp` + `fsync` + `os.replace`).
|
||||
- `orte.csv` missing / empty (`EmptyDataError`) / unreadable now falls back to an
|
||||
empty table or the newest backup instead of crashing.
|
||||
|
||||
### Geocoder robustness (`GeocoderWorker`, `geocode_city`)
|
||||
- Each queue item runs inside `try/except`; a failing lookup or callback logs a
|
||||
traceback and the worker keeps going (previously an unexpected error killed the
|
||||
thread and silently froze all further geocoding).
|
||||
- `geocode_city` raises `GeocodingUnavailable` when every query failed on a
|
||||
service/network error. After 3 in a row the app shows a one-time
|
||||
"Standortdienst nicht erreichbar" dialog; entries still save without
|
||||
coordinates.
|
||||
|
||||
### Easier, safer input
|
||||
- Date fields (entry form + edit dialog) are now `ttkbootstrap.DateEntry`
|
||||
calendar pickers, validated/normalized to ISO by `parse_date()` (also accepts
|
||||
`TT.MM.JJJJ`). Invalid dates are rejected with a dialog.
|
||||
- Edit dialog has manual **Breitengrad / Längengrad** fields for fixing
|
||||
coordinates by hand.
|
||||
- Tab 2: rows without coordinates shown in red, "Nur ohne Koordinaten" filter,
|
||||
count in the hint line, right-click **Koordinaten suchen**.
|
||||
|
||||
### Housekeeping
|
||||
- `git init` + `.gitignore`.
|
||||
- Rotating log file at `data/app.log` (`_setup_logging`).
|
||||
- Shared helpers `filter_locations` / `split_city_plz` / `parse_coord` replace
|
||||
duplicated combobox code.
|
||||
- Config constants: `GEOCODE_REGIONS`, `MAP_DEFAULT_CENTER`, `MAP_DEFAULT_ZOOM`,
|
||||
`MAX_BACKUPS`.
|
||||
- `generate_map` fits the view to the markers.
|
||||
|
||||
### Verification
|
||||
Non-GUI smoke tests (helpers, `DataStore` backup/atomic/recovery, geocoder
|
||||
resilience) pass. **The GUI could not be run on the development Mac** — see
|
||||
[dev-notes.md](dev-notes.md).
|
||||
@@ -0,0 +1,61 @@
|
||||
# Data model
|
||||
|
||||
## `data/orte.csv`
|
||||
|
||||
Plain CSV, comma-separated, UTF-8, with a header row. Created automatically with
|
||||
just the header if it does not exist.
|
||||
|
||||
| Column | Type on disk | Meaning | Notes |
|
||||
|--------|--------------|---------|-------|
|
||||
| `date` | string | Assignment date | Always stored as `YYYY-MM-DD`. Input is validated and normalized by `parse_date()` (accepts `YYYY-MM-DD`, `DD.MM.YYYY`, `DD.MM.YY`, `YYYY/MM/DD`); invalid input is rejected before saving. Rows written before this change may still hold non-ISO strings. |
|
||||
| `city` | string | Place name | Free text. Used for autocomplete and duplicate detection (trimmed, case-insensitive). |
|
||||
| `postal_code` | string | German PLZ | Optional. Kept as a string so leading zeros survive. Regex elsewhere accepts 4–5 digits. |
|
||||
| `lat` | string | Latitude | Blank until geocoded. Rounded to 5 dp. Parsed with `pd.to_numeric(errors="coerce")` when building the map. |
|
||||
| `lon` | string | Longitude | As above. |
|
||||
|
||||
Every column is read as a string (`dtype={"postal_code": str, "lat": str, "lon": str}`
|
||||
plus `city`/`date` default object). Numeric conversion happens only where needed.
|
||||
|
||||
### Row identity
|
||||
|
||||
There is **no ID column**. Rows are identified by their pandas `DataFrame`
|
||||
index, which is reset to `0..n-1` after every delete. UI tables use that index
|
||||
as the Treeview `iid`.
|
||||
|
||||
Implication: a geocode callback that is in flight while the user deletes a
|
||||
different row can land on the wrong row, because indices shift. `update_row` /
|
||||
`_apply_edit_geocode` guard against a *missing* index but not against a
|
||||
*reused* one. Still open — see [improvements.md](improvements.md#7-stable-row-identity-).
|
||||
The manual lat/lon fields in the edit dialog give a way to correct any row
|
||||
that ends up wrong.
|
||||
|
||||
### Duplicate detection
|
||||
|
||||
`find_duplicates` flags a queued row when an existing row matches on all three
|
||||
of: `date` (string-equal), `city` (trimmed, lower-cased), `postal_code`
|
||||
(trimmed). It only produces a status-bar hint; duplicates are still written.
|
||||
|
||||
### Map aggregation
|
||||
|
||||
`get_map_data` groups by `(city, lat, lon)` and counts rows. Two entries for the
|
||||
same city with different coordinates (e.g. Karlsruhe geocoded once to `76133`
|
||||
and once to `76185`) produce **two separate markers**.
|
||||
|
||||
## `data/karte.html`
|
||||
|
||||
Regenerated from scratch every time the user opens the map. Safe to delete; it
|
||||
is a build artefact, not data. Should be git-ignored if the project is ever put
|
||||
under version control.
|
||||
|
||||
## `data/backups/`
|
||||
|
||||
Automatic timestamped copies of `orte.csv`, named `orte-YYYYMMDD-HHMMSS.csv`.
|
||||
One is written at startup and one before every change (skipped when nothing
|
||||
changed since the last backup). The newest 20 are kept; older ones are pruned.
|
||||
Git-ignored. To restore, copy a backup over `data/orte.csv` while the app is
|
||||
closed.
|
||||
|
||||
## `data/app.log`
|
||||
|
||||
Rotating log file (512 KB × 3 generations). Git-ignored. Records saves, map
|
||||
generation, geocoding failures, and uncaught exceptions.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Dev notes — assumptions that turned out wrong
|
||||
|
||||
Running list so the same mistakes aren't repeated. Add to it whenever reality
|
||||
contradicts an assumption.
|
||||
|
||||
## The GUI does not run on the development Mac (2026-09-07)
|
||||
|
||||
The macOS **CommandLineTools** Python 3.9 (`/Library/Developer/CommandLineTools/
|
||||
.../python3.9`) ships **Tk 8.5.9**. `ttkbootstrap` 1.10.1 requires **Tk 8.6+**.
|
||||
|
||||
Symptoms when trying to launch `App()` there:
|
||||
- `_tkinter.TclError: couldn't recognize image data` (ttkbootstrap's window icon
|
||||
is a PNG; Tk 8.5 can't decode it), and
|
||||
- `_tkinter.TclError: unknown option "-style"` on `ttk.Scrollbar`.
|
||||
|
||||
Consequences:
|
||||
- `app.py` **cannot be smoke-tested through the GUI on this machine.** Logic is
|
||||
covered by a non-GUI script (constructs `DataStore`, exercises helpers and the
|
||||
worker with monkey-patched paths).
|
||||
- The **Linux Mint target is unaffected** — its system Tk is 8.6.
|
||||
- If GUI testing on macOS is ever needed, install a Homebrew
|
||||
`python-tk@3.x` / `tcl-tk` combo, or use `pyenv` with `--with-tcltk`.
|
||||
|
||||
## `pandas` reads everything as strings by design
|
||||
|
||||
`DataStore._load` uses `dtype={...: str}` for `postal_code`, `lat`, `lon`.
|
||||
Numeric parsing is deliberately deferred to `get_map_data` (`pd.to_numeric(...,
|
||||
errors="coerce")`). Don't "fix" columns to float on load — blank coordinates and
|
||||
leading-zero PLZ both depend on the string representation.
|
||||
|
||||
## Nominatim query order matters
|
||||
|
||||
`geocode_city` returns the **first** hit, trying PLZ-qualified first, then each
|
||||
region in `GEOCODE_REGIONS`, then a bare `", Germany"`. Reordering changes which
|
||||
coordinates ambiguous town names resolve to.
|
||||
@@ -0,0 +1,196 @@
|
||||
# Improvement roadmap
|
||||
|
||||
Review of the current code, prioritized for the actual deployment: **one
|
||||
non-technical user, Linux Mint laptop, offline some of the time, no one else to
|
||||
fix things when they break.** That ordering puts *data safety* and *robustness*
|
||||
above features.
|
||||
|
||||
Priorities: 🔴 do first · 🟡 worth doing · 🟢 nice to have
|
||||
|
||||
**Status (2026-09-07):** items 1, 2 and 3 are implemented. Items 6 and 10 are
|
||||
partly done (logging added; shared autocomplete helpers extracted; map now
|
||||
`fit_bounds`; region list is now a constant). See
|
||||
[changelog.md](changelog.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Data safety 🔴 — ✅ done
|
||||
|
||||
Implemented: atomic writes (`tmp` + `fsync` + `os.replace`), rotating backups in
|
||||
`data/backups/` (startup + before every change, keep 20), `EmptyDataError` /
|
||||
corrupt-file fallback to an empty frame or the newest backup, and all writes
|
||||
(including append) routed through `_save_csv`. A "restore from backup" button in
|
||||
the UI is still open; for now it is a manual file copy.
|
||||
|
||||
<details><summary>Original finding</summary>
|
||||
|
||||
`data/orte.csv` is the only copy of the data and there is no safety net.
|
||||
|
||||
- **No backups.** A bad edit, a botched delete, or a disk hiccup loses history
|
||||
permanently. Delete is explicitly irreversible and only guarded by a yes/no
|
||||
dialog.
|
||||
- **Non-atomic full rewrites.** `update_row` / `delete_rows` do
|
||||
`df.to_csv(CSV_PATH)` directly. A crash or power loss mid-write can leave a
|
||||
truncated file.
|
||||
- **External-edit clobber.** The CSV is read once at startup. If it is opened in
|
||||
a spreadsheet and changed while the app runs, the next save silently
|
||||
overwrites those changes.
|
||||
- **Empty-file crash.** If `orte.csv` exists but is 0 bytes (e.g. an interrupted
|
||||
write), `pd.read_csv` raises `EmptyDataError` and the app won't start.
|
||||
|
||||
**Suggested changes**
|
||||
|
||||
- Write every full rewrite to a temp file in the same directory, then
|
||||
`os.replace()` it into place (atomic on POSIX).
|
||||
- On startup, and before each destructive write, copy the current file to
|
||||
`data/backups/orte-YYYYMMDD-HHMMSS.csv`; keep the last ~20 and prune the rest.
|
||||
- Add a "Backup jetzt erstellen" / "Aus Backup wiederherstellen" pair in Tab 2,
|
||||
or at least open the backups folder from a menu.
|
||||
- Handle `EmptyDataError` / missing columns by falling back to an empty frame
|
||||
and logging a warning.
|
||||
|
||||
</details>
|
||||
|
||||
## 2. Geocoder thread robustness 🔴 — ✅ done
|
||||
|
||||
Implemented: every queue item is wrapped in `try/except Exception` (both the
|
||||
geocode call and the callback), failures are logged and the loop continues.
|
||||
`geocode_city` now raises `GeocodingUnavailable` when every attempt hit a
|
||||
service/network error, and after 3 consecutive such failures the app shows a
|
||||
one-time "Standortdienst nicht erreichbar" dialog. `RateLimiter` was not
|
||||
adopted; the manual `sleep(1)` stays.
|
||||
|
||||
<details><summary>Original finding</summary>
|
||||
|
||||
`GeocoderWorker.run` has no error boundary around the per-item work. If
|
||||
`geocode_city` or a callback raises anything other than
|
||||
`GeocoderTimedOut` / `GeocoderServiceError` (a network stack error, a bug in a
|
||||
callback, `GeocoderQuotaExceeded`), the **worker thread dies** and every
|
||||
subsequent entry stays stuck on `⏳ wird gesucht…` with no error shown. The user
|
||||
has no way to know geocoding stopped working.
|
||||
|
||||
**Suggested changes**
|
||||
|
||||
- Wrap each item's processing in `try/except Exception`, log the traceback, mark
|
||||
that row as failed, and keep the loop alive.
|
||||
- Consider `geopy.extra.rate_limiter.RateLimiter` (with `swallow_exceptions`)
|
||||
instead of the manual `sleep(1)`.
|
||||
- Surface a clear one-time status message when geocoding fails repeatedly
|
||||
("Standortdienst nicht erreichbar – bitte Internetverbindung prüfen").
|
||||
|
||||
</details>
|
||||
|
||||
## 3. Easier, safer input 🔴 — ✅ done
|
||||
|
||||
Implemented: the date field is now `ttkbootstrap.DateEntry` (calendar picker) in
|
||||
both the entry form and the edit dialog, with `parse_date()` validation on save
|
||||
(a bad date is rejected with a dialog). The edit dialog gained manual
|
||||
**Breitengrad / Längengrad** fields. Tab 2 shows rows without coordinates in red,
|
||||
has a "Nur ohne Koordinaten" filter, a hint line with the count, and a
|
||||
right-click **Koordinaten suchen** action.
|
||||
|
||||
<details><summary>Original finding</summary>
|
||||
|
||||
For the target user the free-text date field is the biggest usability risk.
|
||||
|
||||
- **Date field is unvalidated free text** in both the entry form and
|
||||
`EditDialog`. A typo like `2026-13-05` or `05.02.2026` is stored verbatim,
|
||||
breaks date sorting, and never appears correctly on the timeline.
|
||||
- **No recourse when geocoding fails.** A row that comes back
|
||||
`⚠ nicht gefunden` saves with blank coordinates and then just silently never
|
||||
shows on the map. There is no way to type coordinates by hand or retry from
|
||||
Tab 2.
|
||||
|
||||
**Suggested changes**
|
||||
|
||||
- Replace the date `Entry` with `ttkbootstrap.DateEntry` (calendar picker) in
|
||||
both places. If keeping free text, validate on add/save and refuse invalid
|
||||
dates with a clear message.
|
||||
- In `EditDialog`, add optional `lat` / `lon` fields so a location can be fixed
|
||||
manually.
|
||||
- In Tab 2, highlight rows with missing coordinates (e.g. red text) and add a
|
||||
filter / right-click "Koordinaten suchen" so failed geocodes are visible and
|
||||
fixable after the fact.
|
||||
|
||||
</details>
|
||||
|
||||
## 4. Put it under version control 🟡 — partly done
|
||||
|
||||
`git init` done and `.gitignore` added (covers `.venv/`, `__pycache__/`,
|
||||
`data/karte.html`, `data/backups/`, `data/app.log`). Still open: the initial
|
||||
commit (left to Patrick), and the decision on committing `data/orte.csv` vs. an
|
||||
example file.
|
||||
- Decide on `data/orte.csv`: for a personal tool it's reasonable to commit it,
|
||||
or commit a small `data/orte.example.csv` and ignore the real one.
|
||||
- This `docs/` folder.
|
||||
|
||||
Gives you history, a rollback path, and a clean way to push updates to the
|
||||
laptop (`git pull`).
|
||||
|
||||
## 5. Packaging & distribution 🟡
|
||||
|
||||
Today install is: `apt install python3-tk`, create a venv, `pip install`. That's
|
||||
a one-time terminal session, which is acceptable but fragile (loose version
|
||||
pins mean a future `pip install` could pull an incompatible pandas/folium).
|
||||
|
||||
**Suggested changes**
|
||||
|
||||
- Pin all four dependencies to exact versions and regenerate deliberately.
|
||||
- Ship the `run.sh` + `.desktop` launcher from [setup.md](setup.md) in the repo.
|
||||
- Optional: build a [PyInstaller](https://pyinstaller.org/) one-file bundle on a
|
||||
matching Linux box. It bundles Python, Tk, and all deps, so install becomes
|
||||
"copy one file + double-click" with no apt/venv step. Trade-off: you build it,
|
||||
and rebuild on dependency updates.
|
||||
|
||||
## 6. Diagnostics / logging 🟡 — ✅ done
|
||||
|
||||
`_setup_logging()` writes a `RotatingFileHandler` to `data/app.log`
|
||||
(512 KB × 3). Saves, map generation, geocode failures and uncaught exceptions
|
||||
are logged. `_on_open_map` now logs and narrows its handling. Open: no in-app
|
||||
"open log folder" shortcut yet.
|
||||
|
||||
## 7. Stable row identity 🟡
|
||||
|
||||
Rows are keyed by the pandas index, which is renumbered after every delete. An
|
||||
in-flight geocode callback for one row can land on another row if the user
|
||||
deletes something in between. `_apply_edit_geocode` guards against a *deleted*
|
||||
index but not a *reused* one.
|
||||
|
||||
**Suggested change:** add an immutable `id` column (uuid or incrementing int)
|
||||
written to the CSV, and match callbacks on that instead of the positional index.
|
||||
|
||||
## 8. Duplicate handling 🟡
|
||||
|
||||
`_on_save_all` detects likely duplicates but saves them anyway with only a
|
||||
transient status-bar line the user will probably miss. Consider a modal
|
||||
"Diese Einträge existieren schon – trotzdem speichern?" with a per-row choice,
|
||||
or at least skip exact duplicates by default.
|
||||
|
||||
## 9. Offline map 🟢
|
||||
|
||||
`karte.html` loads Leaflet/jQuery/Bootstrap from CDNs and tiles from CartoDB, so
|
||||
the map is blank without internet even though all the data is local. If offline
|
||||
use matters, vendor the Leaflet JS/CSS into `data/` and post-process the Folium
|
||||
output to point at local files (tiles would still need caching or an offline
|
||||
tile pack — larger effort).
|
||||
|
||||
## 10. Small code-quality items 🟢 — partly done
|
||||
|
||||
- ✅ Shared autocomplete helpers (`filter_locations`, `split_city_plz`)
|
||||
extracted; `EditDialog` and `App` both use them.
|
||||
- ✅ Region list is now the `GEOCODE_REGIONS` constant; map centre/zoom are
|
||||
`MAP_DEFAULT_CENTER` / `MAP_DEFAULT_ZOOM`.
|
||||
- ✅ `generate_map` now `fit_bounds` to the markers.
|
||||
- ⬜ Add a `tests/` folder with pytest coverage for `DataStore` (append / update
|
||||
/ delete / `find_duplicates`) and the input helpers. A non-GUI smoke script
|
||||
exists (used during development) but is not in the repo — worth formalizing.
|
||||
- ⬜ Add `ruff` / `mypy` config.
|
||||
|
||||
---
|
||||
|
||||
## Suggested order of remaining work
|
||||
|
||||
1. Initial git commit (item 4), then decide on committing `orte.csv`.
|
||||
2. Pinned deps + `run.sh` / `.desktop` launcher in the repo (item 5).
|
||||
3. Stable `id` column (item 7) and a modal duplicate prompt (item 8).
|
||||
4. Formalize tests (item 10); offline map (item 9) only if offline use becomes real.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Overview – what the app does
|
||||
|
||||
The app is a single window with a status bar at the bottom and two tabs.
|
||||
|
||||
## Tab 1 – "Neuer Eintrag" (new entry)
|
||||
|
||||
Workflow: build up a **queue** of entries, let them geocode in the background,
|
||||
then save the whole batch at once.
|
||||
|
||||
1. **Entry form** – date (a **calendar picker**, `ttkbootstrap.DateEntry`,
|
||||
pre-filled with today; typed input is accepted in `JJJJ-MM-TT` or
|
||||
`TT.MM.JJJJ` and **validated** – an invalid date is rejected with a dialog),
|
||||
city (autocomplete combobox), postal code (optional). Pressing `Return` in any
|
||||
field, or the **+ Hinzufügen** button, adds the row to the queue.
|
||||
2. **Queue table** – shows date, city, PLZ, and a live coordinate column that
|
||||
updates from `⏳ wird gesucht…` to either `lat / lon` or `⚠ nicht gefunden`
|
||||
as the background geocoder works through the queue.
|
||||
- Left-click the `✕` cell to remove a row.
|
||||
- Right-click a row for *Löschen* / *Koordinaten erneut suchen*.
|
||||
3. **Action bar**
|
||||
- **Alle speichern** – appends every queued row to `orte.csv`. If a row looks
|
||||
like a duplicate of an existing entry (same date + city + PLZ) a hint is
|
||||
shown in the status bar, but the row is **still saved**.
|
||||
- **Leeren** – discards the queue without saving.
|
||||
- **Karte öffnen** – regenerates `karte.html` and opens it in the browser.
|
||||
|
||||
## Tab 2 – "Einträge verwalten" (manage entries)
|
||||
|
||||
A table view of everything in `orte.csv`.
|
||||
|
||||
- **Search box** – live filter across date, city, and PLZ (substring match).
|
||||
- **Sortable columns** – click a header to sort; clicking again reverses.
|
||||
Default sort is by date, newest first.
|
||||
- **"Nur ohne Koordinaten"** toggle – filters to entries that have no
|
||||
coordinates yet. Such rows are also shown in **red** in the full list, and the
|
||||
hint line reports how many there are.
|
||||
- **Edit** – double-click a row (or *Bearbeiten*) opens a modal dialog to change
|
||||
date (calendar picker, validated) / city / PLZ. Two ways to fix coordinates:
|
||||
a "Koordinaten automatisch neu suchen" toggle re-runs geocoding after saving,
|
||||
or the **Breitengrad / Längengrad** fields let you type them in by hand
|
||||
(both empty = no map marker).
|
||||
- **Right-click → Koordinaten suchen** – runs geocoding for that one row
|
||||
(handy for rows that failed the first time).
|
||||
- **Delete** – *Löschen* removes the selected row(s) after a confirmation
|
||||
dialog. This is **irreversible**, but a timestamped copy of the file is
|
||||
written to `data/backups/` before every change (see
|
||||
[data-model.md](data-model.md#backups)).
|
||||
- **Karte öffnen** – same as on Tab 1.
|
||||
|
||||
## The map (`karte.html`)
|
||||
|
||||
Generated by `generate_map()`:
|
||||
|
||||
- Base layer: `CartoDB positron`, initial view centred on `[49.0, 9.0]`,
|
||||
zoom 8 (roughly Baden-Württemberg).
|
||||
- One **`CircleMarker`** per unique `(city, lat, lon)` group. Radius scales
|
||||
linearly with the visit count for that location
|
||||
(`5 + count / max_count * 20` px), colour is DRK red (`#CC0000`).
|
||||
- Popup shows the city and the visit count; tooltip shows the city.
|
||||
- Rows with missing/blank coordinates are silently excluded.
|
||||
- If there are no usable coordinates at all, a `ValueError` is raised and shown
|
||||
in the status bar.
|
||||
|
||||
> The generated HTML pulls Leaflet, jQuery and Bootstrap from CDNs, so the map
|
||||
> needs an internet connection to render even though the data is local.
|
||||
|
||||
## Geocoding behaviour
|
||||
|
||||
`geocode_city(city, postal_code)` tries a series of queries in order and returns
|
||||
the first hit:
|
||||
|
||||
1. `"<PLZ> <city>, Germany"` (only if a PLZ was given)
|
||||
2. `"<city>, Baden-Württemberg, Germany"`
|
||||
3. `"<city>, Hessen, Germany"`
|
||||
4. `"<city>, Germany"`
|
||||
|
||||
The regional bias is the `GEOCODE_REGIONS` constant (`Baden-Württemberg`,
|
||||
`Hessen`) near the top of `app.py`. Results are rounded to five decimal places.
|
||||
Nominatim's usage policy (max 1 req/s, identifying `user_agent`) is respected by
|
||||
a manual `time.sleep(1)` in the worker loop.
|
||||
|
||||
If the geocoding service is unreachable (no internet), entries still save –
|
||||
without coordinates – and after a few consecutive failures a one-time dialog
|
||||
explains that the coordinates can be added later via *Bearbeiten*. The worker
|
||||
thread logs failures to `data/app.log` and keeps running.
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
# Setup & running
|
||||
|
||||
Target machine: **Linux Mint laptop**, single non-technical user. The goal is
|
||||
that day-to-day use is a **double-click**, with the terminal only needed once
|
||||
during install.
|
||||
|
||||
## 1. System packages
|
||||
|
||||
Tkinter is not bundled with the system Python on Mint and must be installed
|
||||
separately:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install python3-tk python3-venv python3-pip
|
||||
```
|
||||
|
||||
## 2. Get the code
|
||||
|
||||
Put the project folder somewhere stable, e.g. `~/Apps/DRK_Blutspende_Orte`.
|
||||
|
||||
## 3. Create a virtual environment and install dependencies
|
||||
|
||||
```bash
|
||||
cd ~/Apps/DRK_Blutspende_Orte
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install -r requirements.txt
|
||||
```
|
||||
|
||||
`requirements.txt`:
|
||||
|
||||
```
|
||||
ttkbootstrap==1.10.1
|
||||
geopy>=2.4.1
|
||||
folium>=0.17.0
|
||||
pandas>=2.2.0
|
||||
```
|
||||
|
||||
> Only `ttkbootstrap` is pinned exactly. For a machine you hand to someone else,
|
||||
> consider pinning all four (see [improvements.md](improvements.md#5-packaging--distribution)).
|
||||
|
||||
## 4. Run it
|
||||
|
||||
```bash
|
||||
.venv/bin/python app.py
|
||||
```
|
||||
|
||||
The window should open. `data/orte.csv` is created automatically on first run if
|
||||
it is missing.
|
||||
|
||||
## 5. Make it a double-click launcher
|
||||
|
||||
### Launcher script
|
||||
|
||||
Create `run.sh` in the project root:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
cd "$(dirname "$0")"
|
||||
exec .venv/bin/python app.py
|
||||
```
|
||||
|
||||
```bash
|
||||
chmod +x run.sh
|
||||
```
|
||||
|
||||
### Desktop entry
|
||||
|
||||
Create `~/.local/share/applications/drk-blutspende-orte.desktop`:
|
||||
|
||||
```ini
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=DRK Blutspende – Arbeitsorte
|
||||
Comment=Einsatzorte protokollieren und auf der Karte anzeigen
|
||||
Exec=/home/USER/Apps/DRK_Blutspende_Orte/run.sh
|
||||
Icon=/home/USER/Apps/DRK_Blutspende_Orte/icon.png
|
||||
Terminal=false
|
||||
Categories=Utility;
|
||||
```
|
||||
|
||||
Replace `USER` with the real username. Add any PNG as `icon.png` (the DRK logo
|
||||
works well). The entry then shows up in the Mint menu and can be pinned to the
|
||||
panel or the desktop.
|
||||
|
||||
## Python / Tk version
|
||||
|
||||
Python 3.9+ is fine (the code uses `from __future__ import annotations`). The
|
||||
system Python on current Mint releases is well above that.
|
||||
|
||||
`ttkbootstrap` needs **Tk 8.6 or newer**. Linux Mint's `python3-tk` provides
|
||||
that. The macOS CommandLineTools Python used during development ships Tk 8.5 and
|
||||
**cannot run the GUI** — see [dev-notes.md](dev-notes.md).
|
||||
|
||||
## Updating
|
||||
|
||||
```bash
|
||||
cd ~/Apps/DRK_Blutspende_Orte
|
||||
git pull # once the project is in git
|
||||
.venv/bin/pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Data location
|
||||
|
||||
All state is in `data/orte.csv` next to `app.py`. To back up the app, copy that
|
||||
one file. To move to a new laptop, copy the whole folder and redo steps 1 & 3.
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
ttkbootstrap==1.10.1
|
||||
geopy>=2.4.1
|
||||
folium>=0.17.0
|
||||
pandas>=2.2.0
|
||||
Reference in New Issue
Block a user