kryten-webqueue 0.19.0__tar.gz → 0.20.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/CHANGELOG.md +21 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/PKG-INFO +1 -1
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/cmsutils/fetchurls.py +29 -5
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/jobs/tasks.py +33 -1
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/logging_config.py +13 -2
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/static/css/main.css +15 -8
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/admin/index.html +4 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/pyproject.toml +1 -1
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/.gitignore +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/README.md +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/config.example.json +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/catalog/db.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/__init__.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/tests/test_search_facets.py +0 -0
|
@@ -6,6 +6,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.20.1] — 2026-06-17
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Z-Coin dashboard layout polish.** The left account column is now a fixed 320px width (was a flexible 280–360px range that shifted with content), and the tab strip has proper folder-tab styling — filled inactive tabs with hover feedback and an active tab that visually connects to its panel — instead of the previous near-invisible underline.
|
|
14
|
+
|
|
15
|
+
[0.20.1]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.20.1
|
|
16
|
+
|
|
17
|
+
## [0.20.0] — 2026-06-17
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Richer fetchurls / job logging.** The fetchurls job now logs a per-section resolved/failed summary and a WARNING line for every failing URL (with its Excel row and the reason), and folds a compact `failures_detail` list into the `job_runs` record so the admin "Detail" column shows a concrete example instead of just a count. The `run()` result gained `section_summary` and `failure_details`.
|
|
22
|
+
- **Actionable log format.** Application loggers (`kryten_webqueue.*`) now include the source `file:line` in each line via a dedicated formatter, while uvicorn keeps its leaner format.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **SharePoint download failures lost their detail.** `download_sharepoint_xlsx` raised `SystemExit` (a `BaseException` the job manager's `except Exception` couldn't catch), so a failed download could bubble up uncaught with no recorded detail. It now raises `RuntimeError` with the HTTP status and a response excerpt, so the failure is caught, logged, and recorded in the job run.
|
|
27
|
+
|
|
28
|
+
[0.20.0]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.20.0
|
|
29
|
+
|
|
9
30
|
## [0.19.0] — 2026-06-17
|
|
10
31
|
|
|
11
32
|
### Changed
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/cmsutils/fetchurls.py
RENAMED
|
@@ -345,9 +345,9 @@ def download_sharepoint_xlsx(token: str, sharing_url: str) -> tuple[bytes, str,
|
|
|
345
345
|
meta_url = f"{GRAPH_BASE}/shares/{encoded}/driveItem"
|
|
346
346
|
r = requests.get(meta_url, headers=headers, timeout=REQUEST_TIMEOUT)
|
|
347
347
|
if r.status_code != 200:
|
|
348
|
-
|
|
349
|
-
f"
|
|
350
|
-
f"
|
|
348
|
+
raise RuntimeError(
|
|
349
|
+
f"Could not resolve SharePoint file via Graph API (HTTP {r.status_code}). "
|
|
350
|
+
f"Response: {r.text[:500]}"
|
|
351
351
|
)
|
|
352
352
|
item = r.json()
|
|
353
353
|
|
|
@@ -368,10 +368,17 @@ def download_sharepoint_xlsx(token: str, sharing_url: str) -> tuple[bytes, str,
|
|
|
368
368
|
if dl_r.status_code == 302:
|
|
369
369
|
download_url = dl_r.headers["Location"]
|
|
370
370
|
else:
|
|
371
|
-
|
|
371
|
+
raise RuntimeError(
|
|
372
|
+
f"Could not get SharePoint download URL (HTTP {dl_r.status_code}). "
|
|
373
|
+
f"Response: {dl_r.text[:500]}"
|
|
374
|
+
)
|
|
372
375
|
|
|
373
376
|
content_r = requests.get(download_url, timeout=60)
|
|
374
|
-
content_r.
|
|
377
|
+
if not content_r.ok:
|
|
378
|
+
raise RuntimeError(
|
|
379
|
+
f"SharePoint file download failed (HTTP {content_r.status_code}). "
|
|
380
|
+
f"Response: {content_r.text[:500]}"
|
|
381
|
+
)
|
|
375
382
|
return content_r.content, drive_id, item_id
|
|
376
383
|
|
|
377
384
|
|
|
@@ -1415,6 +1422,10 @@ def run(params: dict, *, config, progress=None) -> dict:
|
|
|
1415
1422
|
section_lines: dict[str, list[str]] = {}
|
|
1416
1423
|
section_labels: dict[str, str] = {}
|
|
1417
1424
|
all_results: dict[str, list[ProcessResult]] = {}
|
|
1425
|
+
# Per-section {resolved, failed} counts and a flat list of failed rows for
|
|
1426
|
+
# actionable diagnostics (surfaced in the job log + job_runs detail).
|
|
1427
|
+
section_summary: dict[str, dict] = {}
|
|
1428
|
+
failure_details: list[dict] = []
|
|
1418
1429
|
|
|
1419
1430
|
try:
|
|
1420
1431
|
for slug, url_rows in sections.items():
|
|
@@ -1428,16 +1439,27 @@ def run(params: dict, *, config, progress=None) -> dict:
|
|
|
1428
1439
|
all_results[slug] = results
|
|
1429
1440
|
write_playlist(out_dir / f"{sheet_name}-{slug}.txt", results)
|
|
1430
1441
|
lines = []
|
|
1442
|
+
sec_resolved = 0
|
|
1443
|
+
sec_failed = 0
|
|
1431
1444
|
for r in results:
|
|
1432
1445
|
if r.success:
|
|
1433
1446
|
resolved += 1
|
|
1447
|
+
sec_resolved += 1
|
|
1434
1448
|
if r.resolved_url != r.original_url:
|
|
1435
1449
|
downloaded += 1
|
|
1436
1450
|
token = _extract_manifest_token(r.resolved_url)
|
|
1437
1451
|
lines.append(f"cm:{token}" if token else r.resolved_url)
|
|
1438
1452
|
else:
|
|
1439
1453
|
failures += 1
|
|
1454
|
+
sec_failed += 1
|
|
1455
|
+
failure_details.append({
|
|
1456
|
+
"section": label,
|
|
1457
|
+
"row": r.row_number,
|
|
1458
|
+
"url": r.original_url,
|
|
1459
|
+
"note": r.note,
|
|
1460
|
+
})
|
|
1440
1461
|
section_lines[slug] = lines
|
|
1462
|
+
section_summary[label] = {"resolved": sec_resolved, "failed": sec_failed}
|
|
1441
1463
|
write_failures(out_dir / f"{sheet_name}-failures.txt", all_results)
|
|
1442
1464
|
finally:
|
|
1443
1465
|
run_fetch = original_run_fetch
|
|
@@ -1461,6 +1483,8 @@ def run(params: dict, *, config, progress=None) -> dict:
|
|
|
1461
1483
|
"writeback": writeback_stats,
|
|
1462
1484
|
"section_lines": section_lines,
|
|
1463
1485
|
"section_labels": section_labels,
|
|
1486
|
+
"section_summary": section_summary,
|
|
1487
|
+
"failure_details": failure_details,
|
|
1464
1488
|
"imported_playlists": [], # filled in by the async job wrapper
|
|
1465
1489
|
"dry_run": dry_run,
|
|
1466
1490
|
}
|
|
@@ -185,9 +185,41 @@ async def fetchurls_job(params: dict, ctx):
|
|
|
185
185
|
info = await _import_section_as_playlist(ctx, name, lines, triggered_by)
|
|
186
186
|
if info:
|
|
187
187
|
imported.append(info["name"])
|
|
188
|
+
logger.info("fetchurls: imported %d item(s) into '%s'", info["count"], info["name"])
|
|
188
189
|
result["imported_playlists"] = imported
|
|
189
|
-
|
|
190
|
+
|
|
191
|
+
# Per-section resolved/failed summary for the process log.
|
|
192
|
+
sheet = result.get("sheet", "?")
|
|
193
|
+
for label, counts in (result.get("section_summary") or {}).items():
|
|
194
|
+
logger.info(
|
|
195
|
+
"fetchurls[%s] section '%s': resolved %d / failed %d",
|
|
196
|
+
sheet, label, counts.get("resolved", 0), counts.get("failed", 0),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Surface each failing URL (with its Excel row) at WARNING, and keep a
|
|
200
|
+
# compact copy in the job result so the admin "Detail" column shows it.
|
|
201
|
+
failure_details = result.get("failure_details") or []
|
|
202
|
+
if failure_details:
|
|
203
|
+
logger.warning(
|
|
204
|
+
"fetchurls[%s]: %d URL(s) failed to resolve", sheet, len(failure_details)
|
|
205
|
+
)
|
|
206
|
+
for f in failure_details:
|
|
207
|
+
logger.warning(
|
|
208
|
+
"fetchurls[%s] [%s row %s] %s — %s",
|
|
209
|
+
sheet, f.get("section", "?"), f.get("row", "?"),
|
|
210
|
+
f.get("url", ""), f.get("note", ""),
|
|
211
|
+
)
|
|
212
|
+
# Keep at most 25 in the persisted detail to stay compact.
|
|
213
|
+
result["failures_detail"] = [
|
|
214
|
+
{"section": f.get("section"), "row": f.get("row"),
|
|
215
|
+
"url": f.get("url"), "note": f.get("note")}
|
|
216
|
+
for f in failure_details[:25]
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
# Trim the bulky intermediates from the persisted detail.
|
|
220
|
+
result.pop("section_lines", None)
|
|
190
221
|
result.pop("section_labels", None)
|
|
222
|
+
result.pop("failure_details", None)
|
|
191
223
|
return result
|
|
192
224
|
|
|
193
225
|
|
|
@@ -43,6 +43,12 @@ def build_log_config(log_level: str = "INFO", promo_log_level: str | None = None
|
|
|
43
43
|
"format": "%(asctime)s %(levelname)-8s %(name)s: %(message)s",
|
|
44
44
|
"datefmt": "%Y-%m-%d %H:%M:%S",
|
|
45
45
|
},
|
|
46
|
+
# Application formatter includes the source file:line so every app
|
|
47
|
+
# log line is actionable (uvicorn keeps the leaner "default").
|
|
48
|
+
"app": {
|
|
49
|
+
"format": "%(asctime)s %(levelname)-8s %(name)s %(filename)s:%(lineno)d: %(message)s",
|
|
50
|
+
"datefmt": "%Y-%m-%d %H:%M:%S",
|
|
51
|
+
},
|
|
46
52
|
"access": {
|
|
47
53
|
"format": "%(asctime)s %(levelname)-8s %(name)s: %(message)s",
|
|
48
54
|
"datefmt": "%Y-%m-%d %H:%M:%S",
|
|
@@ -54,6 +60,11 @@ def build_log_config(log_level: str = "INFO", promo_log_level: str | None = None
|
|
|
54
60
|
"formatter": "default",
|
|
55
61
|
"stream": "ext://sys.stderr",
|
|
56
62
|
},
|
|
63
|
+
"appconsole": {
|
|
64
|
+
"class": "logging.StreamHandler",
|
|
65
|
+
"formatter": "app",
|
|
66
|
+
"stream": "ext://sys.stderr",
|
|
67
|
+
},
|
|
57
68
|
"access": {
|
|
58
69
|
"class": "logging.StreamHandler",
|
|
59
70
|
"formatter": "access",
|
|
@@ -64,14 +75,14 @@ def build_log_config(log_level: str = "INFO", promo_log_level: str | None = None
|
|
|
64
75
|
"loggers": {
|
|
65
76
|
"kryten_webqueue": {
|
|
66
77
|
"level": app_level,
|
|
67
|
-
"handlers": ["
|
|
78
|
+
"handlers": ["appconsole"],
|
|
68
79
|
"propagate": False,
|
|
69
80
|
},
|
|
70
81
|
# Promo subsystem: independently tunable so operators can crank it to
|
|
71
82
|
# DEBUG for a deep dive without flooding the rest of the app.
|
|
72
83
|
"kryten_webqueue.promos": {
|
|
73
84
|
"level": promo_level,
|
|
74
|
-
"handlers": ["
|
|
85
|
+
"handlers": ["appconsole"],
|
|
75
86
|
"propagate": False,
|
|
76
87
|
},
|
|
77
88
|
"uvicorn": {"level": "INFO", "handlers": ["console"], "propagate": False},
|
|
@@ -797,7 +797,7 @@ a.np-chip {
|
|
|
797
797
|
}
|
|
798
798
|
.dashboard-grid {
|
|
799
799
|
display: grid;
|
|
800
|
-
grid-template-columns: minmax(
|
|
800
|
+
grid-template-columns: 320px minmax(0, 1fr);
|
|
801
801
|
gap: 1.5rem;
|
|
802
802
|
margin-top: 1.5rem;
|
|
803
803
|
align-items: start;
|
|
@@ -816,27 +816,34 @@ a.np-chip {
|
|
|
816
816
|
/* Dashboard tabbed container */
|
|
817
817
|
.tabs {
|
|
818
818
|
display: flex;
|
|
819
|
-
gap: 0.
|
|
819
|
+
gap: 0.35rem;
|
|
820
820
|
border-bottom: 1px solid var(--border);
|
|
821
|
-
margin-bottom:
|
|
821
|
+
margin-bottom: 1.25rem;
|
|
822
822
|
}
|
|
823
823
|
.tab-btn {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
border
|
|
824
|
+
appearance: none;
|
|
825
|
+
background: var(--bg-secondary);
|
|
826
|
+
border: 1px solid var(--border);
|
|
827
|
+
border-bottom: none;
|
|
828
|
+
border-radius: var(--radius) var(--radius) 0 0;
|
|
827
829
|
color: var(--text-secondary);
|
|
828
830
|
cursor: pointer;
|
|
831
|
+
font-family: inherit;
|
|
829
832
|
font-size: 0.95rem;
|
|
830
833
|
font-weight: 600;
|
|
831
|
-
padding: 0.6rem
|
|
834
|
+
padding: 0.6rem 1.1rem;
|
|
832
835
|
margin-bottom: -1px;
|
|
836
|
+
transition: color 0.15s, background 0.15s;
|
|
833
837
|
}
|
|
834
838
|
.tab-btn:hover {
|
|
835
839
|
color: var(--text-primary);
|
|
840
|
+
background: var(--bg-hover);
|
|
836
841
|
}
|
|
837
842
|
.tab-btn.active {
|
|
838
843
|
color: var(--accent);
|
|
839
|
-
|
|
844
|
+
background: var(--bg-card);
|
|
845
|
+
border-color: var(--border);
|
|
846
|
+
border-bottom: 1px solid var(--bg-card);
|
|
840
847
|
}
|
|
841
848
|
.tab-panel[hidden] {
|
|
842
849
|
display: none;
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/admin/index.html
RENAMED
|
@@ -321,6 +321,10 @@ function summarizeRunDetail(detail) {
|
|
|
321
321
|
if (d.imported_playlists && d.imported_playlists.length) parts.push(`imported: ${d.imported_playlists.join(', ')}`);
|
|
322
322
|
if (typeof d.resolved === 'number') parts.push(`resolved ${d.resolved}`);
|
|
323
323
|
if (typeof d.failures === 'number' && d.failures) parts.push(`${d.failures} failed`);
|
|
324
|
+
if (d.failures_detail && d.failures_detail.length) {
|
|
325
|
+
const f = d.failures_detail[0];
|
|
326
|
+
parts.push(`e.g. [${f.section || '?'} row ${f.row || '?'}] ${f.note || ''}`.trim());
|
|
327
|
+
}
|
|
324
328
|
if (typeof d.committed === 'number') parts.push(`committed ${d.committed}`);
|
|
325
329
|
if (typeof d.added_to_playlist !== 'undefined' && d.added_to_playlist) parts.push(`→ playlist ${d.added_to_playlist}`);
|
|
326
330
|
if (parts.length) return parts.join(' · ');
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/cmsutils/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/cmsutils/_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/cmsutils/enrichtv.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/ytpipe/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/integrations/ytpipe/downloader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/admin/promos.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/queue/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.19.0 → kryten_webqueue-0.20.1}/kryten_webqueue/templates/user/dashboard.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|