fix(PROJ-79): next lint kaputt seit Next-16-Upgrade repariert + 30 Findings gefixt

npm run lint rief next lint auf, das es in Next.js 16 nicht mehr gibt —
seit dem Next-16-Upgrade lief effektiv gar kein Lint mehr. Umgestellt auf
eslint . mit Flat-Config (eslint.config.mjs statt .eslintrc.json). Der
dadurch wieder sichtbare Lint-Lauf zeigte 30 Findings (25 Fehler,
5 Warnungen), alle gefixt:

- 19x react-hooks/set-state-in-effect: Loading-States wo möglich als
  echte Ableitung statt eigenem Effect-State (use-mobile.tsx komplett auf
  useSyncExternalStore umgebaut), sonst async-Wrapper mit Cancel-Guard um
  bestehende Loader — Timing/Ladeanzeige unverändert.
- react-hooks/refs (useSearch.ts): Ref-Schreibzugriff aus dem Render in
  einen Effect verschoben.
- 4x no-html-link-for-pages: <a href> durch next/link ersetzt in
  admin/login, forgot-password, signup.
- Rest (exhaustive-deps, no-img-element, unused disable) einzeln gefixt.
- 4 bewusst belassene disable-Kommentare mit Begründung (shadcn/ui-Datei,
  QR-Code-data-URL, Full-Reload nach Auth laut Projektregel).

eslint-Major-Upgrade auf 10 selbst bleibt blockiert: eslint-plugin-react/
jsx-a11y/import unterstützen ESLint 10 in ihrer aktuellen Latest-Version
noch nicht (Crash beim Laden), siehe Feature-Spec PROJ-79.

Verifiziert auf 132 (Build-Sandbox, kein Live-Deploy): npm ci/tsc/lint/
build grün, 8 Kern-Routen per Standalone-Server auf HTTP 200 geprüft.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
This commit is contained in:
sysops
2026-08-05 21:14:17 +02:00
co-authored by Claude Sonnet 5
parent a2e45b950b
commit d1b4497893
37 changed files with 1003 additions and 468 deletions
@@ -94,11 +94,171 @@ zwischen Workstation und 131 zum Zeitpunkt des Checks.
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.
Kein Live-Test auf 131/132 — steht noch aus.
## 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“).
- [ ] `@types/node` auf zur Server-Node-Version passende Major-Version.
- [ ] `tailwindcss` v4 + `tailwind-merge` v3 gemeinsam umgesetzt, Lint-Regel
gegen `*-opacity-*`-Klassen aktiv, visueller Regressionstest