Initial import: Music_Server, MusicFree, catalog-sync
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
# 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("<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**
|
||||
|
||||
```html
|
||||
<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**
|
||||
|
||||
```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
|
||||
<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**
|
||||
|
||||
```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"
|
||||
```
|
||||
Reference in New Issue
Block a user