KI-Tracking vollständig, Cloud-Limit 20/Woche, Statusmail täglich 06:00 — SW by-v434, APP_VER 413
- ki.complete() zählt sich selbst (user_id-Parameter, _track_usage) - CLOUD_WEEKLY_LIMIT=20, geprüft vor jedem Cloud-Call - user_id durchgereicht in health, diary, knigge, notes, ki-route - Admin-Panel: 7-Tage-Ansicht, Limit-Info, Top-Cloud-User-Tabelle - Statusmail täglich 06:00 CEST statt alle 2h
This commit is contained in:
parent
85836e4e6e
commit
570dcd4e93
11 changed files with 135 additions and 78 deletions
103
backend/ki.py
103
backend/ki.py
|
|
@ -22,11 +22,12 @@ from functools import wraps
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
KI_MODE = os.getenv("KI_MODE", "local") # off | local | cloud
|
||||
LOCAL_BASE_URL = os.getenv("KI_LOCAL_URL", "http://10.47.11.70:11435/v1")
|
||||
LOCAL_MODEL = os.getenv("KI_LOCAL_MODEL", "gemma-4-31b-it")
|
||||
CLOUD_MODEL = os.getenv("KI_CLOUD_MODEL", "claude-sonnet-4-6")
|
||||
ANTHROPIC_KEY = os.getenv("ANTHROPIC_API_KEY", "")
|
||||
KI_MODE = os.getenv("KI_MODE", "local") # off | local | cloud
|
||||
LOCAL_BASE_URL = os.getenv("KI_LOCAL_URL", "http://10.47.11.70:11435/v1")
|
||||
LOCAL_MODEL = os.getenv("KI_LOCAL_MODEL", "gemma-4-31b-it")
|
||||
CLOUD_MODEL = os.getenv("KI_CLOUD_MODEL", "claude-sonnet-4-6")
|
||||
ANTHROPIC_KEY = os.getenv("ANTHROPIC_API_KEY", "")
|
||||
CLOUD_WEEKLY_LIMIT = int(os.getenv("KI_CLOUD_WEEKLY_LIMIT", "20"))
|
||||
|
||||
# Lazy Imports — nur laden wenn wirklich benötigt
|
||||
_openai_client = None
|
||||
|
|
@ -62,6 +63,50 @@ class KIPremiumRequired(Exception):
|
|||
# ------------------------------------------------------------------
|
||||
# Haupt-Aufruf-Funktion — zentraler Eingang für alle KI-Requests
|
||||
# ------------------------------------------------------------------
|
||||
def _track_usage(user_id: int | None, source: str) -> None:
|
||||
"""Schreibt einen KI-Aufruf in ki_daily_calls (fire-and-forget, swallows errors)."""
|
||||
if user_id is None:
|
||||
return
|
||||
try:
|
||||
from database import db
|
||||
from datetime import date
|
||||
today = date.today().isoformat()
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
"""INSERT INTO ki_daily_calls (user_id, date, count, source)
|
||||
VALUES (?, ?, 1, ?)
|
||||
ON CONFLICT(user_id, date, source)
|
||||
DO UPDATE SET count = count + 1""",
|
||||
(user_id, today, source)
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(f"KI-Tracking fehlgeschlagen: {exc}")
|
||||
|
||||
|
||||
def _check_weekly_cloud_limit(user_id: int | None) -> None:
|
||||
"""Wirft KIPremiumRequired wenn user_id das wöchentliche Cloud-Limit erreicht hat."""
|
||||
if user_id is None or CLOUD_WEEKLY_LIMIT <= 0:
|
||||
return
|
||||
try:
|
||||
from database import db
|
||||
with db() as conn:
|
||||
used = conn.execute(
|
||||
"""SELECT COALESCE(SUM(count), 0) FROM ki_daily_calls
|
||||
WHERE user_id=? AND source='cloud'
|
||||
AND date >= DATE('now', '-6 days')""",
|
||||
(user_id,)
|
||||
).fetchone()[0]
|
||||
if used >= CLOUD_WEEKLY_LIMIT:
|
||||
raise KIPremiumRequired(
|
||||
f"Wöchentliches KI-Limit erreicht ({CLOUD_WEEKLY_LIMIT} Cloud-Anfragen/Woche). "
|
||||
"Morgen wieder verfügbar."
|
||||
)
|
||||
except KIPremiumRequired:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.warning(f"KI-Limit-Check fehlgeschlagen: {exc}")
|
||||
|
||||
|
||||
async def complete(
|
||||
prompt: str,
|
||||
system: str = "Du bist ein hilfreicher Assistent für Hundebesitzer.",
|
||||
|
|
@ -71,55 +116,43 @@ async def complete(
|
|||
json_mode: bool = False,
|
||||
return_model: bool = False,
|
||||
return_source: bool = False,
|
||||
user_id: int | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
KI-Completion. Wählt automatisch den richtigen Backend.
|
||||
|
||||
Args:
|
||||
prompt: User-Nachricht
|
||||
system: System-Prompt
|
||||
max_tokens: Maximale Antwortlänge
|
||||
requires_premium: True = nur für Premium-User (nutzt Cloud)
|
||||
user_is_premium: Ob der anfragende User Premium hat
|
||||
json_mode: Antwort als JSON anfordern
|
||||
return_source: Falls True: gibt (text, source) zurück, source = 'cloud'|'local'
|
||||
|
||||
Returns:
|
||||
KI-Antwort als String, oder (str, str) wenn return_source=True
|
||||
|
||||
Raises:
|
||||
KIPremiumRequired: Cloud-Feature ohne Premium
|
||||
KIUnavailableError: KI komplett deaktiviert
|
||||
Zählt jeden Aufruf in ki_daily_calls wenn user_id übergeben wird.
|
||||
Prüft wöchentliches Cloud-Limit (CLOUD_WEEKLY_LIMIT) für Cloud-Aufrufe.
|
||||
"""
|
||||
if KI_MODE == "off":
|
||||
raise KIUnavailableError("KI ist deaktiviert.")
|
||||
|
||||
# Premium-Check vor Cloud-Aufrufen
|
||||
if requires_premium and not user_is_premium:
|
||||
raise KIPremiumRequired(
|
||||
"Dieses Feature ist Teil von Ban Yaro Premium."
|
||||
)
|
||||
raise KIPremiumRequired("Dieses Feature ist Teil von Ban Yaro Premium.")
|
||||
|
||||
# Cloud-Aufruf: nur wenn Premium UND cloud-Modus
|
||||
# Cloud-Aufruf: Premium UND cloud-Modus
|
||||
if requires_premium and user_is_premium and KI_MODE == "cloud":
|
||||
_check_weekly_cloud_limit(user_id)
|
||||
text = await _cloud_complete(prompt, system, max_tokens, json_mode)
|
||||
_track_usage(user_id, "cloud")
|
||||
if return_model:
|
||||
return (text, CLOUD_MODEL)
|
||||
return (text, "cloud") if return_source else text
|
||||
|
||||
# Lokaler Aufruf: Entwicklung + Free-User
|
||||
# Lokaler Aufruf + Cloud-Fallback
|
||||
if KI_MODE in ("local", "cloud"):
|
||||
try:
|
||||
text = await _local_complete(prompt, system, max_tokens, json_mode)
|
||||
_track_usage(user_id, "local")
|
||||
if return_model:
|
||||
return (text, LOCAL_MODEL)
|
||||
return (text, "local") if return_source else text
|
||||
except Exception as e:
|
||||
logger.warning(f"Lokales KI-Modell nicht erreichbar: {e}")
|
||||
# Cloud-Fallback: im cloud-Modus immer, sonst nur für Premium-User
|
||||
if ANTHROPIC_KEY and (KI_MODE == "cloud" or (requires_premium and user_is_premium)):
|
||||
logger.info("Fallback auf Cloud-KI.")
|
||||
_check_weekly_cloud_limit(user_id)
|
||||
text = await _cloud_complete(prompt, system, max_tokens, json_mode)
|
||||
_track_usage(user_id, "cloud")
|
||||
if return_model:
|
||||
return (text, CLOUD_MODEL)
|
||||
return (text, "cloud") if return_source else text
|
||||
|
|
@ -188,6 +221,7 @@ async def _cloud_complete(
|
|||
# ------------------------------------------------------------------
|
||||
|
||||
async def symptom_check(symptoms: str, dog_info: dict,
|
||||
user_id: int | None = None,
|
||||
user_is_premium: bool = False) -> dict:
|
||||
"""
|
||||
Symptom-Triage für Hunde.
|
||||
|
|
@ -216,9 +250,10 @@ Antworte NUR als JSON:
|
|||
result = await complete(
|
||||
prompt, system,
|
||||
max_tokens=400,
|
||||
requires_premium=False, # Basis-Triage ist kostenlos
|
||||
requires_premium=False,
|
||||
user_is_premium=user_is_premium,
|
||||
json_mode=True,
|
||||
user_id=user_id,
|
||||
)
|
||||
import json, re
|
||||
# Cloud-Modelle wrappen JSON manchmal in ```json … ``` — stripppen
|
||||
|
|
@ -258,7 +293,7 @@ Strukturiert, konkret, motivierend.
|
|||
)
|
||||
|
||||
|
||||
async def diary_tags(entry_text: str) -> list[str]:
|
||||
async def diary_tags(entry_text: str, user_id: int | None = None) -> list[str]:
|
||||
"""
|
||||
Automatische Tags für Tagebucheinträge — läuft lokal, kostenlos.
|
||||
"""
|
||||
|
|
@ -274,7 +309,7 @@ reise, meilenstein, lustig, traurig, wetter, sonstige
|
|||
Antworte NUR mit den Tags, kommagetrennt, keine Erklärung.
|
||||
"""
|
||||
try:
|
||||
result = await complete(prompt, max_tokens=50, requires_premium=False)
|
||||
result = await complete(prompt, max_tokens=50, requires_premium=False, user_id=user_id)
|
||||
return [t.strip().lower() for t in result.split(",") if t.strip()]
|
||||
except Exception:
|
||||
return []
|
||||
|
|
@ -305,7 +340,8 @@ Bewerte als JSON:
|
|||
|
||||
|
||||
async def health_summary(health_data: list, dog_info: dict,
|
||||
user_is_premium: bool = False) -> str:
|
||||
user_is_premium: bool = False,
|
||||
user_id: int | None = None) -> str:
|
||||
"""
|
||||
Tierärztliche Zusammenfassung aller Gesundheitsdaten — lokal für alle.
|
||||
Fasst Impfungen, Besuche, Medikamente etc. in einem lesbaren Bericht zusammen.
|
||||
|
|
@ -364,8 +400,9 @@ Schreibe klar und verständlich für den Hundebesitzer.
|
|||
return await complete(
|
||||
prompt, system,
|
||||
max_tokens=700,
|
||||
requires_premium=False, # Kostenlos für alle
|
||||
requires_premium=False,
|
||||
user_is_premium=user_is_premium,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -149,41 +149,45 @@ async def stats(user=Depends(require_mod)):
|
|||
).fetchall()
|
||||
}
|
||||
|
||||
# KI-Trainer Nutzung
|
||||
# KI-Nutzung
|
||||
try:
|
||||
from ki import CLOUD_WEEKLY_LIMIT
|
||||
ki_today = conn.execute(
|
||||
"SELECT COALESCE(SUM(count),0) FROM ki_daily_calls WHERE date=DATE('now')"
|
||||
).fetchone()[0]
|
||||
ki_week = conn.execute(
|
||||
"SELECT COALESCE(SUM(count),0) FROM ki_daily_calls WHERE date>=DATE('now','-6 days')"
|
||||
).fetchone()[0]
|
||||
ki_month = conn.execute(
|
||||
"SELECT COALESCE(SUM(count),0) FROM ki_daily_calls WHERE date>=DATE('now','start of month')"
|
||||
).fetchone()[0]
|
||||
ki_users_today = conn.execute(
|
||||
"SELECT COUNT(DISTINCT user_id) FROM ki_daily_calls WHERE date=DATE('now')"
|
||||
).fetchone()[0]
|
||||
# Aufschlüsselung nach Quelle (heute)
|
||||
_src_today = {
|
||||
# Aufschlüsselung nach Quelle (diese Woche)
|
||||
_src_week = {
|
||||
r[0]: r[1] for r in conn.execute(
|
||||
"SELECT source, COALESCE(SUM(count),0) FROM ki_daily_calls "
|
||||
"WHERE date=DATE('now') GROUP BY source"
|
||||
"WHERE date>=DATE('now','-6 days') GROUP BY source"
|
||||
).fetchall()
|
||||
}
|
||||
ki_cloud_today = _src_today.get("cloud", 0)
|
||||
ki_local_today = _src_today.get("local", 0)
|
||||
ki_luna_today = _src_today.get("luna", 0)
|
||||
# Aufschlüsselung nach Quelle (Monat)
|
||||
_src_month = {
|
||||
r[0]: r[1] for r in conn.execute(
|
||||
"SELECT source, COALESCE(SUM(count),0) FROM ki_daily_calls "
|
||||
"WHERE date>=DATE('now','start of month') GROUP BY source"
|
||||
ki_cloud_week = _src_week.get("cloud", 0)
|
||||
ki_local_week = _src_week.get("local", 0)
|
||||
ki_luna_week = _src_week.get("luna", 0)
|
||||
# Top-User Cloud diese Woche
|
||||
ki_top_users = [
|
||||
{"user_id": r[0], "name": r[1], "cloud_calls": r[2]} for r in conn.execute(
|
||||
"""SELECT k.user_id, u.name, SUM(k.count) as n
|
||||
FROM ki_daily_calls k JOIN users u ON u.id=k.user_id
|
||||
WHERE k.source='cloud' AND k.date>=DATE('now','-6 days')
|
||||
GROUP BY k.user_id ORDER BY n DESC LIMIT 10"""
|
||||
).fetchall()
|
||||
}
|
||||
ki_cloud_month = _src_month.get("cloud", 0)
|
||||
ki_local_month = _src_month.get("local", 0)
|
||||
ki_luna_month = _src_month.get("luna", 0)
|
||||
]
|
||||
except Exception:
|
||||
ki_today = ki_month = ki_users_today = 0
|
||||
ki_cloud_today = ki_local_today = ki_luna_today = 0
|
||||
ki_cloud_month = ki_local_month = ki_luna_month = 0
|
||||
from ki import CLOUD_WEEKLY_LIMIT
|
||||
ki_today = ki_week = ki_month = ki_users_today = 0
|
||||
ki_cloud_week = ki_local_week = ki_luna_week = 0
|
||||
ki_top_users = []
|
||||
|
||||
# Ausstehende Wiki-Foto-Einreichungen
|
||||
try:
|
||||
|
|
@ -245,15 +249,15 @@ async def stats(user=Depends(require_mod)):
|
|||
"osm_total": osm_total,
|
||||
"osm_tiles": osm_tiles,
|
||||
"osm_by_type": osm_by_type,
|
||||
"ki_today": ki_today,
|
||||
"ki_month": ki_month,
|
||||
"ki_users_today": ki_users_today,
|
||||
"ki_cloud_today": ki_cloud_today,
|
||||
"ki_local_today": ki_local_today,
|
||||
"ki_luna_today": ki_luna_today,
|
||||
"ki_cloud_month": ki_cloud_month,
|
||||
"ki_local_month": ki_local_month,
|
||||
"ki_luna_month": ki_luna_month,
|
||||
"ki_today": ki_today,
|
||||
"ki_week": ki_week,
|
||||
"ki_month": ki_month,
|
||||
"ki_users_today": ki_users_today,
|
||||
"ki_cloud_week": ki_cloud_week,
|
||||
"ki_local_week": ki_local_week,
|
||||
"ki_luna_week": ki_luna_week,
|
||||
"ki_cloud_weekly_limit": CLOUD_WEEKLY_LIMIT,
|
||||
"ki_top_users": ki_top_users,
|
||||
"social_total": social_total,
|
||||
"social_published": social_published,
|
||||
"social_scheduled": social_scheduled,
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ async def create_diary(dog_id: int, data: DiaryCreate,
|
|||
# KI: Auto-Tags wenn Text vorhanden (lokal, kostenlos)
|
||||
if data.text and len(data.text) > 10:
|
||||
try:
|
||||
ai_tags = await KI.diary_tags(data.text)
|
||||
ai_tags = await KI.diary_tags(data.text, user_id=user["id"])
|
||||
tags = list(set(tags + ai_tags))
|
||||
except Exception:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -412,6 +412,7 @@ async def symptom_check(dog_id: int, data: SymptomCheckRequest,
|
|||
symptoms=data.symptoms,
|
||||
dog_info=dog_info,
|
||||
user_is_premium=bool(user.get("is_premium")),
|
||||
user_id=user["id"],
|
||||
)
|
||||
return result
|
||||
except KIUnavailableError as e:
|
||||
|
|
@ -444,6 +445,7 @@ async def ki_zusammenfassung(dog_id: int, user=Depends(get_current_user)):
|
|||
health_data=health_data,
|
||||
dog_info=dict(dog),
|
||||
user_is_premium=bool(user.get("is_premium")),
|
||||
user_id=user["id"],
|
||||
)
|
||||
return {"zusammenfassung": result}
|
||||
except KIPremiumRequired as e:
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ Schreibe klar und strukturiert, ohne unnötigen Fachjargon."""
|
|||
system=system,
|
||||
max_tokens=600,
|
||||
requires_premium=False,
|
||||
user_id=user["id"],
|
||||
)
|
||||
return {"antwort": result}
|
||||
except ki_module.KIUnavailableError as e:
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ async def ki_rat(data: KiRatRequest, user=Depends(get_current_user)):
|
|||
max_tokens=300,
|
||||
requires_premium=False,
|
||||
user_is_premium=bool(user.get("is_premium")),
|
||||
user_id=user["id"],
|
||||
)
|
||||
return {"rat": rat}
|
||||
except KIPremiumRequired as e:
|
||||
|
|
|
|||
|
|
@ -156,10 +156,11 @@ async def ki_analyse(user=Depends(get_current_user)):
|
|||
|
||||
try:
|
||||
import ki as ki_module
|
||||
suggestions, _ = await ki_module.complete(
|
||||
suggestions = await ki_module.complete(
|
||||
prompt,
|
||||
requires_premium=False,
|
||||
user_is_premium=False,
|
||||
user_id=user["id"],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("KI-Analyse fehlgeschlagen: %s", e)
|
||||
|
|
|
|||
|
|
@ -92,10 +92,10 @@ def start():
|
|||
replace_existing=True,
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
# Alle 2 Stunden Status-Report per Mail
|
||||
# Täglich 06:00 Uhr Status-Report per Mail
|
||||
_scheduler.add_job(
|
||||
_job_status_report,
|
||||
CronTrigger(minute=0, hour="*/2"),
|
||||
CronTrigger(hour=6, minute=0),
|
||||
id="status_report",
|
||||
replace_existing=True,
|
||||
misfire_grace_time=1800,
|
||||
|
|
@ -109,7 +109,7 @@ def start():
|
|||
misfire_grace_time=3600,
|
||||
)
|
||||
_scheduler.start()
|
||||
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats. OSM-Cache: on-demand (kein Prewarm).")
|
||||
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00. OSM-Cache: on-demand (kein Prewarm).")
|
||||
|
||||
|
||||
def stop():
|
||||
|
|
@ -642,7 +642,7 @@ async def _job_ki_health_report():
|
|||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JOB: Status-Report per Mail (4× täglich)
|
||||
# JOB: Status-Report per Mail (täglich 06:00 Uhr)
|
||||
# ------------------------------------------------------------------
|
||||
async def _job_status_report():
|
||||
"""Sendet einen HTML-Status-Report an ADMIN_EMAIL."""
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '412'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '413'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
|
||||
const App = (() => {
|
||||
|
||||
|
|
|
|||
|
|
@ -333,15 +333,13 @@ window.Page_admin = (() => {
|
|||
<p style="font-size:var(--text-sm);font-weight:600;margin:0 0 var(--space-3)">KI-Nutzung</p>
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
|
||||
${[
|
||||
['☁️ Claude heute', s.ki_cloud_today, 'var(--c-primary)'],
|
||||
['🖥️ LM Studio heute', s.ki_local_today, 'var(--c-success)'],
|
||||
['🌙 Luna heute', s.ki_luna_today, 'var(--c-warning)'],
|
||||
['Gesamt heute', s.ki_today, 'var(--c-text-secondary)'],
|
||||
['☁️ Claude Monat', s.ki_cloud_month, 'var(--c-primary)'],
|
||||
['🖥️ LM Studio Monat', s.ki_local_month, 'var(--c-success)'],
|
||||
['🌙 Luna Monat', s.ki_luna_month, 'var(--c-warning)'],
|
||||
['Gesamt Monat', s.ki_month, 'var(--c-text-secondary)'],
|
||||
['Aktive User heute', s.ki_users_today, 'var(--c-text-secondary)'],
|
||||
['☁️ Claude (7 Tage)', s.ki_cloud_week, 'var(--c-primary)'],
|
||||
['🖥️ LM Studio (7 Tage)', s.ki_local_week, 'var(--c-success)'],
|
||||
['🌙 Luna (7 Tage)', s.ki_luna_week, 'var(--c-warning)'],
|
||||
['Gesamt heute', s.ki_today, 'var(--c-text-secondary)'],
|
||||
['Gesamt 7 Tage', s.ki_week, 'var(--c-text-secondary)'],
|
||||
['Gesamt Monat', s.ki_month, 'var(--c-text-secondary)'],
|
||||
['Aktive User heute', s.ki_users_today, 'var(--c-text-secondary)'],
|
||||
].map(([label, val, color]) => `
|
||||
<div style="display:flex;justify-content:space-between;font-size:var(--text-sm)">
|
||||
<span style="color:var(--c-text-secondary)">${label}</span>
|
||||
|
|
@ -349,6 +347,19 @@ window.Page_admin = (() => {
|
|||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div style="margin-top:var(--space-3);padding-top:var(--space-3);border-top:1px solid var(--c-border)">
|
||||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:0 0 var(--space-2)">
|
||||
User-Limit: <strong>${s.ki_cloud_weekly_limit ?? 20} Cloud-Anfragen / Woche</strong>
|
||||
</p>
|
||||
${(s.ki_top_users || []).length ? `
|
||||
<p style="font-size:var(--text-xs);font-weight:600;color:var(--c-text-secondary);margin:var(--space-2) 0 var(--space-1)">Top Cloud-User (7 Tage)</p>
|
||||
${s.ki_top_users.map((u, i) => `
|
||||
<div style="display:flex;justify-content:space-between;font-size:var(--text-xs)">
|
||||
<span style="color:var(--c-text-secondary)">${i+1}. ${_esc(u.name)}</span>
|
||||
<span style="font-weight:600;color:${u.cloud_calls >= (s.ki_cloud_weekly_limit ?? 20) ? 'var(--c-danger)' : 'var(--c-primary)'}">${u.cloud_calls}</span>
|
||||
</div>
|
||||
`).join('')}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="padding:var(--space-4)">
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Offline-Cache + Push Notifications + Tile-Cache
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v433';
|
||||
const CACHE_VERSION = 'by-v434';
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue