test(frontend): add vitest test suite and apply code formatting
This commit is contained in:
1 parent
e4ffa0b356
commit
6632df0bc7
23 files changed
+2209
-815
No files matched your search
+148
-245
@@ -19,18 +19,12 @@ type MediaEntry = {
|
||||
};
|
||||
|
||||
const bodyDataset = document.body.dataset;
|
||||
const moderationLogLimit = Number.parseInt(
|
||||
bodyDataset.adminModerationLogLimit || "",
|
||||
10,
|
||||
);
|
||||
const moderationLogLimit = Number.parseInt(bodyDataset.adminModerationLogLimit || "", 10);
|
||||
|
||||
const adminConfig = {
|
||||
defaultModerator: bodyDataset.adminDefaultModerator || "admin",
|
||||
defaultRoom: bodyDataset.adminDefaultRoom || "main",
|
||||
moderationLogLimit:
|
||||
Number.isFinite(moderationLogLimit) && moderationLogLimit > 0
|
||||
? moderationLogLimit
|
||||
: 50,
|
||||
moderationLogLimit: Number.isFinite(moderationLogLimit) && moderationLogLimit > 0 ? moderationLogLimit : 50,
|
||||
};
|
||||
|
||||
const MAX_UPLOAD_BYTES = 2 * 1024 * 1024 * 1024;
|
||||
@@ -43,16 +37,11 @@ const state = {
|
||||
playlistSaving: false,
|
||||
};
|
||||
|
||||
const uploadProgressWrap = () =>
|
||||
document.getElementById("media-upload-progress-wrap") as HTMLElement | null;
|
||||
const uploadProgressWrap = () => document.getElementById("media-upload-progress-wrap") as HTMLElement | null;
|
||||
|
||||
const uploadProgressBar = () =>
|
||||
document.getElementById(
|
||||
"media-upload-progress",
|
||||
) as HTMLProgressElement | null;
|
||||
const uploadProgressBar = () => document.getElementById("media-upload-progress") as HTMLProgressElement | null;
|
||||
|
||||
const uploadProgressText = () =>
|
||||
document.getElementById("media-upload-progress-text") as HTMLElement | null;
|
||||
const uploadProgressText = () => document.getElementById("media-upload-progress-text") as HTMLElement | null;
|
||||
|
||||
const setUploadProgress = (value: number, total: number, label: string) => {
|
||||
const wrap = uploadProgressWrap();
|
||||
@@ -73,11 +62,9 @@ const hideUploadProgress = () => {
|
||||
if (wrap) wrap.hidden = true;
|
||||
};
|
||||
|
||||
const getFileFingerprint = (file: File) =>
|
||||
`${file.name}:${file.size}:${file.lastModified}`;
|
||||
const getFileFingerprint = (file: File) => `${file.name}:${file.size}:${file.lastModified}`;
|
||||
|
||||
const getResumeStorageKey = (fingerprint: string) =>
|
||||
`${UPLOAD_RESUME_KEY_PREFIX}${fingerprint}`;
|
||||
const getResumeStorageKey = (fingerprint: string) => `${UPLOAD_RESUME_KEY_PREFIX}${fingerprint}`;
|
||||
|
||||
const getStoredUploadId = (fingerprint: string) => {
|
||||
try {
|
||||
@@ -221,10 +208,7 @@ async function refreshAnalytics() {
|
||||
setText("analytics-total-messages", String(payload.totalMessages ?? 0));
|
||||
setText("analytics-messages-24h", String(payload.messagesLast24Hours ?? 0));
|
||||
setText("analytics-distinct-rooms", String(payload.distinctRooms ?? 0));
|
||||
setText(
|
||||
"analytics-moderation-actions",
|
||||
String(payload.moderationActions ?? 0),
|
||||
);
|
||||
setText("analytics-moderation-actions", String(payload.moderationActions ?? 0));
|
||||
setText("analytics-generated-at", payload.generatedAt || "unavailable");
|
||||
|
||||
const tbody = document.getElementById("analytics-table-body");
|
||||
@@ -260,9 +244,7 @@ async function refreshPlaylist() {
|
||||
|
||||
const payload = await response.json();
|
||||
const content: string = payload.content || "";
|
||||
const textarea = document.getElementById(
|
||||
"playlist-content",
|
||||
) as HTMLTextAreaElement | null;
|
||||
const textarea = document.getElementById("playlist-content") as HTMLTextAreaElement | null;
|
||||
if (textarea) textarea.value = content;
|
||||
renderPlaylistSortable(content);
|
||||
await showPlaylistWarnings(content);
|
||||
@@ -279,7 +261,10 @@ function renderPlaylistSortable(content: string) {
|
||||
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
||||
list.innerHTML = lines
|
||||
.map((line, i) => {
|
||||
const path = line.replace(/^file\s+/, "").replace(/['"]/g, "").trim();
|
||||
const path = line
|
||||
.replace(/^file\s+/, "")
|
||||
.replace(/['"]/g, "")
|
||||
.trim();
|
||||
return `<li class="sortable-item" draggable="true" data-index="${i}">
|
||||
<span class="drag-handle" aria-hidden="true">⋮⋮</span>
|
||||
<span class="sortable-index">${i + 1}</span>
|
||||
@@ -348,12 +333,17 @@ async function showPlaylistWarnings(content: string) {
|
||||
const mediaPaths = new Set((media.files || []).map((f: { path: string }) => f.path));
|
||||
const nonNorm = (media.files || []).filter((f: { status: string }) => f.status === "needs_normalization");
|
||||
for (const line of lines) {
|
||||
const path = line.replace(/^file\s+/, "").replace(/['"]/g, "").trim();
|
||||
const path = line
|
||||
.replace(/^file\s+/, "")
|
||||
.replace(/['"]/g, "")
|
||||
.trim();
|
||||
if (path && !mediaPaths.has(path)) msgs.push(`Missing from media library: ${path}`);
|
||||
}
|
||||
if (nonNorm.length > 0) msgs.push(`${nonNorm.length} file(s) need normalization. Run scripts/normalize.sh.`);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
if (msgs.length > 0) {
|
||||
warnings.innerHTML = msgs.map((m) => `<div class="warning-msg">⚠ ${m}</div>`).join("");
|
||||
warnings.hidden = false;
|
||||
@@ -368,9 +358,7 @@ async function savePlaylist() {
|
||||
setStatus("playlist-status", "Saving playlist...");
|
||||
|
||||
try {
|
||||
const textarea = document.getElementById(
|
||||
"playlist-content",
|
||||
) as HTMLTextAreaElement | null;
|
||||
const textarea = document.getElementById("playlist-content") as HTMLTextAreaElement | null;
|
||||
const content = textarea?.value || "";
|
||||
|
||||
const response = await fetch("/api/admin/playlist", {
|
||||
@@ -415,10 +403,7 @@ async function refreshSchedule() {
|
||||
});
|
||||
}
|
||||
|
||||
setStatus(
|
||||
"schedule-status",
|
||||
`Schedule loaded. Total runtime ${payload.totalRuntime || "00:00:00"}.`,
|
||||
);
|
||||
setStatus("schedule-status", `Schedule loaded. Total runtime ${payload.totalRuntime || "00:00:00"}.`);
|
||||
} catch (error) {
|
||||
console.error("Failed to load schedule", error);
|
||||
setStatus("schedule-status", "Failed to load schedule.", true);
|
||||
@@ -445,7 +430,9 @@ async function refreshOverrides() {
|
||||
tbody.querySelectorAll("[data-delete-override]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => deleteOverride(Number((btn as HTMLElement).dataset.deleteOverride)));
|
||||
});
|
||||
} catch { /* non-critical */ }
|
||||
} catch {
|
||||
/* non-critical */
|
||||
}
|
||||
}
|
||||
|
||||
async function submitOverride(e: Event) {
|
||||
@@ -460,18 +447,37 @@ async function submitOverride(e: Event) {
|
||||
const resp = await fetch("/api/admin/schedule/overrides", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ mediaId: Number(mediaId), startTime: new Date(startTime).toISOString(), endTime: new Date(endTime).toISOString(), reason }),
|
||||
body: JSON.stringify({
|
||||
mediaId: Number(mediaId),
|
||||
startTime: new Date(startTime).toISOString(),
|
||||
endTime: new Date(endTime).toISOString(),
|
||||
reason,
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) { const text = await resp.text(); throw new Error(text); }
|
||||
if (status) { status.textContent = "Override added."; status.className = "status-line"; }
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
if (status) {
|
||||
status.textContent = "Override added.";
|
||||
status.className = "status-line";
|
||||
}
|
||||
refreshOverrides();
|
||||
} catch (err) {
|
||||
if (status) { status.textContent = `Failed: ${err}`; status.className = "status-line status-error"; }
|
||||
if (status) {
|
||||
status.textContent = `Failed: ${err}`;
|
||||
status.className = "status-line status-error";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteOverride(id: number) {
|
||||
try { await fetch(`/api/admin/schedule/overrides?id=${id}`, { method: "DELETE" }); refreshOverrides(); } catch { /* ignore */ }
|
||||
try {
|
||||
await fetch(`/api/admin/schedule/overrides?id=${id}`, { method: "DELETE" });
|
||||
refreshOverrides();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSystemSummary() {
|
||||
@@ -540,12 +546,8 @@ function renderMediaRows(files: MediaEntry[]) {
|
||||
<button type="button" class="btn btn-danger btn-small" data-action="delete" data-id="${file.id || ""}">Delete</button>
|
||||
</td>
|
||||
`;
|
||||
tr.querySelector('[data-action="edit"]')?.addEventListener("click", () =>
|
||||
editMedia(file),
|
||||
);
|
||||
tr.querySelector('[data-action="delete"]')?.addEventListener("click", () =>
|
||||
deleteMedia(file),
|
||||
);
|
||||
tr.querySelector('[data-action="edit"]')?.addEventListener("click", () => editMedia(file));
|
||||
tr.querySelector('[data-action="delete"]')?.addEventListener("click", () => deleteMedia(file));
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
@@ -612,18 +614,11 @@ async function uploadVideo(file: File) {
|
||||
|
||||
const alreadyUploaded = Number(initPayload.uploadedChunks || 0);
|
||||
const effectiveTotalChunks = Number(initPayload.totalChunks || totalChunks);
|
||||
const resumeFrom = Math.min(
|
||||
Math.max(alreadyUploaded, 0),
|
||||
effectiveTotalChunks,
|
||||
);
|
||||
const resumeFrom = Math.min(Math.max(alreadyUploaded, 0), effectiveTotalChunks);
|
||||
|
||||
setUploadProgress(resumeFrom, effectiveTotalChunks, "Uploading chunks");
|
||||
|
||||
for (
|
||||
let chunkIndex = resumeFrom;
|
||||
chunkIndex < effectiveTotalChunks;
|
||||
chunkIndex += 1
|
||||
) {
|
||||
for (let chunkIndex = resumeFrom; chunkIndex < effectiveTotalChunks; chunkIndex += 1) {
|
||||
const start = chunkIndex * UPLOAD_CHUNK_BYTES;
|
||||
const end = Math.min(file.size, start + UPLOAD_CHUNK_BYTES);
|
||||
const chunk = file.slice(start, end);
|
||||
@@ -641,23 +636,13 @@ async function uploadVideo(file: File) {
|
||||
throw new Error(await readErrorMessage(chunkResponse));
|
||||
}
|
||||
|
||||
setUploadProgress(
|
||||
chunkIndex + 1,
|
||||
effectiveTotalChunks,
|
||||
"Uploading chunks",
|
||||
);
|
||||
setStatus(
|
||||
"media-status",
|
||||
`Uploading ${file.name} (${chunkIndex + 1}/${effectiveTotalChunks} chunks)...`,
|
||||
);
|
||||
setUploadProgress(chunkIndex + 1, effectiveTotalChunks, "Uploading chunks");
|
||||
setStatus("media-status", `Uploading ${file.name} (${chunkIndex + 1}/${effectiveTotalChunks} chunks)...`);
|
||||
}
|
||||
|
||||
const completeResponse = await fetch(
|
||||
`/api/admin/media/uploads/${encodeURIComponent(uploadId)}/complete`,
|
||||
{
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
const completeResponse = await fetch(`/api/admin/media/uploads/${encodeURIComponent(uploadId)}/complete`, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!completeResponse.ok) {
|
||||
throw new Error(await readErrorMessage(completeResponse));
|
||||
@@ -674,13 +659,8 @@ async function uploadVideo(file: File) {
|
||||
await refreshSchedule();
|
||||
} catch (error) {
|
||||
console.error("Failed to upload media", error);
|
||||
const details =
|
||||
error instanceof Error ? error.message : "Unknown upload error.";
|
||||
setStatus(
|
||||
"media-status",
|
||||
`Upload interrupted. Re-select the same file to resume. (${details})`,
|
||||
true,
|
||||
);
|
||||
const details = error instanceof Error ? error.message : "Unknown upload error.";
|
||||
setStatus("media-status", `Upload interrupted. Re-select the same file to resume. (${details})`, true);
|
||||
} finally {
|
||||
state.mediaUploading = false;
|
||||
setTimeout(() => hideUploadProgress(), 1200);
|
||||
@@ -749,9 +729,7 @@ async function deleteMedia(file: MediaEntry) {
|
||||
async function refreshModerationLogs() {
|
||||
setStatus("moderation-status", "Loading moderation logs...");
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/admin/moderation/logs?limit=${adminConfig.moderationLogLimit}`,
|
||||
);
|
||||
const response = await fetch(`/api/admin/moderation/logs?limit=${adminConfig.moderationLogLimit}`);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const payload = await response.json();
|
||||
@@ -787,10 +765,8 @@ async function submitModerationAction(event: Event) {
|
||||
const payload = {
|
||||
action: (document.getElementById("mod-action") as HTMLSelectElement).value,
|
||||
target: (document.getElementById("mod-target") as HTMLInputElement).value,
|
||||
moderator: (document.getElementById("mod-moderator") as HTMLInputElement)
|
||||
.value,
|
||||
reason: (document.getElementById("mod-reason") as HTMLTextAreaElement)
|
||||
.value,
|
||||
moderator: (document.getElementById("mod-moderator") as HTMLInputElement).value,
|
||||
reason: (document.getElementById("mod-reason") as HTMLTextAreaElement).value,
|
||||
room: (document.getElementById("mod-room") as HTMLInputElement).value,
|
||||
};
|
||||
|
||||
@@ -865,10 +841,7 @@ async function refreshChatLog() {
|
||||
renderChatLogTable();
|
||||
updateChatLogPagination();
|
||||
updateBulkDeleteButton();
|
||||
setStatus(
|
||||
"chat-log-status",
|
||||
`Loaded ${chatLogState.entries.length} entries.`,
|
||||
);
|
||||
setStatus("chat-log-status", `Loaded ${chatLogState.entries.length} entries.`);
|
||||
} catch (error) {
|
||||
console.error("Failed to load chat log", error);
|
||||
setStatus("chat-log-status", "Failed to load chat log.", true);
|
||||
@@ -908,10 +881,7 @@ function renderChatLogTable() {
|
||||
tr.querySelector('[data-action="chat-ban"]')?.addEventListener("click", () => {
|
||||
if (confirm(`Ban ${entry.username}?`)) userAction("ban", entry.username);
|
||||
});
|
||||
tr.querySelector('[data-action="delete-chat"]')?.addEventListener(
|
||||
"click",
|
||||
() => deleteChatEntry(entry.id),
|
||||
);
|
||||
tr.querySelector('[data-action="delete-chat"]')?.addEventListener("click", () => deleteChatEntry(entry.id));
|
||||
tr.querySelector(".chat-log-checkbox")?.addEventListener("change", (e) => {
|
||||
const cb = e.target as HTMLInputElement;
|
||||
if (cb.checked) {
|
||||
@@ -929,38 +899,27 @@ function renderChatLogTable() {
|
||||
}
|
||||
|
||||
function updateSelectAllCheckbox() {
|
||||
const selectAll = document.getElementById(
|
||||
"chat-log-select-all",
|
||||
) as HTMLInputElement | null;
|
||||
const selectAll = document.getElementById("chat-log-select-all") as HTMLInputElement | null;
|
||||
if (!selectAll) return;
|
||||
const allSelected =
|
||||
chatLogState.entries.length > 0 &&
|
||||
chatLogState.entries.every((e) => chatLogState.selected.has(e.id));
|
||||
chatLogState.entries.length > 0 && chatLogState.entries.every((e) => chatLogState.selected.has(e.id));
|
||||
selectAll.checked = allSelected;
|
||||
}
|
||||
|
||||
function updateBulkDeleteButton() {
|
||||
const btn = document.getElementById(
|
||||
"bulk-delete-chat-log-btn",
|
||||
) as HTMLButtonElement | null;
|
||||
const btn = document.getElementById("bulk-delete-chat-log-btn") as HTMLButtonElement | null;
|
||||
if (!btn) return;
|
||||
btn.disabled = chatLogState.selected.size === 0;
|
||||
}
|
||||
|
||||
function updateChatLogPagination() {
|
||||
const prevBtn = document.getElementById(
|
||||
"chat-log-prev-btn",
|
||||
) as HTMLButtonElement | null;
|
||||
const nextBtn = document.getElementById(
|
||||
"chat-log-next-btn",
|
||||
) as HTMLButtonElement | null;
|
||||
const prevBtn = document.getElementById("chat-log-prev-btn") as HTMLButtonElement | null;
|
||||
const nextBtn = document.getElementById("chat-log-next-btn") as HTMLButtonElement | null;
|
||||
const info = document.getElementById("chat-log-page-info");
|
||||
|
||||
if (prevBtn) prevBtn.disabled = !chatLogState.before;
|
||||
if (nextBtn)
|
||||
nextBtn.disabled = chatLogState.entries.length < chatLogState.limit;
|
||||
if (info)
|
||||
info.textContent = `Showing ${chatLogState.entries.length} of ${chatLogState.total}`;
|
||||
if (nextBtn) nextBtn.disabled = chatLogState.entries.length < chatLogState.limit;
|
||||
if (info) info.textContent = `Showing ${chatLogState.entries.length} of ${chatLogState.total}`;
|
||||
}
|
||||
|
||||
function chatLogNextPage() {
|
||||
@@ -975,29 +934,17 @@ function chatLogPrevPage() {
|
||||
}
|
||||
|
||||
function chatLogApplyFilter() {
|
||||
chatLogState.room =
|
||||
(document.getElementById("chat-log-filter-room") as HTMLInputElement)
|
||||
?.value || "";
|
||||
chatLogState.username =
|
||||
(document.getElementById("chat-log-filter-username") as HTMLInputElement)
|
||||
?.value || "";
|
||||
chatLogState.text =
|
||||
(document.getElementById("chat-log-filter-text") as HTMLInputElement)
|
||||
?.value || "";
|
||||
chatLogState.room = (document.getElementById("chat-log-filter-room") as HTMLInputElement)?.value || "";
|
||||
chatLogState.username = (document.getElementById("chat-log-filter-username") as HTMLInputElement)?.value || "";
|
||||
chatLogState.text = (document.getElementById("chat-log-filter-text") as HTMLInputElement)?.value || "";
|
||||
chatLogState.before = "";
|
||||
refreshChatLog();
|
||||
}
|
||||
|
||||
function chatLogClearFilter() {
|
||||
const roomEl = document.getElementById(
|
||||
"chat-log-filter-room",
|
||||
) as HTMLInputElement | null;
|
||||
const userEl = document.getElementById(
|
||||
"chat-log-filter-username",
|
||||
) as HTMLInputElement | null;
|
||||
const textEl = document.getElementById(
|
||||
"chat-log-filter-text",
|
||||
) as HTMLInputElement | null;
|
||||
const roomEl = document.getElementById("chat-log-filter-room") as HTMLInputElement | null;
|
||||
const userEl = document.getElementById("chat-log-filter-username") as HTMLInputElement | null;
|
||||
const textEl = document.getElementById("chat-log-filter-text") as HTMLInputElement | null;
|
||||
if (roomEl) roomEl.value = "";
|
||||
if (userEl) userEl.value = "";
|
||||
if (textEl) textEl.value = "";
|
||||
@@ -1009,8 +956,7 @@ function chatLogClearFilter() {
|
||||
}
|
||||
|
||||
async function deleteChatEntry(id: number) {
|
||||
if (!window.confirm(`Delete chat message #${id}? This cannot be undone.`))
|
||||
return;
|
||||
if (!window.confirm(`Delete chat message #${id}? This cannot be undone.`)) return;
|
||||
|
||||
setStatus("chat-log-status", `Deleting message #${id}...`);
|
||||
try {
|
||||
@@ -1030,17 +976,9 @@ async function deleteChatEntry(id: number) {
|
||||
async function bulkDeleteChatLog() {
|
||||
if (chatLogState.selected.size === 0) return;
|
||||
|
||||
if (
|
||||
!window.confirm(
|
||||
`Delete ${chatLogState.selected.size} selected messages? This cannot be undone.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
if (!window.confirm(`Delete ${chatLogState.selected.size} selected messages? This cannot be undone.`)) return;
|
||||
|
||||
setStatus(
|
||||
"chat-log-status",
|
||||
`Deleting ${chatLogState.selected.size} messages...`,
|
||||
);
|
||||
setStatus("chat-log-status", `Deleting ${chatLogState.selected.size} messages...`);
|
||||
try {
|
||||
const ids = Array.from(chatLogState.selected);
|
||||
const response = await fetch("/api/admin/chat-log/bulk", {
|
||||
@@ -1050,10 +988,7 @@ async function bulkDeleteChatLog() {
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const payload = await response.json();
|
||||
setStatus(
|
||||
"chat-log-status",
|
||||
`${payload.deleted || ids.length} messages deleted.`,
|
||||
);
|
||||
setStatus("chat-log-status", `${payload.deleted || ids.length} messages deleted.`);
|
||||
await refreshChatLog();
|
||||
await refreshOverview();
|
||||
} catch (error) {
|
||||
@@ -1088,17 +1023,23 @@ async function refreshUsers() {
|
||||
</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
tbody.querySelectorAll(".user-mute").forEach(b => b.addEventListener("click", () => userAction("mute", (b as HTMLElement).dataset.user!)));
|
||||
tbody.querySelectorAll(".user-ban").forEach(b => b.addEventListener("click", () => userAction("ban", (b as HTMLElement).dataset.user!)));
|
||||
tbody.querySelectorAll(".user-unmute").forEach(b => b.addEventListener("click", () => userAction("unmute", (b as HTMLElement).dataset.user!)));
|
||||
tbody.querySelectorAll(".user-unban").forEach(b => b.addEventListener("click", () => userAction("unban", (b as HTMLElement).dataset.user!)));
|
||||
tbody
|
||||
.querySelectorAll(".user-mute")
|
||||
.forEach((b) => b.addEventListener("click", () => userAction("mute", (b as HTMLElement).dataset.user!)));
|
||||
tbody
|
||||
.querySelectorAll(".user-ban")
|
||||
.forEach((b) => b.addEventListener("click", () => userAction("ban", (b as HTMLElement).dataset.user!)));
|
||||
tbody
|
||||
.querySelectorAll(".user-unmute")
|
||||
.forEach((b) => b.addEventListener("click", () => userAction("unmute", (b as HTMLElement).dataset.user!)));
|
||||
tbody
|
||||
.querySelectorAll(".user-unban")
|
||||
.forEach((b) => b.addEventListener("click", () => userAction("unban", (b as HTMLElement).dataset.user!)));
|
||||
}
|
||||
|
||||
setStatus(
|
||||
"users-status",
|
||||
payload.redisEnabled
|
||||
? "Active users loaded."
|
||||
: "Redis disabled; no live presence data available.",
|
||||
payload.redisEnabled ? "Active users loaded." : "Redis disabled; no live presence data available.",
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to load users", error);
|
||||
@@ -1127,7 +1068,9 @@ async function loadConfig() {
|
||||
const cfg = await resp.json();
|
||||
(document.getElementById("config-rate-max") as HTMLInputElement).value = cfg.rateLimitMax;
|
||||
(document.getElementById("config-rate-window") as HTMLInputElement).value = cfg.rateLimitWindowSec;
|
||||
} catch { /* non-critical */ }
|
||||
} catch {
|
||||
/* non-critical */
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig(e: Event) {
|
||||
@@ -1144,9 +1087,15 @@ async function saveConfig(e: Event) {
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
if (status) { status.textContent = "Config saved."; status.className = "status-line"; }
|
||||
if (status) {
|
||||
status.textContent = "Config saved.";
|
||||
status.className = "status-line";
|
||||
}
|
||||
} catch (err) {
|
||||
if (status) { status.textContent = `Failed: ${err}`; status.className = "status-line status-error"; }
|
||||
if (status) {
|
||||
status.textContent = `Failed: ${err}`;
|
||||
status.className = "status-line status-error";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1164,14 +1113,12 @@ async function restartService(service: string) {
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
document
|
||||
.querySelectorAll<HTMLButtonElement>(".tab-trigger")
|
||||
.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const tab = button.dataset.tab;
|
||||
if (tab) switchTab(tab);
|
||||
});
|
||||
document.querySelectorAll<HTMLButtonElement>(".tab-trigger").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const tab = button.dataset.tab;
|
||||
if (tab) switchTab(tab);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("logout-btn")?.addEventListener("click", async () => {
|
||||
try {
|
||||
@@ -1186,17 +1133,11 @@ function bindEvents() {
|
||||
function bindPageEvents(tabName: string) {
|
||||
switch (tabName) {
|
||||
case "upload":
|
||||
document
|
||||
.getElementById("reload-media-btn")
|
||||
?.addEventListener("click", refreshMediaLibrary);
|
||||
document.getElementById("reload-media-btn")?.addEventListener("click", refreshMediaLibrary);
|
||||
|
||||
const uploadInput = document.getElementById(
|
||||
"video-upload",
|
||||
) as HTMLInputElement | null;
|
||||
const uploadInput = document.getElementById("video-upload") as HTMLInputElement | null;
|
||||
const uploadDropzone = document.getElementById("upload-dropzone");
|
||||
document
|
||||
.getElementById("upload-trigger-btn")
|
||||
?.addEventListener("click", () => uploadInput?.click());
|
||||
document.getElementById("upload-trigger-btn")?.addEventListener("click", () => uploadInput?.click());
|
||||
|
||||
uploadDropzone?.addEventListener("dragover", (event) => {
|
||||
event.preventDefault();
|
||||
@@ -1221,99 +1162,61 @@ function bindPageEvents(tabName: string) {
|
||||
break;
|
||||
|
||||
case "playlist":
|
||||
document
|
||||
.getElementById("reload-playlist-btn")
|
||||
?.addEventListener("click", refreshPlaylist);
|
||||
document
|
||||
.getElementById("save-playlist-btn")
|
||||
?.addEventListener("click", savePlaylist);
|
||||
document.getElementById("reload-playlist-btn")?.addEventListener("click", refreshPlaylist);
|
||||
document.getElementById("save-playlist-btn")?.addEventListener("click", savePlaylist);
|
||||
break;
|
||||
|
||||
case "schedule":
|
||||
document
|
||||
.getElementById("reload-schedule-btn")
|
||||
?.addEventListener("click", refreshSchedule);
|
||||
document
|
||||
.getElementById("override-form")
|
||||
?.addEventListener("submit", submitOverride);
|
||||
document.getElementById("reload-schedule-btn")?.addEventListener("click", refreshSchedule);
|
||||
document.getElementById("override-form")?.addEventListener("submit", submitOverride);
|
||||
refreshOverrides();
|
||||
break;
|
||||
|
||||
case "moderation":
|
||||
document
|
||||
.getElementById("reload-moderation-btn")
|
||||
?.addEventListener("click", refreshModerationLogs);
|
||||
document
|
||||
.getElementById("export-chat-log-btn")
|
||||
?.addEventListener("click", () => {
|
||||
window.open("/api/admin/chat-log/export", "_blank");
|
||||
});
|
||||
document
|
||||
.getElementById("moderation-form")
|
||||
?.addEventListener("submit", submitModerationAction);
|
||||
(document.getElementById("mod-moderator") as HTMLInputElement).value =
|
||||
adminConfig.defaultModerator;
|
||||
(document.getElementById("mod-room") as HTMLInputElement).value =
|
||||
adminConfig.defaultRoom;
|
||||
document.getElementById("reload-moderation-btn")?.addEventListener("click", refreshModerationLogs);
|
||||
document.getElementById("export-chat-log-btn")?.addEventListener("click", () => {
|
||||
window.open("/api/admin/chat-log/export", "_blank");
|
||||
});
|
||||
document.getElementById("moderation-form")?.addEventListener("submit", submitModerationAction);
|
||||
(document.getElementById("mod-moderator") as HTMLInputElement).value = adminConfig.defaultModerator;
|
||||
(document.getElementById("mod-room") as HTMLInputElement).value = adminConfig.defaultRoom;
|
||||
break;
|
||||
|
||||
case "chat-log":
|
||||
document
|
||||
.getElementById("reload-chat-log-btn")
|
||||
?.addEventListener("click", refreshChatLog);
|
||||
document
|
||||
.getElementById("bulk-delete-chat-log-btn")
|
||||
?.addEventListener("click", bulkDeleteChatLog);
|
||||
document
|
||||
.getElementById("chat-log-filter-btn")
|
||||
?.addEventListener("click", chatLogApplyFilter);
|
||||
document
|
||||
.getElementById("chat-log-clear-filter-btn")
|
||||
?.addEventListener("click", chatLogClearFilter);
|
||||
document
|
||||
.getElementById("chat-log-prev-btn")
|
||||
?.addEventListener("click", chatLogPrevPage);
|
||||
document
|
||||
.getElementById("chat-log-next-btn")
|
||||
?.addEventListener("click", chatLogNextPage);
|
||||
document
|
||||
.getElementById("chat-log-select-all")
|
||||
?.addEventListener("change", (e) => {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
chatLogState.entries.forEach((entry) => {
|
||||
if (checked) {
|
||||
chatLogState.selected.add(entry.id);
|
||||
} else {
|
||||
chatLogState.selected.delete(entry.id);
|
||||
}
|
||||
});
|
||||
updateBulkDeleteButton();
|
||||
renderChatLogTable();
|
||||
document.getElementById("reload-chat-log-btn")?.addEventListener("click", refreshChatLog);
|
||||
document.getElementById("bulk-delete-chat-log-btn")?.addEventListener("click", bulkDeleteChatLog);
|
||||
document.getElementById("chat-log-filter-btn")?.addEventListener("click", chatLogApplyFilter);
|
||||
document.getElementById("chat-log-clear-filter-btn")?.addEventListener("click", chatLogClearFilter);
|
||||
document.getElementById("chat-log-prev-btn")?.addEventListener("click", chatLogPrevPage);
|
||||
document.getElementById("chat-log-next-btn")?.addEventListener("click", chatLogNextPage);
|
||||
document.getElementById("chat-log-select-all")?.addEventListener("change", (e) => {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
chatLogState.entries.forEach((entry) => {
|
||||
if (checked) {
|
||||
chatLogState.selected.add(entry.id);
|
||||
} else {
|
||||
chatLogState.selected.delete(entry.id);
|
||||
}
|
||||
});
|
||||
updateBulkDeleteButton();
|
||||
renderChatLogTable();
|
||||
});
|
||||
break;
|
||||
|
||||
case "analytics":
|
||||
document
|
||||
.getElementById("reload-analytics-btn")
|
||||
?.addEventListener("click", refreshAnalytics);
|
||||
document.getElementById("reload-analytics-btn")?.addEventListener("click", refreshAnalytics);
|
||||
break;
|
||||
|
||||
case "users":
|
||||
document
|
||||
.getElementById("reload-users-btn")
|
||||
?.addEventListener("click", refreshUsers);
|
||||
document.getElementById("reload-users-btn")?.addEventListener("click", refreshUsers);
|
||||
break;
|
||||
|
||||
case "system":
|
||||
document.getElementById("reload-system-btn")?.addEventListener("click", refreshSystemSummary);
|
||||
document.getElementById("config-form")?.addEventListener("submit", saveConfig);
|
||||
document
|
||||
.getElementById("reload-system-btn")
|
||||
?.addEventListener("click", refreshSystemSummary);
|
||||
document
|
||||
.getElementById("config-form")
|
||||
?.addEventListener("submit", saveConfig);
|
||||
document.querySelectorAll(".restart-btn").forEach((btn) =>
|
||||
btn.addEventListener("click", () => restartService((btn as HTMLElement).dataset.service!))
|
||||
);
|
||||
.querySelectorAll(".restart-btn")
|
||||
.forEach((btn) => btn.addEventListener("click", () => restartService((btn as HTMLElement).dataset.service!)));
|
||||
loadConfig();
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user