Compare commits
9 commits
01081fcd75
...
bf26e5faf4
| Author | SHA1 | Date | |
|---|---|---|---|
| bf26e5faf4 | |||
| d399cb84cf | |||
| 3b79efb82b | |||
| 1c6ec6f17e | |||
| 619ff559e6 | |||
| b6fae96334 | |||
| 40b802ae86 | |||
| 1c8ed88dac | |||
| e5bf841d45 |
7 changed files with 138 additions and 49 deletions
|
|
@ -129,6 +129,49 @@ async def share_target(request: Request):
|
||||||
headers={"Cache-Control": "no-cache"}
|
headers={"Cache-Control": "no-cache"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Cache-Reset-Seite — löscht SW + Caches, leitet zur App weiter
|
||||||
|
@app.get("/update")
|
||||||
|
async def force_update():
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
html = """<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Ban Yaro — Aktualisieren</title>
|
||||||
|
<style>
|
||||||
|
body{font-family:-apple-system,sans-serif;display:flex;align-items:center;
|
||||||
|
justify-content:center;min-height:100vh;margin:0;background:#faf8f5}
|
||||||
|
.box{text-align:center;padding:2rem}
|
||||||
|
h1{color:#C4843A;margin-bottom:.5rem}
|
||||||
|
p{color:#666;margin-bottom:1.5rem}
|
||||||
|
.sp{width:44px;height:44px;border:4px solid #eee;border-top-color:#C4843A;
|
||||||
|
border-radius:50%;animation:s .8s linear infinite;margin:0 auto}
|
||||||
|
@keyframes s{to{transform:rotate(360deg)}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="box">
|
||||||
|
<h1>Ban Yaro</h1>
|
||||||
|
<p>App wird aktualisiert…</p>
|
||||||
|
<div class="sp"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(async () => {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
const regs = await navigator.serviceWorker.getRegistrations();
|
||||||
|
await Promise.all(regs.map(r => r.unregister()));
|
||||||
|
}
|
||||||
|
const keys = await caches.keys();
|
||||||
|
await Promise.all(keys.map(k => caches.delete(k)));
|
||||||
|
window.location.replace('/');
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
return HTMLResponse(html, headers={"Cache-Control": "no-store"})
|
||||||
|
|
||||||
|
|
||||||
# SPA Fallback — ALLE nicht-API-Routen gehen zur index.html
|
# SPA Fallback — ALLE nicht-API-Routen gehen zur index.html
|
||||||
@app.get("/{full_path:path}")
|
@app.get("/{full_path:path}")
|
||||||
async def spa_fallback(full_path: str):
|
async def spa_fallback(full_path: str):
|
||||||
|
|
|
||||||
|
|
@ -1565,9 +1565,20 @@ textarea.form-control {
|
||||||
ZENTRALE KARTE (map.js)
|
ZENTRALE KARTE (map.js)
|
||||||
============================================================ */
|
============================================================ */
|
||||||
.map-full-layout {
|
.map-full-layout {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
inset: 0;
|
top: calc(var(--header-height) + var(--safe-top));
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: calc(var(--nav-bottom-height) + var(--safe-bottom));
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.map-full-layout {
|
||||||
|
top: 0;
|
||||||
|
left: var(--nav-sidebar-width);
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.map-full {
|
.map-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -118,17 +118,15 @@
|
||||||
.sidebar-backdrop { display: none !important; }
|
.sidebar-backdrop { display: none !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile Sidebar als Drawer */
|
/* Karte: volle Höhe, kein Scrollen, kein Padding */
|
||||||
@media (max-width: 767px) {
|
#page-map {
|
||||||
#sidebar {
|
height: 100%;
|
||||||
display: flex;
|
overflow: hidden;
|
||||||
z-index: 500;
|
}
|
||||||
transform: translateX(-100%);
|
#page-map > .page-body {
|
||||||
transition: transform 0.28s ease;
|
padding: 0 !important;
|
||||||
}
|
overflow: hidden;
|
||||||
#sidebar.open {
|
height: 100%;
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------
|
/* ------------------------------------------------------------
|
||||||
|
|
@ -267,6 +265,30 @@
|
||||||
#sidebar { display: flex; }
|
#sidebar { display: flex; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Sidebar als Drawer */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
#sidebar {
|
||||||
|
display: flex !important; /* überschreibt display:none aus Base */
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: var(--nav-sidebar-width);
|
||||||
|
z-index: 1000; /* klar über Backdrop (999) */
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.28s ease;
|
||||||
|
background: var(--c-surface);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
#sidebar.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
box-shadow: 4px 0 24px rgba(0,0,0,0.18);
|
||||||
|
}
|
||||||
|
.sidebar-backdrop {
|
||||||
|
z-index: 999; /* unter Sidebar, über Seiteninhalt */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-logo {
|
.sidebar-logo {
|
||||||
padding: var(--space-6) var(--space-5);
|
padding: var(--space-6) var(--space-5);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -20,33 +20,17 @@
|
||||||
|
|
||||||
<title>Ban Yaro</title>
|
<title>Ban Yaro</title>
|
||||||
|
|
||||||
<!-- CSS: Reihenfolge ist wichtig -->
|
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||||
<link rel="stylesheet" href="/css/design-system.css">
|
<link rel="stylesheet" href="/css/design-system.css">
|
||||||
<link rel="stylesheet" href="/css/layout.css">
|
<link rel="stylesheet" href="/css/layout.css?v=32">
|
||||||
<link rel="stylesheet" href="/css/components.css">
|
<link rel="stylesheet" href="/css/components.css?v=32">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- ============================================================
|
<!-- Backdrop + Sidebar direkt im body (kein Ancestor-Stacking-Context) -->
|
||||||
APP SHELL
|
<div id="sidebar-backdrop" class="sidebar-backdrop"></div>
|
||||||
============================================================ -->
|
|
||||||
<div id="app">
|
|
||||||
|
|
||||||
<!-- MOBILE HEADER (wird per JS mit Seitentitel befüllt) -->
|
<nav id="sidebar" role="navigation" aria-label="Hauptnavigation">
|
||||||
<header id="app-header">
|
|
||||||
<button class="header-back hidden" id="header-back" aria-label="Zurück">←</button>
|
|
||||||
<div id="header-dog-switcher" class="dog-switcher">
|
|
||||||
<span class="header-title" id="header-title">Ban Yaro</span>
|
|
||||||
</div>
|
|
||||||
<div id="header-actions"></div>
|
|
||||||
<button class="header-menu-btn" id="header-menu-btn" aria-label="Menü">☰</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Backdrop für mobile Sidebar-Drawer -->
|
|
||||||
<div id="sidebar-backdrop" class="sidebar-backdrop"></div>
|
|
||||||
|
|
||||||
<!-- DESKTOP SIDEBAR -->
|
|
||||||
<nav id="sidebar" role="navigation" aria-label="Hauptnavigation">
|
|
||||||
<div class="sidebar-logo" id="sidebar-dog-switcher">
|
<div class="sidebar-logo" id="sidebar-dog-switcher">
|
||||||
<img class="sidebar-logo-img" src="/icons/icon-180.png" alt="Ban Yaro">
|
<img class="sidebar-logo-img" src="/icons/icon-180.png" alt="Ban Yaro">
|
||||||
<span class="sidebar-logo-text">Ban Yaro</span>
|
<span class="sidebar-logo-text">Ban Yaro</span>
|
||||||
|
|
@ -111,6 +95,21 @@
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- ============================================================
|
||||||
|
APP SHELL
|
||||||
|
============================================================ -->
|
||||||
|
<div id="app">
|
||||||
|
|
||||||
|
<!-- MOBILE HEADER -->
|
||||||
|
<header id="app-header">
|
||||||
|
<button class="header-back hidden" id="header-back" aria-label="Zurück">←</button>
|
||||||
|
<div id="header-dog-switcher" class="dog-switcher">
|
||||||
|
<span class="header-title" id="header-title">Ban Yaro</span>
|
||||||
|
</div>
|
||||||
|
<div id="header-actions"></div>
|
||||||
|
<button class="header-menu-btn" id="header-menu-btn" aria-label="Menü">☰</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- HAUPT-INHALTSBEREICH -->
|
<!-- HAUPT-INHALTSBEREICH -->
|
||||||
<main id="page-content" role="main">
|
<main id="page-content" role="main">
|
||||||
|
|
||||||
|
|
@ -215,9 +214,9 @@
|
||||||
<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"></script>
|
<script src="/js/api.js?v=32"></script>
|
||||||
<script src="/js/ui.js"></script>
|
<script src="/js/ui.js?v=32"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js?v=32"></script>
|
||||||
|
|
||||||
<!-- Feature-Seiten werden lazy geladen -->
|
<!-- Feature-Seiten werden lazy geladen -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,11 @@ const App = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function _openSidebar() {
|
function _openSidebar() {
|
||||||
document.getElementById('sidebar')?.classList.add('open');
|
const s = document.getElementById('sidebar');
|
||||||
|
if (!s) { UI.toast.error('sidebar: NULL'); return; }
|
||||||
|
s.classList.add('open');
|
||||||
|
const cs = getComputedStyle(s);
|
||||||
|
UI.toast.info(`display:${cs.display} | transform:${cs.transform} | z:${cs.zIndex}`);
|
||||||
document.getElementById('sidebar-backdrop')?.classList.add('visible');
|
document.getElementById('sidebar-backdrop')?.classList.add('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -148,8 +148,9 @@ window.Page_map = (() => {
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 })
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 })
|
||||||
.addTo(_map);
|
.addTo(_map);
|
||||||
|
|
||||||
// invalidateSize nach kurzer Verzögerung, damit der Browser das Layout abgeschlossen hat
|
// invalidateSize zweimal: einmal früh, einmal nach möglichen Layout-Delays
|
||||||
setTimeout(() => _map.invalidateSize(), 150);
|
setTimeout(() => _map.invalidateSize(), 100);
|
||||||
|
setTimeout(() => _map.invalidateSize(), 600);
|
||||||
window.addEventListener('resize', () => _map.invalidateSize());
|
window.addEventListener('resize', () => _map.invalidateSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,11 @@
|
||||||
Offline-Cache + Push Notifications
|
Offline-Cache + Push Notifications
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v27';
|
const CACHE_VERSION = 'by-v33';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
|
|
||||||
// Diese Dateien werden beim Install gecacht (App Shell)
|
// index.html wird NICHT pre-gecacht (immer Network-First)
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
'/',
|
|
||||||
'/css/design-system.css',
|
'/css/design-system.css',
|
||||||
'/css/layout.css',
|
'/css/layout.css',
|
||||||
'/css/components.css',
|
'/css/components.css',
|
||||||
|
|
@ -44,7 +43,7 @@ self.addEventListener('activate', event => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// FETCH — Cache-First für statische Assets, Network-First für API
|
// FETCH — Network-First für HTML, Cache-First für Assets, Network für API
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
self.addEventListener('fetch', event => {
|
self.addEventListener('fetch', event => {
|
||||||
const url = new URL(event.request.url);
|
const url = new URL(event.request.url);
|
||||||
|
|
@ -60,12 +59,25 @@ self.addEventListener('fetch', event => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigation (index.html): immer Network-First
|
||||||
|
if (event.request.mode === 'navigate') {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.then(response => {
|
||||||
|
const clone = response.clone();
|
||||||
|
caches.open(CACHE_STATIC).then(c => c.put(event.request, clone));
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => caches.match('/'))
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Statische Assets: Cache-First
|
// Statische Assets: Cache-First
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches.match(event.request)
|
caches.match(event.request)
|
||||||
.then(cached => cached || fetch(event.request)
|
.then(cached => cached || fetch(event.request)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// Erfolgreiche Responses für statische Assets cachen
|
|
||||||
if (response.ok && event.request.method === 'GET') {
|
if (response.ok && event.request.method === 'GET') {
|
||||||
const clone = response.clone();
|
const clone = response.clone();
|
||||||
caches.open(CACHE_STATIC).then(c => c.put(event.request, clone));
|
caches.open(CACHE_STATIC).then(c => c.put(event.request, clone));
|
||||||
|
|
@ -74,7 +86,6 @@ self.addEventListener('fetch', event => {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Offline-Fallback: App Shell zurückgeben
|
|
||||||
if (event.request.mode === 'navigate') {
|
if (event.request.mode === 'navigate') {
|
||||||
return caches.match('/');
|
return caches.match('/');
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +147,6 @@ self.addEventListener('notificationclick', event => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
clients.matchAll({ type: 'window', includeUncontrolled: true })
|
clients.matchAll({ type: 'window', includeUncontrolled: true })
|
||||||
.then(windowClients => {
|
.then(windowClients => {
|
||||||
// Offenes Fenster fokussieren
|
|
||||||
for (const client of windowClients) {
|
for (const client of windowClients) {
|
||||||
if (client.url.includes(self.location.origin)) {
|
if (client.url.includes(self.location.origin)) {
|
||||||
client.focus();
|
client.focus();
|
||||||
|
|
@ -144,7 +154,6 @@ self.addEventListener('notificationclick', event => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Neues Fenster öffnen
|
|
||||||
return clients.openWindow(url);
|
return clients.openWindow(url);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue