Files
Vorrania/backend/app/main.py
Scarriffle 48cc2646e8 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>
2026-07-22 21:21:52 +02:00

110 lines
3.4 KiB
Python
Raw 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.
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import text
from .config import get_settings
from .database import Base, SessionLocal, engine
from .routers import (
api_tokens,
auth,
branding,
categories,
groups,
locations,
maintenance,
products,
settings as settings_router,
stock,
transfer,
units,
users,
views,
)
from .seed import ensure_builtin_categories, ensure_builtin_units, ensure_first_admin
settings = get_settings()
def _ensure_schema() -> None:
"""Schonende Migration: fehlende Spalten auf bestehenden Tabellen nachziehen.
create_all() legt nur fehlende Tabellen an, ändert aber keine bestehenden.
Auf Postgres holen wir neue nullable-Spalten per ADD COLUMN IF NOT EXISTS nach,
damit vorhandene Installationen ihre Daten behalten. Auf SQLite (Tests) sind
die Spalten bereits durch create_all vorhanden.
"""
if engine.dialect.name != "postgresql":
return
stmts = [
"ALTER TABLE products ADD COLUMN IF NOT EXISTS display_unit_id INTEGER "
"REFERENCES units(id) ON DELETE SET NULL",
"ALTER TABLE groups ADD COLUMN IF NOT EXISTS min_stock_unit_id INTEGER "
"REFERENCES units(id) ON DELETE SET NULL",
"ALTER TABLE products ADD COLUMN IF NOT EXISTS min_stock_unit_id INTEGER "
"REFERENCES units(id) ON DELETE SET NULL",
"ALTER TABLE products ADD COLUMN IF NOT EXISTS min_stock_in_packages BOOLEAN "
"NOT NULL DEFAULT FALSE",
"ALTER TABLE products ADD COLUMN IF NOT EXISTS package_label VARCHAR(32)",
"ALTER TABLE products ADD COLUMN IF NOT EXISTS date_precision VARCHAR(8) "
"NOT NULL DEFAULT 'day'",
"ALTER TABLE lots ADD COLUMN IF NOT EXISTS best_before_precision VARCHAR(8) "
"NOT NULL DEFAULT 'day'",
"ALTER TABLE products ADD COLUMN IF NOT EXISTS category_id INTEGER "
"REFERENCES categories(id) ON DELETE SET NULL",
]
with engine.begin() as conn:
for stmt in stmts:
conn.execute(text(stmt))
@asynccontextmanager
async def lifespan(app: FastAPI):
# Tabellen anlegen (MVP: create_all statt Alembic-Migrationen).
Base.metadata.create_all(bind=engine)
_ensure_schema()
db = SessionLocal()
try:
ensure_builtin_units(db)
ensure_builtin_categories(db)
ensure_first_admin(db)
finally:
db.close()
yield
app = FastAPI(title="Project-Good Lebensmittel-Lagerverwaltung", version="1.0.0", lifespan=lifespan)
origins = ["*"] if settings.cors_origins.strip() == "*" else [
o.strip() for o in settings.cors_origins.split(",") if o.strip()
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health", tags=["meta"])
def health() -> dict:
return {"status": "ok"}
app.include_router(auth.router)
app.include_router(users.router)
app.include_router(products.router)
app.include_router(stock.router)
app.include_router(locations.router)
app.include_router(groups.router)
app.include_router(units.router)
app.include_router(views.router)
app.include_router(transfer.router)
app.include_router(api_tokens.router)
app.include_router(settings_router.router)
app.include_router(branding.router)
app.include_router(categories.router)
app.include_router(maintenance.router)