# Task Tree Dashboard Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace the dashboard Task Center detail tables with a stable task -> playlist -> song tree that updates node state in place. **Architecture:** Keep the existing FastAPI endpoints and lazy playlist-song endpoint, but change the repository task query to keep finished tasks visible and change the dashboard frontend from table redraws to keyed tree-node patching. The top dashboard cards remain unchanged in this iteration. **Tech Stack:** Python, FastAPI, Jinja2 templates, vanilla JavaScript, unittest --- ### Task 1: Keep finished tasks in the Task Center query **Files:** - Modify: `musicdl/catalogsync/ops/repository.py` - Modify: `tests/catalogsync/test_ops_repository.py` - [ ] **Step 1: Write the failing repository test** ```python def test_list_task_center_rows_includes_completed_jobs(self): from musicdl.catalogsync.db import initialize_database from musicdl.catalogsync.ops.models import JobStatus from musicdl.catalogsync.ops.repository import OpsRepository with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: db_path = Path(tmpdir) / "catalogsync.db" initialize_database(db_path).close() repo = OpsRepository(db_path) completed_job_id = repo.create_job( job_type="download_only", config_snapshot={}, status=JobStatus.COMPLETED, playlist_scope={"playlist_ids": [42]}, ) rows = repo.list_task_center_rows(limit=20) rows_by_id = {int(row["id"]): row for row in rows} self.assertIn(completed_job_id, rows_by_id) self.assertEqual("completed", rows_by_id[completed_job_id]["status"]) ``` - [ ] **Step 2: Run test to verify it fails** Run: `python -m unittest tests.catalogsync.test_ops_repository.OpsRepositoryTaskCenterTests.test_list_task_center_rows_includes_completed_jobs -v` Expected: FAIL because completed jobs are filtered out of `list_task_center_rows()`. - [ ] **Step 3: Write the minimal implementation** ```python rows = self._fetchall( """ SELECT * FROM job_runs WHERE status IN (?, ?, ?, ?, ?, ?, ?, ?) ... """, ( JobStatus.RUNNING.value, JobStatus.PAUSE_REQUESTED.value, JobStatus.QUEUED.value, JobStatus.PAUSED.value, JobStatus.COMPLETED.value, JobStatus.COMPLETED_WITH_ERRORS.value, JobStatus.FAILED.value, JobStatus.CANCELED.value, ... ), ) ``` - [ ] **Step 4: Run test to verify it passes** Run: `python -m unittest tests.catalogsync.test_ops_repository.OpsRepositoryTaskCenterTests.test_list_task_center_rows_includes_completed_jobs -v` Expected: PASS - [ ] **Step 5: Commit** ```bash git add musicdl/catalogsync/ops/repository.py tests/catalogsync/test_ops_repository.py git commit -m "test: keep completed tasks in task center" ``` ### Task 2: Lock dashboard HTML to the tree shell **Files:** - Modify: `musicdl/catalogsync/templates/ops/dashboard.html` - Modify: `tests/catalogsync/test_ops_api.py` - [ ] **Step 1: Write the failing dashboard HTML test** ```python def test_dashboard_page_renders_task_tree_shell_without_detail_tables(self): from musicdl.catalogsync.ops.models import JobStatus from musicdl.catalogsync.ops.repository import OpsRepository client, db_path, _ = self._build_client() repo = OpsRepository(db_path) job_id = repo.create_job( job_type="download_only", config_snapshot={}, status=JobStatus.RUNNING, ) response = client.get("/dashboard") html = response.text self.assertIn('data-task-tree-root', html) self.assertIn(f'data-task-node="{job_id}"', html) self.assertNotIn("

Summary

", html) self.assertNotIn("

Stages

", html) self.assertNotIn("

Workers

", html) self.assertNotIn("

Running Items

", html) ``` - [ ] **Step 2: Run test to verify it fails** Run: `python -m unittest tests.catalogsync.test_ops_api.OperationsApiTests.test_dashboard_page_renders_task_tree_shell_without_detail_tables -v` Expected: FAIL because the template still renders the table/detail shell. - [ ] **Step 3: Write the minimal template implementation** ```html

Task Center

{% for row in task_rows %}
{{ row.display_name }}
{{ row.job_type }}
{% endfor %}
``` - [ ] **Step 4: Run test to verify it passes** Run: `python -m unittest tests.catalogsync.test_ops_api.OperationsApiTests.test_dashboard_page_renders_task_tree_shell_without_detail_tables -v` Expected: PASS - [ ] **Step 5: Commit** ```bash git add musicdl/catalogsync/templates/ops/dashboard.html tests/catalogsync/test_ops_api.py git commit -m "feat: replace task center table with tree shell" ``` ### Task 3: Replace Task Center redraw with keyed tree patching **Files:** - Modify: `musicdl/catalogsync/static/ops/app.js` - Modify: `musicdl/catalogsync/templates/ops/base.html` - [ ] **Step 1: Add the tree patch helpers** ```javascript function upsertTaskTree(rows) { var root = document.querySelector("[data-task-tree-root]"); if (!root) { return; } var seen = {}; rows.forEach(function (row) { var id = String(row.id); seen[id] = true; var node = ensureTaskNode(root, row); patchTaskNode(node, row); }); pruneMissingTaskNodes(root, seen); } ``` - [ ] **Step 2: Switch `updateDashboard()` away from `setTaskRows()`** ```javascript if (Object.prototype.hasOwnProperty.call(payload, "task_rows")) { dashboardState.taskRows = payload.task_rows || []; pruneTaskState(dashboardState.taskRows); upsertTaskTree(dashboardState.taskRows); restoreExpandedTaskRows(); } ``` - [ ] **Step 3: Rebuild expanded task rendering as playlist nodes only** ```javascript function applyTaskDetail(jobId, payload) { dashboardState.detailCache[String(jobId)] = payload; patchPlaylistTree(String(jobId), payload.playlist_progress || []); restoreExpandedPlaylistRows(String(jobId)); } ``` - [ ] **Step 4: Rebuild playlist song rendering as song child nodes** ```javascript function applyPlaylistSongs(jobId, playlistId, songs) { var key = playlistKey(jobId, playlistId); var body = document.querySelector('[data-playlist-song-list="' + key + '"]'); patchSongTree(body, songs || []); } ``` - [ ] **Step 5: Version the static asset to force the browser to pick up the new script** ```html ``` - [ ] **Step 6: Run targeted API tests** Run: `python -m unittest tests.catalogsync.test_ops_api tests.catalogsync.test_ops_repository -v` Expected: PASS - [ ] **Step 7: Commit** ```bash git add musicdl/catalogsync/static/ops/app.js musicdl/catalogsync/templates/ops/base.html git commit -m "feat: patch dashboard task tree in place" ``` ### Task 4: Final verification and docs sync **Files:** - Modify: `docs/catalogsync.md` - [ ] **Step 1: Document the new dashboard behavior** ```markdown ## Task Center The dashboard Task Center now renders a tree: - task - playlist - song Task state updates patch the existing node in place. Expanding a task no longer renders Summary, Stages, Workers, or Running Items tables. ``` - [ ] **Step 2: Run regression verification** Run: `python -m unittest tests.catalogsync.test_ops_api tests.catalogsync.test_ops_repository -v` Expected: PASS - [ ] **Step 3: Manual browser verification** Run the dashboard, expand one task and one playlist, wait through multiple refresh cycles, and verify: - no large detail tables appear - paused/completed tasks stay visible - expanded nodes remain expanded - the task tree does not visibly flash as a full block - [ ] **Step 4: Commit** ```bash git add docs/catalogsync.md git commit -m "docs: describe task tree dashboard" ```