kryten-webqueue 0.48.0__tar.gz → 0.49.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 (255) hide show
  1. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/CHANGELOG.md +50 -0
  2. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/PKG-INFO +1 -1
  3. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/deploy/nginx-queue.conf +6 -0
  4. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/DATABASE_ARCHITECTURE_PLAN.md +23 -7
  5. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/postgres-cutover.md +61 -3
  6. kryten_webqueue-0.49.0/docs/postgres-migration/HANDOFF-repository-port-status.md +510 -0
  7. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/postgres-migration/PRD-postgres-migration.md +3 -0
  8. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/postgres-migration/SPEC-Sortie-6-tests-cutover-release.md +9 -2
  9. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/__init__.py +121 -22
  10. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_catalog.py +17 -6
  11. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_catalog_db.py +24 -0
  12. kryten_webqueue-0.49.0/kryten_webqueue/catalog/db/_pg_base_domain.py +157 -0
  13. kryten_webqueue-0.49.0/kryten_webqueue/catalog/db/_pg_catalog_db.py +1128 -0
  14. kryten_webqueue-0.49.0/kryten_webqueue/catalog/db/_pg_jobs_db.py +318 -0
  15. kryten_webqueue-0.49.0/kryten_webqueue/catalog/db/_pg_queue_db.py +782 -0
  16. kryten_webqueue-0.49.0/kryten_webqueue/catalog/db/_pg_users_db.py +265 -0
  17. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_playlists.py +7 -3
  18. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/sql/001_initial_schema.sql +1 -0
  19. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/config.py +35 -0
  20. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/pyproject.toml +1 -1
  21. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_multi_db_connection.py +62 -0
  22. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_postgres_config_and_engine.py +36 -0
  23. kryten_webqueue-0.49.0/tests/test_postgres_domain_crud.py +624 -0
  24. kryten_webqueue-0.49.0/tests/test_postgres_live.py +87 -0
  25. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/.github/workflows/gitleaks.yml +0 -0
  26. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/.github/workflows/python-publish.yml +0 -0
  27. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/.github/workflows/release.yml +0 -0
  28. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/.gitignore +0 -0
  29. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/.gitleaks.toml +0 -0
  30. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/.pre-commit-config.yaml +0 -0
  31. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/AGENT-WORKFLOW-GUIDE.md +0 -0
  32. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/AGENTS.md +0 -0
  33. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/Containerfile +0 -0
  34. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/README.md +0 -0
  35. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/config.example.json +0 -0
  36. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/deploy/kryten-webqueue.service +0 -0
  37. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/deploy/podman/webqueue/webqueue-app.container +0 -0
  38. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/deploy/podman/webqueue/webqueue.network +0 -0
  39. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
  40. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/IMPL_API_GATE.md +0 -0
  41. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/IMPL_ECONOMY.md +0 -0
  42. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/IMPL_KRYTEN_PY.md +0 -0
  43. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/IMPL_ROBOT.md +0 -0
  44. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
  45. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
  46. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/Log for incorrect promo insertion problem.txt +0 -0
  47. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  48. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/PRD_TMDB_LOCAL_INDEX.md +0 -0
  49. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/PRE_PLAN_GAPS.md +0 -0
  50. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/PRODUCT_PLAN.md +0 -0
  51. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/PUBLIC_API.md +0 -0
  52. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/SPEC-Visual-UX-Sprint-August-2026.md +0 -0
  53. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/SPEC_ADMIN_ITEM_EDITOR.md +0 -0
  54. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
  55. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/SPEC_EMOTE_REHOST.md +0 -0
  56. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  57. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/SPEC_TMDB_LOCAL_INDEX.md +0 -0
  58. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/UX_POLISH_PLAN.md +0 -0
  59. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/postgres-migration/SPEC-Sortie-1-db-config-and-pool.md +0 -0
  60. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/postgres-migration/SPEC-Sortie-2-connection-layer-port.md +0 -0
  61. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/postgres-migration/SPEC-Sortie-3-fts5-to-tsvector.md +0 -0
  62. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/postgres-migration/SPEC-Sortie-4-tmdb-index-database.md +0 -0
  63. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/postgres-migration/SPEC-Sortie-5-etl-migration.md +0 -0
  64. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/sqlite-domain-separation/PRD-sqlite-domain-separation.md +0 -0
  65. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/sqlite-domain-separation/SPEC-Sortie-1-schema-partitioning-and-config.md +0 -0
  66. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/sqlite-domain-separation/SPEC-Sortie-2-multi-db-connection-layer.md +0 -0
  67. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/sqlite-domain-separation/SPEC-Sortie-3-cross-domain-query-decoupling.md +0 -0
  68. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/docs/sqlite-domain-separation/SPEC-Sortie-4-etl-split-script-and-validation.md +0 -0
  69. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/__init__.py +0 -0
  70. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/__main__.py +0 -0
  71. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/api_gate/__init__.py +0 -0
  72. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/api_gate/client.py +0 -0
  73. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/app.py +0 -0
  74. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/auth/__init__.py +0 -0
  75. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/auth/device_keys.py +0 -0
  76. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/auth/otp.py +0 -0
  77. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/auth/rate_limit.py +0 -0
  78. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/auth/session.py +0 -0
  79. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/__init__.py +0 -0
  80. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_base_domain.py +0 -0
  81. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_blackouts.py +0 -0
  82. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_connection.py +0 -0
  83. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_devices.py +0 -0
  84. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
  85. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_feedback.py +0 -0
  86. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_fetch_queue.py +0 -0
  87. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_jobs_db.py +0 -0
  88. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_motd.py +0 -0
  89. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_people.py +0 -0
  90. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_queue.py +0 -0
  91. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_queue_db.py +0 -0
  92. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_users_db.py +0 -0
  93. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
  94. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/engine.py +0 -0
  95. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/schemas/__init__.py +0 -0
  96. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/schemas/catalog_schema.py +0 -0
  97. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/schemas/jobs_schema.py +0 -0
  98. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/schemas/queue_schema.py +0 -0
  99. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/db/schemas/users_schema.py +0 -0
  100. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
  101. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
  102. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
  103. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
  104. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
  105. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/report.py +0 -0
  106. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
  107. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
  108. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
  109. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/identify.py +0 -0
  110. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
  111. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
  112. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
  113. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
  114. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/images.py +0 -0
  115. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/mediacms.py +0 -0
  116. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/sync.py +0 -0
  117. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/__init__.py +0 -0
  118. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/_schema.py +0 -0
  119. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/_textmatch.py +0 -0
  120. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/_ttscrape.py +0 -0
  121. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/builder.py +0 -0
  122. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/coverage.py +0 -0
  123. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/catalog/tmdb_index/index.py +0 -0
  124. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/__init__.py +0 -0
  125. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  126. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  127. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/blackout_scan.py +0 -0
  128. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  129. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  130. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  131. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  132. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
  133. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
  134. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  135. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  136. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/__init__.py +0 -0
  137. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  138. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/job_scheduler.py +0 -0
  139. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/log_capture.py +0 -0
  140. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/manager.py +0 -0
  141. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/motd_publish.py +0 -0
  142. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
  143. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/jobs/tasks.py +0 -0
  144. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/logging_config.py +0 -0
  145. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/migrate_sqlite_to_pg.py +0 -0
  146. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/motd/__init__.py +0 -0
  147. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/motd/builder.py +0 -0
  148. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/motd/render.py +0 -0
  149. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/playlists/__init__.py +0 -0
  150. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
  151. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/playlists/fire.py +0 -0
  152. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/playlists/importer.py +0 -0
  153. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/playlists/ordering.py +0 -0
  154. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/playlists/scheduler.py +0 -0
  155. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/promos/__init__.py +0 -0
  156. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/promos/director.py +0 -0
  157. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/__init__.py +0 -0
  158. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/completion.py +0 -0
  159. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/ordering.py +0 -0
  160. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/poller.py +0 -0
  161. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/presence.py +0 -0
  162. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/race_poller.py +0 -0
  163. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/queue/shadow.py +0 -0
  164. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/__init__.py +0 -0
  165. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
  166. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_feedback.py +0 -0
  167. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
  168. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
  169. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_moderation.py +0 -0
  170. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_motd.py +0 -0
  171. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
  172. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_promos.py +0 -0
  173. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_queue.py +0 -0
  174. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
  175. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/auth.py +0 -0
  176. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/catalog.py +0 -0
  177. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/devices.py +0 -0
  178. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/feedback.py +0 -0
  179. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/pages.py +0 -0
  180. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/public_api.py +0 -0
  181. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/queue.py +0 -0
  182. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/routes/user.py +0 -0
  183. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/scripts/__init__.py +0 -0
  184. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/scripts/split_databases.py +0 -0
  185. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/static/css/main.css +0 -0
  186. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/admin-motd.js +0 -0
  187. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/admin-playlists.js +0 -0
  188. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/admin-promos.js +0 -0
  189. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
  190. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/admin-schedules.js +0 -0
  191. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/admin.js +0 -0
  192. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/keybindings.js +0 -0
  193. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/static/js/main.js +0 -0
  194. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/admin/index.html +0 -0
  195. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/admin/motd.html +0 -0
  196. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
  197. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/admin/promos.html +0 -0
  198. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  199. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
  200. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/auth/login.html +0 -0
  201. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/base.html +0 -0
  202. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
  203. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/catalog/edit_item_modal.html +0 -0
  204. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  205. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  206. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/feedback/index.html +0 -0
  207. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/motd/channel_z.html +0 -0
  208. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/queue/index.html +0 -0
  209. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/race.html +0 -0
  210. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/user/dashboard.html +0 -0
  211. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/user/link_device.html +0 -0
  212. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/templates/user/my_list.html +0 -0
  213. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/ws/__init__.py +0 -0
  214. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/ws/handler.py +0 -0
  215. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/kryten_webqueue/ws/manager.py +0 -0
  216. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/scripts/AGENTS.md +0 -0
  217. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/scripts/patch_mediacms_bulk_facets.py +0 -0
  218. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/scripts/patch_mediacms_tags.py +0 -0
  219. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/scripts/split_databases.py +0 -0
  220. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/scripts/trigger_enrich.py +0 -0
  221. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/__init__.py +0 -0
  222. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_catalog_blackout.py +0 -0
  223. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_catalog_delete.py +0 -0
  224. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_classify.py +0 -0
  225. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_concurrency_split.py +0 -0
  226. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_config_persistence.py +0 -0
  227. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_database_config_and_schemas.py +0 -0
  228. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_device_linking.py +0 -0
  229. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_feedback.py +0 -0
  230. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_fetch_queue_pacing.py +0 -0
  231. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_fetchurls_sharepoint.py +0 -0
  232. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_identify_step.py +0 -0
  233. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_job_logs_and_queue_visibility.py +0 -0
  234. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_motd_publish.py +0 -0
  235. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_people_facets.py +0 -0
  236. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_phase1.py +0 -0
  237. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_phase2_jobs.py +0 -0
  238. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_phase3_jobs.py +0 -0
  239. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_phase4_live_fixes.py +0 -0
  240. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_playlist_import.py +0 -0
  241. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_postgres_pruner.py +0 -0
  242. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_presence_refund.py +0 -0
  243. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_promo_director.py +0 -0
  244. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_promo_pool_exclusion.py +0 -0
  245. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_providers.py +0 -0
  246. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_queue_announce.py +0 -0
  247. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_queue_preview_fallback.py +0 -0
  248. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_race_poller.py +0 -0
  249. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_recently_played_hide.py +0 -0
  250. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_save_results_to_playlist.py +0 -0
  251. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_schedule_lock.py +0 -0
  252. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_search_facets.py +0 -0
  253. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_title_normalization.py +0 -0
  254. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_tmdb_index.py +0 -0
  255. {kryten_webqueue-0.48.0 → kryten_webqueue-0.49.0}/tests/test_watchlist.py +0 -0
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.49.0] - 2026-09-23
4
+
5
+ ### Added
6
+
7
+ - **Postgres repository port complete (Sortie 2/3)**: all ~140 SQLite domain methods
8
+ across `catalog`/`queue`/`jobs`/`users` are now implemented for the `asyncpg`/Postgres
9
+ backend (`_pg_catalog_db.py`, `_pg_queue_db.py`, `_pg_jobs_db.py`, `_pg_users_db.py`),
10
+ wired into `Database.__init__` for `database.backend == "postgres"`. `search()`/
11
+ `search_count()` use a hybrid `tsvector` + `pg_trgm` two-CTE query in place of SQLite's
12
+ FTS5. Live-tested end-to-end against a disposable `webqueue_test` database on
13
+ `chandra-1` (`tests/test_postgres_domain_crud.py`).
14
+
15
+ ### Fixed
16
+
17
+ - **`Database` facade never dispatched cross-domain orchestration methods (`browse`,
18
+ `search`, `get_item`, `is_restricted`, etc.) for the Postgres backend** — these all
19
+ checked `layout == "partitioned"` only, never `backend == "postgres"`, so a Postgres
20
+ `Database` would silently fall through to the SQLite-monolith code path. Introduced
21
+ `Database._domain_dispatch` (`layout == "partitioned" or backend == "postgres"`) and
22
+ updated every dispatch check to use it.
23
+ - **`rotate_playlist_item_to_bottom` silently failed on every real play completion** in
24
+ both partitioned SQLite and Postgres: it queried a bare `catalog` table from within the
25
+ `queue`-only connection/schema, which doesn't exist there (`OperationalError`/
26
+ `UndefinedTableError`), swallowed by a bare `except Exception` in
27
+ `queue/completion.py`. Added an `is_partitioned` flag to skip the same-connection
28
+ lookup, and a `Database.rotate_playlist_item_to_bottom` facade override that resolves
29
+ `friendly_token -> manifest_url` via the `catalog` domain first, mirroring the existing
30
+ `_get_reserved_tokens()` pattern.
31
+
32
+ ## [0.48.1] - 2026-09-23
33
+
34
+ ### Fixed
35
+
36
+ - **Production 500 on `/catalog/browse` in partitioned SQLite mode**: `get_tags()` (facet
37
+ counts) unconditionally ran the monolith reserved-item exclusion subquery referencing
38
+ `saved_playlist_items`/`saved_playlists`, tables that live in `queue.sqlite3`, not
39
+ `catalog.sqlite3`. Every browse/search page load crashed with
40
+ `sqlite3.OperationalError: no such table: saved_playlist_items` once the site was
41
+ cut over to `layout="partitioned"`. `get_tags` now accepts `exclude_tokens`/
42
+ `is_partitioned`, and the `Database` facade resolves reserved/blackout exclusions via
43
+ the `queue` domain before calling it, matching the existing `browse`/`search` pattern.
44
+ - **Reserved-item exclusion leak in partitioned mode**: `queue.get_reserved_media_ids()`
45
+ returns raw `media_id` values, which are usually CyTube manifest URLs, not bare
46
+ `friendly_token`s. `browse`, `browse_count`, `search`, and `search_count` were comparing
47
+ these raw values directly against `catalog.friendly_token`, so immutable/promo-pool
48
+ items whose playlist entry stored a manifest URL were **not excluded** from public
49
+ results. Added `catalog.resolve_friendly_tokens()` to resolve either shape back to
50
+ `friendly_token`, and all four call sites now use it via a shared
51
+ `Database._get_reserved_tokens()` helper.
52
+
3
53
  ## [0.48.0] - 2026-09-23
4
54
 
5
55
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kryten-webqueue
3
- Version: 0.48.0
3
+ Version: 0.49.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
@@ -13,8 +13,13 @@ server {
13
13
 
14
14
  add_header Strict-Transport-Security "max-age=31536000" always;
15
15
 
16
+ # Upstream application backend:
17
+ # - Local grindhouse service: http://127.0.0.1:2010
18
+ # - Cross-network Chandra-1 Podman container: http://chandra-1.local:2010
19
+
16
20
  # WebSocket upgrade
17
21
  location /ws {
22
+ # Use http://chandra-1.local:2010 when deployed to Chandra-1 Podman pod
18
23
  proxy_pass http://127.0.0.1:2010;
19
24
  proxy_http_version 1.1;
20
25
  proxy_set_header Upgrade $http_upgrade;
@@ -28,6 +33,7 @@ server {
28
33
 
29
34
  # API + pages
30
35
  location / {
36
+ # Use http://chandra-1.local:2010 when deployed to Chandra-1 Podman pod
31
37
  proxy_pass http://127.0.0.1:2010;
32
38
  proxy_http_version 1.1;
33
39
  proxy_set_header Connection "";
@@ -133,18 +133,29 @@ Upon completion of Part 1, the partitioned domain model directly maps to Postgre
133
133
  - `tmdb.*` — Persistent, queryable local TMDB dump index.
134
134
 
135
135
  ```
136
+ ┌─────────────────────────────────────────┐
137
+ │ grindhouse.local Host │
138
+ │ │
139
+ │ ┌─────────────────────────────────┐ │
140
+ │ │ nginx proxy │ │
141
+ │ │ queue.dropsugar.co (SSL) │ │
142
+ │ └────────────────┬────────────────┘ │
143
+ └────────────────────┼────────────────────┘
144
+ │ LAN Reverse Proxy
145
+ │ http://chandra-1.local:2010
146
+ ▼
136
147
  ┌────────────────────────────────────────────────────────────────────────────────────────┐
137
148
  │ chandra-1 Host System │
138
149
  │ │
139
150
  │ ┌──────────────────────────────────────────────────────────────────────────────────┐ │
140
- │ │ Podman Pod: kryten-webqueue │ │
151
+ │ │ Podman Container: webqueue-app │ │
141
152
  │ │ │ │
142
- │ │ ┌─────────────────────────────┐ ┌──────────────────────────────────┐ │ │
143
- │ │ │ kryten-webqueue-app │ │ nginx proxy │ │ │
144
- │ │ │ (FastAPI + uvicorn) │◄─────────►│ (Reverse Proxy + SSL) │ │ │
145
- │ │ │ SQLAlchemy 2.0 (asyncpg) │ │ │ │ │
146
- │ │ └──────────────┬──────────────┘ └────────────────┬─────────────────┘ │ │
147
- │ └─────────────────┼───────────────────────────────────────────┼────────────────────┘ │
153
+ │ │ ┌─────────────────────────────┐ │ │
154
+ │ │ │ kryten-webqueue-app │ │ │
155
+ │ │ │ (FastAPI + uvicorn:2010) │ │ │
156
+ │ │ │ SQLAlchemy 2.0 (asyncpg) │ │ │
157
+ │ │ └──────────────┬──────────────┘ │ │
158
+ │ └─────────────────┼───────────────────────────────────────────┬────────────────────┘ │
148
159
  │ │ │ │
149
160
  │ ▼ ▼ │
150
161
  │ ┌─────────────────────────────────────┐ ┌──────────────────────────────────────┐ │
@@ -156,6 +167,11 @@ Upon completion of Part 1, the partitioned domain model directly maps to Postgre
156
167
  └────────────────────────────────────────────────────────────────────────────────────────┘
157
168
  ```
158
169
 
170
+ The public hostname `https://queue.dropsugar.co/` terminates TLS via Let's Encrypt Nginx on
171
+ `grindhouse.local`. During the Chandra-1 cutover, the Nginx reverse proxy configuration on
172
+ `grindhouse.local` (`/etc/nginx/sites-available/queue.conf`) is updated to forward HTTP and
173
+ WebSocket traffic across the local network to `http://chandra-1.local:2010`.
174
+
159
175
  ### 2.2 Core Technical Specifications
160
176
 
161
177
  1. **ORM & Driver**: **SQLAlchemy 2.0 with `asyncpg`** (`create_async_engine`, `async_sessionmaker`, `AsyncSession`).
@@ -80,15 +80,73 @@ sudo systemctl start webqueue-app.service
80
80
  sudo systemctl status webqueue-app.service --no-pager
81
81
  ```
82
82
 
83
- ### Step 6: Smoke Testing & Health Verification
83
+ Verify local health inside `chandra-1`:
84
84
  ```bash
85
85
  curl -I http://127.0.0.1:2010/auth/login
86
86
  curl -s http://127.0.0.1:2010/queue/next-schedule
87
87
  ```
88
88
 
89
+ ### Step 6: Update Grindhouse Nginx Reverse Proxy Across Network
90
+ The public website `https://queue.dropsugar.co/` terminates SSL via Let's Encrypt Nginx on `grindhouse.local`. Update its upstream to proxy across the LAN to `chandra-1.local:2010`.
91
+
92
+ On `grindhouse.local`:
93
+ 1. Edit `/etc/nginx/sites-available/queue.conf`:
94
+ ```nginx
95
+ # WebSocket upgrade
96
+ location /ws {
97
+ proxy_pass http://chandra-1.local:2010;
98
+ proxy_http_version 1.1;
99
+ proxy_set_header Upgrade $http_upgrade;
100
+ proxy_set_header Connection "upgrade";
101
+ proxy_set_header Host $host;
102
+ proxy_set_header X-Real-IP $remote_addr;
103
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
104
+ proxy_set_header X-Forwarded-Proto $scheme;
105
+ proxy_read_timeout 86400;
106
+ }
107
+
108
+ # API + pages
109
+ location / {
110
+ proxy_pass http://chandra-1.local:2010;
111
+ proxy_http_version 1.1;
112
+ proxy_set_header Connection "";
113
+ proxy_set_header Host $host;
114
+ proxy_set_header X-Real-IP $remote_addr;
115
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
116
+ proxy_set_header X-Forwarded-Proto $scheme;
117
+ }
118
+ ```
119
+
120
+ 2. Test and reload Nginx:
121
+ ```bash
122
+ sudo nginx -t
123
+ sudo systemctl reload nginx
124
+ ```
125
+
126
+ ### Step 7: Public Smoke Testing & Verification
127
+ Test public endpoints through the reverse proxy:
128
+ ```bash
129
+ curl -I https://queue.dropsugar.co/auth/login
130
+ curl -s https://queue.dropsugar.co/queue/next-schedule
131
+ ```
132
+ Monitor logs on `chandra-1`:
133
+ ```bash
134
+ sudo journalctl -u webqueue-app.service -f
135
+ ```
136
+
89
137
  ---
90
138
 
91
139
  ## 3. Rollback & Forward Repair Policy
92
140
 
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.
141
+ - **Pre-Cutover Failure**: If ETL validation fails, container startup fails on `chandra-1`, or initial smoke tests fail before Nginx is updated:
142
+ 1. Do not update or revert Nginx on `grindhouse.local` (`proxy_pass http://127.0.0.1:2010;`).
143
+ 2. Restart `kryten-webqueue.service` on `grindhouse.local`:
144
+ ```bash
145
+ sudo systemctl start kryten-webqueue.service
146
+ ```
147
+ 3. The SQLite databases remain untouched and consistent.
148
+ - **Traffic Rollback (if Nginx was already switched but app has issues)**:
149
+ 1. Revert `/etc/nginx/sites-available/queue.conf` on `grindhouse.local` to point back to `http://127.0.0.1:2010`.
150
+ 2. Reload Nginx: `sudo nginx -t && sudo systemctl reload nginx`.
151
+ 3. Start `kryten-webqueue.service` on `grindhouse.local`.
152
+ - **Post-Cutover Forward Repair**: Once production traffic is accepted by PostgreSQL (user logins, new spend requests, play completions), recovery is a **forward repair in PostgreSQL**. The retained SQLite snapshot is preserved read-only for forensics.