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:
2026-09-07 18:28:28 +02:00
commit 37c9642877
11 changed files with 1809 additions and 0 deletions
+34
View File
@@ -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
+89
View File
@@ -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.
+47
View File
@@ -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).
+61
View File
@@ -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 45 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.
+35
View File
@@ -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.
+196
View File
@@ -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.
+85
View File
@@ -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
View File
@@ -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.