Bisher stand im Formular immer der aktuelle Messwert, auch bei nachträglich erfassten Einträgen. Ändert der Benutzer nun Datum oder Endzeit, werden Temperatur, Feuchte und Luftdruck auf die Messung zu diesem Zeitpunkt umgestellt. /api/wetter nimmt dafür optional ?zeit=YYYY-MM-DDTHH:MM, holt über /range das Fenster ±20 Minuten und liefert die nächstgelegene Messung. Wichtig: die Wetter-API deutet Parameter ohne Zeitzone als UTC — die Route rechnet deshalb Europe/Berlin um, sonst läge das Ergebnis im Sommer zwei Stunden daneben. Gibt es keine Messung (Zukunft, Ausfall der Station), bleiben die bisherigen Werte stehen und es erscheint ein Hinweis unter den Feldern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.0 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.ts → lib/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. SupportskeepOpenprop 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">+<TimePicker5>. Beginn date change syncs Ende date automatically.LogbuchList: acceptscompactandlimitprops. 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.