From dfbc62398d6c50ef9ec1cf1f46620631c23ff423 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Wed, 22 Jul 2026 21:31:45 +0200 Subject: [PATCH] Eigener Rueckfrage-Dialog statt Browsermeldung, Zeitstempel im Export, Notizen an Gruppen-Codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rueckfragen liefen bisher ueber window.confirm. Das reisst aus der Oberflaeche, zeigt den Hostnamen, laesst sich nicht gestalten und die Knoepfe heissen immer "OK/Abbrechen". Neu ist ein eigener Dialog (src/confirm.jsx) als Provider mit dem Hook useConfirm. Der Aufruf bleibt so einfach wie vorher, weil er eine Zusage zurueckgibt: if (!(await confirm({ title: "…?", message: "…" }))) return; Damit sind alle neun Stellen umgestellt: Gruppen, Kategorien, Lagerorte, Einheiten, Benutzer, API-Tokens, Produkt und Charge loeschen sowie das Ersetzen beim Import. Jede Rueckfrage hat jetzt einen sprechenden Titel, einen Satz zu den Folgen und einen benannten Knopf ("Loeschen", "Widerrufen", "Ersetzen") statt eines nichtssagenden OK. Unwiderrufliche Schritte sind rot. Kuenftig gilt: keine Browserdialoge mehr. Export: Dateinamen tragen jetzt Datum und Uhrzeit (bestand_2026-07-22_2130.csv, project-good-backup_2026-07-22_2130.json). Ohne Zeitstempel hiessen mehrere Ausleitungen alle gleich und der Browser haengte (1), (2) an - dann war nicht mehr erkennbar, welche die aktuelle ist. Der Zeitstempel entsteht an beiden Enden gleich: im Content-Disposition-Kopf des Backends und im Dateinamen, den der Browser setzt. Im JSON steht zusaetzlich die Ortszeit neben dem bereits vorhandenen UTC-Zeitpunkt. Die CSV bleibt inhaltlich unveraendert - eine zusaetzliche Spalte oder Kopfzeile wuerde die Datei in Excel nur stoeren. Notizen an Gruppen-Codes: Codes, die beim Zuordnen eines Artikels automatisch entstehen, hatten bisher keine Moeglichkeit, eine Notiz zu bekommen - die liess sich nur beim Anlegen von Hand mitgeben. Neu PATCH /groups/{id}/barcodes/{code} und ein Notizfeld in der Liste, das beim Verlassen speichert. Leeren entfernt die Notiz. Getestet: 57 pytest-Tests unveraendert gruen, Web-Build laeuft durch. Gegen die laufende API geprueft: Zeitstempel in beiden Content-Disposition-Koepfen und im JSON-Inhalt; Notiz an einem automatisch angelegten Code setzen, aendern und leeren, unbekannter Code antwortet mit 404. Die Dialoge selbst habe ich nicht im Browser angeklickt. Co-Authored-By: Claude Opus 4.8 --- backend/app/routers/groups.py | 31 ++++++++++++++ backend/app/routers/transfer.py | 19 ++++++++- backend/app/schemas.py | 5 +++ web/src/api.js | 16 ++++++- web/src/components/BarcodeList.jsx | 17 ++++++-- web/src/confirm.jsx | 68 ++++++++++++++++++++++++++++++ web/src/main.jsx | 5 ++- web/src/pages/Categories.jsx | 10 ++++- web/src/pages/Groups.jsx | 16 ++++++- web/src/pages/Locations.jsx | 10 ++++- web/src/pages/ProductForm.jsx | 19 ++++++++- web/src/pages/Settings.jsx | 10 ++++- web/src/pages/Transfer.jsx | 9 +++- web/src/pages/Units.jsx | 10 ++++- web/src/pages/Users.jsx | 10 ++++- web/src/styles.css | 26 +++++++++++- 16 files changed, 263 insertions(+), 18 deletions(-) create mode 100644 web/src/confirm.jsx diff --git a/backend/app/routers/groups.py b/backend/app/routers/groups.py index dddf363..abca975 100644 --- a/backend/app/routers/groups.py +++ b/backend/app/routers/groups.py @@ -6,6 +6,7 @@ from ..deps import get_current_user, require_admin from ..models import Barcode, Group, Product, User from ..schemas import ( BarcodeCreate, + BarcodeNoteUpdate, BarcodeOut, GroupCreate, GroupOut, @@ -171,6 +172,36 @@ def add_group_barcode( return _group_to_out(db, group) +@router.patch("/{group_id}/barcodes/{code}", response_model=GroupOut) +def update_group_barcode_note( + group_id: int, + code: str, + payload: BarcodeNoteUpdate, + db: Session = Depends(get_db), + _: User = Depends(require_admin), +) -> GroupOut: + """Notiz zu einem Gruppen-Code setzen. + + Betrifft ausdrücklich auch Codes, die beim Zuordnen eines Artikels + automatisch entstanden sind – die hatten bisher gar keine Möglichkeit, + eine Notiz zu bekommen. + """ + group = db.get(Group, group_id) + if group is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "Gruppe nicht gefunden") + entry = ( + db.query(Barcode).filter(Barcode.group_id == group_id, Barcode.code == code).first() + ) + if entry is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "Code nicht gefunden") + + note = (payload.note or "").strip() + entry.note = note or None + db.commit() + db.refresh(group) + return _group_to_out(db, group) + + @router.delete("/{group_id}/barcodes/{code}", status_code=status.HTTP_204_NO_CONTENT) def delete_group_barcode( group_id: int, diff --git a/backend/app/routers/transfer.py b/backend/app/routers/transfer.py index e7de3d8..82eb80a 100644 --- a/backend/app/routers/transfer.py +++ b/backend/app/routers/transfer.py @@ -47,6 +47,15 @@ PACKAGE_TOKENS = {"packung", "package", "pkg", "pack"} # -------------------------------------------------------------------------- # Export # -------------------------------------------------------------------------- +def _zeitstempel() -> str: + """Ortszeit als YYYY-MM-DD_HHMM für Dateinamen. + + Ohne Zeitstempel heißen mehrere Ausleitungen alle gleich und der Browser + haengt (1), (2) an – dann weiß niemand mehr, welche die aktuelle ist. + """ + return datetime.now().strftime("%Y-%m-%d_%H%M") + + def _article_unit(product: Product) -> tuple[float, str]: """Faktor und Bezeichnung der Artikeleinheit (Gebinde, sonst Produkteinheit).""" unit_name, unit_factor = display_unit_info(product) @@ -104,7 +113,9 @@ def export_stock_csv( return Response( content="" + buf.getvalue(), media_type="text/csv; charset=utf-8", - headers={"Content-Disposition": 'attachment; filename="bestand.csv"'}, + headers={ + "Content-Disposition": f'attachment; filename="bestand_{_zeitstempel()}.csv"' + }, ) @@ -119,6 +130,7 @@ def export_backup_json( data = { "version": 1, "exported_at": datetime.now(timezone.utc).isoformat(), + "exported_at_local": datetime.now().isoformat(timespec="seconds"), "units": [ {"name": u.name, "kind": u.kind.value, "factor": u.factor} for u in db.query(Unit).order_by(Unit.id).all() @@ -171,7 +183,10 @@ def export_backup_json( return Response( content=json.dumps(data, ensure_ascii=False, indent=2), media_type="application/json; charset=utf-8", - headers={"Content-Disposition": 'attachment; filename="project-good-backup.json"'}, + headers={ + "Content-Disposition": + f'attachment; filename="project-good-backup_{_zeitstempel()}.json"' + }, ) diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 1937548..885f689 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -244,6 +244,11 @@ class BarcodeCreate(BaseModel): note: str | None = Field(default=None, max_length=120) +class BarcodeNoteUpdate(BaseModel): + """Notiz zu einem Code nachtragen oder ändern.""" + note: str | None = Field(default=None, max_length=120) + + # ---- Stock movements ---- class CheckInRequest(BaseModel): product_id: int | None = None diff --git a/web/src/api.js b/web/src/api.js index 33563ad..81433d3 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -72,6 +72,14 @@ async function request(path, { method = "GET", body, form, formData } = {}) { return data; } +/** Ortszeit als YYYY-MM-DD_HHMM fuer Dateinamen. */ +function zeitstempel() { + const jetzt = new Date(); + const zwei = (n) => String(n).padStart(2, "0"); + return `${jetzt.getFullYear()}-${zwei(jetzt.getMonth() + 1)}-${zwei(jetzt.getDate())}` + + `_${zwei(jetzt.getHours())}${zwei(jetzt.getMinutes())}`; +} + // Datei mit Auth-Header laden und im Browser als Download anbieten. export async function downloadFile(path, filename) { const token = getToken(); @@ -148,12 +156,16 @@ export const api = { updateGroup: (id, body) => request(`/groups/${id}`, { method: "PATCH", body }), deleteGroup: (id) => request(`/groups/${id}`, { method: "DELETE" }), addGroupBarcode: (id, body) => request(`/groups/${id}/barcodes`, { method: "POST", body }), + updateGroupBarcodeNote: (id, code, body) => + request(`/groups/${id}/barcodes/${encodeURIComponent(code)}`, { method: "PATCH", body }), deleteGroupBarcode: (id, code) => request(`/groups/${id}/barcodes/${encodeURIComponent(code)}`, { method: "DELETE" }), // Export / Import - exportCsv: () => downloadFile("/export/stock.csv", "bestand.csv"), - exportJson: () => downloadFile("/export/backup.json", "project-good-backup.json"), + // Zeitstempel im Dateinamen, sonst heissen mehrere Ausleitungen alle gleich. + exportCsv: () => downloadFile("/export/stock.csv", `bestand_${zeitstempel()}.csv`), + exportJson: () => + downloadFile("/export/backup.json", `project-good-backup_${zeitstempel()}.json`), importStock: (file, mode = "add") => { const fd = new FormData(); fd.append("file", file); diff --git a/web/src/components/BarcodeList.jsx b/web/src/components/BarcodeList.jsx index bc26b4e..72a872c 100644 --- a/web/src/components/BarcodeList.jsx +++ b/web/src/components/BarcodeList.jsx @@ -12,7 +12,7 @@ import Icon from "./Icon"; * onAdd({ code, note }) und onDelete(code) werden vom Aufrufer bereitgestellt. */ export default function BarcodeList({ - barcodes = [], productBarcodes = [], onAdd, onDelete, disabled = false, hint, + barcodes = [], productBarcodes = [], onAdd, onDelete, onEditNote, disabled = false, hint, }) { const [code, setCode] = useState(""); const [note, setNote] = useState(""); @@ -45,9 +45,20 @@ export default function BarcodeList({ ))} {barcodes.map((b) => (
  • - + {b.code} - {b.note && {b.note}} + {/* Notiz auch nachtraeglich aenderbar - automatisch angelegte Codes + hatten sonst nie eine Gelegenheit, eine zu bekommen. */} + {onEditNote && !disabled ? ( + { + const wert = e.target.value.trim(); + if (wert !== (b.note || "")) onEditNote(b.code, wert); + }} /> + ) : ( + b.note && {b.note} + )} {!disabled && ( + + + + + )} + + ); +} + +export function useConfirm() { + return useContext(ConfirmContext); +} diff --git a/web/src/main.jsx b/web/src/main.jsx index 4b52330..542cff3 100644 --- a/web/src/main.jsx +++ b/web/src/main.jsx @@ -4,6 +4,7 @@ import { BrowserRouter } from "react-router-dom"; import App from "./App"; import { AuthProvider } from "./auth"; import { SettingsProvider } from "./settings"; +import { ConfirmProvider } from "./confirm"; import "./styles.css"; import { applyFavicon } from "./branding"; @@ -15,7 +16,9 @@ ReactDOM.createRoot(document.getElementById("root")).render( - + + + diff --git a/web/src/pages/Categories.jsx b/web/src/pages/Categories.jsx index 8557d12..be5217a 100644 --- a/web/src/pages/Categories.jsx +++ b/web/src/pages/Categories.jsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { api } from "../api"; +import { useConfirm } from "../confirm"; import { useAuth } from "../auth"; import Icon from "../components/Icon"; import { asTree } from "../categoryTree"; @@ -11,6 +12,7 @@ import { asTree } from "../categoryTree"; * Gruppen. Genau dieser Unterschied soll auf den ersten Blick sichtbar sein. */ export default function Categories() { + const confirm = useConfirm(); const { isAdmin } = useAuth(); const [categories, setCategories] = useState([]); const [form, setForm] = useState({ name: "", parent_id: "" }); @@ -55,7 +57,13 @@ export default function Categories() { const hinweis = category.product_count ? ` ${category.product_count} Artikel verlieren die Zuordnung, bleiben aber erhalten.` : ""; - if (!confirm(`Kategorie "${category.name}" löschen?${hinweis}`)) return; + const ok = await confirm({ + title: `Kategorie „${category.name}“ löschen?`, + message: `Unterkategorien rücken eine Ebene nach oben.${hinweis}`, + confirmLabel: "Löschen", + danger: true, + }); + if (!ok) return; try { await api.deleteCategory(category.id); load(); diff --git a/web/src/pages/Groups.jsx b/web/src/pages/Groups.jsx index 9e31eab..18b2bff 100644 --- a/web/src/pages/Groups.jsx +++ b/web/src/pages/Groups.jsx @@ -1,11 +1,13 @@ import { useEffect, useState } from "react"; import { api } from "../api"; +import { useConfirm } from "../confirm"; import { useAuth } from "../auth"; import BarcodeList from "../components/BarcodeList"; import Icon from "../components/Icon"; import { fmt } from "../units"; export default function Groups() { + const confirm = useConfirm(); const { isAdmin } = useAuth(); const [groups, setGroups] = useState([]); const [units, setUnits] = useState([]); @@ -52,7 +54,13 @@ export default function Groups() { } async function remove(group) { - if (!confirm(`Gruppe "${group.name}" löschen? Produkte bleiben erhalten, verlieren aber die Zuordnung.`)) return; + const ok = await confirm({ + title: `Gruppe „${group.name}“ löschen?`, + message: "Die Produkte bleiben erhalten, verlieren aber ihre Zuordnung zu dieser Gruppe.", + confirmLabel: "Löschen", + danger: true, + }); + if (!ok) return; try { await api.deleteGroup(group.id); if (selectedId === group.id) setSelectedId(null); @@ -174,6 +182,12 @@ export default function Groups() { await load(); } catch (err) { setError(err.message); } }} + onEditNote={async (code, note) => { + try { + await api.updateGroupBarcodeNote(selected.id, code, { note }); + await load(); + } catch (err) { setError(err.message); } + }} onDelete={async (code) => { try { await api.deleteGroupBarcode(selected.id, code); diff --git a/web/src/pages/Locations.jsx b/web/src/pages/Locations.jsx index 2604aa8..9073631 100644 --- a/web/src/pages/Locations.jsx +++ b/web/src/pages/Locations.jsx @@ -1,8 +1,10 @@ import { useEffect, useState } from "react"; import { api } from "../api"; +import { useConfirm } from "../confirm"; import Icon from "../components/Icon"; export default function Locations() { + const confirm = useConfirm(); const [locations, setLocations] = useState([]); const [name, setName] = useState(""); const [parentId, setParentId] = useState(""); @@ -32,7 +34,13 @@ export default function Locations() { } async function remove(id) { - if (!confirm("Lagerort löschen? Untergeordnete Orte werden dann übergeordnet.")) return; + const ok = await confirm({ + title: "Lagerort löschen?", + message: "Untergeordnete Orte rücken eine Ebene nach oben.", + confirmLabel: "Löschen", + danger: true, + }); + if (!ok) return; try { await api.deleteLocation(id); load(); diff --git a/web/src/pages/ProductForm.jsx b/web/src/pages/ProductForm.jsx index 88b49d6..e599deb 100644 --- a/web/src/pages/ProductForm.jsx +++ b/web/src/pages/ProductForm.jsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { api } from "../api"; +import { useConfirm } from "../confirm"; import { useAuth } from "../auth"; import BarcodeList from "../components/BarcodeList"; import Icon from "../components/Icon"; @@ -28,6 +29,7 @@ const KIND_LABEL = { count: "Anzahl", weight: "Gewicht", volume: "Volumen" }; const KIND_ORDER = ["count", "weight", "volume"]; export default function ProductForm() { + const confirm = useConfirm(); const { id } = useParams(); const isNew = !id; const { isAdmin } = useAuth(); @@ -220,7 +222,13 @@ export default function ProductForm() { } async function remove() { - if (!confirm("Produkt wirklich löschen? Alle Chargen gehen verloren.")) return; + const ok = await confirm({ + title: "Produkt löschen?", + message: "Alle Chargen dieses Produkts gehen dabei verloren.", + confirmLabel: "Löschen", + danger: true, + }); + if (!ok) return; try { await api.deleteProduct(id); navigate("/products"); @@ -423,6 +431,7 @@ export default function ProductForm() { /** Chargen-Karte mit Bearbeiten/Löschen. Bearbeitet wird in der Produkteinheit. */ function LotsCard({ product, lots, baseShort, warnDays, isAdmin, imageUrl, onChanged, onError }) { + const confirm = useConfirm(); const { formatBestBefore } = useSettings(); const [editId, setEditId] = useState(null); const [draft, setDraft] = useState({ quantity: "", best_before: "", precision: "day" }); @@ -491,7 +500,13 @@ function LotsCard({ product, lots, baseShort, warnDays, isAdmin, imageUrl, onCha } async function removeLot(l) { - if (!confirm("Charge wirklich löschen?")) return; + const ok = await confirm({ + title: "Charge löschen?", + message: "Die Menge wird als Korrektur im Verlauf protokolliert.", + confirmLabel: "Löschen", + danger: true, + }); + if (!ok) return; try { await api.deleteLot(l.id); await onChanged(); diff --git a/web/src/pages/Settings.jsx b/web/src/pages/Settings.jsx index 54b77ad..65e53ab 100644 --- a/web/src/pages/Settings.jsx +++ b/web/src/pages/Settings.jsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { api } from "../api"; +import { useConfirm } from "../confirm"; import Icon from "../components/Icon"; import BrandingUpload from "../components/BrandingUpload"; import DangerAction from "../components/DangerAction"; @@ -109,6 +110,7 @@ export default function Settings() { /** Langlebige Tokens für externe Zugriffe (Home Assistant, Skripte). */ function ApiTokensCard() { + const confirm = useConfirm(); const [tokens, setTokens] = useState([]); const [name, setName] = useState(""); const [fresh, setFresh] = useState(null); @@ -138,7 +140,13 @@ function ApiTokensCard() { } async function remove(token) { - if (!confirm(`Token "${token.name}" widerrufen? Zugriffe damit funktionieren sofort nicht mehr.`)) return; + const ok = await confirm({ + title: `Token „${token.name}“ widerrufen?`, + message: "Zugriffe mit diesem Token funktionieren sofort nicht mehr.", + confirmLabel: "Widerrufen", + danger: true, + }); + if (!ok) return; try { await api.deleteApiToken(token.id); load(); diff --git a/web/src/pages/Transfer.jsx b/web/src/pages/Transfer.jsx index 5b37eb4..6ef1797 100644 --- a/web/src/pages/Transfer.jsx +++ b/web/src/pages/Transfer.jsx @@ -1,8 +1,10 @@ import { useRef, useState } from "react"; import { api } from "../api"; +import { useConfirm } from "../confirm"; import Icon from "../components/Icon"; export default function Transfer() { + const confirm = useConfirm(); const fileRef = useRef(null); const [file, setFile] = useState(null); const [mode, setMode] = useState("add"); @@ -36,7 +38,12 @@ export default function Transfer() { mode === "replace_all" ? "ALLE Bestände werden vorher gelöscht und aus der Datei neu aufgebaut. Fortfahren?" : "Für alle Produkte in der Datei werden die vorhandenen Chargen gelöscht und ersetzt. Fortfahren?"; - if (!confirm(warning)) return; + if (!(await confirm({ + title: "Vorhandene Bestände ersetzen?", + message: warning, + confirmLabel: "Ersetzen", + danger: true, + }))) return; } setError(null); setResult(null); diff --git a/web/src/pages/Units.jsx b/web/src/pages/Units.jsx index 2f4f102..0c39aea 100644 --- a/web/src/pages/Units.jsx +++ b/web/src/pages/Units.jsx @@ -1,11 +1,13 @@ import { useEffect, useState } from "react"; import { api } from "../api"; +import { useConfirm } from "../confirm"; import Icon from "../components/Icon"; import { fmt } from "../units"; const KIND_LABEL = { count: "Anzahl (Stück)", weight: "Gewicht (Basis: Gramm)", volume: "Volumen (Basis: Milliliter)" }; export default function Units() { + const confirm = useConfirm(); const [units, setUnits] = useState([]); const [form, setForm] = useState({ name: "", kind: "weight", factor: "" }); const [error, setError] = useState(null); @@ -33,7 +35,13 @@ export default function Units() { } async function remove(u) { - if (!confirm(`Einheit "${u.name}" löschen?`)) return; + const ok = await confirm({ + title: `Einheit „${u.name}“ löschen?`, + message: "Produkte, die diese Einheit nutzen, fallen auf ihre Basiseinheit zurück.", + confirmLabel: "Löschen", + danger: true, + }); + if (!ok) return; try { await api.deleteUnit(u.id); load(); diff --git a/web/src/pages/Users.jsx b/web/src/pages/Users.jsx index 3c3be3f..cb7d002 100644 --- a/web/src/pages/Users.jsx +++ b/web/src/pages/Users.jsx @@ -1,9 +1,11 @@ import { useEffect, useState } from "react"; import { api } from "../api"; +import { useConfirm } from "../confirm"; import { useAuth } from "../auth"; import Icon from "../components/Icon"; export default function Users() { + const confirm = useConfirm(); const { user: me } = useAuth(); const [users, setUsers] = useState([]); const [form, setForm] = useState({ username: "", password: "", role: "user" }); @@ -43,7 +45,13 @@ export default function Users() { } async function remove(u) { - if (!confirm(`Benutzer "${u.username}" löschen?`)) return; + const ok = await confirm({ + title: `Benutzer „${u.username}“ löschen?`, + message: "Die Anmeldung mit diesem Konto ist danach nicht mehr möglich.", + confirmLabel: "Löschen", + danger: true, + }); + if (!ok) return; try { await api.deleteUser(u.id); load(); diff --git a/web/src/styles.css b/web/src/styles.css index 941fa60..558e54a 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -305,7 +305,31 @@ td select { width: auto; min-width: 132px; max-width: 100%; } .precision-pick { display: flex; align-items: center; gap: var(--sp-2); margin: 0; font-weight: 400; } .precision-pick select { margin: 0; width: auto; padding-top: 3px; padding-bottom: 3px; font-size: 0.8rem; } /* Hinweis beim Einlagern: welche Kategorie automatisch zugeordnet wird */ - /* Gefahrenbereich: deutlich abgesetzt, damit man nicht versehentlich hineinruscht */ + /* ---------- Rueckfrage-Dialog ---------- */ +.modal-backdrop { + position: fixed; inset: 0; z-index: 50; + display: grid; place-items: center; + padding: var(--sp-4); + background: rgba(9, 12, 18, 0.55); +} +.modal { + width: 100%; max-width: 440px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: var(--sp-5); + box-shadow: 0 12px 32px rgba(9, 12, 18, 0.28); +} +.modal-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-3); } +.modal-head h2 { margin: 0; } +.modal-body { margin: 0 0 var(--sp-5); color: var(--muted); font-size: 0.9rem; } +.icon-danger { color: var(--danger); } +/* Gefuellter Knopf fuer unwiderrufliche Schritte - der flache .btn.danger + geht in einem Dialog zu leicht unter. */ +.btn.danger-solid { background: var(--danger); border-color: var(--danger); color: #fff; } +.btn.danger-solid:hover { background: #a63224; border-color: #a63224; } + +/* Gefahrenbereich: deutlich abgesetzt, damit man nicht versehentlich hineinruscht */ .danger-zone { border-color: var(--danger); } .danger-zone .card-head .icon { color: var(--danger); } .danger-action { padding: var(--sp-4) 0; border-top: 1px solid var(--border); }