we now got websockets baby

This commit is contained in:
2026-01-26 00:29:57 +11:00
parent ce524702f0
commit a3dafa592b
23 changed files with 621 additions and 312 deletions

View File

@@ -8,12 +8,17 @@ func (c Key) String() string {
return "oslstats context key " + string(c)
}
var HTMXLogKey Key = Key("htmxlog")
var DevModeKey Key = Key("devmode")
func HTMXLog(ctx context.Context) bool {
htmxlog, ok := ctx.Value(HTMXLogKey).(bool)
func DevMode(ctx context.Context) DevInfo {
devmode, ok := ctx.Value(DevModeKey).(DevInfo)
if !ok {
return false
return DevInfo{}
}
return htmxlog
return devmode
}
type DevInfo struct {
WebsocketBase string
HTMXLog bool
}

View File

@@ -364,6 +364,9 @@
.mr-5 {
margin-right: calc(var(--spacing) * 5);
}
.mb-2 {
margin-bottom: calc(var(--spacing) * 2);
}
.mb-8 {
margin-bottom: calc(var(--spacing) * 8);
}
@@ -454,6 +457,12 @@
.max-w-7xl {
max-width: var(--container-7xl);
}
.max-w-48 {
max-width: calc(var(--spacing) * 48);
}
.max-w-80 {
max-width: calc(var(--spacing) * 80);
}
.max-w-100 {
max-width: calc(var(--spacing) * 100);
}
@@ -478,6 +487,9 @@
.flex-shrink-0 {
flex-shrink: 0;
}
.shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
@@ -707,6 +719,9 @@
.p-4 {
padding: calc(var(--spacing) * 4);
}
.p-5 {
padding: calc(var(--spacing) * 5);
}
.p-6 {
padding: calc(var(--spacing) * 6);
}
@@ -807,6 +822,9 @@
--tw-tracking: var(--tracking-tight);
letter-spacing: var(--tracking-tight);
}
.text-wrap {
text-wrap: wrap;
}
.break-all {
word-break: break-all;
}

View File

@@ -0,0 +1,180 @@
const popupData = {
// Error modal (blocking, full-screen) - for 500/503 errors
errorModal: {
show: false,
code: 0,
title: "",
message: "",
details: "",
},
// Toast notifications (non-blocking, stacked) - for success/warning/info
toasts: [],
toastIdCounter: 0,
// Handle HTMX beforeSwap event - intercept both errors and successes
handleBeforeSwap(event) {
const xhr = event.detail.xhr;
if (!xhr) return;
const status = xhr.status;
const trigger = xhr.getResponseHeader("HX-Trigger");
if (!trigger) return;
try {
const data = JSON.parse(trigger);
// Handle 500/503 error modals
if ((status === 500 || status === 503) && data.showErrorModal) {
this.errorModal = {
show: true,
code: data.showErrorModal.code,
title: data.showErrorModal.title,
message: data.showErrorModal.message,
details: data.showErrorModal.details,
};
// Prevent swap but allow error logging
event.detail.shouldSwap = false;
return;
}
// Handle success/warning/info toasts (200-299)
if (status >= 200 && status < 300 && data.showNotification) {
this.addToast(data.showNotification);
}
} catch (e) {
console.error("Failed to parse HX-Trigger:", e);
}
},
// Add toast to stack
addToast(notification) {
const toast = {
id: ++this.toastIdCounter,
type: notification.type,
title: notification.title,
message: notification.message,
paused: false,
progress: 0,
};
this.toasts.push(toast);
// Determine timeout based on type
const timeout = notification.type === "warning" ? 5000 : 3000;
// Start progress animation
this.animateToastProgress(toast.id, timeout);
},
// Animate toast progress bar and auto-dismiss
animateToastProgress(toastId, duration) {
const toast = this.toasts.find((t) => t.id === toastId);
if (!toast) return;
const startTime = performance.now();
let totalPausedTime = 0;
let pauseStartTime = null;
const animate = (currentTime) => {
const toast = this.toasts.find((t) => t.id === toastId);
if (!toast) return; // Toast was manually removed
if (toast.paused) {
// Track when pause started
if (pauseStartTime === null) {
pauseStartTime = currentTime;
}
// Keep animating while paused
requestAnimationFrame(animate);
return;
} else {
// If we were paused, accumulate the paused time
if (pauseStartTime !== null) {
totalPausedTime += currentTime - pauseStartTime;
pauseStartTime = null;
}
}
// Calculate actual elapsed time (excluding paused time)
const elapsed = currentTime - startTime - totalPausedTime;
toast.progress = Math.min((elapsed / duration) * 100, 100);
if (elapsed >= duration) {
this.removeToast(toastId);
} else {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
},
// Remove toast from stack
removeToast(id) {
this.toasts = this.toasts.filter((t) => t.id !== id);
},
// Close error modal
closeErrorModal() {
this.errorModal.show = false;
},
};
// Global function for WebSocket toasts to animate progress and auto-dismiss
// Pure vanilla JavaScript - no Alpine.js dependency
// Called via inline script in ToastNotificationWS template
function animateToastProgress(toastId, duration) {
const toastEl = document.getElementById(toastId);
const progressEl = document.getElementById(toastId + "-progress");
if (!toastEl || !progressEl) {
console.error("animateToastProgress: elements not found", toastId);
return;
}
const startTime = performance.now();
let totalPausedTime = 0;
let pauseStartTime = null;
const animate = (currentTime) => {
const el = document.getElementById(toastId);
const progEl = document.getElementById(toastId + "-progress");
if (!el || !progEl) return; // Toast was manually removed
// Check if paused (set via onmouseenter/onmouseleave)
const isPaused = el.dataset.paused === "true";
if (isPaused) {
// Track when pause started
if (pauseStartTime === null) {
pauseStartTime = currentTime;
}
// Keep animating while paused
requestAnimationFrame(animate);
return;
} else {
// If we were paused, accumulate the paused time
if (pauseStartTime !== null) {
totalPausedTime += currentTime - pauseStartTime;
pauseStartTime = null;
}
}
// Calculate actual elapsed time (excluding paused time)
const elapsed = currentTime - startTime - totalPausedTime;
// Progress bar goes from 100% to 0%
const progress = Math.max(100 - (elapsed / duration) * 100, 0);
progEl.style.width = progress + "%";
if (elapsed >= duration) {
// Remove the toast element
el.remove();
} else {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}

View File

@@ -0,0 +1,54 @@
function progressBar(toastId, duration) {
const progressBar = {
progress: 0,
paused: false,
animateToastProgress() {
const toast = document.getElementById(toastId);
if (!toast) return;
const bar = document.getElementById([toastId, "progress"].join("-"));
const startTime = performance.now();
let totalPausedTime = 0;
let pauseStartTime = null;
const animate = (currentTime) => {
const toast = document.getElementById(toastId);
if (!toast) return; // Toast was manually removed
if (this.paused) {
// Track when pause started
if (pauseStartTime === null) {
pauseStartTime = currentTime;
}
// Keep animating while paused
requestAnimationFrame(animate);
return;
} else {
// If we were paused, accumulate the paused time
if (pauseStartTime !== null) {
totalPausedTime += currentTime - pauseStartTime;
pauseStartTime = null;
}
}
// Calculate actual elapsed time (excluding paused time)
const elapsed = currentTime - startTime - totalPausedTime;
this.progress = Math.min((elapsed / duration) * 100, 100) + "%";
bar.style["width"] = this.progress;
if (elapsed >= duration) {
toast.remove()
} else {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}
};
progressBar.animateToastProgress();
return progressBar;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long