feat: Technik/Datenbank-Verwaltung im Admin-Bereich
routers/admin_technik.py: Merkmale (anlegen/bearbeiten inkl. Kategorie und Katalogfähigkeit), Kategorien (anlegen), Workspaces (anlegen + Zusammensetzung aus Merkmalen - Grundbaustein des Workspace-Creators), Backends/Blueprints nur zur Anzeige. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
33c4aebfec
commit
eb9fc6b8a7
@ -1,5 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, Form, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from anode_client import anode_request
|
||||
from auth import require_admin
|
||||
from templating import templates
|
||||
|
||||
@ -8,7 +10,173 @@ router = APIRouter(prefix="/admin/technik")
|
||||
|
||||
|
||||
@router.get("")
|
||||
def index(request: Request, user: dict = Depends(require_admin)):
|
||||
def index(user: dict = Depends(require_admin)):
|
||||
return RedirectResponse("/admin/technik/merkmale", status_code=303)
|
||||
|
||||
|
||||
# --- Merkmale ---
|
||||
|
||||
@router.get("/merkmale")
|
||||
def merkmale_liste(request: Request, user: dict = Depends(require_admin)):
|
||||
merkmale_result = anode_request("GET", "/api/v1/merkmale")
|
||||
merkmale = merkmale_result.get("merkmale", []) if merkmale_result.get("success") else []
|
||||
|
||||
kategorien_result = anode_request("GET", "/api/v1/kategorien")
|
||||
kategorien = kategorien_result.get("kategorien", []) if kategorien_result.get("success") else []
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request, "admin/platzhalter.html", {"user": user, "titel": "Technik/Datenbank"}
|
||||
request,
|
||||
"admin/technik_merkmale.html",
|
||||
{"user": user, "merkmale": merkmale, "kategorien": kategorien},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/merkmale")
|
||||
def merkmal_anlegen(
|
||||
key: str = Form(...), name: str = Form(...), description: str = Form(default=""),
|
||||
user: dict = Depends(require_admin),
|
||||
):
|
||||
anode_request(
|
||||
"POST", "/api/v1/merkmale",
|
||||
json={"key": key, "name": name, "description": description or None},
|
||||
)
|
||||
|
||||
return RedirectResponse("/admin/technik/merkmale", status_code=303)
|
||||
|
||||
|
||||
@router.post("/merkmale/{merkmal_id}")
|
||||
def merkmal_aktualisieren(
|
||||
merkmal_id: str,
|
||||
name: str = Form(...),
|
||||
description: str = Form(default=""),
|
||||
kategorie_id: str = Form(default=""),
|
||||
im_auftragskatalog: str = Form(default=""),
|
||||
user: dict = Depends(require_admin),
|
||||
):
|
||||
anode_request(
|
||||
"PATCH", f"/api/v1/merkmale/{merkmal_id}",
|
||||
json={
|
||||
"name": name,
|
||||
"description": description or None,
|
||||
"kategorie_id": kategorie_id or None,
|
||||
"im_auftragskatalog": im_auftragskatalog == "on",
|
||||
},
|
||||
)
|
||||
|
||||
return RedirectResponse("/admin/technik/merkmale", status_code=303)
|
||||
|
||||
|
||||
# --- Kategorien ---
|
||||
|
||||
@router.get("/kategorien")
|
||||
def kategorien_liste(request: Request, user: dict = Depends(require_admin)):
|
||||
result = anode_request("GET", "/api/v1/kategorien")
|
||||
kategorien = result.get("kategorien", []) if result.get("success") else []
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request, "admin/technik_kategorien.html", {"user": user, "kategorien": kategorien}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/kategorien")
|
||||
def kategorie_anlegen(
|
||||
key: str = Form(...), name: str = Form(...), description: str = Form(default=""),
|
||||
sort_order: int = Form(default=0), user: dict = Depends(require_admin),
|
||||
):
|
||||
anode_request(
|
||||
"POST", "/api/v1/kategorien",
|
||||
json={"key": key, "name": name, "description": description or None, "sort_order": sort_order},
|
||||
)
|
||||
|
||||
return RedirectResponse("/admin/technik/kategorien", status_code=303)
|
||||
|
||||
|
||||
# --- Workspaces ---
|
||||
|
||||
@router.get("/workspaces")
|
||||
def workspaces_liste(request: Request, user: dict = Depends(require_admin)):
|
||||
result = anode_request("GET", "/api/v1/workspaces")
|
||||
workspaces = result.get("workspaces", []) if result.get("success") else []
|
||||
|
||||
orgs_result = anode_request("GET", "/api/v1/organizations")
|
||||
organizations = orgs_result.get("organizations", []) if orgs_result.get("success") else []
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/technik_workspaces.html",
|
||||
{"user": user, "workspaces": workspaces, "organizations": organizations},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/workspaces")
|
||||
def workspace_anlegen(
|
||||
key: str = Form(...), name: str = Form(...), description: str = Form(default=""),
|
||||
organization_id: str = Form(default=""), user: dict = Depends(require_admin),
|
||||
):
|
||||
anode_request(
|
||||
"POST", "/api/v1/workspaces",
|
||||
json={
|
||||
"key": key, "name": name, "description": description or None,
|
||||
"organization_id": organization_id or None,
|
||||
},
|
||||
)
|
||||
|
||||
return RedirectResponse("/admin/technik/workspaces", status_code=303)
|
||||
|
||||
|
||||
@router.get("/workspaces/{workspace_id}")
|
||||
def workspace_detail(request: Request, workspace_id: str, user: dict = Depends(require_admin)):
|
||||
result = anode_request("GET", f"/api/v1/workspaces/{workspace_id}")
|
||||
workspace = result.get("workspace") if result.get("success") else None
|
||||
|
||||
merkmale_result = anode_request("GET", "/api/v1/merkmale")
|
||||
alle_merkmale = merkmale_result.get("merkmale", []) if merkmale_result.get("success") else []
|
||||
|
||||
komponierte_ids = {m["id"] for m in workspace["merkmale"]} if workspace else set()
|
||||
verfuegbare_merkmale = [m for m in alle_merkmale if m["id"] not in komponierte_ids]
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/technik_workspace_detail.html",
|
||||
{
|
||||
"user": user,
|
||||
"workspace": workspace,
|
||||
"workspace_id": workspace_id,
|
||||
"verfuegbare_merkmale": verfuegbare_merkmale,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/workspaces/{workspace_id}/merkmale")
|
||||
def workspace_merkmal_hinzufuegen(
|
||||
workspace_id: str, merkmal_id: str = Form(...), user: dict = Depends(require_admin)
|
||||
):
|
||||
anode_request("POST", f"/api/v1/workspaces/{workspace_id}/merkmale/{merkmal_id}")
|
||||
|
||||
return RedirectResponse(f"/admin/technik/workspaces/{workspace_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/workspaces/{workspace_id}/merkmale/{merkmal_id}/entfernen")
|
||||
def workspace_merkmal_entfernen(
|
||||
workspace_id: str, merkmal_id: str, user: dict = Depends(require_admin)
|
||||
):
|
||||
anode_request("DELETE", f"/api/v1/workspaces/{workspace_id}/merkmale/{merkmal_id}")
|
||||
|
||||
return RedirectResponse(f"/admin/technik/workspaces/{workspace_id}", status_code=303)
|
||||
|
||||
|
||||
# --- Backends / Blueprints (nur Anzeige) ---
|
||||
|
||||
@router.get("/backends")
|
||||
def backends_liste(request: Request, user: dict = Depends(require_admin)):
|
||||
backends_result = anode_request("GET", "/api/v1/backends")
|
||||
backends = backends_result.get("backends", []) if backends_result.get("success") else []
|
||||
|
||||
blueprints_result = anode_request("GET", "/api/v1/blueprints")
|
||||
blueprints = blueprints_result.get("blueprints", []) if blueprints_result.get("success") else []
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/technik_backends.html",
|
||||
{"user": user, "backends": backends, "blueprints": blueprints},
|
||||
)
|
||||
|
||||
36
templates/admin/technik_backends.html
Normal file
36
templates/admin/technik_backends.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends "admin/layout.html" %}
|
||||
{% block title %}Backends — Tuxflotte Admin{% endblock %}
|
||||
{% block admin_content %}
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/admin/technik/merkmale">Merkmale</a></li>
|
||||
<li><a href="/admin/technik/kategorien">Kategorien</a></li>
|
||||
<li><a href="/admin/technik/workspaces">Workspaces</a></li>
|
||||
<li><a href="/admin/technik/backends"><strong>Backends/Blueprints</strong></a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<h2>Backends</h2>
|
||||
<p><small>Nur Anzeige — Änderungen brauchen echten Ansible-Rollen-Code, nicht nur einen Datenbankeintrag.</small></p>
|
||||
<figure>
|
||||
<table>
|
||||
<thead><tr><th>Key</th><th>Name</th><th>Installer-Typ</th><th>Version</th></tr></thead>
|
||||
<tbody>
|
||||
{% for b in backends %}
|
||||
<tr><td><code>{{ b.key }}</code></td><td>{{ b.name }}</td><td>{{ b.installer_type }}</td><td>{{ b.version or "—" }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
|
||||
<h2>Blueprints</h2>
|
||||
<figure>
|
||||
<table>
|
||||
<thead><tr><th>Merkmal</th><th>Backend</th><th>Ansible-Rolle</th></tr></thead>
|
||||
<tbody>
|
||||
{% for bp in blueprints %}
|
||||
<tr><td><code>{{ bp.merkmal }}</code></td><td>{{ bp.backend }}</td><td><code>{{ bp.ansible_role }}</code></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
{% endblock %}
|
||||
39
templates/admin/technik_kategorien.html
Normal file
39
templates/admin/technik_kategorien.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends "admin/layout.html" %}
|
||||
{% block title %}Kategorien — Tuxflotte Admin{% endblock %}
|
||||
{% block admin_content %}
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/admin/technik/merkmale">Merkmale</a></li>
|
||||
<li><a href="/admin/technik/kategorien"><strong>Kategorien</strong></a></li>
|
||||
<li><a href="/admin/technik/workspaces">Workspaces</a></li>
|
||||
<li><a href="/admin/technik/backends">Backends/Blueprints</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<h2>Kategorien</h2>
|
||||
|
||||
<figure>
|
||||
<table>
|
||||
<thead><tr><th>Key</th><th>Name</th><th>Reihenfolge</th></tr></thead>
|
||||
<tbody>
|
||||
{% for k in kategorien %}
|
||||
<tr>
|
||||
<td><code>{{ k.key }}</code></td>
|
||||
<td>{{ k.name }}</td>
|
||||
<td>{{ k.sort_order }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
|
||||
<details>
|
||||
<summary role="button" class="secondary outline">Neue Kategorie anlegen</summary>
|
||||
<form method="post" action="/admin/technik/kategorien">
|
||||
<label>Key<input type="text" name="key" required></label>
|
||||
<label>Name<input type="text" name="name" required></label>
|
||||
<label>Beschreibung<input type="text" name="description"></label>
|
||||
<label>Reihenfolge<input type="number" name="sort_order" value="0"></label>
|
||||
<button type="submit">Anlegen</button>
|
||||
</form>
|
||||
</details>
|
||||
{% endblock %}
|
||||
61
templates/admin/technik_merkmale.html
Normal file
61
templates/admin/technik_merkmale.html
Normal file
@ -0,0 +1,61 @@
|
||||
{% extends "admin/layout.html" %}
|
||||
{% block title %}Merkmale — Tuxflotte Admin{% endblock %}
|
||||
{% block admin_content %}
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/admin/technik/merkmale"><strong>Merkmale</strong></a></li>
|
||||
<li><a href="/admin/technik/kategorien">Kategorien</a></li>
|
||||
<li><a href="/admin/technik/workspaces">Workspaces</a></li>
|
||||
<li><a href="/admin/technik/backends">Backends/Blueprints</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<h2>Merkmale</h2>
|
||||
|
||||
<figure>
|
||||
<table>
|
||||
<thead><tr><th>Key</th><th>Name</th><th>Kategorie</th><th>Katalogfähig</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for m in merkmale %}
|
||||
<tr>
|
||||
<td><code>{{ m.key }}</code></td>
|
||||
<td>{{ m.name }}</td>
|
||||
<td>{{ m.kategorie_name or "—" }}</td>
|
||||
<td>{{ "ja" if m.im_auftragskatalog else "nein" }}</td>
|
||||
<td>
|
||||
<details>
|
||||
<summary role="button" class="outline">Bearbeiten</summary>
|
||||
<form method="post" action="/admin/technik/merkmale/{{ m.id }}">
|
||||
<label>Name<input type="text" name="name" value="{{ m.name }}" required></label>
|
||||
<label>Beschreibung<input type="text" name="description" value="{{ m.description or '' }}"></label>
|
||||
<label>Kategorie
|
||||
<select name="kategorie_id">
|
||||
<option value="">— keine —</option>
|
||||
{% for k in kategorien %}
|
||||
<option value="{{ k.id }}" {{ "selected" if k.id == m.kategorie_id else "" }}>{{ k.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="im_auftragskatalog" {{ "checked" if m.im_auftragskatalog else "" }}>
|
||||
Im Auftragskatalog
|
||||
</label>
|
||||
<button type="submit">Speichern</button>
|
||||
</form>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
|
||||
<details>
|
||||
<summary role="button" class="secondary outline">Neues Merkmal anlegen</summary>
|
||||
<form method="post" action="/admin/technik/merkmale">
|
||||
<label>Key<input type="text" name="key" required></label>
|
||||
<label>Name<input type="text" name="name" required></label>
|
||||
<label>Beschreibung<input type="text" name="description"></label>
|
||||
<button type="submit">Anlegen</button>
|
||||
</form>
|
||||
</details>
|
||||
{% endblock %}
|
||||
46
templates/admin/technik_workspace_detail.html
Normal file
46
templates/admin/technik_workspace_detail.html
Normal file
@ -0,0 +1,46 @@
|
||||
{% extends "admin/layout.html" %}
|
||||
{% block title %}{{ workspace.name if workspace else workspace_id }} — Tuxflotte Admin{% endblock %}
|
||||
{% block admin_content %}
|
||||
<p><a href="/admin/technik/workspaces">← Workspaces</a></p>
|
||||
{% if not workspace %}
|
||||
<p><mark>Workspace wurde nicht gefunden.</mark></p>
|
||||
{% else %}
|
||||
<h2>{{ workspace.name }}</h2>
|
||||
<p>{{ workspace.organization_name or "Global" }}{% if workspace.description %} — {{ workspace.description }}{% endif %}</p>
|
||||
|
||||
<h3>Zusammensetzung</h3>
|
||||
{% if not workspace.merkmale %}
|
||||
<p>Noch keine Merkmale zugeordnet.</p>
|
||||
{% else %}
|
||||
<figure>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for m in workspace.merkmale %}
|
||||
<tr>
|
||||
<td>{{ m.name }} <code>{{ m.key }}</code></td>
|
||||
<td>
|
||||
<form method="post" action="/admin/technik/workspaces/{{ workspace_id }}/merkmale/{{ m.id }}/entfernen">
|
||||
<button type="submit" class="secondary outline">Entfernen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
{% endif %}
|
||||
|
||||
{% if verfuegbare_merkmale %}
|
||||
<form method="post" action="/admin/technik/workspaces/{{ workspace_id }}/merkmale">
|
||||
<label>Merkmal hinzufügen
|
||||
<select name="merkmal_id">
|
||||
{% for m in verfuegbare_merkmale %}
|
||||
<option value="{{ m.id }}">{{ m.name }} ({{ m.key }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Hinzufügen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
47
templates/admin/technik_workspaces.html
Normal file
47
templates/admin/technik_workspaces.html
Normal file
@ -0,0 +1,47 @@
|
||||
{% extends "admin/layout.html" %}
|
||||
{% block title %}Workspaces — Tuxflotte Admin{% endblock %}
|
||||
{% block admin_content %}
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/admin/technik/merkmale">Merkmale</a></li>
|
||||
<li><a href="/admin/technik/kategorien">Kategorien</a></li>
|
||||
<li><a href="/admin/technik/workspaces"><strong>Workspaces</strong></a></li>
|
||||
<li><a href="/admin/technik/backends">Backends/Blueprints</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<h2>Workspaces</h2>
|
||||
|
||||
<figure>
|
||||
<table>
|
||||
<thead><tr><th>Key</th><th>Name</th><th>Organisation</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for w in workspaces %}
|
||||
<tr>
|
||||
<td><code>{{ w.key }}</code></td>
|
||||
<td>{{ w.name }}</td>
|
||||
<td>{{ w.organization_name or "Global" }}</td>
|
||||
<td><a href="/admin/technik/workspaces/{{ w.id }}" role="button" class="outline">Zusammensetzung</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
|
||||
<details>
|
||||
<summary role="button" class="secondary outline">Neuen Workspace anlegen</summary>
|
||||
<form method="post" action="/admin/technik/workspaces">
|
||||
<label>Key<input type="text" name="key" required></label>
|
||||
<label>Name<input type="text" name="name" required></label>
|
||||
<label>Beschreibung<input type="text" name="description"></label>
|
||||
<label>Organisation (leer = global)
|
||||
<select name="organization_id">
|
||||
<option value="">— global —</option>
|
||||
{% for org in organizations %}
|
||||
<option value="{{ org.id }}">{{ org.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Anlegen</button>
|
||||
</form>
|
||||
</details>
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user