feat(foundation): FOUND-013 optionales Error-Tracking (GlitchTip)
Zielserver-Entscheidung durch optionales Install-Script aufgelöst statt festgeschrieben: deploy/glitchtip-install.sh installiert GlitchTip per Docker Compose auf einem beliebigen, selbst gewählten Host - nichts automatisch/ungefragt. DSN wird zur Laufzeit über eine neue Admin-GUI-Seite ein-/ausschaltbar (Systemeinstellung-Singleton, GET/PATCH /einstellungen/sentry), statt nur per .env - kein Redeploy zum Umschalten nötig. Frontend lädt @sentry/react nur dynamisch nach, wenn tatsächlich aktiv. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
1 parent
11e6ed4332
commit
e5dc80c3a6
21 files changed
+612
-6
No files matched your search
@@ -135,7 +135,7 @@ vergeben statt geraten.
|
||||
| FOUND-010 | Foundation | Backup-Strategie | P1 | S | MEDIUM | FOUND-002 | ✅ (`deploy/backup_datenbank.sh` + systemd-Timer-Vorlagen, taeglich mit Rotation) |
|
||||
| FOUND-011 | Foundation | Test-Grundgerüst | P0 | M | LOW | FOUND-002 | ✅ (`backend/pytest.ini`+`backend/tests/`, seit 2026-09-08 auch `frontend/vitest.config.ts`+`src/**/*.test.ts(x)`, beide in CI) |
|
||||
| FOUND-012 | Foundation | Dokumentationsgerüst | P1 | XS | LOW | FOUND-001 | ✅ (`README.md` im Projektwurzelverzeichnis) |
|
||||
| FOUND-013 | Foundation | Error-Tracking (GlitchTip, neu, geplant 2026-09-10) | P2 | M | LOW | — | ⬜ Plan steht, Zielserver-Entscheidung offen |
|
||||
| FOUND-013 | Foundation | Error-Tracking (GlitchTip, neu, geplant 2026-09-10) | P2 | M | LOW | — | ✅ erledigt 2026-09-10 (optionales Install-Script statt Zielserver-Festlegung) |
|
||||
| IDENT-001 | Identity | Objekt-ID-Schema | P0 | S | LOW | FOUND-002 | ✅ (Objekt-Code-Vergabe, `/objekte/naechster-code`) |
|
||||
| IDENT-002 | Identity | Inventarnummer manuell | P0 | S | LOW | IDENT-001 | ✅ (Objektposition-Code, `objektposition.py`) |
|
||||
| IDENT-003 | Identity | Inventarnummer-Nummernschemata | P1 | M | MEDIUM | IDENT-002 | ✅ (`Nummernschema` je Objekttyp, Migration 0032, `/objekte/naechster-code?objekttyp_id=` nutzt es, Admin-UI in ObjekttypSection) |
|
||||
|
||||
@@ -281,9 +281,23 @@ nicht detaillierten) siehe `00_index.md`.
|
||||
- **Akzeptanzkriterien:** eine absichtlich ausgelöste Backend-Exception UND
|
||||
eine Frontend-Exception erscheinen in GlitchTip mit Stacktrace.
|
||||
- **Tests:** keine (reine Infrastruktur-/Konfigurationsintegration).
|
||||
- **DoD:** offen — Plan steht, **Zielserver-Entscheidung + tatsächliche
|
||||
Docker-Compose-Inbetriebnahme ausstehend**, nicht ohne explizite
|
||||
Bestätigung durchführen (Server-Deploy-Regel, CLAUDE.md).
|
||||
- **DoD:** ✅ erledigt (2026-09-10) — **Zielserver-Entscheidung durch
|
||||
optionales Install-Script aufgelöst statt festgeschrieben:**
|
||||
`deploy/glitchtip-install.sh` installiert GlitchTip per Docker Compose auf
|
||||
einem BELIEBIGEN, vom Nutzer gewählten Host (MABEA-Server selbst oder
|
||||
separat) — nichts wird automatisch/ungefragt installiert, das Script muss
|
||||
bewusst ausgeführt werden. Zusätzlich, über die ursprüngliche Planung
|
||||
hinaus: DSN wird NICHT nur per `.env` gesetzt, sondern zur Laufzeit über
|
||||
eine neue Admin-GUI-Seite („Fehler-Tracking") ein-/ausschaltbar —
|
||||
`Systemeinstellung`-Singleton-Tabelle (Migration `0053_systemeinstellung.py`),
|
||||
`GET/PATCH /einstellungen/sentry` (GET bewusst ohne Auth, DSN ist kein
|
||||
Geheimnis, wird ohnehin im Frontend-Bundle sichtbar), `sentry_sdk.init()`
|
||||
wird beim Setzen sofort erneut aufgerufen (kein Backend-Neustart nötig).
|
||||
Frontend lädt die DSN zur Laufzeit (`src/monitoring/sentry.ts`) und lädt
|
||||
`@sentry/react` nur dynamisch nach, wenn tatsächlich aktiv (kein Bundle-
|
||||
Gewicht im Normalfall). `.env`-Variable `SENTRY_DSN` bleibt als Bootstrap-
|
||||
Fallback bestehen, DB-Wert hat Vorrang. `send_default_pii=False` fest
|
||||
verdrahtet (DSGVO). Tests: `test_found013_systemeinstellung.py`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""FOUND-013: Systemeinstellung-Singleton (GlitchTip/Sentry-DSN per Admin-GUI).
|
||||
|
||||
Revision ID: 0053_systemeinstellung
|
||||
Revises: 0052_inventur
|
||||
Create Date: 2026-09-10
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0053_systemeinstellung"
|
||||
down_revision: Union[str, None] = "0052_inventur"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE systemeinstellung (
|
||||
id INTEGER PRIMARY KEY,
|
||||
sentry_dsn TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute("INSERT INTO systemeinstellung (id, sentry_dsn) VALUES (1, NULL)")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP TABLE systemeinstellung")
|
||||
@@ -32,6 +32,7 @@ from app.api.v1.endpoints import (
|
||||
sollmengen_gruppe,
|
||||
stammdaten,
|
||||
suche,
|
||||
systemeinstellung,
|
||||
tankbuch,
|
||||
verleih,
|
||||
vorlagen,
|
||||
@@ -82,3 +83,4 @@ api_router.include_router(readiness_regel.router, tags=["readiness_regel"])
|
||||
api_router.include_router(einsatz.router, tags=["einsatz"])
|
||||
api_router.include_router(sollmengen_gruppe.router, tags=["sollmengen_gruppe"])
|
||||
api_router.include_router(inventur.router, tags=["inventur"])
|
||||
api_router.include_router(systemeinstellung.router, tags=["systemeinstellung"])
|
||||
@@ -0,0 +1,30 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import require_roles
|
||||
from app.db.session import get_db
|
||||
from app.models.auth import RolleTyp
|
||||
from app.schemas.systemeinstellung import SentryEinstellungRead, SentryEinstellungUpdate
|
||||
from app.services.systemeinstellung import hole_einstellung, setze_sentry_dsn
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_administration = require_roles(RolleTyp.administration)
|
||||
|
||||
|
||||
@router.get("/einstellungen/sentry", response_model=SentryEinstellungRead)
|
||||
async def hole_sentry_einstellung(db: AsyncSession = Depends(get_db)) -> SentryEinstellungRead:
|
||||
"""Bewusst OHNE Auth: der DSN ist kein Geheimnis (wird ohnehin unverschlüsselt
|
||||
im Frontend-Bundle/Browser-Request sichtbar, Sentry-SDKs sind dafür designt) -
|
||||
das Frontend muss ihn schon vor dem Login laden können, um auch
|
||||
Fehler auf der Login-Seite selbst zu erfassen."""
|
||||
einstellung = await hole_einstellung(db)
|
||||
return SentryEinstellungRead(sentry_dsn=einstellung.sentry_dsn, aktiv=bool(einstellung.sentry_dsn))
|
||||
|
||||
|
||||
@router.patch("/einstellungen/sentry", response_model=SentryEinstellungRead)
|
||||
async def aendere_sentry_einstellung(
|
||||
payload: SentryEinstellungUpdate, db: AsyncSession = Depends(get_db), _=Depends(_administration)
|
||||
) -> SentryEinstellungRead:
|
||||
einstellung = await setze_sentry_dsn(db, sentry_dsn=payload.sentry_dsn)
|
||||
return SentryEinstellungRead(sentry_dsn=einstellung.sentry_dsn, aktiv=bool(einstellung.sentry_dsn))
|
||||
@@ -36,5 +36,10 @@ class Settings(BaseSettings):
|
||||
# Code-Änderung/Neubau.
|
||||
log_level: str = "INFO"
|
||||
|
||||
# FOUND-013: GlitchTip (Sentry-API-kompatibel) für Error-Tracking, optional
|
||||
# per deploy/glitchtip-install.sh selbst gehostet. Leer (Default) = kein
|
||||
# Fehler-Tracking, No-Op - kein Zwang, GlitchTip zu betreiben.
|
||||
sentry_dsn: str = ""
|
||||
|
||||
|
||||
settings = Settings()
|
||||
+16
-1
@@ -1,5 +1,6 @@
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import sentry_sdk
|
||||
from fastapi import FastAPI
|
||||
from slowapi import _rate_limit_exceeded_handler
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
@@ -11,7 +12,8 @@ from app.api.v1.api import api_router
|
||||
from app.core.app_settings import settings
|
||||
from app.core.logging_config import konfiguriere_logging
|
||||
from app.core.rate_limit import limiter
|
||||
from app.db.session import engine
|
||||
from app.db.session import SessionLocal, engine
|
||||
from app.services.systemeinstellung import hole_einstellung
|
||||
|
||||
konfiguriere_logging()
|
||||
|
||||
@@ -25,6 +27,19 @@ async def lifespan(app: FastAPI):
|
||||
"JWT_SECRET_KEY ist leer oder noch der Platzhalter aus example.env – "
|
||||
"in .env auf dem Zielsystem einen echten zufälligen Wert setzen."
|
||||
)
|
||||
|
||||
# FOUND-013: per Admin-GUI gesetzter DSN (DB) hat Vorrang vor der
|
||||
# .env-Variable (Bootstrap-Fallback, z.B. beim allerersten Start ohne
|
||||
# bereits existierende Systemeinstellung-Zeile).
|
||||
async with SessionLocal() as db:
|
||||
einstellung = await hole_einstellung(db)
|
||||
await db.commit()
|
||||
dsn = einstellung.sentry_dsn or settings.sentry_dsn
|
||||
if dsn:
|
||||
# send_default_pii bewusst False (DSGVO, Art.-9-Bezug bei
|
||||
# Rettungsdienst) - keine IP-Adressen/Nutzerdaten automatisch mitschicken.
|
||||
sentry_sdk.init(dsn=dsn, send_default_pii=False, traces_sample_rate=0.0)
|
||||
|
||||
yield
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ from app.models.personal import (
|
||||
)
|
||||
from app.models.sollmengen_gruppe import SollmengenGruppe
|
||||
from app.models.stammdaten import Bereich, Kategorie, Material, MaterialTyp, Objekttyp, Standort
|
||||
from app.models.systemeinstellung import Systemeinstellung
|
||||
from app.models.tankbuch import TankbuchEintrag
|
||||
from app.models.verleih import ExterneStelle, ExterneStelleKategorie, Verleih
|
||||
from app.models.vorlage import Beladungsvorlage, VorlageStatus, Vorlagenposition
|
||||
@@ -118,6 +119,7 @@ __all__ = [
|
||||
"Objekttyp",
|
||||
"SollmengenGruppe",
|
||||
"Standort",
|
||||
"Systemeinstellung",
|
||||
"TankbuchEintrag",
|
||||
"ExterneStelle",
|
||||
"ExterneStelleKategorie",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy import Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class Systemeinstellung(Base):
|
||||
"""FOUND-013: Singleton-Konfigurationszeile (id fest =1) für Einstellungen,
|
||||
die über die Admin-GUI statt nur per .env gesetzt werden sollen - aktuell
|
||||
nur der GlitchTip/Sentry-DSN (optionales Error-Tracking, leer = deaktiviert,
|
||||
kein Zwang, GlitchTip zu betreiben)."""
|
||||
|
||||
__tablename__ = "systemeinstellung"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
sentry_dsn: Mapped[str | None] = mapped_column(String)
|
||||
@@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class SentryEinstellungRead(BaseModel):
|
||||
sentry_dsn: str | None
|
||||
aktiv: bool
|
||||
|
||||
|
||||
class SentryEinstellungUpdate(BaseModel):
|
||||
sentry_dsn: str | None = None
|
||||
@@ -0,0 +1,27 @@
|
||||
import sentry_sdk
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.systemeinstellung import Systemeinstellung
|
||||
|
||||
_SINGLETON_ID = 1
|
||||
|
||||
|
||||
async def hole_einstellung(db: AsyncSession) -> Systemeinstellung:
|
||||
einstellung = await db.get(Systemeinstellung, _SINGLETON_ID)
|
||||
if einstellung is None:
|
||||
einstellung = Systemeinstellung(id=_SINGLETON_ID, sentry_dsn=None)
|
||||
db.add(einstellung)
|
||||
await db.flush()
|
||||
return einstellung
|
||||
|
||||
|
||||
async def setze_sentry_dsn(db: AsyncSession, *, sentry_dsn: str | None) -> Systemeinstellung:
|
||||
einstellung = await hole_einstellung(db)
|
||||
einstellung.sentry_dsn = sentry_dsn or None
|
||||
await db.flush()
|
||||
|
||||
# FOUND-013: sentry_sdk.init() ist idempotent aufrufbar - Änderung greift
|
||||
# sofort im laufenden Prozess, kein Backend-Neustart nötig. Leerer DSN
|
||||
# deaktiviert das Tracking wieder (kein Fehler, reines No-Op).
|
||||
sentry_sdk.init(dsn=einstellung.sentry_dsn or None, send_default_pii=False, traces_sample_rate=0.0)
|
||||
return einstellung
|
||||
@@ -21,6 +21,7 @@ dependencies = [
|
||||
"python-barcode[images]>=0.15,<0.16",
|
||||
"reportlab>=4.2,<4.3",
|
||||
"slowapi>=0.1.9,<0.2",
|
||||
"sentry-sdk[fastapi]>=2.17,<2.18",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
|
||||
from tests.conftest import auth_header, login
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sentry_einstellung_default_inaktiv(client):
|
||||
response = await client.get("/api/v1/einstellungen/sentry")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"sentry_dsn": None, "aktiv": False}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sentry_einstellung_setzen_erfordert_admin(client, mitarbeiter_user):
|
||||
token = await login(client, "mitarbeiter1")
|
||||
response = await client.patch(
|
||||
"/api/v1/einstellungen/sentry",
|
||||
json={"sentry_dsn": "https://example@glitchtip.example.org/1"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_kann_sentry_dsn_setzen_und_wieder_leeren(client, admin_user):
|
||||
token = await login(client, "admin1")
|
||||
|
||||
gesetzt = await client.patch(
|
||||
"/api/v1/einstellungen/sentry",
|
||||
json={"sentry_dsn": "https://example@glitchtip.example.org/1"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert gesetzt.status_code == 200
|
||||
assert gesetzt.json()["aktiv"] is True
|
||||
assert gesetzt.json()["sentry_dsn"] == "https://example@glitchtip.example.org/1"
|
||||
|
||||
geholt = await client.get("/api/v1/einstellungen/sentry")
|
||||
assert geholt.json()["aktiv"] is True
|
||||
|
||||
geleert = await client.patch(
|
||||
"/api/v1/einstellungen/sentry", json={"sentry_dsn": None}, headers=auth_header(token)
|
||||
)
|
||||
assert geleert.status_code == 200
|
||||
assert geleert.json() == {"sentry_dsn": None, "aktiv": False}
|
||||
@@ -67,6 +67,33 @@ Tagen (`AUFBEWAHRUNG_TAGE` in der `.service`-Datei anpassbar). Manueller Testlau
|
||||
**Restore-Test nie automatisiert durchgeführt** — vor echtem Ernstfall einmal manuell
|
||||
verifizieren (`gunzip -c <backup>.sql.gz | psql <testdatenbank>`).
|
||||
|
||||
## Fehler-Tracking (GlitchTip) einrichten (FOUND-013, optional)
|
||||
|
||||
GlitchTip ist Sentry-API-kompatibles, selbst gehostetes Error-Tracking —
|
||||
komplett optional, MABEA läuft ohne unverändert weiter. Installation auf
|
||||
einem BELIEBIGEN Host (kann der MABEA-Server selbst oder ein separater sein):
|
||||
|
||||
```bash
|
||||
scp glitchtip-install.sh root@<glitchtip-host>:/root/
|
||||
ssh root@<glitchtip-host>
|
||||
DOMAIN=<glitchtip-domain-oder-ip> ./glitchtip-install.sh
|
||||
```
|
||||
|
||||
Installiert Docker (falls fehlend) + GlitchTip per Docker Compose unter
|
||||
`/opt/glitchtip`, legt zufälliges `SECRET_KEY`/DB-Passwort an. Danach:
|
||||
|
||||
```bash
|
||||
cd /opt/glitchtip && docker compose exec web ./manage.py createsuperuser
|
||||
```
|
||||
|
||||
Im Browser einloggen, Organisation + Projekt anlegen, angezeigte DSN-URL
|
||||
kopieren und in MABEA unter **Admin > Fehler-Tracking** eintragen — greift
|
||||
sofort (Backend + Frontend), kein Redeploy nötig. Leer lassen/Feld leeren
|
||||
deaktiviert das Tracking wieder vollständig.
|
||||
|
||||
Deinstallation: `cd /opt/glitchtip && docker compose down -v` (`-v` löscht
|
||||
auch alle GlitchTip-Daten).
|
||||
|
||||
## Dateien
|
||||
- `install_server.sh` – Server-Grundausstattung (PostgreSQL, nginx), idempotent
|
||||
- `nginx_mabea.conf.template` – Reverse-Proxy-Vorlage, von `install_server.sh` verwendet
|
||||
@@ -75,3 +102,4 @@ verifizieren (`gunzip -c <backup>.sql.gz | psql <testdatenbank>`).
|
||||
- `mabea-readiness-snapshot.service.template` / `.timer.template` – täglicher Bereitschafts-Snapshot (READY-006)
|
||||
- `mabea-wartung-benachrichtigung.service.template` / `.timer.template` – täglicher Wartungsfälligkeits-Prüflauf (NOTIF-003)
|
||||
- `backup_datenbank.sh` / `mabea-backup.service.template` / `.timer.template` – tägliches DB-Backup mit Rotation (FOUND-010)
|
||||
- `glitchtip-install.sh` – optionale GlitchTip-Installation (FOUND-013, Error-Tracking), unabhängiger Host
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
# FOUND-013: GlitchTip (Sentry-API-kompatibles Error-Tracking) optional
|
||||
# installieren. GlitchTip wird offiziell nur als Docker-Image vertrieben,
|
||||
# widerspricht NICHT "kein Docker-Zwang" aus CLAUDE.md (das gilt für MABEA
|
||||
# selbst) — für diese Fremdsoftware ist Docker die einzige praktikable,
|
||||
# isolierte Betriebsart.
|
||||
#
|
||||
# Läuft AUF dem Zielserver, unabhängig vom MABEA-Server (kann derselbe oder
|
||||
# ein separater Host sein - siehe arbeitskacheln/01_foundation.md FOUND-013).
|
||||
# Danach in MABEA (Admin > Fehler-Tracking) die vom GlitchTip-Projekt
|
||||
# angezeigte DSN-URL eintragen.
|
||||
#
|
||||
# Nutzung:
|
||||
# DOMAIN=glitchtip.example.org ./glitchtip-install.sh
|
||||
# ./glitchtip-install.sh # fragt Domain/Port interaktiv ab
|
||||
#
|
||||
# Deinstallation:
|
||||
# cd /opt/glitchtip && docker compose down -v # -v löscht auch die Daten
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_DIR="/opt/glitchtip"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Muss als root laufen." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Docker nicht gefunden. Installiere Docker Engine + Compose-Plugin..."
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
fi
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
echo "Docker-Compose-Plugin fehlt (docker compose). Bitte manuell nachinstallieren." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DOMAIN="${DOMAIN:-}"
|
||||
if [ -z "$DOMAIN" ]; then
|
||||
read -rp "Domain oder IP, unter der GlitchTip erreichbar sein soll: " DOMAIN
|
||||
fi
|
||||
PORT="${PORT:-8000}"
|
||||
|
||||
if [ -d "$INSTALL_DIR" ] && [ -f "$INSTALL_DIR/docker-compose.yml" ]; then
|
||||
echo "GlitchTip ist bereits unter $INSTALL_DIR installiert."
|
||||
read -rp "Neu konfigurieren und neu starten? (j/N) " ANTWORT
|
||||
if [ "$ANTWORT" != "j" ] && [ "$ANTWORT" != "J" ]; then
|
||||
echo "Abgebrochen, bestehende Installation unverändert."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
cd "$INSTALL_DIR"
|
||||
|
||||
SECRET_KEY="$(openssl rand -hex 32)"
|
||||
DB_PASSWORD="$(openssl rand -hex 20)"
|
||||
|
||||
cat > docker-compose.yml <<EOF
|
||||
services:
|
||||
web:
|
||||
image: glitchtip/glitchtip:latest
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
ports:
|
||||
- "${PORT}:8000"
|
||||
environment:
|
||||
DATABASE_URL: postgresql://glitchtip:${DB_PASSWORD}@postgres:5432/glitchtip
|
||||
SECRET_KEY: "${SECRET_KEY}"
|
||||
PORT: 8000
|
||||
EMAIL_URL: "consolemail://"
|
||||
GLITCHTIP_DOMAIN: "https://${DOMAIN}"
|
||||
DEFAULT_FROM_EMAIL: "glitchtip@${DOMAIN}"
|
||||
CELERY_WORKER_AUTOSCALE: "1,3"
|
||||
CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- uploads:/code/uploads
|
||||
|
||||
worker:
|
||||
image: glitchtip/glitchtip:latest
|
||||
command: ./bin/run-celery-with-beat.sh
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
environment:
|
||||
DATABASE_URL: postgresql://glitchtip:${DB_PASSWORD}@postgres:5432/glitchtip
|
||||
SECRET_KEY: "${SECRET_KEY}"
|
||||
EMAIL_URL: "consolemail://"
|
||||
GLITCHTIP_DOMAIN: "https://${DOMAIN}"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- uploads:/code/uploads
|
||||
|
||||
postgres:
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_DB: glitchtip
|
||||
POSTGRES_USER: glitchtip
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
uploads:
|
||||
EOF
|
||||
|
||||
chmod 600 docker-compose.yml
|
||||
|
||||
echo "==> Container starten"
|
||||
docker compose up -d
|
||||
|
||||
echo "==> Warte auf Postgres/Web-Start"
|
||||
sleep 10
|
||||
|
||||
echo "==> Datenbank-Migrationen"
|
||||
docker compose exec -T web ./manage.py migrate
|
||||
|
||||
echo ""
|
||||
echo "OK: GlitchTip läuft auf Port ${PORT}."
|
||||
echo "Admin-Account anlegen mit:"
|
||||
echo " cd ${INSTALL_DIR} && docker compose exec web ./manage.py createsuperuser"
|
||||
echo ""
|
||||
echo "Danach im Browser http://<server>:${PORT} öffnen (oder per nginx/Reverse-Proxy"
|
||||
echo "auf https://${DOMAIN} legen), Organisation + Projekt anlegen, DSN kopieren"
|
||||
echo "und in MABEA unter Admin > Fehler-Tracking eintragen."
|
||||
echo ""
|
||||
echo "Zugangsdaten liegen ausschließlich in ${INSTALL_DIR}/docker-compose.yml (chmod 600) -"
|
||||
echo "keine separate Sicherung dieser Datei vergessen, falls der Server neu aufgesetzt wird."
|
||||
Generated
+107
@@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.3.3",
|
||||
"@sentry/react": "^10.74.0",
|
||||
"@zxing/browser": "^0.1.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -2628,6 +2629,112 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "10.74.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.74.0.tgz",
|
||||
"integrity": "sha512-U6Zn+YcAgnR7bXZmpjMxivptPSsOeDKV+SlSqbJZqViOaCcZSG/vJiQr50UrdTccCwiU1LnwuOOoclJOvW+cLQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/browser-utils": "10.74.0",
|
||||
"@sentry/conventions": "^0.16.0",
|
||||
"@sentry/core": "10.74.0",
|
||||
"@sentry/feedback": "10.74.0",
|
||||
"@sentry/replay": "10.74.0",
|
||||
"@sentry/replay-canvas": "10.74.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser-utils": {
|
||||
"version": "10.74.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser-utils/-/browser-utils-10.74.0.tgz",
|
||||
"integrity": "sha512-/Rh3WH4i2nStdOqHpaWocbpSMFPjKhlxKQgp9vkJBSolqjJzXlIpLYXOrldZHHZLG1iDoX3PFBQkNDYcPXCg0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/conventions": "^0.16.0",
|
||||
"@sentry/core": "10.74.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/conventions": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.16.0.tgz",
|
||||
"integrity": "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "10.74.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.74.0.tgz",
|
||||
"integrity": "sha512-u9rY8vcZfktccwm6LznfCZlqP5C9A+p76r4/pFS1grqpuTO0m21Cl8rosnlESrDGP/Xd9tfr91rWYk0jPH8jeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/conventions": "^0.16.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/feedback": {
|
||||
"version": "10.74.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/feedback/-/feedback-10.74.0.tgz",
|
||||
"integrity": "sha512-WwUao9K2XgRIxTUTaBhm7aglhsyP7LQTGi2lxvFbXzlSagUzwew/EL2c5oqQ23Mjn94i5SY+rM4wMdgyWdtpAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "10.74.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/react": {
|
||||
"version": "10.74.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.74.0.tgz",
|
||||
"integrity": "sha512-eKSNCbLyvc/qk4rIrm1IQZnC1zYPMJBqOVTX4VQCbK+d6YnzLc8JuZsw3PaGeB2x1g3CGe4qtBd9HUvsvBD9aw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/browser": "10.74.0",
|
||||
"@sentry/conventions": "^0.16.0",
|
||||
"@sentry/core": "10.74.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0 || 17.x || 18.x || 19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/replay": {
|
||||
"version": "10.74.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-10.74.0.tgz",
|
||||
"integrity": "sha512-amIu0zKzhQJZoWmlwfFnn/uKRGyF+Tikph3lBgYwj5XbHv4U94PQXi9Ll8CYzzLVS7BkrCVc2Hl7RvpRg5M2Yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/browser-utils": "10.74.0",
|
||||
"@sentry/core": "10.74.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/replay-canvas": {
|
||||
"version": "10.74.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/replay-canvas/-/replay-canvas-10.74.0.tgz",
|
||||
"integrity": "sha512-HAdu4ViRsuOD5ifphQR24HugL0geZXV8LnGQVkA8vlzZWiuYgGNqDRoUWI75IgVcCYLYSaVBoKN+jTimdzG/sQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "10.74.0",
|
||||
"@sentry/replay": "10.74.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.3.3",
|
||||
"@sentry/react": "^10.74.0",
|
||||
"@zxing/browser": "^0.1.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -3,8 +3,11 @@ import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import App from "./App";
|
||||
import { initSentryFallsAktiv } from "./monitoring/sentry";
|
||||
import "./styles/tailwind.css";
|
||||
|
||||
void initSentryFallsAktiv();
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// FOUND-013: GlitchTip/Sentry ist optional und wird über die Admin-GUI
|
||||
// aktiviert (Backend-Endpunkt /einstellungen/sentry, öffentlich lesbar - der
|
||||
// DSN ist kein Geheimnis). Kein Build-Time-Env nötig, kein Redeploy beim
|
||||
// Ein-/Ausschalten - reiner Laufzeit-Fetch beim App-Start. @sentry/react wird
|
||||
// nur dynamisch nachgeladen, wenn tatsächlich ein DSN gesetzt ist (kein
|
||||
// unnötiges Bundle-Gewicht im Normalfall ohne Error-Tracking).
|
||||
export async function initSentryFallsAktiv(): Promise<void> {
|
||||
try {
|
||||
const basis = import.meta.env.VITE_API_BASE_URL ?? "/api/v1";
|
||||
const response = await fetch(`${basis}/einstellungen/sentry`);
|
||||
if (!response.ok) return;
|
||||
const daten = (await response.json()) as { sentry_dsn: string | null; aktiv: boolean };
|
||||
if (!daten.aktiv || !daten.sentry_dsn) return;
|
||||
|
||||
const Sentry = await import("@sentry/react");
|
||||
Sentry.init({ dsn: daten.sentry_dsn, sendDefaultPii: false, tracesSampleRate: 0 });
|
||||
} catch {
|
||||
// Fehler-Tracking selbst darf niemals den App-Start verhindern.
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import { EinsatzSection } from "./admin/EinsatzSection";
|
||||
import { MaterialSetSection } from "./admin/MaterialSetSection";
|
||||
import { SollmengenGruppeSection } from "./admin/SollmengenGruppeSection";
|
||||
import { InventurSection } from "./admin/InventurSection";
|
||||
import { FehlerTrackingSection } from "./admin/FehlerTrackingSection";
|
||||
import { VerleihSection } from "./admin/VerleihSection";
|
||||
|
||||
type Tab =
|
||||
@@ -56,7 +57,8 @@ type Tab =
|
||||
| "materialSets"
|
||||
| "verleih"
|
||||
| "sollmengenGruppen"
|
||||
| "inventur";
|
||||
| "inventur"
|
||||
| "fehlerTracking";
|
||||
|
||||
// Deckt sich mit den require_roles()-Gates der jeweiligen Backend-Endpunkte
|
||||
// (Rollen-Vorgabe: Material-/Leitungsverantwortliche sahen bisher exakt
|
||||
@@ -109,6 +111,7 @@ const TABS: { key: Tab; label: string; rollen: ("administration" | "materialvera
|
||||
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
|
||||
},
|
||||
{ key: "inventur", label: "Inventur", rollen: ["administration", "materialverantwortlicher"] },
|
||||
{ key: "fehlerTracking", label: "Fehler-Tracking", rollen: ["administration"] },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -333,6 +336,7 @@ export function AdminPage() {
|
||||
<SollmengenGruppeSection materialien={materialien} onFehler={setFehler} />
|
||||
)}
|
||||
{aktiverTab === "inventur" && <InventurSection materialien={materialien} onFehler={setFehler} />}
|
||||
{aktiverTab === "fehlerTracking" && <FehlerTrackingSection onFehler={setFehler} />}
|
||||
{aktiverTab === "verleih" && <VerleihSection objekte={objekte} onFehler={setFehler} />}
|
||||
{aktiverTab === "eskalation" && <EskalationSection onFehler={setFehler} />}
|
||||
{aktiverTab === "personal" && (
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { apiRequest } from "../../api/client";
|
||||
|
||||
interface Props {
|
||||
onFehler: (text: string) => void;
|
||||
}
|
||||
|
||||
interface SentryEinstellung {
|
||||
sentry_dsn: string | null;
|
||||
aktiv: boolean;
|
||||
}
|
||||
|
||||
// FOUND-013: GlitchTip (Sentry-API-kompatibel) optional per DSN aktivieren -
|
||||
// keine .env/Redeploy nötig, greift sofort im Backend und beim nächsten
|
||||
// Frontend-Laden. GlitchTip selbst wird separat per
|
||||
// deploy/glitchtip-install.sh auf einem beliebigen Host installiert.
|
||||
export function FehlerTrackingSection({ onFehler }: Props) {
|
||||
const [aktuell, setAktuell] = useState<SentryEinstellung | null>(null);
|
||||
const [dsn, setDsn] = useState("");
|
||||
const [speichert, setSpeichert] = useState(false);
|
||||
|
||||
async function laden() {
|
||||
try {
|
||||
const daten = await apiRequest<SentryEinstellung>("/einstellungen/sentry");
|
||||
setAktuell(daten);
|
||||
setDsn(daten.sentry_dsn ?? "");
|
||||
} catch {
|
||||
onFehler("Einstellung konnte nicht geladen werden.");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
laden();
|
||||
}, []);
|
||||
|
||||
async function speichern() {
|
||||
setSpeichert(true);
|
||||
try {
|
||||
const daten = await apiRequest<SentryEinstellung>("/einstellungen/sentry", {
|
||||
method: "PATCH",
|
||||
body: { sentry_dsn: dsn || null },
|
||||
});
|
||||
setAktuell(daten);
|
||||
} catch {
|
||||
onFehler("Einstellung konnte nicht gespeichert werden.");
|
||||
} finally {
|
||||
setSpeichert(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function deaktivieren() {
|
||||
setDsn("");
|
||||
setSpeichert(true);
|
||||
try {
|
||||
const daten = await apiRequest<SentryEinstellung>("/einstellungen/sentry", {
|
||||
method: "PATCH",
|
||||
body: { sentry_dsn: null },
|
||||
});
|
||||
setAktuell(daten);
|
||||
} catch {
|
||||
onFehler("Einstellung konnte nicht gespeichert werden.");
|
||||
} finally {
|
||||
setSpeichert(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="section">
|
||||
<h2>Fehler-Tracking (GlitchTip)</h2>
|
||||
<div className="card">
|
||||
<p className="text-muted" style={{ marginTop: 0 }}>
|
||||
Optional: unbehandelte Fehler in Backend und Frontend an eine selbst gehostete
|
||||
GlitchTip-Instanz melden (Sentry-API-kompatibel). Installation der GlitchTip-Instanz
|
||||
selbst per <code>deploy/glitchtip-install.sh</code> auf einem beliebigen Server — hier
|
||||
nur die DSN-URL eintragen, die GlitchTip beim Anlegen eines Projekts anzeigt. Leer lassen
|
||||
= deaktiviert, keine Daten verlassen den eigenen Server.
|
||||
</p>
|
||||
|
||||
{aktuell && (
|
||||
<div style={{ marginBottom: "0.75rem" }}>
|
||||
<span className={`badge ${aktuell.aktiv ? "badge-success" : "badge-neutral"}`}>
|
||||
{aktuell.aktiv ? "Aktiv" : "Deaktiviert"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row" style={{ flexWrap: "wrap" }}>
|
||||
<input
|
||||
className="input"
|
||||
style={{ minWidth: "24rem" }}
|
||||
placeholder="https://<key>@<glitchtip-host>/<projekt-id>"
|
||||
value={dsn}
|
||||
onChange={(e) => setDsn(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={speichern} disabled={speichert}>
|
||||
Speichern
|
||||
</button>
|
||||
{aktuell?.aktiv && (
|
||||
<button className="btn btn-danger" onClick={deaktivieren} disabled={speichert}>
|
||||
Deaktivieren
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user