kryten-webqueue 0.46.1__tar.gz → 0.48.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 (248) hide show
  1. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/.gitignore +4 -0
  2. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/CHANGELOG.md +35 -0
  3. kryten_webqueue-0.48.0/Containerfile +23 -0
  4. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/PKG-INFO +4 -1
  5. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/config.example.json +21 -0
  6. kryten_webqueue-0.48.0/deploy/podman/webqueue/webqueue-app.container +24 -0
  7. kryten_webqueue-0.48.0/deploy/podman/webqueue/webqueue.network +3 -0
  8. kryten_webqueue-0.48.0/docs/DATABASE_ARCHITECTURE_PLAN.md +206 -0
  9. kryten_webqueue-0.48.0/docs/postgres-cutover.md +94 -0
  10. kryten_webqueue-0.48.0/docs/postgres-migration/PRD-postgres-migration.md +203 -0
  11. kryten_webqueue-0.48.0/docs/postgres-migration/SPEC-Sortie-1-db-config-and-pool.md +90 -0
  12. kryten_webqueue-0.48.0/docs/postgres-migration/SPEC-Sortie-2-connection-layer-port.md +137 -0
  13. kryten_webqueue-0.48.0/docs/postgres-migration/SPEC-Sortie-3-fts5-to-tsvector.md +113 -0
  14. kryten_webqueue-0.48.0/docs/postgres-migration/SPEC-Sortie-4-tmdb-index-database.md +96 -0
  15. kryten_webqueue-0.48.0/docs/postgres-migration/SPEC-Sortie-5-etl-migration.md +69 -0
  16. kryten_webqueue-0.48.0/docs/postgres-migration/SPEC-Sortie-6-tests-cutover-release.md +93 -0
  17. kryten_webqueue-0.48.0/docs/sqlite-domain-separation/PRD-sqlite-domain-separation.md +144 -0
  18. kryten_webqueue-0.48.0/docs/sqlite-domain-separation/SPEC-Sortie-1-schema-partitioning-and-config.md +113 -0
  19. kryten_webqueue-0.48.0/docs/sqlite-domain-separation/SPEC-Sortie-2-multi-db-connection-layer.md +106 -0
  20. kryten_webqueue-0.48.0/docs/sqlite-domain-separation/SPEC-Sortie-3-cross-domain-query-decoupling.md +104 -0
  21. kryten_webqueue-0.48.0/docs/sqlite-domain-separation/SPEC-Sortie-4-etl-split-script-and-validation.md +51 -0
  22. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/app.py +9 -1
  23. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/__init__.py +680 -0
  24. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/_base_domain.py +138 -0
  25. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_catalog.py +189 -68
  26. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/_catalog_db.py +57 -0
  27. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/_jobs_db.py +151 -0
  28. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/_queue_db.py +198 -0
  29. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/_users_db.py +53 -0
  30. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/engine.py +29 -0
  31. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/schemas/__init__.py +13 -0
  32. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/schemas/catalog_schema.py +148 -0
  33. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/schemas/jobs_schema.py +67 -0
  34. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/schemas/queue_schema.py +136 -0
  35. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/schemas/users_schema.py +78 -0
  36. kryten_webqueue-0.48.0/kryten_webqueue/catalog/db/sql/001_initial_schema.sql +463 -0
  37. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/config.py +151 -2
  38. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/ytpipe/downloader.py +1 -3
  39. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/tasks.py +27 -0
  40. kryten_webqueue-0.48.0/kryten_webqueue/migrate_sqlite_to_pg.py +350 -0
  41. kryten_webqueue-0.48.0/kryten_webqueue/scripts/__init__.py +1 -0
  42. kryten_webqueue-0.48.0/kryten_webqueue/scripts/split_databases.py +371 -0
  43. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/pyproject.toml +6 -1
  44. kryten_webqueue-0.48.0/scripts/split_databases.py +13 -0
  45. kryten_webqueue-0.48.0/tests/test_concurrency_split.py +294 -0
  46. kryten_webqueue-0.48.0/tests/test_database_config_and_schemas.py +163 -0
  47. kryten_webqueue-0.48.0/tests/test_multi_db_connection.py +197 -0
  48. kryten_webqueue-0.48.0/tests/test_postgres_config_and_engine.py +77 -0
  49. kryten_webqueue-0.48.0/tests/test_postgres_pruner.py +131 -0
  50. kryten_webqueue-0.46.1/kryten_webqueue/catalog/db/__init__.py +0 -42
  51. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/.github/workflows/gitleaks.yml +0 -0
  52. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/.github/workflows/python-publish.yml +0 -0
  53. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/.github/workflows/release.yml +0 -0
  54. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/.gitleaks.toml +0 -0
  55. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/.pre-commit-config.yaml +0 -0
  56. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/AGENT-WORKFLOW-GUIDE.md +0 -0
  57. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/AGENTS.md +0 -0
  58. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/README.md +0 -0
  59. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/deploy/kryten-webqueue.service +0 -0
  60. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/deploy/nginx-queue.conf +0 -0
  61. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
  62. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/IMPL_API_GATE.md +0 -0
  63. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/IMPL_ECONOMY.md +0 -0
  64. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/IMPL_KRYTEN_PY.md +0 -0
  65. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/IMPL_ROBOT.md +0 -0
  66. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  67. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  68. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/Log for incorrect promo insertion problem.txt +0 -0
  69. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  70. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/PRD_TMDB_LOCAL_INDEX.md +0 -0
  71. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/PRE_PLAN_GAPS.md +0 -0
  72. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/PRODUCT_PLAN.md +0 -0
  73. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/PUBLIC_API.md +0 -0
  74. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/SPEC-Visual-UX-Sprint-August-2026.md +0 -0
  75. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/SPEC_ADMIN_ITEM_EDITOR.md +0 -0
  76. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
  77. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/SPEC_EMOTE_REHOST.md +0 -0
  78. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  79. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/SPEC_TMDB_LOCAL_INDEX.md +0 -0
  80. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/docs/UX_POLISH_PLAN.md +0 -0
  81. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/__init__.py +0 -0
  82. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/__main__.py +0 -0
  83. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/api_gate/__init__.py +0 -0
  84. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/api_gate/client.py +0 -0
  85. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/auth/__init__.py +0 -0
  86. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/auth/device_keys.py +0 -0
  87. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/auth/otp.py +0 -0
  88. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/auth/rate_limit.py +0 -0
  89. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/auth/session.py +0 -0
  90. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/__init__.py +0 -0
  91. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_blackouts.py +0 -0
  92. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_connection.py +0 -0
  93. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_devices.py +0 -0
  94. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
  95. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  96. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_fetch_queue.py +0 -0
  97. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_motd.py +0 -0
  98. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_people.py +0 -0
  99. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  100. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_queue.py +0 -0
  101. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
  102. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
  103. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
  104. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
  105. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
  106. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
  107. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/report.py +0 -0
  108. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
  109. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
  110. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
  111. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/identify.py +0 -0
  112. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
  113. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
  114. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
  115. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
  116. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/images.py +0 -0
  117. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/mediacms.py +0 -0
  118. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/sync.py +0 -0
  119. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/__init__.py +0 -0
  120. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/_schema.py +0 -0
  121. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/_textmatch.py +0 -0
  122. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/_ttscrape.py +0 -0
  123. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/builder.py +0 -0
  124. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/coverage.py +0 -0
  125. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/catalog/tmdb_index/index.py +0 -0
  126. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/__init__.py +0 -0
  127. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  128. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  129. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/blackout_scan.py +0 -0
  130. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  131. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  132. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  133. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  134. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  135. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  136. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  137. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/__init__.py +0 -0
  138. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  139. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  140. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/log_capture.py +0 -0
  141. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/manager.py +0 -0
  142. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/motd_publish.py +0 -0
  143. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  144. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/logging_config.py +0 -0
  145. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/motd/__init__.py +0 -0
  146. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/motd/builder.py +0 -0
  147. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/motd/render.py +0 -0
  148. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/playlists/__init__.py +0 -0
  149. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
  150. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/playlists/fire.py +0 -0
  151. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/playlists/importer.py +0 -0
  152. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/playlists/ordering.py +0 -0
  153. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/playlists/scheduler.py +0 -0
  154. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/promos/__init__.py +0 -0
  155. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/promos/director.py +0 -0
  156. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/__init__.py +0 -0
  157. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/completion.py +0 -0
  158. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/ordering.py +0 -0
  159. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/poller.py +0 -0
  160. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/presence.py +0 -0
  161. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/race_poller.py +0 -0
  162. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/queue/shadow.py +0 -0
  163. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/__init__.py +0 -0
  164. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
  165. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_feedback.py +0 -0
  166. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  167. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
  168. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_moderation.py +0 -0
  169. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_motd.py +0 -0
  170. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
  171. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_promos.py +0 -0
  172. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_queue.py +0 -0
  173. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
  174. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/auth.py +0 -0
  175. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/catalog.py +0 -0
  176. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/devices.py +0 -0
  177. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/feedback.py +0 -0
  178. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/pages.py +0 -0
  179. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/public_api.py +0 -0
  180. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/queue.py +0 -0
  181. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/routes/user.py +0 -0
  182. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/static/css/main.css +0 -0
  183. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/admin-motd.js +0 -0
  184. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  185. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/admin-promos.js +0 -0
  186. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  187. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  188. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/admin.js +0 -0
  189. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/keybindings.js +0 -0
  190. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/static/js/main.js +0 -0
  191. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/admin/index.html +0 -0
  192. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/admin/motd.html +0 -0
  193. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
  194. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/admin/promos.html +0 -0
  195. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  196. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
  197. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/auth/login.html +0 -0
  198. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/base.html +0 -0
  199. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
  200. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/catalog/edit_item_modal.html +0 -0
  201. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  202. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  203. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/feedback/index.html +0 -0
  204. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/motd/channel_z.html +0 -0
  205. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/queue/index.html +0 -0
  206. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/race.html +0 -0
  207. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/user/dashboard.html +0 -0
  208. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/user/link_device.html +0 -0
  209. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/templates/user/my_list.html +0 -0
  210. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/ws/__init__.py +0 -0
  211. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/ws/handler.py +0 -0
  212. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/kryten_webqueue/ws/manager.py +0 -0
  213. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/scripts/AGENTS.md +0 -0
  214. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/scripts/patch_mediacms_bulk_facets.py +0 -0
  215. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/scripts/patch_mediacms_tags.py +0 -0
  216. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/scripts/trigger_enrich.py +0 -0
  217. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/__init__.py +0 -0
  218. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_catalog_blackout.py +0 -0
  219. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_catalog_delete.py +0 -0
  220. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_classify.py +0 -0
  221. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_config_persistence.py +0 -0
  222. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_device_linking.py +0 -0
  223. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_feedback.py +0 -0
  224. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_fetch_queue_pacing.py +0 -0
  225. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_fetchurls_sharepoint.py +0 -0
  226. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_identify_step.py +0 -0
  227. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_job_logs_and_queue_visibility.py +0 -0
  228. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_motd_publish.py +0 -0
  229. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_people_facets.py +0 -0
  230. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_phase1.py +0 -0
  231. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_phase2_jobs.py +0 -0
  232. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_phase3_jobs.py +0 -0
  233. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_phase4_live_fixes.py +0 -0
  234. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_playlist_import.py +0 -0
  235. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_presence_refund.py +0 -0
  236. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_promo_director.py +0 -0
  237. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_promo_pool_exclusion.py +0 -0
  238. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_providers.py +0 -0
  239. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_queue_announce.py +0 -0
  240. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_queue_preview_fallback.py +0 -0
  241. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_race_poller.py +0 -0
  242. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_recently_played_hide.py +0 -0
  243. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_save_results_to_playlist.py +0 -0
  244. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_schedule_lock.py +0 -0
  245. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_search_facets.py +0 -0
  246. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_title_normalization.py +0 -0
  247. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_tmdb_index.py +0 -0
  248. {kryten_webqueue-0.46.1 → kryten_webqueue-0.48.0}/tests/test_watchlist.py +0 -0
@@ -62,6 +62,10 @@ config-*.json
62
62
  *.db
63
63
  *.db-wal
64
64
  *.db-shm
65
+ *.sqlite3
66
+ *.sqlite3-wal
67
+ *.sqlite3-shm
68
+ data/
65
69
 
66
70
  # OS
67
71
  .DS_Store
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.48.0] - 2026-09-23
4
+
5
+ ### Added
6
+
7
+ - **PostgreSQL & Chandra-1 Podman Migration Foundations (`postgres-migration`)**:
8
+ - Added PostgreSQL backend support and configuration in `kryten_webqueue/config.py` with `PostgresConfig` and `DatabaseConfig(backend="sqlite"|"postgres")`.
9
+ - Added SQLAlchemy 2.0 async engine and session factory builder in `kryten_webqueue/catalog/db/engine.py` (`create_pg_engine`) utilizing `asyncpg` with connection pooling (`pool_size=10`, `max_overflow=20`, `pool_pre_ping=True`).
10
+ - Added schema DDL `kryten_webqueue/catalog/db/sql/001_initial_schema.sql` defining 40 tables across 5 logical schemas (`catalog`, `queue`, `jobs`, `users`, `tmdb`), English `tsvector` generated search vector, and `pg_trgm` fuzzy trigram matching.
11
+ - Implemented automated 30-day job run log pruning task (`job_log_prune_job`) in `kryten_webqueue/jobs/tasks.py` and `kryten_webqueue/catalog/db/_jobs_db.py` with strict exemptions for financial, chat, feedback, and audit history.
12
+ - Implemented one-shot ETL streaming script `kryten_webqueue/migrate_sqlite_to_pg.py` (`kryten-webqueue-migrate-pg`) with batched row streaming, identity sequence synchronization (`setval`), type normalization, and verification reporting.
13
+ - Added rootful Podman Quadlet specifications (`deploy/podman/webqueue/webqueue.network` and `webqueue-app.container`) and Containerfile for deployment on `chandra-1`.
14
+ - Added comprehensive operational cutover runbook `docs/postgres-cutover.md`.
15
+
16
+ ## [0.47.0] - 2026-09-23
17
+
18
+ ### Added
19
+
20
+ - **SQLite Domain Partitioning (`sqlite-domain-separation`)**:
21
+ - Split database architecture isolating heavy background write jobs from real-time pollers across 4 discrete SQLite databases:
22
+ - `catalog.sqlite3` — Catalog items, FTS5 index, categories, tags, people, studios, enrichment state, MOTD overrides.
23
+ - `queue.sqlite3` — Queue shadow, spend requests, queue history, saved playlists, schedules, play completions, blackouts.
24
+ - `jobs.sqlite3` — Background job runs, per-line job run logs, schedules, and fetch queue.
25
+ - `users.sqlite3` — OTPs, device link codes, device API keys, watchlists, feedback, title suggestions.
26
+ - Multi-database connection facade (`Database`) managing domain sub-connections (`self.catalog`, `self.queue`, `self.jobs`, `self.users`) with independent WAL, checkpoints, and busy timeouts.
27
+ - Configurable `DatabaseConfig` supporting `layout="monolith"` and `layout="partitioned"` with safe path resolution and guards against accidental silent bypass of existing monolith databases.
28
+ - Automated database migration ETL script `scripts/split_databases.py` (`kryten-webqueue-split-db`) with checkpoint-safe reading, foreign key verification, and deterministic SHA-256 data parity validation.
29
+ - Concurrency stress test suite (`tests/test_concurrency_split.py`) verifying zero cross-domain lock contention between high-frequency queue polling, burst job logging, and web browse requests.
30
+
31
+ ### Changed
32
+
33
+ - Decoupled cross-domain SQL queries:
34
+ - User watchlist ("My List") queries `users.sqlite3` for ordered tokens and hydrates item metadata from `catalog.sqlite3`.
35
+ - Recently-played hiding and weekend blackout exclusions resolved across domain boundaries without cross-file SQL joins.
36
+ - Promo-pool hide state purge orchestrated across `queue.sqlite3` and `catalog.sqlite3`.
37
+
3
38
  ## [0.46.1] - 2026-09-10
4
39
 
5
40
  ### Changed
@@ -0,0 +1,23 @@
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update \
6
+ && apt-get install -y --no-install-recommends build-essential ffmpeg curl \
7
+ && rm -rf /var/lib/apt/lists/* \
8
+ && useradd --system --create-home --uid 1001 kryten
9
+
10
+ COPY . /tmp/kryten-webqueue
11
+
12
+ RUN pip install --no-cache-dir --upgrade pip \
13
+ && pip install --no-cache-dir /tmp/kryten-webqueue
14
+
15
+ RUN mkdir -p /etc/kryten-webqueue /var/lib/kryten-webqueue /var/log/kryten-webqueue \
16
+ && chown -R kryten:kryten /etc/kryten-webqueue /var/lib/kryten-webqueue /var/log/kryten-webqueue
17
+
18
+ USER kryten
19
+
20
+ EXPOSE 2010
21
+
22
+ ENTRYPOINT ["kryten-webqueue"]
23
+ CMD ["--config", "/etc/kryten-webqueue/config.json"]
@@ -1,13 +1,15 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.46.1
3
+ Version: 0.48.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
7
7
  Requires-Python: >=3.12
8
8
  Requires-Dist: aiosqlite>=0.20
9
9
  Requires-Dist: apscheduler>=3.10
10
+ Requires-Dist: asyncpg>=0.29.0
10
11
  Requires-Dist: fastapi>=0.115
12
+ Requires-Dist: greenlet>=3.0
11
13
  Requires-Dist: httpx>=0.27
12
14
  Requires-Dist: jinja2>=3.1
13
15
  Requires-Dist: msal>=1.28
@@ -21,6 +23,7 @@ Requires-Dist: python-multipart>=0.0.9
21
23
  Requires-Dist: python-slugify>=8.0
22
24
  Requires-Dist: pyyaml>=6.0
23
25
  Requires-Dist: requests>=2.31
26
+ Requires-Dist: sqlalchemy>=2.0.30
24
27
  Requires-Dist: uvicorn[standard]>=0.30
25
28
  Requires-Dist: websockets>=12.0
26
29
  Requires-Dist: yt-dlp>=2024.1
@@ -62,6 +62,27 @@
62
62
  }
63
63
  },
64
64
 
65
+ "database": {
66
+ "backend": "sqlite",
67
+ "layout": "monolith",
68
+ "db_path": "/var/lib/kryten-webqueue/webqueue.db",
69
+ "data_dir": "/var/lib/kryten-webqueue/data",
70
+ "catalog_db_path": null,
71
+ "queue_db_path": null,
72
+ "jobs_db_path": null,
73
+ "users_db_path": null,
74
+ "postgres": {
75
+ "host": "host.containers.internal",
76
+ "port": 5432,
77
+ "user": "kryten",
78
+ "dbname": "webqueue",
79
+ "password_env": "KRYTEN_WEBQUEUE_PG_PASSWORD",
80
+ "dsn_env": null,
81
+ "dsn": null,
82
+ "pool_size": 10,
83
+ "max_overflow": 20
84
+ }
85
+ },
65
86
  "db_path": "/var/lib/kryten-webqueue/webqueue.db",
66
87
 
67
88
  "image_dir": "/var/lib/kryten-webqueue/images",
@@ -0,0 +1,24 @@
1
+ [Unit]
2
+ Description=kryten-webqueue - CyTube Queue Management (FastAPI app)
3
+ After=network-online.target
4
+
5
+ [Container]
6
+ ContainerName=webqueue-app
7
+ Image=localhost/kryten-webqueue:latest
8
+ Network=podman-webqueue
9
+ PublishPort=2010:2010
10
+ AddHost=host.containers.internal:host-gateway
11
+
12
+ EnvironmentFile=/etc/kryten/webqueue/webqueue.env
13
+ Volume=/etc/kryten/webqueue/config.json:/etc/kryten-webqueue/config.json:ro,z
14
+ Volume=/var/lib/kryten/media:/var/lib/kryten-webqueue:Z
15
+ Volume=/var/log/kryten/webqueue:/var/log/kryten-webqueue:Z
16
+
17
+ Exec=uvicorn kryten_webqueue.app:app --host 0.0.0.0 --port 2010
18
+
19
+ [Service]
20
+ Restart=always
21
+ RestartSec=5
22
+
23
+ [Install]
24
+ WantedBy=multi-user.target default.target
@@ -0,0 +1,3 @@
1
+ [Network]
2
+ NetworkName=podman-webqueue
3
+ Subnet=10.89.30.0/24
@@ -0,0 +1,206 @@
1
+ # Database Architecture & Evolution Plan: Kryten-WebQueue
2
+
3
+ **Status**: Approved Architecture Plan
4
+ **Target System**: `kryten-webqueue`
5
+ **Author**: Senior Microservice Architect (Kryten)
6
+ **Last Updated**: September 2026
7
+
8
+ ---
9
+
10
+ ## Executive Summary
11
+
12
+ `kryten-webqueue` has evolved from a lightweight catalog viewer into a multi-faceted web application hosting the CyTube channel catalog, real-time queue shadows, automated playlist schedulers, background media ingestion, TMDB enrichment pipelines, user watchlists, device API authentication, and per-line job logging.
13
+
14
+ Under heavy concurrent operation, the application suffers from SQLite database write-lock contention (`sqlite3.OperationalError: database is locked`) because all domains share a single SQLite database file and a single async worker connection.
15
+
16
+ This document outlines the **two-part architectural roadmap**:
17
+
18
+ 1. **Part 1 (Immediate / Stopgap)**: **SQLite Domain Partitioning** — Split the monolithic SQLite database into 4 logical database files (`catalog.db`, `queue.db`, `jobs.db`, `users.db`) to isolate high-throughput writers (job logs, enrichment syncs) from real-time pollers and public browse traffic. This removes cross-domain contention; it does not remove writer serialization within a single domain file.
19
+ 2. **Part 2 (Strategic / Chandra-1 Migration)**: **PostgreSQL on Chandra-1 via Podman** — Migrate all partitioned domains into one PostgreSQL database (`webqueue`) on `chandra-1` utilizing schemas (`catalog`, `queue`, `jobs`, `users`, `tmdb`), powered by **SQLAlchemy 2.0 (`asyncpg`)**, PostgreSQL Full-Text Search (`tsvector` + GIN) + `pg_trgm` fuzzy matching, and automated 30-day job log pruning (strictly exempting all financial, chat, and purchase records).
20
+
21
+ ```
22
+ ┌────────────────────────────────────────────────────────────────────────────────────────┐
23
+ │ TWO-PART ROADMAP │
24
+ ├────────────────────────────────────────┬───────────────────────────────────────────────┤
25
+ │ PART 1: SQLite Domain Partitioning │ PART 2: Chandra-1 PostgreSQL & Podman Pod │
26
+ ├────────────────────────────────────────┼───────────────────────────────────────────────┤
27
+ │ • Split single DB into 4 files: │ • Single Postgres database `webqueue` on │
28
+ │ - catalog.db (MediaCMS mirror, art) │ chandra-1 with logical schemas. │
29
+ │ - queue.db (shadow, schedules, play) │ • SQLAlchemy 2.0 (asyncpg) connection pool. │
30
+ │ - jobs.db (job runs, logs, fetch) │ • English `tsvector` + GIN + `pg_trgm` search.│
31
+ │ - users.db (OTPs, devices, list) │ • 30-day job run log pruning policy (strictly │
32
+ │ • Decouple cross-table JOINs in Python │ exempting economy, spend, and chat data). │
33
+ │ • Zero lock contention between batch │ • Podman container deployment on chandra-1 │
34
+ │ jobs and real-time playback pollers. │ with shared media asset volumes. │
35
+ └────────────────────────────────────────┴───────────────────────────────────────────────┘
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Part 1: SQLite Domain Partitioning & Lock Elimination
41
+
42
+ ### 1.1 Root Cause of Current Contention
43
+
44
+ 1. **Single Writer Serialization**: In SQLite WAL mode, any number of readers can proceed concurrently, but only one write transaction can execute at a time per database file.
45
+ 2. **Single Connection Dispatcher**: `_connection.py` instantiates a single `aiosqlite.Connection`. In `aiosqlite`, all calls queue onto a single background OS thread, serializing even concurrent read operations.
46
+ 3. **Collision of Asynchronous Workloads**:
47
+ - **Heavy Writers**: `catalog_enrich` (updating thousands of item records), `catalog_sync`, `fetch_queue_drain` (yt-dlp ingestion), and `log_capture.py` (bulk inserting hundreds of log rows per job run).
48
+ - **Real-Time Pollers & API**: `StatePoller` (every 2–5s), `RacePoller`, `PlaylistScheduler`, `CompletionRecorder`, `PresenceRefundMonitor`, and device API key authentication (updating `last_used_at` per HTTP request).
49
+ - When a batch job holds the WAL write lock or issues rapid write transactions, real-time pollers and web requests exceed the 5000ms `busy_timeout` and crash with `database is locked`.
50
+
51
+ ### 1.2 Domain Partitioning Strategy
52
+
53
+ We partition tables into 4 dedicated SQLite databases:
54
+
55
+ ```
56
+ ┌──────────────────────────┐ ┌──────────────────────────┐
57
+ │ catalog.db │ │ queue.db │
58
+ ├──────────────────────────┤ ├──────────────────────────┤
59
+ │ • catalog │ │ • queue_shadow │
60
+ │ • catalog_fts (FTS5) │ │ • spend_requests │
61
+ │ • categories │ │ • queue_history │
62
+ │ • catalog_categories │ │ • saved_playlists │
63
+ │ • tags │ │ • saved_playlist_items │
64
+ │ • catalog_tags │ │ • playlist_schedules │
65
+ │ • people │ │ • active_schedule │
66
+ │ • catalog_people │ │ • play_completions │
67
+ │ • studios │ │ • playlist_item_played │
68
+ │ • catalog_studios │ │ • catalog_blackouts │
69
+ │ • item_enrichment_state │ └──────────────────────────┘
70
+ │ • item_edit_log │
71
+ │ • sync_log │ ┌──────────────────────────┐
72
+ │ • motd_overrides │ │ users.db │
73
+ └──────────────────────────┘ ├──────────────────────────┤
74
+ │ • user_watchlist │
75
+ ┌──────────────────────────┐ │ • otps │
76
+ │ jobs.db │ │ • device_link_codes │
77
+ ├──────────────────────────┤ │ • device_api_keys │
78
+ │ • job_runs │ │ • feedback │
79
+ │ • job_run_logs │ │ • title_suggestions │
80
+ │ • job_schedules │ └──────────────────────────┘
81
+ │ • fetch_queue │
82
+ └──────────────────────────┘
83
+ ```
84
+
85
+ ### 1.3 Cross-Domain Query Decoupling
86
+
87
+ Separating SQLite database files removes the ability to perform single SQL `JOIN`s across tables in different files. We resolve the existing cross-table queries in the application layer:
88
+
89
+ 1. **Recently-Played Catalog Suppression Filter**:
90
+ - *Previous*: `catalog` query joined `play_completions` and `playlist_item_played`.
91
+ - *Partitioned*: `queue.db` executes `get_active_hidden_media_ids() -> set[str]`. The browse query in `catalog.db` filters with `WHERE friendly_token NOT IN (...)`.
92
+ 2. **User Watchlist ("My List")**:
93
+ - *Previous*: `user_watchlist JOIN catalog`.
94
+ - *Partitioned*: Query `users.db` for `friendly_tokens` for the user, then query `catalog.db` with `get_catalog_items_by_tokens(tokens)`.
95
+ 3. **Title Suggestions Catalog Match**:
96
+ - *Previous*: `title_suggestions` lookup checking if `catalog_token` exists.
97
+ - *Partitioned*: Check `catalog.db` by token/title independently during triage or submission.
98
+ 4. **Job Run Logs & Job History**:
99
+ - `JobManager` writes exclusively to `jobs.db`. Background enrichment tasks write their metadata updates to `catalog.db` without locking the job log tables.
100
+
101
+ ### 1.4 Partitioning Limits and Safety
102
+
103
+ Partitioning removes only cross-domain SQLite contention. `catalog` still contains browse reads
104
+ and enrichment writes, while `queue` still contains poller, scheduler, and queue-history writes.
105
+ Each domain therefore uses short, bounded transactions, retry/latency metrics, and domain-scoped
106
+ concurrency tests. A `busy_timeout` is a wait budget, not a correctness mechanism.
107
+
108
+ The layout is selected explicitly as `monolith` or `partitioned`. Startup never chooses a new
109
+ default data directory when a legacy database exists, and migration history is never copied from
110
+ the monolith: each partition has a fresh baseline schema version after ETL validation.
111
+
112
+ ### 1.5 Part 1 Implementation Artifacts
113
+
114
+ - **Sprint Specs**: Located in `docs/sqlite-domain-separation/`
115
+ - `PRD-sqlite-domain-separation.md`
116
+ - `SPEC-Sortie-1-schema-partitioning-and-config.md`
117
+ - `SPEC-Sortie-2-multi-db-connection-layer.md`
118
+ - `SPEC-Sortie-3-cross-domain-query-decoupling.md`
119
+ - `SPEC-Sortie-4-etl-split-script-and-validation.md`
120
+
121
+ ---
122
+
123
+ ## Part 2: Chandra-1 PostgreSQL & Podman Migration
124
+
125
+ ### 2.1 Target Architecture
126
+
127
+ Upon completion of Part 1, the partitioned domain model directly maps to PostgreSQL. Instead of maintaining multiple SQLite files, the application connects to a **single PostgreSQL database (`webqueue`)** on `chandra-1`, structured with logical schemas:
128
+
129
+ - `catalog.*` — MediaCMS catalog mirror, cast/crew, tags, categories, enrichment state, MOTD overrides.
130
+ - `queue.*` — Queue shadow, spend requests, saved playlists, schedules, play completions, blackouts.
131
+ - `jobs.*` — Job schedules, job runs, per-line job logs, fetch queue.
132
+ - `users.*` — OTP authentication, device link codes & API keys, user watchlists, feedback, title suggestions.
133
+ - `tmdb.*` — Persistent, queryable local TMDB dump index.
134
+
135
+ ```
136
+ ┌────────────────────────────────────────────────────────────────────────────────────────┐
137
+ │ chandra-1 Host System │
138
+ │ │
139
+ │ ┌──────────────────────────────────────────────────────────────────────────────────┐ │
140
+ │ │ Podman Pod: kryten-webqueue │ │
141
+ │ │ │ │
142
+ │ │ ┌─────────────────────────────┐ ┌──────────────────────────────────┐ │ │
143
+ │ │ │ kryten-webqueue-app │ │ nginx proxy │ │ │
144
+ │ │ │ (FastAPI + uvicorn) │◄─────────►│ (Reverse Proxy + SSL) │ │ │
145
+ │ │ │ SQLAlchemy 2.0 (asyncpg) │ │ │ │ │
146
+ │ │ └──────────────┬──────────────┘ └────────────────┬─────────────────┘ │ │
147
+ │ └─────────────────┼───────────────────────────────────────────┼────────────────────┘ │
148
+ │ │ │ │
149
+ │ ▼ ▼ │
150
+ │ ┌─────────────────────────────────────┐ ┌──────────────────────────────────────┐ │
151
+ │ │ PostgreSQL on chandra-1 │ │ Shared Media Volume │ │
152
+ │ │ Database: webqueue │ │ /var/lib/kryten/media │ │
153
+ │ │ Schemas: catalog, queue, │ │ - cover art / posters │ │
154
+ │ │ jobs, users, tmdb │ │ - TMDB dump files │ │
155
+ │ └─────────────────────────────────────┘ └──────────────────────────────────────┘ │
156
+ └────────────────────────────────────────────────────────────────────────────────────────┘
157
+ ```
158
+
159
+ ### 2.2 Core Technical Specifications
160
+
161
+ 1. **ORM & Driver**: **SQLAlchemy 2.0 with `asyncpg`** (`create_async_engine`, `async_sessionmaker`, `AsyncSession`).
162
+ - Retains clean async repository layer with type-safe query construction and parameterized execution.
163
+ - Automatic connection pooling (`pool_size=10, max_overflow=20, pool_pre_ping=True`).
164
+ 2. **Search Engine**:
165
+ - **Full-Text Search**: English `tsvector` generated column + `GIN` index on `catalog.title` and `catalog.description`.
166
+ - **Typo-Tolerant Matching**: `pg_trgm` extension with GIN/GiST index on `catalog.title` for fuzzy `similarity()` matching (e.g., matching "terminatr" to "The Terminator").
167
+ - Eliminates all SQLite FTS5 query-sanitization crashes.
168
+ 3. **Automated Log Pruning Policy**:
169
+ - A scheduled background job cleans up rows in `jobs.job_run_logs` older than **30 days**.
170
+ - **CRITICAL COMPLIANCE CONSTRAINT**: This pruning policy applies **EXCLUSIVELY** to `jobs.job_run_logs`. It must **NEVER** touch or prune:
171
+ - Chat logs or user communication records
172
+ - Economy or z-coin purchase history (`spend_requests`, `queue_history`)
173
+ - User feedback or moderation audit logs (`item_edit_log`, `feedback`)
174
+ 4. **ETL & Data Migration**:
175
+ - Streaming migration script (`migrate_sqlite_to_pg.py`) reading from the 4 SQLite databases and writing into PostgreSQL with foreign key verification, sequence resets, and JSONB conversion.
176
+
177
+ ### 2.3 Cutover Contract
178
+
179
+ WebQueue runs as a rootful Quadlet application container on `chandra-1`; PostgreSQL is
180
+ host-managed on the same host and reached through an explicit container host gateway, never
181
+ container-localhost. The PostgreSQL cutover uses a maintenance window and forward-fix recovery:
182
+ the SQLite snapshot remains read-only for one release, but it is not an automatic rollback target
183
+ after PostgreSQL accepts writes.
184
+
185
+ ### 2.4 Part 2 Implementation Artifacts
186
+
187
+ - **Sprint Specs**: Located in `docs/postgres-migration/`
188
+ - `PRD-postgres-migration.md`
189
+ - `SPEC-Sortie-1-db-config-and-pool.md`
190
+ - `SPEC-Sortie-2-connection-layer-port.md`
191
+ - `SPEC-Sortie-3-fts5-to-tsvector.md`
192
+ - `SPEC-Sortie-4-tmdb-index-database.md`
193
+ - `SPEC-Sortie-5-etl-migration.md`
194
+ - `SPEC-Sortie-6-tests-cutover-release.md`
195
+
196
+ ---
197
+
198
+ ## Sequencing & Next Steps
199
+
200
+ 1. **Phase 1 Execution (Immediate)**:
201
+ - Implement Part 1 (SQLite Domain Partitioning) to resolve production write-lock crashes immediately without requiring infrastructure migration on chandra-1.
202
+ - Run `split_databases.py` in staging/production to migrate single SQLite DB to `catalog.sqlite3`, `queue.sqlite3`, `jobs.sqlite3`, `users.sqlite3`.
203
+ 2. **Phase 2 Execution (Subsequent)**:
204
+ - Bootstrap PostgreSQL `webqueue` database on `chandra-1`.
205
+ - Deploy Podman container on `chandra-1` with SQLAlchemy 2.0 + asyncpg.
206
+ - Execute ETL migration and switch production traffic.
@@ -0,0 +1,94 @@
1
+ # PostgreSQL Cutover Runbook: Kryten-WebQueue
2
+
3
+ **Target Version**: `0.48.0`
4
+ **Host Target**: `chandra-1` (PostgreSQL 16 + Podman Quadlet)
5
+ **Source Host**: `grindhouse.local` (pipx systemd / SQLite partitioned data)
6
+ **Parent Plan**: [DATABASE_ARCHITECTURE_PLAN.md](DATABASE_ARCHITECTURE_PLAN.md)
7
+ **PRD**: [postgres-migration/PRD-postgres-migration.md](postgres-migration/PRD-postgres-migration.md)
8
+
9
+ ---
10
+
11
+ ## 1. Pre-Flight Checklist
12
+
13
+ - [ ] `chandra-1` has PostgreSQL 16+ active and `pg_trgm` extension installed.
14
+ - [ ] Database `webqueue` exists with owner `kryten`.
15
+ - [ ] Schema `001_initial_schema.sql` applied cleanly across `catalog`, `queue`, `jobs`, `users`, `tmdb`.
16
+ - [ ] Privileges granted to `kryten`:
17
+ ```sql
18
+ GRANT USAGE, CREATE ON SCHEMA catalog, queue, jobs, users, tmdb TO kryten;
19
+ GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA catalog, queue, jobs, users, tmdb, public TO kryten;
20
+ GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA catalog, queue, jobs, users, tmdb, public TO kryten;
21
+ ALTER DEFAULT PRIVILEGES IN SCHEMA catalog, queue, jobs, users, tmdb GRANT ALL ON TABLES TO kryten;
22
+ ALTER DEFAULT PRIVILEGES IN SCHEMA catalog, queue, jobs, users, tmdb GRANT ALL ON SEQUENCES TO kryten;
23
+ ```
24
+ - [ ] Podman image built on `chandra-1`:
25
+ ```bash
26
+ sudo podman build -f Containerfile -t localhost/kryten-webqueue:latest .
27
+ ```
28
+ - [ ] Environment file `/etc/kryten/webqueue/webqueue.env` created on `chandra-1`:
29
+ ```env
30
+ KRYTEN_WEBQUEUE_PG_PASSWORD=kryten_secret_password
31
+ ```
32
+
33
+ ---
34
+
35
+ ## 2. Maintenance Window Cutover (Step-by-Step)
36
+
37
+ ### Step 1: Stop Source Service & Freeze Writes
38
+ On `grindhouse.local`:
39
+ ```bash
40
+ sudo systemctl stop kryten-webqueue.service
41
+ sudo systemctl status kryten-webqueue.service --no-pager
42
+ ```
43
+
44
+ ### Step 2: Checkpoint SQLite WAL & Create Final Snapshot
45
+ On `grindhouse.local`:
46
+ ```bash
47
+ SNAPSHOT_DIR="/var/lib/kryten-webqueue/backups/cutover_$(date +%Y%m%d_%H%M%S)"
48
+ sudo mkdir -p "$SNAPSHOT_DIR"
49
+
50
+ for db in catalog queue jobs users; do
51
+ sudo sqlite3 "/var/lib/kryten-webqueue/data/$db.sqlite3" "PRAGMA wal_checkpoint(TRUNCATE);"
52
+ done
53
+
54
+ sudo cp -av /var/lib/kryten-webqueue/data "$SNAPSHOT_DIR/"
55
+ sudo cp -av /etc/kryten-webqueue/config.json "$SNAPSHOT_DIR/"
56
+ ```
57
+
58
+ ### Step 3: Stream SQLite Data to PostgreSQL on Chandra-1
59
+ Run the ETL migration script with `--verify`:
60
+ ```bash
61
+ sudo -u kryten /home/kryten/.local/pipx/venvs/kryten-webqueue/bin/python -m kryten_webqueue.migrate_sqlite_to_pg \
62
+ --data-dir /var/lib/kryten-webqueue/data \
63
+ --pg-dsn "postgresql://kryten:kryten_secret_password@chandra-1.local:5432/webqueue"
64
+ ```
65
+ Verify `postgres_migration_report.json` indicates `"status": "success"` and 0 errors.
66
+
67
+ ### Step 4: Transfer Media & Image Assets to Chandra-1
68
+ Sync cover art images to `/var/lib/kryten/media`:
69
+ ```bash
70
+ rsync -avz /var/lib/kryten-webqueue/images/ groberts@chandra-1.local:/var/lib/kryten/media/images/
71
+ ```
72
+
73
+ ### Step 5: Start Podman Quadlet on Chandra-1
74
+ On `chandra-1`:
75
+ ```bash
76
+ sudo cp deploy/podman/webqueue/webqueue.network /etc/containers/systemd/
77
+ sudo cp deploy/podman/webqueue/webqueue-app.container /etc/containers/systemd/
78
+ sudo systemctl daemon-reload
79
+ sudo systemctl start webqueue-app.service
80
+ sudo systemctl status webqueue-app.service --no-pager
81
+ ```
82
+
83
+ ### Step 6: Smoke Testing & Health Verification
84
+ ```bash
85
+ curl -I http://127.0.0.1:2010/auth/login
86
+ curl -s http://127.0.0.1:2010/queue/next-schedule
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 3. Rollback & Forward Repair Policy
92
+
93
+ - **Pre-Cutover Failure**: If ETL validation fails or container startup fails before DNS/traffic cutover, immediately restart `kryten-webqueue.service` on `grindhouse.local`. SQLite databases remain untouched and consistent.
94
+ - **Post-Cutover Failure**: Once traffic is switched and PostgreSQL accepts writes (new spend requests, logins, completions), recovery is a **forward repair in PostgreSQL**. The retained SQLite snapshot is preserved read-only for forensics.