- 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>
279 lines
10 KiB
Python
279 lines
10 KiB
Python
#!/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"))
|