Fix duplicate WS progress bug - rebuild tree on structure change
- Normalize task ID comparison in retry logic for consistent lookups - Use normalized keys in pendingManifestUpdates Map - Skip manifest_update in ProgressPanel (already handled by tasks.js) - Clean up existing handler before registering new one in ProgressPanel.init - Add name-based fallback lookups for sections/children/items when IDs change - Detect structure changes (section count, children, items, names) and rebuild tree - Clear progress-empty placeholder before rendering tree - Add detailed BUILD_TREE logging for debugging - Add cache-busting version to tasks.js script tag
This commit is contained in:
parent
4499bcda7a
commit
b51c542afa
8 changed files with 1659 additions and 573 deletions
|
|
@ -246,8 +246,6 @@ struct WsQuery {
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
struct OptionalWsQuery {
|
struct OptionalWsQuery {
|
||||||
session_id: Option<String>,
|
|
||||||
user_id: Option<String>,
|
|
||||||
task_id: Option<String>,
|
task_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -362,8 +360,23 @@ async fn handle_task_progress_ws_proxy(
|
||||||
while let Some(msg) = backend_rx.next().await {
|
while let Some(msg) = backend_rx.next().await {
|
||||||
match msg {
|
match msg {
|
||||||
Ok(TungsteniteMessage::Text(text)) => {
|
Ok(TungsteniteMessage::Text(text)) => {
|
||||||
if client_tx.send(AxumMessage::Text(text)).await.is_err() {
|
// Log manifest_update messages for debugging
|
||||||
break;
|
let is_manifest = text.contains("manifest_update");
|
||||||
|
if is_manifest {
|
||||||
|
info!("[WS_PROXY] Forwarding manifest_update to client: {}...", &text[..text.len().min(200)]);
|
||||||
|
} else if text.contains("task_progress") {
|
||||||
|
debug!("[WS_PROXY] Forwarding task_progress to client");
|
||||||
|
}
|
||||||
|
match client_tx.send(AxumMessage::Text(text)).await {
|
||||||
|
Ok(()) => {
|
||||||
|
if is_manifest {
|
||||||
|
info!("[WS_PROXY] manifest_update SENT successfully to client");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("[WS_PROXY] Failed to send message to client: {:?}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(TungsteniteMessage::Binary(data)) => {
|
Ok(TungsteniteMessage::Binary(data)) => {
|
||||||
|
|
@ -529,6 +542,18 @@ fn create_ui_router() -> Router<AppState> {
|
||||||
Router::new().fallback(any(proxy_api))
|
Router::new().fallback(any(proxy_api))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn serve_favicon() -> impl IntoResponse {
|
||||||
|
let favicon_path = PathBuf::from("./ui/suite/public/favicon.ico");
|
||||||
|
match tokio::fs::read(&favicon_path).await {
|
||||||
|
Ok(bytes) => (
|
||||||
|
StatusCode::OK,
|
||||||
|
[("content-type", "image/x-icon")],
|
||||||
|
bytes,
|
||||||
|
).into_response(),
|
||||||
|
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn add_static_routes(router: Router<AppState>, suite_path: &Path) -> Router<AppState> {
|
fn add_static_routes(router: Router<AppState>, suite_path: &Path) -> Router<AppState> {
|
||||||
let mut r = router;
|
let mut r = router;
|
||||||
|
|
||||||
|
|
@ -554,7 +579,8 @@ pub fn configure_router() -> Router {
|
||||||
.nest("/apps", create_apps_router())
|
.nest("/apps", create_apps_router())
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.route("/minimal", get(serve_minimal))
|
.route("/minimal", get(serve_minimal))
|
||||||
.route("/suite", get(serve_suite));
|
.route("/suite", get(serve_suite))
|
||||||
|
.route("/favicon.ico", get(serve_favicon));
|
||||||
|
|
||||||
router = add_static_routes(router, &suite_path);
|
router = add_static_routes(router, &suite_path);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -968,7 +968,7 @@
|
||||||
<!-- Core scripts -->
|
<!-- Core scripts -->
|
||||||
<script src="js/theme-manager.js"></script>
|
<script src="js/theme-manager.js"></script>
|
||||||
<script src="js/htmx-app.js"></script>
|
<script src="js/htmx-app.js"></script>
|
||||||
<script src="tasks/tasks.js"></script>
|
<script src="tasks/tasks.js?v=20260102"></script>
|
||||||
|
|
||||||
<!-- Application initialization -->
|
<!-- Application initialization -->
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
BIN
ui/suite/public/favicon.ico
Normal file
BIN
ui/suite/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
|
|
@ -178,40 +178,36 @@ function initWebSocket() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTaskProgressWebSocket() {
|
function initTaskProgressWebSocket() {
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
// Use the singleton WebSocket from tasks.js instead of creating a duplicate connection
|
||||||
const wsUrl = `${protocol}//${window.location.host}/ws/task-progress`;
|
// This prevents the "2 receivers" problem where manifest_update events go to one
|
||||||
|
// WebSocket while the browser UI is listening on a different one
|
||||||
|
|
||||||
try {
|
console.log("[AutoTask] Using singleton WebSocket for task progress");
|
||||||
AutoTaskState.progressWsConnection = new WebSocket(wsUrl);
|
|
||||||
|
|
||||||
AutoTaskState.progressWsConnection.onopen = function () {
|
// Create handler for task progress messages
|
||||||
console.log("Task Progress WebSocket connected");
|
const handler = function (data) {
|
||||||
};
|
handleTaskProgressMessage(data);
|
||||||
|
};
|
||||||
|
|
||||||
AutoTaskState.progressWsConnection.onmessage = function (event) {
|
// Store handler reference for cleanup
|
||||||
handleTaskProgressMessage(JSON.parse(event.data));
|
AutoTaskState._progressHandler = handler;
|
||||||
};
|
|
||||||
|
|
||||||
AutoTaskState.progressWsConnection.onclose = function () {
|
// Register with the global singleton WebSocket from tasks.js
|
||||||
console.log("Task Progress WebSocket disconnected, reconnecting...");
|
if (typeof registerTaskProgressHandler === "function") {
|
||||||
setTimeout(initTaskProgressWebSocket, 3000);
|
registerTaskProgressHandler(handler);
|
||||||
};
|
console.log("[AutoTask] Registered with singleton WebSocket");
|
||||||
|
} else {
|
||||||
AutoTaskState.progressWsConnection.onerror = function (error) {
|
// Fallback: wait for tasks.js to load and retry
|
||||||
console.error("Task Progress WebSocket error:", error);
|
console.log("[AutoTask] Waiting for tasks.js singleton to be available...");
|
||||||
};
|
setTimeout(initTaskProgressWebSocket, 500);
|
||||||
} catch (e) {
|
|
||||||
console.warn("Task Progress WebSocket not available");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTaskProgressMessage(data) {
|
function handleTaskProgressMessage(data) {
|
||||||
// Forward to ProgressPanel if available
|
// Note: ProgressPanel now registers its own handler with the singleton,
|
||||||
if (typeof ProgressPanel !== "undefined" && ProgressPanel.manifest) {
|
// so we don't need to forward messages manually here
|
||||||
ProgressPanel.handleProgressUpdate(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Task progress:", data);
|
console.log("[AutoTask] Task progress:", data.type, data.task_id);
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case "connected":
|
case "connected":
|
||||||
|
|
|
||||||
|
|
@ -1,197 +1,254 @@
|
||||||
const ProgressPanel = {
|
const ProgressPanel = {
|
||||||
manifest: null,
|
manifest: null,
|
||||||
wsConnection: null,
|
wsConnection: null, // Deprecated - now uses singleton from tasks.js
|
||||||
startTime: null,
|
startTime: null,
|
||||||
runtimeInterval: null,
|
runtimeInterval: null,
|
||||||
|
_boundHandler: null, // Store bound handler for cleanup
|
||||||
|
|
||||||
init(taskId) {
|
init(taskId) {
|
||||||
this.taskId = taskId;
|
// Clean up any existing handler before registering a new one
|
||||||
this.startTime = Date.now();
|
// This prevents duplicate handlers if init is called multiple times
|
||||||
this.startRuntimeCounter();
|
if (
|
||||||
this.connectWebSocket(taskId);
|
this._boundHandler &&
|
||||||
},
|
typeof unregisterTaskProgressHandler === "function"
|
||||||
|
) {
|
||||||
|
unregisterTaskProgressHandler(this._boundHandler);
|
||||||
|
this._boundHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
connectWebSocket(taskId) {
|
this.taskId = taskId;
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
this.startTime = Date.now();
|
||||||
const wsUrl = `${protocol}//${window.location.host}/ws/task-progress/${taskId}`;
|
this.startRuntimeCounter();
|
||||||
|
this.connectWebSocket(taskId);
|
||||||
|
},
|
||||||
|
|
||||||
this.wsConnection = new WebSocket(wsUrl);
|
connectWebSocket(taskId) {
|
||||||
|
// Instead of creating our own WebSocket, register with the singleton from tasks.js
|
||||||
|
// This prevents the "2 receivers" problem where manifest_update goes to one connection
|
||||||
|
// while the browser UI is listening on another
|
||||||
|
|
||||||
this.wsConnection.onopen = () => {
|
console.log("[ProgressPanel] Using singleton WebSocket for task:", taskId);
|
||||||
console.log('Progress panel WebSocket connected');
|
|
||||||
};
|
|
||||||
|
|
||||||
this.wsConnection.onmessage = (event) => {
|
// Create bound handler that filters for our task
|
||||||
const data = JSON.parse(event.data);
|
this._boundHandler = (data) => {
|
||||||
this.handleProgressUpdate(data);
|
// Only process messages for our task
|
||||||
};
|
if (data.task_id && String(data.task_id) !== String(taskId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.handleProgressUpdate(data);
|
||||||
|
};
|
||||||
|
|
||||||
this.wsConnection.onclose = () => {
|
// Register with the global singleton WebSocket
|
||||||
console.log('Progress panel WebSocket closed');
|
if (typeof registerTaskProgressHandler === "function") {
|
||||||
setTimeout(() => this.connectWebSocket(taskId), 3000);
|
registerTaskProgressHandler(this._boundHandler);
|
||||||
};
|
console.log("[ProgressPanel] Registered with singleton WebSocket");
|
||||||
|
} else {
|
||||||
|
// Fallback: wait for tasks.js to load and retry
|
||||||
|
console.log(
|
||||||
|
"[ProgressPanel] Waiting for tasks.js singleton to be available...",
|
||||||
|
);
|
||||||
|
setTimeout(() => this.connectWebSocket(taskId), 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
this.wsConnection.onerror = (error) => {
|
handleProgressUpdate(data) {
|
||||||
console.error('Progress panel WebSocket error:', error);
|
// Skip manifest_update - already handled by tasks.js renderManifestProgress()
|
||||||
};
|
// Processing it here would cause duplicate updates and race conditions
|
||||||
},
|
if (
|
||||||
|
data.type === "manifest_update" ||
|
||||||
|
data.event_type === "manifest_update"
|
||||||
|
) {
|
||||||
|
// Don't process here - tasks.js handles this via handleWebSocketMessage()
|
||||||
|
// which calls renderManifestProgress() with proper normalized ID handling
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
handleProgressUpdate(data) {
|
if (data.type === "section_update") {
|
||||||
if (data.type === 'manifest_update') {
|
this.updateSection(data.section_id, data.status, data.progress);
|
||||||
this.manifest = data.manifest;
|
} else if (data.type === "item_update") {
|
||||||
this.render();
|
this.updateItem(
|
||||||
} else if (data.type === 'section_update') {
|
data.section_id,
|
||||||
this.updateSection(data.section_id, data.status, data.progress);
|
data.item_id,
|
||||||
} else if (data.type === 'item_update') {
|
data.status,
|
||||||
this.updateItem(data.section_id, data.item_id, data.status, data.duration);
|
data.duration,
|
||||||
} else if (data.type === 'terminal_line') {
|
);
|
||||||
this.addTerminalLine(data.content, data.line_type);
|
} else if (data.type === "terminal_line") {
|
||||||
} else if (data.type === 'stats_update') {
|
this.addTerminalLine(data.content, data.line_type);
|
||||||
this.updateStats(data.stats);
|
} else if (data.type === "stats_update") {
|
||||||
} else if (data.type === 'task_progress') {
|
this.updateStats(data.stats);
|
||||||
this.handleTaskProgress(data);
|
} else if (data.type === "task_progress") {
|
||||||
|
this.handleTaskProgress(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTaskProgress(data) {
|
||||||
|
// Check for manifest in activity
|
||||||
|
if (data.activity && data.activity.manifest) {
|
||||||
|
this.manifest = data.activity.manifest;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check for manifest in details (manifest_update events)
|
||||||
|
if (
|
||||||
|
data.details &&
|
||||||
|
(data.step === "manifest_update" || data.event_type === "manifest_update")
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const parsed =
|
||||||
|
typeof data.details === "string"
|
||||||
|
? JSON.parse(data.details)
|
||||||
|
: data.details;
|
||||||
|
if (parsed && parsed.sections) {
|
||||||
|
this.manifest = parsed;
|
||||||
|
this.render();
|
||||||
}
|
}
|
||||||
},
|
} catch (e) {
|
||||||
|
// Not a manifest JSON, might be terminal output
|
||||||
|
console.debug("Details is not manifest JSON:", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleTaskProgress(data) {
|
if (data.step && data.step !== "manifest_update") {
|
||||||
if (data.activity && data.activity.manifest) {
|
this.updateCurrentAction(data.message || data.step);
|
||||||
this.manifest = data.activity.manifest;
|
}
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.step) {
|
// Only add non-manifest details as terminal lines
|
||||||
this.updateCurrentAction(data.message || data.step);
|
if (
|
||||||
}
|
data.details &&
|
||||||
|
data.step !== "manifest_update" &&
|
||||||
|
data.event_type !== "manifest_update"
|
||||||
|
) {
|
||||||
|
this.addTerminalLine(data.details, "info");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
if (data.details) {
|
startRuntimeCounter() {
|
||||||
this.addTerminalLine(data.details, 'info');
|
this.runtimeInterval = setInterval(() => {
|
||||||
}
|
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
||||||
},
|
const runtimeEl = document.getElementById("status-runtime");
|
||||||
|
if (runtimeEl) {
|
||||||
|
runtimeEl.textContent = this.formatDuration(elapsed);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
|
||||||
startRuntimeCounter() {
|
stopRuntimeCounter() {
|
||||||
this.runtimeInterval = setInterval(() => {
|
if (this.runtimeInterval) {
|
||||||
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
clearInterval(this.runtimeInterval);
|
||||||
const runtimeEl = document.getElementById('status-runtime');
|
this.runtimeInterval = null;
|
||||||
if (runtimeEl) {
|
}
|
||||||
runtimeEl.textContent = this.formatDuration(elapsed);
|
},
|
||||||
|
|
||||||
|
formatDuration(seconds) {
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${seconds} sec`;
|
||||||
|
} else if (seconds < 3600) {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
return `${mins} min`;
|
||||||
|
} else {
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const mins = Math.floor((seconds % 3600) / 60);
|
||||||
|
return `${hours} hr ${mins} min`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.manifest) return;
|
||||||
|
|
||||||
|
this.renderStatus();
|
||||||
|
this.renderProgressLog();
|
||||||
|
this.renderTerminal();
|
||||||
|
},
|
||||||
|
|
||||||
|
renderStatus() {
|
||||||
|
const titleEl = document.getElementById("status-title");
|
||||||
|
if (titleEl) {
|
||||||
|
titleEl.textContent = this.manifest.description || this.manifest.app_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const estimatedEl = document.getElementById("estimated-time");
|
||||||
|
if (estimatedEl && this.manifest.estimated_seconds) {
|
||||||
|
estimatedEl.textContent = this.formatDuration(
|
||||||
|
this.manifest.estimated_seconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentAction = this.getCurrentAction();
|
||||||
|
const actionEl = document.getElementById("current-action");
|
||||||
|
if (actionEl && currentAction) {
|
||||||
|
actionEl.textContent = currentAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateDecisionPoint();
|
||||||
|
},
|
||||||
|
|
||||||
|
getCurrentAction() {
|
||||||
|
if (!this.manifest || !this.manifest.sections) return null;
|
||||||
|
|
||||||
|
for (const section of this.manifest.sections) {
|
||||||
|
if (section.status === "Running") {
|
||||||
|
for (const child of section.children || []) {
|
||||||
|
if (child.status === "Running") {
|
||||||
|
for (const item of child.items || []) {
|
||||||
|
if (item.status === "Running") {
|
||||||
|
return item.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 1000);
|
return child.name;
|
||||||
},
|
}
|
||||||
|
|
||||||
stopRuntimeCounter() {
|
|
||||||
if (this.runtimeInterval) {
|
|
||||||
clearInterval(this.runtimeInterval);
|
|
||||||
this.runtimeInterval = null;
|
|
||||||
}
|
}
|
||||||
},
|
return section.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
formatDuration(seconds) {
|
updateCurrentAction(action) {
|
||||||
if (seconds < 60) {
|
const actionEl = document.getElementById("current-action");
|
||||||
return `${seconds} sec`;
|
if (actionEl) {
|
||||||
} else if (seconds < 3600) {
|
actionEl.textContent = action;
|
||||||
const mins = Math.floor(seconds / 60);
|
}
|
||||||
return `${mins} min`;
|
},
|
||||||
} else {
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const mins = Math.floor((seconds % 3600) / 60);
|
|
||||||
return `${hours} hr ${mins} min`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
updateDecisionPoint() {
|
||||||
if (!this.manifest) return;
|
const decisionStepEl = document.getElementById("decision-step");
|
||||||
|
const decisionTotalEl = document.getElementById("decision-total");
|
||||||
|
|
||||||
this.renderStatus();
|
if (decisionStepEl && this.manifest) {
|
||||||
this.renderProgressLog();
|
decisionStepEl.textContent = this.manifest.completed_steps || 0;
|
||||||
this.renderTerminal();
|
}
|
||||||
},
|
if (decisionTotalEl && this.manifest) {
|
||||||
|
decisionTotalEl.textContent = this.manifest.total_steps || 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
renderStatus() {
|
renderProgressLog() {
|
||||||
const titleEl = document.getElementById('status-title');
|
const container = document.getElementById("progress-log-content");
|
||||||
if (titleEl) {
|
if (!container || !this.manifest || !this.manifest.sections) return;
|
||||||
titleEl.textContent = this.manifest.description || this.manifest.app_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const estimatedEl = document.getElementById('estimated-time');
|
container.innerHTML = "";
|
||||||
if (estimatedEl && this.manifest.estimated_seconds) {
|
|
||||||
estimatedEl.textContent = this.formatDuration(this.manifest.estimated_seconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentAction = this.getCurrentAction();
|
for (const section of this.manifest.sections) {
|
||||||
const actionEl = document.getElementById('current-action');
|
const sectionEl = this.createSectionElement(section);
|
||||||
if (actionEl && currentAction) {
|
container.appendChild(sectionEl);
|
||||||
actionEl.textContent = currentAction;
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
this.updateDecisionPoint();
|
createSectionElement(section) {
|
||||||
},
|
const sectionDiv = document.createElement("div");
|
||||||
|
sectionDiv.className = "log-section";
|
||||||
|
sectionDiv.dataset.sectionId = section.id;
|
||||||
|
|
||||||
getCurrentAction() {
|
if (section.status === "Running" || section.status === "Completed") {
|
||||||
if (!this.manifest || !this.manifest.sections) return null;
|
sectionDiv.classList.add("expanded");
|
||||||
|
}
|
||||||
|
|
||||||
for (const section of this.manifest.sections) {
|
const statusClass = section.status.toLowerCase();
|
||||||
if (section.status === 'Running') {
|
// Support both direct fields and nested progress object
|
||||||
for (const child of section.children || []) {
|
const stepCurrent = section.current_step ?? section.progress?.current ?? 0;
|
||||||
if (child.status === 'Running') {
|
const stepTotal = section.total_steps ?? section.progress?.total ?? 0;
|
||||||
for (const item of child.items || []) {
|
|
||||||
if (item.status === 'Running') {
|
|
||||||
return item.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return child.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return section.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateCurrentAction(action) {
|
sectionDiv.innerHTML = `
|
||||||
const actionEl = document.getElementById('current-action');
|
|
||||||
if (actionEl) {
|
|
||||||
actionEl.textContent = action;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateDecisionPoint() {
|
|
||||||
const decisionStepEl = document.getElementById('decision-step');
|
|
||||||
const decisionTotalEl = document.getElementById('decision-total');
|
|
||||||
|
|
||||||
if (decisionStepEl && this.manifest) {
|
|
||||||
decisionStepEl.textContent = this.manifest.completed_steps || 0;
|
|
||||||
}
|
|
||||||
if (decisionTotalEl && this.manifest) {
|
|
||||||
decisionTotalEl.textContent = this.manifest.total_steps || 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderProgressLog() {
|
|
||||||
const container = document.getElementById('progress-log-content');
|
|
||||||
if (!container || !this.manifest || !this.manifest.sections) return;
|
|
||||||
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
for (const section of this.manifest.sections) {
|
|
||||||
const sectionEl = this.createSectionElement(section);
|
|
||||||
container.appendChild(sectionEl);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
createSectionElement(section) {
|
|
||||||
const sectionDiv = document.createElement('div');
|
|
||||||
sectionDiv.className = 'log-section';
|
|
||||||
sectionDiv.dataset.sectionId = section.id;
|
|
||||||
|
|
||||||
if (section.status === 'Running' || section.status === 'Completed') {
|
|
||||||
sectionDiv.classList.add('expanded');
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusClass = section.status.toLowerCase();
|
|
||||||
const stepCurrent = section.current_step || 0;
|
|
||||||
const stepTotal = section.total_steps || 0;
|
|
||||||
|
|
||||||
sectionDiv.innerHTML = `
|
|
||||||
<div class="log-section-header" onclick="ProgressPanel.toggleSection('${section.id}')">
|
<div class="log-section-header" onclick="ProgressPanel.toggleSection('${section.id}')">
|
||||||
<span class="section-indicator ${statusClass}"></span>
|
<span class="section-indicator ${statusClass}"></span>
|
||||||
<span class="section-name">${this.escapeHtml(section.name)}</span>
|
<span class="section-name">${this.escapeHtml(section.name)}</span>
|
||||||
|
|
@ -205,38 +262,45 @@ const ProgressPanel = {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const childrenContainer = sectionDiv.querySelector('.log-children');
|
const childrenContainer = sectionDiv.querySelector(".log-children");
|
||||||
|
|
||||||
for (const child of section.children || []) {
|
for (const child of section.children || []) {
|
||||||
const childEl = this.createChildElement(child, section.id);
|
const childEl = this.createChildElement(child, section.id);
|
||||||
childrenContainer.appendChild(childEl);
|
childrenContainer.appendChild(childEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section.items && section.items.length > 0 && (!section.children || section.children.length === 0)) {
|
if (
|
||||||
for (const item of section.items) {
|
section.items &&
|
||||||
const itemEl = this.createItemElement(item);
|
section.items.length > 0 &&
|
||||||
childrenContainer.appendChild(itemEl);
|
(!section.children || section.children.length === 0)
|
||||||
}
|
) {
|
||||||
}
|
for (const item of section.items) {
|
||||||
|
const itemEl = this.createItemElement(item);
|
||||||
|
childrenContainer.appendChild(itemEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return sectionDiv;
|
return sectionDiv;
|
||||||
},
|
},
|
||||||
|
|
||||||
createChildElement(child, parentId) {
|
createChildElement(child, parentId) {
|
||||||
const childDiv = document.createElement('div');
|
const childDiv = document.createElement("div");
|
||||||
childDiv.className = 'log-child';
|
childDiv.className = "log-child";
|
||||||
childDiv.dataset.childId = child.id;
|
childDiv.dataset.childId = child.id;
|
||||||
|
|
||||||
if (child.status === 'Running' || child.status === 'Completed') {
|
if (child.status === "Running" || child.status === "Completed") {
|
||||||
childDiv.classList.add('expanded');
|
childDiv.classList.add("expanded");
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusClass = child.status.toLowerCase();
|
const statusClass = child.status.toLowerCase();
|
||||||
const stepCurrent = child.current_step || 0;
|
// Support both direct fields and nested progress object
|
||||||
const stepTotal = child.total_steps || 0;
|
const stepCurrent = child.current_step ?? child.progress?.current ?? 0;
|
||||||
const duration = child.duration_seconds ? this.formatDuration(child.duration_seconds) : '';
|
const stepTotal = child.total_steps ?? child.progress?.total ?? 0;
|
||||||
|
const duration = child.duration_seconds
|
||||||
|
? this.formatDuration(child.duration_seconds)
|
||||||
|
: "";
|
||||||
|
|
||||||
childDiv.innerHTML = `
|
childDiv.innerHTML = `
|
||||||
<div class="log-child-header" onclick="ProgressPanel.toggleChild('${child.id}')">
|
<div class="log-child-header" onclick="ProgressPanel.toggleChild('${child.id}')">
|
||||||
<span class="child-indent"></span>
|
<span class="child-indent"></span>
|
||||||
<span class="child-name">${this.escapeHtml(child.name)}</span>
|
<span class="child-name">${this.escapeHtml(child.name)}</span>
|
||||||
|
|
@ -250,215 +314,240 @@ const ProgressPanel = {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const itemsContainer = childDiv.querySelector('.log-items');
|
const itemsContainer = childDiv.querySelector(".log-items");
|
||||||
|
|
||||||
for (const item of child.items || []) {
|
for (const item of child.items || []) {
|
||||||
const itemEl = this.createItemElement(item);
|
const itemEl = this.createItemElement(item);
|
||||||
itemsContainer.appendChild(itemEl);
|
itemsContainer.appendChild(itemEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return childDiv;
|
return childDiv;
|
||||||
},
|
},
|
||||||
|
|
||||||
createItemElement(item) {
|
createItemElement(item) {
|
||||||
const itemDiv = document.createElement('div');
|
const itemDiv = document.createElement("div");
|
||||||
itemDiv.className = 'log-item';
|
itemDiv.className = "log-item";
|
||||||
itemDiv.dataset.itemId = item.id;
|
itemDiv.dataset.itemId = item.id;
|
||||||
|
|
||||||
const statusClass = item.status.toLowerCase();
|
const statusClass = item.status.toLowerCase();
|
||||||
const duration = item.duration_seconds ? `Duration: ${this.formatDuration(item.duration_seconds)}` : '';
|
const duration = item.duration_seconds
|
||||||
const checkIcon = item.status === 'Completed' ? '✓' : (item.status === 'Running' ? '◎' : '○');
|
? `Duration: ${this.formatDuration(item.duration_seconds)}`
|
||||||
|
: "";
|
||||||
|
const checkIcon =
|
||||||
|
item.status === "Completed" ? "✓" : item.status === "Running" ? "◎" : "○";
|
||||||
|
|
||||||
itemDiv.innerHTML = `
|
itemDiv.innerHTML = `
|
||||||
<span class="item-dot ${statusClass}"></span>
|
<span class="item-dot ${statusClass}"></span>
|
||||||
<span class="item-name">${this.escapeHtml(item.name)}${item.details ? ` - ${this.escapeHtml(item.details)}` : ''}</span>
|
<span class="item-name">${this.escapeHtml(item.name)}${item.details ? ` - ${this.escapeHtml(item.details)}` : ""}</span>
|
||||||
<div class="item-info">
|
<div class="item-info">
|
||||||
<span class="item-duration">${duration}</span>
|
<span class="item-duration">${duration}</span>
|
||||||
<span class="item-check ${statusClass}">${checkIcon}</span>
|
<span class="item-check ${statusClass}">${checkIcon}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return itemDiv;
|
return itemDiv;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderTerminal() {
|
renderTerminal() {
|
||||||
if (!this.manifest || !this.manifest.terminal_output) return;
|
// Support both formats: terminal_output (direct) and terminal.lines (web JSON)
|
||||||
|
const terminalLines =
|
||||||
|
this.manifest?.terminal_output || this.manifest?.terminal?.lines || [];
|
||||||
|
|
||||||
const container = document.getElementById('terminal-content');
|
if (!terminalLines.length) return;
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
container.innerHTML = '';
|
const container = document.getElementById("terminal-content");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
for (const line of this.manifest.terminal_output.slice(-50)) {
|
container.innerHTML = "";
|
||||||
this.appendTerminalLine(container, line.content, line.line_type || 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
container.scrollTop = container.scrollHeight;
|
for (const line of terminalLines.slice(-50)) {
|
||||||
},
|
this.appendTerminalLine(
|
||||||
|
container,
|
||||||
addTerminalLine(content, lineType) {
|
line.content,
|
||||||
const container = document.getElementById('terminal-content');
|
line.type || line.line_type || "info",
|
||||||
if (!container) return;
|
);
|
||||||
|
|
||||||
this.appendTerminalLine(container, content, lineType);
|
|
||||||
container.scrollTop = container.scrollHeight;
|
|
||||||
|
|
||||||
this.incrementProcessedCount();
|
|
||||||
},
|
|
||||||
|
|
||||||
appendTerminalLine(container, content, lineType) {
|
|
||||||
const lineDiv = document.createElement('div');
|
|
||||||
lineDiv.className = `terminal-line ${lineType || 'info'}`;
|
|
||||||
lineDiv.textContent = content;
|
|
||||||
container.appendChild(lineDiv);
|
|
||||||
},
|
|
||||||
|
|
||||||
incrementProcessedCount() {
|
|
||||||
const processedEl = document.getElementById('terminal-processed');
|
|
||||||
if (processedEl) {
|
|
||||||
const current = parseInt(processedEl.textContent, 10) || 0;
|
|
||||||
processedEl.textContent = current + 1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateStats(stats) {
|
|
||||||
const processedEl = document.getElementById('terminal-processed');
|
|
||||||
if (processedEl && stats.data_points_processed !== undefined) {
|
|
||||||
processedEl.textContent = stats.data_points_processed;
|
|
||||||
}
|
|
||||||
|
|
||||||
const speedEl = document.getElementById('terminal-speed');
|
|
||||||
if (speedEl && stats.sources_per_min !== undefined) {
|
|
||||||
speedEl.textContent = `~${stats.sources_per_min.toFixed(1)} sources/min`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const etaEl = document.getElementById('terminal-eta');
|
|
||||||
if (etaEl && stats.estimated_remaining_seconds !== undefined) {
|
|
||||||
etaEl.textContent = this.formatDuration(stats.estimated_remaining_seconds);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSection(sectionId, status, progress) {
|
|
||||||
const sectionEl = document.querySelector(`[data-section-id="${sectionId}"]`);
|
|
||||||
if (!sectionEl) return;
|
|
||||||
|
|
||||||
const indicator = sectionEl.querySelector('.section-indicator');
|
|
||||||
const statusBadge = sectionEl.querySelector('.section-status-badge');
|
|
||||||
const stepBadge = sectionEl.querySelector('.section-step-badge');
|
|
||||||
|
|
||||||
if (indicator) {
|
|
||||||
indicator.className = `section-indicator ${status.toLowerCase()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusBadge) {
|
|
||||||
statusBadge.className = `section-status-badge ${status.toLowerCase()}`;
|
|
||||||
statusBadge.textContent = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stepBadge && progress) {
|
|
||||||
stepBadge.textContent = `Step ${progress.current}/${progress.total}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'Running' || status === 'Completed') {
|
|
||||||
sectionEl.classList.add('expanded');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateItem(sectionId, itemId, status, duration) {
|
|
||||||
const itemEl = document.querySelector(`[data-item-id="${itemId}"]`);
|
|
||||||
if (!itemEl) return;
|
|
||||||
|
|
||||||
const dot = itemEl.querySelector('.item-dot');
|
|
||||||
const check = itemEl.querySelector('.item-check');
|
|
||||||
const durationEl = itemEl.querySelector('.item-duration');
|
|
||||||
|
|
||||||
const statusClass = status.toLowerCase();
|
|
||||||
|
|
||||||
if (dot) {
|
|
||||||
dot.className = `item-dot ${statusClass}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (check) {
|
|
||||||
check.className = `item-check ${statusClass}`;
|
|
||||||
check.textContent = status === 'Completed' ? '✓' : (status === 'Running' ? '◎' : '○');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (durationEl && duration) {
|
|
||||||
durationEl.textContent = `Duration: ${this.formatDuration(duration)}`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleSection(sectionId) {
|
|
||||||
const sectionEl = document.querySelector(`[data-section-id="${sectionId}"]`);
|
|
||||||
if (sectionEl) {
|
|
||||||
sectionEl.classList.toggle('expanded');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleChild(childId) {
|
|
||||||
const childEl = document.querySelector(`[data-child-id="${childId}"]`);
|
|
||||||
if (childEl) {
|
|
||||||
childEl.classList.toggle('expanded');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
viewDetails(sectionId) {
|
|
||||||
console.log('View details for section:', sectionId);
|
|
||||||
},
|
|
||||||
|
|
||||||
viewChildDetails(childId) {
|
|
||||||
console.log('View details for child:', childId);
|
|
||||||
},
|
|
||||||
|
|
||||||
escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
},
|
|
||||||
|
|
||||||
loadManifest(taskId) {
|
|
||||||
fetch(`/api/autotask/${taskId}/manifest`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success && data.manifest) {
|
|
||||||
this.manifest = data.manifest;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Failed to load manifest:', error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.stopRuntimeCounter();
|
|
||||||
if (this.wsConnection) {
|
|
||||||
this.wsConnection.close();
|
|
||||||
this.wsConnection = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
},
|
||||||
|
|
||||||
|
addTerminalLine(content, lineType) {
|
||||||
|
const container = document.getElementById("terminal-content");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
this.appendTerminalLine(container, content, lineType);
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
|
||||||
|
this.incrementProcessedCount();
|
||||||
|
},
|
||||||
|
|
||||||
|
appendTerminalLine(container, content, lineType) {
|
||||||
|
const lineDiv = document.createElement("div");
|
||||||
|
lineDiv.className = `terminal-line ${lineType || "info"}`;
|
||||||
|
lineDiv.textContent = content;
|
||||||
|
container.appendChild(lineDiv);
|
||||||
|
},
|
||||||
|
|
||||||
|
incrementProcessedCount() {
|
||||||
|
const processedEl = document.getElementById("terminal-processed");
|
||||||
|
if (processedEl) {
|
||||||
|
const current = parseInt(processedEl.textContent, 10) || 0;
|
||||||
|
processedEl.textContent = current + 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStats(stats) {
|
||||||
|
const processedEl = document.getElementById("terminal-processed");
|
||||||
|
if (processedEl && stats.data_points_processed !== undefined) {
|
||||||
|
processedEl.textContent = stats.data_points_processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const speedEl = document.getElementById("terminal-speed");
|
||||||
|
if (speedEl && stats.sources_per_min !== undefined) {
|
||||||
|
speedEl.textContent = `~${stats.sources_per_min.toFixed(1)} sources/min`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const etaEl = document.getElementById("terminal-eta");
|
||||||
|
if (etaEl && stats.estimated_remaining_seconds !== undefined) {
|
||||||
|
etaEl.textContent = this.formatDuration(
|
||||||
|
stats.estimated_remaining_seconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSection(sectionId, status, progress) {
|
||||||
|
const sectionEl = document.querySelector(
|
||||||
|
`[data-section-id="${sectionId}"]`,
|
||||||
|
);
|
||||||
|
if (!sectionEl) return;
|
||||||
|
|
||||||
|
const indicator = sectionEl.querySelector(".section-indicator");
|
||||||
|
const statusBadge = sectionEl.querySelector(".section-status-badge");
|
||||||
|
const stepBadge = sectionEl.querySelector(".section-step-badge");
|
||||||
|
|
||||||
|
if (indicator) {
|
||||||
|
indicator.className = `section-indicator ${status.toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusBadge) {
|
||||||
|
statusBadge.className = `section-status-badge ${status.toLowerCase()}`;
|
||||||
|
statusBadge.textContent = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepBadge && progress) {
|
||||||
|
stepBadge.textContent = `Step ${progress.current}/${progress.total}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "Running" || status === "Completed") {
|
||||||
|
sectionEl.classList.add("expanded");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateItem(sectionId, itemId, status, duration) {
|
||||||
|
const itemEl = document.querySelector(`[data-item-id="${itemId}"]`);
|
||||||
|
if (!itemEl) return;
|
||||||
|
|
||||||
|
const dot = itemEl.querySelector(".item-dot");
|
||||||
|
const check = itemEl.querySelector(".item-check");
|
||||||
|
const durationEl = itemEl.querySelector(".item-duration");
|
||||||
|
|
||||||
|
const statusClass = status.toLowerCase();
|
||||||
|
|
||||||
|
if (dot) {
|
||||||
|
dot.className = `item-dot ${statusClass}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (check) {
|
||||||
|
check.className = `item-check ${statusClass}`;
|
||||||
|
check.textContent =
|
||||||
|
status === "Completed" ? "✓" : status === "Running" ? "◎" : "○";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (durationEl && duration) {
|
||||||
|
durationEl.textContent = `Duration: ${this.formatDuration(duration)}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleSection(sectionId) {
|
||||||
|
const sectionEl = document.querySelector(
|
||||||
|
`[data-section-id="${sectionId}"]`,
|
||||||
|
);
|
||||||
|
if (sectionEl) {
|
||||||
|
sectionEl.classList.toggle("expanded");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleChild(childId) {
|
||||||
|
const childEl = document.querySelector(`[data-child-id="${childId}"]`);
|
||||||
|
if (childEl) {
|
||||||
|
childEl.classList.toggle("expanded");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
viewDetails(sectionId) {
|
||||||
|
console.log("View details for section:", sectionId);
|
||||||
|
},
|
||||||
|
|
||||||
|
viewChildDetails(childId) {
|
||||||
|
console.log("View details for child:", childId);
|
||||||
|
},
|
||||||
|
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
},
|
||||||
|
|
||||||
|
loadManifest(taskId) {
|
||||||
|
fetch(`/api/autotask/${taskId}/manifest`)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.success && data.manifest) {
|
||||||
|
this.manifest = data.manifest;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to load manifest:", error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.stopRuntimeCounter();
|
||||||
|
// Unregister from singleton instead of closing our own connection
|
||||||
|
if (
|
||||||
|
this._boundHandler &&
|
||||||
|
typeof unregisterTaskProgressHandler === "function"
|
||||||
|
) {
|
||||||
|
unregisterTaskProgressHandler(this._boundHandler);
|
||||||
|
this._boundHandler = null;
|
||||||
|
console.log("[ProgressPanel] Unregistered from singleton WebSocket");
|
||||||
|
}
|
||||||
|
// Don't close the singleton connection - other components may be using it
|
||||||
|
this.wsConnection = null;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function toggleLogSection(header) {
|
function toggleLogSection(header) {
|
||||||
const section = header.closest('.log-section');
|
const section = header.closest(".log-section");
|
||||||
if (section) {
|
if (section) {
|
||||||
section.classList.toggle('expanded');
|
section.classList.toggle("expanded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLogChild(header) {
|
function toggleLogChild(header) {
|
||||||
const child = header.closest('.log-child');
|
const child = header.closest(".log-child");
|
||||||
if (child) {
|
if (child) {
|
||||||
child.classList.toggle('expanded');
|
child.classList.toggle("expanded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function viewSectionDetails(sectionId) {
|
function viewSectionDetails(sectionId) {
|
||||||
ProgressPanel.viewDetails(sectionId);
|
ProgressPanel.viewDetails(sectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function viewChildDetails(childId) {
|
function viewChildDetails(childId) {
|
||||||
ProgressPanel.viewChildDetails(childId);
|
ProgressPanel.viewChildDetails(childId);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.ProgressPanel = ProgressPanel;
|
window.ProgressPanel = ProgressPanel;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
max-height: 100vh;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -17,6 +18,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
background: var(--bg, #0a0a0a);
|
background: var(--bg, #0a0a0a);
|
||||||
color: var(--text-secondary, #e0e0e0);
|
color: var(--text-secondary, #e0e0e0);
|
||||||
|
|
@ -95,6 +97,31 @@
|
||||||
/* Section Container */
|
/* Section Container */
|
||||||
.taskmd-section {
|
.taskmd-section {
|
||||||
border-bottom: 1px solid var(--border, #1a1a1a);
|
border-bottom: 1px solid var(--border, #1a1a1a);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status section - compact */
|
||||||
|
.taskmd-section-status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress log section - scrollable */
|
||||||
|
.taskmd-section-progress {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-height: 100px;
|
||||||
|
max-height: 40%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Terminal section - takes remaining space */
|
||||||
|
.taskmd-section-terminal {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 150px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.taskmd-section-header {
|
.taskmd-section-header {
|
||||||
|
|
@ -206,9 +233,9 @@
|
||||||
/* PROGRESS LOG Section */
|
/* PROGRESS LOG Section */
|
||||||
.taskmd-progress-content {
|
.taskmd-progress-content {
|
||||||
background: var(--bg, #0a0a0a);
|
background: var(--bg, #0a0a0a);
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-height: 350px;
|
max-height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -516,11 +543,10 @@
|
||||||
|
|
||||||
/* TERMINAL Section */
|
/* TERMINAL Section */
|
||||||
.taskmd-terminal {
|
.taskmd-terminal {
|
||||||
flex-shrink: 0;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 120px;
|
min-height: 120px;
|
||||||
max-height: 200px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -575,7 +601,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.taskmd-terminal-output {
|
.taskmd-terminal-output {
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
background: var(--bg, #0a0a0a);
|
background: var(--bg, #0a0a0a);
|
||||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
font-family: "JetBrains Mono", "Fira Code", monospace;
|
||||||
|
|
@ -583,9 +609,118 @@
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: var(--text-secondary, #888);
|
color: var(--text-secondary, #888);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Markdown content in terminal */
|
||||||
|
.taskmd-terminal-output .markdown-content {
|
||||||
|
font-family: var(
|
||||||
|
--sentient-font-family,
|
||||||
|
"Inter",
|
||||||
|
-apple-system,
|
||||||
|
sans-serif
|
||||||
|
);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-secondary, #ccc);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content h1,
|
||||||
|
.taskmd-terminal-output .markdown-content h2,
|
||||||
|
.taskmd-terminal-output .markdown-content h3 {
|
||||||
|
color: var(--text, #fff);
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.taskmd-terminal-output .markdown-content h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.taskmd-terminal-output .markdown-content h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content p {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content ul,
|
||||||
|
.taskmd-terminal-output .markdown-content ol {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content li {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content pre {
|
||||||
|
background: #151515;
|
||||||
|
border: 1px solid #252525;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 12px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: "JetBrains Mono", "Fira Code", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content code {
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: "JetBrains Mono", "Fira Code", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--accent, #c5f82a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content pre code {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content blockquote {
|
||||||
|
border-left: 3px solid var(--accent, #c5f82a);
|
||||||
|
padding-left: 16px;
|
||||||
|
margin: 12px 0;
|
||||||
|
color: var(--text-muted, #888);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content th,
|
||||||
|
.taskmd-terminal-output .markdown-content td {
|
||||||
|
border: 1px solid #252525;
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content th {
|
||||||
|
background: #151515;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content a {
|
||||||
|
color: var(--accent, #c5f82a);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .markdown-content a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.taskmd-terminal-output .terminal-line {
|
.taskmd-terminal-output .terminal-line {
|
||||||
padding: 3px 0;
|
padding: 3px 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|
@ -619,12 +754,14 @@
|
||||||
.taskmd-terminal-output .terminal-code {
|
.taskmd-terminal-output .terminal-code {
|
||||||
background: #151515;
|
background: #151515;
|
||||||
border: 1px solid #252525;
|
border: 1px solid #252525;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
padding: 8px 12px;
|
padding: 12px 16px;
|
||||||
margin: 6px 0;
|
margin: 8px 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Checkmark items */
|
/* Checkmark items */
|
||||||
|
|
@ -660,14 +797,110 @@
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Terminal markdown elements */
|
||||||
|
.taskmd-terminal-output .terminal-h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text, #fff);
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
font-family: var(--sentient-font-family, "Inter", sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .terminal-h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text, #fff);
|
||||||
|
margin: 14px 0 6px 0;
|
||||||
|
font-family: var(--sentient-font-family, "Inter", sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .terminal-h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text, #fff);
|
||||||
|
margin: 12px 0 4px 0;
|
||||||
|
font-family: var(--sentient-font-family, "Inter", sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .terminal-p {
|
||||||
|
margin: 8px 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .terminal-ul,
|
||||||
|
.taskmd-terminal-output .terminal-ol {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .terminal-li {
|
||||||
|
margin: 4px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .terminal-li::before {
|
||||||
|
content: "•";
|
||||||
|
position: absolute;
|
||||||
|
left: -16px;
|
||||||
|
color: var(--accent, #c5f82a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .terminal-oli {
|
||||||
|
margin: 4px 0;
|
||||||
|
list-style: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .terminal-inline-code {
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: "JetBrains Mono", "Fira Code", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--accent, #c5f82a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .terminal-quote {
|
||||||
|
border-left: 3px solid var(--accent, #c5f82a);
|
||||||
|
padding-left: 16px;
|
||||||
|
margin: 12px 0;
|
||||||
|
color: var(--text-muted, #888);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .terminal-hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border, #252525);
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .check-mark {
|
||||||
|
color: var(--success, #22c55e);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output .check-empty {
|
||||||
|
color: var(--text-muted, #555);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output a {
|
||||||
|
color: var(--accent, #c5f82a);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskmd-terminal-output a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
.taskmd-actions {
|
.taskmd-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 16px 24px;
|
padding: 20px 24px;
|
||||||
border-top: 1px solid var(--border, #1a1a1a);
|
border-top: 1px solid var(--border, #1a1a1a);
|
||||||
background: var(--bg-secondary, #0d0d0d);
|
background: var(--bg-secondary, #0d0d0d);
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.taskmd-actions .btn-action-rich {
|
.taskmd-actions .btn-action-rich {
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,8 @@
|
||||||
class="tasks-list-scroll"
|
class="tasks-list-scroll"
|
||||||
id="task-list"
|
id="task-list"
|
||||||
hx-get="/api/tasks?filter=all"
|
hx-get="/api/tasks?filter=all"
|
||||||
hx-trigger="load, taskCreated from:body"
|
hx-trigger="load, taskCreated from:body throttle:2s"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML transition:false"
|
||||||
>
|
>
|
||||||
<!-- Loading state - replaced by HTMX -->
|
<!-- Loading state - replaced by HTMX -->
|
||||||
<div class="loading-state">
|
<div class="loading-state">
|
||||||
|
|
@ -334,19 +334,12 @@
|
||||||
"quick-intent-input",
|
"quick-intent-input",
|
||||||
).value = "";
|
).value = "";
|
||||||
|
|
||||||
// Trigger task list refresh
|
// Select the task and let tasks.js handle polling
|
||||||
htmx.trigger(document.body, "taskCreated");
|
selectTask(response.task_id);
|
||||||
|
// Hide success message after task is selected
|
||||||
// After a short delay to let the list reload, select the new task
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
selectTask(response.task_id);
|
intentResult.style.display = "none";
|
||||||
// Start polling for updates
|
}, 2000);
|
||||||
startTaskPolling(response.task_id);
|
|
||||||
// Hide success message after task is selected
|
|
||||||
setTimeout(function () {
|
|
||||||
intentResult.style.display = "none";
|
|
||||||
}, 2000);
|
|
||||||
}, 500);
|
|
||||||
} else {
|
} else {
|
||||||
intentResult.innerHTML = `<span class="intent-error">✗ ${response.message || "Failed to create task"}</span>`;
|
intentResult.innerHTML = `<span class="intent-error">✗ ${response.message || "Failed to create task"}</span>`;
|
||||||
intentResult.style.display = "block";
|
intentResult.style.display = "block";
|
||||||
|
|
@ -369,55 +362,6 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Poll for task status updates
|
|
||||||
let taskPollingInterval = null;
|
|
||||||
function startTaskPolling(taskId) {
|
|
||||||
// Clear any existing polling
|
|
||||||
if (taskPollingInterval) {
|
|
||||||
clearInterval(taskPollingInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
taskPollingInterval = setInterval(function () {
|
|
||||||
fetch(`/api/tasks/${taskId}`, {
|
|
||||||
headers: { Accept: "application/json" },
|
|
||||||
})
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((task) => {
|
|
||||||
console.log("[TASK] Poll status:", task.status);
|
|
||||||
|
|
||||||
// Refresh the detail panel
|
|
||||||
htmx.ajax("GET", `/api/tasks/${taskId}`, {
|
|
||||||
target: "#task-detail-content",
|
|
||||||
swap: "innerHTML",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Refresh task list to update status badges
|
|
||||||
htmx.trigger(document.body, "taskCreated");
|
|
||||||
|
|
||||||
// Stop polling if task is complete or failed
|
|
||||||
if (
|
|
||||||
task.status === "completed" ||
|
|
||||||
task.status === "failed" ||
|
|
||||||
task.status === "cancelled"
|
|
||||||
) {
|
|
||||||
clearInterval(taskPollingInterval);
|
|
||||||
taskPollingInterval = null;
|
|
||||||
loadTaskStats();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.warn("Failed to poll task:", e);
|
|
||||||
});
|
|
||||||
}, 2000); // Poll every 2 seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopTaskPolling() {
|
|
||||||
if (taskPollingInterval) {
|
|
||||||
clearInterval(taskPollingInterval);
|
|
||||||
taskPollingInterval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load task statistics
|
// Load task statistics
|
||||||
function loadTaskStats() {
|
function loadTaskStats() {
|
||||||
fetch("/api/tasks/stats/json")
|
fetch("/api/tasks/stats/json")
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue