Update: UI files and add error-reporter.js
This commit is contained in:
parent
5e10222a94
commit
5618ed4367
11 changed files with 1625 additions and 1299 deletions
|
|
@ -7,10 +7,10 @@ use axum::{
|
|||
http::{Request, StatusCode},
|
||||
response::{Html, IntoResponse, Response},
|
||||
routing::{any, get},
|
||||
Router,
|
||||
Json, Router,
|
||||
};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use log::{debug, error, info};
|
||||
use log::{debug, error, info, warn};
|
||||
#[cfg(feature = "embed-ui")]
|
||||
use rust_embed::RustEmbed;
|
||||
use serde::Deserialize;
|
||||
|
|
@ -130,8 +130,98 @@ const ROOT_FILES: &[&str] = &[
|
|||
"single.gbui",
|
||||
];
|
||||
|
||||
pub async fn index() -> impl IntoResponse {
|
||||
serve_suite().await
|
||||
pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
|
||||
let path = uri.path();
|
||||
|
||||
// Check if path contains static asset directories - serve them directly
|
||||
let path_lower = path.to_lowercase();
|
||||
if path_lower.contains("/js/")
|
||||
|| path_lower.contains("/css/")
|
||||
|| path_lower.contains("/vendor/")
|
||||
|| path_lower.contains("/assets/")
|
||||
|| path_lower.contains("/public/")
|
||||
|| path_lower.contains("/partials/")
|
||||
|| path_lower.ends_with(".js")
|
||||
|| path_lower.ends_with(".css")
|
||||
|| path_lower.ends_with(".png")
|
||||
|| path_lower.ends_with(".jpg")
|
||||
|| path_lower.ends_with(".jpeg")
|
||||
|| path_lower.ends_with(".gif")
|
||||
|| path_lower.ends_with(".svg")
|
||||
|| path_lower.ends_with(".ico")
|
||||
|| path_lower.ends_with(".woff")
|
||||
|| path_lower.ends_with(".woff2")
|
||||
|| path_lower.ends_with(".ttf")
|
||||
|| path_lower.ends_with(".eot")
|
||||
|| path_lower.ends_with(".mp4")
|
||||
|| path_lower.ends_with(".webm")
|
||||
|| path_lower.ends_with(".mp3")
|
||||
|| path_lower.ends_with(".wav")
|
||||
{
|
||||
// Remove bot name prefix if present (e.g., /edu/suite/js/file.js -> suite/js/file.js)
|
||||
let path_parts: Vec<&str> = path.split('/').collect();
|
||||
let fs_path = if path_parts.len() > 1 {
|
||||
let mut start_idx = 1;
|
||||
let known_dirs = ["suite", "js", "css", "vendor", "assets", "public", "partials", "settings", "auth", "about", "drive", "chat", "tasks", "admin", "mail", "calendar", "meet", "docs", "sheet", "slides", "paper", "research", "sources", "learn", "analytics", "dashboards", "monitoring", "people", "crm", "tickets", "billing", "products", "video", "player", "canvas", "social", "project", "goals", "workspace", "designer"];
|
||||
|
||||
if path_parts.len() > start_idx && !known_dirs.contains(&path_parts[start_idx]) {
|
||||
start_idx += 1;
|
||||
}
|
||||
|
||||
path_parts[start_idx..].join("/")
|
||||
} else {
|
||||
path.to_string()
|
||||
};
|
||||
|
||||
let full_path = get_ui_root().join(&fs_path);
|
||||
|
||||
debug!("index: Serving static file: {} -> {:?} (fs_path: {})", path, full_path, fs_path);
|
||||
|
||||
#[cfg(feature = "embed-ui")]
|
||||
{
|
||||
let asset_path = fs_path.trim_start_matches('/');
|
||||
if let Some(content) = Assets::get(asset_path) {
|
||||
let mime = mime_guess::from_path(asset_path).first_or_octet_stream();
|
||||
return ([(axum::http::header::CONTENT_TYPE, mime.as_ref())], content.data).into_response();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "embed-ui"))]
|
||||
{
|
||||
if let Ok(bytes) = tokio::fs::read(&full_path).await {
|
||||
let mime = mime_guess::from_path(&full_path).first_or_octet_stream();
|
||||
return (StatusCode::OK, [("content-type", mime.as_ref())], bytes).into_response();
|
||||
}
|
||||
}
|
||||
|
||||
warn!("index: Static file not found: {} -> {:?}", path, full_path);
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
}
|
||||
|
||||
let path_parts: Vec<&str> = path.split('/').collect();
|
||||
let bot_name = path_parts
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|part| {
|
||||
!part.is_empty()
|
||||
&& **part != "chat"
|
||||
&& **part != "app"
|
||||
&& **part != "ws"
|
||||
&& **part != "ui"
|
||||
&& **part != "api"
|
||||
&& **part != "auth"
|
||||
&& **part != "suite"
|
||||
&& !part.ends_with(".js")
|
||||
&& !part.ends_with(".css")
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
info!(
|
||||
"index: Extracted bot_name: {:?} from path: {}",
|
||||
bot_name,
|
||||
path
|
||||
);
|
||||
serve_suite(bot_name).await.into_response()
|
||||
}
|
||||
|
||||
pub fn get_ui_root() -> PathBuf {
|
||||
|
|
@ -196,7 +286,7 @@ pub async fn serve_minimal() -> impl IntoResponse {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn serve_suite() -> impl IntoResponse {
|
||||
pub async fn serve_suite(bot_name: Option<String>) -> impl IntoResponse {
|
||||
let raw_html_res = {
|
||||
#[cfg(feature = "embed-ui")]
|
||||
{
|
||||
|
|
@ -235,6 +325,32 @@ pub async fn serve_suite() -> impl IntoResponse {
|
|||
#[allow(unused_mut)] // Mutable required for feature-gated blocks
|
||||
let mut html = raw_html;
|
||||
|
||||
// Inject base tag and bot_name into the page
|
||||
if let Some(head_end) = html.find("</head>") {
|
||||
// Set base href to include bot context if present (e.g., /edu/)
|
||||
let base_href = if let Some(ref name) = bot_name {
|
||||
format!("/{}/", name)
|
||||
} else {
|
||||
"/".to_string()
|
||||
};
|
||||
let base_tag = format!(r#"<base href="{}">"#, base_href);
|
||||
html.insert_str(head_end, &base_tag);
|
||||
|
||||
if let Some(name) = bot_name {
|
||||
info!("serve_suite: Injecting bot_name '{}' into page with base href='{}'", name, base_href);
|
||||
let bot_script = format!(
|
||||
r#"<script>window.__INITIAL_BOT_NAME__ = "{}";</script>"#,
|
||||
&name
|
||||
);
|
||||
html.insert_str(head_end + base_tag.len(), &bot_script);
|
||||
info!("serve_suite: Successfully injected base tag and bot_name script");
|
||||
} else {
|
||||
info!("serve_suite: Successfully injected base tag (no bot_name)");
|
||||
}
|
||||
} else {
|
||||
error!("serve_suite: Failed to find </head> tag to inject content");
|
||||
}
|
||||
|
||||
// Core Apps
|
||||
#[cfg(not(feature = "chat"))]
|
||||
{
|
||||
|
|
@ -452,14 +568,26 @@ async fn health(State(state): State<AppState>) -> (StatusCode, axum::Json<serde_
|
|||
}
|
||||
}
|
||||
|
||||
async fn api_health() -> (StatusCode, axum::Json<serde_json::Value>) {
|
||||
(
|
||||
StatusCode::OK,
|
||||
axum::Json(serde_json::json!({
|
||||
"status": "ok",
|
||||
"version": env!("CARGO_PKG_VERSION")
|
||||
})),
|
||||
)
|
||||
async fn api_health(State(state): State<AppState>) -> (StatusCode, axum::Json<serde_json::Value>) {
|
||||
if state.health_check().await {
|
||||
(
|
||||
StatusCode::OK,
|
||||
axum::Json(serde_json::json!({
|
||||
"status": "ok",
|
||||
"botserver": "healthy",
|
||||
"version": env!("CARGO_PKG_VERSION")
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
axum::Json(serde_json::json!({
|
||||
"status": "error",
|
||||
"botserver": "unhealthy",
|
||||
"version": env!("CARGO_PKG_VERSION")
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_app_context(headers: &axum::http::HeaderMap, path: &str) -> Option<String> {
|
||||
|
|
@ -588,6 +716,7 @@ async fn build_proxy_response(resp: reqwest::Response) -> Response<Body> {
|
|||
fn create_api_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/health", get(api_health))
|
||||
.route("/client-error", axum::routing::post(handle_client_error))
|
||||
.fallback(any(proxy_api))
|
||||
}
|
||||
|
||||
|
|
@ -598,6 +727,35 @@ struct WsQuery {
|
|||
bot_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ClientError {
|
||||
message: String,
|
||||
stack: Option<String>,
|
||||
source: String,
|
||||
url: String,
|
||||
user_agent: String,
|
||||
timestamp: String,
|
||||
}
|
||||
|
||||
async fn handle_client_error(Json(error): Json<ClientError>) -> impl IntoResponse {
|
||||
warn!(
|
||||
"CLIENT:{}: {} at {} ({}) - {}",
|
||||
error.source.to_uppercase(),
|
||||
error.message,
|
||||
error.url,
|
||||
error.timestamp,
|
||||
error.user_agent
|
||||
);
|
||||
|
||||
if let Some(stack) = &error.stack {
|
||||
if !stack.is_empty() {
|
||||
warn!("CLIENT:STACK: {}", stack);
|
||||
}
|
||||
}
|
||||
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct OptionalWsQuery {
|
||||
task_id: Option<String>,
|
||||
|
|
@ -613,11 +771,14 @@ async fn ws_proxy(
|
|||
let path_parts: Vec<&str> = uri.path().split('/').collect();
|
||||
let bot_name = params
|
||||
.bot_name
|
||||
.filter(|name| name != "ws" && !name.is_empty())
|
||||
.or_else(|| {
|
||||
// Try to extract from path like /edu or /app/edu
|
||||
path_parts
|
||||
.iter()
|
||||
.find(|part| !part.is_empty() && *part != "chat" && *part != "app")
|
||||
.find(|part| {
|
||||
!part.is_empty() && **part != "chat" && **part != "app" && **part != "ws"
|
||||
})
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.unwrap_or_else(|| "default".to_string());
|
||||
|
|
@ -996,10 +1157,12 @@ fn add_static_routes(router: Router<AppState>, _suite_path: &Path) -> Router<App
|
|||
#[cfg(not(feature = "embed-ui"))]
|
||||
{
|
||||
let mut r = router;
|
||||
// Serve suite directories ONLY through /suite/{dir} path
|
||||
// This prevents duplicate routes that cause ServeDir to return index.html for file requests
|
||||
// Serve suite directories at BOTH root level and /suite/{dir} path
|
||||
// This allows HTML files to reference js/vendor/file.js directly
|
||||
for dir in SUITE_DIRS {
|
||||
let path = _suite_path.join(dir);
|
||||
info!("Adding route for /{} -> {:?}", dir, path);
|
||||
r = r.nest_service(&format!("/{dir}"), ServeDir::new(path.clone()));
|
||||
info!("Adding route for /suite/{} -> {:?}", dir, path);
|
||||
r = r.nest_service(&format!("/suite/{dir}"), ServeDir::new(path.clone()));
|
||||
}
|
||||
|
|
@ -1024,13 +1187,14 @@ pub fn configure_router() -> Router {
|
|||
.nest("/ui", create_ui_router())
|
||||
.nest("/ws", create_ws_router())
|
||||
.nest("/apps", create_apps_router())
|
||||
.route("/", get(index))
|
||||
.route("/minimal", get(serve_minimal))
|
||||
.route("/suite", get(serve_suite))
|
||||
.route("/favicon.ico", get(serve_favicon))
|
||||
.nest_service("/auth", ServeDir::new(suite_path.join("auth")));
|
||||
.route("/favicon.ico", get(serve_favicon));
|
||||
|
||||
router = add_static_routes(router, &suite_path);
|
||||
|
||||
router.fallback(get(index)).with_state(state)
|
||||
router
|
||||
.route("/", get(index))
|
||||
.route("/minimal", get(serve_minimal))
|
||||
.route("/suite", get(serve_suite))
|
||||
.fallback(get(index))
|
||||
.with_state(state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
<script src="/js/vendor/htmx-ws.js"></script>
|
||||
<script src="/js/vendor/htmx-json-enc.js"></script>
|
||||
|
||||
<!-- ERROR REPORTER - Captures JS errors and sends to server log -->
|
||||
<script src="/js/error-reporter.js"></script>
|
||||
|
||||
<!-- i18n -->
|
||||
<script src="/js/i18n.js"></script>
|
||||
|
||||
|
|
@ -116,7 +119,7 @@
|
|||
href="#tasks"
|
||||
class="app-item"
|
||||
role="menuitem"
|
||||
hx-get="/tasks/tasks.html"
|
||||
hx-get="/suite/tasks/autotask.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="true"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -156,34 +156,34 @@
|
|||
<!-- Application initialization -->
|
||||
<script>
|
||||
// Initialize application
|
||||
(function initApp() {
|
||||
"use strict";
|
||||
(function initApp() {
|
||||
"use strict";
|
||||
|
||||
// Initialize ThemeManager
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
console.log("🚀 Initializing General Bots Desktop...");
|
||||
// Initialize ThemeManager
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
console.log("🚀 Initializing General Bots Desktop...");
|
||||
|
||||
// Initialize theme system
|
||||
if (window.ThemeManager) {
|
||||
ThemeManager.init();
|
||||
console.log("✓ Theme Manager initialized");
|
||||
} else {
|
||||
console.warn("⚠ ThemeManager not found");
|
||||
// Initialize theme system
|
||||
if (window.ThemeManager) {
|
||||
ThemeManager.init();
|
||||
console.log("✓ Theme Manager initialized");
|
||||
} else {
|
||||
console.warn("⚠ ThemeManager not found");
|
||||
}
|
||||
|
||||
// Initialize apps menu
|
||||
initAppsMenu();
|
||||
|
||||
// Hide loading overlay after initialization
|
||||
setTimeout(() => {
|
||||
const loadingOverlay =
|
||||
document.getElementById("loadingOverlay");
|
||||
if (loadingOverlay) {
|
||||
loadingOverlay.classList.add("hidden");
|
||||
console.log("✓ Application ready");
|
||||
}
|
||||
|
||||
// Initialize apps menu
|
||||
initAppsMenu();
|
||||
|
||||
// Hide loading overlay after initialization
|
||||
setTimeout(() => {
|
||||
const loadingOverlay =
|
||||
document.getElementById("loadingOverlay");
|
||||
if (loadingOverlay) {
|
||||
loadingOverlay.classList.add("hidden");
|
||||
console.log("✓ Application ready");
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Apps menu functionality
|
||||
function initAppsMenu() {
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@
|
|||
<a
|
||||
href="#tasks"
|
||||
class="app-card"
|
||||
hx-get="/suite/tasks/tasks.html"
|
||||
hx-get="/suite/tasks/autotask.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#tasks"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -351,8 +351,8 @@
|
|||
this.clearAuth();
|
||||
this.emit("tokenExpired");
|
||||
|
||||
const currentPath = window.location.pathname;
|
||||
if (!currentPath.startsWith("/auth/")) {
|
||||
const currentPath = window.location.pathname + window.location.hash;
|
||||
if (!window.location.pathname.startsWith("/auth/")) {
|
||||
window.location.href =
|
||||
"/auth/login.html?expired=1&redirect=" +
|
||||
encodeURIComponent(currentPath);
|
||||
|
|
|
|||
135
ui/suite/js/error-reporter.js
Normal file
135
ui/suite/js/error-reporter.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
(function() {
|
||||
'use strict';
|
||||
|
||||
const MAX_ERRORS = 50;
|
||||
const REPORT_ENDPOINT = '/api/client-errors';
|
||||
let errorQueue = [];
|
||||
let isReporting = false;
|
||||
|
||||
function formatError(error, context = {}) {
|
||||
return {
|
||||
type: error.name || 'Error',
|
||||
message: error.message || String(error),
|
||||
stack: error.stack,
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent,
|
||||
timestamp: new Date().toISOString(),
|
||||
context: context
|
||||
};
|
||||
}
|
||||
|
||||
async function reportErrors() {
|
||||
if (isReporting || errorQueue.length === 0) return;
|
||||
|
||||
isReporting = true;
|
||||
const errorsToReport = errorQueue.splice(0, MAX_ERRORS);
|
||||
errorQueue = [];
|
||||
|
||||
try {
|
||||
const response = await fetch(REPORT_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ errors: errorsToReport })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ErrorReporter] Failed to send errors:', response.status);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[ErrorReporter] Failed to send errors:', e.message);
|
||||
errorQueue.unshift(...errorsToReport);
|
||||
} finally {
|
||||
isReporting = false;
|
||||
|
||||
if (errorQueue.length > 0) {
|
||||
setTimeout(reportErrors, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function queueError(errorData) {
|
||||
errorQueue.push(errorData);
|
||||
if (errorQueue.length >= 10) {
|
||||
reportErrors();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('error', (event) => {
|
||||
const errorData = formatError(event.error || new Error(event.message), {
|
||||
filename: event.filename,
|
||||
lineno: event.lineno,
|
||||
colno: event.colno
|
||||
});
|
||||
queueError(errorData);
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
const errorData = formatError(event.reason || new Error(String(event.reason)), {
|
||||
type: 'unhandledRejection'
|
||||
});
|
||||
queueError(errorData);
|
||||
});
|
||||
|
||||
window.ErrorReporter = {
|
||||
report: function(error, context) {
|
||||
queueError(formatError(error, context));
|
||||
},
|
||||
flush: function() {
|
||||
reportErrors();
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === 'complete') {
|
||||
setTimeout(reportErrors, 1000);
|
||||
} else {
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(reportErrors, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[ErrorReporter] Client-side error reporting initialized');
|
||||
|
||||
window.NavigationLogger = {
|
||||
log: function(from, to, method) {
|
||||
const navEvent = {
|
||||
type: 'navigation',
|
||||
from: from,
|
||||
to: to,
|
||||
method: method,
|
||||
url: window.location.href,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
queueError({
|
||||
name: 'Navigation',
|
||||
message: `${method}: ${from} -> ${to}`,
|
||||
stack: undefined
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener('click', function(e) {
|
||||
const target = e.target.closest('[data-section]');
|
||||
if (target) {
|
||||
const section = target.getAttribute('data-section');
|
||||
const currentHash = window.location.hash.slice(1) || '';
|
||||
if (section !== currentHash) {
|
||||
setTimeout(() => {
|
||||
window.NavigationLogger.log(currentHash || 'home', section, 'click');
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
window.addEventListener('hashchange', function(e) {
|
||||
const oldURL = new URL(e.oldURL);
|
||||
const newURL = new URL(e.newURL);
|
||||
const fromHash = oldURL.hash.slice(1) || '';
|
||||
const toHash = newURL.hash.slice(1) || '';
|
||||
window.NavigationLogger.log(fromHash || 'home', toHash, 'hashchange');
|
||||
});
|
||||
|
||||
console.log('[NavigationLogger] Navigation tracking initialized');
|
||||
})();
|
||||
|
|
@ -203,7 +203,12 @@
|
|||
|
||||
// Handle WebSocket messages
|
||||
function handleWebSocketMessage(message) {
|
||||
switch (message.type) {
|
||||
const messageType = message.type || message.event;
|
||||
|
||||
// Debug logging
|
||||
console.log("handleWebSocketMessage called with:", { messageType, message });
|
||||
|
||||
switch (messageType) {
|
||||
case "message":
|
||||
appendMessage(message);
|
||||
break;
|
||||
|
|
@ -216,8 +221,28 @@
|
|||
case "suggestion":
|
||||
addSuggestion(message.text);
|
||||
break;
|
||||
case "change_theme":
|
||||
console.log("Processing change_theme event, not appending to chat");
|
||||
if (message.data) {
|
||||
ThemeManager.setThemeFromServer(message.data);
|
||||
|
||||
if (message.data.color1 || message.data.color2) {
|
||||
const root = document.documentElement;
|
||||
if (message.data.color1)
|
||||
root.style.setProperty("--color1", message.data.color1);
|
||||
if (message.data.color2)
|
||||
root.style.setProperty("--color2", message.data.color2);
|
||||
}
|
||||
}
|
||||
return; // Don't append theme events to chat
|
||||
default:
|
||||
console.log("Unknown message type:", message.type);
|
||||
// Only append unknown message types to chat if they have text content
|
||||
if (message.text || message.content) {
|
||||
console.log("Unknown message type, treating as chat message:", messageType);
|
||||
appendMessage(message);
|
||||
} else {
|
||||
console.log("Unknown message type:", messageType, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -295,6 +295,12 @@
|
|||
});
|
||||
|
||||
window.addEventListener("gb:auth:expired", function (event) {
|
||||
// Check if current bot is public - if so, skip redirect
|
||||
if (window.__BOT_IS_PUBLIC__ === true) {
|
||||
console.log("[GBSecurity] Bot is public, skipping auth redirect");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[GBSecurity] Auth expired, clearing tokens and redirecting",
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
<link rel="stylesheet" href="tasks/autotask.css" />
|
||||
<link rel="stylesheet" href="/suite/tasks/autotask.css" />
|
||||
|
||||
<div class="autotask-container" data-theme="sentient">
|
||||
<!-- Top Navigation Bar -->
|
||||
|
|
@ -478,6 +478,6 @@ Examples:
|
|||
<!-- Toast Container -->
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
||||
<link rel="stylesheet" href="tasks/progress-panel.css" />
|
||||
<script src="tasks/progress-panel.js"></script>
|
||||
<script src="tasks/autotask.js"></script>
|
||||
<link rel="stylesheet" href="/suite/tasks/progress-panel.css" />
|
||||
<script src="/suite/tasks/progress-panel.js"></script>
|
||||
<script src="/suite/tasks/autotask.js"></script>
|
||||
|
|
|
|||
|
|
@ -213,8 +213,10 @@ function setupIntentInputHandlers() {
|
|||
}
|
||||
|
||||
// Task polling for async task creation
|
||||
let activePollingTaskId = null;
|
||||
let pollingInterval = null;
|
||||
if (typeof activePollingTaskId === "undefined") {
|
||||
var activePollingTaskId = null;
|
||||
var pollingInterval = null;
|
||||
}
|
||||
|
||||
function startTaskPolling(taskId) {
|
||||
// Stop any existing polling
|
||||
|
|
@ -629,7 +631,9 @@ function handleWebSocketMessage(data) {
|
|||
}
|
||||
|
||||
// Store pending manifest updates for tasks whose elements aren't loaded yet
|
||||
const pendingManifestUpdates = new Map();
|
||||
if (typeof pendingManifestUpdates === "undefined") {
|
||||
var pendingManifestUpdates = new Map();
|
||||
}
|
||||
|
||||
function renderManifestProgress(
|
||||
taskId,
|
||||
|
|
@ -2759,8 +2763,9 @@ function formatTime(seconds) {
|
|||
// GLOBAL STYLES FOR TOAST ANIMATIONS
|
||||
// =============================================================================
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
if (typeof taskStyleElement === "undefined") {
|
||||
var taskStyleElement = document.createElement("style");
|
||||
taskStyleElement.textContent = `
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
|
@ -2805,7 +2810,8 @@ style.textContent = `
|
|||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
document.head.appendChild(taskStyleElement);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GOALS, PENDING INFO, SCHEDULERS, MONITORS
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue