Promote develop → main (2026-07-25 22:22 UTC) #16
@@ -76,32 +76,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Artikel-Übersetzung -->
|
|
||||||
<div class="card" id="translationCard" style="margin-top:16px;">
|
|
||||||
<div class="card-header">
|
|
||||||
<h2>Artikel-Übersetzung</h2>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="text-muted" style="margin-top:0;">
|
|
||||||
Die automatische Übersetzung im Monitor ist deaktiviert. Hier lassen sich
|
|
||||||
fremdsprachige Artikel ohne deutsche Fassung manuell übersetzen.
|
|
||||||
</p>
|
|
||||||
<p id="translationInfo" style="margin:12px 0;">Status wird geladen…</p>
|
|
||||||
|
|
||||||
<div id="translationProgressWrap" style="display:none; margin:12px 0;">
|
|
||||||
<div style="background:rgba(128,128,128,0.25); border-radius:6px; height:14px; overflow:hidden;">
|
|
||||||
<div id="translationProgressBar" style="background:#1565c0; height:100%; width:0%; transition:width .3s;"></div>
|
|
||||||
</div>
|
|
||||||
<p class="text-muted" id="translationProgressText" style="margin:6px 0 0;"></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top:12px; display:flex; gap:8px;">
|
|
||||||
<button class="btn btn-primary" id="translationRunBtn">Übersetzung starten</button>
|
|
||||||
<button class="btn btn-danger" id="translationCancelBtn" style="display:none;">Abbrechen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Organizations Section -->
|
<!-- Organizations Section -->
|
||||||
|
|||||||
@@ -130,11 +130,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
setupNavTabs();
|
setupNavTabs();
|
||||||
setupOrgDetailTabs();
|
setupOrgDetailTabs();
|
||||||
setupForms();
|
setupForms();
|
||||||
setupTranslation();
|
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
loadTariffInfo();
|
loadTariffInfo();
|
||||||
loadDashboardTokenStats();
|
loadDashboardTokenStats();
|
||||||
loadTranslationStatus();
|
|
||||||
loadOrgs();
|
loadOrgs();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,7 +152,6 @@ function setupNavTabs() {
|
|||||||
document.querySelectorAll(".app-content > .section").forEach(s => s.classList.remove("active"));
|
document.querySelectorAll(".app-content > .section").forEach(s => s.classList.remove("active"));
|
||||||
document.getElementById(`sec-${section}`).classList.add("active");
|
document.getElementById(`sec-${section}`).classList.add("active");
|
||||||
|
|
||||||
if (section === "dashboard") loadTranslationStatus();
|
|
||||||
if (section === "audit" && typeof loadAudit === "function") loadAudit();
|
if (section === "audit" && typeof loadAudit === "function") loadAudit();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -918,151 +915,6 @@ function formatDate(iso) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ===== Artikel-Übersetzung =====
|
|
||||||
let translationPollTimer = null;
|
|
||||||
|
|
||||||
function setupTranslation() {
|
|
||||||
const runBtn = document.getElementById("translationRunBtn");
|
|
||||||
const cancelBtn = document.getElementById("translationCancelBtn");
|
|
||||||
if (runBtn) runBtn.addEventListener("click", startTranslation);
|
|
||||||
if (cancelBtn) cancelBtn.addEventListener("click", cancelTranslation);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDuration(seconds) {
|
|
||||||
seconds = Math.max(0, Math.round(seconds || 0));
|
|
||||||
if (seconds < 60) return seconds + " Sek.";
|
|
||||||
const min = Math.round(seconds / 60);
|
|
||||||
if (min < 60) return min + " Min.";
|
|
||||||
const h = Math.floor(min / 60), m = min % 60;
|
|
||||||
return h + " Std. " + (m ? m + " Min." : "").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTranslation(st) {
|
|
||||||
const info = document.getElementById("translationInfo");
|
|
||||||
const wrap = document.getElementById("translationProgressWrap");
|
|
||||||
const bar = document.getElementById("translationProgressBar");
|
|
||||||
const ptext = document.getElementById("translationProgressText");
|
|
||||||
const runBtn = document.getElementById("translationRunBtn");
|
|
||||||
const cancelBtn = document.getElementById("translationCancelBtn");
|
|
||||||
if (!info || !runBtn) return;
|
|
||||||
|
|
||||||
if (st.running) {
|
|
||||||
runBtn.style.display = "none";
|
|
||||||
cancelBtn.style.display = "";
|
|
||||||
wrap.style.display = "";
|
|
||||||
const pct = st.total > 0 ? Math.round((st.done / st.total) * 100) : 0;
|
|
||||||
bar.style.width = pct + "%";
|
|
||||||
ptext.textContent = `${st.done} / ${st.total} verarbeitet, ${st.translated} übersetzt (${pct}%)`;
|
|
||||||
info.textContent = "Übersetzung läuft…";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
runBtn.style.display = "";
|
|
||||||
cancelBtn.style.display = "none";
|
|
||||||
wrap.style.display = "none";
|
|
||||||
|
|
||||||
let resultLine = "";
|
|
||||||
if (st.finished_at && (st.total > 0 || st.error)) {
|
|
||||||
if (st.error) {
|
|
||||||
resultLine = `Letzter Lauf mit Fehler beendet: ${st.error}. `;
|
|
||||||
} else if (st.cancelled) {
|
|
||||||
resultLine = `Letzter Lauf abgebrochen, ${st.translated} von ${st.total} Artikeln übersetzt. `;
|
|
||||||
} else {
|
|
||||||
resultLine = `Letzter Lauf abgeschlossen, ${st.translated} Artikel übersetzt. `;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (st.pending > 0) {
|
|
||||||
const est = st.estimate || {};
|
|
||||||
info.textContent = resultLine +
|
|
||||||
`${st.pending} Artikel ohne deutsche Übersetzung. ` +
|
|
||||||
`Geschätzt: ${formatDuration(est.seconds)}, ca. $${est.cost_usd}.`;
|
|
||||||
runBtn.disabled = false;
|
|
||||||
} else {
|
|
||||||
info.textContent = resultLine + "Alle Artikel sind übersetzt.";
|
|
||||||
runBtn.disabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTranslationStatus() {
|
|
||||||
try {
|
|
||||||
const st = await API.get("/api/translation/status");
|
|
||||||
renderTranslation(st);
|
|
||||||
if (st.running && !translationPollTimer) {
|
|
||||||
translationPollTimer = setInterval(pollTranslation, 3000);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
const info = document.getElementById("translationInfo");
|
|
||||||
if (info) info.textContent = "Status nicht abrufbar: " + (e.message || e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollTranslation() {
|
|
||||||
try {
|
|
||||||
const st = await API.get("/api/translation/status");
|
|
||||||
renderTranslation(st);
|
|
||||||
if (!st.running) {
|
|
||||||
clearInterval(translationPollTimer);
|
|
||||||
translationPollTimer = null;
|
|
||||||
if (st.error) {
|
|
||||||
showToast("Übersetzung mit Fehler beendet", "error");
|
|
||||||
} else if (st.cancelled) {
|
|
||||||
showToast(`Übersetzung abgebrochen, ${st.translated} übersetzt`, "info");
|
|
||||||
} else {
|
|
||||||
showToast(`Übersetzung fertig: ${st.translated} Artikel`, "success");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Translation-Poll fehlgeschlagen:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startTranslation() {
|
|
||||||
let st;
|
|
||||||
try {
|
|
||||||
st = await API.get("/api/translation/status");
|
|
||||||
} catch (e) {
|
|
||||||
showToast(e.message || "Status nicht abrufbar", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (st.running) { showToast("Es läuft bereits eine Übersetzung", "info"); return; }
|
|
||||||
if (!st.pending) { showToast("Es gibt nichts zu übersetzen", "info"); return; }
|
|
||||||
|
|
||||||
const est = st.estimate || {};
|
|
||||||
const ok = await showConfirm(
|
|
||||||
"Übersetzung starten",
|
|
||||||
`${st.pending} Artikel werden ins Deutsche übersetzt. ` +
|
|
||||||
`Geschätzte Dauer: ${formatDuration(est.seconds)}, geschätzte Kosten: ca. $${est.cost_usd}. ` +
|
|
||||||
`Der Lauf kann jederzeit abgebrochen werden.`
|
|
||||||
);
|
|
||||||
if (!ok) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await API.post("/api/translation/run", {});
|
|
||||||
if (res && res.status === "started") {
|
|
||||||
showToast(`Übersetzung gestartet (${res.pending} Artikel)`, "success");
|
|
||||||
await loadTranslationStatus();
|
|
||||||
if (!translationPollTimer) {
|
|
||||||
translationPollTimer = setInterval(pollTranslation, 3000);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showToast("Es gibt nichts zu übersetzen", "info");
|
|
||||||
loadTranslationStatus();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showToast(e.message || "Start fehlgeschlagen", "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cancelTranslation() {
|
|
||||||
try {
|
|
||||||
await API.post("/api/translation/cancel", {});
|
|
||||||
showToast("Übersetzung wird abgebrochen…", "info");
|
|
||||||
} catch (e) {
|
|
||||||
showToast(e.message || "Abbruch fehlgeschlagen", "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Token-Nutzung =====
|
// ===== Token-Nutzung =====
|
||||||
async function loadOrgTokenUsage(orgId) {
|
async function loadOrgTokenUsage(orgId) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren