5e37a9427c
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>
55 lines
6.0 KiB
Markdown
55 lines
6.0 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
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. 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">` + `<TimePicker5>`. Beginn date change syncs Ende date automatically.
|
|
- **`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.
|