Favicon aus dem Seitenleisten-Zeichen und Gefahrenbereich in den Einstellungen
Favicon: Bisher diente der Barcode-Glyph des App-Icons als Standard. Jetzt
zeigt der Browser-Tab dasselbe Zeichen wie die Seitenleiste - das "box"-Icon
aus components/Icon.jsx auf der Akzentflaeche. Die Farbe folgt der Oberflaeche:
im dunklen Modus der hellere Akzent, damit das Symbol auf beiden Untergruenden
sitzt. Ein eigenes Favicon laesst sich weiterhin hochladen und hat Vorrang.
Gefahrenbereich (Einstellungen, nur Admin) mit drei Stufen, damit man nicht
mehr wegwirft als noetig:
1. Alle Bestaende loeschen - Chargen und Bewegungsverlauf. Der Katalog bleibt
vollstaendig erhalten, der Bestand steht ueberall auf null.
2. Alle Artikel loeschen - zusaetzlich der Artikelkatalog samt seiner
EAN-Codes. Gruppen, Kategorien, Lagerorte und Einheiten bleiben. Die Codes,
die an Gruppen haengen, bleiben ebenfalls.
3. Alles zuruecksetzen - zusaetzlich Gruppen, Kategorien, Lagerorte, Einheiten.
Benutzer, API-Tokens, Einstellungen und das eigene Logo bleiben in allen drei
Stufen erhalten. Anders liesse man sich aussperren und muesste externe Zugriffe
wie Home Assistant neu einrichten. Nach Stufe 3 werden die eingebauten
Einheiten und die Kategorie-Startliste sofort neu angelegt - ohne Einheiten
liesse sich nichts mehr einlagern, die Installation waere unbrauchbar.
Absicherung: Jede Aktion nennt vorher mit Zahlen, was genau verschwindet
("Betrifft 43 Chargen und 12 Bewegungen"), und laeuft erst, wenn das Wort
LOESCHEN eingetippt wurde. Ein Fehlklick kann nichts anrichten. Der Abschnitt
ist rot abgesetzt und weist auf das JSON-Backup hin.
Getestet: 57 pytest-Tests gruen, 3 davon neu (Bestaende loeschen laesst den
Katalog stehen, Artikel loeschen laesst Gruppen, Lagerorte und Einheiten sowie
den Gruppen-Code stehen, Startkategorien kommen zurueck). Gegen die laufende
API alle drei Stufen nacheinander durchgespielt und die Zahlen nach jedem
Schritt geprueft; Benutzer bleiben, Einheiten sind danach wieder da, ohne
Adminrechte antwortet der Endpunkt mit 401. Das Favicon wurde gerendert und
mit dem Zeichen der Seitenleiste verglichen. Web-Build laeuft durch.
Die Oberflaeche habe ich nicht selbst bedient - insbesondere habe ich keine
der Loeschaktionen ueber die Web-Oberflaeche ausgeloest.
Nebenbei die deutschen Anfuehrungszeichen im ganzen Projekt vereinheitlicht
(schliessendes Zeichen typografisch statt gerade).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
121
backend/app/routers/maintenance.py
Normal file
121
backend/app/routers/maintenance.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Gefahrenbereich: Daten in großem Stil löschen.
|
||||
|
||||
Bewusst in drei Stufen, damit man nicht mehr wegwirft als nötig:
|
||||
|
||||
1. ``/reset/stock`` – Chargen und Bewegungsverlauf. Der Katalog bleibt.
|
||||
2. ``/reset/products`` – zusätzlich alle Artikel samt EAN-Codes.
|
||||
3. ``/reset/all`` – zusätzlich Gruppen, Kategorien, Lagerorte, Einheiten.
|
||||
|
||||
Benutzer, API-Tokens, Einstellungen und das eigene Logo bleiben in **allen**
|
||||
Stufen erhalten: Sonst würde man sich aussperren und müsste externe Zugriffe
|
||||
(Home Assistant) neu einrichten.
|
||||
|
||||
Nach Stufe 3 werden die eingebauten Einheiten und die Kategorie-Startliste
|
||||
sofort neu angelegt – ohne Einheiten ließe sich nichts mehr einlagern.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from ..deps import require_admin
|
||||
from ..models import (
|
||||
Barcode,
|
||||
Category,
|
||||
Group,
|
||||
Location,
|
||||
Lot,
|
||||
Movement,
|
||||
Product,
|
||||
Unit,
|
||||
User,
|
||||
)
|
||||
from ..seed import ensure_builtin_categories, ensure_builtin_units
|
||||
|
||||
router = APIRouter(prefix="/maintenance", tags=["maintenance"])
|
||||
|
||||
|
||||
class ResetSummary(BaseModel):
|
||||
"""Was gerade in der Datenbank steht – für die Rückfrage vor dem Löschen."""
|
||||
lots: int = 0
|
||||
movements: int = 0
|
||||
products: int = 0
|
||||
groups: int = 0
|
||||
categories: int = 0
|
||||
locations: int = 0
|
||||
units: int = 0
|
||||
|
||||
|
||||
def _summary(db: Session) -> ResetSummary:
|
||||
return ResetSummary(
|
||||
lots=db.query(Lot).count(),
|
||||
movements=db.query(Movement).count(),
|
||||
products=db.query(Product).count(),
|
||||
groups=db.query(Group).count(),
|
||||
categories=db.query(Category).count(),
|
||||
locations=db.query(Location).count(),
|
||||
units=db.query(Unit).count(),
|
||||
)
|
||||
|
||||
|
||||
def _delete_stock(db: Session) -> None:
|
||||
"""Chargen und Verlauf. Zuerst die Bewegungen, sie verweisen auf Chargen."""
|
||||
db.query(Movement).delete(synchronize_session=False)
|
||||
db.query(Lot).delete(synchronize_session=False)
|
||||
|
||||
|
||||
def _delete_products(db: Session) -> None:
|
||||
_delete_stock(db)
|
||||
# EAN-Codes hängen an Artikeln UND an Gruppen. Hier gehen nur die der
|
||||
# Artikel weg; die Gruppen-Codes bleiben, solange die Gruppen bleiben.
|
||||
db.query(Barcode).filter(Barcode.product_id.isnot(None)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
db.query(Product).delete(synchronize_session=False)
|
||||
|
||||
|
||||
@router.get("/summary", response_model=ResetSummary)
|
||||
def summary(db: Session = Depends(get_db), _: User = Depends(require_admin)) -> ResetSummary:
|
||||
return _summary(db)
|
||||
|
||||
|
||||
@router.post("/reset/stock", response_model=ResetSummary)
|
||||
def reset_stock(
|
||||
db: Session = Depends(get_db), _: User = Depends(require_admin)
|
||||
) -> ResetSummary:
|
||||
"""Alle Bestände auf null – Artikel, Gruppen und Stammdaten bleiben."""
|
||||
_delete_stock(db)
|
||||
db.commit()
|
||||
return _summary(db)
|
||||
|
||||
|
||||
@router.post("/reset/products", response_model=ResetSummary)
|
||||
def reset_products(
|
||||
db: Session = Depends(get_db), _: User = Depends(require_admin)
|
||||
) -> ResetSummary:
|
||||
"""Katalog leeren: Artikel samt Chargen, Verlauf und ihren EAN-Codes."""
|
||||
_delete_products(db)
|
||||
db.commit()
|
||||
return _summary(db)
|
||||
|
||||
|
||||
@router.post("/reset/all", response_model=ResetSummary)
|
||||
def reset_all(
|
||||
db: Session = Depends(get_db), _: User = Depends(require_admin)
|
||||
) -> ResetSummary:
|
||||
"""Zusätzlich Gruppen, Kategorien, Lagerorte und Einheiten."""
|
||||
_delete_products(db)
|
||||
# Restliche EAN-Codes (die an Gruppen hängen) mitnehmen.
|
||||
db.query(Barcode).delete(synchronize_session=False)
|
||||
db.query(Group).delete(synchronize_session=False)
|
||||
db.query(Category).delete(synchronize_session=False)
|
||||
db.query(Location).delete(synchronize_session=False)
|
||||
db.query(Unit).delete(synchronize_session=False)
|
||||
db.commit()
|
||||
|
||||
# Ohne Einheiten wäre die Installation unbrauchbar, deshalb sofort zurück
|
||||
# auf den Auslieferungsstand.
|
||||
ensure_builtin_units(db)
|
||||
ensure_builtin_categories(db)
|
||||
return _summary(db)
|
||||
Reference in New Issue
Block a user