diff --git a/app.py b/app.py
new file mode 100644
index 0000000..3e6dbf3
--- /dev/null
+++ b/app.py
@@ -0,0 +1,644 @@
+"""
+ITSM-Plattform -- eigenstaendiges Produkt (nicht Teil von AES).
+
+Architektur-Entscheidung (2026-07-13, Nutzer-Vorgabe): "Es soll ein ITSM werden,
+mit AES als 'buchbares' Modul." D.h. dieses Repo/Deployment ist NICHT eine
+Erweiterung des AES-Dashboards, sondern ein eigenes System mit Service-Katalog.
+AES ist darin nur der erste eingetragene, buchbare Service -- integriert ueber
+die bestehende AES-Dashboard-HTTP-API (Dispatch, Projekt-Status), nicht per
+Code-Import. Weitere Services (Netzwerk, Cloud, Security, ...) koennen spaeter
+denselben Mechanismus nutzen.
+
+Persistenz: einfache JSON-Dateien unter DATA_DIR (kein Datenbankserver noetig,
+gleiches Prinzip wie im AES-Dashboard). Ein Prozess, ein Worker -- Threading-Lock
+schuetzt gegen Nebenlaeufigkeit bei mehreren Gunicorn-Threads.
+
+ENV:
+ ITSM_DATA_DIR Verzeichnis fuer tickets.json/services.json/users.json
+ ITSM_USER / ITSM_PASSWORD / ITSM_SECRET_KEY Login + Session-Secret
+ AES_DASHBOARD_URL Basis-URL des AES-Dashboards fuer die Service-Integration
+"""
+import os
+import json
+import html
+import time
+import uuid
+import secrets
+import hashlib
+import threading
+import urllib.request
+import urllib.error
+import datetime as _dt
+
+from flask import Flask, request, redirect, url_for, session, jsonify, abort
+
+DATA_DIR = os.getenv("ITSM_DATA_DIR", "/data")
+ITSM_USER = os.getenv("ITSM_USER", "admin")
+ITSM_PASSWORD = os.getenv("ITSM_PASSWORD", "")
+AES_DASHBOARD_URL = os.getenv("AES_DASHBOARD_URL", "").rstrip("/")
+
+app = Flask(__name__)
+app.secret_key = os.getenv("ITSM_SECRET_KEY") or secrets.token_hex(32)
+
+_LOCK = threading.Lock()
+
+CATEGORIES = ["Incident", "Problem", "Task", "Change", "Release"]
+PRIORITIES = ["Niedrig", "Mittel", "Hoch", "Kritisch"]
+STATUSES = ["Offen", "In Bearbeitung", "Warten", "Geloest", "Geschlossen"]
+
+
+# ── Persistenz ────────────────────────────────────────────────────────────────
+def _path(name):
+ return os.path.join(DATA_DIR, name)
+
+
+def _load(name, default):
+ p = _path(name)
+ if not os.path.isfile(p):
+ return default
+ try:
+ with open(p, "r", encoding="utf-8") as f:
+ return json.load(f)
+ except Exception:
+ return default
+
+
+def _save(name, data):
+ os.makedirs(DATA_DIR, exist_ok=True)
+ p = _path(name)
+ tmp = p + ".tmp"
+ with open(tmp, "w", encoding="utf-8") as f:
+ json.dump(data, f, ensure_ascii=False, indent=2)
+ os.replace(tmp, p)
+
+
+def _now():
+ return _dt.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
+
+
+def _next_ticket_id(tickets):
+ year = _dt.datetime.utcnow().year
+ seq = 1
+ prefix = "TKT-%d-" % year
+ existing = [t["id"] for t in tickets if t["id"].startswith(prefix)]
+ if existing:
+ nums = [int(i.split("-")[-1]) for i in existing]
+ seq = max(nums) + 1
+ return "%s%06d" % (prefix, seq)
+
+
+def _seed_if_empty():
+ with _LOCK:
+ tickets = _load("tickets.json", [])
+ if tickets:
+ return
+ services = _load("services.json", [])
+ service_names = [s["name"] for s in services] or ["Allgemein"]
+ seed = [
+ {
+ "id": "TKT-%d-000001" % _dt.datetime.utcnow().year,
+ "titel": "Willkommen im ITSM",
+ "beschreibung": (
+ "Dies ist ein Beispiel-Ticket. Ueber 'Neues Ticket' oben rechts "
+ "kannst du echte Vorgaenge anlegen. Tickets koennen optional "
+ "einem Service aus dem Katalog zugeordnet werden (z. B. AES)."
+ ),
+ "service": service_names[0],
+ "status": "Offen",
+ "prioritaet": "Niedrig",
+ "kategorie": "Task",
+ "zugewiesen_an": ITSM_USER,
+ "erstellt": _now(),
+ "aktualisiert": _now(),
+ "fortschritt": 0,
+ "betroffene_services": [],
+ "ci_elemente": [],
+ "sla_antwort_minuten": 480,
+ "sla_loesung_minuten": 2880,
+ "timeline": [{"zeit": _now(), "akteur": "System", "text": "Ticket angelegt."}],
+ }
+ ]
+ _save("tickets.json", seed)
+
+
+def _seed_services_if_empty():
+ with _LOCK:
+ services = _load("services.json", [])
+ if services:
+ return
+ services = [
+ {
+ "id": "aes",
+ "name": "AES -- Autonomous Engineering System",
+ "beschreibung": (
+ "Automatisierte Software-Entwicklung: Projekte, Phasen, "
+ "LLM-gestuetzte Agenten, Auto-Fix und Release-Pipeline."
+ ),
+ "kategorie": "Entwicklung",
+ "gebucht": True,
+ "endpoint": AES_DASHBOARD_URL,
+ "hinzugefuegt": _now(),
+ }
+ ]
+ _save("services.json", services)
+
+
+# ── Auth ──────────────────────────────────────────────────────────────────────
+def _check_login(user, pw):
+ return user == ITSM_USER and ITSM_PASSWORD and pw == ITSM_PASSWORD
+
+
+def _require_login():
+ if not session.get("user"):
+ return redirect(url_for("login", next=request.path))
+ return None
+
+
+@app.route("/login", methods=["GET", "POST"])
+def login():
+ error = None
+ if request.method == "POST":
+ u = request.form.get("username", "")
+ p = request.form.get("password", "")
+ if _check_login(u, p):
+ session["user"] = u
+ return redirect(request.args.get("next") or url_for("tickets_route"))
+ error = "Benutzername oder Passwort falsch."
+ err_html = "
%s
" % html.escape(error) if error else ""
+ return """
+
+Anmelden -- ITSM %s
+""" % (_CSS_TAG, err_html)
+
+
+@app.route("/logout")
+def logout():
+ session.clear()
+ return redirect(url_for("login"))
+
+
+# ── CSS (eigenstaendig, an AES-Farbschema angelehnt aber neu) ─────────────────
+_CSS = """
+:root{--bg:#161d2b;--panel:#1e2738;--panel2:#232e42;--border:#324259;--text:#e6edf5;
+--sub:#93a3b8;--accent:#4c9eba;--accent2:#5db3d0;--ok:#3ecf8e;--warn:#e0a83e;
+--bad:#e5534b;--crit:#c9364a;}
+*{box-sizing:border-box}
+body{margin:0;background:var(--bg);color:var(--text);font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:14px}
+a{color:var(--accent2);text-decoration:none}
+a:hover{text-decoration:underline}
+.login-wrap{min-height:100vh;display:flex;align-items:center;justify-content:center}
+.login-box{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:28px;width:300px}
+.login-box h1{margin:0 0 4px;font-size:22px}
+.login-box input{width:100%;padding:9px 10px;margin-bottom:10px;background:var(--bg);
+ border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px}
+.btn{background:var(--accent);border:none;color:#0c1420;font-weight:600;padding:9px 14px;
+ border-radius:6px;cursor:pointer;font-size:13px}
+.btn:hover{background:var(--accent2)}
+.btn.ghost{background:transparent;border:1px solid var(--border);color:var(--text)}
+.sz{color:var(--sub);font-size:12px}
+.shell{display:flex;min-height:100vh}
+.sidebar{width:220px;flex:0 0 220px;background:var(--panel);border-right:1px solid var(--border);padding:16px 0;overflow-y:auto}
+.brand{padding:0 16px 16px;font-weight:700;font-size:16px}
+.brand .sz{font-weight:400}
+.navgroup{margin-top:14px}
+.navgroup h4{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--sub);
+ padding:0 16px;margin:0 0 4px}
+.navgroup a{display:block;padding:6px 16px;color:var(--text);font-size:13px}
+.navgroup a:hover{background:var(--panel2);text-decoration:none}
+.navgroup a.active{background:var(--panel2);border-left:2px solid var(--accent);color:var(--accent2)}
+.main{flex:1;min-width:0}
+.topbar{display:flex;align-items:center;justify-content:space-between;padding:12px 24px;
+ border-bottom:1px solid var(--border);background:var(--panel)}
+.topbar .search{flex:1;max-width:420px;background:var(--bg);border:1px solid var(--border);
+ border-radius:6px;padding:7px 12px;color:var(--sub);font-size:13px}
+.content{padding:24px}
+h1.page-title{font-size:22px;margin:0 0 18px}
+.kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:18px}
+.kpi{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:14px 16px}
+.kpi .lbl{color:var(--sub);font-size:12px;margin-bottom:6px}
+.kpi .val{font-size:26px;font-weight:700}
+.kpi .trend{font-size:11px;margin-top:4px}
+.kpi .trend.up{color:var(--ok)} .kpi .trend.down{color:var(--bad)}
+.tabs{display:flex;gap:4px;margin-bottom:14px;flex-wrap:wrap}
+.tabs a{padding:6px 12px;border-radius:6px;font-size:13px;color:var(--sub);border:1px solid transparent}
+.tabs a.active{background:var(--panel2);color:var(--text);border-color:var(--border)}
+table.tickets{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--border);border-radius:8px;overflow:hidden}
+table.tickets th{text-align:left;font-size:11px;text-transform:uppercase;color:var(--sub);
+ padding:10px 12px;border-bottom:1px solid var(--border);background:var(--panel2)}
+table.tickets td{padding:10px 12px;border-bottom:1px solid var(--border);vertical-align:top}
+table.tickets tr:last-child td{border-bottom:none}
+table.tickets tr.row{cursor:pointer}
+table.tickets tr.row:hover{background:var(--panel2)}
+.pill{display:inline-block;padding:2px 9px;border-radius:99px;font-size:11px;font-weight:600}
+.pill.offen{background:#2e3a52;color:#9fb3ce}
+.pill.bearbeitung{background:#4a3a1a;color:var(--warn)}
+.pill.warten{background:#3a2f52;color:#b79ee0}
+.pill.geloest{background:#1a4a35;color:var(--ok)}
+.pill.geschlossen{background:#2a2a2a;color:#888}
+.pill.ueberfaellig{background:#4a1e22;color:var(--bad)}
+.pill.krit{color:var(--crit)} .pill.hoch{color:var(--bad)} .pill.mittel{color:var(--warn)} .pill.niedrig{color:var(--sub)}
+.bar-track{background:#2a3446;border-radius:99px;height:6px;width:90px;display:inline-block;vertical-align:middle}
+.bar-fill{background:var(--accent);height:6px;border-radius:99px}
+.panel{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:18px;margin-bottom:16px}
+.card{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:12px}
+.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
+.svc-card{display:flex;justify-content:space-between;align-items:flex-start}
+.svc-card .badge{padding:2px 9px;border-radius:99px;font-size:11px;background:#1a4a35;color:var(--ok)}
+input[type=text],textarea,select{background:var(--bg);border:1px solid var(--border);border-radius:6px;
+ color:var(--text);padding:8px 10px;font-size:13px;width:100%}
+textarea{min-height:70px;font-family:inherit}
+.formrow{margin-bottom:10px}
+#detail-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);display:none;z-index:40}
+#detail-panel{position:fixed;right:0;top:0;bottom:0;width:420px;background:var(--panel);
+ border-left:1px solid var(--border);z-index:41;transform:translateX(100%);
+ transition:transform .18s ease;overflow-y:auto;padding:20px}
+#detail-panel.open{transform:translateX(0)}
+#detail-overlay.open{display:block}
+.dt-tabs{display:flex;gap:14px;border-bottom:1px solid var(--border);margin:14px 0}
+.dt-tabs span{padding-bottom:8px;color:var(--sub);font-size:13px;cursor:pointer}
+.dt-tabs span.active{color:var(--accent2);border-bottom:2px solid var(--accent2)}
+.tl-item{border-left:2px solid var(--border);padding-left:12px;margin-bottom:12px;position:relative}
+.tl-item::before{content:'';position:absolute;left:-5px;top:2px;width:8px;height:8px;border-radius:50%;background:var(--accent)}
+"""
+_CSS_TAG = ""
+
+_NAV = [
+ ("Service Management", [
+ ("/tickets", "Tickets"),
+ ("/probleme", "Probleme"),
+ ("/aenderungen", "Aenderungen"),
+ ("/releases", "Releases"),
+ ]),
+ ("Services", [
+ ("/services", "Service-Katalog"),
+ ]),
+ ("Wissen", [
+ ("/wissen", "Wissensdatenbank"),
+ ]),
+ ("Assets", [
+ ("/assets", "Asset-Liste"),
+ ]),
+ ("Administration", [
+ ("/admin", "Einstellungen"),
+ ]),
+]
+
+
+def _shell(active_path, title, body_html, extra_head=""):
+ groups_html = []
+ for group_name, items in _NAV:
+ links = "".join(
+ '%s ' % (href, "active" if href == active_path else "", html.escape(label))
+ for href, label in items
+ )
+ groups_html.append('
%s %s' % (html.escape(group_name), links))
+ nav_html = "".join(groups_html)
+ return """
+
+%s -- ITSM %s%s
+
+
+
+""" % (html.escape(title), _CSS_TAG, extra_head, nav_html, html.escape(session.get("user", "")), body_html)
+
+
+# ── Tickets: Berechnung + Rendering ───────────────────────────────────────────
+def _is_overdue(t):
+ if t["status"] in ("Geloest", "Geschlossen"):
+ return False
+ try:
+ created = _dt.datetime.fromisoformat(t["erstellt"].replace("Z", "+00:00"))
+ except Exception:
+ return False
+ age_min = (_dt.datetime.now(_dt.timezone.utc) - created).total_seconds() / 60
+ return age_min > t.get("sla_loesung_minuten", 999999)
+
+
+def _kpis(tickets):
+ total = len(tickets)
+ offen = sum(1 for t in tickets if t["status"] == "Offen")
+ bearbeitung = sum(1 for t in tickets if t["status"] == "In Bearbeitung")
+ warten = sum(1 for t in tickets if t["status"] == "Warten")
+ ueberfaellig = sum(1 for t in tickets if _is_overdue(t))
+ now = _dt.datetime.now(_dt.timezone.utc)
+ geloest_7d = 0
+ for t in tickets:
+ if t["status"] in ("Geloest", "Geschlossen"):
+ try:
+ upd = _dt.datetime.fromisoformat(t["aktualisiert"].replace("Z", "+00:00"))
+ if (now - upd).days <= 7:
+ geloest_7d += 1
+ except Exception:
+ pass
+ return {
+ "Gesamt": total, "Offen": offen, "In Bearbeitung": bearbeitung,
+ "Warten auf Input": warten, "Ueberfaellig": ueberfaellig,
+ "Geloest (7 Tage)": geloest_7d,
+ }
+
+
+def _status_class(s):
+ return {"Offen": "offen", "In Bearbeitung": "bearbeitung", "Warten": "warten",
+ "Geloest": "geloest", "Geschlossen": "geschlossen"}.get(s, "offen")
+
+
+def _prio_class(p):
+ return {"Kritisch": "krit", "Hoch": "hoch", "Mittel": "mittel", "Niedrig": "niedrig"}.get(p, "mittel")
+
+
+def _ticket_row(t):
+ status = "ueberfaellig" if _is_overdue(t) else _status_class(t["status"])
+ status_label = "Ueberfaellig" if _is_overdue(t) else t["status"]
+ return """
+ %s
+ %s %s
+ %s
+ %s
+ %s
+ %s
+ %s
+ %s%%
+ """ % (
+ t["id"], t["id"], t["id"], html.escape(t["titel"]), html.escape(t.get("service", "")),
+ status, status_label, _prio_class(t["prioritaet"]), t["prioritaet"], t["kategorie"],
+ html.escape(t["zugewiesen_an"]), t["aktualisiert"][:16].replace("T", " "),
+ t["fortschritt"], t["fortschritt"])
+
+
+def _kpi_tile(label, value):
+ return '' % (html.escape(label), value)
+
+
+def _tickets_body(filter_status=None, filter_kategorie=None):
+ tickets = sorted(_load("tickets.json", []), key=lambda t: t["aktualisiert"], reverse=True)
+ kpis = _kpis(tickets)
+ kpi_html = "".join(_kpi_tile(k, v) for k, v in kpis.items())
+ shown = tickets
+ if filter_kategorie:
+ shown = [t for t in shown if t["kategorie"] == filter_kategorie]
+ tabs = [("", "Alle"), ("Offen", "Offen"), ("In Bearbeitung", "In Bearbeitung"),
+ ("Warten", "Warten"), ("Geloest", "Geloest"), ("Ueberfaellig", "Ueberfaellig")]
+ base = "/probleme" if filter_kategorie == "Problem" else (
+ "/aenderungen" if filter_kategorie == "Change" else (
+ "/releases" if filter_kategorie == "Release" else "/tickets"))
+ tabs_html = "".join(
+ '%s ' % (
+ base, ("?status=" + s) if s else "", "active" if (s or "") == (filter_status or "") else "", label)
+ for s, label in tabs)
+ if filter_status == "Ueberfaellig":
+ shown = [t for t in shown if _is_overdue(t)]
+ elif filter_status:
+ shown = [t for t in shown if t["status"] == filter_status]
+ rows = "".join(_ticket_row(t) for t in shown) or 'Keine Tickets. '
+ new_form = """""" % (
+ "".join('%s ' % (c, c) for c in CATEGORIES),
+ "".join('%s ' % (p, p) for p in PRIORITIES))
+ return """Tickets
+%s
+%s
+%s
+
+Ticket-Nr Titel (Service) Status Prioritaet
+Kategorie Zugewiesen an Aktualisiert Fortschritt
+ %s
+""" % (kpi_html, new_form, tabs_html, rows)
+
+
+@app.route("/tickets")
+def tickets_route():
+ r = _require_login()
+ if r:
+ return r
+ return _shell("/tickets", "Tickets", _tickets_body(filter_status=request.args.get("status")))
+
+
+@app.route("/probleme")
+def probleme_route():
+ r = _require_login()
+ if r:
+ return r
+ return _shell("/probleme", "Probleme", _tickets_body(request.args.get("status"), "Problem"))
+
+
+@app.route("/aenderungen")
+def aenderungen_route():
+ r = _require_login()
+ if r:
+ return r
+ return _shell("/aenderungen", "Aenderungen", _tickets_body(request.args.get("status"), "Change"))
+
+
+@app.route("/releases")
+def releases_route():
+ r = _require_login()
+ if r:
+ return r
+ return _shell("/releases", "Releases", _tickets_body(request.args.get("status"), "Release"))
+
+
+@app.route("/tickets/new", methods=["POST"])
+def tickets_new_route():
+ r = _require_login()
+ if r:
+ return r
+ with _LOCK:
+ tickets = _load("tickets.json", [])
+ tid = _next_ticket_id(tickets)
+ t = {
+ "id": tid,
+ "titel": request.form.get("titel", "").strip() or "(ohne Titel)",
+ "beschreibung": request.form.get("beschreibung", "").strip(),
+ "service": request.form.get("service", "").strip(),
+ "status": "Offen",
+ "prioritaet": request.form.get("prioritaet", "Mittel"),
+ "kategorie": request.form.get("kategorie", "Task"),
+ "zugewiesen_an": session.get("user", ITSM_USER),
+ "erstellt": _now(),
+ "aktualisiert": _now(),
+ "fortschritt": 0,
+ "betroffene_services": [],
+ "ci_elemente": [],
+ "sla_antwort_minuten": 480,
+ "sla_loesung_minuten": 2880,
+ "timeline": [{"zeit": _now(), "akteur": session.get("user", ITSM_USER), "text": "Ticket angelegt."}],
+ }
+ tickets.append(t)
+ _save("tickets.json", tickets)
+ return redirect(url_for("tickets_route"))
+
+
+@app.route("/api/tickets/")
+def api_ticket(tid):
+ r = _require_login()
+ if r:
+ return r
+ tickets = _load("tickets.json", [])
+ for t in tickets:
+ if t["id"] == tid:
+ return jsonify(t)
+ abort(404)
+
+
+# ── Service-Katalog ────────────────────────────────────────────────────────────
+def _aes_live_status():
+ """Fragt den Status des AES-Service live ueber dessen Dashboard-API ab
+ (kein Code-Import -- ITSM und AES sind getrennte Systeme, verbunden nur
+ per HTTP, wie es die Rolle 'AES als buchbarer Service' verlangt)."""
+ if not AES_DASHBOARD_URL:
+ return None
+ try:
+ req = urllib.request.Request(AES_DASHBOARD_URL + "/health", method="GET")
+ with urllib.request.urlopen(req, timeout=4) as resp:
+ return resp.status == 200
+ except Exception:
+ return None
+
+
+def _services_body():
+ services = _load("services.json", [])
+ cards = []
+ for s in services:
+ badge = 'gebucht ' if s.get("gebucht") else 'nicht gebucht '
+ link = (''
+ % html.escape(s["endpoint"])) if s.get("endpoint") else ""
+ cards.append("""""" % (
+ html.escape(s["name"]), html.escape(s["beschreibung"]), badge, link))
+ add_form = """"""
+ return 'Service-Katalog ' + "".join(cards) + add_form
+
+
+@app.route("/services")
+def services_route():
+ r = _require_login()
+ if r:
+ return r
+ return _shell("/services", "Service-Katalog", _services_body())
+
+
+@app.route("/services/new", methods=["POST"])
+def services_new_route():
+ r = _require_login()
+ if r:
+ return r
+ with _LOCK:
+ services = _load("services.json", [])
+ services.append({
+ "id": uuid.uuid4().hex[:8],
+ "name": request.form.get("name", "").strip(),
+ "beschreibung": request.form.get("beschreibung", "").strip(),
+ "kategorie": "Sonstiges",
+ "gebucht": True,
+ "endpoint": request.form.get("endpoint", "").strip(),
+ "hinzugefuegt": _now(),
+ })
+ _save("services.json", services)
+ return redirect(url_for("services_route"))
+
+
+# ── Platzhalterseiten (Wissen/Assets/Administration) ──────────────────────────
+def _placeholder(title, note):
+ return '%s ' % (
+ html.escape(title), html.escape(note))
+
+
+@app.route("/wissen")
+def wissen_route():
+ r = _require_login()
+ if r:
+ return r
+ return _shell("/wissen", "Wissensdatenbank",
+ _placeholder("Wissensdatenbank", "Noch nicht aufgebaut -- folgt in einer naechsten Ausbaustufe."))
+
+
+@app.route("/assets")
+def assets_route():
+ r = _require_login()
+ if r:
+ return r
+ return _shell("/assets", "Asset-Liste",
+ _placeholder("Asset-Liste", "Noch nicht aufgebaut -- folgt in einer naechsten Ausbaustufe."))
+
+
+@app.route("/admin")
+def admin_route():
+ r = _require_login()
+ if r:
+ return r
+ return _shell("/admin", "Einstellungen",
+ _placeholder("Einstellungen", "Benutzer/Rollen/Workflows folgen in einer naechsten Ausbaustufe."))
+
+
+@app.route("/")
+def root_route():
+ return redirect(url_for("tickets_route"))
+
+
+@app.route("/health")
+def health_route():
+ return jsonify({"status": "ok"})
+
+
+_seed_services_if_empty()
+_seed_if_empty()
+
+
+if __name__ == "__main__":
+ app.run(host="0.0.0.0", port=8080)