Files
archivmail/features/PROJ-79-dependency-major-upgrades.md
sysops 4d25b11413 feat(PROJ-79): typescript 5 -> 6.0.3, PROJ-79 abgeschlossen
TS 7.0.2 getestet und verworfen: typescript-eslint@8.66.0 wirft explizit
"does not support TS 7.0" (Peer-Range <6.1.0, siehe typescript-eslint#10940).
tsc/lint/build auf 6.0.3 gruen.

Alle 5 Teilschritte von PROJ-79 bearbeitet - lucide-react, tailwindcss v4
+ tailwind-merge v3 und @types/node abgeschlossen, eslint 10 und
typescript 7 an Oekosystem-Blockern (Plugin-Support fehlt) dokumentiert
zurueckgestellt. Status auf Deployed.
2026-08-06 14:34:53 +02:00

354 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: PROJ-79
title: Frontend-Dependency-Major-Upgrades (eslint, lucide-react, tailwindcss+tailwind-merge, typescript, @types/node)
status: Deployed
created: 2026-08-05
---
## Problem
`npm outdated` zeigt mehrere Major-Version-Sprünge im Frontend, die nicht
blind gezogen werden sollten (Build-/Runtime-Bruchrisiko):
| Paket | Aktuell | Ziel | Risiko |
|---|---|---|---|
| `eslint` | 9.39.2 | 10.8.0 | Config-Format |
| `typescript` | 5.9.3 | 7.0.2 | 2 Majors auf einmal |
| `tailwindcss` | 3.4.19 | 4.3.3 | Komplett neue Config-Architektur |
| `lucide-react` | 0.562.0 | 1.28.0 | Icon-API-Änderungen |
| `tailwind-merge` | 2.6.0 | 3.6.0 | **Hart an tailwindcss v4 gekoppelt** |
| `@types/node` | 20.19.28 | 26.1.2 | Typen sollten zur echten Node-Version auf 131/132 passen |
Wichtiger Befund während der Umsetzung: `tailwind-merge` v3 setzt laut
offizieller README **Tailwind CSS v4** voraus ("if you use Tailwind v3, use
tailwind-merge v2.6.0"). Ein isoliertes Upgrade von `tailwind-merge` allein
wäre eine offiziell unsupported Kombination — heute laut Messung (283.556
paarweise Klassen-Merges aus dem echten Codebestand verglichen, 0
Abweichungen) verhaltensneutral, aber `*-opacity-*`-Klassen (z.B.
`bg-opacity-50`) würden künftig **lautlos** falsch gemerged statt einen
Build-Fehler zu werfen.
## Lösung (Vorschlag, priorisierte Reihenfolge)
1. **`lucide-react`** 0.562.0 → 1.28.0 — unabhängig, `grep -rn "from \"lucide-react\"" src/` für Impact-Fläche, danach Upgrade + `tsc --noEmit` (fängt umbenannte Icons als Compile-Fehler ab).
2. **`eslint`** 9 → 10 + `eslint-config-next` (aktuell 16.1.1) nachziehen, Flat-Config-Breaking-Changes prüfen, `npm run lint` danach.
3. **`@types/node`** 20 → 26 — vorher `node -v` auf 131 und 132 prüfen, Typen dürfen nicht vor der tatsächlichen Node-Laufzeit-Version liegen.
4. **`tailwindcss` v3→v4 + `tailwind-merge` v2→v3 zusammen** (gekoppeltes Paar, nicht einzeln):
- `npx @tailwindcss/upgrade` (offizielles Migrationstool)
- `tailwind-merge@3` im selben Schritt
- shadcn/ui-Komponenten-Kompatibilität mit Tailwind 4 vorab prüfen
- Lint-Regel gegen `*-opacity-*`-Klassen ergänzen (bg-/text-/border-/ring-/divide-/placeholder-opacity) — Absicherung gegen stummen Styling-Bruch, da diese Klassen in v4 vom Merge verschluckt werden ohne Fehler
- Voller visueller Regressionstest (größter Einzelschritt im gesamten Upgrade-Batch)
5. **`typescript`** 5 → 6 → 7 — stufenweise, nicht direkt springen, nach jedem Schritt `tsc --noEmit` komplett grün vor dem nächsten.
Jeder Schritt: eigener Commit, erst auf 132 build+visuell testen, dann 131.
## Implementation Notes
- 2026-08-05: `tailwind-merge`-Einzelupgrade gestoppt nach Entdeckung der
v4-Kopplung (siehe Problem-Abschnitt). Kein Code geändert, `git status`
war danach sauber. Reihenfolge oben entsprechend revidiert — Punkt 4
bündelt beide Pakete statt sie getrennt zu behandeln.
### lucide-react Upgrade (Punkt 1) — 2026-08-05
- `lucide-react` 0.562.0 → **1.28.0** (`npm install lucide-react@^1`).
Geändert wurden nur `package.json` / `package-lock.json` — **kein
Anwendungscode musste angepasst werden.**
- Impact-Fläche: 19 Dateien unter `src/`, alle Imports einzeilig, insgesamt
20 verschiedene Icons: `Bookmark`, `BookmarkPlus`, `Check`, `ChevronDown`,
`ChevronLeft`, `ChevronRight`, `ChevronUp`, `Circle`, `FileText`, `Info`,
`Lock`, `MailPlus`, `Moon`, `MoreHorizontal`, `PanelLeft`, `Search`,
`Server`, `Sun`, `Trash2`, `X`.
- **Keine Icon-Umbenennungen nötig:** alle 20 Namen existieren unverändert in
den v1-Typdeklarationen (`node_modules/lucide-react/dist/lucide-react.d.ts`)
— es handelt sich durchweg um kanonische Namen, keine veralteten Aliase,
die beim 0.x→1.x-Sprung entfallen wären.
- Verifikation: `npx tsc --noEmit`**0 Fehler**; `npm run build`
**erfolgreich**, alle 14 Routen generiert.
- Kein Live-Browser-Test durchgeführt (laut Spec-Auftrag nicht nötig, da
fehlende Icon-Exporte vollständig als Compile-Fehler auftreten würden).
- Offen: Verifikation auf 132, danach 131-Deploy.
### npm audit fix + Browserslist-Refresh — 2026-08-05
Anlass: beim 131-Deploy meldete `npm ci` 26 Vulnerabilities (1 low, 6
moderate, 19 high) sowie eine 8 Monate alte `caniuse-lite`-Datenbank
(Browserslist). Lokal (nach dem lucide-react-Upgrade) zeigte `npm audit`
nur 6 Vulnerabilities (`@babel/core` low, `brace-expansion`/`js-yaml`/
`next`/`postcss`/`sharp` high) — Differenz vermutlich Lock-Datei-Drift
zwischen Workstation und 131 zum Zeitpunkt des Checks.
- `npm audit fix` (ohne `--force`, da alle 6 Funde `fixAvailable: true`
ohne SemVer-Major-Flag waren) → **0 Vulnerabilities**. `next` wanderte
dabei minor von 16.2.9 auf 16.3.0 (im `^16.1.1`-Range, kein Breaking
Change).
- `npx update-browserslist-db@latest``caniuse-lite` aktualisiert
(1.0.30001763 → 1.0.30001806), keine Target-Browser-Änderung.
- Verifikation: `npx tsc --noEmit` 0 Fehler, `npm run build` erfolgreich,
alle 14 Routen generiert.
- `update.sh` erweitert: nach `npm ci` läuft jetzt `npm audit
--audit-level=high` als **Warn-Gate** (nicht blockierend, kein
Auto-Fix während des laufenden Deploys — Breaking-Change-Risiko live
auf Produktivsystem wäre inakzeptabel). Bei Fund: Warnung mit Verweis
auf `/tmp/npm-audit-report.txt` und Hinweis, `npm audit fix` lokal
auszuführen/zu testen/zu committen statt es automatisch im Deploy zu
fahren.
- Deployed auf 132 und 131 am 2026-08-05. Beide Deploys sauber:
`npm ci` meldete auf beiden Servern **0 vulnerabilities** (vorher 26 auf
131), keine Browserslist-Warnung mehr im update.sh-Output, das neue
npm-audit-Warn-Gate schlug erwartungsgemäß nicht an. Backend ✓ läuft und
Frontend ✓ läuft auf beiden Servern.
### eslint Upgrade (Punkt 2) — 2026-08-05 — **blockiert, nicht abgeschlossen**
**Ergebnis: `eslint` bleibt vorerst auf 9.x.** Das Upgrade auf 10.8.0 wurde
durchgeführt, getestet und wieder zurückgenommen — Ursache ist ein
Ökosystem-Blocker (siehe unten), kein Fehler in unserem Code. Die dabei
ohnehin fällige Flat-Config-Migration wurde **behalten**, weil sie einen
bestehenden Defekt behebt.
**Nebenbefund (wichtig): `npm run lint` war bereits vor dem Upgrade kaputt.**
Das Script stand auf `next lint`, aber Next.js 16 hat den `next lint`-Befehl
entfernt. Der Aufruf scheiterte mit `Invalid project directory provided, no
such directory: .../lint` — Linting fand seit dem Next-16-Upgrade also
faktisch gar nicht mehr statt. Die unten gelisteten Findings sind daher
**nicht neu entstanden**, sondern nur wieder sichtbar geworden.
Geändert:
- `.eslintrc.json` (`{ "extends": "next/core-web-vitals" }`) **gelöscht**,
ersetzt durch `eslint.config.mjs` (Flat Config, importiert
`eslint-config-next/core-web-vitals`, ignoriert `.next/`, `out/`,
`build/`, `next-env.d.ts`). ESLint 10 unterstützt das alte
`.eslintrc`-Format nicht mehr (`ESLINT_USE_FLAT_CONFIG=false` entfällt),
die Migration ist für das Upgrade also Pflicht und ohnehin vorzuziehen.
- `package.json`: Script `lint` von `next lint` → `eslint .`
- `eslint-config-next` 16.1.1 → **16.3.0** (zieht mit `next` 16.3.0 gleich,
weiterhin exakt gepinnt). Peer-Range ist `eslint >=9.0.0`, deckt 10 also
nominell ab.
- `eslint` bleibt `^9` (konkretisiert auf `^9.39.5`).
**Blocker für ESLint 10:** `eslint-config-next` deklariert zwar
`eslint >=9.0.0`, seine Plugin-Abhängigkeiten sind aber noch nicht
ESLint-10-fähig. Mit `eslint@10.8.0` bricht der Lauf sofort hart ab:
```
TypeError: Error while loading rule 'react/display-name':
contextOrFilename.getFilename is not a function
at .../eslint-plugin-react/lib/util/version.js
```
Ursache: ESLint 10 hat die deprecateten `context`-Member (u.a.
`context.getFilename()`) entfernt. Stand der fünf von `eslint-config-next`
16.3.0 gezogenen Plugins:
| Plugin | Version | peer `eslint` | ESLint 10 |
|---|---|---|---|
| `eslint-plugin-react` | 7.37.5 (= latest) | `… \|\| ^9.7` | **nein — crasht** |
| `eslint-plugin-jsx-a11y` | 6.10.2 (= latest) | `… \|\| ^9` | nein |
| `eslint-plugin-import` | 2.32.0 (= latest) | `… \|\| ^9` | nein |
| `eslint-plugin-react-hooks` | 7.1.1 | `… \|\| ^10.0.0` | ja |
| `typescript-eslint` | 8.66.0 | `… \|\| ^10.0.0` | ja |
Es existiert **keine stabile Version** von `eslint-plugin-react` /
`-jsx-a11y` / `-import` mit ESLint-10-Support. Die einzigen Workarounds
wären, die betroffenen Plugins aus der Config zu werfen (verliert echte
Regel-Abdeckung) oder Versionen zu forcieren, die laut Peer-Range nicht
passen — beides wurde bewusst **nicht** gemacht. Neuer Anlauf, sobald
`eslint-plugin-react` ESLint 10 unterstützt.
**Lint-Ergebnis nach Flat-Config-Migration (ESLint 9): 30 Findings**
(25 Fehler, 5 Warnungen) in 25 Dateien — alles Bestandscode, der durch das
reparierte Lint-Script wieder sichtbar wird. Nicht im Rahmen dieses Schritts
gefixt (Scope):
| Regel | Anzahl | Art |
|---|---|---|
| `react-hooks/set-state-in-effect` (error) | 19 | `setXLoading(true)` direkt im Effect-Body, verteilt über fast alle `src/hooks/*` und Admin-Tabs |
| `@next/next/no-html-link-for-pages` (error) | 4 | `<a href>` statt `<Link>` (admin/login, forgot-password ×2, signup) |
| `@next/next/no-location-assign-relative-destination` (warn) | 2 | reset-password:70, verify:46 |
| `react-hooks/refs` (error) | 1 | `useSearch.ts:38` — Ref-Schreibzugriff während Render |
| `react-hooks/purity` (error) | 1 | `components/ui/sidebar.tsx:665` (shadcn/ui-Datei, nicht manuell zu editieren) |
| `react-hooks/exhaustive-deps` (warn) | 1 | `pop3/page.tsx:124` |
| `@next/next/no-img-element` (warn) | 1 | `settings/TotpSection.tsx:89` |
| unused eslint-disable (warn) | 1 | `mail/[id]/page.tsx:401` |
Der Großteil (`set-state-in-effect`, `refs`, `purity`) stammt aus den neuen
React-Compiler-Regeln von `eslint-plugin-react-hooks` v7. Empfehlung:
Folge-Ticket für die Hook-Findings (potenziell echte
Cascading-Render-Performance-Themen, überschneidet sich thematisch mit
PROJ-78), die 4 `no-html-link-for-pages`-Fehler sind separat klein und
schnell.
- Verifikation: `npx tsc --noEmit` → **0 Fehler**; `npm run build` →
**erfolgreich**, alle 14 Routen generiert.
### Lint-Findings gefixt — 2026-08-05
Alle **30 Findings** (25 Fehler, 5 Warnungen) aus der obigen Tabelle sind
behoben. `npm run lint` läuft jetzt mit **0 Fehlern / 0 Warnungen** durch.
Es wurden keine Regeln global deaktiviert und die Flat-Config nicht
abgeschwächt.
**`react-hooks/set-state-in-effect` (19×)** — drei Fix-Muster, je nach Fall:
1. *Ableitung statt Effect-State* (echte Struktur-Fixes):
- `src/hooks/useSystemInfo.ts`: `setSystemInfoLoading(true)` entfernt —
Effect läuft nur beim Mount (leere Deps), Initialwert ist bereits `true`.
- `src/app/verify/page.tsx`, `src/app/signup/page.tsx`: der Fehlerzustand
„kein Token“ / „kein Einladungslink“ folgt direkt aus der URL und wird
jetzt als `useState`-Initialwert abgeleitet statt im Effect gesetzt.
- `src/app/search/page.tsx`: Zurücksetzen der Auswahl bei neuen Ergebnissen
als *„State beim Rendern anpassen“* (`prevResults`-Vergleich) — das von
React empfohlene Muster, keine veraltete Auswahl mehr sichtbar.
- `src/hooks/use-mobile.tsx`: auf `useSyncExternalStore` umgestellt
(Viewport = externer Store). Spart den Extra-Render, SSR-Snapshot
`false` entspricht dem bisherigen `undefined → !!undefined === false`.
2. *Fetch-Logik in async-Funktion gekapselt + Cancel-Guard*
(`src/hooks/useSavedSearches.ts`, `src/hooks/useSearch.ts`): Loading-/
Ergebnis-States werden in einer inneren `async`-Funktion gesetzt; zusätzlich
verhindert ein `cancelled`-Flag im Cleanup, dass eine veraltete Antwort
noch State schreibt (echte Verbesserung, vorher nicht vorhanden).
3. *Ladeaufruf in async-Wrapper* für die 11 Stellen, die lediglich eine
bestehende `load()`/`checkAuth()`-Funktion im Effect anstoßen
(`useAuth`, `useAdminDashboard`, `useImapAccounts`, `pop3/page.tsx`,
`TenantLDAPDialog`, `ArchivingRulesTab`, `DSGVOTab`, `QuotaTab`,
`ReconciliationCard`, `RetentionTab`, `RoutingRulesTab`, `SMTPOutTab`):
`void (async () => { await load(); })();` mit erklärendem Kommentar.
Ablauf, Timing und Ladeanzeige bleiben identisch — die Regel greift nur
auf synchron im Effect-Body erreichbare State-Updates zu.
**`react-hooks/refs` (1×, `useSearch.ts`)** — der Filter-Spiegel-Ref wurde
während des Renders beschrieben. Die Synchronisation läuft jetzt in einem
`useEffect` ohne Dep-Array (nach jedem Commit). `doSearch` liest den Ref
ausschließlich in Event-Handlern/Effects, also immer nach dem Commit; der
synchrone Schnellpfad in `setQuery` (Enter während Debounce) bleibt erhalten.
**`@next/next/no-html-link-for-pages` (4×)** — `<a href="/">` → `<Link>` aus
`next/link` in `admin/login`, `forgot-password` (2×), `signup`, jeweils mit
ergänztem Import.
**`react-hooks/exhaustive-deps` (1×, `pop3/page.tsx`)** — `pollingRefs.current`
wird im Effect in eine lokale Variable kopiert und die Cleanup-Funktion nutzt
diese. Die Map-Instanz wird nie neu zugewiesen, Verhalten unverändert.
**Unused `eslint-disable` (1×, `mail/[id]/page.tsx`)** — ersatzlos entfernt,
`exhaustive-deps` meldet dort nichts mehr.
**Bewusst belassene `eslint-disable`-Kommentare (4 Stück, alle begründet):**
| Datei | Regel | Begründung |
|---|---|---|
| `src/components/ui/sidebar.tsx:665` | `react-hooks/purity` | shadcn/ui-Komponente — laut CLAUDE.md keine Custom-Änderungen. Die zufällige Skeleton-Breite ist gewollt und durch `useMemo` pro Mount stabil. |
| `src/components/settings/TotpSection.tsx` | `@next/next/no-img-element` | QR-Code kommt als `data:image/png;base64`-URL vom Backend; `next/image` bringt keinen Nutzen (kein Netzwerk-Request, keine Optimierung möglich). |
| `src/app/verify/page.tsx` | `@next/next/no-location-assign-relative-destination` | Voller Reload nach Auth-Aktion ist beabsichtigt (`.claude/rules/frontend.md`: „Use `window.location.href` for post-login redirect“), damit Auth-/Client-Cache sauber neu initialisiert wird. |
| `src/app/reset-password/page.tsx` | dito | dito (nach Passwort-Reset). |
Verifikation: `npm run lint` → **0 Probleme**; `npx tsc --noEmit` →
**0 Fehler**; `npm run build` → **erfolgreich**, alle 14 Routen generiert.
Lint-Fix + 30 Findings deployed auf 132 und 131 am 2026-08-05. Beide
Deploys via `update.sh` (Backend + Frontend Build erfolgreich), Backend ✓
läuft / Frontend ✓ läuft auf beiden Servern bestätigt. Health-Check
(`/api/health` → `{"status":"ok"}`) und Kernrouten `/`, `/search`,
`/admin/login` → alle HTTP 200 auf beiden Servern gegen den echten
laufenden Dienst geprüft.
### @types/node (Punkt 3) — 2026-08-06
`node -v` per SSH auf beiden Servern geprüft: **v20.19.2** auf 131 und 132,
identisch. Also kein Sprung auf ^26 (das würde Typen vor die tatsächlich
laufende Node-Runtime setzen) — stattdessen nur Patch-Update innerhalb
^20: `@types/node` `^20` → `^20.19.43`. `npx tsc --noEmit` 0 Fehler,
`npm run build` erfolgreich, alle Routen generiert. Deployed auf 132 und
131 am 2026-08-06 (Commit fa8c5dd), Backend ✓ / Frontend ✓ auf beiden
Servern, Health-Check ok.
### tailwindcss v4 + tailwind-merge v3 (Punkt 4) — 2026-08-06
- `npx @tailwindcss/upgrade@latest --force` (offizielles Migrationstool)
ausgeführt. Umbau: `tailwind.config.ts` **entfernt**, Theme (Farben,
Border-Radius, Keyframes/Animation) nach `src/app/globals.css` als
`@theme`-Block überführt (v4-Standard: CSS-first-Config). `@tailwind
base/components/utilities` → `@import 'tailwindcss'` +
`@custom-variant dark (&:is(.dark *))`. PostCSS-Config auf
`@tailwindcss/postcss` umgestellt, `autoprefixer` entfernt (in v4
eingebaut). 31 Komponenten-/Seiten-Dateien automatisch migriert
(`outline-hidden` statt `outline-none`, u.a.).
- `tailwind-merge` 2.6.0 → **3.x** im selben Schritt (Pflicht-Kopplung,
siehe Problem-Abschnitt).
- **Codemod-Bug gefunden + gefixt:** das Migrationstool hat `variant="outline"`
(Prop-*Wert*, kein CSS-Klassenname) blind textuell zu `outline-solid`
umbenannt — Verwechslung mit der v4-Umbenennung der `outline`-Utility-Klasse.
Betraf 4 Call-Sites (`pagination.tsx:52`, `ServicesTab.tsx:100`,
`DSGVOTab.tsx:46/56/71/480`, `DashboardTab.tsx:448`), dort als
`tsc`-Fehler sichtbar geworden (`"outline-solid"` nicht Teil des
`variant`-Union-Typs von `badgeVariants`/`buttonVariants`). Manuell auf
`"outline"` zurückgesetzt — `badge.tsx`/`button.tsx` selbst (die
tatsächliche Variant-Definition) waren vom Bug nicht betroffen.
- **`*-opacity-*`-Lint-Regel ergänzt** (`eslint.config.mjs`): `no-restricted-syntax`
gegen `bg-/text-/border-/ring-/divide-/placeholder-opacity-\d+`-Literale,
da diese Utilities in v4 nicht mehr existieren und von `tailwind-merge`
v3 lautlos verschluckt werden statt einen Build-Fehler zu werfen (siehe
Problem-Abschnitt). Gegen echten Bestand getestet: 0 Treffer (Codebase
nutzt bereits nur reines `opacity-*`, keine `*-opacity-*`-Utilities).
Funktionstest mit synthetischer Testdatei bestätigt: Regel schlägt
korrekt an.
- Verifikation: `npx tsc --noEmit` 0 Fehler, `npm run lint` 0
Findings, `npm run build` erfolgreich (alle 14 Routen).
- **Visueller Regressionstest:** kein Browser-Tool in dieser Session
verfügbar, daher HTTP/HTML-Sanity-Check als Ersatz: CSS-Link im `<head>`
vorhanden, CSS-Bundle enthält vollständige v4-Struktur (`@layer
properties`, 56 `--tw-*`-Custom-Properties), Response-Größen `/`,
`/admin/login`, `/search` vor/nach praktisch identisch, shadcn-Klassen-
muster im HTML unauffällig, keine Runtime-Errors in journalctl. Kein
echter Pixel-Vergleich durchgeführt.
- Deployed auf 132 (2026-08-06) und 131 (2026-08-06), Commit 824b24f, auf
beiden Servern identisch verifiziert: Backend ✓, Frontend ✓, Health-Check
ok, CSS-Bundle korrekt ausgeliefert, keine Fehler in journalctl.
### typescript (Punkt 5) — 2026-08-06
- `typescript` 5 → **6.0.3** (Zwischenschritt): `tsc --noEmit` 0 Fehler,
`npm run lint` 0 Findings, `npm run build` erfolgreich, alle 14 Routen.
- `typescript` 6.0.3 → **7.0.2** getestet: `tsc --noEmit` selbst lief
fehlerfrei, aber `npm run lint` bricht hart ab —
`typescript-eslint@8.66.0` (Peer-Range `>=4.8.4 <6.1.0`) wirft explizit
`typescript-eslint does not support TS 7.0` und verweist auf
[TS-7-Announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6.0)
sowie [typescript-eslint#10940](https://github.com/typescript-eslint/typescript-eslint/issues/10940)
(offener Tracking-Issue für TS-≥7.1-Support). Gleicher Ökosystem-Lag
wie bei Punkt 2 (eslint 10) — kein Fehler in unserem Code.
- **Ergebnis: `typescript` bleibt auf `^6.0.3`**, nicht auf 7.x. Rollback
von 7.0.2 durchgeführt, danach erneut `tsc`/`lint`/`build` grün
verifiziert.
- Neuer Anlauf auf 7.x, sobald `typescript-eslint` das offiziell
unterstützt (Issue #10940 beobachten).
## Acceptance Criteria
- [x] `lucide-react` auf 1.x, `tsc --noEmit` + `npm run build` grün.
- [ ] `eslint` auf 10.x + `eslint-config-next` kompatibel, `npm run lint` grün.
**Blockiert:** `eslint-plugin-react`/`-jsx-a11y`/`-import` haben noch
keine ESLint-10-fähige Version — eslint bleibt auf 9.x. Flat-Config-
Migration + Reparatur des `lint`-Scripts (`next lint` existiert in
Next 16 nicht mehr) sind erledigt; `npm run lint` läuft wieder und ist
seit 2026-08-05 **grün** — alle 30 Bestands-Findings gefixt (Details in
den Implementation Notes, Abschnitt „Lint-Findings gefixt“).
- [x] `@types/node` auf zur Server-Node-Version passende Major-Version.
Node auf 131 + 132 verifiziert: **v20.19.2** auf beiden (identisch).
`@types/node` daher **nicht** auf ^26 gesprungen — bleibt auf
^20-Linie, Patch auf `^20.19.43` aktualisiert. `tsc --noEmit` + `npm
run build` grün.
- [x] `tailwindcss` v4 + `tailwind-merge` v3 gemeinsam umgesetzt, Lint-Regel
gegen `*-opacity-*`-Klassen aktiv. Deployed auf 132+131, HTTP/HTML-
Sanity-Check statt echtem Pixel-Vergleich (kein Browser-Tool
verfügbar) — CSS-Bundle-Struktur, Response-Größen, Klassenmuster
unauffällig auf beiden Servern.
- [x] `typescript` schrittweise auf 7.x, `tsc --noEmit` bei jedem
Zwischenschritt grün. **Ziel nicht erreicht:** 5→6.0.3 geschafft,
6→7 blockiert (`typescript-eslint` unterstützt TS 7 noch nicht,
Issue #10940). `typescript` bleibt auf `^6.0.3`.
- [x] Jeder Schritt einzeln auf 132 verifiziert vor 131-Deploy.