Phase 3a Frontend-Hygiene: Toast statt alert/confirm
- src/static/css/style.css: Toast-Styles (.toast-container, .toast,
Varianten info/success/warning/error, Animations)
- src/static/dashboard.html: <div id=toastContainer> vor </body>,
Cancel-Button im Confirm-Modal bekommt id=confirmCancelBtn
- src/static/js/app.js:
- showToast(msg, type) neu - links oben, autoclose 3.5s (error: 6s)
- showConfirm(title, text, callback?) jetzt Promise<boolean>-fähig
(Backwards-compat: Legacy-Callback wird bei OK weiter aufgerufen)
- Cancel/Close-Hooks am modalConfirm setzen Promise auf false
- alle 18 alert() in app.js / source-health.js / sources.js durch
showToast(msg, type) ersetzt (type je nach Kontext error/success/warning/info)
- 2 confirm() in source-health.js durch await showConfirm() ersetzt
Dieser Commit ist enthalten in:
@@ -790,3 +790,44 @@ tr:hover td {
|
|||||||
.audit-diff .diff-new { color: #2ecc71; word-break: break-word; }
|
.audit-diff .diff-new { color: #2ecc71; word-break: break-word; }
|
||||||
.token-budget-bar.over-limit { background: repeating-linear-gradient(45deg, #c0392b, #c0392b 6px, #962d22 6px, #962d22 12px); }
|
.token-budget-bar.over-limit { background: repeating-linear-gradient(45deg, #c0392b, #c0392b 6px, #962d22 6px, #962d22 12px); }
|
||||||
input[type="date"].filter-select { padding: 6px 10px; }
|
input[type="date"].filter-select { padding: 6px 10px; }
|
||||||
|
|
||||||
|
/* === Toast-Notifications (Phase 3) === */
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 24px;
|
||||||
|
right: 24px;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
max-width: 380px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.toast {
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||||
|
pointer-events: auto;
|
||||||
|
animation: toast-in 0.18s ease-out;
|
||||||
|
}
|
||||||
|
.toast.toast-out {
|
||||||
|
animation: toast-out 0.18s ease-in forwards;
|
||||||
|
}
|
||||||
|
.toast-info { border-left-color: #3b82f6; }
|
||||||
|
.toast-success { border-left-color: #10b981; }
|
||||||
|
.toast-warning { border-left-color: #f59e0b; }
|
||||||
|
.toast-error { border-left-color: #ef4444; }
|
||||||
|
@keyframes toast-in {
|
||||||
|
from { opacity: 0; transform: translateX(20px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
@keyframes toast-out {
|
||||||
|
from { opacity: 1; transform: translateX(0); }
|
||||||
|
to { opacity: 0; transform: translateX(20px); }
|
||||||
|
}
|
||||||
|
|||||||
@@ -672,7 +672,7 @@
|
|||||||
<p class="confirm-text" id="confirmText"></p>
|
<p class="confirm-text" id="confirmText"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-secondary" onclick="closeModal('modalConfirm')">Abbrechen</button>
|
<button class="btn btn-secondary" id="confirmCancelBtn" onclick="closeModal('modalConfirm')">Abbrechen</button>
|
||||||
<button class="btn btn-danger" id="confirmOkBtn">Bestätigen</button>
|
<button class="btn btn-danger" id="confirmOkBtn">Bestätigen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -682,5 +682,6 @@
|
|||||||
<script src="/static/js/sources.js"></script>
|
<script src="/static/js/sources.js"></script>
|
||||||
<script src="/static/js/source-health.js"></script>
|
<script src="/static/js/source-health.js"></script>
|
||||||
<script src="/static/js/audit.js"></script>
|
<script src="/static/js/audit.js"></script>
|
||||||
|
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ async function changeRole(userId, role) {
|
|||||||
try {
|
try {
|
||||||
await API.put(`/api/users/${userId}/role?role=${role}`);
|
await API.put(`/api/users/${userId}/role?role=${role}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
showToast(err.message, "error");
|
||||||
if (currentOrgId) loadOrgUsers(currentOrgId);
|
if (currentOrgId) loadOrgUsers(currentOrgId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ async function toggleUser(userId, activate) {
|
|||||||
await API.put(`/api/users/${userId}/${activate ? "activate" : "deactivate"}`);
|
await API.put(`/api/users/${userId}/${activate ? "activate" : "deactivate"}`);
|
||||||
if (currentOrgId) loadOrgUsers(currentOrgId);
|
if (currentOrgId) loadOrgUsers(currentOrgId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
showToast(err.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +287,7 @@ async function toggleGlobeAccess(userId) {
|
|||||||
await API.put("/api/users/" + userId + "/globe-access");
|
await API.put("/api/users/" + userId + "/globe-access");
|
||||||
if (currentOrgId) loadOrgUsers(currentOrgId);
|
if (currentOrgId) loadOrgUsers(currentOrgId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
showToast(err.message, "error");
|
||||||
if (currentOrgId) loadOrgUsers(currentOrgId);
|
if (currentOrgId) loadOrgUsers(currentOrgId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ async function toggleNetworkAccess(userId) {
|
|||||||
await API.put("/api/users/" + userId + "/network-access");
|
await API.put("/api/users/" + userId + "/network-access");
|
||||||
if (currentOrgId) loadOrgUsers(currentOrgId);
|
if (currentOrgId) loadOrgUsers(currentOrgId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
showToast(err.message, "error");
|
||||||
if (currentOrgId) loadOrgUsers(currentOrgId);
|
if (currentOrgId) loadOrgUsers(currentOrgId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,7 +311,7 @@ function confirmDeleteUser(userId, email) {
|
|||||||
await API.del(`/api/users/${userId}`);
|
await API.del(`/api/users/${userId}`);
|
||||||
if (currentOrgId) loadOrgUsers(currentOrgId);
|
if (currentOrgId) loadOrgUsers(currentOrgId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
showToast(err.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -353,7 +353,7 @@ async function extendLicense(licId) {
|
|||||||
await API.put(`/api/licenses/${licId}/extend?days=${parseInt(days)}`);
|
await API.put(`/api/licenses/${licId}/extend?days=${parseInt(days)}`);
|
||||||
if (currentOrgId) loadOrgLicenses(currentOrgId);
|
if (currentOrgId) loadOrgLicenses(currentOrgId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
showToast(err.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@ function confirmRevokeLicense(licId) {
|
|||||||
await API.put(`/api/licenses/${licId}/revoke`);
|
await API.put(`/api/licenses/${licId}/revoke`);
|
||||||
if (currentOrgId) loadOrgLicenses(currentOrgId);
|
if (currentOrgId) loadOrgLicenses(currentOrgId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
showToast(err.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -523,7 +523,7 @@ function setupForms() {
|
|||||||
loadOrgs();
|
loadOrgs();
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
showToast(err.message, "error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -541,7 +541,7 @@ function setupForms() {
|
|||||||
loadOrgs();
|
loadOrgs();
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
showToast(err.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -560,19 +560,56 @@ function closeModal(id) {
|
|||||||
// Confirm dialog
|
// Confirm dialog
|
||||||
let confirmCallback = null;
|
let confirmCallback = null;
|
||||||
|
|
||||||
|
let confirmResolver = null;
|
||||||
|
|
||||||
function showConfirm(title, text, callback) {
|
function showConfirm(title, text, callback) {
|
||||||
document.getElementById("confirmTitle").textContent = title;
|
document.getElementById("confirmTitle").textContent = title;
|
||||||
document.getElementById("confirmText").textContent = text;
|
document.getElementById("confirmText").textContent = text;
|
||||||
confirmCallback = callback;
|
// Backward-compat: legacy Callback wird bei OK aufgerufen
|
||||||
|
confirmCallback = callback || null;
|
||||||
openModal("modalConfirm");
|
openModal("modalConfirm");
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (confirmResolver) confirmResolver(false); // alten Resolver schliessen
|
||||||
|
confirmResolver = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(msg, type) {
|
||||||
|
type = type || "info";
|
||||||
|
const c = document.getElementById("toastContainer");
|
||||||
|
if (!c) { console.log("[toast]", type, msg); return; }
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "toast toast-" + type;
|
||||||
|
el.textContent = msg;
|
||||||
|
c.appendChild(el);
|
||||||
|
setTimeout(() => {
|
||||||
|
el.classList.add("toast-out");
|
||||||
|
setTimeout(() => el.remove(), 220);
|
||||||
|
}, type === "error" ? 6000 : 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
document.getElementById("confirmOkBtn").addEventListener("click", async () => {
|
document.getElementById("confirmOkBtn").addEventListener("click", async () => {
|
||||||
closeModal("modalConfirm");
|
closeModal("modalConfirm");
|
||||||
if (confirmCallback) await confirmCallback();
|
const cb = confirmCallback;
|
||||||
|
const rs = confirmResolver;
|
||||||
confirmCallback = null;
|
confirmCallback = null;
|
||||||
|
confirmResolver = null;
|
||||||
|
if (cb) {
|
||||||
|
try { await cb(); } catch (e) { showToast(e.message || String(e), "error"); }
|
||||||
|
}
|
||||||
|
if (rs) rs(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cancel/Close -> resolver(false)
|
||||||
|
function _confirmCancel() {
|
||||||
|
const rs = confirmResolver;
|
||||||
|
confirmCallback = null;
|
||||||
|
confirmResolver = null;
|
||||||
|
if (rs) rs(false);
|
||||||
|
}
|
||||||
|
document.getElementById("confirmCancelBtn")?.addEventListener("click", _confirmCancel);
|
||||||
|
document.querySelector("#modalConfirm .modal-close")?.addEventListener("click", _confirmCancel);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Utilities ---
|
// --- Utilities ---
|
||||||
|
|||||||
@@ -202,18 +202,19 @@ async function handleSuggestion(id, accept) {
|
|||||||
const suggestion = suggestionsCache.find((s) => s.id === id);
|
const suggestion = suggestionsCache.find((s) => s.id === id);
|
||||||
if (!suggestion) return;
|
if (!suggestion) return;
|
||||||
|
|
||||||
if (!confirm(`Vorschlag "${suggestion.title}" ${action}?`)) return;
|
const ok = await showConfirm("Vorschlag " + (action === "annehmen" ? "annehmen" : "ablehnen"), `Soll "${suggestion.title}" ${action}?`);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await API.put("/api/sources/suggestions/" + id, { accept });
|
const result = await API.put("/api/sources/suggestions/" + id, { accept });
|
||||||
if (result.action) {
|
if (result.action) {
|
||||||
alert(`Ergebnis: ${result.action}`);
|
showToast("Ergebnis: " + result.action, "success");
|
||||||
}
|
}
|
||||||
loadHealthData();
|
loadHealthData();
|
||||||
// Grundquellen-Liste auch aktualisieren
|
// Grundquellen-Liste auch aktualisieren
|
||||||
if (typeof loadGlobalSources === "function") loadGlobalSources();
|
if (typeof loadGlobalSources === "function") loadGlobalSources();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Fehler: " + err.message);
|
showToast("Fehler: " + err.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +323,8 @@ function formatDateTime(dateStr) {
|
|||||||
async function searchFix(btn) {
|
async function searchFix(btn) {
|
||||||
const sourceId = btn.dataset.sourceId;
|
const sourceId = btn.dataset.sourceId;
|
||||||
const sourceName = btn.dataset.sourceName;
|
const sourceName = btn.dataset.sourceName;
|
||||||
if (!confirm(`Sonnet mit WebSearch nach einer Lösung für "${sourceName}" suchen lassen?\n\nDas kann einige Minuten dauern.`)) return;
|
const ok = await showConfirm("Lösung suchen", `Sonnet mit WebSearch nach einer Lösung für "${sourceName}" suchen lassen? Das kann einige Minuten dauern.`);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = "Sucht...";
|
btn.textContent = "Sucht...";
|
||||||
@@ -337,10 +339,10 @@ async function searchFix(btn) {
|
|||||||
if (result.cost_usd) {
|
if (result.cost_usd) {
|
||||||
msg += `\n\nKosten: $${result.cost_usd.toFixed(2)}`;
|
msg += `\n\nKosten: $${result.cost_usd.toFixed(2)}`;
|
||||||
}
|
}
|
||||||
alert(msg);
|
showToast(msg, "info");
|
||||||
loadHealthData();
|
loadHealthData();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Fehler: " + err.message);
|
showToast("Fehler: " + err.message, "error");
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = "Lösung suchen";
|
btn.textContent = "Lösung suchen";
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ function confirmDeleteGlobalSource(id, name) {
|
|||||||
await API.del("/api/sources/global/" + id);
|
await API.del("/api/sources/global/" + id);
|
||||||
loadGlobalSources();
|
loadGlobalSources();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
showToast(err.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -320,7 +320,7 @@ function promoteSource(id, name) {
|
|||||||
await API.post("/api/sources/tenant/" + id + "/promote");
|
await API.post("/api/sources/tenant/" + id + "/promote");
|
||||||
loadTenantSources();
|
loadTenantSources();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
showToast(err.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -399,7 +399,7 @@ async function addDiscoveredFeeds() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (selected.length === 0) {
|
if (selected.length === 0) {
|
||||||
alert("Keine Feeds ausgewählt");
|
showToast("Keine Feeds ausgewählt", "warning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,9 +411,9 @@ async function addDiscoveredFeeds() {
|
|||||||
const result = await API.post("/api/sources/discover/add", selected);
|
const result = await API.post("/api/sources/discover/add", selected);
|
||||||
closeModal("modalDiscover");
|
closeModal("modalDiscover");
|
||||||
loadGlobalSources();
|
loadGlobalSources();
|
||||||
alert(result.added + " Grundquelle(n) hinzugefügt" + (result.skipped ? ", " + result.skipped + " übersprungen" : ""));
|
showToast(result.added + " Grundquelle(n) hinzugefügt" + (result.skipped ? ", " + result.skipped + " übersprungen" : ""), "success");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Fehler: " + err.message);
|
showToast("Fehler: " + err.message, "error");
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = "Ausgewählte hinzufügen";
|
btn.textContent = "Ausgewählte hinzufügen";
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren