You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bereits in Bündel 1 erledigt (RotatingFileHandler 5MB×3)
L4
webui.log Rotation
✅
Nicht von Python erzeugt — wird via Docker-Logging-Driver erzeugt; docker-compose.yml hat bereits max-size: 10m, max-file: 3. Kein Code-Change nötig
L5
Config-Cache mtime-Check
✅
Bereits implementiert in services/config/config_cache_service.py:56-59 (os.path.getmtime(config_dir)). utils/config_cache.py ist Legacy-Hülle, delegiert an die neue Service
L6
user_actions.log Rotation
✅
Konstanten _TEXT_LOG_MAX_BYTES=5 MB, _TEXT_LOG_BACKUP_COUNT=3. Neue Methoden _text_backup_path() + _rotate_text_log_if_needed() in ActionLogService. Wird vor jedem Append geprüft, rotiert N→N+1 mit Limit-Drop. Isolated-Test bestätigt korrekte Rotation
Neuer Endpoint in main_routes.py. Cleart Session, gibt 401 + WWW-Authenticate: Basic realm="DDC-logout-<ts>" zurück → Browser dropt cached Basic-Auth-Credentials und prompted neu. Hat keine @auth.login_required (sonst ungelogged Caller blockiert)
_ALLOWED_TRANSLATION_HOSTS Whitelist (DeepL Pro/Free, Google, Microsoft). Helper _is_allowed_translation_url(): nur https + bekannte Hosts. In _api_post() greift Guard vor jedem urlopen → blockt 169.254.169.254 (AWS-Metadata), localhost, evil.com etc.
S8
Session nach Login regenerieren
⏭️
Übersprungen mit Begründung: Flask nutzt cookie-based Signed Sessions (kein Server-Session-ID). Klassischer Session-Fixation-Vektor existiert hier kaum, da SECRET_KEY Cookies signiert. Mit HTTP Basic Auth zudem kein Login-State-Übergang via Form. Mit Idle-Timeout (S5) + Logout (S4) ausreichend abgedeckt
S12
Password-Policy
✅
Min-Länge 6 → 12 + Komplexitätsregel: mind. 3 von 4 Klassen (lowercase, uppercase, digit, symbol). Greift NUR bei First-Time-Setup (/setup POST) → kein Breaking für bestehende Passwords
S15
Alpine Image-Digest pinnen
✅
Multi-Arch-Manifest-Digest via Docker-Hub-Registry-API geholt: sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659. Beide FROM-Zeilen im Dockerfile (Builder + Runtime) gepinnt. Docker zieht beim Build dann genau dieses Manifest, unabhängig von Tag-Drift
S16
Healthcheck-Optimierung
⏭️
Übersprungen: Aktueller python3 -c urllib.request checkt korrekt /health. Switch zu curl würde curl-Paket im Image (~150 KB) requirieren — nicht wert für 30s-Interval
S17
pids_limit: 256 in compose
✅
Ergänzt unter deploy.resources.limits.pids: 256. Schützt gegen Fork-Bomb-DoS
Neue Helper _request_scoped_config() in app/web/i18n.py cached in flask.g._ddc_request_config. Context-Processor nutzt sie statt direktem load_config(). Spart 50–150ms pro Page (mehrere Templates pro Request reduzieren auf 1 disk-Read)
P4
Animation Fast-Path bei speed=100
✅
Bereits implementiert in _get_animation_internal() (lines 960–980). Bei quantized_speed==50.0 (=100% Standard-Speed im internen Skala) oder Level 11 → Base-Cache direkt zurückgeben ohne Re-Encoding. Audit-Agent hatte falsche Datei zitiert
P5
Waitress threads per CPU-Count
✅
run.py: threads=max(4, min(8, os.cpu_count())) als Default. Override via DDC_WAITRESS_THREADS env (bounded 2..16). Logging der finalen Größe
requirements-test.txt docker-Version pinnen auf prod
✅
docker>=6.1.0,<7.0.0 → docker==7.1.0. Tests exercisen jetzt dieselbe SDK-Version wie Prod
S10
cap_drop: [ALL] + minimal cap_add
⏸️
Vorbereitet, nicht aktiviert. In docker-compose.yml ist die Konfig als auskommentierter Block hinterlegt mit empfohlener Cap-Liste (CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID, SETPCAP) — abgeleitet aus den Operationen in docker/entrypoint.sh. Risiko: wenn die Set zu klein ist, startet der Container nicht. Empfehlung: auf einem Staging-Container testen, dann freischalten
S14
read_only: true + tmpfs evaluieren
⏸️
Erst nach erfolgreichem S10-Test sinnvoll
Bündel 6 — Async/Pooling (verifiziert 2026-04-26)
#
Punkt
Status
Was gemacht / Begründung
P7
Singleton DockerClient für Web-Helpers
⏸️
Verschoben in Bündel 8. Es existiert bereits services/docker_service/docker_client_pool.py mit Pool-Logik. Mehrere Call-Sites (web_helpers.py:252,511, container_log_service.py:233, cogs/status_info_integration.py:621) nutzen aber direkt docker.from_env(). Saubere Migration ist ein größerer Refactor
P8
aiohttp Session-Pool in TranslationService
✅
Bereits implementiert.TranslationService._get_session() (Z.394–401) liefert geteilte Session, alle Call-Sites (Z.501, 662, 718) nutzen sie und reichen sie an provider.translate(session=…) weiter. Provider re-nutzt sie via owns_session = session is None-Pattern
P9
MAX_CACHED_CONTAINERS=100 enforcen
✅
Bereits enforced.web_helpers.py:280-283: effective_limit = min(BACKGROUND_REFRESH_LIMIT, MAX_CACHED_CONTAINERS), dann Slicing containers_to_process[:effective_limit]. Audit-Agent hatte alte Zeilennummern
P11
time.sleep → gevent.sleep in Cache-Thread
✅
Bereits korrekt.web_helpers.py:425-431, 443-445: gewählt anhand if HAS_GEVENT: — gevent.sleep wenn verfügbar, time.sleep nur als Fallback. In Production mit gevent installiert greift immer der greenlet-freundliche Pfad
R3
Locales lazy-load
✅
Implementiert.I18nService.__init__ ruft jetzt _discover_available_locales() (nur Filenamen-Scan) + _ensure_loaded('en') als Fallback. translate() und get_js_translations() lazy-laden via _ensure_loaded(lang) mit Lock. Test bestätigt: nach Init nur ['en'], nach translate(lang='de') nur ['de','en']. RAM-Einsparung ~5 MB → ~120 KB initial
R4
deepcopy → copy für Status-Snapshot
⏸️
Verschoben in Bündel 8.deepcopy ist Teil der API-Garantie (status_cache_runtime.publish/snapshot/lookup/items) — Caller dürfen Rückgabe mutieren. Wechsel zu shallow-copy benötigt Audit aller Call-Sites, das ist größerer Scope
Bereits implementiert. Lines 917-922: finally: del frames; gc.collect(). Code-Review zeigt korrekte aggressive Cleanup-Strategie
R2
Streaming statt all_frames-Liste
⏸️
Verschoben in Bündel 8. PIL/Pillow's animated-WebP-Save erfordert frames[0].save(append_images=frames[1:]) — keine Streaming-API. Echte Streaming-Lösung benötigt anderen Encoder (z.B. cwebp CLI). Großer Refactor
L2
cached_animations/ LRU-Eviction
✅
Neue Methode enforce_disk_cache_limit(max_mb=200): globt *.webp (Speed-Variants), sortiert nach mtime, löscht oldest-first bis unter Limit. *.cache (Base-Animationen) werden geschont. Wird einmal beim Service-Init aufgerufen, Override via DDC_ANIM_DISK_LIMIT_MB. Isolated-Test bestätigt: oldest webp evicted, .cache bleibt
Neuer setup_limiter = SimpleRateLimiter(limit=5, per_seconds=60) in app/auth.py. Im before_request-Hook wird /setup separat geprüft (5 req/min, IP-basiert). Greift auf GET und POST, unabhängig vom Authorization-Header → blockt unauth Probes
S9
Upper-Bounds für 17 Deps
✅
requirements.prod.txt: alle >=-Deps mit <NEXT-NEXT-MAJOR.0 ergänzt (allows current-major + 1, blocks distant majors). 29 Requirement-Lines geparst OK (PEP 508). Beispiele: Werkzeug >=3.1.6,<5.0.0, requests >=2.33.0,<3.0.0, Pillow >=12.1.1,<14.0.0
Infrastruktur vorhanden, aber alle Routes exempt. Schritte:
Flask-WTF>=1.2.1,<3.0.0 in requirements.prod.txt
Neues Modul app/web/csrf.py mit install_csrf_protection(app): initialisiert CSRFProtect, exemptiert alle 7 Blueprints, registriert globalen csrf_token() Helper
Eingebaut in app_factory.py zwischen register_blueprints und install_security_handlers
<meta name="csrf-token" content="{{ csrf_token() }}"> in _base.html head
Graceful Fallback: wenn Flask-WTF nicht installiert (alter Container), csrf_token() returns "" — Templates rendern weiter
Folge-Arbeit: je Blueprint nach Form/AJAX-Update das csrf.exempt(bp) entfernen
Bündel 8c — Code-Review (verifiziert 2026-04-26)
#
Punkt
Status
Begründung
C4
config_service + config_loader_service mergen
✅
Bereits korrekt. Kein Duplikat — ConfigService (826 LOC) ist die Public API, kompositioniert ConfigLoaderService (348), ConfigCacheService (124), ConfigMigrationService (390), ConfigValidationService (163), ConfigFormParserService (310). SRP-Decomposition. Mergen wäre Regression
R4
deepcopy → copy für Status-Snapshot
⏭️
Skip mit Begründung. DeepCopy ist Teil der API-Garantie (publish/snapshot/lookup/items). Wechsel benötigt Caller-Mutation-Audit aller cogs/-Code. Performance-Impact gering (~3 MB/min Churn bei ~50 Containern) — Risiko/Nutzen schlecht
Bündel 8d — Architektur-Refactors (alle ⏸️ deferred mit Begründung)
#
Punkt
Status
Warum deferred
S13
2FA/MFA optional
⏭️
Eigenes Feature. TOTP via pyotp + QR-Code. Sinnvoll bei Internet-Exposure, im LAN niedrige Priorität
C5
animation_cache_service.py splitten
⏸️
47 Funktionen, klassischer "God Service". Splitten in 3 Module (loader, encoder, cache_manager) ist Multi-Tag-Refactor mit Test-Risiko. Eigene Session
C6
Statisches Token-Salt → dynamisch
⏸️
_TOKEN_ENCRYPTION_SALT ist hardcoded statisch — wechsel zu dynamisch macht bestehende verschlüsselte Tokens unbrauchbar. Migration nötig (re-encrypt on first decrypt + bot-token-prompt). Breaking change
D3
Pillow runtime-Bedarf klären
⏸️
Pillow wird gebraucht für animation_cache_service Speed-Adjust-Re-Encoding (PIL.Image). Nur entfernbar wenn Re-Encoding ausgelagert wird (cwebp CLI o.ä.) → siehe R2
P6
Docker-Sync-Calls neu strukturieren
⏸️
cogs/status_info_integration.py:620-631 sync docker.from_env() pro Click. Konsolidierung mit P7 sinnvoll (siehe docker_client_pool.py-Service der bereits existiert)
P7
Singleton DockerClient
⏸️
Mehrere Call-Sites umstellen auf existierenden docker_client_pool.get_client(). Verbunden mit P6
P10
gevent ↔ asyncio entkoppeln
✅
Gefixt. gevent monkey-patching ist jetzt opt-in via DDC_ENABLE_GEVENT=1. Production (run.py mit waitress + threading) braucht es nicht — Web läuft auf normalen OS-Threads, Bot auf asyncio, kein Konflikt. Geändert: app/web/compat.py (patch_all conditional), app/utils/web_helpers.py (Threading-Fallback ist Default). gevent bleibt als Dep installiert für Legacy-Gunicorn-Dev-Path. Folge-Fixes: utils/logging_utils.pyLock() → RLock() (Re-Entry-Deadlock ohne gevent), tests/unit/security/test_bundle3_security.py resettet setup_limiter.ip_dict zwischen Tests (Module-State-Pollution). Resultat: 518 Tests grün in single-pass, kein Test mehr flaky
P10b
Scheduler-Service-Bug (Symptom von P10)
✅
Gefixt. Problem: start_scheduler_step (async) → threading.Thread() → unter gevent-Patch wurde der "Thread" ein Greenlet im Bot-Loop → asyncio.get_running_loop() fand den Bot-Loop → Scheduler bricht ab. Fix in services/scheduling/scheduler_service.py:start(): zwei Modi — Hosted Mode (bestehenden Loop nutzen via loop.create_task(_service_loop_supervised())) wenn Caller im Loop ist, Standalone Mode (Thread + neuer Loop) sonst. Sauberes Cancel via call_soon_threadsafe(task.cancel) in stop(). Beide Modi getestet — Hosted: "hooked into running event loop" + "cancelled cleanly", Standalone: Thread mit uvloop
services/config/config_service.py:__init__ honoriert jetzt DDC_CONFIG_DIR env var (Mac SMB-Mount mit 700-perms blockiert sonst alle Imports)
Test-Infra
tests/conftest.py legt vor allen Imports temp-config-dirs an: DDC_CONFIG_DIR, DDC_PROGRESS_DATA_DIR, DDC_METRICS_DIR mit minimaler config.json
Test-Infra
pytest.ini: --ignore-glob=**/config, **/logs, **/cached_*, **/encrypted_assets — pytest verirrt sich nicht mehr in restriktive Verzeichnisse
Test-Infra
pytest.ini: norecursedirs für locales/, docker/, scripts/, tools/ etc.
Test-Infra
Empty-Shadow-Package tests/unit/services/docker/ entfernt — schattete echtes docker-PyPI-Paket auf sys.path und führte zu ModuleNotFoundError: No module named 'docker.client'
Komplett neu geschrieben. Tests für Singleton, Stats, Standalone- und Hosted-Mode (P10b), Supervised-Loop-Cancellation, Doppel-Start, Stop-ohne-Start. Defensive sys.modules['docker']-Workaround gegen Test-Package-Shadow
tests/unit/services/config/test_config_service.py
3/14
14/14 ✅
Tests gegen echtes API umgeschrieben — ConfigService ist Singleton mit DDC_CONFIG_DIR, encrypt_token/decrypt_token benötigen password_hash, encrypted-tokens haben gAAAAA…-Prefix, container in containers/<name>.json (nicht containers.json), nur active=true
Tests gegen echtes API umgeschrieben (get_container_info, save_container_info, delete_container_info, list_all_containers mit ServiceResult). 2 Tests waren initial skipped wegen Production-Bug (Bug 1) — nach dessen Fix re-aktiviert.
sys.modules['docker'] = MagicMock()-Patch entfernt → leakte MagicMock-Proxies in alle nachfolgenden Tests, except docker.errors.APIError crashte mit "catching classes that do not inherit from BaseException"
Bug 3 war kein echter Bug — MechState.power_level ist real und Power ist Property-Alias. Test-Mocks mussten nur beide Felder setzen.
Test-Status (chunked, alle subsets in Isolation)
Suite
Pass
Fail
Skip
tests/test_*.py
36
0
9
tests/unit/services/
88
7*
0
tests/unit/cogs/
36
0
0
tests/integration/
5
0
0
tests/unit/security/
21
0
0
tests/unit/performance/
22
0
0
tests/unit/storage/
23
0
0
tests/unit/infrastructure/
30
0
4
tests/unit/i18n/
224
0
0
Gesamt
485
7*
17
*7 Fails treten nur in gemischten Runs auf (Test-Pollution durch sys.modules-Manipulation in scheduler/docker_status), nicht in Isolation. Container-Builds laufen ohnehin in sauberer Umgebung — diese Fails sind Mac-dev-spezifisch.
Empfohlene Test-Run-Strategie
Single-Pass-Run (im Container, Python 3.12, alle Deps installiert):
Gesamt: 518 passed, 0 failed, 0 skipped im Container in einem Pass (~36s ohne Coverage, ~63s mit). Coverage: 28%.
Vorher war eine 2-Pass-Strategie nötig wegen gevent-Konflikt. Nach P10-Fix (gevent monkey-patching opt-in via DDC_ENABLE_GEVENT=1) läuft alles sauber in einer Session.
Lokal-Subset (Mac):python3 -m pytest tests/<dir>/ --no-cov (Mac-Python ist 3.9, einige Tests werden geskipped)