Sprint 13: WebCal-Abo / Kalender-Integration

- GET /api/webcal/token: erzeugt personl. Kalender-Token (einmalig)
- GET /api/webcal/{token}.ics: iCal-Feed mit Health-Erinnerungen,
  eigenen Events, Gassi-Treffen (erstellt + beigetreten), angenommenen Sittings
- RRULE für wiederkehrende Health-Einträge (intervall_tage)
- Migration: users.calendar_token (TEXT UNIQUE)
- Settings: "Kalender abonnieren" öffnet webcal://-Link + Kopier-Button
- api.js: API.webcal.getToken() / resetToken()
- SW-Cache: by-v104, APP_VER: 80
This commit is contained in:
rene 2026-04-16 22:39:50 +02:00
parent b58789373c
commit a4f74b6c64
8 changed files with 362 additions and 8 deletions

View file

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

View file

@ -400,6 +400,14 @@ const API = (() => {
});
}
// ----------------------------------------------------------
// WEBCAL
// ----------------------------------------------------------
const webcal = {
getToken: () => get('/webcal/token'),
resetToken: () => del('/webcal/token'),
};
// ----------------------------------------------------------
// ERROR-KLASSE
// ----------------------------------------------------------
@ -417,7 +425,7 @@ const API = (() => {
get, post, put, patch, del, upload,
auth, dogs, diary, health, tieraerzte, poison,
places, routes, walks, events, sitting, forum, lost, knigge, weather, push,
friends, chat,
friends, chat, webcal,
subscribeToPush, getLocation,
APIError,
};

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '79'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '80'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const App = (() => {

View file

@ -78,6 +78,12 @@ window.Page_settings = (() => {
<span>Push-Benachrichtigungen</span>
<span style="margin-left:auto;color:var(--c-text-secondary)"></span>
</div>
<div class="sidebar-item" id="settings-calendar-btn"
style="padding:var(--space-4);border-radius:0;border-bottom:1px solid var(--c-border)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calendar-dots"></use></svg>
<span>Kalender abonnieren</span>
<span style="margin-left:auto;color:var(--c-text-secondary)"></span>
</div>
<div class="sidebar-item" id="settings-logout-btn"
style="padding:var(--space-4);border-radius:0;cursor:pointer;
color:var(--c-danger)">
@ -171,6 +177,52 @@ window.Page_settings = (() => {
}
});
document.getElementById('settings-calendar-btn')?.addEventListener('click', async () => {
try {
const { token } = await API.webcal.getToken();
const url = `webcal://${location.host}/api/webcal/${token}.ics`;
const httpsUrl = `https://${location.host}/api/webcal/${token}.ics`;
UI.modal.open({
title: `${UI.icon('calendar-dots')} Kalender abonnieren`,
body: `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-4)">
Abonniere deinen persönlichen Ban-Yaro-Kalender. Er enthält Impf-Erinnerungen,
Läufigkeits-Termine, Events und Gassi-Treffen immer aktuell.
</p>
<div style="background:var(--c-bg);border-radius:var(--radius-md);
padding:var(--space-3) var(--space-4);
font-size:var(--text-xs);color:var(--c-text-secondary);
word-break:break-all;margin-bottom:var(--space-4)">
${UI.escHtml(httpsUrl)}
</div>
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
<a href="${UI.escHtml(url)}"
class="btn btn-primary"
style="text-align:center">
${UI.icon('calendar-dots')} In Kalender-App öffnen
</a>
<button class="btn btn-secondary" id="cal-copy-btn">
${UI.icon('clipboard-text')} URL kopieren
</button>
</div>
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-4)">
Tipp: iOS Einstellungen Kalender Accounts Account hinzufügen Andere Kalenderabo
</p>
`,
});
document.getElementById('cal-copy-btn')?.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(httpsUrl);
UI.toast.success('URL kopiert.');
} catch {
UI.toast.warning('Kopieren nicht möglich — URL oben manuell kopieren.');
}
});
} catch {
UI.toast.error('Kalender-Token konnte nicht geladen werden.');
}
});
document.getElementById('toggle-pocket-mode')?.addEventListener('change', e => {
localStorage.setItem('by_pocket_mode', String(e.target.checked));
UI.toast.info(e.target.checked

View file

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