ibo/config.py
timu 650d7f3394 ibo: containerize + Gitea-CI + dev/prod-Deploy + Zitadel-SSO + noVNC-Live-View
- Dockerfile/compose: Flask-App + selenium/standalone-chrome (noVNC), Healthcheck
- app.py: /healthz + eingebettete noVNC-Live-Ansicht (NOVNC_URL, lokal:7900 / Cluster:/novnc/)
- Helm-Chart infra/k8s/base/ibo: App + Selenium-Sidecar, 2 Ports, Ingress
  (/ + /novnc via stripPrefix), SSO-Middlewares aus dem CD-Repo
- Gitea-CI: version -> test -> build-push -> helm-validate -> deploy-dev (Write-back)
- .gitignore/.dockerignore (Dev-Samples raus)
- infra/RUNBOOK-ibo.md: Zitadel/DNS/Deploy/Prod-Promote

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 01:03:32 +02:00

279 lines
10 KiB
Python
Raw Permalink 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.

#!/usr/bin/env python3
"""
config.py - laedt die Config und stellt die Werte bereit.
WICHTIG - was der Modus (test|prod) aendert:
NUR die FORMULARDATEN sind pro Modus verschieden:
contact -> Step 1 (Kontaktformular-Daten: Name, Telefon, Nachricht)
application -> Step 3 (Bewerbungsformular / Mieter-Selbstauskunft)
ALLES ANDERE ist GETEILT und in BEIDEN Modi ECHT (wird NICHT gemockt):
email -> Step 2 (mail.tm / IMAP) -> echte Adresse + echter Token
run -> Browser/Parallelitaet (headless, parallel_workers, ...)
Es wird also NICHTS an der Funktionalitaet gemockt. E-Mail und Token sind
IMMER echt. Der Modus bestimmt lediglich, welche Formular-Testdaten bzw.
echten Bewerbungsdaten eingetragen werden.
test -> Formulardaten = Dummy/Testdaten
prod -> Formulardaten = deine echten Bewerbungsdaten
Dateien:
config/email.json -> "email" (GETEILT)
config/run.json -> "run" (GETEILT)
config/<modus>/contact.json -> "contact" (pro Modus)
config/<modus>/application.json -> "application" (pro Modus)
Aktiver Modus: ENV IBO_MODE (test|prod) hat Vorrang, sonst config/active_mode.txt
(via UI umschaltbar), sonst 'prod'.
"""
import os
import json
HERE = os.path.dirname(os.path.abspath(__file__))
CONFIG_DIR = os.path.join(HERE, "config")
MODES = ("test", "prod")
DEFAULT_MODE = "prod"
MODE_STATE_PATH = os.path.join(CONFIG_DIR, "active_mode.txt")
# Geteilte Sektionen (fuer ALLE Modi gleich, echt): Datei im CONFIG_DIR.
SHARED_FILE = {
"email": "email.json", # Step 2 (Mail) - echt
"run": "run.json", # Browser/Parallelitaet
}
# Pro-Modus-Sektionen (nur Formulardaten): Datei im Modus-Ordner.
MODE_FILE = {
"contact": "contact.json", # Step 1
"application": "application.json", # Step 3
}
# Alte Ablagen: fuer Migration/Seed. (a) frueheres config/<mode>/stepN.json,
# (b) noch aeltere flache config_stepN.json, (c) alte gemeinsame config.json.
LEGACY_MODE_STEPFILE = { # frueheres Schema (stepN pro Modus)
"contact": "step1.json", "email": "step2.json",
"application": "step3.json", "run": "run.json",
}
LEGACY_FLAT = {
"contact": os.path.join(HERE, "config_step1.json"),
"email": os.path.join(HERE, "config_step2.json"),
"application": os.path.join(HERE, "config_step3.json"),
"run": os.path.join(HERE, "config_run.json"),
}
LEGACY_COMBINED = os.path.join(HERE, "config.json")
_HINWEIS = {
"contact": "Step 1 – Kontaktformular (PRO MODUS). 'email' wird pro URL "
"automatisch (mail.tm) erzeugt - leer lassen.",
"email": "Step 2 – Mail (GETEILT, in test UND prod identisch/echt). "
"mode: 'mailtm' (Wegwerf) oder 'imap'. sender_filter = erwarteter Absender.",
"application": "Step 3 – Bewerbungsformular (PRO MODUS). reserveNumber, validUntil, "
"numberRooms, secondTenant, consent, tenant1/tenant2 (Selbstauskunft).",
"run": "App-Einstellungen (GETEILT). headless, parallel_workers, auto_submit, "
"keep_browser_open.",
}
# ---------------------------------------------------------------------------
# Eingebaute Defaults
# ---------------------------------------------------------------------------
# GETEILT (echt) - identisch fuer test und prod.
SHARED_DEFAULTS = {
"email": {
"mode": "mailtm", "sender_filter": "muenchner-wohnen",
"imap_host": "imap.gmail.com", "imap_user": "", "imap_password": "",
},
"run": {"headless": False, "parallel_workers": 3,
"auto_submit": False, "keep_browser_open": True},
}
# PRO MODUS - nur Formulardaten (contact + application).
FORM_DEFAULTS = {
"prod": {
"contact": {
"salutation": "mr", "firstName": "Max", "lastName": "Mustermann",
"phone": "", "email": "",
"message": "Hallo, ich interessiere mich fuer das Objekt und wuerde mich "
"ueber einen Besichtigungstermin freuen. Bitte kontaktieren Sie "
"mich unter den angegebenen Daten. Vielen Dank!",
},
"application": {
"consent": True, "numberRooms": "3", "reserveNumber": "RES-12345",
"secondTenant": True, "validUntil": "31.12.2026",
"tenant1": {"firstName": "Max", "lastName": "Mustermann",
"dateOfBirth": "01.01.1990", "street": "Hauptstrasse",
"housenumber": "12", "zipcode": "80331", "city": "Muenchen",
"phonePrivate": "", "phoneBusiness": "0891234567"},
"tenant2": {"firstName": "Erika", "lastName": "Mustermann",
"dateOfBirth": "02.02.1992", "street": "Hauptstrasse",
"housenumber": "12", "zipcode": "80331", "city": "Muenchen",
"phonePrivate": "", "phoneBusiness": ""},
},
},
"test": {
"contact": {
"salutation": "mr", "firstName": "Test", "lastName": "Tester",
"phone": "0000000000", "email": "",
"message": "TEST – automatische Testnachricht.",
},
"application": {
"consent": True, "numberRooms": "2", "reserveNumber": "TEST-000",
"secondTenant": True, "validUntil": "31.12.2099",
"tenant1": {"firstName": "Test", "lastName": "Eins",
"dateOfBirth": "01.01.2000", "street": "Teststrasse",
"housenumber": "1", "zipcode": "10000", "city": "Teststadt",
"phonePrivate": "0000000001", "phoneBusiness": "0000000002"},
"tenant2": {"firstName": "Test", "lastName": "Zwei",
"dateOfBirth": "02.02.2000", "street": "Teststrasse",
"housenumber": "2", "zipcode": "10000", "city": "Teststadt",
"phonePrivate": "0000000003", "phoneBusiness": "0000000004"},
},
},
}
# ---------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------
def _read_json(path):
try:
with open(path, encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
def _mode_dir(mode):
return os.path.join(CONFIG_DIR, mode)
def _valid_mode(mode):
mode = (mode or "").strip().lower()
if mode == "mock": # alter Name -> neuer Name
mode = "test"
return mode if mode in MODES else DEFAULT_MODE
def current_mode():
"""Aktiver Modus: ENV IBO_MODE hat Vorrang, sonst persistierte Datei, sonst Default."""
env = os.environ.get("IBO_MODE")
if env:
return _valid_mode(env)
if os.path.exists(MODE_STATE_PATH):
try:
with open(MODE_STATE_PATH, encoding="utf-8") as f:
return _valid_mode(f.read())
except Exception:
pass
return DEFAULT_MODE
def _write(path, section, value):
os.makedirs(os.path.dirname(path), exist_ok=True)
out = {"_hinweis": _HINWEIS.get(section, ""), section: value}
with open(path, "w", encoding="utf-8") as f:
json.dump(out, f, indent=2, ensure_ascii=False)
def _seed_from_chain(section, sources, default):
"""Erste Quelle nehmen, die die Sektion enthaelt; sonst default."""
for src in sources:
data = _read_json(src)
if section in data:
return data[section]
return default
def _seed_shared():
"""Geteilte Dateien (email, run) sicherstellen. Seed-Reihenfolge:
fruehere Modus-Datei (prod) -> flache Legacy -> alte config.json -> Default."""
os.makedirs(CONFIG_DIR, exist_ok=True)
for section, fname in SHARED_FILE.items():
path = os.path.join(CONFIG_DIR, fname)
if os.path.exists(path):
continue
sources = [
os.path.join(_mode_dir("prod"), LEGACY_MODE_STEPFILE[section]),
LEGACY_FLAT[section],
LEGACY_COMBINED,
]
_write(path, section, _seed_from_chain(section, sources, SHARED_DEFAULTS[section]))
def _seed_mode(mode):
"""Pro-Modus-Dateien (contact, application) sicherstellen."""
os.makedirs(_mode_dir(mode), exist_ok=True)
for section, fname in MODE_FILE.items():
path = os.path.join(_mode_dir(mode), fname)
if os.path.exists(path):
continue
sources = [os.path.join(_mode_dir(mode), LEGACY_MODE_STEPFILE[section])]
if mode == "prod": # prod darf auch aus den flachen Legacy-Dateien seeden
sources += [LEGACY_FLAT[section], LEGACY_COMBINED]
default = FORM_DEFAULTS[mode][section]
_write(path, section, _seed_from_chain(section, sources, default))
# ---------------------------------------------------------------------------
# Laden / Speichern
# ---------------------------------------------------------------------------
MODE = current_mode()
def load():
"""Geteilte (email, run) + Pro-Modus (contact, application) zu einer CFG."""
_seed_shared()
_seed_mode(MODE)
cfg = {}
for section, fname in SHARED_FILE.items():
data = _read_json(os.path.join(CONFIG_DIR, fname))
cfg[section] = data.get(section, SHARED_DEFAULTS[section])
for section, fname in MODE_FILE.items():
data = _read_json(os.path.join(_mode_dir(MODE), fname))
cfg[section] = data.get(section, FORM_DEFAULTS[MODE][section])
return cfg
def save(cfg):
"""email/run -> geteilte Dateien; contact/application -> Modus-Dateien."""
for section, fname in SHARED_FILE.items():
if section in cfg:
_write(os.path.join(CONFIG_DIR, fname), section, cfg[section])
for section, fname in MODE_FILE.items():
if section in cfg:
_write(os.path.join(_mode_dir(MODE), fname), section, cfg[section])
CFG = load()
def reload():
global MODE, CFG
MODE = current_mode()
CFG = load()
return CFG
def set_mode(mode):
"""Aktiven Modus umschalten (persistiert) und Config neu laden.
ENV IBO_MODE hat weiterhin Vorrang."""
global MODE, CFG
mode = _valid_mode(mode)
os.makedirs(CONFIG_DIR, exist_ok=True)
with open(MODE_STATE_PATH, "w", encoding="utf-8") as f:
f.write(mode)
MODE = current_mode()
CFG = load()
return MODE
def mode_locked_by_env():
"""True, wenn der Modus per ENV festgelegt ist (UI-Umschalten dann gesperrt)."""
return bool(os.environ.get("IBO_MODE"))