feat(tasks): Add task overview page with active, completed, and failed task summaries
This commit is contained in:
@@ -214,6 +214,21 @@
|
||||
data-bs-target="#truncate-series-modal">
|
||||
<i class="bi bi-scissors"></i> Truncate series
|
||||
</button>
|
||||
|
||||
<div class="card mt-2">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span class="small fw-semibold">Background Task Progress</span>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-outline-info btn-sm" href="{% url 'atlas:task_overview' %}">Task Overview</a>
|
||||
<button id="clear-recon-monitors-btn" type="button" class="btn btn-outline-secondary btn-sm">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<div id="persistent-recon-tasks" class="d-flex flex-column gap-2">
|
||||
<div id="persistent-recon-empty" class="small text-muted">No tracked reconstruction tasks yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -399,6 +414,7 @@
|
||||
const totalImages = {{ image_url_array_and_count.1 }};
|
||||
const viewerHeightKey = `series_viewer_height_${seriesPk}`;
|
||||
const truncateViewerHeightKey = `truncate_viewer_height_${seriesPk}`;
|
||||
const reconTaskStorageKey = `series_recon_tasks_${seriesPk}`;
|
||||
|
||||
// Restore viewer height from localStorage
|
||||
function restoreViewerHeight() {
|
||||
@@ -465,6 +481,94 @@
|
||||
syncOptimizeBounds();
|
||||
}
|
||||
|
||||
function getStoredReconTasks() {
|
||||
try {
|
||||
const raw = localStorage.getItem(reconTaskStorageKey);
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function setStoredReconTasks(items) {
|
||||
localStorage.setItem(reconTaskStorageKey, JSON.stringify(items));
|
||||
}
|
||||
|
||||
function upsertStoredReconTask(taskId, statusUrl) {
|
||||
const current = getStoredReconTasks();
|
||||
const existing = current.find((t) => t.taskId === taskId);
|
||||
if (existing) {
|
||||
existing.statusUrl = statusUrl;
|
||||
existing.addedAt = Date.now();
|
||||
} else {
|
||||
current.unshift({ taskId, statusUrl, addedAt: Date.now() });
|
||||
}
|
||||
setStoredReconTasks(current.slice(0, 25));
|
||||
}
|
||||
|
||||
function removeStoredReconTask(taskId) {
|
||||
const filtered = getStoredReconTasks().filter((t) => t.taskId !== taskId);
|
||||
setStoredReconTasks(filtered);
|
||||
}
|
||||
|
||||
function createPersistentTaskMonitor(taskId, statusUrl) {
|
||||
const wrap = document.getElementById('persistent-recon-tasks');
|
||||
const empty = document.getElementById('persistent-recon-empty');
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cardId = `persistent-recon-card-${taskId}`;
|
||||
if (document.getElementById(cardId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
empty?.classList.add('d-none');
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.id = cardId;
|
||||
card.className = 'border rounded p-2 bg-body-tertiary';
|
||||
|
||||
const statusId = `persistent-recon-status-${taskId}`;
|
||||
const statusUrlWithTarget = `${statusUrl}?target_id=${statusId}`;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="d-flex align-items-center justify-content-between mb-1">
|
||||
<div class="small fw-semibold">Recon task ${taskId}</div>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-remove-task-id="${taskId}">Hide</button>
|
||||
</div>
|
||||
<div id="${statusId}" hx-get="${statusUrlWithTarget}" hx-trigger="load, every 4s" hx-swap="outerHTML">
|
||||
<div class="small text-muted">Loading task status...</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
wrap.prepend(card);
|
||||
card.querySelector('[data-remove-task-id]')?.addEventListener('click', () => {
|
||||
removeStoredReconTask(taskId);
|
||||
card.remove();
|
||||
if (!wrap.querySelector('[id^="persistent-recon-card-"]')) {
|
||||
empty?.classList.remove('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
if (window.htmx) {
|
||||
window.htmx.process(card);
|
||||
}
|
||||
}
|
||||
|
||||
function renderStoredReconTaskMonitors() {
|
||||
const tasks = getStoredReconTasks();
|
||||
tasks.forEach((task) => {
|
||||
if (task?.taskId && task?.statusUrl) {
|
||||
createPersistentTaskMonitor(task.taskId, task.statusUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getTruncateViewerElement() {
|
||||
return document.getElementById(truncateApiKey);
|
||||
}
|
||||
@@ -612,6 +716,16 @@
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', function (event) {
|
||||
if (event.target && event.target.id === 'series-optimize-feedback') {
|
||||
const reconPayload = document.getElementById('recon-task-payload');
|
||||
if (reconPayload) {
|
||||
const taskId = reconPayload.dataset.taskId;
|
||||
const statusUrl = reconPayload.dataset.statusUrl;
|
||||
if (taskId && statusUrl) {
|
||||
upsertStoredReconTask(taskId, statusUrl);
|
||||
createPersistentTaskMonitor(taskId, statusUrl);
|
||||
}
|
||||
}
|
||||
|
||||
const truncatePayload = document.getElementById('truncate-range-payload');
|
||||
if (truncatePayload) {
|
||||
const start = parseInt(truncatePayload.dataset.start || '1', 10);
|
||||
@@ -667,6 +781,15 @@
|
||||
});
|
||||
|
||||
syncOptimizeBounds();
|
||||
renderStoredReconTaskMonitors();
|
||||
|
||||
document.getElementById('clear-recon-monitors-btn')?.addEventListener('click', () => {
|
||||
setStoredReconTasks([]);
|
||||
const wrap = document.getElementById('persistent-recon-tasks');
|
||||
const empty = document.getElementById('persistent-recon-empty');
|
||||
wrap?.querySelectorAll('[id^="persistent-recon-card-"]')?.forEach((node) => node.remove());
|
||||
empty?.classList.remove('d-none');
|
||||
});
|
||||
|
||||
$("#add-finding-button").click(() => {
|
||||
$("#hidden-form").show()
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load django_htmx %}
|
||||
|
||||
{% block title %}
|
||||
Atlas Task Overview
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xl py-3">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||
<h2 class="h4 mb-0">Atlas Task Overview</h2>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-outline-secondary btn-sm" href="?status=all">All</a>
|
||||
<a class="btn btn-outline-warning btn-sm" href="?status=active">Active</a>
|
||||
<a class="btn btn-outline-success btn-sm" href="?status=completed">Completed</a>
|
||||
<a class="btn btn-outline-danger btn-sm" href="?status=failed">Failed</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if backend_missing %}
|
||||
<div class="alert alert-warning">
|
||||
django_tasks_db backend is not available in this environment. Install and migrate it to see task records.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-warning">
|
||||
<div class="card-body py-2">
|
||||
<div class="small text-muted">Active</div>
|
||||
<div class="h4 mb-0 text-warning">{{ counts.active }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-success">
|
||||
<div class="card-body py-2">
|
||||
<div class="small text-muted">Completed</div>
|
||||
<div class="h4 mb-0 text-success">{{ counts.completed }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-danger">
|
||||
<div class="card-body py-2">
|
||||
<div class="small text-muted">Failed</div>
|
||||
<div class="h4 mb-0 text-danger">{{ counts.failed }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Task</th>
|
||||
<th>Status</th>
|
||||
<th>Queue</th>
|
||||
<th>Enqueued</th>
|
||||
<th>Started</th>
|
||||
<th>Finished</th>
|
||||
<th>Worker</th>
|
||||
<th>Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for task in tasks %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="small fw-semibold">{{ task.task_path }}</div>
|
||||
<div class="small text-muted">{{ task.id }}</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if task.status == "FAILED" %}
|
||||
<span class="badge bg-danger">{{ task.status }}</span>
|
||||
{% elif task.status == "SUCCESSFUL" %}
|
||||
<span class="badge bg-success">{{ task.status }}</span>
|
||||
{% elif task.status == "RUNNING" or task.status == "READY" %}
|
||||
<span class="badge bg-warning text-dark">{{ task.status }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ task.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ task.queue_name }}</td>
|
||||
<td class="small">{{ task.enqueued_at|date:"Y-m-d H:i:s" }}</td>
|
||||
<td class="small">{{ task.started_at|date:"Y-m-d H:i:s" }}</td>
|
||||
<td class="small">{{ task.finished_at|date:"Y-m-d H:i:s" }}</td>
|
||||
<td class="small">{{ task.worker_ids }}</td>
|
||||
<td class="small text-danger">{{ task.exception_class_path|default:"" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-muted">No tasks found for this filter.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
Reference in New Issue
Block a user