kryten-webqueue 0.46.0__tar.gz → 0.47.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 (239) hide show
  1. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/.gitignore +4 -0
  2. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/CHANGELOG.md +36 -0
  3. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/PKG-INFO +1 -1
  4. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/config.example.json +9 -0
  5. kryten_webqueue-0.47.0/docs/DATABASE_ARCHITECTURE_PLAN.md +206 -0
  6. kryten_webqueue-0.47.0/docs/postgres-migration/PRD-postgres-migration.md +203 -0
  7. kryten_webqueue-0.47.0/docs/postgres-migration/SPEC-Sortie-1-db-config-and-pool.md +90 -0
  8. kryten_webqueue-0.47.0/docs/postgres-migration/SPEC-Sortie-2-connection-layer-port.md +137 -0
  9. kryten_webqueue-0.47.0/docs/postgres-migration/SPEC-Sortie-3-fts5-to-tsvector.md +113 -0
  10. kryten_webqueue-0.47.0/docs/postgres-migration/SPEC-Sortie-4-tmdb-index-database.md +96 -0
  11. kryten_webqueue-0.47.0/docs/postgres-migration/SPEC-Sortie-5-etl-migration.md +69 -0
  12. kryten_webqueue-0.47.0/docs/postgres-migration/SPEC-Sortie-6-tests-cutover-release.md +93 -0
  13. kryten_webqueue-0.47.0/docs/sqlite-domain-separation/PRD-sqlite-domain-separation.md +144 -0
  14. kryten_webqueue-0.47.0/docs/sqlite-domain-separation/SPEC-Sortie-1-schema-partitioning-and-config.md +113 -0
  15. kryten_webqueue-0.47.0/docs/sqlite-domain-separation/SPEC-Sortie-2-multi-db-connection-layer.md +106 -0
  16. kryten_webqueue-0.47.0/docs/sqlite-domain-separation/SPEC-Sortie-3-cross-domain-query-decoupling.md +104 -0
  17. kryten_webqueue-0.47.0/docs/sqlite-domain-separation/SPEC-Sortie-4-etl-split-script-and-validation.md +51 -0
  18. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/app.py +1 -1
  19. kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/__init__.py +668 -0
  20. kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/_base_domain.py +138 -0
  21. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_catalog.py +165 -68
  22. kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/_catalog_db.py +57 -0
  23. kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/_jobs_db.py +123 -0
  24. kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/_queue_db.py +198 -0
  25. kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/_users_db.py +53 -0
  26. kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/schemas/__init__.py +13 -0
  27. kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/schemas/catalog_schema.py +148 -0
  28. kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/schemas/jobs_schema.py +67 -0
  29. kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/schemas/queue_schema.py +136 -0
  30. kryten_webqueue-0.47.0/kryten_webqueue/catalog/db/schemas/users_schema.py +78 -0
  31. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/config.py +81 -1
  32. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/ytpipe/downloader.py +1 -3
  33. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/motd/builder.py +27 -23
  34. kryten_webqueue-0.47.0/kryten_webqueue/scripts/__init__.py +1 -0
  35. kryten_webqueue-0.47.0/kryten_webqueue/scripts/split_databases.py +14 -0
  36. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/pyproject.toml +2 -1
  37. kryten_webqueue-0.47.0/scripts/split_databases.py +379 -0
  38. kryten_webqueue-0.47.0/tests/test_concurrency_split.py +294 -0
  39. kryten_webqueue-0.47.0/tests/test_database_config_and_schemas.py +163 -0
  40. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_motd_publish.py +13 -10
  41. kryten_webqueue-0.47.0/tests/test_multi_db_connection.py +197 -0
  42. kryten_webqueue-0.46.0/kryten_webqueue/catalog/db/__init__.py +0 -42
  43. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/.github/workflows/gitleaks.yml +0 -0
  44. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/.github/workflows/python-publish.yml +0 -0
  45. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/.github/workflows/release.yml +0 -0
  46. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/.gitleaks.toml +0 -0
  47. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/.pre-commit-config.yaml +0 -0
  48. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/AGENT-WORKFLOW-GUIDE.md +0 -0
  49. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/AGENTS.md +0 -0
  50. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/README.md +0 -0
  51. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/deploy/kryten-webqueue.service +0 -0
  52. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/deploy/nginx-queue.conf +0 -0
  53. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
  54. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/IMPL_API_GATE.md +0 -0
  55. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/IMPL_ECONOMY.md +0 -0
  56. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/IMPL_KRYTEN_PY.md +0 -0
  57. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/IMPL_ROBOT.md +0 -0
  58. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  59. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  60. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/Log for incorrect promo insertion problem.txt +0 -0
  61. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  62. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/PRD_TMDB_LOCAL_INDEX.md +0 -0
  63. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/PRE_PLAN_GAPS.md +0 -0
  64. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/PRODUCT_PLAN.md +0 -0
  65. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/PUBLIC_API.md +0 -0
  66. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/SPEC-Visual-UX-Sprint-August-2026.md +0 -0
  67. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/SPEC_ADMIN_ITEM_EDITOR.md +0 -0
  68. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
  69. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/SPEC_EMOTE_REHOST.md +0 -0
  70. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  71. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/SPEC_TMDB_LOCAL_INDEX.md +0 -0
  72. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/docs/UX_POLISH_PLAN.md +0 -0
  73. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/__init__.py +0 -0
  74. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/__main__.py +0 -0
  75. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/api_gate/__init__.py +0 -0
  76. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/api_gate/client.py +0 -0
  77. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/auth/__init__.py +0 -0
  78. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/auth/device_keys.py +0 -0
  79. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/auth/otp.py +0 -0
  80. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/auth/rate_limit.py +0 -0
  81. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/auth/session.py +0 -0
  82. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/__init__.py +0 -0
  83. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_blackouts.py +0 -0
  84. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_connection.py +0 -0
  85. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_devices.py +0 -0
  86. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
  87. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  88. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_fetch_queue.py +0 -0
  89. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_motd.py +0 -0
  90. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_people.py +0 -0
  91. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_playlists.py +0 -0
  92. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_queue.py +0 -0
  93. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
  94. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
  95. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
  96. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
  97. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
  98. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
  99. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/report.py +0 -0
  100. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
  101. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
  102. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
  103. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/identify.py +0 -0
  104. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
  105. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
  106. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
  107. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
  108. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/images.py +0 -0
  109. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/mediacms.py +0 -0
  110. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/sync.py +0 -0
  111. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/__init__.py +0 -0
  112. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/_schema.py +0 -0
  113. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/_textmatch.py +0 -0
  114. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/_ttscrape.py +0 -0
  115. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/builder.py +0 -0
  116. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/coverage.py +0 -0
  117. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/catalog/tmdb_index/index.py +0 -0
  118. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/__init__.py +0 -0
  119. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  120. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  121. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/blackout_scan.py +0 -0
  122. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  123. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  124. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  125. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  126. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  127. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  128. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  129. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/__init__.py +0 -0
  130. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  131. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  132. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/log_capture.py +0 -0
  133. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/manager.py +0 -0
  134. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/motd_publish.py +0 -0
  135. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  136. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/jobs/tasks.py +0 -0
  137. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/logging_config.py +0 -0
  138. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/motd/__init__.py +0 -0
  139. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/motd/render.py +0 -0
  140. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/playlists/__init__.py +0 -0
  141. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
  142. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/playlists/fire.py +0 -0
  143. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/playlists/importer.py +0 -0
  144. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/playlists/ordering.py +0 -0
  145. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/playlists/scheduler.py +0 -0
  146. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/promos/__init__.py +0 -0
  147. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/promos/director.py +0 -0
  148. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/__init__.py +0 -0
  149. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/completion.py +0 -0
  150. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/ordering.py +0 -0
  151. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/poller.py +0 -0
  152. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/presence.py +0 -0
  153. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/race_poller.py +0 -0
  154. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/queue/shadow.py +0 -0
  155. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/__init__.py +0 -0
  156. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
  157. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_feedback.py +0 -0
  158. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  159. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
  160. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_moderation.py +0 -0
  161. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_motd.py +0 -0
  162. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
  163. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_promos.py +0 -0
  164. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_queue.py +0 -0
  165. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
  166. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/auth.py +0 -0
  167. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/catalog.py +0 -0
  168. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/devices.py +0 -0
  169. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/feedback.py +0 -0
  170. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/pages.py +0 -0
  171. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/public_api.py +0 -0
  172. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/queue.py +0 -0
  173. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/routes/user.py +0 -0
  174. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/css/main.css +0 -0
  175. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/admin-motd.js +0 -0
  176. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  177. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/admin-promos.js +0 -0
  178. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  179. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  180. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/admin.js +0 -0
  181. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/keybindings.js +0 -0
  182. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/static/js/main.js +0 -0
  183. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/admin/index.html +0 -0
  184. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/admin/motd.html +0 -0
  185. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
  186. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/admin/promos.html +0 -0
  187. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  188. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
  189. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/auth/login.html +0 -0
  190. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/base.html +0 -0
  191. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
  192. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/catalog/edit_item_modal.html +0 -0
  193. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  194. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  195. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/feedback/index.html +0 -0
  196. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/motd/channel_z.html +0 -0
  197. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/queue/index.html +0 -0
  198. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/race.html +0 -0
  199. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/user/dashboard.html +0 -0
  200. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/user/link_device.html +0 -0
  201. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/templates/user/my_list.html +0 -0
  202. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/ws/__init__.py +0 -0
  203. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/ws/handler.py +0 -0
  204. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/kryten_webqueue/ws/manager.py +0 -0
  205. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/scripts/AGENTS.md +0 -0
  206. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/scripts/patch_mediacms_bulk_facets.py +0 -0
  207. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/scripts/patch_mediacms_tags.py +0 -0
  208. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/scripts/trigger_enrich.py +0 -0
  209. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/__init__.py +0 -0
  210. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_catalog_blackout.py +0 -0
  211. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_catalog_delete.py +0 -0
  212. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_classify.py +0 -0
  213. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_config_persistence.py +0 -0
  214. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_device_linking.py +0 -0
  215. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_feedback.py +0 -0
  216. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_fetch_queue_pacing.py +0 -0
  217. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_fetchurls_sharepoint.py +0 -0
  218. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_identify_step.py +0 -0
  219. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_job_logs_and_queue_visibility.py +0 -0
  220. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_people_facets.py +0 -0
  221. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_phase1.py +0 -0
  222. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_phase2_jobs.py +0 -0
  223. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_phase3_jobs.py +0 -0
  224. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_phase4_live_fixes.py +0 -0
  225. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_playlist_import.py +0 -0
  226. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_presence_refund.py +0 -0
  227. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_promo_director.py +0 -0
  228. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_promo_pool_exclusion.py +0 -0
  229. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_providers.py +0 -0
  230. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_queue_announce.py +0 -0
  231. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_queue_preview_fallback.py +0 -0
  232. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_race_poller.py +0 -0
  233. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_recently_played_hide.py +0 -0
  234. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_save_results_to_playlist.py +0 -0
  235. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_schedule_lock.py +0 -0
  236. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_search_facets.py +0 -0
  237. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_title_normalization.py +0 -0
  238. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.0}/tests/test_tmdb_index.py +0 -0
  239. {kryten_webqueue-0.46.0 → kryten_webqueue-0.47.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.47.0] - 2026-09-23
4
+
5
+ ### Added
6
+
7
+ - **SQLite Domain Partitioning (`sqlite-domain-separation`)**:
8
+ - Split database architecture isolating heavy background write jobs from real-time pollers across 4 discrete SQLite databases:
9
+ - `catalog.sqlite3` — Catalog items, FTS5 index, categories, tags, people, studios, enrichment state, MOTD overrides.
10
+ - `queue.sqlite3` — Queue shadow, spend requests, queue history, saved playlists, schedules, play completions, blackouts.
11
+ - `jobs.sqlite3` — Background job runs, per-line job run logs, schedules, and fetch queue.
12
+ - `users.sqlite3` — OTPs, device link codes, device API keys, watchlists, feedback, title suggestions.
13
+ - Multi-database connection facade (`Database`) managing domain sub-connections (`self.catalog`, `self.queue`, `self.jobs`, `self.users`) with independent WAL, checkpoints, and busy timeouts.
14
+ - Configurable `DatabaseConfig` supporting `layout="monolith"` and `layout="partitioned"` with safe path resolution and guards against accidental silent bypass of existing monolith databases.
15
+ - 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.
16
+ - 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.
17
+
18
+ ### Changed
19
+
20
+ - Decoupled cross-domain SQL queries:
21
+ - User watchlist ("My List") queries `users.sqlite3` for ordered tokens and hydrates item metadata from `catalog.sqlite3`.
22
+ - Recently-played hiding and weekend blackout exclusions resolved across domain boundaries without cross-file SQL joins.
23
+ - Promo-pool hide state purge orchestrated across `queue.sqlite3` and `catalog.sqlite3`.
24
+
25
+ ## [0.46.1] - 2026-09-10
26
+
27
+ ### Changed
28
+
29
+ - **The grid is an even split again** (6/6 for a 12-slot Friday+Saturday
30
+ weekend). The 5/7 shape 0.46.0 inferred from the live MOTD was an artifact of
31
+ a markup bug: with the `<a>` tags left unclosed, the blank line after the
32
+ fifth poster looks like a night break but isn't, and most screen widths hide
33
+ the resulting reflow. The generated snippet closes its tags, so it renders the
34
+ intended 6/6.
35
+ - A night with more scheduled titles than grid positions now records a warning
36
+ (job result, admin panel, and log) instead of silently dropping the extras.
37
+
3
38
  ## [0.46.0] - 2026-09-10
4
39
 
5
40
  ### Changed
@@ -15,6 +50,7 @@
15
50
  night is never truncated, so an over-full weekend shows every scheduled movie;
16
51
  `motd.slots` is a target the grid is only padded *up* to when the weekend is
17
52
  still thin.
53
+ (Reverted in 0.46.1 — the 5/7 reading was a markup artifact.)
18
54
  - **Sunday is excluded.** New `motd.nights` (default `[1, 2]`) controls which
19
55
  nights the grid covers; Sunday has no schedule yet, so its workbook titles are
20
56
  parsed but ignored. Add `3` once the scheduled-playlists feature fills that
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.46.0
3
+ Version: 0.47.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
@@ -62,6 +62,15 @@
62
62
  }
63
63
  },
64
64
 
65
+ "database": {
66
+ "layout": "monolith",
67
+ "db_path": "/var/lib/kryten-webqueue/webqueue.db",
68
+ "data_dir": "/var/lib/kryten-webqueue/data",
69
+ "catalog_db_path": null,
70
+ "queue_db_path": null,
71
+ "jobs_db_path": null,
72
+ "users_db_path": null
73
+ },
65
74
  "db_path": "/var/lib/kryten-webqueue/webqueue.db",
66
75
 
67
76
  "image_dir": "/var/lib/kryten-webqueue/images",
@@ -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,203 @@
1
+ # PRD: PostgreSQL & Chandra-1 Podman Migration
2
+
3
+ **Sprint**: `postgres-migration`
4
+ **Status**: Planned - Post SQLite Domain Separation
5
+ **Builds on**: `sqlite-domain-separation`
6
+ **Target version**: `0.48.0`
7
+ **Workflow**: [../../AGENT-WORKFLOW-GUIDE.md](../../AGENT-WORKFLOW-GUIDE.md)
8
+ **Parent Plan**: [../DATABASE_ARCHITECTURE_PLAN.md](../DATABASE_ARCHITECTURE_PLAN.md)
9
+
10
+ ---
11
+
12
+ ## 1. Executive Summary
13
+
14
+ Following SQLite domain separation, `kryten-webqueue` will move its application data to one
15
+ PostgreSQL 16+ database, `webqueue`, on `chandra-1`. The database has five logical schemas:
16
+ `catalog`, `queue`, `jobs`, `users`, and `tmdb`. The application will use SQLAlchemy 2.0 async
17
+ sessions backed by `asyncpg`, with PostgreSQL full-text search and trigram matching replacing
18
+ SQLite FTS5.
19
+
20
+ The cutover is a planned maintenance-window migration. SQLite remains an immutable recovery
21
+ snapshot for one release, but production writes accepted after cutover are repaired forward in
22
+ PostgreSQL; the system does not silently fall back to a divergent SQLite copy.
23
+
24
+ ## 2. Decisions and Boundaries
25
+
26
+ The following decisions are fixed for this sprint:
27
+
28
+ - One PostgreSQL database named `webqueue` owns all five schemas. `tmdb` is a schema, not a
29
+ separate database, so catalog enrichment can use relational queries when needed.
30
+ - The application runs as a rootful Podman Quadlet service on `chandra-1`.
31
+ - PostgreSQL is host-managed on `chandra-1`. The rootful application container connects through
32
+ an explicitly configured host-gateway address; it must not use `localhost`.
33
+ - The migration uses a maintenance window and a forward-fix rollback policy. There is no
34
+ dual-write phase and no automatic return to SQLite after PostgreSQL accepts writes.
35
+ - Existing api-gate, MediaCMS, and CyTube integration contracts are unchanged. This sprint does
36
+ not add direct service-to-service communication paths.
37
+
38
+ ## 3. Goals and Success Metrics
39
+
40
+ - Preserve all application records and identity values during migration without external
41
+ TMDB/OMDB calls.
42
+ - Remove SQLite writer starvation between independent workloads after the SQLite partitioning
43
+ release, then move to PostgreSQL MVCC and pooled connections.
44
+ - Preserve public and administrative response shapes, queue semantics, OTP single-use behavior,
45
+ device-key behavior, and audit history.
46
+ - Provide typo-tolerant search while removing FTS5 parser failures.
47
+ - Complete staging rehearsal and production cutover with source/destination parity, foreign-key
48
+ validation, application smoke tests, and no unplanned data loss.
49
+
50
+ Success is measured by zero unhandled database errors during the concurrency test suite, no
51
+ migration verification mismatch, successful health/browse/search/queue smoke tests, and a
52
+ recorded cutover checkpoint that can be restored for investigation.
53
+
54
+ ## 4. Target Architecture
55
+
56
+ ### 4.1 Schema Ownership
57
+
58
+ | Schema | Tables and responsibility |
59
+ | --- | --- |
60
+ | `catalog` | Catalog mirror, FTS source data, categories, tags, people, studios, enrichment state, edit log, sync log, MOTD overrides. |
61
+ | `queue` | Queue shadow, spend requests, queue history, playlists, schedules, completions, and blackouts. |
62
+ | `jobs` | Job schedules, job runs, run logs, and fetch queue. |
63
+ | `users` | OTPs, device link codes, device API keys, watchlists, feedback, and title suggestions. |
64
+ | `tmdb` | Persistent TMDB dump index and index metadata. |
65
+
66
+ Schema ownership organizes privileges, migrations, and repository modules. It does not imply
67
+ separate transactions: a single SQLAlchemy session may atomically update related WebQueue
68
+ schemas. External economy and CyTube actions remain distributed operations and require
69
+ idempotency and reconciliation rather than a cross-system transaction.
70
+
71
+ ### 4.2 Access Model
72
+
73
+ - All application repository I/O uses SQLAlchemy 2.0 async sessions and parameterized named
74
+ binds. Raw `asyncpg` calls and mechanical `?` to `$n` translation are out of scope.
75
+ - Repository methods retain their public return shapes. A request or job defines the transaction
76
+ boundary; helpers must not commit independently unless explicitly documented as standalone.
77
+ - Migrations use a dedicated migrator role and a PostgreSQL advisory lock. Application instances
78
+ never race to apply DDL during startup.
79
+ - The runtime role receives only application DML privileges. The migration role owns schema DDL.
80
+ The scheduled log-pruner uses a restricted maintenance role that can delete only from
81
+ `jobs.job_run_logs`.
82
+
83
+ ### 4.3 Configuration and Secrets
84
+
85
+ `database.backend` is `sqlite` or `postgres`. The transitional SQLite configuration explicitly
86
+ selects `monolith` or `partitioned`; startup rejects ambiguous layouts and rejects an unmigrated
87
+ legacy database instead of creating empty default files.
88
+
89
+ For PostgreSQL, connection resolution is `dsn_env`, then a password-free `dsn`, then assembled
90
+ host/port/user/database values. `KRYTEN_WEBQUEUE_PG_PASSWORD` is the only password source.
91
+ Configuration files must not contain a password or a DSN with an embedded password. The
92
+ container receives its database settings through a root-owned, mode-0600 environment file.
93
+
94
+ ### 4.4 Container Connectivity
95
+
96
+ The rootful Quadlet supplies the host-gateway mapping and configures the database host as that
97
+ mapping (for example, `host.containers.internal` mapped to `host-gateway`). `localhost:5432` is
98
+ not valid for the application container unless PostgreSQL is deliberately moved into the same
99
+ pod, which is not part of this sprint. Deployment verification includes a connection test from
100
+ inside the running application container.
101
+
102
+ ## 5. PostgreSQL Design
103
+
104
+ ### 5.1 Migrations and Types
105
+
106
+ PostgreSQL DDL lives in ordered, immutable migration files with a tracked schema-version table.
107
+ The initial migration is a clean PostgreSQL baseline; it does not replay the SQLite migration
108
+ history or its historic data mutations. Each foreign key is schema-qualified, and migration tests
109
+ validate constraints, unique indexes, cascade behavior, and application-required indexes.
110
+
111
+ SQLite timestamps are interpreted as UTC during ETL. The ETL preflight rejects unparseable
112
+ values rather than guessing. Columns designated as structured JSON are validated before writing
113
+ `jsonb`; free-form log and detail fields remain `text` unless a separate compatibility decision
114
+ changes their contract. Identity values are imported with `OVERRIDING SYSTEM VALUE`, then every
115
+ identity sequence is reset with correct empty-table semantics.
116
+
117
+ ### 5.2 Search
118
+
119
+ `catalog.catalog` stores a generated English `tsvector` with title-weighted ranking and a GIN
120
+ index. `pg_trgm` supplies a title trigram index and a fuzzy fallback with an initial similarity
121
+ threshold of `0.3`. Search uses `websearch_to_tsquery` with bound parameters.
122
+
123
+ `catalog_fts` is a derived SQLite-only index. It is neither copied nor checksum-compared during
124
+ ETL. PostgreSQL search is rebuilt from `catalog.catalog` and verified against exact, multi-word,
125
+ fuzzy, and adversarial-input fixtures.
126
+
127
+ ### 5.3 Retention
128
+
129
+ Only `jobs.job_run_logs` is eligible for retention deletion. The daily 04:00 UTC task deletes
130
+ bounded batches using an index on `logged_at`, records a metric and audit log with the deleted
131
+ row count, and retries on transient database failures. It never deletes `queue.spend_requests`,
132
+ `queue.queue_history`, `users.feedback`, `users.title_suggestions`, `catalog.item_edit_log`, or
133
+ records owned by other services.
134
+
135
+ ## 6. ETL and Verification
136
+
137
+ The ETL reads partitioned SQLite files read-only after the source service and schedulers have
138
+ stopped. It creates a fresh PostgreSQL target, streams tables in foreign-key-safe order, and
139
+ never calls an external API. It writes only to a staging target that has passed migrations.
140
+
141
+ Verification is deterministic for every source table:
142
+
143
+ 1. Compare row counts.
144
+ 2. Compare canonical hashes over stable primary-key ordering, normalized UTC timestamps, and
145
+ canonical JSON representations.
146
+ 3. Validate foreign keys, uniqueness, and sequence next values in PostgreSQL.
147
+ 4. Rebuild and test derived search indexes.
148
+ 5. Run application-level fixtures for browse, recently-played suppression, watchlist ordering,
149
+ queue history, OTP redemption, device-key authentication, jobs, and search.
150
+
151
+ The source is captured with a SQLite backup/checkpoint-safe procedure while the service is
152
+ stopped. Target files are created in a staging location for the SQLite split release; the source
153
+ is never renamed or deleted automatically. Archival requires an explicit operator action after
154
+ verification.
155
+
156
+ ## 7. Cutover and Recovery
157
+
158
+ 1. Rehearse the complete migration against a representative production snapshot and record
159
+ timings, hashes, and failures.
160
+ 2. Build and verify the rootful Quadlet image and host-gateway database connectivity on
161
+ `chandra-1`.
162
+ 3. Enter maintenance mode; stop WebQueue, its scheduler, and any writer that can touch the
163
+ SQLite files.
164
+ 4. Capture a verified SQLite backup, run PostgreSQL migrations once with the migrator role, and
165
+ run the ETL into a fresh target.
166
+ 5. Require all verification gates to pass before granting the runtime/pruner roles and starting
167
+ the Quadlet service.
168
+ 6. Run health, browse, search, queue, admin, OTP, device-key, and job-log smoke tests. Monitor
169
+ connection-pool saturation, query latency, database errors, and scheduler health.
170
+ 7. Preserve the SQLite snapshot read-only for one release. After PostgreSQL accepts a write,
171
+ recovery is a forward repair in PostgreSQL using recorded request identifiers and audit data;
172
+ reverting to SQLite is not an automatic rollback path.
173
+
174
+ A failed pre-cutover verification returns to the unchanged SQLite service. A post-cutover defect
175
+ is triaged as a forward fix; any manual recovery must explicitly account for writes accepted by
176
+ PostgreSQL.
177
+
178
+ ## 8. Dependencies and Security
179
+
180
+ Runtime dependencies are `sqlalchemy>=2.0.30`, `asyncpg>=0.29.0`, and `greenlet>=3.0`.
181
+ PostgreSQL 16+ must provide `pg_trgm`; extension installation is performed by an administrator,
182
+ not the application runtime role.
183
+
184
+ Device-key hashes, OTPs, and session/auth behavior are preserved. TLS is required for a remote
185
+ PostgreSQL endpoint; the approved host-local connection uses the configured host-gateway and
186
+ host firewall restrictions. Backup ownership, retention, and restore testing are operational
187
+ requirements before production cutover.
188
+
189
+ ## 9. Explicit Non-Goals
190
+
191
+ - Redis caching, vector search, recommender features, and cross-service live database joins.
192
+ - A dual-write migration or automatic rollback to a stale SQLite database.
193
+ - Raw single-row speed as the primary success metric.
194
+
195
+ ## 10. Release Acceptance Criteria
196
+
197
+ - One canonical `webqueue` database contains the five approved schemas.
198
+ - SQLite configuration prevents accidental empty-database startup during transition.
199
+ - All migrations, ETL verification, and application fixture checks pass in rehearsal.
200
+ - The runtime uses SQLAlchemy async sessions; migrations and pruning use least-privilege roles.
201
+ - The Quadlet app connects to host-managed PostgreSQL without relying on container-localhost.
202
+ - Search, queue/payment reconciliation, retention, observability, and forward-recovery procedures
203
+ are documented and tested before the production cutover.
@@ -0,0 +1,90 @@
1
+ # SPEC — Sortie 1: Database Config + SQLAlchemy 2.0 Async Engine
2
+
3
+ **Sprint**: `postgres-migration`
4
+ **PRD**: [PRD-postgres-migration.md](PRD-postgres-migration.md)
5
+ **Depends on**: none
6
+ **Estimated**: 2–4 h
7
+
8
+ ---
9
+
10
+ ## 1. Overview
11
+
12
+ Add the Postgres backend selector, DSN resolution, and **SQLAlchemy 2.0 (`asyncpg`)** engine and session factory for the unified `webqueue` database on `chandra-1`, supporting both partitioned SQLite (from Part 1) and PostgreSQL backends.
13
+
14
+ ## 2. Scope and Non-Goals
15
+
16
+ **In scope**: configuration models, DSN resolution, SQLAlchemy `AsyncEngine` and `async_sessionmaker` factory, example configuration updates, dependency additions (`sqlalchemy>=2.0.30`, `asyncpg>=0.29.0`, `greenlet>=3.0`).
17
+ **Non-goals**: no repository port (Sortie 2), no FTS change (Sortie 3), no TMDB schema port (Sortie 4), no ETL (Sortie 5).
18
+
19
+ ## 3. Requirements
20
+
21
+ - `database.backend: sqlite | postgres` (default `sqlite` during development/transitional phase).
22
+ - `database.postgres`: `dsn_env`/password-free `dsn`/`host`/`port`/`user`/`dbname`/`password_env`/`pool_size`/`max_overflow`.
23
+ - Single database `dbname = webqueue` hosting all logical schemas (`catalog`, `queue`, `jobs`, `users`, `tmdb`).
24
+ - DSN precedence: `dsn_env` → password-free `dsn` plus `password_env` → assembled URL using `password_env`. No secrets, password field, or password-bearing DSN is permitted in `config.json`.
25
+
26
+ ## 4. Design
27
+
28
+ ```python
29
+ # kryten_webqueue/config.py
30
+ class PostgresConfig(BaseModel):
31
+ dsn_env: str | None = None
32
+ dsn: str | None = None
33
+ host: str = "host.containers.internal"
34
+ port: int = 5432
35
+ user: str = "kryten"
36
+ dbname: str = "webqueue"
37
+ password_env: str | None = "KRYTEN_WEBQUEUE_PG_PASSWORD"
38
+ pool_size: int = 10
39
+ max_overflow: int = 20
40
+
41
+ class DatabaseConfig(BaseModel):
42
+ backend: Literal["sqlite", "postgres"] = "sqlite"
43
+ sqlite_layout: Literal["monolith", "partitioned"] = "monolith"
44
+ data_dir: str = "./data" # for partitioned sqlite
45
+ postgres: PostgresConfig = PostgresConfig()
46
+ ```
47
+
48
+ ```python
49
+ # kryten_webqueue/catalog/db/engine.py
50
+ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
51
+
52
+ def create_pg_engine(config: PostgresConfig):
53
+ url = config.get_async_url() # postgresql+asyncpg://...
54
+ engine = create_async_engine(
55
+ url,
56
+ pool_size=config.pool_size,
57
+ max_overflow=config.max_overflow,
58
+ pool_pre_ping=True,
59
+ )
60
+ session_factory = async_sessionmaker(
61
+ bind=engine,
62
+ class_=AsyncSession,
63
+ expire_on_commit=False,
64
+ )
65
+ return engine, session_factory
66
+ ```
67
+
68
+ The runtime container resolves `host.containers.internal` through an explicit rootful Quadlet
69
+ host-gateway mapping. This is not a container-localhost connection. Configuration validation
70
+ rejects password-bearing `dsn` values and ambiguous SQLite layouts before startup.
71
+
72
+ ## 5. Implementation Plan
73
+
74
+ - **Modify** `config.py`: add `PostgresConfig`, `DatabaseConfig` with `postgresql+asyncpg` URL builder.
75
+ - **Create** `kryten_webqueue/catalog/db/engine.py`.
76
+ - **Add dependencies** in `pyproject.toml`: `sqlalchemy>=2.0.30`, `asyncpg>=0.29.0`, `greenlet>=3.0`.
77
+ - **Modify** `config.example.json`: document Postgres connection block with `KRYTEN_WEBQUEUE_PG_PASSWORD` environment placeholder.
78
+
79
+ ## 6. Testing Strategy
80
+
81
+ - Unit tests for `PostgresConfig.get_async_url()` testing precedence order, environment-only password substitution, and rejection of password-bearing file DSNs.
82
+ - Configuration loading tests for default SQLite and PostgreSQL configurations.
83
+
84
+ ## 7. Acceptance Criteria
85
+
86
+ - [ ] Backend selectable; supports `sqlite` and `postgres`.
87
+ - [ ] SQLAlchemy 2.0 `AsyncEngine` initialized with connection pooling.
88
+ - [ ] Environment variable secret resolution verified without leaks.
89
+ - [ ] SQLite layout validation prevents an existing monolith from being silently replaced by empty partitioned files.
90
+ - [ ] Dependencies installed and all checks green (`black`, `ruff`, `mypy`, `pytest`).