Files
logbuch/CLAUDE.md
T
admin bb3f6f05a3 fix: Führungen über Mitternacht richtig datieren — Version 1.16.0
Das Formular hat Beginn und Ende bisher stumpf dasselbe Datum gegeben. Wer
einen Eintrag nach Mitternacht erfasste, bekam damit einen Beginn, der nach
dem Ende lag: Datum 9.8., Ende 00:30, Startzeit 21:00 ergab den 9.8. 21:00
als Beginn.

Beginn und Ende werden jetzt auseinander abgeleitet. Anker ist das Ende — der
Zeitpunkt, zu dem das Logbuch ausgefüllt wird und der von der Vorbelegung her
verlässlich ist. Ist die Startzeit später als die Endzeit, liegt der Beginn am
Vortag; wird die Endzeit unter die Startzeit gesetzt, wandert das Ende auf den
Folgetag. Das Datumsfeld bleibt das Startdatum und springt sichtbar zurück,
erklärt durch eine Zeile unter den Feldern. Über 12 Stunden Dauer erscheint ein
Hinweis, der das Speichern nicht blockiert.

migrate_zeiten.sql datiert Altbestände mit Beginn > Ende um einen Tag zurück.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 10:14:44 +02:00

6.5 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Commands

npm run dev       # Development server
npm run build     # Production build (run after every change to verify)
npm run lint      # ESLint

No test suite exists. Deploy via ./deploy.sh [tag] — builds multiplatform Docker image (amd64 + arm64) and pushes to docker.citysensor.de.

Architecture

Next.js 16 App Router application. All pages are server components; interactive parts are Client Components in app/MainClient.tsx and components/.

Auth flow: Users come from the existing MySQL beos table (not a separate users table). Login via app/login/actions.tslib/auth.ts (bcryptjs). Sessions are JWT cookies via jose (lib/session.ts, 1-hour expiry). If pw IS NULL, the default password is welzheim and mustChangePassword is forced to true. Middleware lives in proxy.ts (Next.js 16 convention) and exports middleware (not proxy).

Database: MySQL, database name sternwarte, via lib/db.ts connection pool. The pre-existing beos table has non-standard columns: `kürzel` (umlaut → always needs backticks), pw, id (all lowercase). The DB charset is utf8mb4 (collation utf8mb4_unicode_ci); connection pool uses charset: 'utf8mb4'.

SQL in JS: MySQL backticks inside JS template literals cause parse errors. Write complex queries using string concatenation (+), not template literals. LIMIT cannot be a parameterized placeholder in complex grouped queries — embed it directly after validating: LIST_SQL + \ LIMIT ${limit}``.

API routes (app/api/): all check getSession() and return 401 if unauthenticated. The logbuch list query uses GROUP_CONCAT to aggregate BEOs and Objekte into comma-separated strings per entry.

Key components

  • CustomSelect: replaces native <select> everywhere — iOS/Android native popups ignore CSS sizing. Supports keepOpen prop for multi-select use cases (BEOs, Objekte).
  • TimePicker5: custom time picker, no native <input type="time">. Shows HH:MM with ▲/▼ buttons, 5-minute steps, auto-repeat on hold (400 ms delay → 1-hour steps at 350 ms). Keyboard: ↑/↓.
  • LogbuchForm: Beginn/Ende stored as "YYYY-MM-DDTHH:MM" strings. Date and time are split into separate <input type="date"> + <TimeInput>. The single date field is the Beginn date; Ende's date is derived, never entered. Rule for sessions crossing midnight: start time-of-day later than end time-of-day ⇒ Beginn is the previous day, anchored on Ende (the moment the form is filled in, hence the reliable one) — beginnAusEnde() / endeAusBeginn(). So typing a start time can move the visible date back a day; a grey line below the row explains it. A duration over 12 h shows a soft hint but never blocks saving. Entries created before 1.16.0 may have Beginn > Ende; migrate_zeiten.sql fixes them.
  • LogbuchList: accepts compact and limit props. Compact mode used for the 5-entry preview below the form on desktop (hidden lg:block).

Data model

ArtFuehrung is stored as abbreviations in the DB (RF, SF, PrF, BEOS, SonF, TD, Beob, ToT, Sonst). Display names are in ARTEN_MAP in types/logbuch.ts. BEOS and TD hide the Besucher and Objekte fields. SonF pre-selects "Sonne" as the only object. SF (Sonderführung) additionally shows SonderName plus the mandatory Spende select (bar, ueberw, kasse, keine — labels in SPENDE_MAP); SpendeBetrag is only filled for bar. Both columns stay NULL for every other Art, enforced client-side in LogbuchForm and server-side in spendeValues() in DB4js_all.php.

Saving an SF entry also writes back into the Sonderführungs register SoFue2 (stattgefunden, anzahl_echt, bezahlt, remarks) via RepoSoFue::ausLogbuch() in DB4js_all.php — matched by DATE(wtermin) against the entry's Beginn, nearest time wins. It runs in the dispatcher after the logbuch transaction commits and never throws; the outcome travels back as a sofue field in the API response and is shown above the form by MainClient. Note SoFue2 is latin1 while logbuch is utf8mb4, and remarks holds only 100 chars — cp1252Safe() handles both.

Führungen that fall out never produce a logbuch entry. The Führungen tab (components/Fuehrungen.tsx, open to every logged-in BEO) lists upcoming status=2 rows and offers cancel (status=3, date kept) and postpone (old wtermin saved into the long-unused verlegt column, new date written). Both append a dated line with the user's Kürzel to bemerkung rather than replacing it. Backed by LB_SOFUE_TERMINE / LB_SOFUE_ABSAGEN / LB_SOFUE_VERSCHIEBEN in DB4js_all.php and the routes under app/api/sofue/.

Weather

app/api/wetter/route.ts proxies stwwetter.fuerst-stuttgart.de/api/weather. Without params it returns /latest; with ?zeit=YYYY-MM-DDTHH:MM (local time) it queries /range ±20 min around that moment and returns the closest reading, or 404 when the station has nothing (future dates, outages). The weather API treats naive query params as UTC, so the route converts Europe/Berlin → UTC via Intl offsets — without that, summer readings are off by two hours. LogbuchForm refetches whenever ende changes (date or Endzeit), skipping the first run so an edited entry keeps its stored values.

Documentation

ANLEITUNG.md is the single source for the user manual — edit only this file. public/anleitung.html (linked from the app footer) is generated by scripts/build-anleitung.mjs and is untracked (.gitignore); never hand-edit it. npm run anleitung builds it; a prebuild hook runs the same script before every next build, so the Docker image always gets a current copy (.dockerignore excludes *.md but re-includes ANLEITUNG.md for exactly this).

The page design lives in scripts/anleitung.template.html (placeholders {{H1}}, {{SUBTITLE}}, {{TOC}}, {{SECTIONS}}). Markdown conventions the generator relies on: ## N. Titel becomes <section id="sN"> with a numbered heading, the ## Inhaltsverzeichnis list feeds the TOC, in-page links like (#7-drucken) are rewritten to #s7, and blockquotes become callouts — > [!WARNUNG] yellow, > [!ACHTUNG] red, plain or > [!HINWEIS] blue. Renumbering a section means updating both its heading and the TOC entry.

Deployment

output: 'standalone' is set in next.config.ts for Docker. The MySQL container name in production is db — set DB_HOST=db in the server's environment.