Files
godoxy-yusing/internal/idlewatcher/html/loading.js
yusing 910ef639a4 feat(idlewatcher): implement real-time SSE-based loading page with enhanced UX
This major overhaul of the idlewatcher system introduces a modern, real-time loading experience with Server-Sent Events (SSE) streaming and improved error handling.

- **Real-time Event Streaming**: New SSE endpoint (`/$godoxy/wake-events`) provides live updates during container wake process
- **Enhanced Loading Page**: Modern console-style interface with timestamped events and color-coded status messages
- **Improved Static Asset Management**: Dedicated paths for CSS, JS, and favicon to avoid conflicting with upstream assets
- **Event History Buffer**: Stores wake events for reconnecting clients and debugging

- Refactored HTTP request handling with cleaner static asset routing
- Added `WakeEvent` system with structured event types (starting, waking_dep, dep_ready, container_woke, waiting_ready, ready, error)
- Implemented thread-safe event broadcasting using xsync.Map for concurrent SSE connections
- Enhanced error handling with detailed logging and user-friendly error messages
- Simplified loading page template system with better asset path management
- Fixed race conditions in dependency waking and state management

- Removed `common.go` functions (canceled, waitStarted) - moved inline for better context
- Updated Waker interface to accept context parameter in Wake() method
- New static asset paths use `/$godoxy/` prefix to avoid conflicts

- Console-style output with Fira Code font for better readability
- Color-coded event types (yellow for starting, blue for dependencies, green for success, red for errors)
- Automatic page refresh when container becomes ready
- Improved visual design with better glassmorphism effects and responsive layout
- Real-time progress feedback during dependency wake and container startup

This change transforms the static loading page into a dynamic, informative experience that keeps users informed during the wake process while maintaining backward compatibility with existing routing behavior.
2025-11-07 14:58:33 +08:00

81 lines
2.2 KiB
JavaScript

let ready = false;
window.onload = async function () {
const consoleEl = document.getElementById("console");
const loadingDotsEl = document.getElementById("loading-dots");
function formatTimestamp(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
fractionalSecondDigits: 3,
});
}
function addConsoleLine(type, message, timestamp) {
const line = document.createElement("div");
line.className = `console-line ${type}`;
const timestampEl = document.createElement("span");
timestampEl.className = "console-timestamp";
timestampEl.textContent = formatTimestamp(timestamp);
const messageEl = document.createElement("span");
messageEl.className = "console-message";
messageEl.textContent = message;
line.appendChild(timestampEl);
line.appendChild(messageEl);
consoleEl.appendChild(line);
consoleEl.scrollTop = consoleEl.scrollHeight;
}
// Connect to SSE endpoint
const eventSource = new EventSource(wakeEventsPath);
eventSource.onmessage = function (event) {
const data = JSON.parse(event.data);
if (data.type === "ready") {
ready = true;
// Container is ready, hide loading dots and refresh
loadingDotsEl.style.display = "none";
addConsoleLine(
data.type,
"Container is ready, refreshing...",
data.timestamp
);
setTimeout(() => {
window.location.reload();
}, 200);
} else if (data.type === "error") {
// Show error message and hide loading dots
const errorMessage = data.error || data.message;
addConsoleLine(data.type, errorMessage, data.timestamp);
loadingDotsEl.style.display = "none";
eventSource.close();
} else {
// Show other message types
addConsoleLine(data.type, data.message, data.timestamp);
}
};
eventSource.onerror = function (event) {
if (ready) {
// event will be closed by the server
return;
}
addConsoleLine(
"error",
"Connection lost. Please refresh the page.",
new Date().toISOString()
);
loadingDotsEl.style.display = "none";
eventSource.close();
};
};