fix turbo on dashboard + optimize a few more things.

This commit is contained in:
KingKDot
2026-07-13 21:54:21 -05:00
parent b08d8ba028
commit feb712e2de
16 changed files with 194 additions and 83 deletions
+2 -5
View File
@@ -2,7 +2,6 @@ import { createBuildProfileManager } from "./build-profile-manager.js";
import {
createBuildHistoryManager,
formatFileSize,
updateExpirationTimer,
} from "./build-history-manager.js";
const form = document.getElementById("build-form");
@@ -2259,7 +2258,8 @@ function showBuildFiles(files, buildId, expiresAt) {
const timer = document.createElement("span");
timer.id = "expiration-timer";
timer.className = "text-yellow-400 font-medium";
timer.dataset.expires = String(expiresAt);
timer.dataset.controller = "countdown";
timer.dataset.countdownExpiresAtValue = String(expiresAt);
timer.textContent = "Calculating...";
right.appendChild(clockIcon);
right.appendChild(expiresLabel);
@@ -2270,9 +2270,6 @@ function showBuildFiles(files, buildId, expiresAt) {
buildInfoDiv.appendChild(infoRow);
buildFilesDiv.appendChild(buildInfoDiv);
updateExpirationTimer(timer, expiresAt);
setInterval(() => updateExpirationTimer(timer, expiresAt), 60000);
files.forEach((file) => {
const fileDiv = document.createElement("div");
fileDiv.className =
+1
View File
@@ -108,6 +108,7 @@ function moveClientCardImmediately(msg) {
export function registerRenderer(fn) {
render = fn;
state.lastDigest = "";
}
function clientQueryParams(page = state.page) {
+14 -52
View File
@@ -23,12 +23,6 @@ uploadInfinite?.addEventListener("change", () => {
});
if (uploadMaxDownloads) uploadMaxDownloads.disabled = true;
const passwordInfoBtn = document.getElementById("password-info-btn");
const passwordInfoPanel = document.getElementById("password-info-panel");
passwordInfoBtn?.addEventListener("click", () => {
passwordInfoPanel?.classList.toggle("hidden");
});
async function loadCurrentUser() {
try {
const res = await fetch("/api/auth/me");
@@ -113,12 +107,14 @@ async function loadFiles() {
<td class="px-6 py-4">
<div class="flex items-center justify-end gap-2">
<button class="action-btn px-2.5 py-1.5 text-sm bg-slate-800 hover:bg-slate-700 text-slate-200 rounded border border-slate-700 transition-colors"
data-action="copy-link" data-file-id="${f.id}" title="Copy download link">
data-controller="clipboard" data-action="clipboard#copy"
data-clipboard-text-value="${getDownloadUrl(f.id)}"
data-clipboard-success-message-value="Download link copied to clipboard!" title="Copy download link">
<i class="fa-solid fa-link"></i>
</button>
${canManage ? `
<button class="action-btn px-2.5 py-1.5 text-sm bg-slate-800 hover:bg-slate-700 text-slate-200 rounded border border-slate-700 transition-colors"
data-action="edit" data-file-id="${f.id}"
data-file-action="edit" data-file-id="${f.id}"
data-has-password="${f.hasPassword ? 1 : 0}"
data-max-downloads="${f.maxDownloads !== null ? f.maxDownloads : ''}"
data-expires-at="${f.expiresAt || ''}"
@@ -127,7 +123,9 @@ async function loadFiles() {
<i class="fa-solid fa-pen-to-square"></i>
</button>
<button class="action-btn px-2.5 py-1.5 text-sm bg-red-900/40 hover:bg-red-800/60 text-red-200 rounded border border-red-700/60 transition-colors"
data-action="delete" data-file-id="${f.id}" data-filename="${escapeHtml(f.filename)}" title="Delete file">
data-controller="confirm" data-action="confirm#confirm"
data-confirm-message-value="Delete this file? This cannot be undone."
data-file-action="delete" data-file-id="${f.id}" title="Delete file">
<i class="fa-solid fa-trash"></i>
</button>
` : ""}
@@ -137,57 +135,21 @@ async function loadFiles() {
})
.join("");
attachActionListeners();
} catch (err) {
console.error("Load files error:", err);
fileList.innerHTML = `<tr><td colspan="7" class="px-6 py-12 text-center text-red-400">Failed to load files</td></tr>`;
}
}
function attachActionListeners() {
fileList.addEventListener("click", (e) => {
const btn = e.target.closest(".action-btn");
if (!btn) return;
fileList?.addEventListener("click", (e) => {
const btn = e.target.closest("[data-file-action]");
if (!btn) return;
const action = btn.dataset.action;
const fileId = btn.dataset.fileId;
switch (action) {
case "copy-link":
copyDownloadLink(fileId);
break;
case "edit":
openEditModal(btn);
break;
case "delete":
deleteFile(fileId, btn.dataset.filename);
break;
}
});
}
function copyDownloadLink(fileId) {
const url = getDownloadUrl(fileId);
navigator.clipboard.writeText(url).then(() => {
if (window.showToast) {
window.showToast("Download link copied to clipboard!", "success");
}
}).catch(() => {
const input = document.createElement("input");
input.value = url;
document.body.appendChild(input);
input.select();
document.execCommand("copy");
document.body.removeChild(input);
if (window.showToast) {
window.showToast("Download link copied!", "success");
}
});
}
async function deleteFile(fileId, filename) {
if (!confirm(`Delete "${filename}"? This cannot be undone.`)) return;
if (btn.dataset.fileAction === "edit") openEditModal(btn);
if (btn.dataset.fileAction === "delete") deleteFile(btn.dataset.fileId);
});
async function deleteFile(fileId) {
try {
const res = await fetch(`/api/file-share/${fileId}`, { method: "DELETE" });
if (res.ok) {
+1
View File
@@ -1141,6 +1141,7 @@ function initializeRenderer() {
}
});
initDashboardStats();
renderCachedClients({ reorder: true, force: true });
setupDashboardThumbnailLoader();
refreshGroupFilter();
loadWithOptions();
@@ -1,9 +1,19 @@
import { Application } from "/vendor/hotwired/stimulus.js";
import SessionsController from "./controllers/sessions-controller.js";
import ClipboardController from "./controllers/clipboard-controller.js";
import ConfirmController from "./controllers/confirm-controller.js";
import CountdownController from "./controllers/countdown-controller.js";
import RevealController from "./controllers/reveal-controller.js";
import ToggleController from "./controllers/toggle-controller.js";
const application = Application.start();
application.debug = false;
application.register("sessions", SessionsController);
application.register("clipboard", ClipboardController);
application.register("confirm", ConfirmController);
application.register("countdown", CountdownController);
application.register("reveal", RevealController);
application.register("toggle", ToggleController);
window.OverlordStimulus = application;
@@ -0,0 +1,33 @@
import { Controller } from "/vendor/hotwired/stimulus.js";
export default class extends Controller {
static values = {
text: String,
successMessage: { type: String, default: "Copied to clipboard" },
};
async copy(event) {
event.preventDefault();
if (!this.hasTextValue) return;
try {
await navigator.clipboard.writeText(this.textValue);
} catch {
const input = document.createElement("textarea");
input.value = this.textValue;
input.setAttribute("readonly", "");
input.style.position = "fixed";
input.style.opacity = "0";
document.body.appendChild(input);
input.select();
document.execCommand("copy");
input.remove();
}
window.showToast?.(this.successMessageValue, "success");
this.element.dispatchEvent(new CustomEvent("clipboard:copied", {
bubbles: true,
detail: { text: this.textValue },
}));
}
}
@@ -0,0 +1,12 @@
import { Controller } from "/vendor/hotwired/stimulus.js";
export default class extends Controller {
static values = { message: String };
confirm(event) {
if (!this.hasMessageValue || window.confirm(this.messageValue)) return;
event.preventDefault();
event.stopImmediatePropagation();
}
}
@@ -0,0 +1,53 @@
import { Controller } from "/vendor/hotwired/stimulus.js";
export default class extends Controller {
static values = {
expiresAt: Number,
interval: { type: Number, default: 60_000 },
};
connect() {
this.update();
if (this.expiresAtValue > Date.now()) {
this.clock = window.setInterval(() => this.update(), this.intervalValue);
}
}
disconnect() {
window.clearInterval(this.clock);
}
update() {
const remaining = this.expiresAtValue - Date.now();
if (remaining <= 0) {
this.element.textContent = "Expired";
this.setTone("text-red-400");
window.clearInterval(this.clock);
return;
}
const days = Math.floor(remaining / 86_400_000);
const hours = Math.floor((remaining % 86_400_000) / 3_600_000);
const minutes = Math.floor((remaining % 3_600_000) / 60_000);
this.element.textContent = days > 0
? `${days}d ${hours}h`
: hours > 0
? `${hours}h ${minutes}m`
: `${minutes}m`;
this.setTone(days >= 3
? "text-green-400"
: days >= 1
? "text-yellow-400"
: "text-orange-400");
}
setTone(tone) {
this.element.classList.remove(
"text-red-400",
"text-green-400",
"text-yellow-400",
"text-orange-400",
);
this.element.classList.add(tone, "font-medium");
}
}
@@ -0,0 +1,17 @@
import { Controller } from "/vendor/hotwired/stimulus.js";
export default class extends Controller {
static targets = ["input", "icon"];
toggle(event) {
event.preventDefault();
const revealing = this.inputTarget.type === "password";
this.inputTarget.type = revealing ? "text" : "password";
event.currentTarget.setAttribute("aria-pressed", String(revealing));
event.currentTarget.setAttribute("aria-label", revealing ? "Hide value" : "Show value");
if (this.hasIconTarget) {
this.iconTarget.classList.toggle("fa-eye", !revealing);
this.iconTarget.classList.toggle("fa-eye-slash", revealing);
}
}
}
@@ -12,13 +12,6 @@ export default class extends Controller {
window.clearInterval(this.clock);
}
confirm(event) {
const message = event.currentTarget.dataset.confirmMessage;
if (message && !window.confirm(message)) {
event.preventDefault();
}
}
updateTimes() {
const now = Math.floor(Date.now() / 1000);
for (const element of this.timeTargets) {
@@ -0,0 +1,11 @@
import { Controller } from "/vendor/hotwired/stimulus.js";
export default class extends Controller {
static targets = ["content"];
toggle(event) {
event.preventDefault();
const hidden = this.contentTarget.classList.toggle("hidden");
event.currentTarget.setAttribute("aria-expanded", String(!hidden));
}
}
@@ -22,6 +22,7 @@ const persistentIds = [
"chat-bubble",
"chat-panel",
"cert-trust-banner",
"command-menu",
];
function normalizeUrl(value, base = window.location.href) {
+14 -9
View File
@@ -1,3 +1,5 @@
import { runWithoutPageTracking } from "./turbo-navigation.js";
// ── Context menu data ─────────────────────────────────────────────────────────
const MENU_GROUPS = [
{
@@ -254,7 +256,6 @@ menuStyle.textContent = `
#ctx-main { border-radius: 8px 8px 0 0; }
}
`;
document.head.appendChild(menuStyle);
// ── Build DOM ─────────────────────────────────────────────────────────────────
function buildItemHTML(item) {
@@ -282,9 +283,11 @@ const subPanelsHTML =
const menu = document.createElement("div");
menu.id = "command-menu";
menu.setAttribute("data-turbo-permanent", "");
menu.setAttribute("role", "menu");
menu.setAttribute("aria-hidden", "true");
menu.innerHTML = `<div id="ctx-main">${mainRowsHTML}</div><div id="ctx-sub">${subPanelsHTML}</div>`;
menu.prepend(menuStyle);
document.body.appendChild(menu);
const ctxMain = menu.querySelector("#ctx-main");
@@ -363,32 +366,34 @@ function refitMobileMenu() {
}
ctxMain.querySelectorAll(".ctx-row").forEach(rowEl => {
rowEl.addEventListener("mouseenter", () => {
runWithoutPageTracking(() => rowEl.addEventListener("mouseenter", () => {
if (isMobileMenu()) return;
if (rowEl.disabled || rowEl.getAttribute("aria-disabled") === "true") return;
const groupId = rowEl.dataset.groupToggle;
if (groupId) showSubmenu(groupId, rowEl);
});
}));
});
// Debounced hide — prevents the 5px gap between panels from closing the submenu
function scheduleHide() { if (!isMobileMenu()) _hideTimer = setTimeout(hideSubmenu, 150); }
function cancelHide() { clearTimeout(_hideTimer); _hideTimer = null; }
ctxMain.addEventListener("mouseleave", scheduleHide);
ctxMain.addEventListener("mouseenter", cancelHide);
ctxSub.addEventListener("mouseenter", cancelHide);
ctxSub.addEventListener("mouseleave", scheduleHide);
runWithoutPageTracking(() => {
ctxMain.addEventListener("mouseleave", scheduleHide);
ctxMain.addEventListener("mouseenter", cancelHide);
ctxSub.addEventListener("mouseenter", cancelHide);
ctxSub.addEventListener("mouseleave", scheduleHide);
});
// Touch / mobile: tap to toggle group
ctxMain.querySelectorAll(".ctx-row").forEach(rowEl => {
rowEl.addEventListener("click", (e) => {
runWithoutPageTracking(() => rowEl.addEventListener("click", (e) => {
const groupId = rowEl.dataset.groupToggle;
if (!groupId) return;
if (rowEl.disabled || rowEl.getAttribute("aria-disabled") === "true") return;
e.stopPropagation();
if (activeGroupId === groupId) { hideSubmenu(); } else { showSubmenu(groupId, rowEl); }
});
}));
});
const modal = document.createElement("div");