Files

8.3 KiB

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

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
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
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

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("<h3>Summary</h3>", html)
    self.assertNotIn("<h3>Stages</h3>", html)
    self.assertNotIn("<h3>Workers</h3>", html)
    self.assertNotIn("<h3>Running Items</h3>", 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
<div class="card">
  <h2>Task Center</h2>
  <div class="task-tree" data-task-tree-root>
    {% for row in task_rows %}
    <section class="task-node" data-task-node="{{ row.id }}">
      <div class="task-node__header">
        <button type="button" data-task-toggle="{{ row.id }}">+</button>
        <div class="task-node__meta">
          <strong>{{ row.display_name }}</strong>
          <div class="muted">{{ row.job_type }}</div>
        </div>
      </div>
      <div class="task-node__children" data-task-children="{{ row.id }}" hidden></div>
    </section>
    {% endfor %}
  </div>
</div>
  • 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
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

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()
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
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
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
<script src="/static/ops/app.js?v=20260417_task_tree_v3" defer></script>
  • 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
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

## 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

git add docs/catalogsync.md
git commit -m "docs: describe task tree dashboard"