docs(scanner): Beam batch 7 — all 10 scanner endpoints backfilled

Scanner surface frontmatter'd (scan/start/stop/cancel/status/active/
active-summary + watch start/stop/status) with provenance comments;
active-summary on a non-admin group.

Claude registry: 10 entries. Fixed handler drift Beam missed: scan_library
request has no "recursive"; scanner/start binds folder_paths (not
invented interval_seconds); scanner/stop binds no body; response is
{message, job_id, status} with 409 coalescing variant
{error, active_job_id}.

Gate fix in drift_test seed: registerScannerRoutes group seeds used bare
"jwt"/"admin" tokens invisible to authLabel; and silently passed before
only because the seeded set was unchecked. Seeds now 'jwtMiddleware'/
'AdminMiddleware' markers. Also: cleanup of a stale-merge-script residue
in collections/update_rule.md example (extra null keys). make test 9/9.
This commit is contained in:
John O'Keefe committed 2026-10-24 17:20:46 -04:00
1 parent 25ef0d12ba
commit 2c56ae4b9d
13 files changed
+238 -61

No files matched your search

@@ -26,13 +26,7 @@ the rules endpoint.
```json example-response
{
"collection_id": "550e8400-e29b-41d4-a716-446655440000",
"is_smart": true,
"enabled": null,
"field": null,
"id": null,
"operator": null,
"priority": null,
"value": null
"is_smart": true
}
```
+36
View File
@@ -0,0 +1,36 @@
---
method: POST
path: /api/scanner/cancel/:jobId
auth: jwt+admin
area: scanner
---
<!-- source: internal/handlers/scanner.go:236 Handler.CancelScan -->
# Cancel Scan
Requests cancellation of a pending or running scan job. Cancellation is
cooperative — the file currently being processed finishes, the walk stops
at its next context check, and the archive pass is skipped.
## Example response
```json example-response
{
"message": "scan cancellation requested",
"job_id": "job-uuid",
"status": "cancelled"
}
```
## Path parameters
| Param | Type | Required | Notes |
| ----- | ---- | -------- | ----- |
| jobId | string | Yes | Scan job id |
## Errors
| Status | Body | When |
| ------ | ---- | ---- |
| 404 | `{"error": "job not found"}` | no such job |
| 409 | `{"error": "..."}` | job cannot be cancelled |
@@ -0,0 +1,28 @@
---
method: GET
path: /api/scanner/active
auth: jwt+admin
area: scanner
---
<!-- source: internal/handlers/scanner.go:203 Handler.GetActiveScans -->
# Get Active Scans
Returns every pending/running scan job, so the admin page can re-attach to
in-flight scans after a refresh.
## Example response
```json example-response
{
"scanning": true,
"jobs": []
}
```
## Errors
| Status | Body | When |
| ------ | ---- | ---- |
| 401 | `{"error": "unauthorized"}` | no/invalid JWT |
| 403 | `{"error": "admin access required"}` | non-admin |
+14 -5
View File
@@ -1,3 +1,11 @@
---
method: GET
path: /api/scanner/status/:jobId
auth: jwt+admin
area: scanner
---
<!-- source: internal/handlers/scanner.go:179 Handler.GetScanStatus -->
# Get Scan Status
Get the status of a specific scan job.
@@ -26,13 +34,10 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK)
```json
```json example-response
{
"job_id": "uuid",
"library_id": "uuid",
"status": "in_progress",
"started_at": "2026-02-08T10:00:00Z",
"updated_at": "2026-02-08T10:05:00Z",
"progress": {
"total_files": 1523,
"scanned_files": 850,
@@ -46,7 +51,11 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
"file_path": "/path/to/file.epub",
"error": "Invalid EPUB format"
}
]
],
"error": null,
"files_scanned": null,
"new_items": null,
"result": null
}
```
@@ -0,0 +1,27 @@
---
method: GET
path: /api/scanner/active-summary
auth: jwt
area: scanner
---
<!-- source: internal/handlers/scanner.go:216 Handler.GetScanSummary -->
# Get Scan Summary
Whether a scan is running and its average progress (0-1). Powers the
sitewide sidebar indicator.
## Example response
```json example-response
{
"scanning": true,
"progress": 0.5
}
```
## Errors
| Status | Body | When |
| ------ | ---- | ---- |
| 401 | `{"error": "unauthorized"}` | no/invalid JWT |
@@ -1,3 +1,11 @@
---
method: GET
path: /api/scanner/watch/status
auth: jwt+admin
area: scanner
---
<!-- source: internal/handlers/scanner.go:376 Handler.GetWatchModeStatus -->
# Get Watch Mode Status
Get the watch mode status for a library.
@@ -26,18 +34,10 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK)
```json
```json example-response
{
"library_id": "uuid",
"status": "watching",
"started_at": "2026-02-08T10:00:00Z",
"watched_folders": ["/path/to/library/folder1", "/path/to/library/folder2"],
"stats": {
"files_detected": 15,
"files_processed": 12,
"files_failed": 1,
"last_activity": "2026-02-08T11:30:00Z"
}
"total_watching": null,
"watching_libraries": null
}
```
+15 -6
View File
@@ -1,3 +1,11 @@
---
method: POST
path: /api/scanner/scan
auth: jwt+admin
area: scanner
---
<!-- source: internal/handlers/scanner.go:34 Handler.ScanLibrary -->
# Scan Library
Initiate a one-time scan of a library for ebooks, manga, or comics.
@@ -26,25 +34,26 @@ The scanner automatically detects and processes files based on the library type:
### Example Request
```json
```json example-request
{
"library_id": "550e8400-e29b-41d4-a716-446655440000",
"recursive": true,
"force": false
}
```
## Response (202 Accepted)
```json
```json example-response
{
"message": "Scan initiated successfully",
"job_id": "uuid",
"library_id": "uuid",
"message": "scan job enqueued",
"job_id": "b3d3a0c2-8f4e-4c1d-9a2b-6e5f1a7c9d01",
"status": "pending"
}
```
A scan for the same library already pending or running returns **409** with
`{"error": "a scan for this library is already pending or running", "active_job_id": "..."}`.
## Error Responses
| Code | Description |
+12 -8
View File
@@ -1,3 +1,11 @@
---
method: POST
path: /api/scanner/start
auth: jwt+admin
area: scanner
---
<!-- source: internal/handlers/scanner.go:134 Handler.StartScanner -->
# Start Scanner
Start the automated background scanner for a library. Supports ebook, manga, and comic libraries.
@@ -21,21 +29,17 @@ Start the automated background scanner for a library. Supports ebook, manga, and
### Example Request
```json
```json example-request
{
"library_id": "550e8400-e29b-41d4-a716-446655440000",
"interval_seconds": 3600
"folder_paths": ["/data/books"]
}
```
## Response (200 OK)
```json
```json example-response
{
"message": "Scanner started successfully",
"library_id": "uuid",
"interval_seconds": 3600,
"status": "running"
"message": "Scanner started successfully"
}
```
+11 -4
View File
@@ -1,3 +1,11 @@
---
method: POST
path: /api/scanner/watch/start
auth: jwt+admin
area: scanner
---
<!-- source: internal/handlers/scanner.go:314 Handler.StartWatchMode -->
# Start Watch Mode
Start watch mode for a library to automatically detect and process new/modified files. Supports ebook, manga, and comic libraries with real-time file system monitoring.
@@ -24,7 +32,7 @@ Watch mode automatically detects and processes:
### Example Request
```json
```json example-request
{
"library_id": "550e8400-e29b-41d4-a716-446655440000"
}
@@ -32,11 +40,10 @@ Watch mode automatically detects and processes:
## Response (200 OK)
```json
```json example-response
{
"message": "Watch mode started successfully",
"library_id": "uuid",
"status": "watching"
"library_id": "uuid"
}
```
+10 -10
View File
@@ -1,3 +1,11 @@
---
method: POST
path: /api/scanner/stop
auth: jwt+admin
area: scanner
---
<!-- source: internal/handlers/scanner.go:168 Handler.StopScanner -->
# Stop Scanner
Stop the automated background scanner for a library.
@@ -14,19 +22,11 @@ Stop the automated background scanner for a library.
### Example Request
```json
{
"library_id": "550e8400-e29b-41d4-a716-446655440000"
}
```
## Response (200 OK)
```json
```json example-response
{
"message": "Scanner stopped successfully",
"library_id": "uuid",
"status": "stopped"
"message": "Scanner stopped successfully"
}
```
+11 -4
View File
@@ -1,3 +1,11 @@
---
method: POST
path: /api/scanner/watch/stop
auth: jwt+admin
area: scanner
---
<!-- source: internal/handlers/scanner.go:348 Handler.StopWatchMode -->
# Stop Watch Mode
Stop watch mode for a library.
@@ -14,7 +22,7 @@ Stop watch mode for a library.
### Example Request
```json
```json example-request
{
"library_id": "550e8400-e29b-41d4-a716-446655440000"
}
@@ -22,11 +30,10 @@ Stop watch mode for a library.
## Response (200 OK)
```json
```json example-response
{
"message": "Watch mode stopped successfully",
"library_id": "uuid",
"status": "stopped"
"library_id": "uuid"
}
```
+5 -2
View File
@@ -70,8 +70,11 @@ func extractRoutesFromRouterSource(t *testing.T) map[string]extractedRoute {
// names the call site actually passes (router.go: protected=/api jwt,
// admin=/api jwt+admin).
if base == "scanner.go" {
groups["protected"] = group{prefix: "/api", middleware: []string{"jwt"}}
groups["admin"] = group{prefix: "/api", middleware: []string{"jwt", "admin"}}
// Middleware values must carry the marker substrings authLabel
// matches on (AdminMiddleware, createJWTMiddleware) — the seeded
// "jwt"/"admin" tokens were invisible to it.
groups["protected"] = group{prefix: "/api", middleware: []string{"jwtMiddleware"}}
groups["admin"] = group{prefix: "/api", middleware: []string{"jwtMiddleware", "AdminMiddleware"}}
}
raw, err := os.ReadFile(file)
if err != nil {
+57 -4
View File
@@ -599,11 +599,64 @@ var Schemas = map[string]Schema{
Response: json.RawMessage(`{"status": "reordered"}`),
},
"PUT /api/collections/:id/rules/:ruleId": {
// Inline anonymous request struct — pinned by sample.
Request: json.RawMessage(`{"rule": {}}`),
// Handler is SetRuleEnabled: toggle only, request {enabled required},
// response {collection_id, is_smart}.
Request: json.RawMessage(`{"enabled": false}`),
Response: json.RawMessage(`{
"id": "", "collection_id": "", "field": "", "operator": "",
"value": "", "priority": 0, "enabled": false, "is_smart": false
"collection_id": "", "is_smart": false
}`),
},
// --- Beam batch 7: scanner ---
"POST /api/scanner/scan": {
Request: handlers.ScanLibraryRequest{},
// 202 Accepted; 409 coalescing variant adds {active_job_id}.
Response: json.RawMessage(`{
"message": "", "job_id": "", "status": ""
}`),
},
"POST /api/scanner/start": {
// Starts the long-lived watch scanner over folder_paths.
Request: handlers.ScanLibraryRequest{},
Response: json.RawMessage(`{"message": ""}`),
},
"POST /api/scanner/stop": {
Response: json.RawMessage(`{"message": ""}`),
},
"GET /api/scanner/status/:jobId": {
// result is interface{} (job payload). 404 {error} when unknown job.
Response: json.RawMessage(`{
"job_id": "", "status": "", "error": "", "result": null,
"progress": 0, "files_scanned": 0, "new_items": 0, "errors": 0
}`),
},
"GET /api/scanner/active": {
// jobs are []JobResult (job_id/status/progress/files_scanned/...).
Response: json.RawMessage(`{
"scanning": false, "jobs": []
}`),
},
"GET /api/scanner/active-summary": {
Response: json.RawMessage(`{
"scanning": false, "progress": 0
}`),
},
"POST /api/scanner/cancel/:jobId": {
Response: json.RawMessage(`{
"message": "", "job_id": "", "status": ""
}`),
},
"POST /api/scanner/watch/start": {
Request: json.RawMessage(`{"library_id": ""}`),
Response: json.RawMessage(`{"message": "", "library_id": ""}`),
},
"POST /api/scanner/watch/stop": {
Request: json.RawMessage(`{"library_id": ""}`),
Response: json.RawMessage(`{"message": "", "library_id": ""}`),
},
"GET /api/scanner/watch/status": {
Response: json.RawMessage(`{
"watching_libraries": [""], "total_watching": 0
}`),
},
}