Unified /einstellungen page replaces the standalone CalDAV screen. Three
tabs today (Profil / Benachrichtigungen / CalDAV); adding more is additive
(one <a> in the tab nav, one <section> panel, one loader). Tab switching
is client-side from ?tab=<name> — default tab is Profil.
Profil tab lets users fix onboarding data without admin intervention:
display name, office, role, Dezernat, language. Email is read-only (the
source of truth is auth.users and an account-level change is out of
scope for the settings page).
Benachrichtigungen tab exposes deadline reminder preferences as a master
toggle plus three per-kind sub-toggles (overdue / tomorrow / weekly).
Preferences land in paliad.users.email_preferences (JSONB); missing keys
are treated as opt-in so existing users keep the behaviour they had
before the page shipped.
CalDAV tab is the old /einstellungen/caldav screen ported inline.
/einstellungen/caldav now 301-redirects to /einstellungen?tab=caldav so
bookmarks keep working.
Backend:
- PATCH /api/me (handlers/users.go) mutates the caller's paliad.users
row. Attempts to include "email" in the body return 400 — the field is
always server-authoritative.
- UserService.UpdateProfile builds a dynamic UPDATE from the pointer
fields supplied; omitted keys are left untouched. Re-uses the
admin-bootstrap guard for role changes.
- GetByID SELECT now includes lang + email_preferences so /api/me
returns the data the settings page needs without a second round-trip.
- ReminderService consults email_preferences before sending — the helper
reminderEnabled covers the master switch and per-kind overrides; corrupt
JSON falls back to on so a bad row can't silence reminders.
- Migration 017 adds email_preferences jsonb NOT NULL DEFAULT '{}' and
upgrades lang from nullable (from 016) to NOT NULL DEFAULT 'de' with a
one-shot backfill. Down restores the nullable lang and drops
email_preferences.
Model change: User.Lang moved from *string to string — it's NOT NULL in
the DB now, so the indirection was carrying no information. Inviter.Lang
and reminder row structs followed suit; the templates and callers used
""/"en" comparisons that translate 1:1.
Sidebar: the "Einstellungen" group now links to /einstellungen (instead
of just /einstellungen/caldav); the CalDAV sub-item is folded into the
tab nav on the page itself.
Tests: reminderEnabled has table-driven coverage (master switch,
per-kind, corrupt JSON, non-bool values). DB-backed user tests still
skip without TEST_DATABASE_URL as before.
Verified: go build ./..., go vet ./..., go test ./..., bun run build —
all clean.
118 lines
5.5 KiB
TypeScript
118 lines
5.5 KiB
TypeScript
import { mkdir, cp, rm } from "fs/promises";
|
|
import { join } from "path";
|
|
import { renderIndex } from "./src/index";
|
|
import { renderLogin } from "./src/login";
|
|
import { renderKostenrechner } from "./src/kostenrechner";
|
|
import { renderFristenrechner } from "./src/fristenrechner";
|
|
import { renderDownloads } from "./src/downloads";
|
|
import { renderLinks } from "./src/links";
|
|
import { renderGlossar } from "./src/glossar";
|
|
import { renderGebuehrentabellen } from "./src/gebuehrentabellen";
|
|
import { renderChecklisten } from "./src/checklisten";
|
|
import { renderChecklistenDetail } from "./src/checklisten-detail";
|
|
import { renderChecklistenInstance } from "./src/checklisten-instance";
|
|
import { renderGerichte } from "./src/gerichte";
|
|
import { renderAkten } from "./src/akten";
|
|
import { renderAktenNeu } from "./src/akten-neu";
|
|
import { renderAktenDetail } from "./src/akten-detail";
|
|
import { renderFristen } from "./src/fristen";
|
|
import { renderFristenNeu } from "./src/fristen-neu";
|
|
import { renderFristenDetail } from "./src/fristen-detail";
|
|
import { renderFristenKalender } from "./src/fristen-kalender";
|
|
import { renderTermine } from "./src/termine";
|
|
import { renderTermineNeu } from "./src/termine-neu";
|
|
import { renderTermineDetail } from "./src/termine-detail";
|
|
import { renderTermineKalender } from "./src/termine-kalender";
|
|
import { renderEinstellungen } from "./src/einstellungen";
|
|
import { renderDashboard } from "./src/dashboard";
|
|
import { renderOnboarding } from "./src/onboarding";
|
|
|
|
const DIST = join(import.meta.dir, "dist");
|
|
|
|
async function build() {
|
|
// Clean dist/
|
|
await rm(DIST, { recursive: true, force: true });
|
|
await mkdir(join(DIST, "assets"), { recursive: true });
|
|
|
|
// Bundle client-side JS
|
|
const result = await Bun.build({
|
|
entrypoints: [
|
|
join(import.meta.dir, "src/client/index.ts"),
|
|
join(import.meta.dir, "src/client/login.ts"),
|
|
join(import.meta.dir, "src/client/kostenrechner.ts"),
|
|
join(import.meta.dir, "src/client/fristenrechner.ts"),
|
|
join(import.meta.dir, "src/client/downloads.ts"),
|
|
join(import.meta.dir, "src/client/links.ts"),
|
|
join(import.meta.dir, "src/client/glossar.ts"),
|
|
join(import.meta.dir, "src/client/gebuehrentabellen.ts"),
|
|
join(import.meta.dir, "src/client/checklisten.ts"),
|
|
join(import.meta.dir, "src/client/checklisten-detail.ts"),
|
|
join(import.meta.dir, "src/client/checklisten-instance.ts"),
|
|
join(import.meta.dir, "src/client/gerichte.ts"),
|
|
join(import.meta.dir, "src/client/akten.ts"),
|
|
join(import.meta.dir, "src/client/akten-neu.ts"),
|
|
join(import.meta.dir, "src/client/akten-detail.ts"),
|
|
join(import.meta.dir, "src/client/fristen.ts"),
|
|
join(import.meta.dir, "src/client/fristen-neu.ts"),
|
|
join(import.meta.dir, "src/client/fristen-detail.ts"),
|
|
join(import.meta.dir, "src/client/fristen-kalender.ts"),
|
|
join(import.meta.dir, "src/client/termine.ts"),
|
|
join(import.meta.dir, "src/client/termine-neu.ts"),
|
|
join(import.meta.dir, "src/client/termine-detail.ts"),
|
|
join(import.meta.dir, "src/client/termine-kalender.ts"),
|
|
join(import.meta.dir, "src/client/einstellungen.ts"),
|
|
join(import.meta.dir, "src/client/dashboard.ts"),
|
|
join(import.meta.dir, "src/client/onboarding.ts"),
|
|
],
|
|
outdir: join(DIST, "assets"),
|
|
naming: "[name].js",
|
|
minify: true,
|
|
});
|
|
|
|
if (!result.success) {
|
|
console.error("JS build failed:");
|
|
for (const log of result.logs) {
|
|
console.error(log);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
// Copy CSS
|
|
await cp(
|
|
join(import.meta.dir, "src/styles/global.css"),
|
|
join(DIST, "assets/global.css"),
|
|
);
|
|
|
|
// Render HTML pages
|
|
await Bun.write(join(DIST, "index.html"), renderIndex());
|
|
await Bun.write(join(DIST, "login.html"), renderLogin("login.js"));
|
|
await Bun.write(join(DIST, "kostenrechner.html"), renderKostenrechner());
|
|
await Bun.write(join(DIST, "fristenrechner.html"), renderFristenrechner());
|
|
await Bun.write(join(DIST, "downloads.html"), renderDownloads());
|
|
await Bun.write(join(DIST, "links.html"), renderLinks());
|
|
await Bun.write(join(DIST, "glossar.html"), renderGlossar());
|
|
await Bun.write(join(DIST, "gebuehrentabellen.html"), renderGebuehrentabellen());
|
|
await Bun.write(join(DIST, "checklisten.html"), renderChecklisten());
|
|
await Bun.write(join(DIST, "checklisten-detail.html"), renderChecklistenDetail());
|
|
await Bun.write(join(DIST, "checklisten-instance.html"), renderChecklistenInstance());
|
|
await Bun.write(join(DIST, "gerichte.html"), renderGerichte());
|
|
await Bun.write(join(DIST, "akten.html"), renderAkten());
|
|
await Bun.write(join(DIST, "akten-neu.html"), renderAktenNeu());
|
|
await Bun.write(join(DIST, "akten-detail.html"), renderAktenDetail());
|
|
await Bun.write(join(DIST, "fristen.html"), renderFristen());
|
|
await Bun.write(join(DIST, "fristen-neu.html"), renderFristenNeu());
|
|
await Bun.write(join(DIST, "fristen-detail.html"), renderFristenDetail());
|
|
await Bun.write(join(DIST, "fristen-kalender.html"), renderFristenKalender());
|
|
await Bun.write(join(DIST, "termine.html"), renderTermine());
|
|
await Bun.write(join(DIST, "termine-neu.html"), renderTermineNeu());
|
|
await Bun.write(join(DIST, "termine-detail.html"), renderTermineDetail());
|
|
await Bun.write(join(DIST, "termine-kalender.html"), renderTermineKalender());
|
|
await Bun.write(join(DIST, "einstellungen.html"), renderEinstellungen());
|
|
await Bun.write(join(DIST, "dashboard.html"), renderDashboard());
|
|
await Bun.write(join(DIST, "onboarding.html"), renderOnboarding());
|
|
|
|
console.log("Build complete \u2192 dist/");
|
|
}
|
|
|
|
build();
|