Feature: Widerristhöhe im Hundeprofil + Ausweis-Fix (SW by-v873)

This commit is contained in:
rene 2026-05-11 21:30:07 +02:00
parent b12467286c
commit 83034c0db0
7 changed files with 58 additions and 33 deletions

View file

@ -580,6 +580,8 @@ def _migrate(conn_factory):
("users", "password_reset_expires", "TEXT"), ("users", "password_reset_expires", "TEXT"),
# Fell-Typ für personalisierte Wetter-Hinweise # Fell-Typ für personalisierte Wetter-Hinweise
("dogs", "fell_typ", "TEXT"), # kurz|mittel|lang|drahtaar|doppel|nackt ("dogs", "fell_typ", "TEXT"), # kurz|mittel|lang|drahtaar|doppel|nackt
# Widerristhöhe in cm (höchster Punkt Schulterblatt → Boden)
("dogs", "widerrist_cm", "REAL"),
# Tierarzt-Bewertungen: Durchschnitt + Anzahl am Tierarzt-Datensatz # Tierarzt-Bewertungen: Durchschnitt + Anzahl am Tierarzt-Datensatz
("tieraerzte", "avg_rating", "REAL DEFAULT 0"), ("tieraerzte", "avg_rating", "REAL DEFAULT 0"),
("tieraerzte", "anz_bewertungen", "INTEGER DEFAULT 0"), ("tieraerzte", "anz_bewertungen", "INTEGER DEFAULT 0"),

View file

@ -376,7 +376,7 @@ if STAGING and os.path.isdir(PROD_MEDIA_DIR):
else: else:
app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")
APP_VER = "872" # muss mit APP_VER in app.js übereinstimmen APP_VER = "873" # muss mit APP_VER in app.js übereinstimmen
@app.get("/.well-known/assetlinks.json") @app.get("/.well-known/assetlinks.json")
async def assetlinks(): async def assetlinks():
@ -1407,6 +1407,7 @@ async def ausweis_page(dog_id: int, request: Request):
<div class="meta-item"><div class="label">Geschlecht</div><div class="value">{geschlecht}</div></div> <div class="meta-item"><div class="label">Geschlecht</div><div class="value">{geschlecht}</div></div>
<div class="meta-item"><div class="label">Gewicht</div><div class="value">{f'{dog["gewicht_kg"]} kg' if dog.get("gewicht_kg") else ""}</div></div> <div class="meta-item"><div class="label">Gewicht</div><div class="value">{f'{dog["gewicht_kg"]} kg' if dog.get("gewicht_kg") else ""}</div></div>
<div class="meta-item"><div class="label">Transponder</div><div class="value">{esc(dog.get("chip_nr")) or ""}</div></div> <div class="meta-item"><div class="label">Transponder</div><div class="value">{esc(dog.get("chip_nr")) or ""}</div></div>
{f'<div class="meta-item"><div class="label">Widerrist</div><div class="value">{dog["widerrist_cm"]} cm</div></div>' if dog.get("widerrist_cm") else ''}
<div class="meta-item"><div class="label">Besitzer</div><div class="value">{esc(owner["name"]) if owner else ""}</div></div> <div class="meta-item"><div class="label">Besitzer</div><div class="value">{esc(owner["name"]) if owner else ""}</div></div>
</div> </div>
</div> </div>

View file

@ -15,26 +15,28 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
class DogCreate(BaseModel): class DogCreate(BaseModel):
name: str name: str
rasse: Optional[str] = None rasse: Optional[str] = None
geburtstag: Optional[str] = None geburtstag: Optional[str] = None
geschlecht: Optional[str] = None geschlecht: Optional[str] = None
gewicht_kg: Optional[float] = None gewicht_kg: Optional[float] = None
chip_nr: Optional[str] = None widerrist_cm: Optional[float] = None
bio: Optional[str] = None chip_nr: Optional[str] = None
is_public: bool = False bio: Optional[str] = None
is_public: bool = False
class DogUpdate(BaseModel): class DogUpdate(BaseModel):
name: Optional[str] = None name: Optional[str] = None
rasse: Optional[str] = None rasse: Optional[str] = None
rasse_id: Optional[int] = None rasse_id: Optional[int] = None
geburtstag: Optional[str] = None geburtstag: Optional[str] = None
geschlecht: Optional[str] = None geschlecht: Optional[str] = None
gewicht_kg: Optional[float] = None gewicht_kg: Optional[float] = None
chip_nr: Optional[str] = None widerrist_cm: Optional[float] = None
bio: Optional[str] = None chip_nr: Optional[str] = None
is_public: Optional[bool] = None bio: Optional[str] = None
is_public: Optional[bool] = None
@router.get("") @router.get("")
@ -141,11 +143,11 @@ async def create_dog(data: DogCreate, user=Depends(get_current_user)):
) )
conn.execute( conn.execute(
"""INSERT INTO dogs (user_id, name, rasse, geburtstag, geschlecht, """INSERT INTO dogs (user_id, name, rasse, geburtstag, geschlecht,
gewicht_kg, chip_nr, bio, is_public) gewicht_kg, widerrist_cm, chip_nr, bio, is_public)
VALUES (?,?,?,?,?,?,?,?,?)""", VALUES (?,?,?,?,?,?,?,?,?,?)""",
(user["id"], data.name, data.rasse, data.geburtstag, (user["id"], data.name, data.rasse, data.geburtstag,
data.geschlecht, data.gewicht_kg, data.chip_nr, data.geschlecht, data.gewicht_kg, data.widerrist_cm,
data.bio, int(data.is_public)) data.chip_nr, data.bio, int(data.is_public))
) )
dog = conn.execute( dog = conn.execute(
"SELECT * FROM dogs WHERE user_id=? ORDER BY id DESC LIMIT 1", "SELECT * FROM dogs WHERE user_id=? ORDER BY id DESC LIMIT 1",

View file

@ -101,9 +101,9 @@
</script> </script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung --> <!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=872"> <link rel="stylesheet" href="/css/design-system.css?v=873">
<link rel="stylesheet" href="/css/layout.css?v=872"> <link rel="stylesheet" href="/css/layout.css?v=873">
<link rel="stylesheet" href="/css/components.css?v=872"> <link rel="stylesheet" href="/css/components.css?v=873">
</head> </head>
<body> <body>
@ -583,10 +583,10 @@
<div id="modal-container"></div> <div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features --> <!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=872"></script> <script src="/js/api.js?v=873"></script>
<script src="/js/ui.js?v=872"></script> <script src="/js/ui.js?v=873"></script>
<script src="/js/app.js?v=872"></script> <script src="/js/app.js?v=873"></script>
<script src="/js/worlds.js?v=872"></script> <script src="/js/worlds.js?v=873"></script>
<!-- Feature-Seiten werden lazy geladen --> <!-- Feature-Seiten werden lazy geladen -->

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung. Router, State-Management, Navigation, Initialisierung.
============================================================ */ ============================================================ */
const APP_VER = '872'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VER = '873'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app'; const IS_STAGING = location.hostname === 'staging.banyaro.app';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen // Cache-Bust-Parameter nach Update-Reload sofort entfernen

View file

@ -129,6 +129,12 @@ window.Page_dog_profile = (() => {
<div style="font-weight:500;font-size:var(--text-sm)">${dog.gewicht_kg} kg</div> <div style="font-weight:500;font-size:var(--text-sm)">${dog.gewicht_kg} kg</div>
</div> </div>
` : ''} ` : ''}
${dog.widerrist_cm ? `
<div class="card" style="padding:var(--space-3)">
<div class="dp-info-label"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#ruler"></use></svg> Widerrist</div>
<div style="font-weight:500;font-size:var(--text-sm)">${dog.widerrist_cm} cm</div>
</div>
` : ''}
<div class="card" style="padding:var(--space-3)"> <div class="card" style="padding:var(--space-3)">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary); <div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-bottom:2px"> margin-bottom:2px">
@ -1133,6 +1139,18 @@ window.Page_dog_profile = (() => {
value="${dog?.gewicht_kg || ''}" value="${dog?.gewicht_kg || ''}"
min="0.1" max="120" step="0.1" placeholder="z. B. 28.5"> min="0.1" max="120" step="0.1" placeholder="z. B. 28.5">
</div> </div>
<div class="form-group">
<label class="form-label">
Widerristhöhe (cm)
${UI.help('Der Widerrist ist der höchste Punkt zwischen den Schulterblättern. Hund gerade hinstellen, senkrecht von diesem Punkt zum Boden messen. Ab 40 cm gilt der Hund in NRW als „großer Hund" (Anleinpflicht + Versicherungspflicht). In anderen Bundesländern gelten teils andere Regeln — im Zweifel bei der Gemeinde nachfragen.')}
</label>
<input class="form-control" type="number" name="widerrist_cm"
value="${dog?.widerrist_cm || ''}"
min="10" max="120" step="1" placeholder="z. B. 58">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
Chip-Nummer Chip-Nummer
@ -1141,6 +1159,7 @@ window.Page_dog_profile = (() => {
<input class="form-control" type="text" name="chip_nr" <input class="form-control" type="text" name="chip_nr"
value="${_esc(dog?.chip_nr || '')}" placeholder="15-stellig"> value="${_esc(dog?.chip_nr || '')}" placeholder="15-stellig">
</div> </div>
<div></div>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -1327,8 +1346,9 @@ window.Page_dog_profile = (() => {
rasse_id: fd.rasse_id ? parseInt(fd.rasse_id) : null, rasse_id: fd.rasse_id ? parseInt(fd.rasse_id) : null,
geburtstag: fd.geburtstag || null, geburtstag: fd.geburtstag || null,
geschlecht: fd.geschlecht || null, geschlecht: fd.geschlecht || null,
gewicht_kg: fd.gewicht_kg ? parseFloat(fd.gewicht_kg) : null, gewicht_kg: fd.gewicht_kg ? parseFloat(fd.gewicht_kg) : null,
chip_nr: fd.chip_nr || null, widerrist_cm: fd.widerrist_cm ? parseFloat(fd.widerrist_cm) : null,
chip_nr: fd.chip_nr || null,
bio: fd.bio || null, bio: fd.bio || null,
is_public: 'is_public' in fd, is_public: 'is_public' in fd,
fell_typ: fd.fell_typ || null, fell_typ: fd.fell_typ || null,

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache Offline-Cache + Push Notifications + Tile-Cache
============================================================ */ ============================================================ */
const CACHE_VERSION = 'by-v872'; const CACHE_VERSION = 'by-v873';
const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache