Aufraeumarbeiten: ESLint, DOM-Verschachtelung, eigene Testwehr
ESLint hatte keine Konfiguration — das Skript "npm run lint" pruefte nichts. Jetzt Flat Config ohne Formatierungsregeln, dafuer mit den Regeln, die die Fehler dieses Projekts gemeldet haetten. Er hat gleich zwei gefunden, die niemand gesucht hatte. Das Badge rendert ein span statt eines div. Damit ist die Meldung "div cannot be a descendant of p" weg, die seit Phase 3 auf der Einsatzseite stand — sie ist der klassische Ausloeser fuer einen Hydration-Fehler. Die E2E-Tests laufen gegen eine eigene Wehr. Nach fuenf Phasen standen in der Beispielwehr 120 Atemschutz-Ueberwachungen statt einer und 115 Geraete statt 45; bei jedem Lauf aenderten sich alle Bildschirmfotos. Der Bestand ist einmal zurueckgesetzt, npm run db:test-reset baut die Testwehr neu auf. Behoben nebenbei: - Die Fehlerflaeche der Karte konnte nie etwas anzeigen. - Zwei useMemo der Lagekarte rechneten bei jedem Rendern neu. - tests/e2e war vom Typecheck ausgenommen und liess einen doppelt deklarierten Bezeichner durch. Closes #10 Closes #11 Closes #12 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8081131eb7
commit
7196c7110c
@@ -3,7 +3,7 @@
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { BellRing, Copy, Send, Trash2, TriangleAlert } from "lucide-react";
|
||||
import { EreignisTyp, Kanal } from "@prisma/client";
|
||||
import { Kanal } from "@prisma/client";
|
||||
import type { inferRouterOutputs } from "@trpc/server";
|
||||
import { trpc } from "@/lib/trpc/client";
|
||||
import type { AppRouter } from "@/server/routers/_app";
|
||||
|
||||
@@ -81,7 +81,16 @@ export function Lagekarte({
|
||||
onError: (fehler) => setMeldung(fehler.message),
|
||||
});
|
||||
|
||||
const elemente = karte.data?.elemente ?? [];
|
||||
/**
|
||||
* Die Elemente der Lagekarte.
|
||||
*
|
||||
* Das `?? []` hat vor Phase 8 bei jedem Rendern ein neues leeres Feld
|
||||
* erzeugt — und damit die beiden useMemo darunter jedes Mal neu rechnen
|
||||
* lassen, obwohl sich nichts geändert hatte. Bei einer Lagekarte mit
|
||||
* dreißig Elementen und einem Ereignisstrom, der alle zwei Sekunden etwas
|
||||
* schickt, ist das kein akademischer Punkt.
|
||||
*/
|
||||
const elemente = React.useMemo(() => karte.data?.elemente ?? [], [karte.data?.elemente]);
|
||||
|
||||
const punkte = React.useMemo<KartenPunkt[]>(() => {
|
||||
const liste: KartenPunkt[] = [];
|
||||
|
||||
@@ -63,6 +63,14 @@ export default async function FahrzeugSeite({ params }: { params: Promise<{ id:
|
||||
|
||||
if (!fahrzeug) notFound();
|
||||
|
||||
/*
|
||||
* eslint-disable-next-line react/jsx-key -- Fehlalarm: Diese JSX-Elemente
|
||||
* sind nicht die Kinder einer Liste, sondern die zweiten Glieder von
|
||||
* Wertepaaren. Gerendert werden sie unten in einer map, deren Wrapper den
|
||||
* key traegt (`<div key={bezeichnung}>`). Ein key an dieser Stelle waere
|
||||
* wirkungslos und wuerde nur verschleiern, wo er wirklich sitzt.
|
||||
*/
|
||||
/* eslint-disable react/jsx-key */
|
||||
const angaben: Array<[string, React.ReactNode]> = [
|
||||
["Typ nach DIN", fahrzeug.dinTyp ?? "—"],
|
||||
["Kennzeichen", <span className="tabellenzahlen">{fahrzeug.kennzeichen ?? "—"}</span>],
|
||||
@@ -88,6 +96,7 @@ export default async function FahrzeugSeite({ params }: { params: Promise<{ id:
|
||||
],
|
||||
["Standort", fahrzeug.standort?.name ?? "—"],
|
||||
];
|
||||
/* eslint-enable react/jsx-key */
|
||||
|
||||
const alleRaeume = [
|
||||
...fahrzeug.geraeteraeume,
|
||||
|
||||
@@ -104,6 +104,14 @@ export default async function GeraetSeite({ params }: { params: Promise<{ id: st
|
||||
const { geraet, verlauf, pruefungen, maengel, termine } = daten;
|
||||
const qr = await qrAlsDataUri(geraet.inventarnummer, 320);
|
||||
|
||||
/*
|
||||
* eslint-disable-next-line react/jsx-key -- Fehlalarm: Diese JSX-Elemente
|
||||
* sind nicht die Kinder einer Liste, sondern die zweiten Glieder von
|
||||
* Wertepaaren. Gerendert werden sie unten in einer map, deren Wrapper den
|
||||
* key traegt (`<div key={bezeichnung}>`). Ein key an dieser Stelle waere
|
||||
* wirkungslos und wuerde nur verschleiern, wo er wirklich sitzt.
|
||||
*/
|
||||
/* eslint-disable react/jsx-key */
|
||||
const angaben: Array<[string, React.ReactNode]> = [
|
||||
["Inventarnummer", <span className="tabellenzahlen">{geraet.inventarnummer}</span>],
|
||||
["Gerätetyp", geraet.geraetetyp?.name ?? "—"],
|
||||
@@ -129,6 +137,7 @@ export default async function GeraetSeite({ params }: { params: Promise<{ id: st
|
||||
["Lebensdauer bis", <FristAnzeige datum={geraet.lebensdauerBis} vorwarnTage={90} />],
|
||||
["Prüfvorschrift", geraet.geraetetyp?.pruefvorschrift ?? "—"],
|
||||
];
|
||||
/* eslint-enable react/jsx-key */
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -118,7 +118,6 @@ export function Scanner() {
|
||||
{fehler && <Fehlermeldung meldung={fehler} />}
|
||||
|
||||
<div className="overflow-hidden rounded-lg border border-[var(--border)] bg-black">
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="aspect-video w-full object-cover"
|
||||
|
||||
@@ -8,13 +8,6 @@ import { trpc } from "@/lib/trpc/client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
|
||||
@@ -78,7 +78,17 @@ export function KartenAnsicht({
|
||||
breitengrad: number;
|
||||
laengengrad: number;
|
||||
} | null>(null);
|
||||
const [meldung, setMeldung] = React.useState<string | null>(null);
|
||||
/**
|
||||
* Was schiefging.
|
||||
*
|
||||
* Bis Phase 8 stand hier ein Zustand, der nie gesetzt wurde — die
|
||||
* Fehlerfläche darunter konnte also nichts anzeigen, und eine gescheiterte
|
||||
* Abfrage sah aus wie „keine Hydranten vorhanden". Gefunden hat das der
|
||||
* neue Linter.
|
||||
*
|
||||
* Jetzt kommt die Meldung aus den Abfragen selbst. Ein eigener Zustand
|
||||
* dafür wäre eine zweite Wahrheit neben der von React Query.
|
||||
*/
|
||||
|
||||
const hydranten = trpc.hydrant.liste.useQuery({
|
||||
suche: suche || undefined,
|
||||
@@ -137,6 +147,9 @@ export function KartenAnsicht({
|
||||
? objekte.data?.find((eintrag) => eintrag.id === gewaehlt.id)
|
||||
: undefined;
|
||||
|
||||
const meldung =
|
||||
hydranten.error?.message ?? objekte.error?.message ?? kennzahlen.error?.message ?? null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4" data-testid="kartenansicht">
|
||||
{meldung && <Fehlermeldung meldung={meldung} kennung="kartenfehler" />}
|
||||
|
||||
@@ -19,13 +19,6 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Fehlermeldung } from "@/components/stammdaten/anzeige";
|
||||
import { KATEGORIE_TEXT } from "@/app/(app)/karte/objekte/objekt-liste";
|
||||
@@ -232,7 +225,7 @@ export function ObjektDetail({ id, darfBearbeiten }: { id: string; darfBearbeite
|
||||
<div>
|
||||
<CardTitle>Ansprechpartner</CardTitle>
|
||||
<CardDescription>
|
||||
Wer nachts ans Telefon geht, steht mit „jederzeit" markiert.
|
||||
Wer nachts ans Telefon geht, steht mit „jederzeit“ markiert.
|
||||
</CardDescription>
|
||||
</div>
|
||||
{darfBearbeiten && (
|
||||
|
||||
@@ -65,6 +65,14 @@ export default async function MitgliedSeite({ params }: { params: Promise<{ id:
|
||||
|
||||
if (!person) notFound();
|
||||
|
||||
/*
|
||||
* eslint-disable-next-line react/jsx-key -- Fehlalarm: Diese JSX-Elemente
|
||||
* sind nicht die Kinder einer Liste, sondern die zweiten Glieder von
|
||||
* Wertepaaren. Gerendert werden sie unten in einer map, deren Wrapper den
|
||||
* key traegt (`<div key={bezeichnung}>`). Ein key an dieser Stelle waere
|
||||
* wirkungslos und wuerde nur verschleiern, wo er wirklich sitzt.
|
||||
*/
|
||||
/* eslint-disable react/jsx-key */
|
||||
const angaben: Array<[string, React.ReactNode]> = [
|
||||
["Mitgliedsnummer", <span className="tabellenzahlen">{person.mitgliedsnummer ?? "—"}</span>],
|
||||
["Dienstgrad", DIENSTGRAD_TEXT[person.dienstgrad] ?? person.dienstgrad],
|
||||
@@ -79,6 +87,7 @@ export default async function MitgliedSeite({ params }: { params: Promise<{ id:
|
||||
person.strasse ? `${person.strasse}, ${person.plz ?? ""} ${person.ort ?? ""}`.trim() : "—",
|
||||
],
|
||||
];
|
||||
/* eslint-enable react/jsx-key */
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -278,7 +278,7 @@ export function FristenDashboard({
|
||||
Offene Termine
|
||||
{fenster && (
|
||||
<span className="ml-2 text-sm font-normal text-[var(--muted-foreground)]">
|
||||
gefiltert auf „{fenster.replace(/_/g, " ")}"
|
||||
gefiltert auf „{fenster.replace(/_/g, " ")}“
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
|
||||
@@ -133,7 +133,7 @@ export function ChecklistenEditor({
|
||||
<CardDescription>
|
||||
Der Prüfwizard fragt die Punkte in dieser Reihenfolge ab. Pflichtpunkte müssen
|
||||
beantwortet sein, bevor sich eine Prüfung abschließen lässt — und ein durchgefallener
|
||||
Pflichtpunkt bedeutet „nicht bestanden".
|
||||
Pflichtpunkt bedeutet „nicht bestanden“.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -222,7 +222,7 @@ export function ChecklistenEditor({
|
||||
<CardHeader>
|
||||
<CardTitle>Prüfpunkt ergänzen</CardTitle>
|
||||
<CardDescription>
|
||||
Bei einem Messwert entscheidet der zulässige Bereich über „in Ordnung" — gerechnet
|
||||
Bei einem Messwert entscheidet der zulässige Bereich über „in Ordnung“ — gerechnet
|
||||
wird das auf dem Server, nicht im Browser.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Fehlermeldung } from "@/components/stammdaten/anzeige";
|
||||
import { Markdown } from "@/components/ki/markdown";
|
||||
import { WIKI_KATEGORIE_TEXT } from "@/lib/ki/texte";
|
||||
|
||||
@@ -108,7 +108,6 @@ export function Diktat({ entitaet, entitaetId, onUebernehmen }: DiktatProps) {
|
||||
}, 500);
|
||||
|
||||
return () => clearInterval(zeitgeber);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [zustand, moeglichkeiten.data?.maxSekunden]);
|
||||
|
||||
function zuruecksetzen() {
|
||||
|
||||
@@ -98,7 +98,6 @@ export function QrLeser({
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="overflow-hidden rounded-lg border border-[var(--border)] bg-black">
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<video ref={videoRef} className="aspect-video w-full object-cover" muted playsInline />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -31,11 +31,24 @@ const badgeVarianten = cva(
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
extends React.HTMLAttributes<HTMLSpanElement>,
|
||||
VariantProps<typeof badgeVarianten> {}
|
||||
|
||||
/**
|
||||
* Ein `<span>`, kein `<div>`.
|
||||
*
|
||||
* Bis Phase 8 war es ein `<div>`, und React meldete auf der Einsatzseite seit
|
||||
* Phase 3: „In HTML, <div> cannot be a descendant of <p>." Dort stehen die
|
||||
* Abzeichen für Kategorie und Status in einer Zeile Fließtext — und ein
|
||||
* Blockelement in einem Absatz ist ungültiges HTML. Der Browser schließt den
|
||||
* Absatz von sich aus davor, baut also einen anderen Baum als React erwartet,
|
||||
* und das ist der klassische Auslöser für einen Hydration-Fehler.
|
||||
*
|
||||
* Ein Abzeichen ist ohnehin Fließtext-Inhalt. Die Darstellung ändert sich
|
||||
* nicht: `inline-flex` macht es auch als span zu einer Zeile mit Innenabstand.
|
||||
*/
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVarianten({ variant }), className)} {...props} />;
|
||||
return <span className={cn(badgeVarianten({ variant }), className)} {...props} />;
|
||||
}
|
||||
|
||||
export { Badge, badgeVarianten };
|
||||
|
||||
@@ -104,7 +104,6 @@ class EchtesNtfy implements NtfyVersand {
|
||||
* Aufruf oder — schlimmer — ein Titel voller Fragezeichen.
|
||||
*/
|
||||
function alsAsciiKopf(text: string): string {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
if (/^[\x00-\x7F]*$/.test(text)) return text;
|
||||
return `=?UTF-8?B?${Buffer.from(text, "utf8").toString("base64")}?=`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user