diff --git a/VERSION b/VERSION
index a624bd7..ff7be53 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1133
\ No newline at end of file
+1134
\ No newline at end of file
diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py
index 1c37b99..9c414d8 100644
--- a/backend/routes/dogs.py
+++ b/backend/routes/dogs.py
@@ -232,15 +232,42 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
ORDER BY d.datum DESC, d.id DESC, dm.id ASC""",
(dog_id,)
).fetchall()
+ # Cross-Plattform-stabiles Tagesfoto: einmal pro Tag pro Hund festgelegt
+ # und in daily_photo_cache persistiert. Sobald ein Client (PWA oder
+ # iOS) zum ersten Mal heute zugreift, wird die Wahl gespeichert; alle
+ # weiteren Clients liefern dasselbe Bild.
+ import datetime as _dt2
+ conn.execute("""
+ CREATE TABLE IF NOT EXISTS daily_photo_cache (
+ dog_id INTEGER NOT NULL,
+ datum TEXT NOT NULL,
+ photo_url TEXT NOT NULL,
+ PRIMARY KEY (dog_id, datum)
+ )
+ """)
+ today_iso = _dt2.date.today().isoformat()
+ cached = conn.execute(
+ "SELECT photo_url FROM daily_photo_cache WHERE dog_id=? AND datum=?",
+ (dog_id, today_iso)
+ ).fetchone()
+
random_photo = None
- if photos:
- import datetime as _dt2
+ if cached and cached["photo_url"]:
+ random_photo = {
+ "url": cached["photo_url"],
+ "preview_url": preview_url_from(cached["photo_url"]),
+ }
+ elif photos:
tick = (_dt2.date.today() - _dt2.date(2024, 1, 1)).days
chosen_url = photos[tick % len(photos)]["url"]
random_photo = {
"url": chosen_url,
"preview_url": preview_url_from(chosen_url),
}
+ conn.execute(
+ "INSERT OR IGNORE INTO daily_photo_cache (dog_id, datum, photo_url) VALUES (?, ?, ?)",
+ (dog_id, today_iso, chosen_url)
+ )
# Neuester Tagebucheintrag
last_diary_row = conn.execute(
diff --git a/backend/routes/expenses.py b/backend/routes/expenses.py
index 9a34f27..9e32e43 100644
--- a/backend/routes/expenses.py
+++ b/backend/routes/expenses.py
@@ -12,7 +12,15 @@ from auth import get_current_user
router = APIRouter()
logger = logging.getLogger(__name__)
-KATEGORIEN = {"tierarzt", "futter", "zubehoer", "versicherung", "sitter", "sonstiges"}
+KATEGORIE_META = {
+ "futter": {"label": "Futter", "color": "#f59e0b"},
+ "tierarzt": {"label": "Tierarzt", "color": "#ef4444"},
+ "zubehoer": {"label": "Zubehör", "color": "#8b5cf6"},
+ "versicherung": {"label": "Versicherung", "color": "#3b82f6"},
+ "sitter": {"label": "Sitter", "color": "#10b981"},
+ "sonstiges": {"label": "Sonstiges", "color": "#6b7280"},
+}
+KATEGORIEN = set(KATEGORIE_META.keys())
# ------------------------------------------------------------------
@@ -75,6 +83,18 @@ def _serialize(row) -> dict:
return dict(row)
+# ------------------------------------------------------------------
+# GET /api/expenses/categories — gültige Kategorien (id + label + color)
+# Single source of truth für PWA und mobile Clients.
+# ------------------------------------------------------------------
+@router.get("/categories")
+async def list_categories():
+ return [
+ {"id": key, **meta}
+ for key, meta in KATEGORIE_META.items()
+ ]
+
+
# ------------------------------------------------------------------
# GET /api/expenses/summary — Monats- und Jahressummen
# WICHTIG: Diese Route muss VOR /{id} stehen!
diff --git a/backend/static/index.html b/backend/static/index.html
index 1e0a5d0..cd373a2 100644
--- a/backend/static/index.html
+++ b/backend/static/index.html
@@ -86,14 +86,14 @@
Ban Yaro
-
+
-
-
-
-
-
+
+
+
+
+
@@ -617,11 +617,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -631,7 +631,7 @@
-
+
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index a7b5e90..e116fa3 100644
--- a/backend/static/js/app.js
+++ b/backend/static/js/app.js
@@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
-const APP_VER = '1133'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '1134'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;
diff --git a/backend/static/js/pages/onboarding.js b/backend/static/js/pages/onboarding.js
index c292f03..5ba7638 100644
--- a/backend/static/js/pages/onboarding.js
+++ b/backend/static/js/pages/onboarding.js
@@ -15,6 +15,28 @@ window.Page_onboarding = (() => {
async function init(container, appState) {
_container = container;
_appState = appState;
+
+ // Hunde frisch laden — der appState kann veraltet sein (z.B. nach
+ // Anlage in mobiler App). Wenn schon ein Hund da ist, Wizard
+ // komplett überspringen.
+ try {
+ const dogs = await API.dogs.list();
+ _appState.dogs = dogs;
+ if (dogs.length > 0) {
+ _appState.activeDog = dogs[0];
+ localStorage.setItem('by_active_dog', String(dogs[0].id));
+ localStorage.setItem('by_onboarding_done', '1');
+ App.renderDogSwitcher?.();
+ if (window.Worlds) window.Worlds.init(_appState);
+ else App.navigate('diary');
+ return;
+ }
+ } catch (e) {
+ // Lieber Wizard zeigen als komplett blockieren — wenn API down ist,
+ // landet der User halt im Standard-Flow.
+ console.warn('Onboarding: dog-list refresh failed', e);
+ }
+
_step = 1;
_render();
}
@@ -315,10 +337,12 @@ window.Page_onboarding = (() => {
// EVENTS
// ----------------------------------------------------------
function _bindEvents() {
- // Weiter-Button (Schritt 1)
+ // Weiter-Button (Schritt 1) — direkt ins richtige Hunde-Profil,
+ // statt eines doppelten Mini-Formulars im Wizard. Onboarding gilt
+ // damit als erledigt, sobald der User dort angekommen ist.
_container.querySelector('#ob-next-btn')?.addEventListener('click', () => {
- _step = 2;
- _render();
+ localStorage.setItem('by_onboarding_done', '1');
+ App.navigate('dog-profile');
});
// Zurück-Button (Schritt 2)
@@ -422,6 +446,24 @@ window.Page_onboarding = (() => {
_render();
} catch (err) {
+ // 403 „schon einen Hund" → kein Stuck-State, weiter zu Schritt 3
+ const isAlreadyHas = err?.status === 403
+ || /Pro-Feature|schon|already|maximal/i.test(err?.message || '');
+ if (isAlreadyHas) {
+ try {
+ const dogs = await API.dogs.list();
+ _appState.dogs = dogs;
+ if (dogs.length > 0) {
+ _appState.activeDog = dogs[0];
+ localStorage.setItem('by_active_dog', String(dogs[0].id));
+ App.renderDogSwitcher?.();
+ }
+ } catch {}
+ UI.toast.info?.('Du hast bereits einen Hund — geht direkt weiter.');
+ _step = 3;
+ _render();
+ return;
+ }
UI.toast.error(err.message || 'Hund konnte nicht angelegt werden.');
} finally {
if (saveBtn) {
diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js
index c6e05eb..52f3baa 100644
--- a/backend/static/js/worlds.js
+++ b/backend/static/js/worlds.js
@@ -1425,7 +1425,7 @@ window.Worlds = (() => {
Lege ein Profil an und schalte alle Features frei
-