From fc4c41c7cf95cd2383bd52245cdb7ebeb999abea Mon Sep 17 00:00:00 2001 From: JB Date: Mon, 6 Oct 2025 12:41:22 -0700 Subject: [PATCH] feat: Implement job management features with improved UI for active and finished jobs --- abogen/web/routes.py | 27 ++++- abogen/web/service.py | 22 ++++ abogen/web/static/styles.css | 141 ++++++++++++++++++++++++ abogen/web/templates/index.html | 4 +- abogen/web/templates/partials/jobs.html | 105 ++++++++++++------ 5 files changed, 262 insertions(+), 37 deletions(-) diff --git a/abogen/web/routes.py b/abogen/web/routes.py index 70ebe32..2f63fe8 100644 --- a/abogen/web/routes.py +++ b/abogen/web/routes.py @@ -763,6 +763,19 @@ def enqueue_job() -> Response: return redirect(url_for("web.job_detail", job_id=job.id)) +def _render_jobs_panel() -> str: + jobs = _service().list_jobs() + active_jobs = [job for job in jobs if job.status in {JobStatus.PENDING, JobStatus.RUNNING}] + finished_jobs = [job for job in jobs if job.status not in {JobStatus.PENDING, JobStatus.RUNNING}] + return render_template( + "partials/jobs.html", + active_jobs=active_jobs, + finished_jobs=finished_jobs[:5], + total_finished=len(finished_jobs), + JobStatus=JobStatus, + ) + + @web_bp.get("/jobs/") def job_detail(job_id: str) -> str: job = _service().get_job(job_id) @@ -778,15 +791,27 @@ def job_detail(job_id: str) -> str: @web_bp.post("/jobs//cancel") def cancel_job(job_id: str) -> Response: _service().cancel(job_id) + if request.headers.get("HX-Request"): + return _render_jobs_panel() return redirect(url_for("web.job_detail", job_id=job_id)) @web_bp.post("/jobs//delete") def delete_job(job_id: str) -> Response: _service().delete(job_id) + if request.headers.get("HX-Request"): + return _render_jobs_panel() return redirect(url_for("web.index")) +@web_bp.post("/jobs/clear-finished") +def clear_finished_jobs() -> Response: + _service().clear_finished() + if request.headers.get("HX-Request"): + return _render_jobs_panel() + return redirect(url_for("web.queue_page")) + + @web_bp.get("/jobs//download") def download_job(job_id: str) -> Response: job = _service().get_job(job_id) @@ -812,7 +837,7 @@ def download_job(job_id: str) -> Response: @web_bp.get("/partials/jobs") def jobs_partial() -> str: - return render_template("partials/jobs.html", jobs=_service().list_jobs()) + return _render_jobs_panel() @web_bp.get("/partials/jobs//logs") diff --git a/abogen/web/service.py b/abogen/web/service.py index 280a12f..f8c097d 100644 --- a/abogen/web/service.py +++ b/abogen/web/service.py @@ -222,6 +222,28 @@ class ConversionService: self._update_queue_positions_locked() return True + def clear_finished(self, *, statuses: Optional[Iterable[JobStatus]] = None) -> int: + finished_statuses = set(statuses or {JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED}) + removed = 0 + with self._lock: + # Remove any queued entries first to avoid stale references + filtered_queue: List[str] = [] + for job_id in self._queue: + job = self._jobs.get(job_id) + if job and job.status in finished_statuses: + continue + filtered_queue.append(job_id) + self._queue = filtered_queue + + for job_id, job in list(self._jobs.items()): + if job.status in finished_statuses: + self._jobs.pop(job_id) + removed += 1 + + if removed: + self._update_queue_positions_locked() + return removed + def shutdown(self) -> None: self._stop_event.set() self._wake_event.set() diff --git a/abogen/web/static/styles.css b/abogen/web/static/styles.css index 5dc5c4e..d966bfb 100644 --- a/abogen/web/static/styles.css +++ b/abogen/web/static/styles.css @@ -144,6 +144,9 @@ body { .form-grid > .grid { align-items: start; + max-width: 460px; + width: min(100%, 460px); + justify-self: start; } .button { @@ -367,6 +370,7 @@ body { border-radius: 999px; background: rgba(148, 163, 184, 0.15); overflow: hidden; + --progress: 0%; } .progress-bar__fill { @@ -374,6 +378,7 @@ body { background: linear-gradient(90deg, var(--accent), var(--accent-strong)); border-radius: inherit; transition: width 0.35s ease; + width: var(--progress); } .list { @@ -395,6 +400,142 @@ body { gap: 1rem; } +.queue-section { + display: grid; + gap: 1rem; + margin-bottom: 1.75rem; +} + +.queue-section:last-of-type { + margin-bottom: 0; +} + +.queue-section__header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.queue-section__header h3 { + margin: 0; + font-size: 1.1rem; + font-weight: 600; +} + +.job-cards { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 1rem; +} + +.job-card { + border: 1px solid rgba(148, 163, 184, 0.16); + border-radius: 20px; + padding: 1rem 1.25rem; + background: rgba(15, 23, 42, 0.35); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); + display: grid; + gap: 0.75rem; +} + +.job-card__header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; +} + +.job-card__meta { + color: var(--muted); + font-size: 0.85rem; + margin-top: 0.35rem; +} + +.job-card__progress { + display: grid; + gap: 0.4rem; +} + +.job-card__progress small { + color: var(--muted); +} + +.job-card__footer { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.job-card__footer .button--ghost { + padding: 0.6rem 0.95rem; +} + +.job-cards--compact .job-card { + background: rgba(15, 23, 42, 0.28); +} + +.job-cards--compact .job-card__meta { + margin-top: 0.25rem; +} + +.job-card--info { + border-style: dashed; + border-color: rgba(148, 163, 184, 0.25); + background: rgba(15, 23, 42, 0.2); + text-align: center; +} + +.job-card--info .job-card__meta { + margin-top: 0; +} + +.queue-empty { + color: var(--muted); +} + +.field--file input[type="file"] { + width: 100%; + border-radius: 14px; + border: 1px dashed rgba(148, 163, 184, 0.35); + background: rgba(15, 23, 42, 0.45); + padding: 0.85rem 1rem; + font-size: 0.95rem; + color: var(--muted); + cursor: pointer; + transition: border 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; +} + +.field--file input[type="file"]:hover, +.field--file input[type="file"]:focus { + border-color: var(--accent); + background: rgba(56, 189, 248, 0.1); + box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.12); +} + +.field--file input[type="file"]::file-selector-button, +.field--file input[type="file"]::-webkit-file-upload-button { + border: none; + border-radius: 12px; + background: rgba(56, 189, 248, 0.2); + color: var(--accent); + font-weight: 600; + padding: 0.55rem 1.05rem; + margin-right: 1rem; + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease; +} + +.field--file input[type="file"]:hover::file-selector-button, +.field--file input[type="file"]:focus::file-selector-button, +.field--file input[type="file"]:hover::-webkit-file-upload-button, +.field--file input[type="file"]:focus::-webkit-file-upload-button { + background: rgba(56, 189, 248, 0.35); + color: #fff; +} + .alert { margin: 1rem 0; padding: 0.75rem 1rem; diff --git a/abogen/web/templates/index.html b/abogen/web/templates/index.html index 7a32398..b99a274 100644 --- a/abogen/web/templates/index.html +++ b/abogen/web/templates/index.html @@ -7,8 +7,8 @@

Create a New Audiobook

-
- +
+
diff --git a/abogen/web/templates/partials/jobs.html b/abogen/web/templates/partials/jobs.html index 6a9b903..756fd5f 100644 --- a/abogen/web/templates/partials/jobs.html +++ b/abogen/web/templates/partials/jobs.html @@ -1,37 +1,74 @@
Queue
-{% if jobs %} - - - - - - - - - - - - {% for job in jobs %} - - - - - - - +
+
+
+
+ {{ job.processed_characters }} / {{ job.total_characters or '—' }} +
+ + {% endfor %} - -
JobStatusProgressCreated
- {{ job.original_filename }} -
- {% if job.voice_profile %}Profile: {{ job.voice_profile }}{% else %}Voice: {{ job.voice }}{% endif %} · {{ job.language }} + +
+
+

Active jobs

+
+ {% if active_jobs %} +
    + {% for job in active_jobs %} +
  • +
    +
    + {{ job.original_filename }} +
    {% if job.voice_profile %}Profile: {{ job.voice_profile }}{% else %}Voice: {{ job.voice }}{% endif %} · {{ job.language }}
    +
    + {{ job.status.value|title }}
    -
- {{ job.status.value|title }} - - - {{ job.processed_characters }} / {{ job.total_characters or '—' }} - {{ job.created_at|datetimeformat }}Inspect
-{% else %} -

No jobs yet. Drop a file or paste text to get started.

-{% endif %} + + {% else %} +

No active jobs. Drop a file or paste text to get started.

+ {% endif %} + + +
+
+

Recent results

+ {% if total_finished > 0 %} + + {% endif %} +
+ {% if finished_jobs %} +
    + {% for job in finished_jobs %} +
  • +
    +
    + {{ job.original_filename }} +
    Finished {{ job.finished_at|datetimeformat }}
    +
    + {{ job.status.value|title }} +
    + +
  • + {% endfor %} + {% if total_finished > finished_jobs|length %} +
  • +
    {{ total_finished - finished_jobs|length }} more finished job{{ 's' if (total_finished - finished_jobs|length) != 1 else '' }} hidden. Clear finished to remove them.
    +
  • + {% endif %} +
+ {% else %} +

Completed jobs will appear here.

+ {% endif %} +