feat(queue): 404 on unknown hash, show error in UI

POST /api/queue/cancel answers 404 {"error":"hash not in queue"} for
untracked hashes instead of 200 ok:true. The UI surfaces the server's
error message instead of swallowing it.
This commit is contained in:
2026-10-19 19:11:40 +02:00
parent 44c2888a2e
commit 99dcb298f8
8 changed files with 297 additions and 52 deletions

View File

@@ -3,3 +3,4 @@
What the project is, how it is laid out, and how to build, test and run it.
- [Project Overview](./overview.md) - purpose, stack, repo layout, verified build/test/run commands.
- [Web UI JSON API](./ui-api.md) - the UI-facing JSON routes (search, queue add/list/cancel), the 404-on-cancel contract, and UI error surfacing.

View File

@@ -36,6 +36,7 @@ Webshare credentials are **optional**: with empty `WEBSHARE_USERNAME`/`WEBSHARE_
src/ # source (CommonJS)
index.js # express app + app.listen at module load (server entry)
qbt.js # fake qBittorrent client / download queue; cancelDownload returns a boolean
ui-api.js # Web UI JSON API routes (search, queue add/list/cancel), mounted before static
mcp.js # MCP server entry (Streamable HTTP)
webshare.js # Webshare API client
torznab.js # Torznab caps/feed builders
@@ -48,7 +49,7 @@ compose.yaml # Docker Compose
data/ # host download data dir (Compose mount target)
```
The `src/` modules: `qbt.js` is the shared download engine used by both the Web UI queue and the fake qBittorrent API; `cancelDownload(hash)` returns a boolean indicating whether a matching download was cancelled.
The `src/` modules: `qbt.js` is the shared download engine used by both the Web UI queue and the fake qBittorrent API; `cancelDownload(hash)` returns a boolean indicating whether a matching download was cancelled. The UI-facing JSON routes live in `ui-api.js` (extracted from `index.js` so they are testable with supertest); see [Web UI JSON API](./ui-api.md) for their contracts, including the `404` on cancelling a hash that is not in the queue. The fake qBittorrent API in `qbt.js` deliberately keeps its always-`Ok.` contract for real *arr clients.
## Build / test / run (verified)

36
doc/structure/ui-api.md Normal file
View File

@@ -0,0 +1,36 @@
---
type: API
title: Web UI JSON API
description: The UI-facing JSON routes served by src/ui-api.js (search, queue add/list/cancel), the 404-on-cancel contract, and the UI error surfacing.
tags: [api, ui, queue, express]
timestamp: 2026-10-19T00:00:00Z
---
# Web UI JSON API
The Web UI talks to the app over four JSON routes registered by `registerUiApi(app, { qbt, client })` in `src/ui-api.js`, which `src/index.js` mounts **before** static file serving so `/api/*` is never swallowed. `__tests__/ui-api.test.js` covers all four routes.
| Route | Success | Error |
|-------|---------|-------|
| `GET /api/search?q=…` | `200` result array (`[]` when `q` is empty) | `500` `{error}` when the Webshare client fails |
| `POST /api/queue/add` | `200` `{ok: true}` | `400` `{error: 'ident required'}`, `409` `{error: 'already in queue'}`, `500` `{error}` |
| `GET /api/queue` | `200` queue array | `500` `{error}` |
| `POST /api/queue/cancel` | `200` `{ok: true}` | `400` `{error: 'hash required'}`, **`404` `{error: 'hash not in queue'}`** when the hash is not in the queue, `500` `{error}` |
## Cancel returns 404 for unknown hashes
`POST /api/queue/cancel` honors the return value of `qbt.cancelDownload(hash)` (see [Project Overview](./overview.md)), which is a boolean: `false` means no queued download matched the hash, and the route responds `404 {error: 'hash not in queue'}`. Before this, the route always answered `200 {ok: true}` even when nothing was cancelled.
The 404 body message is deliberately human-readable — the UI surfaces `error` verbatim (below), and a raw body read is still intelligible.
## The UI surfaces the error
`apiFetch` in `ui/scripts.js` parses a failed response's JSON body when `content-type` is `application/json` and throws `new Error(data.error)` (falling back to the raw text or `HTTP <status>` for non-JSON or malformed bodies). The cancel-button handler then shows the failure inline as a transient `[err]` button state (re-enabled after 2 s) instead of swallowing it in `catch {}`; the 2 s queue poll repaints the row. This mirrors the established inline-error pattern of the add button — the codebase has no toasts or alerts.
## The fake qBittorrent API is intentionally different
The fake qBittorrent Web API in `src/qbt.js` (`POST /api/v2/torrents/delete` and friends) was **not** changed: it answers `Ok.` for real qBittorrent-API clients such as Sonarr, whose contract is always `Ok.`, and Sonarr deletes finished entries — a `404` there would break it. The two APIs serve different consumers and keep different error contracts on purpose.
## Related
- [Project Overview](./overview.md) — module layout and build/test commands.