feat: task archive

fix: simplified
This commit is contained in:
Per Stark
2025-10-13 15:02:22 +02:00
parent 41fc7bb99c
commit aa0b1462a1
7 changed files with 404 additions and 73 deletions

File diff suppressed because one or more lines are too long

View File

@@ -4,6 +4,7 @@ use axum::{
http::{header, HeaderMap, HeaderValue, StatusCode},
response::IntoResponse,
};
use chrono::{DateTime, Utc};
use futures::try_join;
use serde::Serialize;
@@ -139,6 +140,32 @@ pub struct ActiveJobsData {
pub user: User,
}
#[derive(Serialize)]
struct TaskArchiveEntry {
id: String,
state_label: String,
state_raw: String,
attempts: u32,
max_attempts: u32,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
scheduled_at: DateTime<Utc>,
locked_at: Option<DateTime<Utc>>,
last_error_at: Option<DateTime<Utc>>,
error_message: Option<String>,
worker_id: Option<String>,
priority: i32,
lease_duration_secs: i64,
content_kind: String,
content_summary: String,
}
#[derive(Serialize)]
struct TaskArchiveData {
user: User,
tasks: Vec<TaskArchiveEntry>,
}
pub async fn delete_job(
State(state): State<HtmlState>,
RequireUser(user): RequireUser,
@@ -173,6 +200,70 @@ pub async fn show_active_jobs(
))
}
pub async fn show_task_archive(
State(state): State<HtmlState>,
RequireUser(user): RequireUser,
) -> Result<impl IntoResponse, HtmlError> {
let tasks = User::get_all_ingestion_tasks(&user.id, &state.db).await?;
let entries: Vec<TaskArchiveEntry> = tasks
.into_iter()
.map(|task| {
let (content_kind, content_summary) = summarize_task_content(&task);
TaskArchiveEntry {
id: task.id.clone(),
state_label: task.state.display_label().to_string(),
state_raw: task.state.as_str().to_string(),
attempts: task.attempts,
max_attempts: task.max_attempts,
created_at: task.created_at,
updated_at: task.updated_at,
scheduled_at: task.scheduled_at,
locked_at: task.locked_at,
last_error_at: task.last_error_at,
error_message: task.error_message.clone(),
worker_id: task.worker_id.clone(),
priority: task.priority,
lease_duration_secs: task.lease_duration_secs,
content_kind,
content_summary,
}
})
.collect();
Ok(TemplateResponse::new_template(
"dashboard/task_archive_modal.html",
TaskArchiveData {
user,
tasks: entries,
},
))
}
fn summarize_task_content(task: &IngestionTask) -> (String, String) {
match &task.content {
common::storage::types::ingestion_payload::IngestionPayload::Text { text, .. } => {
("Text".to_string(), truncate_summary(text, 80))
}
common::storage::types::ingestion_payload::IngestionPayload::Url { url, .. } => {
("URL".to_string(), url.to_string())
}
common::storage::types::ingestion_payload::IngestionPayload::File { file_info, .. } => {
("File".to_string(), file_info.file_name.clone())
}
}
}
fn truncate_summary(input: &str, max_chars: usize) -> String {
if input.chars().count() <= max_chars {
input.to_string()
} else {
let truncated: String = input.chars().take(max_chars).collect();
format!("{truncated}")
}
}
pub async fn serve_file(
State(state): State<HtmlState>,
RequireUser(user): RequireUser,

View File

@@ -5,7 +5,9 @@ use axum::{
routing::{delete, get},
Router,
};
use handlers::{delete_job, delete_text_content, index_handler, serve_file, show_active_jobs};
use handlers::{
delete_job, delete_text_content, index_handler, serve_file, show_active_jobs, show_task_archive,
};
use crate::html_state::HtmlState;
@@ -24,6 +26,7 @@ where
{
Router::new()
.route("/jobs/{job_id}", delete(delete_job))
.route("/jobs/archive", get(show_task_archive))
.route("/active-jobs", get(show_active_jobs))
.route("/text-content/{id}", delete(delete_text_content))
.route("/file/{id}", get(serve_file))

View File

@@ -2,10 +2,16 @@
<section id="active_jobs_section" class="nb-panel p-4 space-y-4 mt-6 sm:mt-8">
<header class="flex flex-wrap items-center justify-between gap-3">
<h2 class="text-xl font-extrabold tracking-tight">Active Tasks</h2>
<button class="nb-btn btn-square btn-sm" hx-get="/active-jobs" hx-target="#active_jobs_section" hx-swap="outerHTML"
aria-label="Refresh active tasks">
{% include "icons/refresh_icon.html" %}
</button>
<div class="flex gap-2">
<button class="nb-btn btn-square btn-sm" hx-get="/active-jobs" hx-target="#active_jobs_section" hx-swap="outerHTML"
aria-label="Refresh active tasks">
{% include "icons/refresh_icon.html" %}
</button>
<button class="nb-btn btn-sm" hx-get="/jobs/archive" hx-target="#modal" hx-swap="innerHTML"
aria-label="View task archive">
View Archive
</button>
</div>
</header>
{% if active_jobs %}
<ul class="flex flex-col gap-3 list-none p-0 m-0">

View File

@@ -0,0 +1,152 @@
{% extends "modal_base.html" %}
{% block modal_class %}w-11/12 max-w-[90ch] max-h-[95%] overflow-y-auto{% endblock %}
{% block form_attributes %}onsubmit="event.preventDefault();"{% endblock %}
{% block modal_content %}
<h3 class="text-xl font-extrabold tracking-tight flex items-center gap-2">
Ingestion Task Archive
<span class="badge badge-neutral text-xs font-normal">{{ tasks|length }} total</span>
</h3>
<p class="text-sm opacity-70">A history of all ingestion tasks for {{ user.email }}.</p>
{% if tasks %}
<div class="hidden lg:block overflow-x-auto nb-card mt-4">
<table class="nb-table">
<thead>
<tr>
<th class="text-left">Content</th>
<th class="text-left">State</th>
<th class="text-left">Attempts</th>
<th class="text-left">Scheduled</th>
<th class="text-left">Updated</th>
<th class="text-left">Worker</th>
<th class="text-left">Error</th>
</tr>
</thead>
<tbody>
{% for task in tasks %}
<tr>
<td>
<div class="flex flex-col gap-1">
<div class="text-sm font-semibold">{{ task.content_kind }}</div>
<div class="text-xs opacity-70 break-words">{{ task.content_summary }}</div>
<div class="text-[11px] opacity-60 lowercase tracking-wider">{{ task.id }}</div>
</div>
</td>
<td>
<span class="badge badge-primary badge-outline tracking-wide">{{ task.state_label }}</span>
</td>
<td>
<div class="text-sm font-semibold">{{ task.attempts }} / {{ task.max_attempts }}</div>
<div class="text-xs opacity-60">Priority {{ task.priority }}</div>
</td>
<td>
<div class="text-sm">
{{ task.scheduled_at|datetimeformat(format="short", tz=user.timezone) }}
</div>
{% if task.locked_at %}
<div class="text-xs opacity-60">Locked {{ task.locked_at|datetimeformat(format="short", tz=user.timezone) }}
</div>
{% endif %}
</td>
<td>
<div class="text-sm">
{{ task.updated_at|datetimeformat(format="short", tz=user.timezone) }}
</div>
<div class="text-xs opacity-60">Created {{ task.created_at|datetimeformat(format="short", tz=user.timezone) }}
</div>
</td>
<td>
{% if task.worker_id %}
<span class="text-sm font-semibold">{{ task.worker_id }}</span>
<div class="text-xs opacity-60">Lease {{ task.lease_duration_secs }}s</div>
{% else %}
<span class="text-xs opacity-60">Not assigned</span>
{% endif %}
</td>
<td>
{% if task.error_message %}
<div class="text-sm text-error font-semibold">{{ task.error_message }}</div>
{% if task.last_error_at %}
<div class="text-xs opacity-60">{{ task.last_error_at|datetimeformat(format="short", tz=user.timezone) }}
</div>
{% endif %}
{% else %}
<span class="text-xs opacity-60"></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="lg:hidden flex flex-col gap-3 mt-4">
{% for task in tasks %}
<details class="nb-panel p-3 space-y-3">
<summary class="flex items-center justify-between gap-2 text-sm font-semibold cursor-pointer">
<span>{{ task.content_kind }}</span>
<span class="badge badge-primary badge-outline tracking-wide">{{ task.state_label }}</span>
</summary>
<div class="text-xs opacity-70 break-words">{{ task.content_summary }}</div>
<div class="text-[11px] opacity-60 lowercase tracking-wider">{{ task.id }}</div>
<div class="grid grid-cols-1 gap-2 text-xs">
<div class="flex justify-between">
<span class="opacity-60 uppercase tracking-wide">Attempts</span>
<span class="text-sm font-semibold">{{ task.attempts }} / {{ task.max_attempts }}</span>
</div>
<div class="flex justify-between">
<span class="opacity-60 uppercase tracking-wide">Priority</span>
<span class="text-sm font-semibold">{{ task.priority }}</span>
</div>
<div class="flex justify-between">
<span class="opacity-60 uppercase tracking-wide">Scheduled</span>
<span>{{ task.scheduled_at|datetimeformat(format="short", tz=user.timezone) }}</span>
</div>
<div class="flex justify-between">
<span class="opacity-60 uppercase tracking-wide">Updated</span>
<span>{{ task.updated_at|datetimeformat(format="short", tz=user.timezone) }}</span>
</div>
<div class="flex justify-between">
<span class="opacity-60 uppercase tracking-wide">Created</span>
<span>{{ task.created_at|datetimeformat(format="short", tz=user.timezone) }}</span>
</div>
<div class="flex justify-between">
<span class="opacity-60 uppercase tracking-wide">Worker</span>
{% if task.worker_id %}
<span class="text-sm font-semibold">{{ task.worker_id }}</span>
{% else %}
<span class="opacity-60">Unassigned</span>
{% endif %}
</div>
<div class="flex justify-between">
<span class="opacity-60 uppercase tracking-wide">Lease</span>
<span>{{ task.lease_duration_secs }}s</span>
</div>
{% if task.locked_at %}
<div class="flex justify-between">
<span class="opacity-60 uppercase tracking-wide">Locked</span>
<span>{{ task.locked_at|datetimeformat(format="short", tz=user.timezone) }}</span>
</div>
{% endif %}
</div>
{% if task.error_message or task.last_error_at %}
<div class="border-t border-base-200 pt-2 text-xs space-y-1">
{% if task.error_message %}
<div class="text-sm text-error font-semibold">{{ task.error_message }}</div>
{% endif %}
{% if task.last_error_at %}
<div class="opacity-60">Last error {{ task.last_error_at|datetimeformat(format="short", tz=user.timezone) }}</div>
{% endif %}
</div>
{% endif %}
</details>
{% endfor %}
</div>
{% else %}
<p class="text-sm opacity-70 mt-4">No tasks yet. Start an ingestion to populate the archive.</p>
{% endif %}
{% endblock %}
{% block primary_actions %}{% endblock %}