Add in-app self-update via git (Hilfe menu)
New Updater class + "Hilfe" menubar: - "Nach Update suchen": git fetch + count of new upstream commits. - Quiet background check on start; surfaces via status bar + menu label. - Install: git merge --ff-only, pip install if requirements.txt changed, then restart via os.execv. data/ is git-ignored and untouched. - Fast-forward only; diverged history or offline -> clear message, no action. - Inert unless run from a git clone with git on PATH. Also: "Version…" menu item shows the installed commit. Verified against throwaway git repos (check / ff-update / diverged / no-op / non-clone) and via GUI build on Python 3.14 / Tk 9. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,8 @@ import math
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import webbrowser
|
import webbrowser
|
||||||
@@ -451,6 +453,90 @@ def generate_map(store: DataStore) -> Path:
|
|||||||
return MAP_PATH
|
return MAP_PATH
|
||||||
|
|
||||||
|
|
||||||
|
# ── Selbst-Update (git) ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateError(Exception):
|
||||||
|
"""Ein Schritt beim Aktualisieren ist fehlgeschlagen (Text ist benutzertauglich)."""
|
||||||
|
|
||||||
|
|
||||||
|
class Updater:
|
||||||
|
"""Aktualisiert die App per ``git`` – nur wenn sie aus einem Git-Klon läuft.
|
||||||
|
|
||||||
|
Fährt bewusst konservativ: es wird ausschließlich per *fast-forward* auf den
|
||||||
|
Stand des Servers gezogen. Lokale Änderungen oder abweichende Historie führen
|
||||||
|
zu einer klaren Fehlermeldung statt zu einem automatischen Merge.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base_dir: Path) -> None:
|
||||||
|
self.base_dir = base_dir
|
||||||
|
self.available = bool(shutil.which("git")) and (base_dir / ".git").is_dir()
|
||||||
|
|
||||||
|
def _git(self, *args: str, timeout: int = 30) -> subprocess.CompletedProcess:
|
||||||
|
return subprocess.run(
|
||||||
|
["git", "-C", str(self.base_dir), *args],
|
||||||
|
capture_output=True, text=True, timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _upstream_ref(self) -> str:
|
||||||
|
r = self._git("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
|
||||||
|
return r.stdout.strip() or "origin/main"
|
||||||
|
|
||||||
|
def current_version(self) -> str:
|
||||||
|
r = self._git("log", "-1", "--format=%h · %cs")
|
||||||
|
return r.stdout.strip() or "unbekannt"
|
||||||
|
|
||||||
|
def check(self) -> int:
|
||||||
|
"""Anzahl neuer Commits auf dem Server. Wirft ``UpdateError`` bei Problemen."""
|
||||||
|
f = self._git("fetch", "--quiet", timeout=60)
|
||||||
|
if f.returncode != 0:
|
||||||
|
raise UpdateError(f.stderr.strip() or "Server nicht erreichbar (git fetch).")
|
||||||
|
ref = self._upstream_ref()
|
||||||
|
c = self._git("rev-list", "--count", f"HEAD..{ref}")
|
||||||
|
if c.returncode != 0:
|
||||||
|
raise UpdateError(c.stderr.strip() or "Versionsvergleich fehlgeschlagen.")
|
||||||
|
return int(c.stdout.strip() or "0")
|
||||||
|
|
||||||
|
def update(self) -> bool:
|
||||||
|
"""Zieht den neuen Stand (fast-forward) und installiert ggf. neue Pakete.
|
||||||
|
|
||||||
|
Gibt ``True`` zurück, wenn tatsächlich etwas aktualisiert wurde.
|
||||||
|
"""
|
||||||
|
f = self._git("fetch", "--quiet", timeout=60)
|
||||||
|
if f.returncode != 0:
|
||||||
|
raise UpdateError(f.stderr.strip() or "Server nicht erreichbar (git fetch).")
|
||||||
|
ref = self._upstream_ref()
|
||||||
|
before = self._git("rev-parse", "HEAD").stdout.strip()
|
||||||
|
m = self._git("merge", "--ff-only", ref, timeout=60)
|
||||||
|
if m.returncode != 0:
|
||||||
|
raise UpdateError(
|
||||||
|
(m.stderr.strip() or "Automatisches Update nicht möglich.")
|
||||||
|
+ "\n\nBitte Patrick Bescheid geben."
|
||||||
|
)
|
||||||
|
after = self._git("rev-parse", "HEAD").stdout.strip()
|
||||||
|
if before == after:
|
||||||
|
return False
|
||||||
|
changed = self._git("diff", "--name-only", before, after).stdout.split()
|
||||||
|
if "requirements.txt" in changed:
|
||||||
|
self._install_requirements()
|
||||||
|
log.info("Update: %s → %s", before[:9], after[:9])
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _install_requirements(self) -> None:
|
||||||
|
req = self.base_dir / "requirements.txt"
|
||||||
|
if not req.exists():
|
||||||
|
return
|
||||||
|
r = subprocess.run(
|
||||||
|
[sys.executable, "-m", "pip", "install", "-r", str(req)],
|
||||||
|
capture_output=True, text=True, timeout=600,
|
||||||
|
)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise UpdateError(
|
||||||
|
"Neue Programmbibliotheken konnten nicht installiert werden:\n"
|
||||||
|
+ (r.stderr.strip()[-400:] or "pip-Fehler")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── Bearbeiten-Dialog ─────────────────────────────────────────────────────────
|
# ── Bearbeiten-Dialog ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -612,6 +698,9 @@ class App(ttk.Window):
|
|||||||
self.geocoder.start()
|
self.geocoder.start()
|
||||||
self._service_error_shown = False
|
self._service_error_shown = False
|
||||||
|
|
||||||
|
self.updater = Updater(BASE_DIR)
|
||||||
|
self._update_busy = False
|
||||||
|
|
||||||
self._queue: list[dict] = []
|
self._queue: list[dict] = []
|
||||||
self._next_id = 0
|
self._next_id = 0
|
||||||
self._status_after_id: str | None = None
|
self._status_after_id: str | None = None
|
||||||
@@ -623,6 +712,9 @@ class App(ttk.Window):
|
|||||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||||
log.info("App gestartet (%d Einträge).", self.store.total_count())
|
log.info("App gestartet (%d Einträge).", self.store.total_count())
|
||||||
|
|
||||||
|
if self.updater.available:
|
||||||
|
threading.Thread(target=self._auto_check_update, daemon=True).start()
|
||||||
|
|
||||||
# ── Aussehen ──────────────────────────────────────────────────────────────
|
# ── Aussehen ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _scale_fonts(self) -> None:
|
def _scale_fonts(self) -> None:
|
||||||
@@ -684,6 +776,150 @@ class App(ttk.Window):
|
|||||||
except OSError:
|
except OSError:
|
||||||
log.warning("Fensterposition konnte nicht gespeichert werden.")
|
log.warning("Fensterposition konnte nicht gespeichert werden.")
|
||||||
|
|
||||||
|
# ── Menüleiste & Selbst-Update ───────────────────────────────────────────
|
||||||
|
|
||||||
|
def _build_menubar(self) -> None:
|
||||||
|
menubar = tk.Menu(self)
|
||||||
|
help_menu = tk.Menu(menubar, tearoff=0)
|
||||||
|
if self.updater.available:
|
||||||
|
self._update_menu = help_menu
|
||||||
|
self._update_menu_index = 0
|
||||||
|
help_menu.add_command(
|
||||||
|
label="Nach Update suchen", command=lambda: self._check_update(manual=True)
|
||||||
|
)
|
||||||
|
help_menu.add_separator()
|
||||||
|
else:
|
||||||
|
self._update_menu = None
|
||||||
|
help_menu.add_command(label="Version…", command=self._show_version)
|
||||||
|
menubar.add_cascade(label="Hilfe", menu=help_menu)
|
||||||
|
self.configure(menu=menubar)
|
||||||
|
|
||||||
|
def _show_version(self) -> None:
|
||||||
|
if self.updater.available:
|
||||||
|
info = f"Installierte Version:\n{self.updater.current_version()}"
|
||||||
|
else:
|
||||||
|
info = "Version: unbekannt (kein Git-Klon – Update über das Menü nicht möglich)."
|
||||||
|
messagebox.showinfo("DRK Blutspende – Arbeitsorte", info, parent=self)
|
||||||
|
|
||||||
|
def _auto_check_update(self) -> None:
|
||||||
|
"""Stiller Hintergrund-Check beim Start – meldet sich nur, wenn es etwas gibt."""
|
||||||
|
time.sleep(2)
|
||||||
|
try:
|
||||||
|
n = self.updater.check()
|
||||||
|
except Exception as exc: # offline o. Ä. – nicht stören
|
||||||
|
log.info("Automatischer Update-Check übersprungen: %s", exc)
|
||||||
|
return
|
||||||
|
if n > 0:
|
||||||
|
self.after(0, lambda: self._on_update_available(n, manual=False))
|
||||||
|
|
||||||
|
def _check_update(self, *, manual: bool) -> None:
|
||||||
|
if self._update_busy:
|
||||||
|
return
|
||||||
|
if not self.updater.available:
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Update", "Diese Installation kann sich nicht selbst aktualisieren.",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self._update_busy = True
|
||||||
|
self._set_status("Suche nach Updates…", sticky=True)
|
||||||
|
|
||||||
|
def work() -> None:
|
||||||
|
try:
|
||||||
|
n = self.updater.check()
|
||||||
|
err = None
|
||||||
|
except Exception as exc:
|
||||||
|
n, err = 0, str(exc)
|
||||||
|
self.after(0, self._on_update_checked, n, err, manual)
|
||||||
|
|
||||||
|
threading.Thread(target=work, daemon=True).start()
|
||||||
|
|
||||||
|
def _on_update_checked(self, n: int, err: str | None, manual: bool) -> None:
|
||||||
|
self._update_busy = False
|
||||||
|
if err is not None:
|
||||||
|
self._set_status("Update-Prüfung fehlgeschlagen.", "warning")
|
||||||
|
if manual:
|
||||||
|
messagebox.showwarning(
|
||||||
|
"Update",
|
||||||
|
f"Es konnte nicht nach Updates gesucht werden:\n\n{err}\n\n"
|
||||||
|
"Ist der Computer mit dem Internet verbunden?",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if n == 0:
|
||||||
|
self._set_status("Die App ist auf dem neuesten Stand.", "success")
|
||||||
|
if manual:
|
||||||
|
messagebox.showinfo("Update", "Die App ist auf dem neuesten Stand.", parent=self)
|
||||||
|
return
|
||||||
|
self._on_update_available(n, manual=manual)
|
||||||
|
|
||||||
|
def _on_update_available(self, n: int, *, manual: bool) -> None:
|
||||||
|
if self._update_menu is not None:
|
||||||
|
self._update_menu.entryconfigure(
|
||||||
|
self._update_menu_index, label=f"Update installieren ({n} verfügbar)"
|
||||||
|
)
|
||||||
|
self._set_status(
|
||||||
|
f"{n} Aktualisierung(en) verfügbar – Menü „Hilfe“ › Update installieren.",
|
||||||
|
"info", sticky=True,
|
||||||
|
)
|
||||||
|
count = f"{n} Aktualisierung" + ("en" if n != 1 else "")
|
||||||
|
if messagebox.askyesno(
|
||||||
|
"Update verfügbar",
|
||||||
|
f"Es gibt {count} für diese App.\n\n"
|
||||||
|
"Jetzt installieren? Die App wird dabei kurz neu gestartet.\n"
|
||||||
|
"Deine gespeicherten Einträge bleiben unverändert.",
|
||||||
|
parent=self,
|
||||||
|
):
|
||||||
|
self._do_update()
|
||||||
|
|
||||||
|
def _do_update(self) -> None:
|
||||||
|
if self._update_busy:
|
||||||
|
return
|
||||||
|
self._update_busy = True
|
||||||
|
self._set_status("App wird aktualisiert – bitte warten…", sticky=True)
|
||||||
|
|
||||||
|
def work() -> None:
|
||||||
|
try:
|
||||||
|
changed = self.updater.update()
|
||||||
|
err = None
|
||||||
|
except Exception as exc:
|
||||||
|
changed, err = False, str(exc)
|
||||||
|
self.after(0, self._on_update_done, changed, err)
|
||||||
|
|
||||||
|
threading.Thread(target=work, daemon=True).start()
|
||||||
|
|
||||||
|
def _on_update_done(self, changed: bool, err: str | None) -> None:
|
||||||
|
self._update_busy = False
|
||||||
|
if err is not None:
|
||||||
|
self._set_status("Update fehlgeschlagen.", "error")
|
||||||
|
messagebox.showerror("Update fehlgeschlagen", err, parent=self)
|
||||||
|
return
|
||||||
|
if not changed:
|
||||||
|
self._set_status("Nichts zu aktualisieren.", "info")
|
||||||
|
return
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Update installiert",
|
||||||
|
"Die Aktualisierung wurde installiert. Die App startet jetzt neu.",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
self._restart()
|
||||||
|
|
||||||
|
def _restart(self) -> None:
|
||||||
|
log.info("Neustart nach Update.")
|
||||||
|
self._save_window_state()
|
||||||
|
self.geocoder.stop()
|
||||||
|
try:
|
||||||
|
os.chdir(BASE_DIR)
|
||||||
|
os.execv(sys.executable, [sys.executable, str(Path(__file__).resolve())])
|
||||||
|
except OSError:
|
||||||
|
log.exception("Neustart fehlgeschlagen.")
|
||||||
|
messagebox.showwarning(
|
||||||
|
"Neustart nötig",
|
||||||
|
"Bitte die App einmal schließen und neu öffnen, "
|
||||||
|
"damit das Update wirksam wird.",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
|
||||||
def _on_geocode_service_error(self) -> None:
|
def _on_geocode_service_error(self) -> None:
|
||||||
"""Einmalige Warnung, wenn der Standortdienst wiederholt nicht erreichbar ist."""
|
"""Einmalige Warnung, wenn der Standortdienst wiederholt nicht erreichbar ist."""
|
||||||
self._set_status(
|
self._set_status(
|
||||||
@@ -704,6 +940,8 @@ class App(ttk.Window):
|
|||||||
# ── UI-Aufbau ─────────────────────────────────────────────────────────────
|
# ── UI-Aufbau ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _build_ui(self) -> None:
|
def _build_ui(self) -> None:
|
||||||
|
self._build_menubar()
|
||||||
|
|
||||||
# Kopfzeile mit DRK-Rot
|
# Kopfzeile mit DRK-Rot
|
||||||
header = ttk.Frame(self, bootstyle="danger", padding=(14, 8))
|
header = ttk.Frame(self, bootstyle="danger", padding=(14, 8))
|
||||||
header.pack(fill=X, side=TOP)
|
header.pack(fill=X, side=TOP)
|
||||||
|
|||||||
+1
-1
@@ -31,4 +31,4 @@ code) and plots them on an interactive map.
|
|||||||
- **UI language:** German; DRK-red header, enlarged font, colour-coded status bar, window position remembered
|
- **UI language:** German; DRK-red header, enlarged font, colour-coded status bar, window position remembered
|
||||||
- **Persistence:** atomic CSV writes + rotating backups in `data/backups/`; log at `data/app.log`
|
- **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)
|
- **Tests:** non-GUI smoke checks only (not yet a committed `tests/` suite)
|
||||||
- **Version control:** git initialized 2026-09-07
|
- **Version control:** git initialized 2026-09-07; in-app self-update (`git` fast-forward + restart) via the Hilfe menu
|
||||||
|
|||||||
+29
-1
@@ -12,7 +12,8 @@ Everything lives in [`app.py`](../app.py). There is no package structure.
|
|||||||
| `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`. |
|
| `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. |
|
| `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`. |
|
| `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. |
|
| `Updater` | Wraps `git` for self-update. `available` is true only from a git clone with `git` on `PATH`. `check()` → new-commit count (`fetch` + `rev-list`); `update()` → `fetch` + `merge --ff-only` + conditional `pip install`. All git calls are `subprocess.run` with timeouts. Raises `UpdateError` (user-facing German text) on any failure. |
|
||||||
|
| `App` | `ttkbootstrap.Window`. Builds the UI + menubar, owns the `DataStore`, the `GeocoderWorker`, the `Updater`, and the in-memory `_queue` list for Tab 1. |
|
||||||
|
|
||||||
## Data flow
|
## Data flow
|
||||||
|
|
||||||
@@ -71,6 +72,33 @@ user clicks "Alle speichern" -> _on_save_all()
|
|||||||
app is open, the next in-app save overwrites those changes (but the pre-write
|
app is open, the next in-app save overwrites those changes (but the pre-write
|
||||||
backup captures them).
|
backup captures them).
|
||||||
|
|
||||||
|
## Self-update flow
|
||||||
|
|
||||||
|
```
|
||||||
|
App start ─► daemon thread: sleep 2s ─► Updater.check()
|
||||||
|
(fetch + count) └─ on error: log only, stay quiet
|
||||||
|
│
|
||||||
|
N > 0 ─► self.after(0, …) ─► status hint + menu label
|
||||||
|
+ "Update verfügbar?" dialog
|
||||||
|
|
||||||
|
Hilfe ▸ Nach Update suchen ─► worker thread ─► Updater.check()
|
||||||
|
│
|
||||||
|
error ─► warning dialog (offline?)
|
||||||
|
N = 0 ─► "aktuell" dialog
|
||||||
|
N > 0 ─► "jetzt installieren?" dialog
|
||||||
|
|
||||||
|
install ─► worker thread ─► Updater.update() (fetch, ff-only merge, pip)
|
||||||
|
success + changed ─► info dialog ─► _restart()
|
||||||
|
save window state,
|
||||||
|
stop geocoder,
|
||||||
|
os.chdir(BASE_DIR),
|
||||||
|
os.execv(python, [python, app.py])
|
||||||
|
ff-only fails / pip fails ─► error dialog, no restart
|
||||||
|
```
|
||||||
|
|
||||||
|
Everything network- or subprocess-bound runs off the UI thread; results are
|
||||||
|
marshalled back with `self.after(0, …)` (same rule as the geocoder).
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
`_setup_logging()` (called from `__main__`) attaches a `RotatingFileHandler` to
|
`_setup_logging()` (called from `__main__`) attaches a `RotatingFileHandler` to
|
||||||
|
|||||||
@@ -2,6 +2,26 @@
|
|||||||
|
|
||||||
Notable changes to the app. Newest first.
|
Notable changes to the app. Newest first.
|
||||||
|
|
||||||
|
## 2026-09-07 — built-in self-update
|
||||||
|
|
||||||
|
New **Hilfe** menu:
|
||||||
|
|
||||||
|
- **Nach Update suchen** – `git fetch` + count of new commits on the server.
|
||||||
|
- **Version…** – shows the installed commit (`<hash> · <date>`).
|
||||||
|
|
||||||
|
On start, a quiet background check runs; if updates exist the status bar says so
|
||||||
|
and the menu item becomes "Update installieren (N verfügbar)".
|
||||||
|
|
||||||
|
Installing runs `git merge --ff-only` (fast-forward only – never an automatic
|
||||||
|
merge), re-runs `pip install -r requirements.txt` if that file changed, then
|
||||||
|
restarts the app via `os.execv`. User data (`data/`) is git-ignored and
|
||||||
|
untouched. If the local checkout has diverged or the network is down, it shows a
|
||||||
|
clear message and does nothing.
|
||||||
|
|
||||||
|
Only active when the app runs from a **git clone** with `git` installed;
|
||||||
|
otherwise the menu just shows "Version: unbekannt". New `Updater` class,
|
||||||
|
`app.py`.
|
||||||
|
|
||||||
## 2026-09-07 — map tiles switched to OpenStreetMap
|
## 2026-09-07 — map tiles switched to OpenStreetMap
|
||||||
|
|
||||||
`folium.Map(tiles="CartoDB positron")` began showing an "API KEY REQUIRED"
|
`folium.Map(tiles="CartoDB positron")` began showing an "API KEY REQUIRED"
|
||||||
|
|||||||
+13
-3
@@ -212,9 +212,19 @@ Second review, focused on the interface. Items 1–12 implemented; 13 deferred.
|
|||||||
Full custom DRK-red *theme* (recolouring `primary` etc.) was **not** done — the
|
Full custom DRK-red *theme* (recolouring `primary` etc.) was **not** done — the
|
||||||
red header bar gives the branding without fighting ttkbootstrap's theme system.
|
red header bar gives the branding without fighting ttkbootstrap's theme system.
|
||||||
|
|
||||||
|
## Self-update (2026-09-07) — ✅ done
|
||||||
|
|
||||||
|
**Hilfe ▸ Nach Update suchen** / quiet check on start / restart via `os.execv`.
|
||||||
|
`git merge --ff-only` only, `pip install` if `requirements.txt` changed, `data/`
|
||||||
|
untouched. Needs a git clone + `git` installed (`Updater.available`).
|
||||||
|
Open follow-ups: no rollback if a pushed update is broken (mitigated by
|
||||||
|
`--ff-only` from a branch Patrick controls + pip errors caught pre-restart);
|
||||||
|
`run.sh` / `.desktop` still not committed (item 5).
|
||||||
|
|
||||||
## Suggested order of remaining work
|
## Suggested order of remaining work
|
||||||
|
|
||||||
1. Initial git commit (item 4), then decide on committing `orte.csv`.
|
1. Initial git commit (item 4), then decide on committing `orte.csv`.
|
||||||
2. Pinned deps + `run.sh` / `.desktop` launcher in the repo (item 5).
|
2. **Set up the git remote** the updater pulls from, and pin all deps (item 5).
|
||||||
3. Stable `id` column (item 7 of the first review).
|
3. `run.sh` + `.desktop` launcher in the repo (item 5).
|
||||||
4. Formalize tests (item 10); offline map (item 9) only if offline use becomes real.
|
4. Stable `id` column (item 7 of the first review).
|
||||||
|
5. Formalize tests (item 10); offline map (item 9) only if offline use becomes real.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Overview – what the app does
|
# Overview – what the app does
|
||||||
|
|
||||||
|
There is a **Hilfe** menu with "Nach Update suchen" (self-update via `git`, see
|
||||||
|
[setup.md](setup.md#updating)) and "Version…".
|
||||||
|
|
||||||
The app is a single window: a **DRK-red header bar**, two tabs, and a
|
The app is a single window: a **DRK-red header bar**, two tabs, and a
|
||||||
**colour-coded status bar** at the bottom (grey = neutral, green = success,
|
**colour-coded status bar** at the bottom (grey = neutral, green = success,
|
||||||
orange = warning, red = error; transient messages fade back to neutral after
|
orange = warning, red = error; transient messages fade back to neutral after
|
||||||
|
|||||||
+26
-5
@@ -7,16 +7,24 @@ during install.
|
|||||||
## 1. System packages
|
## 1. System packages
|
||||||
|
|
||||||
Tkinter is not bundled with the system Python on Mint and must be installed
|
Tkinter is not bundled with the system Python on Mint and must be installed
|
||||||
separately:
|
separately. `git` is needed for the in-app updater:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install python3-tk python3-venv python3-pip
|
sudo apt install python3-tk python3-venv python3-pip git
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. Get the code
|
## 2. Get the code
|
||||||
|
|
||||||
Put the project folder somewhere stable, e.g. `~/Apps/DRK_Blutspende_Orte`.
|
**Clone it** (don't download a zip) – the in-app "Nach Update suchen" only works
|
||||||
|
from a git clone:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/Apps && cd ~/Apps
|
||||||
|
git clone <repo-url> DRK_Blutspende_Orte
|
||||||
|
```
|
||||||
|
|
||||||
|
Put it somewhere stable, e.g. `~/Apps/DRK_Blutspende_Orte`.
|
||||||
|
|
||||||
## 3. Create a virtual environment and install dependencies
|
## 3. Create a virtual environment and install dependencies
|
||||||
|
|
||||||
@@ -94,13 +102,26 @@ that. The macOS CommandLineTools Python used during development ships Tk 8.5 and
|
|||||||
|
|
||||||
## Updating
|
## Updating
|
||||||
|
|
||||||
|
**From inside the app:** menu **Hilfe ▸ Nach Update suchen**. The app also
|
||||||
|
checks quietly on start and offers the update if there is one. It fast-forwards
|
||||||
|
to the server version, reinstalls dependencies if `requirements.txt` changed,
|
||||||
|
and restarts itself. `data/` is never touched.
|
||||||
|
|
||||||
|
For this to work the app must run from a **git clone** (step 2) whose `.venv`
|
||||||
|
was made with the same Python it runs on, and `git` must be installed. The
|
||||||
|
updater only ever fast-forwards – if the local copy has been changed by hand it
|
||||||
|
refuses and says to get in touch.
|
||||||
|
|
||||||
|
**Manually** (equivalent):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/Apps/DRK_Blutspende_Orte
|
cd ~/Apps/DRK_Blutspende_Orte
|
||||||
git pull # once the project is in git
|
git pull --ff-only
|
||||||
.venv/bin/pip install -r requirements.txt
|
.venv/bin/pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
## Data location
|
## Data location
|
||||||
|
|
||||||
All state is in `data/orte.csv` next to `app.py`. To back up the app, copy that
|
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.
|
one file (plus `data/backups/` if you want the history). To move to a new
|
||||||
|
laptop, clone the repo again and redo steps 1 & 3; copy `data/orte.csv` across.
|
||||||
|
|||||||
Reference in New Issue
Block a user