kryten-webqueue 0.35.2__tar.gz → 0.35.3__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 (162) hide show
  1. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/CHANGELOG.md +18 -0
  2. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/static/js/admin.js +114 -10
  4. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/pyproject.toml +1 -1
  5. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/.github/workflows/gitleaks.yml +0 -0
  6. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/.github/workflows/python-publish.yml +0 -0
  7. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/.github/workflows/release.yml +0 -0
  8. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/.gitignore +0 -0
  9. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/.gitleaks.toml +0 -0
  10. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/.pre-commit-config.yaml +0 -0
  11. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/AGENT-WORKFLOW-GUIDE.md +0 -0
  12. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/README.md +0 -0
  13. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/config.example.json +0 -0
  14. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/deploy/kryten-webqueue.service +0 -0
  15. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/deploy/nginx-queue.conf +0 -0
  16. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/IMPLEMENTATION_SPEC.md +0 -0
  17. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/IMPL_API_GATE.md +0 -0
  18. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/IMPL_ECONOMY.md +0 -0
  19. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/IMPL_KRYTEN_PY.md +0 -0
  20. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/IMPL_ROBOT.md +0 -0
  21. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  22. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  23. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/Log for incorrect promo insertion problem.txt +0 -0
  24. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  25. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/PRE_PLAN_GAPS.md +0 -0
  26. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/PRODUCT_PLAN.md +0 -0
  27. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
  28. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/SPEC_EMOTE_REHOST.md +0 -0
  29. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  30. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/docs/UX_POLISH_PLAN.md +0 -0
  31. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/__init__.py +0 -0
  32. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/__main__.py +0 -0
  33. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/api_gate/__init__.py +0 -0
  34. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/api_gate/client.py +0 -0
  35. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/app.py +0 -0
  36. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/auth/__init__.py +0 -0
  37. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/auth/otp.py +0 -0
  38. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/auth/rate_limit.py +0 -0
  39. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/auth/session.py +0 -0
  40. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/__init__.py +0 -0
  41. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/db/__init__.py +0 -0
  42. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/db/_catalog.py +0 -0
  43. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/db/_connection.py +0 -0
  44. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
  45. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  46. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/db/_people.py +0 -0
  47. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  48. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/db/_queue.py +0 -0
  49. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
  50. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
  51. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
  52. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
  53. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
  54. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
  55. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/enrichment/report.py +0 -0
  56. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
  57. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
  58. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
  59. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
  60. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
  61. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
  62. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
  63. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/images.py +0 -0
  64. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/mediacms.py +0 -0
  65. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/catalog/sync.py +0 -0
  66. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/config.py +0 -0
  67. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/integrations/__init__.py +0 -0
  68. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  69. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  70. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  71. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  72. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  73. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  74. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  75. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  76. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  77. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  78. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/jobs/__init__.py +0 -0
  79. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  80. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  81. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/jobs/manager.py +0 -0
  82. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  83. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/jobs/tasks.py +0 -0
  84. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/logging_config.py +0 -0
  85. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/playlists/__init__.py +0 -0
  86. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/playlists/bulk_add.py +0 -0
  87. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/playlists/fire.py +0 -0
  88. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/playlists/importer.py +0 -0
  89. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/playlists/ordering.py +0 -0
  90. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/playlists/scheduler.py +0 -0
  91. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/promos/__init__.py +0 -0
  92. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/promos/director.py +0 -0
  93. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/queue/__init__.py +0 -0
  94. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/queue/completion.py +0 -0
  95. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/queue/ordering.py +0 -0
  96. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/queue/poller.py +0 -0
  97. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/queue/presence.py +0 -0
  98. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/queue/race_poller.py +0 -0
  99. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/queue/shadow.py +0 -0
  100. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/__init__.py +0 -0
  101. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/admin_catalog.py +0 -0
  102. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/admin_feedback.py +0 -0
  103. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  104. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/admin_jobs.py +0 -0
  105. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/admin_moderation.py +0 -0
  106. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/admin_playlists.py +0 -0
  107. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/admin_promos.py +0 -0
  108. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/admin_queue.py +0 -0
  109. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/admin_schedules.py +0 -0
  110. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/auth.py +0 -0
  111. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/catalog.py +0 -0
  112. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/feedback.py +0 -0
  113. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/pages.py +0 -0
  114. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/queue.py +0 -0
  115. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/routes/user.py +0 -0
  116. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/static/css/main.css +0 -0
  117. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  118. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/static/js/admin-promos.js +0 -0
  119. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  120. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  121. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/static/js/main.js +0 -0
  122. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/admin/index.html +0 -0
  123. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/admin/playlists.html +0 -0
  124. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/admin/promos.html +0 -0
  125. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  126. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/admin/schedules.html +0 -0
  127. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/auth/login.html +0 -0
  128. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/base.html +0 -0
  129. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/catalog/browse.html +0 -0
  130. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  131. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  132. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/feedback/index.html +0 -0
  133. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/queue/index.html +0 -0
  134. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/race.html +0 -0
  135. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/user/dashboard.html +0 -0
  136. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/templates/user/my_list.html +0 -0
  137. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/ws/__init__.py +0 -0
  138. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/ws/handler.py +0 -0
  139. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/kryten_webqueue/ws/manager.py +0 -0
  140. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/__init__.py +0 -0
  141. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_classify.py +0 -0
  142. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_config_persistence.py +0 -0
  143. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_feedback.py +0 -0
  144. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_fetchurls_sharepoint.py +0 -0
  145. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_people_facets.py +0 -0
  146. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_phase1.py +0 -0
  147. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_phase2_jobs.py +0 -0
  148. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_phase3_jobs.py +0 -0
  149. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_phase4_live_fixes.py +0 -0
  150. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_playlist_import.py +0 -0
  151. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_presence_refund.py +0 -0
  152. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_promo_director.py +0 -0
  153. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_promo_pool_exclusion.py +0 -0
  154. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_providers.py +0 -0
  155. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_queue_announce.py +0 -0
  156. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_race_poller.py +0 -0
  157. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_recently_played_hide.py +0 -0
  158. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_save_results_to_playlist.py +0 -0
  159. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_schedule_lock.py +0 -0
  160. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_search_facets.py +0 -0
  161. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_title_normalization.py +0 -0
  162. {kryten_webqueue-0.35.2 → kryten_webqueue-0.35.3}/tests/test_watchlist.py +0 -0
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.35.3] - 2026-08-13
6
+
7
+ ### Added
8
+
9
+ - **Job run detail view.** Every row in the "Recent Job Runs" table now has an
10
+ ℹ button that opens a modal with the full output for that run.
11
+ - **Pipeline enrichment reports** are rendered as a per-step table
12
+ (Processed / Changed / Skipped / Failed) with any per-step error messages
13
+ shown inline beneath their row.
14
+ - **In-progress snapshots** are clearly labelled as such with a ⏳ indicator
15
+ and show the last heartbeat values while the job is still running.
16
+ - **Legacy / other job results** fall back to pretty-printed JSON.
17
+ - **Summary column** now understands the `catalog_enrich` report format and
18
+ shows a compact per-step summary (e.g. `classify: 5200✓ · meta: 1500✓ (42s)`).
19
+ An ⏳ prefix marks an in-progress heartbeat.
20
+ - Runs table shows 15 rows (up from 10) and marks still-running rows with an
21
+ amber "running…" indicator instead of a blank ended-at cell.
22
+
5
23
  ## [0.35.2] - 2026-08-13
6
24
 
7
25
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.35.2
3
+ Version: 0.35.3
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
@@ -281,23 +281,33 @@ async function loadJobs() {
281
281
  : '<p>No jobs registered</p>';
282
282
  }
283
283
 
284
- // Recent job-run history
285
- const rResp = await fetch('/admin/jobs/runs?limit=10');
284
+ // Recent job-run history — table includes an expand button for full output
285
+ const rResp = await fetch('/admin/jobs/runs?limit=15');
286
286
  if (rResp.ok) {
287
287
  const runs = await rResp.json();
288
288
  const el = document.getElementById('job-runs');
289
289
  if (runs.length > 0) {
290
290
  el.innerHTML = `<table class="admin-table">
291
- <tr><th>Job</th><th>Started</th><th>Ended</th><th>Status</th><th>Detail</th></tr>
292
- ${runs.map(r => `
291
+ <tr><th>Job</th><th>Started</th><th>Ended</th><th>Status</th><th>Detail</th><th></th></tr>
292
+ ${runs.map(r => {
293
+ const summary = escapeHtml(summarizeRunDetail(r.detail));
294
+ const hasDetail = r.detail && r.detail.length > 2;
295
+ const expandBtn = hasDetail
296
+ ? `<button class="btn btn-sm" onclick="showRunDetail(${r.id})" title="View full output">\u2139</button>`
297
+ : '';
298
+ const endedCell = r.ended_at
299
+ ? formatLocalDateTime(r.ended_at)
300
+ : `<span style="color:var(--warning)">running\u2026</span>`;
301
+ return `
293
302
  <tr>
294
303
  <td>${escapeHtml(r.job_name || '')}</td>
295
304
  <td>${formatLocalDateTime(r.started_at)}</td>
296
- <td>${r.ended_at ? formatLocalDateTime(r.ended_at) : '—'}</td>
305
+ <td>${endedCell}</td>
297
306
  <td><span class="job-status job-status-${escapeHtml(r.status || '')}">${escapeHtml(r.status || '')}</span></td>
298
- <td class="job-detail">${escapeHtml(summarizeRunDetail(r.detail))}</td>
299
- </tr>
300
- `).join('')}
307
+ <td class="job-detail">${summary}</td>
308
+ <td>${expandBtn}</td>
309
+ </tr>`;
310
+ }).join('')}
301
311
  </table>`;
302
312
  } else {
303
313
  el.innerHTML = '<p>No job runs yet</p>';
@@ -305,6 +315,81 @@ async function loadJobs() {
305
315
  }
306
316
  }
307
317
 
318
+ // Run-detail cache so we don't re-fetch on re-open.
319
+ const RUN_DETAIL_CACHE = {};
320
+
321
+ async function showRunDetail(runId) {
322
+ if (!RUN_DETAIL_CACHE[runId]) {
323
+ const resp = await fetch('/admin/jobs/runs?limit=50');
324
+ if (resp.ok) { (await resp.json()).forEach(r => { RUN_DETAIL_CACHE[r.id] = r; }); }
325
+ }
326
+ const run = RUN_DETAIL_CACHE[runId];
327
+ if (!run) { showToast('Run not found', 'error'); return; }
328
+
329
+ const existingModal = document.getElementById('run-detail-modal');
330
+ if (existingModal) existingModal.remove();
331
+
332
+ let d = null;
333
+ try { d = run.detail ? JSON.parse(run.detail) : null; } catch (e) {}
334
+
335
+ let bodyHtml;
336
+ if (d && d.steps_run && d.steps) {
337
+ // Pipeline enrichment final report — render as a table
338
+ const dryBadge = d.dry_run
339
+ ? ' <span style="color:var(--warning);font-size:.8rem">(dry run \u2014 no writes)</span>' : '';
340
+ const elapsed = d.elapsed_sec ? ` \u00b7 ${d.elapsed_sec}s elapsed` : '';
341
+ const rowsHtml = d.steps_run.map(s => {
342
+ const r = d.steps[s];
343
+ if (!r) return '';
344
+ const errs = (r.errors || []).map(e =>
345
+ `<div style="font-size:.78rem;color:var(--danger);padding:.15rem 0">${escapeHtml(e)}</div>`
346
+ ).join('');
347
+ return `<tr>
348
+ <td><strong>${escapeHtml(s)}</strong></td>
349
+ <td>${r.processed}</td>
350
+ <td>${r.changed}</td>
351
+ <td>${r.skipped}</td>
352
+ <td style="${r.failed ? 'color:var(--danger)' : ''}">${r.failed || 0}</td>
353
+ </tr>${errs ? `<tr><td colspan="5" style="padding:0 0 .5rem 1.5rem">${errs}</td></tr>` : ''}`;
354
+ }).join('');
355
+ bodyHtml = `
356
+ <p style="margin-bottom:.75rem">
357
+ Steps: <strong>${escapeHtml(d.steps_run.join(' \u2192 '))}</strong>${dryBadge}<br>
358
+ Total items: <strong>${(d.total_items || 0).toLocaleString()}</strong>${elapsed}
359
+ </p>
360
+ <table class="admin-table">
361
+ <tr><th>Step</th><th>Processed</th><th>Changed</th><th>Skipped</th><th>Failed</th></tr>
362
+ ${rowsHtml}
363
+ </table>`;
364
+ } else if (d && d.step && typeof d.processed === 'number') {
365
+ // In-progress heartbeat snapshot
366
+ bodyHtml = `<p style="color:var(--warning)">\u23f3 Job still running — last progress snapshot:</p>
367
+ <pre style="white-space:pre-wrap;font-size:.82rem">${escapeHtml(JSON.stringify(d, null, 2))}</pre>`;
368
+ } else {
369
+ // Generic / legacy format
370
+ const raw = typeof run.detail === 'string' ? run.detail : JSON.stringify(run.detail, null, 2);
371
+ bodyHtml = `<pre style="white-space:pre-wrap;font-size:.82rem">${escapeHtml(raw)}</pre>`;
372
+ }
373
+
374
+ const overlay = document.createElement('div');
375
+ overlay.id = 'run-detail-modal';
376
+ overlay.className = 'modal-overlay';
377
+ overlay.innerHTML = `
378
+ <div class="modal-box" role="dialog" aria-modal="true">
379
+ <h3>${escapeHtml(run.job_name || 'Run')} \u2014 ${escapeHtml(run.status || '')}
380
+ <span style="font-size:.75rem;font-weight:400;opacity:.7">#${run.id}</span></h3>
381
+ <div class="modal-body">${bodyHtml}</div>
382
+ <div class="modal-actions">
383
+ <button class="btn btn-secondary" data-action="close">Close</button>
384
+ </div>
385
+ </div>`;
386
+ overlay.addEventListener('click', e => {
387
+ if (e.target === overlay || e.target.getAttribute('data-action') === 'close')
388
+ overlay.remove();
389
+ });
390
+ document.body.appendChild(overlay);
391
+ }
392
+
308
393
  async function loadAdminData() {
309
394
  // Queue status
310
395
  const qResp = await fetch('/queue/state');
@@ -376,8 +461,7 @@ function connectAdminWebSocket() {
376
461
  }, 30000);
377
462
  }
378
463
 
379
- // Summarize a job_runs.detail JSON blob for the history table. Prefers a
380
- // failure message, otherwise a compact success summary.
464
+ // Summarize a job_runs.detail JSON blob for the history table.
381
465
  function summarizeRunDetail(detail) {
382
466
  if (!detail) return '';
383
467
  let d;
@@ -385,6 +469,26 @@ function summarizeRunDetail(detail) {
385
469
  catch { return String(detail).slice(0, 160); }
386
470
  if (d && d.error) return d.error;
387
471
  if (d && typeof d === 'object') {
472
+ // Pipeline in-progress heartbeat
473
+ if (d.step && typeof d.processed === 'number') {
474
+ const bits = [`${d.step}: ${d.processed} processed`];
475
+ if (d.changed) bits.push(`${d.changed} changed`);
476
+ if (d.failed) bits.push(`${d.failed} failed`);
477
+ return '\u23f3 ' + bits.join(', ');
478
+ }
479
+ // Pipeline final report
480
+ if (d.steps_run && d.steps) {
481
+ const tag = d.dry_run ? ' [dry run]' : '';
482
+ const elapsed = d.elapsed_sec ? ` (${d.elapsed_sec}s)` : '';
483
+ const parts = d.steps_run.filter(s => d.steps[s] && d.steps[s].processed).map(s => {
484
+ const r = d.steps[s];
485
+ const bits = [];
486
+ if (r.changed) bits.push(`${r.changed}\u2713`);
487
+ if (r.failed) bits.push(`${r.failed}\u2717`);
488
+ return `${s}: ${bits.join(' ') || r.processed}`;
489
+ });
490
+ return parts.join(' \u00b7 ') + elapsed + tag;
491
+ }
388
492
  const parts = [];
389
493
  if (d.sheet) parts.push(d.sheet);
390
494
  if (d.imported_playlists && d.imported_playlists.length) parts.push(`imported: ${d.imported_playlists.join(', ')}`);
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.35.2"
3
+ version = "0.35.3"
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"