fix(oberflaeche): Kopfleiste einzeilig halten, Demo-Uhr feststellen
Zwei Mängel, die erst in den aufgenommenen Bildern auffielen. Auf Handybreite brach die Kopfleiste um und drängte den Namen der Anwendung in die zweite Zeile, mitten zwischen die Menüpunkte. Sie bleibt jetzt auf allen Breiten einzeilig; wird es eng, scrollt die Navigation waagerecht. Das Tagesdatum auf der Startseite wird serverseitig gerendert, weshalb Playwrights Uhr nicht griff — die Bilder hätten sich täglich geändert. In der Screenshot-Instanz (DEMO_MODUS=true) steht die Uhr jetzt fest. Reine Funktionen bekommen den Zeitpunkt weiterhin als Parameter; src/server/ zeitgeber.ts ist die eine Stelle, an der er entsteht. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cf28ec19f5
commit
6d04bc2c1c
@@ -0,0 +1,118 @@
|
|||||||
|
# Entwicklungstagebuch
|
||||||
|
|
||||||
|
Neuester Eintrag oben. Die Dauer je Sitzung stammt aus `devlog/zeiten.csv`,
|
||||||
|
die Übersicht aus `scripts/zeit.sh summe`.
|
||||||
|
|
||||||
|
## Zeit je Meilenstein
|
||||||
|
|
||||||
|
| Meilenstein | Art | Minuten | Stunden |
|
||||||
|
| ----------- | ------ | ----------------------------------- | ------- |
|
||||||
|
| M1 | claude | _(wird beim Abschluss eingetragen)_ | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-09-10 – M1: Fundament
|
||||||
|
|
||||||
|
**Ziel der Sitzung:** Das Projekt von Grund auf aufsetzen: Gerüst,
|
||||||
|
Datenbank, Anmeldung, Rollen, Audit, Instanzsteuerung, Betrieb und
|
||||||
|
Dokumentation.
|
||||||
|
|
||||||
|
### Erledigt
|
||||||
|
|
||||||
|
- Projektgerüst mit Next 16, React 19, TypeScript strict, Tailwind 4,
|
||||||
|
alle Versionen exakt gepinnt
|
||||||
|
- `npm run pruefen` als Sammelprüfung: Prettier, ESLint, `tsc`, Vitest,
|
||||||
|
ShellCheck
|
||||||
|
- Datenmodell und erste Migration: Benutzer mit Rolle, OIDC-Konten,
|
||||||
|
Passkeys, WebAuthn-Herausforderungen, Bereiche, Audit
|
||||||
|
- Anmeldung über OIDC, Passkey und Passwort, je Instanz zuschaltbar
|
||||||
|
- Rollenprüfung in Seite **und** Server Action, Audit je Schreibaktion
|
||||||
|
- Instanzsteuerung mit 404 statt Hinweis
|
||||||
|
- Bereiche verwalten, Protokollseite, Startseite, Einstellungen
|
||||||
|
- `/api/health`, Dockerfile, Compose-Stapel mit Migrationsdienst
|
||||||
|
- `backup.sh`, `update.sh`, `scripts/release.sh`, `scripts/zeit.sh`
|
||||||
|
- Unit-, Integrations-, Migrations- und E2E-Tests
|
||||||
|
- Screenshot-Kette mit eigener Wegwerf-Instanz
|
||||||
|
- README, Schnellstart, Handbuch, ROADMAP, CHANGELOG
|
||||||
|
|
||||||
|
### Entscheidungen mit Begründung
|
||||||
|
|
||||||
|
**Prisma 7.10.0 statt der notierten Fassung 6.** Fassung 6 ist überholt,
|
||||||
|
Fassung 8 liegt nur als RC auf `latest`. Prisma 7 nimmt die Verbindung
|
||||||
|
nicht mehr im Schema entgegen — sie steht in `prisma.config.ts`, der
|
||||||
|
Client bekommt den pg-Treiberadapter.
|
||||||
|
|
||||||
|
**TypeScript 6.0.3 statt 7.0.2.** TS 7 ist zwar `latest`, aber
|
||||||
|
`typescript-eslint@8.70` verlangt `typescript >=4.8.4 <6.1.0`. Mit TS 7
|
||||||
|
wäre die Lint-Kette kaputt gewesen.
|
||||||
|
|
||||||
|
**ESLint 9.39.5 statt 10.** Das in `eslint-config-next@16` gebündelte
|
||||||
|
`eslint-plugin-react` ruft noch `context.getFilename()`, das ESLint 10
|
||||||
|
entfernt hat. Der Lint-Lauf brach reproduzierbar ab.
|
||||||
|
|
||||||
|
**Eigener Auth.js-Adapter.** Der mitgelieferte `@auth/prisma-adapter`
|
||||||
|
greift fest auf `prisma.user` und `prisma.account` zu; unsere Modelle
|
||||||
|
heißen `Benutzer` und `Konto`. Der Adapter ist schmal und ersparte es,
|
||||||
|
die Fachbegriffe der Konvention zu opfern.
|
||||||
|
|
||||||
|
**Passkeys direkt über SimpleWebAuthn 14.** Der WebAuthn-Provider von
|
||||||
|
Auth.js ist als experimentell markiert und auf SimpleWebAuthn 9
|
||||||
|
festgelegt. Direkt angebunden bleibt die Fassung aktuell und der Ablauf
|
||||||
|
nachvollziehbar. Für die npm-Auflösung brauchte es `overrides`, weil
|
||||||
|
next-auth die älteren Fassungen als optionale Peers deklariert.
|
||||||
|
|
||||||
|
**Sitzungen als JWT, Rolle trotzdem aus der Datenbank.** Credentials-
|
||||||
|
Provider verlangen JWT. Damit eine Sperre nicht bis zum Ablauf des Tokens
|
||||||
|
wirkungslos bleibt, liest der `session`-Callback Rolle und Status bei
|
||||||
|
jeder Anfrage frisch. Die Tabellen `sitzung` und `verifizierungs_token`
|
||||||
|
wären damit tot und wurden wieder entfernt, solange es nur eine einzige
|
||||||
|
Migration gab.
|
||||||
|
|
||||||
|
**Migrationen als eigener Compose-Dienst.** Der Versuch, nur die
|
||||||
|
Prisma-CLI ins schlanke Laufzeitabbild zu kopieren, scheiterte an ihren
|
||||||
|
transitiven Modulen (`effect` über `@prisma/config`). Alle mitzunehmen
|
||||||
|
hätte das Abbild aufgebläht. Ein Einmaldienst aus der Bau-Stufe läuft vor
|
||||||
|
der App und muss sauber durchlaufen.
|
||||||
|
|
||||||
|
**Aufgabe, Zeitblock und Kalenderkonto bewusst nicht modelliert.** Sie
|
||||||
|
kommen mit M2, M4 und M3 — dann mit den Feldern, die der jeweilige
|
||||||
|
Provider wirklich braucht, statt heute geraten.
|
||||||
|
|
||||||
|
### Probleme und Lösungen
|
||||||
|
|
||||||
|
| Problem | Lösung |
|
||||||
|
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `prisma validate` lehnte `url` im Schema ab | Prisma 7 erwartet die Verbindung in `prisma.config.ts` und einen Treiberadapter im Client. |
|
||||||
|
| Integrationstest brach nach der ersten erwarteten Verletzung ab | In PostgreSQL bricht ein fehlgeschlagener Befehl die ganze Transaktion ab. Der Test läuft jetzt ohne Sammeltransaktion; alle Testdaten tragen eine Kennung und werden am Ende entfernt. |
|
||||||
|
| `prisma migrate diff` kannte `--from-url` nicht | In Prisma 7 heißen die Flaggen anders. Der Test vergleicht jetzt die eingespielte Wegwerf-Datenbank über `--from-config-datasource` gegen das Schema — und braucht damit keine Schattendatenbank. |
|
||||||
|
| Nach der Anmeldung ging es nicht zum ursprünglichen Ziel zurück | Das Layout leitet um, bevor die Seite drankommt, und kennt seinen Pfad nicht. Ein schlanker Proxy reicht ihn als Kopfzeile durch — ohne Anmeldelogik, weil er im Edge-Umfeld läuft. |
|
||||||
|
| Der Docker-Bau brach beim Sammeln der Seitendaten ab | Prisma-Client und Auth.js-Konfiguration wurden beim Laden des Moduls ausgewertet und brauchten Umgebungsvariablen, die es zur Bauzeit nicht gibt. Beide entstehen jetzt faul. |
|
||||||
|
| Ein Typfehler in `scripts/screenshots.ts` blockierte den Abbild-Bau | Entwicklungsskripte gehören nicht in den Docker-Kontext. `.dockerignore` schließt sie aus; geprüft werden sie von `npm run pruefen`. |
|
||||||
|
| ESLint beanstandete `setState` im Effekt | Der Hydratations-Trick im Farbschema-Knopf war unnötig: `next-themes` setzt die `dark`-Klasse vor der Hydratation, das erledigt CSS ohne Zustand. |
|
||||||
|
|
||||||
|
### Eigene Fehler, die ich korrigiert habe
|
||||||
|
|
||||||
|
- Die ersten vier Commits lagen direkt auf `main` statt auf einem
|
||||||
|
Meilenstein-Zweig und ohne Attributionszeile. Historie umgebaut:
|
||||||
|
`main` trägt nur noch einen leeren Wurzel-Commit, die Arbeit liegt auf
|
||||||
|
`m1-fundament`.
|
||||||
|
- Ein `git add -A` zog die Proxy-Umbenennung in den Doku-Commit. Wieder
|
||||||
|
getrennt.
|
||||||
|
- `scripts/screenshots.ts` wurde geschrieben, ohne danach `npm run
|
||||||
|
pruefen` laufen zu lassen; der Typfehler fiel erst im Docker-Bau auf.
|
||||||
|
|
||||||
|
### Offene Punkte
|
||||||
|
|
||||||
|
- Die `origin`-URL fehlt noch. Push und Merge stehen ohnehin erst nach
|
||||||
|
der Freigabe an.
|
||||||
|
- Der Schnellstart endet beim Passkey statt beim ersten Kalenderkonto und
|
||||||
|
der ersten Aufgabe — beides gibt es erst mit M3 und M2.
|
||||||
|
- Der Demo-Seed enthält noch keine erfundenen Termine; einen
|
||||||
|
Demo-Kalenderprovider gibt es erst mit M3.
|
||||||
|
- Lokal weichen die Ports ab (Datenbank 5435, App 3001), weil 5432 und
|
||||||
|
3000 auf diesem Rechner belegt sind. In `.env.example` stehen die
|
||||||
|
üblichen Werte.
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
Siehe `git log --oneline main..m1-fundament`.
|
||||||
@@ -10,10 +10,11 @@ import { wochenKapazitaet } from '@/lib/bereich'
|
|||||||
import { alsDatum, alsDauer } from '@/lib/zeit'
|
import { alsDatum, alsDauer } from '@/lib/zeit'
|
||||||
import { verlangeAnmeldung } from '@/server/berechtigung'
|
import { verlangeAnmeldung } from '@/server/berechtigung'
|
||||||
import { db } from '@/server/db'
|
import { db } from '@/server/db'
|
||||||
|
import { jetzt } from '@/server/zeitgeber'
|
||||||
|
|
||||||
export default async function StartSeite() {
|
export default async function StartSeite() {
|
||||||
const benutzer = await verlangeAnmeldung('/')
|
const benutzer = await verlangeAnmeldung('/')
|
||||||
const jetzt = new Date()
|
const heute = jetzt()
|
||||||
|
|
||||||
const bereiche = await db.bereich.findMany({
|
const bereiche = await db.bereich.findMany({
|
||||||
where: { aktiv: true },
|
where: { aktiv: true },
|
||||||
@@ -26,7 +27,7 @@ export default async function StartSeite() {
|
|||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<header>
|
<header>
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Guten Tag, {anrede}</h1>
|
<h1 className="text-2xl font-semibold tracking-tight">Guten Tag, {anrede}</h1>
|
||||||
<p className="text-gedaempft-vordergrund mt-1 text-sm">{alsDatum(jetzt)}</p>
|
<p className="text-gedaempft-vordergrund mt-1 text-sm">{alsDatum(heute)}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
|
|||||||
@@ -35,12 +35,20 @@ export function Kopfleiste({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="border-rand bg-karte border-b">
|
<header className="border-rand bg-karte border-b">
|
||||||
<div className="mx-auto flex w-full max-w-5xl flex-wrap items-center gap-x-6 gap-y-3 px-4 py-3">
|
{/*
|
||||||
<Link href="/" className="text-base font-semibold tracking-tight">
|
Bewusst eine einzige Zeile auf allen Breiten: Wird es eng, scrollt die
|
||||||
|
Navigation waagerecht, statt umzubrechen und den Namen der Anwendung in
|
||||||
|
die zweite Zeile zu drängen.
|
||||||
|
*/}
|
||||||
|
<div className="mx-auto flex w-full max-w-5xl items-center gap-3 px-4 py-3 sm:gap-6">
|
||||||
|
<Link href="/" className="shrink-0 text-base font-semibold tracking-tight">
|
||||||
Planer
|
Planer
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav aria-label="Hauptnavigation" className="flex flex-1 flex-wrap gap-1">
|
<nav
|
||||||
|
aria-label="Hauptnavigation"
|
||||||
|
className="flex min-w-0 flex-1 gap-1 overflow-x-auto"
|
||||||
|
>
|
||||||
{sichtbar.map((eintrag) => {
|
{sichtbar.map((eintrag) => {
|
||||||
const aktiv =
|
const aktiv =
|
||||||
eintrag.pfad === '/' ? pfad === '/' : pfad.startsWith(eintrag.pfad)
|
eintrag.pfad === '/' ? pfad === '/' : pfad.startsWith(eintrag.pfad)
|
||||||
@@ -50,7 +58,7 @@ export function Kopfleiste({
|
|||||||
href={eintrag.pfad}
|
href={eintrag.pfad}
|
||||||
aria-current={aktiv ? 'page' : undefined}
|
aria-current={aktiv ? 'page' : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md px-3 py-1.5 text-sm transition-colors',
|
'shrink-0 rounded-md px-3 py-1.5 text-sm whitespace-nowrap transition-colors',
|
||||||
aktiv
|
aktiv
|
||||||
? 'bg-gedaempft text-vordergrund font-medium'
|
? 'bg-gedaempft text-vordergrund font-medium'
|
||||||
: 'text-gedaempft-vordergrund hover:bg-gedaempft hover:text-vordergrund',
|
: 'text-gedaempft-vordergrund hover:bg-gedaempft hover:text-vordergrund',
|
||||||
@@ -65,7 +73,7 @@ export function Kopfleiste({
|
|||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
<FarbschemaKnopf />
|
<FarbschemaKnopf />
|
||||||
<span
|
<span
|
||||||
className="text-gedaempft-vordergrund hidden text-xs sm:inline"
|
className="text-gedaempft-vordergrund hidden text-xs sm:inline"
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'server-only'
|
||||||
|
import { umgebung } from '@/server/umgebung'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die aktuelle Zeit — für alles, was serverseitig gerendert wird.
|
||||||
|
*
|
||||||
|
* Reine Funktionen in src/lib bekommen den Zeitpunkt weiterhin als Parameter.
|
||||||
|
* Hier steht nur die eine Stelle, an der er entsteht.
|
||||||
|
*
|
||||||
|
* In der Screenshot-Instanz (DEMO_MODUS=true) steht die Uhr fest. Sonst würde
|
||||||
|
* sich in jedem Bild das Tagesdatum ändern und die Bilder wären bei jedem Lauf
|
||||||
|
* neu, ohne dass sich etwas Sichtbares geändert hätte.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Dienstag, 17. März 2026, 09:20 Uhr in Europe/Berlin. */
|
||||||
|
export const DEMO_ZEITPUNKT = new Date('2026-03-17T08:20:00.000Z')
|
||||||
|
|
||||||
|
export function jetzt(): Date {
|
||||||
|
return umgebung().DEMO_MODUS ? DEMO_ZEITPUNKT : new Date()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user