Tasks Demo Frontend — Design Spec¶
Status: Approved Date: 2026-03-30 Scope: Frontend-only async task visualization with stub backend endpoints
Summary¶
Add a new tasks_demo Django app that demonstrates background task visualization with animated pulse-ring indicators and live polling. The backend uses stub threading to simulate async work. Real django.tasks integration is deferred to a follow-up.
Motivation¶
The starter currently demonstrates CRUD with server-rendered templates but has no async/background task example. Adding a tasks demo page shows how a desktop Django app can handle longer-running operations with visual feedback — a common need for local desktop tools. This also creates the frontend foundation for a future django.tasks integration.
Decisions¶
Simulated work: Tasks sleep for a random duration rather than doing real computation. This keeps the demo generic and lets the visual treatment be the focus.
Separate page: Tasks live on their own page (
/tasks/) with a nav link in the masthead, rather than being embedded in the items page or a drawer. This preserves the teaching pattern of one app = one concern.Pulse ring visualization: Running tasks show animated concentric teal rings radiating from a dot. Chosen for its calm, desktop-app-native feel over alternatives (waveform, orbit spinner, segmented fill).
Polling, not WebSocket: The frontend polls a JSON endpoint every 2 seconds. No WebSocket/SSE complexity.
No JS build step: All JavaScript is inline in the template, consistent with the starter’s server-rendered philosophy.
Frontend-only slice: The backend is a stub with threading.
django.taskscomes later.
New App: tasks_demo¶
Location: src/tasks_demo/
Model: SimulatedTask¶
Field |
Type |
Notes |
|---|---|---|
|
AutoField |
Primary key |
|
CharField |
Human-readable name (e.g., “Restocking hay loft”) |
|
CharField (choices) |
PENDING, RUNNING, DONE, FAILED |
|
TextField (nullable) |
Success message or error string |
|
FloatField (nullable) |
Seconds the task took |
|
DateTimeField (auto_now_add) |
When the task was created |
|
DateTimeField (nullable) |
When the task finished |
Views¶
task_list— Renders the tasks page (HTML). Lists all tasks ordered by newest first.task_create— POST only. Returns201 Createdwith a JSON body:{"id": 5, "label": "Restocking hay loft", "status": "PENDING"}
Creates a new
SimulatedTaskin PENDING state with a label randomly chosen from a small hardcoded list (e.g., “Restocking hay loft”, “Brushing parade manes”, “Polishing tack room brass”, “Counting sugar-cube tins”). Launches the worker via_launch_task(task_id)(see Testing section), which starts a daemon thread targeting_run_task_worker(task_id). The worker:Sets status to RUNNING
Sleeps for a random 3–10 seconds
Sets status to DONE (with a result string) or FAILED (~20% chance, with an error message)
Records duration and completed_at
task_status— GET, returns200 OKwith a JSON body:{ "tasks": [ { "id": 5, "label": "Restocking hay loft", "status": "RUNNING", "result": null, "duration": null, "created_at": "2026-03-30T14:22:01Z", "completed_at": null }, { "id": 4, "label": "Brushing parade manes", "status": "DONE", "result": "Routine complete: every stall passed inspection.", "duration": 6.2, "created_at": "2026-03-30T14:20:50Z", "completed_at": "2026-03-30T14:20:56Z" } ] }
Returns all tasks ordered newest first. The frontend uses
created_atto compute elapsed time for running tasks and relative completion time for finished tasks.
URLs (under /tasks/)¶
Path |
Name |
Method |
View |
|---|---|---|---|
|
|
GET |
|
|
|
POST |
|
|
|
GET |
|
Startup reconciliation¶
The tasks_demo app’s AppConfig.ready() marks any PENDING or RUNNING tasks as FAILED with the result string "Abandoned — app restarted" and sets completed_at to the current time. This prevents stale non-terminal rows from surviving across app restarts, which would otherwise cause the frontend to poll indefinitely and misrepresent task state.
Frontend¶
Template: task_list.html¶
Extends
base.htmlwith heading “Stable Routines”Run bar: “Start Routine” button + hint text (“Starts a simulated stable routine for the pony crew”)
Task list: Rows showing indicator, task name, status badge, and timing metadata
Result rows: Completed/failed tasks show result or error text below the task row
Empty state: “No routines underway. Start a routine to see the stable crew at work.”
Task States and Indicators¶
State |
Indicator |
Badge |
Meta |
|---|---|---|---|
PENDING |
Hollow amber dot |
Amber “Pending” |
“queued” |
RUNNING |
Pulsing teal rings |
Teal “Running” |
Elapsed seconds (live) |
DONE |
Solid teal dot + ✓ |
Muted “Done” |
Duration + relative time |
FAILED |
Solid red dot + × |
Red “Failed” |
Duration + relative time |
JavaScript (inline, no build step)¶
Polling: Calls
/tasks/status/every 2 seconds while any task is PENDING or RUNNING. Stops when all tasks are terminal.Start Routine: POSTs to
/tasks/run/viafetch, then starts/resumes polling.DOM updates: On each poll, updates indicators, badges, elapsed times, and result rows.
CSRF: The
task_listview is decorated with@ensure_csrf_cookieso the CSRF cookie is always set on the tasks page, even though there is no rendered<form>. The JS reads the token from the cookie and sends it as anX-CSRFTokenheader on the POST.
CSS additions to app.css¶
Pulse ring
@keyframesanimation and indicator classes (running, pending, done, failed)Status badge colors (teal, amber, muted gray, red)
Task row layout (flex with indicator, name, badge, meta)
Result row styling (indented, smaller text)
Nav link styles in masthead (active vs inactive)
Testing¶
Deterministic testing seam¶
Two separate module-level functions provide the testing seam:
_launch_task(task_id)— Called by the view. In production, starts a daemon thread targeting_run_task_worker. Tests patch this to call_run_task_workersynchronously (or to do nothing, when testing only the view/HTTP layer)._run_task_worker(task_id)— The pure worker body. Performs the PENDING → RUNNING → DONE/FAILED transitions, sleep, and DB updates. Tests call this directly for deterministic state-transition coverage.
The random duration (3–10s) and failure probability (~20%) are drawn from module-level constants DURATION_RANGE = (3, 10) and FAILURE_RATE = 0.2, also patchable in tests.
Backend tests (tests/test_tasks_demo.py)¶
Task creation via POST returns 201 JSON with
id,label,statusfieldsStatus endpoint returns 200 JSON matching the documented schema
Status endpoint returns
{"tasks": []}when no tasks existTask list page renders with 200 and includes the “Start Routine” button
Task model status choices are valid
Worker function (called directly, not via thread) correctly transitions PENDING → RUNNING → DONE
Worker function (called directly, patched to fail) correctly transitions PENDING → RUNNING → FAILED
Startup reconciliation marks stale PENDING/RUNNING tasks as FAILED with “Abandoned — app restarted”
Validation¶
Existing tests remain untouched
just testmust pass after the changeNo JS-level testing (consistent with how the starter handles the example app)
Documentation Updates¶
README.md: Mention the tasks demo in the project descriptiondocs/index.md: Add tasks demo to the documentation indexdocs/specification.mdsection 9: Note tasks demo frontend is implemented as an optional post-v1 extension,django.tasksdeferreddocs/decisions.md: Add D-011 recording that the tasks demo is an optional post-v1 extension with stub threadingdocs/architecture.md: Update deferred areas sectionllms.txt: Mention the newtasks_demoapp
Relationship to v1 Scope¶
This is an optional post-v1 extension, not a change to the starter’s core v1 story. The specification (section 9) and decisions log (D-005) established that background tasks are deferred from v1. This demo fulfills the spec’s note to “document a future extension path for one local background-task flow only after the minimal starter is stable.” The tasks demo app is additive — the core starter remains complete without it.
Explicit Non-Scope¶
No
django.tasksintegration (deferred to follow-up)Tasks persist in the database for the demo (no ephemeral in-memory store), but no retention policy, cleanup, or pagination is included
No task cancellation
No WebSocket or SSE
No JS build tooling or framework