' % (tabs, search_form, table))
@app.route("/wissen")
@@ -769,8 +945,141 @@ def wissen_route():
r = _require_login()
if r:
return r
- return _shell("/wissen", "Wissensdatenbank",
- _placeholder("Wissensdatenbank", "Noch nicht aufgebaut -- folgt in einer naechsten Ausbaustufe."))
+ return _shell("/wissen", "Wissensdatenbank", _kb_list_body(session["tenant_id"]))
+
+
+@app.route("/wissen/new", methods=["GET", "POST"])
+def wissen_new_route():
+ r = _require_login()
+ if r:
+ return r
+ tenant_id = session["tenant_id"]
+ error = None
+ titel, kategorie, inhalt, tags = "", "Allgemein", "", ""
+ if request.method == "POST":
+ titel = request.form.get("titel", "").strip()
+ kategorie = request.form.get("kategorie", "Allgemein").strip()
+ inhalt = request.form.get("inhalt", "").strip()
+ tags = request.form.get("tags", "").strip()
+ if not titel:
+ error = "Titel ist erforderlich."
+ elif not inhalt:
+ error = "Inhalt darf nicht leer sein."
+ else:
+ aid = db.create_kb_article(tenant_id, titel, kategorie, inhalt, tags, session["user_id"])
+ db.log_audit(tenant_id, session["user_id"], "kb_article_created", "kb_article", str(aid),
+ {"titel": titel}, _client_ip())
+ return redirect(url_for("wissen_detail_route", article_id=aid))
+ kat_opts = "".join('' % (k, "selected" if k == kategorie else "", k)
+ for k in KB_KATEGORIEN)
+ err_html = "
%s
" % html.escape(error) if error else ""
+ body = """
Neuer Wissensartikel
+
%s
""" % (err_html, html.escape(titel), kat_opts, html.escape(inhalt), html.escape(tags))
+ return _shell("/wissen", "Neuer Wissensartikel", body)
+
+
+@app.route("/wissen/")
+def wissen_detail_route(article_id):
+ r = _require_login()
+ if r:
+ return r
+ a = db.get_kb_article(session["tenant_id"], article_id)
+ if not a:
+ abort(404)
+ tag_pills = "".join('%s' % html.escape(t.strip())
+ for t in (a["tags"] or "").split(",") if t.strip())
+ body = """
%s
+
%s · %s · aktualisiert %s
+
%s
+
%s
+Bearbeiten
+Zurueck zur Uebersicht""" % (
+ html.escape(a["titel"]), html.escape(a["kategorie"]), html.escape(a["autor_email"] or ""),
+ a["updated_at"].strftime("%Y-%m-%d %H:%M"), html.escape(a["inhalt"]), tag_pills, a["id"])
+ return _shell("/wissen", a["titel"], body)
+
+
+@app.route("/wissen//edit", methods=["GET", "POST"])
+def wissen_edit_route(article_id):
+ r = _require_login()
+ if r:
+ return r
+ tenant_id = session["tenant_id"]
+ a = db.get_kb_article(tenant_id, article_id)
+ if not a:
+ abort(404)
+ error = None
+ if request.method == "POST":
+ titel = request.form.get("titel", "").strip()
+ kategorie = request.form.get("kategorie", "Allgemein").strip()
+ inhalt = request.form.get("inhalt", "").strip()
+ tags = request.form.get("tags", "").strip()
+ if not titel or not inhalt:
+ error = "Titel und Inhalt duerfen nicht leer sein."
+ else:
+ db.update_kb_article(tenant_id, article_id, titel, kategorie, inhalt, tags)
+ db.log_audit(tenant_id, session["user_id"], "kb_article_updated", "kb_article", str(article_id),
+ {"titel": titel}, _client_ip())
+ return redirect(url_for("wissen_detail_route", article_id=article_id))
+ a = dict(a)
+ a.update({"titel": titel, "kategorie": kategorie, "inhalt": inhalt, "tags": tags})
+ kat_opts = "".join('' % (k, "selected" if k == a["kategorie"] else "", k)
+ for k in KB_KATEGORIEN)
+ err_html = "
%s
" % html.escape(error) if error else ""
+ body = """
Artikel bearbeiten
+
%s
+
""" % (err_html, html.escape(a["titel"]), kat_opts, html.escape(a["inhalt"]), html.escape(a["tags"] or ""), article_id)
+ return _shell("/wissen", "Artikel bearbeiten", body)
+
+
+@app.route("/wissen//delete", methods=["POST"])
+def wissen_delete_route(article_id):
+ r = _require_login()
+ if r:
+ return r
+ tenant_id = session["tenant_id"]
+ db.delete_kb_article(tenant_id, article_id)
+ db.log_audit(tenant_id, session["user_id"], "kb_article_deleted", "kb_article", str(article_id), None, _client_ip())
+ return redirect(url_for("wissen_route"))
+
+
+# ── CMDB (Configuration Management Database) ───────────────────────────────────
+def _ci_status_pill(status):
+ cls = {"Aktiv": "geloest", "Inaktiv": "geschlossen", "Wartung": "bearbeitung", "Ausgemustert": "ueberfaellig"}.get(status, "offen")
+ return '%s' % (cls, html.escape(status))
+
+
+def _cmdb_list_body(tenant_id):
+ ci_typ = request.args.get("typ") or None
+ cis = db.list_cis(tenant_id, ci_typ=ci_typ)
+ tabs = 'Alle' % ("active" if not ci_typ else "")
+ tabs += "".join('%s' % (t, "active" if ci_typ == t else "", t) for t in CI_TYPES)
+ rows = []
+ for c in cis:
+ rows.append("""
' % (tabs, table))
@app.route("/assets")
@@ -778,8 +1087,150 @@ 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."))
+ return _shell("/assets", "CMDB", _cmdb_list_body(session["tenant_id"]))
+
+
+@app.route("/assets/new", methods=["GET", "POST"])
+def assets_new_route():
+ r = _require_login()
+ if r:
+ return r
+ tenant_id = session["tenant_id"]
+ error = None
+ name, ci_typ, status, beschreibung, attr_text = "", "Server", "Aktiv", "", ""
+ if request.method == "POST":
+ name = request.form.get("name", "").strip()
+ ci_typ = request.form.get("ci_typ", "Sonstiges").strip()
+ status = request.form.get("status", "Aktiv").strip()
+ beschreibung = request.form.get("beschreibung", "").strip()
+ attr_text = request.form.get("attribute", "")
+ if not name:
+ error = "Name ist erforderlich."
+ else:
+ cid = db.create_ci(tenant_id, name, ci_typ, status, beschreibung, _text_to_attrs(attr_text))
+ db.log_audit(tenant_id, session["user_id"], "ci_created", "ci", str(cid),
+ {"name": name, "ci_typ": ci_typ}, _client_ip())
+ return redirect(url_for("assets_detail_route", ci_id=cid))
+ typ_opts = "".join('' % (t, "selected" if t == ci_typ else "", t) for t in CI_TYPES)
+ status_opts = "".join('' % (s, "selected" if s == status else "", s) for s in CI_STATUS)
+ err_html = "
%s
" % html.escape(error) if error else ""
+ body = """
Neues Configuration Item
+
%s
""" % (err_html, html.escape(name), typ_opts, status_opts, html.escape(beschreibung), html.escape(attr_text))
+ return _shell("/assets", "Neues Configuration Item", body)
+
+
+@app.route("/assets/", methods=["GET", "POST"])
+def assets_detail_route(ci_id):
+ r = _require_login()
+ if r:
+ return r
+ tenant_id = session["tenant_id"]
+ error = None
+ if request.method == "POST":
+ name = request.form.get("name", "").strip()
+ ci_typ = request.form.get("ci_typ", "Sonstiges").strip()
+ status = request.form.get("status", "Aktiv").strip()
+ beschreibung = request.form.get("beschreibung", "").strip()
+ attr_text = request.form.get("attribute", "")
+ if not name:
+ error = "Name ist erforderlich."
+ else:
+ db.update_ci(tenant_id, ci_id, name, ci_typ, status, beschreibung, _text_to_attrs(attr_text))
+ db.log_audit(tenant_id, session["user_id"], "ci_updated", "ci", str(ci_id), {"name": name}, _client_ip())
+ c = db.get_ci(tenant_id, ci_id)
+ if not c:
+ abort(404)
+ typ_opts = "".join('' % (t, "selected" if t == c["ci_typ"] else "", t) for t in CI_TYPES)
+ status_opts = "".join('' % (s, "selected" if s == c["status"] else "", s) for s in CI_STATUS)
+ err_html = "
%s
" % html.escape(error) if error else ""
+
+ other_cis = [x for x in db.list_cis(tenant_id) if x["id"] != ci_id]
+ ci_opts = "".join('' % (x["id"], html.escape(x["name"]), x["ci_typ"]) for x in other_cis)
+ rel_typ_opts = "".join('' % (t, t) for t in REL_TYPES)
+ rel_out_rows = "".join(
+ "
%s %s
" % (
+ html.escape(r["beziehungs_typ"]), html.escape(r["to_name"]), ci_id, r["id"])
+ for r in c["rel_out"]
+ ) or "
Keine ausgehenden Beziehungen.
"
+ rel_in_rows = "".join(
+ "
%s %s(eingehend)
" % (
+ html.escape(r["beziehungs_typ"]), html.escape(r["from_name"]))
+ for r in c["rel_in"]
+ ) or "