feat: birthday tracking, dashboard KPIs, and app name customization (#88)

- Add Birthdays module: CRUD with calendar/reminder auto-sync, photo upload, age notes
- Add DB migration 18 (birthdays table with calendar_event_id, trigger, indexes)
- Add dashboard widgets: birthdays, family participants, budget overview
- Add Settings > General: admins can set a custom app name (reflected in title/sidebar/login)
- Improve service worker: network-first caching for mutable JS/CSS assets
- Add translations for 16 locales (birthday keys)

Fixes applied during integration:
- innerHTML replaced with insertAdjacentHTML/replaceChildren throughout birthdays.js and dashboard.js
- docker-compose.yml personal dev changes reverted

Co-authored-by: Rafael Foster <rafaelgfoster@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ulas Kalayci
2026-04-27 07:37:09 +02:00
39 changed files with 4026 additions and 156 deletions
+25 -3
View File
@@ -8,16 +8,30 @@ import { auth } from '/api.js';
import { t } from '/i18n.js';
const VERSION_URL = '/api/v1/version';
const DEFAULT_APP_NAME = 'Oikos';
const APP_NAME_STORAGE_KEY = 'oikos-app-name';
function getStoredAppName() {
return localStorage.getItem(APP_NAME_STORAGE_KEY) || DEFAULT_APP_NAME;
}
function setAppBranding(appName) {
const name = String(appName || '').trim() || DEFAULT_APP_NAME;
document.title = name;
const titleEl = document.querySelector('.login-hero__title');
if (titleEl) titleEl.textContent = name;
}
/**
* Rendert die Login-Seite in den gegebenen Container.
* @param {HTMLElement} container
*/
export async function render(container) {
const storedAppName = getStoredAppName();
container.innerHTML = `
<main class="login-page" id="main-content">
<div class="login-hero">
<h1 class="login-hero__title">Oikos</h1>
<h1 class="login-hero__title">${storedAppName}</h1>
<p class="login-hero__tagline">${t('login.tagline')}</p>
</div>
<div class="login-card card card--padded">
@@ -67,9 +81,17 @@ export async function render(container) {
const submitBtn = container.querySelector('#login-btn');
const versionEl = container.querySelector('#login-version');
fetch(VERSION_URL)
setAppBranding(storedAppName);
fetch(VERSION_URL, { cache: 'no-store' })
.then((r) => r.json())
.then((d) => { versionEl.textContent = t('login.version', { version: d.version }); })
.then((d) => {
if (d?.app_name) {
try { localStorage.setItem(APP_NAME_STORAGE_KEY, d.app_name); } catch (_) {}
setAppBranding(d.app_name);
}
versionEl.textContent = t('login.version', { version: d.version });
})
.catch(() => {});
form.addEventListener('submit', async (e) => {