feat(org-detail): Slug aus der Oberfläche, Info-Zeile im Kopf entfernt

Der Slug ist eine rein technische Kennung und verschwindet aus Tabelle,
Kopfzeile und Anlege-Formular. Beim Anlegen wird er unsichtbar aus dem
Namen erzeugt, bei Kollision hängt das Portal selbstständig eine Nummer
an. Die Info-Zeile unter dem Org-Namen (Slug, Nutzer, Lizenz,
Kontingent) entfällt komplett, dieselben Werte stehen direkt darunter
in der Übersicht und der Bearbeiten-Karte.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-07-25 17:01:40 +00:00
Ursprung 15cea7cb3b
Commit 68fc1794a9
2 geänderte Dateien mit 18 neuen und 23 gelöschten Zeilen

Datei anzeigen

@@ -118,7 +118,6 @@
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Slug</th>
<th>Nutzer</th> <th>Nutzer</th>
<th>Lizenz</th> <th>Lizenz</th>
<th>Kontingent</th> <th>Kontingent</th>
@@ -523,10 +522,7 @@
<label for="newOrgName">Name</label> <label for="newOrgName">Name</label>
<input type="text" id="newOrgName" required> <input type="text" id="newOrgName" required>
</div> </div>
<div class="form-group"> <input type="hidden" id="newOrgSlug">
<label for="newOrgSlug">Slug (URL-freundlich)</label>
<input type="text" id="newOrgSlug" required pattern="[a-z0-9-]+" placeholder="z.B. bundespolizei">
</div>
<div class="form-group"> <div class="form-group">
<label for="newOrgLanguage">Pipeline-Sprache</label> <label for="newOrgLanguage">Pipeline-Sprache</label>
<select id="newOrgLanguage"> <select id="newOrgLanguage">
@@ -1109,7 +1105,7 @@
</div> </div>
<script src="/static/js/a11y.js?v=20260725a"></script> <script src="/static/js/a11y.js?v=20260725a"></script>
<script src="/static/js/app.js?v=20260725q"></script> <script src="/static/js/app.js?v=20260725r"></script>
<script src="/static/js/sources.js?v=20260522x2"></script> <script src="/static/js/sources.js?v=20260522x2"></script>
<script src="/static/js/x-scraper.js?v=20260522a"></script> <script src="/static/js/x-scraper.js?v=20260522a"></script>
<script src="/static/js/source-health.js?v=20260509l"></script> <script src="/static/js/source-health.js?v=20260509l"></script>

Datei anzeigen

@@ -253,13 +253,12 @@ function orgUsageCell(orgId) {
function renderOrgTable(orgs) { function renderOrgTable(orgs) {
const tbody = document.getElementById("orgTable"); const tbody = document.getElementById("orgTable");
if (orgs.length === 0) { if (orgs.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-muted">Keine Organisationen</td></tr>'; tbody.innerHTML = '<tr><td colspan="6" class="text-muted">Keine Organisationen</td></tr>';
return; return;
} }
tbody.innerHTML = orgs.map(o => ` tbody.innerHTML = orgs.map(o => `
<tr class="org-row" onclick="openOrg(${o.id})" title="Organisation öffnen"> <tr class="org-row" onclick="openOrg(${o.id})" title="Organisation öffnen">
<td><a href="#" class="text-accent" style="text-decoration: none;" onclick="openOrg(${o.id}); return false;">${esc(o.name)}</a></td> <td><a href="#" class="text-accent" style="text-decoration: none;" onclick="openOrg(${o.id}); return false;">${esc(o.name)}</a></td>
<td class="text-secondary">${esc(o.slug)}</td>
<td>${o.user_count}</td> <td>${o.user_count}</td>
<td><span class="badge badge-${o.license_type || 'none'}">${o.license_type || "Keine"}</span></td> <td><span class="badge badge-${o.license_type || 'none'}">${o.license_type || "Keine"}</span></td>
<td>${orgUsageCell(o.id)}</td> <td>${orgUsageCell(o.id)}</td>
@@ -289,8 +288,6 @@ document.addEventListener("DOMContentLoaded", () => {
let currentOrgData = null; // zuletzt geladene Org (fuer Inline-Einstellungen) let currentOrgData = null; // zuletzt geladene Org (fuer Inline-Einstellungen)
function renderOrgHeader(org) { function renderOrgHeader(org) {
const kpiAlt = document.getElementById("orgKpiCredits");
const kpiText = kpiAlt ? kpiAlt.textContent : "wird geladen …";
document.getElementById("orgDetailHeader").innerHTML = ` document.getElementById("orgDetailHeader").innerHTML = `
<div style="display:flex; align-items:flex-start; gap:10px; flex-wrap:wrap;"> <div style="display:flex; align-items:flex-start; gap:10px; flex-wrap:wrap;">
<div style="flex:1; min-width:260px;"> <div style="flex:1; min-width:260px;">
@@ -306,7 +303,6 @@ function renderOrgHeader(org) {
</button> </button>
</div> </div>
</h2> </h2>
<div class="text-secondary mt-8">Slug: ${esc(org.slug)} | Nutzer: ${org.user_count} | Lizenz: ${org.license_type || "Keine"} | Kontingent: <span id="orgKpiCredits">${esc(kpiText)}</span></div>
</div> </div>
<button class="btn btn-secondary btn-small" onclick="renameOrg()" title="Organisation umbenennen"> <button class="btn btn-secondary btn-small" onclick="renameOrg()" title="Organisation umbenennen">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
@@ -668,9 +664,13 @@ function setupForms() {
e.preventDefault(); e.preventDefault();
const errEl = document.getElementById("newOrgError"); const errEl = document.getElementById("newOrgError");
errEl.style.display = "none"; errEl.style.display = "none";
// Slug wird unsichtbar aus dem Namen erzeugt, bei Kollision haengt der
// Submit-Handler unten selbststaendig eine Nummer an.
const baseSlug = (document.getElementById("newOrgSlug").value ||
document.getElementById("newOrgName").value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")) || "organisation";
const body = { const body = {
name: document.getElementById("newOrgName").value, name: document.getElementById("newOrgName").value,
slug: document.getElementById("newOrgSlug").value, slug: baseSlug,
output_language: document.getElementById("newOrgLanguage").value || "de", output_language: document.getElementById("newOrgLanguage").value || "de",
}; };
// Optional direkt eine Lizenz mit Credits-Kontingent mitgeben // Optional direkt eine Lizenz mit Credits-Kontingent mitgeben
@@ -698,7 +698,16 @@ function setupForms() {
body.license = lic; body.license = lic;
} }
try { try {
await API.post("/api/orgs", body); let angelegt = false;
for (let i = 0; i < 8 && !angelegt; i++) {
body.slug = i === 0 ? baseSlug : baseSlug + "-" + (i + 1);
try {
await API.post("/api/orgs", body);
angelegt = true;
} catch (err) {
if (!(err.message || "").includes("Slug bereits vergeben") || i === 7) throw err;
}
}
closeModal("modalNewOrg"); closeModal("modalNewOrg");
document.getElementById("newOrgForm").reset(); document.getElementById("newOrgForm").reset();
onNewOrgLicenseToggle(); onNewOrgLicenseToggle();
@@ -1100,16 +1109,6 @@ async function loadOrgTokenUsage(orgId) {
fillBudgetForm(budget); fillBudgetForm(budget);
// Kontingent-Kurzinfo im Org-Kopf
const kpi = document.getElementById("orgKpiCredits");
if (kpi) {
if (unlimited) kpi.textContent = "unbegrenzt";
else if (budget.credits_available) {
const pct = budget.credits_percent_used != null ? ` (${budget.credits_percent_used.toFixed(0)}% verbraucht)` : "";
kpi.textContent = `${Math.round(budget.credits_used || 0).toLocaleString("de-DE")} von ${Math.round(budget.credits_available).toLocaleString("de-DE")} Credits${pct}`;
} else kpi.textContent = "kein Kontingent";
}
const tbody = document.getElementById('tokenMonthlyTable'); const tbody = document.getElementById('tokenMonthlyTable');
if (tbody) { if (tbody) {
tbody.innerHTML = monthly.map(function(m) { tbody.innerHTML = monthly.map(function(m) {