kryten-webqueue 0.29.0__tar.gz → 0.31.0__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.
Files changed (127) hide show
  1. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/CHANGELOG.md +21 -0
  2. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/app.py +3 -1
  4. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/catalog/sync.py +13 -2
  5. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/jobs/manager.py +30 -7
  6. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/static/js/admin.js +6 -1
  7. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/pyproject.toml +1 -1
  8. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/.github/workflows/python-publish.yml +0 -0
  9. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/.github/workflows/release.yml +0 -0
  10. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/.gitignore +0 -0
  11. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/README.md +0 -0
  12. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/config.example.json +0 -0
  13. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/deploy/kryten-webqueue.service +0 -0
  14. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/deploy/nginx-queue.conf +0 -0
  15. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
  16. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/docs/IMPL_API_GATE.md +0 -0
  17. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/docs/IMPL_ECONOMY.md +0 -0
  18. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/docs/IMPL_KRYTEN_PY.md +0 -0
  19. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/docs/IMPL_ROBOT.md +0 -0
  20. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  21. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  22. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/docs/Log for incorrect promo insertion problem.txt +0 -0
  23. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  24. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/docs/PRE_PLAN_GAPS.md +0 -0
  25. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/docs/PRODUCT_PLAN.md +0 -0
  26. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  27. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/docs/UX_POLISH_PLAN.md +0 -0
  28. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/__init__.py +0 -0
  29. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/__main__.py +0 -0
  30. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/api_gate/__init__.py +0 -0
  31. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/api_gate/client.py +0 -0
  32. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/auth/__init__.py +0 -0
  33. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/auth/otp.py +0 -0
  34. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/auth/rate_limit.py +0 -0
  35. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/auth/session.py +0 -0
  36. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/catalog/__init__.py +0 -0
  37. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/catalog/db/__init__.py +0 -0
  38. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/catalog/db/_catalog.py +0 -0
  39. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/catalog/db/_connection.py +0 -0
  40. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  41. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  42. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/catalog/db/_queue.py +0 -0
  43. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/catalog/images.py +0 -0
  44. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/catalog/mediacms.py +0 -0
  45. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/config.py +0 -0
  46. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/integrations/__init__.py +0 -0
  47. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  48. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  49. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  50. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  51. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  52. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  53. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  54. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  55. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/jobs/__init__.py +0 -0
  56. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  57. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/jobs/tasks.py +0 -0
  58. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/logging_config.py +0 -0
  59. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/playlists/__init__.py +0 -0
  60. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
  61. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/playlists/fire.py +0 -0
  62. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/playlists/importer.py +0 -0
  63. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/playlists/ordering.py +0 -0
  64. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/playlists/scheduler.py +0 -0
  65. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/promos/__init__.py +0 -0
  66. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/promos/director.py +0 -0
  67. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/queue/__init__.py +0 -0
  68. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/queue/ordering.py +0 -0
  69. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/queue/poller.py +0 -0
  70. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/queue/presence.py +0 -0
  71. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/queue/race_poller.py +0 -0
  72. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/queue/shadow.py +0 -0
  73. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/__init__.py +0 -0
  74. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
  75. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/admin_feedback.py +0 -0
  76. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
  77. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/admin_moderation.py +0 -0
  78. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
  79. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/admin_promos.py +0 -0
  80. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/admin_queue.py +0 -0
  81. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
  82. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/auth.py +0 -0
  83. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/catalog.py +0 -0
  84. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/feedback.py +0 -0
  85. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/pages.py +0 -0
  86. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/queue.py +0 -0
  87. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/routes/user.py +0 -0
  88. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/static/css/main.css +0 -0
  89. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  90. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/static/js/admin-promos.js +0 -0
  91. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  92. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  93. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/static/js/main.js +0 -0
  94. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/admin/index.html +0 -0
  95. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
  96. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/admin/promos.html +0 -0
  97. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  98. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
  99. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/auth/login.html +0 -0
  100. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/base.html +0 -0
  101. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
  102. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  103. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  104. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/feedback/index.html +0 -0
  105. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/queue/index.html +0 -0
  106. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/race.html +0 -0
  107. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/templates/user/dashboard.html +0 -0
  108. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/ws/__init__.py +0 -0
  109. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/ws/handler.py +0 -0
  110. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/kryten_webqueue/ws/manager.py +0 -0
  111. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/__init__.py +0 -0
  112. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_config_persistence.py +0 -0
  113. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_feedback.py +0 -0
  114. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_fetchurls_sharepoint.py +0 -0
  115. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_phase1.py +0 -0
  116. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_phase2_jobs.py +0 -0
  117. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_phase3_jobs.py +0 -0
  118. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_phase4_live_fixes.py +0 -0
  119. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_playlist_import.py +0 -0
  120. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_presence_refund.py +0 -0
  121. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_promo_director.py +0 -0
  122. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_promo_pool_exclusion.py +0 -0
  123. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_queue_announce.py +0 -0
  124. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_race_poller.py +0 -0
  125. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_save_results_to_playlist.py +0 -0
  126. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_schedule_lock.py +0 -0
  127. {kryten_webqueue-0.29.0 → kryten_webqueue-0.31.0}/tests/test_search_facets.py +0 -0
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
5
5
  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
 
8
+ ## [0.31.0] — 2026-07-04
9
+
10
+ ### Fixed
11
+
12
+ - **Moderation list "Remove" button now works.** The button was rendered with `onclick="removeModEntry(${JSON.stringify(e.username)})"` — `JSON.stringify` wraps the value in double quotes that immediately terminate the `onclick="…"` attribute, so browsers reported *"unexpected end of input at admin:1:16"* and the function was never called. The button now uses a `data-mod-rm-username` attribute with a delegated `click` listener on the stable `#mod-entries-list` container, consistent with how the "Moderate…" buttons in the Recent Users table work. The existing `confirm()` dialog inside `removeModEntry` is unaffected and will now prompt as intended.
13
+
14
+ [0.31.0]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.31.0
15
+
16
+ ## [0.30.0] — 2026-07-04
17
+
18
+ ### Fixed
19
+
20
+ - **Clean shutdown when a catalog sync (or other job) is in progress.** Previously, shutting the server down while a background job was running caused a cascade of WARNING/ERROR log lines as the database connection and HTTP client were closed underneath the still-running job task. Three changes fix this:
21
+ - `JobManager` gains a `stop()` method that cancels all in-flight task and awaits them.
22
+ - The lifespan shutdown now calls `job_manager.stop()` *before* closing the database, HTTP clients, or any other shared resource, so the cancelled tasks can still record their final status cleanly.
23
+ - Background loop tasks (`bg_tasks`) are now properly awaited after cancellation, preventing "Task exception was never retrieved" warnings.
24
+ - `JobManager._execute` wraps every database write in a defensive `try/except` so that a race between task teardown and resource teardown never produces an unhandled exception or spurious ERROR log.
25
+ - `CatalogSync.sync` adds an explicit `asyncio.CancelledError` handler that logs a clean info message and re-raises (letting `JobManager` record the `cancelled` status), and wraps the error-path `finish_sync_log` calls defensively for the same reason.
26
+
27
+ [0.30.0]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.30.0
28
+
8
29
  ## [0.29.0] — 2026-07-04
9
30
 
10
31
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kryten-webqueue
3
- Version: 0.29.0
3
+ Version: 0.31.0
4
4
  Summary: Netflix/Tubi-style catalog browser and pay-to-play queue management for CyTube
5
5
  Author: grobertson
6
6
  License-Expression: MIT
@@ -227,9 +227,11 @@ async def lifespan(app: FastAPI):
227
227
 
228
228
  yield
229
229
 
230
- # Shutdown
230
+ # Shutdown — order matters: cancel in-flight work before closing resources.
231
231
  for task in bg_tasks:
232
232
  task.cancel()
233
+ await asyncio.gather(*bg_tasks, return_exceptions=True)
234
+ await job_manager.stop() # cancel running jobs while DB/client still open
233
235
  await poller.stop()
234
236
  await race_poller.stop()
235
237
  await scheduler.stop()
@@ -96,14 +96,25 @@ class CatalogSync:
96
96
 
97
97
  await self._db.finish_sync_log(log_id, stats, "completed")
98
98
  logger.info(f"Catalog sync complete: {stats} ({page} pages)")
99
+ except asyncio.CancelledError:
100
+ # Server is shutting down — propagate cleanly so JobManager can
101
+ # record the cancelled status while the DB is still open.
102
+ logger.info("Catalog sync cancelled (shutdown in progress)")
103
+ raise
99
104
  except httpx.TransportError as exc:
100
105
  logger.error(_describe_httpx_error(exc, f"{self._url}/api/v1/media"))
101
106
  stats["errors"] += 1
102
- await self._db.finish_sync_log(log_id, stats, "error")
107
+ try:
108
+ await self._db.finish_sync_log(log_id, stats, "error")
109
+ except Exception: # noqa: BLE001
110
+ logger.debug("Could not persist sync error status (DB unavailable)")
103
111
  except Exception as exc:
104
112
  logger.exception(f"Catalog sync failed: {type(exc).__name__}: {exc}")
105
113
  stats["errors"] += 1
106
- await self._db.finish_sync_log(log_id, stats, "error")
114
+ try:
115
+ await self._db.finish_sync_log(log_id, stats, "error")
116
+ except Exception: # noqa: BLE001
117
+ logger.debug("Could not persist sync error status (DB unavailable)")
107
118
 
108
119
  async def _process_item(self, media: dict, stats: dict):
109
120
  token = media.get("friendly_token")
@@ -158,6 +158,20 @@ class JobManager:
158
158
  def is_running(self, name: str) -> bool:
159
159
  return name in self._running
160
160
 
161
+ async def stop(self) -> None:
162
+ """Cancel all running job tasks and wait for them to finish.
163
+
164
+ Call this during application shutdown *before* closing shared resources
165
+ (database, HTTP clients) so that in-progress tasks receive
166
+ ``CancelledError`` while those resources are still usable.
167
+ """
168
+ if not self._running:
169
+ return
170
+ logger.info("Stopping %d running job(s) for shutdown", len(self._running))
171
+ for task in list(self._running.values()):
172
+ task.cancel()
173
+ await asyncio.gather(*self._running.values(), return_exceptions=True)
174
+
161
175
  async def run(self, name: str, *, triggered_by: str | None = None,
162
176
  params: dict | None = None) -> dict:
163
177
  """Start a job in the background. Returns immediately.
@@ -191,19 +205,28 @@ class JobManager:
191
205
  detail = json.dumps(result) if result is not None else None
192
206
  await self._db.finish_job_run(run_id, "completed", detail)
193
207
  except asyncio.CancelledError:
194
- await self._db.finish_job_run(run_id, "cancelled", None)
208
+ try:
209
+ await self._db.finish_job_run(run_id, "cancelled", None)
210
+ except Exception: # noqa: BLE001
211
+ logger.debug("Could not persist 'cancelled' status for job '%s' (DB unavailable)", name)
195
212
  raise
196
213
  except JobError as exc:
197
214
  # Expected, user-facing failure (bad input/config): record a clean
198
215
  # message and log without a stack trace.
199
216
  logger.warning("Job '%s' failed: %s", name, exc)
200
- await self._db.finish_job_run(
201
- run_id, "failed", json.dumps({"error": str(exc)})
202
- )
217
+ try:
218
+ await self._db.finish_job_run(
219
+ run_id, "failed", json.dumps({"error": str(exc)})
220
+ )
221
+ except Exception: # noqa: BLE001
222
+ logger.debug("Could not persist 'failed' status for job '%s' (DB unavailable)", name)
203
223
  except Exception as exc: # noqa: BLE001 - record any failure
204
224
  logger.exception("Job '%s' failed", name)
205
- await self._db.finish_job_run(
206
- run_id, "failed", json.dumps({"error": f"{type(exc).__name__}: {exc}"})
207
- )
225
+ try:
226
+ await self._db.finish_job_run(
227
+ run_id, "failed", json.dumps({"error": f"{type(exc).__name__}: {exc}"})
228
+ )
229
+ except Exception: # noqa: BLE001
230
+ logger.debug("Could not persist 'failed' status for job '%s' (DB unavailable)", name)
208
231
  finally:
209
232
  self._running.pop(name, None)
@@ -527,7 +527,7 @@ function renderModEntries(el, entries) {
527
527
  <td>${escapeHtml(e.reason || '—')}</td>
528
528
  <td>${escapeHtml(e.moderator || '—')}</td>
529
529
  <td>${e.added_at ? formatLocalDateTime(e.added_at) : '—'}</td>
530
- <td><button class="btn btn-sm btn-danger" onclick="removeModEntry(${JSON.stringify(e.username)})">Remove</button></td>
530
+ <td><button class="btn btn-sm btn-danger" data-mod-rm-username="${escapeHtml(e.username)}">Remove</button></td>
531
531
  </tr>`).join('')}
532
532
  </table>`;
533
533
  }
@@ -561,6 +561,11 @@ async function submitAddModEntry() {
561
561
  }
562
562
 
563
563
  document.getElementById('mod-entries-refresh').addEventListener('click', loadModEntries);
564
+
565
+ document.getElementById('mod-entries-list').addEventListener('click', (e) => {
566
+ const btn = e.target.closest('[data-mod-rm-username]');
567
+ if (btn) removeModEntry(btn.dataset.modRmUsername);
568
+ });
564
569
  document.querySelectorAll('.mod-filter-btn').forEach(btn => {
565
570
  btn.addEventListener('click', () => {
566
571
  document.querySelectorAll('.mod-filter-btn').forEach(b => b.classList.remove('active'));
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.29.0"
3
+ version = "0.31.0"
4
4
  description = "Netflix/Tubi-style catalog browser and pay-to-play queue management for CyTube"
5
5
  readme = "README.md"
6
6
  license = "MIT"