we now got websockets baby
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
54
pkg/embedfs/files/js/toasts.js
Normal file
54
pkg/embedfs/files/js/toasts.js
Normal 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;
|
||||
}
|
||||
|
||||
5
pkg/embedfs/files/vendored/alpinejs@3.15.4.min.js
vendored
Normal file
5
pkg/embedfs/files/vendored/alpinejs@3.15.4.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
pkg/embedfs/files/vendored/htmx-ext-ws.min.js
vendored
Normal file
1
pkg/embedfs/files/vendored/htmx-ext-ws.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
pkg/embedfs/files/vendored/htmx@2.0.8.min.js
vendored
Normal file
1
pkg/embedfs/files/vendored/htmx@2.0.8.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user