Big Sweep: Security + Race-Conditions + Tests + DSGVO + A11y, SW by-v1095
SECURITY (auth.py, routes/auth.py, database.py, main.py) - JWT bekommt jti; Logout trägt in neue jwt_blacklist-Tabelle ein, decode_token() prüft → server-side Invalidierung - JWT-Expiry default 30 → 7 Tage (ENV JWT_EXPIRY_DAYS überschreibt) - Sliding-Refresh-Middleware: erneuert Cookie wenn >50% verbraucht (Schwelle via JWT_REFRESH_FRACTION, Default 2) - Login-Lockout in DB-Tabelle login_attempts (5 Versuche / 15 Min, überlebt Container-Restart) — alte In-Memory-Lockouts ersetzt - SMTP-Versand: alle 'except: pass' durch logger.exception ersetzt; Fehlversuche landen in failed_emails-Tabelle für späteres Retry - Referral-Counter Race gefixt: UPDATE partner_codes SET uses=uses+1 ... WHERE uses<max_uses RETURNING — atomar statt SELECT+UPDATE RACE CONDITIONS (routes/invoices.py, database.py) - Neue invoice_counters-Tabelle für atomare Nummernvergabe - _next_invoice_number nutzt BEGIN IMMEDIATE + atomares UPDATE - Funktioniert für RG- und ST-Prefixe (Stornorechnungen) - Race-Test verifiziert (5 Threads × 20 Calls = 100 eindeutige Nummern) VERSION + TESTS + ERROR-DIGEST (VERSION, Makefile, tests/, scheduler.py) - Neue VERSION-Datei (Single Source of Truth) — main.py liest beim Startup - Makefile-Target 'make bump' propagiert in sw.js, app.js, index.html - Makefile-Target 'make test' setzt venv auf, läuft pytest - 19 Smoke-Tests in tests/ (health, auth, diary, invoice) — alle grün - Scheduler: täglicher _job_error_digest um 06:30 → schickt Error- Zusammenfassung an ADMIN_EMAIL (still wenn keine Errors) DSGVO + A11Y + ERSTE-HILFE - landing.html: 'HTML und ODS' → 'JSON' (tatsächlich implementiert) - datenschutz.js: Sektion Account-Löschung erweitert (sofort gelöscht / anonymisiert / 10 Jahre für Rechnungen) - erste-hilfe.js: prominentes Warning-Banner oben (ersetzt keine Tierarzt-Beratung); Notfallnummern gruppiert nach Land, TODO-Platz- halter für AT-Uni-Klinik, CH Tox Info Suisse, CH Tierspital Zürich - ui.js Modal: ESC schließt, Focus-Trap, Auto-Focus erstes Element, Restore Focus auf vorigen Caller - impressum.js Kontaktformular: Labels mit for=cf-name etc. NEUE DB-TABELLEN (idempotent via CREATE TABLE IF NOT EXISTS) - jwt_blacklist, login_attempts, failed_emails, invoice_counters NEUE ENV-VARS - JWT_REFRESH_FRACTION (Default 2) - JWT_EXPIRY_DAYS Default geändert (30 → 7)
This commit is contained in:
parent
6224044654
commit
9394bab1fb
23 changed files with 1208 additions and 78 deletions
|
|
@ -101,9 +101,9 @@
|
|||
</script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1094">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1094">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1094">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1095">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1095">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1095">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -625,11 +625,11 @@
|
|||
<div id="modal-container"></div>
|
||||
|
||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||
<script src="/js/api.js?v=1094"></script>
|
||||
<script src="/js/ui.js?v=1094"></script>
|
||||
<script src="/js/app.js?v=1094"></script>
|
||||
<script src="/js/worlds.js?v=1094"></script>
|
||||
<script src="/js/offline-indicator.js?v=1094"></script>
|
||||
<script src="/js/api.js?v=1095"></script>
|
||||
<script src="/js/ui.js?v=1095"></script>
|
||||
<script src="/js/app.js?v=1095"></script>
|
||||
<script src="/js/worlds.js?v=1095"></script>
|
||||
<script src="/js/offline-indicator.js?v=1095"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '1094'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '1095'; // ← 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;
|
||||
|
|
|
|||
|
|
@ -270,8 +270,14 @@ window.Page_datenschutz = (() => {
|
|||
<strong>Datenexport (Art. 20 DSGVO):</strong> Du kannst jederzeit unter
|
||||
Einstellungen → „Meine Daten exportieren" eine vollständige Kopie deiner
|
||||
gespeicherten Daten als JSON-Datei herunterladen. Der Export enthält Profildaten,
|
||||
Hundedaten, Tagebuch, Gesundheitseinträge, Trainingsfortschritt, Ausgaben,
|
||||
Verhaltensprotokoll, Forum-Beiträge und Gassi-Teilnahmen.
|
||||
Hundedaten, Tagebuch (inkl. Medien-URLs), Gesundheitseinträge, Trainingsfortschritt,
|
||||
Ausgaben, Verhaltensprotokoll, Versicherung, Ernährungsprofil und Futter-Reaktionen,
|
||||
eigene Routen, Forum-Beiträge sowie Gassi-Teilnahmen und Gassi-Fotos.
|
||||
</p>
|
||||
<p style="${S.p};margin-top:var(--space-3)">
|
||||
Das JSON-Format ist maschinenlesbar und kann z. B. mit jedem Texteditor geöffnet
|
||||
oder in andere Anwendungen importiert werden. Der Export wird direkt im Browser
|
||||
erzeugt und nicht dauerhaft auf dem Server gespeichert.
|
||||
</p>
|
||||
<p style="${S.p};margin-top:var(--space-3)">
|
||||
Zur Ausübung weiterer Rechte wende dich per E-Mail an
|
||||
|
|
@ -303,10 +309,31 @@ window.Page_datenschutz = (() => {
|
|||
|
||||
${sec('Speicherdauer', `
|
||||
<p style="${S.p}">
|
||||
Deine Daten werden vollständig gelöscht, sobald du deinen Account löschst —
|
||||
einschließlich Tagebuch, Gesundheitseinträge, Fotos, Forenbeiträge und Hundeprofil.
|
||||
Es gibt keine anonymisierte Weiterverarbeitung deiner Inhalte nach Account-Löschung.
|
||||
Server-Logs werden nach 30 Tagen rotiert.
|
||||
Server-Logs werden nach 30 Tagen rotiert. IP-Adressen werden ausschließlich
|
||||
zur Sicherheit und für Rate-Limiting maximal 30 Tage gespeichert.
|
||||
</p>`)}
|
||||
|
||||
${sec('Account-Löschung', `
|
||||
<p style="${S.p}">
|
||||
Wenn du deinen Account löschst, werden deine Daten nach folgendem Schema verarbeitet:
|
||||
</p>
|
||||
<ul style="${S.ul}">
|
||||
<li><strong>Sofort und unwiderruflich gelöscht:</strong> Account, Hundeprofile, Tagebuch
|
||||
und Tagebuch-Medien, Gesundheitseinträge, Trainingsfortschritt, Ausgaben,
|
||||
Verhaltensprotokoll, Versicherung, Ernährungsprofil, Futter-Einträge und -Reaktionen,
|
||||
Forum-Beiträge, eigene Notizen, Direktnachrichten, Freundschaften,
|
||||
Push-Benachrichtigungen, Einstellungen und Welten-Konfiguration.</li>
|
||||
<li><strong>Anonymisiert (Urheber-Bezug auf NULL gesetzt):</strong> Eigene Routen,
|
||||
Forum-Threads sowie von dir angelegte Wiki-Inhalte bleiben zur Verfügbarkeit für
|
||||
die Community erhalten, sind aber nicht mehr deinem Account zuordenbar.</li>
|
||||
<li><strong>10 Jahre aufbewahrt (gesetzliche Pflicht):</strong> Rechnungen und
|
||||
Rechnungspositionen aus kostenpflichtigen Abonnements gemäß § 147 AO. Diese
|
||||
enthalten Name, E-Mail-Adresse und Rechnungsadresse zum Zeitpunkt der Rechnung
|
||||
und können vor Ablauf der Frist nicht gelöscht werden.</li>
|
||||
</ul>
|
||||
<p style="${S.p};margin-top:var(--space-3)">
|
||||
Es findet keine anonymisierte Weiterverarbeitung deiner privaten Inhalte
|
||||
(Tagebuch, Gesundheit, Notizen) zu Trainings- oder Statistikzwecken statt.
|
||||
</p>`)}
|
||||
|
||||
${sec('Mindestalter', `
|
||||
|
|
|
|||
|
|
@ -15,10 +15,35 @@ window.Page_erste_hilfe = (() => {
|
|||
// ----------------------------------------------------------------
|
||||
// DATA
|
||||
// ----------------------------------------------------------------
|
||||
const NOTFALLNUMMERN = [
|
||||
{ label: 'Tiergiftzentrale München', tel: '+4989 19240', display: '+49 89 19240' },
|
||||
{ label: 'Tiergiftzentrale Berlin', tel: '+4930 19240', display: '+49 30 19240' },
|
||||
{ label: 'Tiergiftzentrale Wien', tel: '+431 4064343', display: '+43 1 4064343' },
|
||||
// Liste von Notrufen, nach Land gruppiert.
|
||||
// Struktur ist erweiterbar: weitere Länder/Städte einfach an die jeweilige
|
||||
// Gruppe anhängen. Einträge mit tel:null werden als "TODO: Nummer einfügen"
|
||||
// dargestellt — Rendering kümmert sich um Optik und tel: -Link.
|
||||
const NOTFALLNUMMERN_GRUPPEN = [
|
||||
{
|
||||
land: 'Deutschland',
|
||||
flag: 'DE',
|
||||
eintraege: [
|
||||
{ label: 'Tiergiftzentrale München', tel: '+4989 19240', display: '+49 89 19240' },
|
||||
{ label: 'Tiergiftzentrale Berlin', tel: '+4930 19240', display: '+49 30 19240' },
|
||||
],
|
||||
},
|
||||
{
|
||||
land: 'Österreich',
|
||||
flag: 'AT',
|
||||
eintraege: [
|
||||
{ label: 'Vergiftungsinformationszentrale Wien', tel: '+431 4064343', display: '+43 1 4064343' },
|
||||
{ label: 'Veterinärmedizinische Universität Wien (Notfallklinik)', tel: null, display: 'TODO: Nummer einfügen' },
|
||||
],
|
||||
},
|
||||
{
|
||||
land: 'Schweiz',
|
||||
flag: 'CH',
|
||||
eintraege: [
|
||||
{ label: 'Tox Info Suisse (Tiergiftnotruf)', tel: null, display: 'TODO: Nummer einfügen (ggf. 145)' },
|
||||
{ label: 'Tierspital Zürich', tel: null, display: 'TODO: Nummer einfügen' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const SCHNELL = [
|
||||
|
|
@ -213,6 +238,7 @@ window.Page_erste_hilfe = (() => {
|
|||
_container.innerHTML = `
|
||||
<div id="eh-wrap" style="padding-bottom:var(--space-8)">
|
||||
|
||||
${_renderDisclaimer()}
|
||||
${_renderNotfallbanner()}
|
||||
${_renderSchnell()}
|
||||
|
||||
|
|
@ -244,13 +270,51 @@ window.Page_erste_hilfe = (() => {
|
|||
_activateTab('lebensgefahr');
|
||||
}
|
||||
|
||||
function _renderDisclaimer() {
|
||||
return `
|
||||
<div role="alert" style="display:flex;align-items:flex-start;gap:var(--space-3);
|
||||
background:#fef3c7;color:#78350f;border-left:4px solid #d97706;
|
||||
border-radius:var(--radius-md);padding:var(--space-3) var(--space-4);
|
||||
margin-bottom:var(--space-4);font-size:var(--text-sm);line-height:1.5">
|
||||
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:#d97706;width:22px;height:22px;margin-top:2px"><use href="/icons/phosphor.svg#warning"></use></svg>
|
||||
<div>
|
||||
<strong style="display:block;margin-bottom:2px">Diese Hinweise ersetzen keine tierärztliche Beratung.</strong>
|
||||
Im Notfall sofort einen Tierarzt aufsuchen!
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function _renderNotfallbanner() {
|
||||
const nums = NOTFALLNUMMERN.map(n => `
|
||||
<a href="tel:${n.tel}"
|
||||
style="display:flex;align-items:center;gap:var(--space-2);color:#fff;text-decoration:none;font-size:var(--text-sm);padding:var(--space-2) var(--space-3);background:rgba(255,255,255,0.15);border-radius:var(--radius-md)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone"></use></svg>
|
||||
<span><strong>${n.label}</strong><br>${n.display}</span>
|
||||
</a>
|
||||
const renderEintrag = (n) => {
|
||||
// Eintrag mit verfügbarer Nummer → tel:-Link
|
||||
if (n.tel) {
|
||||
return `
|
||||
<a href="tel:${n.tel}"
|
||||
style="display:flex;align-items:center;gap:var(--space-2);color:#fff;text-decoration:none;font-size:var(--text-sm);padding:var(--space-2) var(--space-3);background:rgba(255,255,255,0.15);border-radius:var(--radius-md)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone"></use></svg>
|
||||
<span><strong>${n.label}</strong><br>${n.display}</span>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
// Eintrag ohne Nummer → ausgegrauter Platzhalter
|
||||
return `
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);color:rgba(255,255,255,0.85);font-size:var(--text-sm);padding:var(--space-2) var(--space-3);background:rgba(255,255,255,0.08);border-radius:var(--radius-md);border:1px dashed rgba(255,255,255,0.35)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone"></use></svg>
|
||||
<span><strong>${n.label}</strong><br><em style="font-style:normal;opacity:.85">${n.display}</em></span>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const gruppen = NOTFALLNUMMERN_GRUPPEN.map(g => `
|
||||
<div>
|
||||
<div style="font-size:var(--text-xs);font-weight:var(--weight-semibold);color:rgba(255,255,255,0.85);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:var(--space-1)">
|
||||
${g.flag} · ${g.land}
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
|
||||
${g.eintraege.map(renderEintrag).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
return `
|
||||
|
|
@ -259,8 +323,8 @@ window.Page_erste_hilfe = (() => {
|
|||
<svg class="ph-icon" style="width:20px;height:20px" aria-hidden="true"><use href="/icons/phosphor.svg#siren"></use></svg>
|
||||
Tiergiftzentralen — jetzt anrufen
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
|
||||
${nums}
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
|
||||
${gruppen}
|
||||
</div>
|
||||
<p style="margin-top:var(--space-3);font-size:var(--text-xs);color:rgba(255,255,255,0.8)">
|
||||
Tierärztlicher Notdienst: Über die Tierarztsuche in der Banyaro-Karte
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ window.Page_impressum = (() => {
|
|||
<form id="contact-form" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-xs);font-weight:600;margin-bottom:4px;color:var(--c-text)">Name *</label>
|
||||
<label for="cf-name" style="display:block;font-size:var(--text-xs);font-weight:600;margin-bottom:4px;color:var(--c-text)">Name *</label>
|
||||
<input id="cf-name" type="text" required maxlength="100"
|
||||
placeholder="Dein Name"
|
||||
style="width:100%;padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
|
||||
|
|
@ -41,7 +41,7 @@ window.Page_impressum = (() => {
|
|||
color:var(--c-text);font-size:var(--text-sm);box-sizing:border-box">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-xs);font-weight:600;margin-bottom:4px;color:var(--c-text)">E-Mail *</label>
|
||||
<label for="cf-email" style="display:block;font-size:var(--text-xs);font-weight:600;margin-bottom:4px;color:var(--c-text)">E-Mail *</label>
|
||||
<input id="cf-email" type="email" required maxlength="200"
|
||||
placeholder="deine@email.de"
|
||||
style="width:100%;padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
|
||||
|
|
@ -50,7 +50,7 @@ window.Page_impressum = (() => {
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-xs);font-weight:600;margin-bottom:4px;color:var(--c-text)">Betreff *</label>
|
||||
<label for="cf-subject" style="display:block;font-size:var(--text-xs);font-weight:600;margin-bottom:4px;color:var(--c-text)">Betreff *</label>
|
||||
<input id="cf-subject" type="text" required maxlength="150"
|
||||
placeholder="Worum geht es?"
|
||||
style="width:100%;padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
|
||||
|
|
@ -58,7 +58,7 @@ window.Page_impressum = (() => {
|
|||
color:var(--c-text);font-size:var(--text-sm);box-sizing:border-box">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-xs);font-weight:600;margin-bottom:4px;color:var(--c-text)">Nachricht *</label>
|
||||
<label for="cf-message" style="display:block;font-size:var(--text-xs);font-weight:600;margin-bottom:4px;color:var(--c-text)">Nachricht *</label>
|
||||
<textarea id="cf-message" required maxlength="3000" rows="5"
|
||||
placeholder="Deine Nachricht…"
|
||||
style="width:100%;padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
|
||||
|
|
|
|||
|
|
@ -123,20 +123,69 @@ const UI = (() => {
|
|||
};
|
||||
overlay.addEventListener('focusin', _onFocusin);
|
||||
|
||||
_current = { overlay, onClose, _vvCleanup, _onFocusin };
|
||||
// -----------------------------------------------------
|
||||
// Accessibility: ESC schließt + Focus-Trap
|
||||
// -----------------------------------------------------
|
||||
const FOCUSABLE_SEL = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
const _getFocusables = () =>
|
||||
Array.from(modal?.querySelectorAll(FOCUSABLE_SEL) || [])
|
||||
.filter(el => el.offsetParent !== null || el === document.activeElement);
|
||||
|
||||
const _prevFocus = document.activeElement;
|
||||
|
||||
const _onKeydown = e => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if (e.key !== 'Tab') return;
|
||||
const focusables = _getFocusables();
|
||||
if (!focusables.length) { e.preventDefault(); return; }
|
||||
const first = focusables[0];
|
||||
const last = focusables[focusables.length - 1];
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === first || !modal.contains(document.activeElement)) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === last || !modal.contains(document.activeElement)) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', _onKeydown);
|
||||
|
||||
// Erstes fokussierbares Element autofokussieren (nach Render)
|
||||
setTimeout(() => {
|
||||
const focusables = _getFocusables();
|
||||
// Schließen-Button überspringen, falls weitere Elemente vorhanden
|
||||
const target = focusables.find(el => !el.classList.contains('modal-close-btn')) || focusables[0];
|
||||
target?.focus();
|
||||
}, 50);
|
||||
|
||||
_current = { overlay, onClose, _vvCleanup, _onFocusin, _onKeydown, _prevFocus };
|
||||
|
||||
return overlay.querySelector('.modal');
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (!_current) return;
|
||||
const { onClose, _vvCleanup, _onFocusin } = _current;
|
||||
const { onClose, _vvCleanup, _onFocusin, _onKeydown, _prevFocus } = _current;
|
||||
onClose?.();
|
||||
_vvCleanup?.();
|
||||
if (_onFocusin) _current.overlay.removeEventListener('focusin', _onFocusin);
|
||||
if (_onKeydown) document.removeEventListener('keydown', _onKeydown);
|
||||
_current.overlay.remove();
|
||||
document.documentElement.classList.remove('modal-open');
|
||||
_current = null;
|
||||
// Fokus auf vorheriges Element zurücksetzen (falls noch im DOM)
|
||||
if (_prevFocus && typeof _prevFocus.focus === 'function' && document.body.contains(_prevFocus)) {
|
||||
try { _prevFocus.focus(); } catch (_) {}
|
||||
}
|
||||
// iOS Safari setzt den Zoom nach Input-Fokus nicht zurück — Viewport kurz neu setzen
|
||||
const meta = document.querySelector('meta[name="viewport"]');
|
||||
if (meta) {
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@
|
|||
"Probeverpaarung mit IK-Simulation und genetischer Risikoanalyse",
|
||||
"Tierschutz-Check automatisch bei jeder Verpaarung",
|
||||
"KI-Züchter-Assistenz: Wurfankündigungen, Genetik, Paarungsanalyse",
|
||||
"Datenexport als HTML und ODS",
|
||||
"Datenexport als JSON (DSGVO Art. 20)",
|
||||
"Hunde-Filmdatenbank: 68 Filme, Serien und Dokumentationen sortier- und filterbar",
|
||||
"Filmdatenbank-Feature: Stirbt der Hund? — Taschentuch-Warnung",
|
||||
"Berühmte Hunde der Geschichte",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
============================================================ */
|
||||
|
||||
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
|
||||
const VER = '1094';
|
||||
const VER = '1095';
|
||||
const CACHE_VERSION = `by-v${VER}`;
|
||||
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