kryten-webqueue 0.47.0__tar.gz → 0.48.1__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.
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/CHANGELOG.md +34 -0
- kryten_webqueue-0.48.1/Containerfile +23 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/PKG-INFO +4 -1
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/config.example.json +13 -1
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/deploy/nginx-queue.conf +6 -0
- kryten_webqueue-0.48.1/deploy/podman/webqueue/webqueue-app.container +24 -0
- kryten_webqueue-0.48.1/deploy/podman/webqueue/webqueue.network +3 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/DATABASE_ARCHITECTURE_PLAN.md +23 -7
- kryten_webqueue-0.48.1/docs/postgres-cutover.md +152 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/PRD-postgres-migration.md +3 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/SPEC-Sortie-6-tests-cutover-release.md +9 -2
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/app.py +8 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/__init__.py +74 -19
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_catalog.py +41 -6
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_catalog_db.py +24 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_jobs_db.py +28 -0
- kryten_webqueue-0.48.1/kryten_webqueue/catalog/db/engine.py +29 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/schemas/catalog_schema.py +3 -3
- kryten_webqueue-0.48.1/kryten_webqueue/catalog/db/sql/001_initial_schema.sql +463 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/config.py +96 -27
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/tasks.py +27 -0
- kryten_webqueue-0.48.1/kryten_webqueue/migrate_sqlite_to_pg.py +350 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1/kryten_webqueue}/scripts/split_databases.py +4 -12
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/pyproject.toml +5 -1
- kryten_webqueue-0.48.1/scripts/split_databases.py +13 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_multi_db_connection.py +62 -0
- kryten_webqueue-0.48.1/tests/test_postgres_config_and_engine.py +77 -0
- kryten_webqueue-0.48.1/tests/test_postgres_live.py +87 -0
- kryten_webqueue-0.48.1/tests/test_postgres_pruner.py +131 -0
- kryten_webqueue-0.47.0/kryten_webqueue/scripts/split_databases.py +0 -14
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/.github/workflows/gitleaks.yml +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/.gitignore +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/.gitleaks.toml +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/.pre-commit-config.yaml +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/AGENT-WORKFLOW-GUIDE.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/AGENTS.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/README.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/Log for incorrect promo insertion problem.txt +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/PRD_TMDB_LOCAL_INDEX.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/PUBLIC_API.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/SPEC-Visual-UX-Sprint-August-2026.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/SPEC_ADMIN_ITEM_EDITOR.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/SPEC_CATALOG_ENRICHMENT_PIPELINE.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/SPEC_EMOTE_REHOST.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/SPEC_TMDB_LOCAL_INDEX.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/SPEC-Sortie-1-db-config-and-pool.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/SPEC-Sortie-2-connection-layer-port.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/SPEC-Sortie-3-fts5-to-tsvector.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/SPEC-Sortie-4-tmdb-index-database.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/SPEC-Sortie-5-etl-migration.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/sqlite-domain-separation/PRD-sqlite-domain-separation.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/sqlite-domain-separation/SPEC-Sortie-1-schema-partitioning-and-config.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/sqlite-domain-separation/SPEC-Sortie-2-multi-db-connection-layer.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/sqlite-domain-separation/SPEC-Sortie-3-cross-domain-query-decoupling.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/sqlite-domain-separation/SPEC-Sortie-4-etl-split-script-and-validation.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/auth/device_keys.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_base_domain.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_blackouts.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_connection.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_devices.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_enrichment.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_feedback.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_fetch_queue.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_motd.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_people.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_playlists.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_queue.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_queue_db.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_users_db.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/_watchlist.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/schemas/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/schemas/jobs_schema.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/schemas/queue_schema.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/db/schemas/users_schema.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/classify.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/normalise.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/pipeline.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/providers.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/report.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/art.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/categories.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/identify.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/meta.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/sync.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/tags.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/enrichment/steps/title.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/_schema.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/_textmatch.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/_ttscrape.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/builder.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/coverage.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/catalog/tmdb_index/index.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/blackout_scan.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/motdposters.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/cmsutils/playedmovies.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/job_scheduler.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/log_capture.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/motd_publish.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/jobs/rehost_emotes.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/motd/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/motd/builder.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/motd/render.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/completion.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/race_poller.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_feedback.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_job_schedules.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_moderation.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_motd.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/devices.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/feedback.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/public_api.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/scripts/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/admin-motd.js +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/admin-playlists.js +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/admin-promos.js +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/admin-schedules.js +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/admin.js +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/keybindings.js +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/admin/motd.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/catalog/edit_item_modal.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/feedback/index.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/motd/channel_z.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/race.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/user/link_device.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/templates/user/my_list.html +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/scripts/AGENTS.md +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/scripts/patch_mediacms_bulk_facets.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/scripts/patch_mediacms_tags.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/scripts/trigger_enrich.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/__init__.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_catalog_blackout.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_catalog_delete.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_classify.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_concurrency_split.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_database_config_and_schemas.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_device_linking.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_feedback.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_fetch_queue_pacing.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_identify_step.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_job_logs_and_queue_visibility.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_motd_publish.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_people_facets.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_providers.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_queue_preview_fallback.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_race_poller.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_recently_played_hide.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_schedule_lock.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_search_facets.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_title_normalization.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_tmdb_index.py +0 -0
- {kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/tests/test_watchlist.py +0 -0
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.48.1] - 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Production 500 on `/catalog/browse` in partitioned SQLite mode**: `get_tags()` (facet
|
|
8
|
+
counts) unconditionally ran the monolith reserved-item exclusion subquery referencing
|
|
9
|
+
`saved_playlist_items`/`saved_playlists`, tables that live in `queue.sqlite3`, not
|
|
10
|
+
`catalog.sqlite3`. Every browse/search page load crashed with
|
|
11
|
+
`sqlite3.OperationalError: no such table: saved_playlist_items` once the site was
|
|
12
|
+
cut over to `layout="partitioned"`. `get_tags` now accepts `exclude_tokens`/
|
|
13
|
+
`is_partitioned`, and the `Database` facade resolves reserved/blackout exclusions via
|
|
14
|
+
the `queue` domain before calling it, matching the existing `browse`/`search` pattern.
|
|
15
|
+
- **Reserved-item exclusion leak in partitioned mode**: `queue.get_reserved_media_ids()`
|
|
16
|
+
returns raw `media_id` values, which are usually CyTube manifest URLs, not bare
|
|
17
|
+
`friendly_token`s. `browse`, `browse_count`, `search`, and `search_count` were comparing
|
|
18
|
+
these raw values directly against `catalog.friendly_token`, so immutable/promo-pool
|
|
19
|
+
items whose playlist entry stored a manifest URL were **not excluded** from public
|
|
20
|
+
results. Added `catalog.resolve_friendly_tokens()` to resolve either shape back to
|
|
21
|
+
`friendly_token`, and all four call sites now use it via a shared
|
|
22
|
+
`Database._get_reserved_tokens()` helper.
|
|
23
|
+
|
|
24
|
+
## [0.48.0] - 2026-09-23
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **PostgreSQL & Chandra-1 Podman Migration Foundations (`postgres-migration`)**:
|
|
29
|
+
- Added PostgreSQL backend support and configuration in `kryten_webqueue/config.py` with `PostgresConfig` and `DatabaseConfig(backend="sqlite"|"postgres")`.
|
|
30
|
+
- Added SQLAlchemy 2.0 async engine and session factory builder in `kryten_webqueue/catalog/db/engine.py` (`create_pg_engine`) utilizing `asyncpg` with connection pooling (`pool_size=10`, `max_overflow=20`, `pool_pre_ping=True`).
|
|
31
|
+
- Added schema DDL `kryten_webqueue/catalog/db/sql/001_initial_schema.sql` defining 40 tables across 5 logical schemas (`catalog`, `queue`, `jobs`, `users`, `tmdb`), English `tsvector` generated search vector, and `pg_trgm` fuzzy trigram matching.
|
|
32
|
+
- Implemented automated 30-day job run log pruning task (`job_log_prune_job`) in `kryten_webqueue/jobs/tasks.py` and `kryten_webqueue/catalog/db/_jobs_db.py` with strict exemptions for financial, chat, feedback, and audit history.
|
|
33
|
+
- Implemented one-shot ETL streaming script `kryten_webqueue/migrate_sqlite_to_pg.py` (`kryten-webqueue-migrate-pg`) with batched row streaming, identity sequence synchronization (`setval`), type normalization, and verification reporting.
|
|
34
|
+
- Added rootful Podman Quadlet specifications (`deploy/podman/webqueue/webqueue.network` and `webqueue-app.container`) and Containerfile for deployment on `chandra-1`.
|
|
35
|
+
- Added comprehensive operational cutover runbook `docs/postgres-cutover.md`.
|
|
36
|
+
|
|
3
37
|
## [0.47.0] - 2026-09-23
|
|
4
38
|
|
|
5
39
|
### Added
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
RUN apt-get update \
|
|
6
|
+
&& apt-get install -y --no-install-recommends build-essential ffmpeg curl \
|
|
7
|
+
&& rm -rf /var/lib/apt/lists/* \
|
|
8
|
+
&& useradd --system --create-home --uid 1001 kryten
|
|
9
|
+
|
|
10
|
+
COPY . /tmp/kryten-webqueue
|
|
11
|
+
|
|
12
|
+
RUN pip install --no-cache-dir --upgrade pip \
|
|
13
|
+
&& pip install --no-cache-dir /tmp/kryten-webqueue
|
|
14
|
+
|
|
15
|
+
RUN mkdir -p /etc/kryten-webqueue /var/lib/kryten-webqueue /var/log/kryten-webqueue \
|
|
16
|
+
&& chown -R kryten:kryten /etc/kryten-webqueue /var/lib/kryten-webqueue /var/log/kryten-webqueue
|
|
17
|
+
|
|
18
|
+
USER kryten
|
|
19
|
+
|
|
20
|
+
EXPOSE 2010
|
|
21
|
+
|
|
22
|
+
ENTRYPOINT ["kryten-webqueue"]
|
|
23
|
+
CMD ["--config", "/etc/kryten-webqueue/config.json"]
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: kryten-webqueue
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.48.1
|
|
4
4
|
Summary: Netflix/Tubi-style catalog browser and pay-to-play queue management for CyTube
|
|
5
5
|
Author: grobertson
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Requires-Python: >=3.12
|
|
8
8
|
Requires-Dist: aiosqlite>=0.20
|
|
9
9
|
Requires-Dist: apscheduler>=3.10
|
|
10
|
+
Requires-Dist: asyncpg>=0.29.0
|
|
10
11
|
Requires-Dist: fastapi>=0.115
|
|
12
|
+
Requires-Dist: greenlet>=3.0
|
|
11
13
|
Requires-Dist: httpx>=0.27
|
|
12
14
|
Requires-Dist: jinja2>=3.1
|
|
13
15
|
Requires-Dist: msal>=1.28
|
|
@@ -21,6 +23,7 @@ Requires-Dist: python-multipart>=0.0.9
|
|
|
21
23
|
Requires-Dist: python-slugify>=8.0
|
|
22
24
|
Requires-Dist: pyyaml>=6.0
|
|
23
25
|
Requires-Dist: requests>=2.31
|
|
26
|
+
Requires-Dist: sqlalchemy>=2.0.30
|
|
24
27
|
Requires-Dist: uvicorn[standard]>=0.30
|
|
25
28
|
Requires-Dist: websockets>=12.0
|
|
26
29
|
Requires-Dist: yt-dlp>=2024.1
|
|
@@ -63,13 +63,25 @@
|
|
|
63
63
|
},
|
|
64
64
|
|
|
65
65
|
"database": {
|
|
66
|
+
"backend": "sqlite",
|
|
66
67
|
"layout": "monolith",
|
|
67
68
|
"db_path": "/var/lib/kryten-webqueue/webqueue.db",
|
|
68
69
|
"data_dir": "/var/lib/kryten-webqueue/data",
|
|
69
70
|
"catalog_db_path": null,
|
|
70
71
|
"queue_db_path": null,
|
|
71
72
|
"jobs_db_path": null,
|
|
72
|
-
"users_db_path": null
|
|
73
|
+
"users_db_path": null,
|
|
74
|
+
"postgres": {
|
|
75
|
+
"host": "host.containers.internal",
|
|
76
|
+
"port": 5432,
|
|
77
|
+
"user": "kryten",
|
|
78
|
+
"dbname": "webqueue",
|
|
79
|
+
"password_env": "KRYTEN_WEBQUEUE_PG_PASSWORD",
|
|
80
|
+
"dsn_env": null,
|
|
81
|
+
"dsn": null,
|
|
82
|
+
"pool_size": 10,
|
|
83
|
+
"max_overflow": 20
|
|
84
|
+
}
|
|
73
85
|
},
|
|
74
86
|
"db_path": "/var/lib/kryten-webqueue/webqueue.db",
|
|
75
87
|
|
|
@@ -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 "";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=kryten-webqueue - CyTube Queue Management (FastAPI app)
|
|
3
|
+
After=network-online.target
|
|
4
|
+
|
|
5
|
+
[Container]
|
|
6
|
+
ContainerName=webqueue-app
|
|
7
|
+
Image=localhost/kryten-webqueue:latest
|
|
8
|
+
Network=podman-webqueue
|
|
9
|
+
PublishPort=2010:2010
|
|
10
|
+
AddHost=host.containers.internal:host-gateway
|
|
11
|
+
|
|
12
|
+
EnvironmentFile=/etc/kryten/webqueue/webqueue.env
|
|
13
|
+
Volume=/etc/kryten/webqueue/config.json:/etc/kryten-webqueue/config.json:ro,z
|
|
14
|
+
Volume=/var/lib/kryten/media:/var/lib/kryten-webqueue:Z
|
|
15
|
+
Volume=/var/log/kryten/webqueue:/var/log/kryten-webqueue:Z
|
|
16
|
+
|
|
17
|
+
Exec=uvicorn kryten_webqueue.app:app --host 0.0.0.0 --port 2010
|
|
18
|
+
|
|
19
|
+
[Service]
|
|
20
|
+
Restart=always
|
|
21
|
+
RestartSec=5
|
|
22
|
+
|
|
23
|
+
[Install]
|
|
24
|
+
WantedBy=multi-user.target default.target
|
|
@@ -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
|
-
│ │
|
|
151
|
+
│ │ Podman Container: webqueue-app │ │
|
|
141
152
|
│ │ │ │
|
|
142
|
-
│ │ ┌─────────────────────────────┐
|
|
143
|
-
│ │ │ kryten-webqueue-app │
|
|
144
|
-
│ │ │ (FastAPI + uvicorn)
|
|
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`).
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# PostgreSQL Cutover Runbook: Kryten-WebQueue
|
|
2
|
+
|
|
3
|
+
**Target Version**: `0.48.0`
|
|
4
|
+
**Host Target**: `chandra-1` (PostgreSQL 16 + Podman Quadlet)
|
|
5
|
+
**Source Host**: `grindhouse.local` (pipx systemd / SQLite partitioned data)
|
|
6
|
+
**Parent Plan**: [DATABASE_ARCHITECTURE_PLAN.md](DATABASE_ARCHITECTURE_PLAN.md)
|
|
7
|
+
**PRD**: [postgres-migration/PRD-postgres-migration.md](postgres-migration/PRD-postgres-migration.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Pre-Flight Checklist
|
|
12
|
+
|
|
13
|
+
- [ ] `chandra-1` has PostgreSQL 16+ active and `pg_trgm` extension installed.
|
|
14
|
+
- [ ] Database `webqueue` exists with owner `kryten`.
|
|
15
|
+
- [ ] Schema `001_initial_schema.sql` applied cleanly across `catalog`, `queue`, `jobs`, `users`, `tmdb`.
|
|
16
|
+
- [ ] Privileges granted to `kryten`:
|
|
17
|
+
```sql
|
|
18
|
+
GRANT USAGE, CREATE ON SCHEMA catalog, queue, jobs, users, tmdb TO kryten;
|
|
19
|
+
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA catalog, queue, jobs, users, tmdb, public TO kryten;
|
|
20
|
+
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA catalog, queue, jobs, users, tmdb, public TO kryten;
|
|
21
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA catalog, queue, jobs, users, tmdb GRANT ALL ON TABLES TO kryten;
|
|
22
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA catalog, queue, jobs, users, tmdb GRANT ALL ON SEQUENCES TO kryten;
|
|
23
|
+
```
|
|
24
|
+
- [ ] Podman image built on `chandra-1`:
|
|
25
|
+
```bash
|
|
26
|
+
sudo podman build -f Containerfile -t localhost/kryten-webqueue:latest .
|
|
27
|
+
```
|
|
28
|
+
- [ ] Environment file `/etc/kryten/webqueue/webqueue.env` created on `chandra-1`:
|
|
29
|
+
```env
|
|
30
|
+
KRYTEN_WEBQUEUE_PG_PASSWORD=kryten_secret_password
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 2. Maintenance Window Cutover (Step-by-Step)
|
|
36
|
+
|
|
37
|
+
### Step 1: Stop Source Service & Freeze Writes
|
|
38
|
+
On `grindhouse.local`:
|
|
39
|
+
```bash
|
|
40
|
+
sudo systemctl stop kryten-webqueue.service
|
|
41
|
+
sudo systemctl status kryten-webqueue.service --no-pager
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Step 2: Checkpoint SQLite WAL & Create Final Snapshot
|
|
45
|
+
On `grindhouse.local`:
|
|
46
|
+
```bash
|
|
47
|
+
SNAPSHOT_DIR="/var/lib/kryten-webqueue/backups/cutover_$(date +%Y%m%d_%H%M%S)"
|
|
48
|
+
sudo mkdir -p "$SNAPSHOT_DIR"
|
|
49
|
+
|
|
50
|
+
for db in catalog queue jobs users; do
|
|
51
|
+
sudo sqlite3 "/var/lib/kryten-webqueue/data/$db.sqlite3" "PRAGMA wal_checkpoint(TRUNCATE);"
|
|
52
|
+
done
|
|
53
|
+
|
|
54
|
+
sudo cp -av /var/lib/kryten-webqueue/data "$SNAPSHOT_DIR/"
|
|
55
|
+
sudo cp -av /etc/kryten-webqueue/config.json "$SNAPSHOT_DIR/"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Step 3: Stream SQLite Data to PostgreSQL on Chandra-1
|
|
59
|
+
Run the ETL migration script with `--verify`:
|
|
60
|
+
```bash
|
|
61
|
+
sudo -u kryten /home/kryten/.local/pipx/venvs/kryten-webqueue/bin/python -m kryten_webqueue.migrate_sqlite_to_pg \
|
|
62
|
+
--data-dir /var/lib/kryten-webqueue/data \
|
|
63
|
+
--pg-dsn "postgresql://kryten:kryten_secret_password@chandra-1.local:5432/webqueue"
|
|
64
|
+
```
|
|
65
|
+
Verify `postgres_migration_report.json` indicates `"status": "success"` and 0 errors.
|
|
66
|
+
|
|
67
|
+
### Step 4: Transfer Media & Image Assets to Chandra-1
|
|
68
|
+
Sync cover art images to `/var/lib/kryten/media`:
|
|
69
|
+
```bash
|
|
70
|
+
rsync -avz /var/lib/kryten-webqueue/images/ groberts@chandra-1.local:/var/lib/kryten/media/images/
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Step 5: Start Podman Quadlet on Chandra-1
|
|
74
|
+
On `chandra-1`:
|
|
75
|
+
```bash
|
|
76
|
+
sudo cp deploy/podman/webqueue/webqueue.network /etc/containers/systemd/
|
|
77
|
+
sudo cp deploy/podman/webqueue/webqueue-app.container /etc/containers/systemd/
|
|
78
|
+
sudo systemctl daemon-reload
|
|
79
|
+
sudo systemctl start webqueue-app.service
|
|
80
|
+
sudo systemctl status webqueue-app.service --no-pager
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Verify local health inside `chandra-1`:
|
|
84
|
+
```bash
|
|
85
|
+
curl -I http://127.0.0.1:2010/auth/login
|
|
86
|
+
curl -s http://127.0.0.1:2010/queue/next-schedule
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 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
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 3. Rollback & Forward Repair Policy
|
|
140
|
+
|
|
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.
|
{kryten_webqueue-0.47.0 → kryten_webqueue-0.48.1}/docs/postgres-migration/PRD-postgres-migration.md
RENAMED
|
@@ -28,6 +28,9 @@ The following decisions are fixed for this sprint:
|
|
|
28
28
|
- One PostgreSQL database named `webqueue` owns all five schemas. `tmdb` is a schema, not a
|
|
29
29
|
separate database, so catalog enrichment can use relational queries when needed.
|
|
30
30
|
- The application runs as a rootful Podman Quadlet service on `chandra-1`.
|
|
31
|
+
- Public traffic on `https://queue.dropsugar.co/` terminates TLS at Nginx on `grindhouse.local`.
|
|
32
|
+
During cutover, Nginx on `grindhouse.local` (`/etc/nginx/sites-available/queue.conf`) is updated
|
|
33
|
+
to reverse-proxy HTTP and WebSocket (`/ws`) traffic across the local network to `http://chandra-1.local:2010`.
|
|
31
34
|
- PostgreSQL is host-managed on `chandra-1`. The rootful application container connects through
|
|
32
35
|
an explicitly configured host-gateway address; it must not use `localhost`.
|
|
33
36
|
- The migration uses a maintenance window and a forward-fix rollback policy. There is no
|
|
@@ -66,12 +66,19 @@ python -m kryten_webqueue.migrate_sqlite_to_pg --data-dir /path/to/sqlite/data -
|
|
|
66
66
|
# 5. Grant runtime/pruner roles only after verification, then start the rootful Quadlet.
|
|
67
67
|
sudo systemctl start webqueue-app.service
|
|
68
68
|
|
|
69
|
-
# 6.
|
|
69
|
+
# 6. Update Nginx reverse proxy on grindhouse.local to forward across the LAN:
|
|
70
|
+
# In /etc/nginx/sites-available/queue.conf:
|
|
71
|
+
# proxy_pass http://chandra-1.local:2010; (for /ws and /)
|
|
72
|
+
# sudo nginx -t && sudo systemctl reload nginx
|
|
73
|
+
|
|
74
|
+
# 7. Verify health and smoke test public HTTPS and WebSocket paths:
|
|
75
|
+
curl -f https://queue.dropsugar.co/auth/login
|
|
70
76
|
curl -f http://127.0.0.1:2010/api/public/v1/state
|
|
71
77
|
```
|
|
72
78
|
|
|
73
79
|
If a gate fails before PostgreSQL receives production writes, restart the unchanged SQLite
|
|
74
|
-
service
|
|
80
|
+
service and revert the Nginx proxy_pass on grindhouse.local back to http://127.0.0.1:2010.
|
|
81
|
+
Once PostgreSQL accepts a write, recovery is a forward repair using request identifiers
|
|
75
82
|
and audit records; the retained SQLite snapshot is not an automatic rollback target.
|
|
76
83
|
|
|
77
84
|
## 4. Implementation Plan
|
|
@@ -111,6 +111,7 @@ async def lifespan(app: FastAPI):
|
|
|
111
111
|
motd_posters_job,
|
|
112
112
|
tmdb_index_refresh_job,
|
|
113
113
|
tmdb_coverage_report_job,
|
|
114
|
+
job_log_prune_job,
|
|
114
115
|
CATALOG_ENRICH_SCHEMA,
|
|
115
116
|
CATALOG_BLACKOUT_SCHEMA,
|
|
116
117
|
DEVICE_KEY_BAN_RECONCILE_SCHEMA,
|
|
@@ -121,6 +122,7 @@ async def lifespan(app: FastAPI):
|
|
|
121
122
|
MOTD_POSTERS_SCHEMA,
|
|
122
123
|
TMDB_INDEX_REFRESH_SCHEMA,
|
|
123
124
|
TMDB_COVERAGE_REPORT_SCHEMA,
|
|
125
|
+
JOB_LOG_PRUNE_SCHEMA,
|
|
124
126
|
)
|
|
125
127
|
|
|
126
128
|
job_manager.register(
|
|
@@ -168,6 +170,12 @@ async def lifespan(app: FastAPI):
|
|
|
168
170
|
label="MOTD Posters (weekend poster grid)",
|
|
169
171
|
schema=MOTD_POSTERS_SCHEMA,
|
|
170
172
|
)
|
|
173
|
+
job_manager.register(
|
|
174
|
+
"job_log_prune",
|
|
175
|
+
job_log_prune_job,
|
|
176
|
+
label="Job Run Log Pruner (30-day retention)",
|
|
177
|
+
schema=JOB_LOG_PRUNE_SCHEMA,
|
|
178
|
+
)
|
|
171
179
|
from .jobs.motd_publish import motd_publish_job, MOTD_PUBLISH_SCHEMA
|
|
172
180
|
|
|
173
181
|
job_manager.register(
|
|
@@ -107,17 +107,17 @@ _DOMAIN_METHOD_MAP: dict[str, str] = {
|
|
|
107
107
|
"get_job_schedule": "jobs",
|
|
108
108
|
"upsert_job_schedule": "jobs",
|
|
109
109
|
"delete_job_schedule": "jobs",
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
110
|
+
"prune_job_run_logs": "jobs",
|
|
111
|
+
"enqueue_fetch": "jobs",
|
|
112
|
+
"claim_next_fetch_item": "jobs",
|
|
113
|
+
"finish_fetch_item": "jobs",
|
|
114
|
+
"requeue_fetch_item": "jobs",
|
|
115
|
+
"requeue_fetch_item_for_retry": "jobs",
|
|
116
|
+
"reset_running_fetch_items": "jobs",
|
|
117
|
+
"get_fetch_queue": "jobs",
|
|
118
|
+
"count_fetch_queue": "jobs",
|
|
119
|
+
"count_fetch_queue_pending": "jobs",
|
|
120
|
+
"delete_fetch_queue_item": "jobs",
|
|
121
121
|
# users
|
|
122
122
|
"watchlist_add": "users",
|
|
123
123
|
"watchlist_remove": "users",
|
|
@@ -182,6 +182,7 @@ _DOMAIN_METHOD_MAP: dict[str, str] = {
|
|
|
182
182
|
"get_item_edit_history": "catalog",
|
|
183
183
|
"get_items_by_tokens": "catalog",
|
|
184
184
|
"get_hidden_category_and_tag_tokens": "catalog",
|
|
185
|
+
"resolve_friendly_tokens": "catalog",
|
|
185
186
|
"get_people": "catalog",
|
|
186
187
|
"get_studios": "catalog",
|
|
187
188
|
"upsert_person": "catalog",
|
|
@@ -303,10 +304,22 @@ class Database(
|
|
|
303
304
|
except AttributeError:
|
|
304
305
|
layout = "monolith"
|
|
305
306
|
|
|
306
|
-
if layout == "partitioned"
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
307
|
+
if layout == "partitioned":
|
|
308
|
+
if name in Database.__dict__ or name.startswith("_"):
|
|
309
|
+
return object.__getattribute__(self, name)
|
|
310
|
+
|
|
311
|
+
if name in _DOMAIN_METHOD_MAP:
|
|
312
|
+
domain_name = _DOMAIN_METHOD_MAP[name]
|
|
313
|
+
domain_obj = object.__getattribute__(self, domain_name)
|
|
314
|
+
return getattr(domain_obj, name)
|
|
315
|
+
|
|
316
|
+
for domain_name in ("jobs", "queue", "users", "catalog"):
|
|
317
|
+
try:
|
|
318
|
+
domain_obj = object.__getattribute__(self, domain_name)
|
|
319
|
+
if hasattr(domain_obj, name) and not hasattr(_DBBase, name):
|
|
320
|
+
return getattr(domain_obj, name)
|
|
321
|
+
except AttributeError:
|
|
322
|
+
pass
|
|
310
323
|
|
|
311
324
|
return object.__getattribute__(self, name)
|
|
312
325
|
|
|
@@ -321,6 +334,16 @@ class Database(
|
|
|
321
334
|
|
|
322
335
|
# --- Decoupled Cross-Domain Orchestrations ---
|
|
323
336
|
|
|
337
|
+
async def _get_reserved_tokens(self) -> set[str]:
|
|
338
|
+
"""Reserved (immutable/promo) media_ids resolved to friendly_tokens.
|
|
339
|
+
|
|
340
|
+
queue.db stores media_id as either a bare token or a manifest URL, so
|
|
341
|
+
the raw values must be resolved through catalog.db before comparing
|
|
342
|
+
against friendly_token.
|
|
343
|
+
"""
|
|
344
|
+
raw = await self.queue.get_reserved_media_ids()
|
|
345
|
+
return await self.catalog.resolve_friendly_tokens(raw)
|
|
346
|
+
|
|
324
347
|
async def browse(
|
|
325
348
|
self,
|
|
326
349
|
*,
|
|
@@ -345,7 +368,7 @@ class Database(
|
|
|
345
368
|
recently_played_days
|
|
346
369
|
)
|
|
347
370
|
exclude_set.update(hidden)
|
|
348
|
-
reserved = await self.
|
|
371
|
+
reserved = await self._get_reserved_tokens()
|
|
349
372
|
exclude_set.update(reserved)
|
|
350
373
|
blackouts = await self.queue.get_active_blackout_tokens()
|
|
351
374
|
if not show_hidden:
|
|
@@ -413,7 +436,7 @@ class Database(
|
|
|
413
436
|
recently_played_days
|
|
414
437
|
)
|
|
415
438
|
exclude_set.update(hidden)
|
|
416
|
-
reserved = await self.
|
|
439
|
+
reserved = await self._get_reserved_tokens()
|
|
417
440
|
exclude_set.update(reserved)
|
|
418
441
|
if not show_hidden:
|
|
419
442
|
blackouts = await self.queue.get_active_blackout_tokens()
|
|
@@ -471,7 +494,7 @@ class Database(
|
|
|
471
494
|
recently_played_days
|
|
472
495
|
)
|
|
473
496
|
exclude_set.update(hidden)
|
|
474
|
-
reserved = await self.
|
|
497
|
+
reserved = await self._get_reserved_tokens()
|
|
475
498
|
exclude_set.update(reserved)
|
|
476
499
|
blackouts = await self.queue.get_active_blackout_tokens()
|
|
477
500
|
if not show_hidden:
|
|
@@ -542,7 +565,7 @@ class Database(
|
|
|
542
565
|
recently_played_days
|
|
543
566
|
)
|
|
544
567
|
exclude_set.update(hidden)
|
|
545
|
-
reserved = await self.
|
|
568
|
+
reserved = await self._get_reserved_tokens()
|
|
546
569
|
exclude_set.update(reserved)
|
|
547
570
|
if not show_hidden:
|
|
548
571
|
blackouts = await self.queue.get_active_blackout_tokens()
|
|
@@ -600,6 +623,38 @@ class Database(
|
|
|
600
623
|
self, username, page=page, per_page=per_page
|
|
601
624
|
)
|
|
602
625
|
|
|
626
|
+
async def get_tags(
|
|
627
|
+
self,
|
|
628
|
+
*,
|
|
629
|
+
limit: int = 100,
|
|
630
|
+
show_hidden: bool = False,
|
|
631
|
+
min_duration_sec: int = 0,
|
|
632
|
+
max_duration_sec: int | None = None,
|
|
633
|
+
**kwargs: Any,
|
|
634
|
+
) -> list[dict]:
|
|
635
|
+
if self._layout == "partitioned":
|
|
636
|
+
exclude_set: set[str] = await self._get_reserved_tokens()
|
|
637
|
+
if not show_hidden:
|
|
638
|
+
blackouts = await self.queue.get_active_blackout_tokens()
|
|
639
|
+
exclude_set.update(blackouts)
|
|
640
|
+
return await self.catalog.get_tags(
|
|
641
|
+
limit=limit,
|
|
642
|
+
show_hidden=show_hidden,
|
|
643
|
+
min_duration_sec=min_duration_sec,
|
|
644
|
+
max_duration_sec=max_duration_sec,
|
|
645
|
+
exclude_tokens=exclude_set,
|
|
646
|
+
is_partitioned=True,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
return await _CatalogMixin.get_tags(
|
|
650
|
+
self,
|
|
651
|
+
limit=limit,
|
|
652
|
+
show_hidden=show_hidden,
|
|
653
|
+
min_duration_sec=min_duration_sec,
|
|
654
|
+
max_duration_sec=max_duration_sec,
|
|
655
|
+
is_partitioned=False,
|
|
656
|
+
)
|
|
657
|
+
|
|
603
658
|
async def get_item(self, friendly_token: str) -> dict | None:
|
|
604
659
|
if self._layout == "partitioned":
|
|
605
660
|
item = await self.catalog.get_item(friendly_token, is_partitioned=True)
|
|
@@ -916,6 +916,8 @@ class _CatalogMixin:
|
|
|
916
916
|
show_hidden: bool = False,
|
|
917
917
|
min_duration_sec: int = 0,
|
|
918
918
|
max_duration_sec: int | None = None,
|
|
919
|
+
exclude_tokens: set[str] | list[str] | None = None,
|
|
920
|
+
is_partitioned: bool = False,
|
|
919
921
|
) -> list[dict]:
|
|
920
922
|
"""Most-used tags that have at least one catalog item, for facets.
|
|
921
923
|
|
|
@@ -929,16 +931,25 @@ class _CatalogMixin:
|
|
|
929
931
|
WHERE 1=1
|
|
930
932
|
"""
|
|
931
933
|
params: list = []
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
934
|
+
if not is_partitioned:
|
|
935
|
+
resv_sql, resv_params = _reserved_exclusion("c")
|
|
936
|
+
sql += resv_sql
|
|
937
|
+
params.extend(resv_params)
|
|
938
|
+
if not show_hidden:
|
|
939
|
+
bo_sql, bo_params = _blackout_exclusion("c")
|
|
940
|
+
sql += bo_sql
|
|
941
|
+
params.extend(bo_params)
|
|
935
942
|
if not show_hidden:
|
|
936
|
-
bo_sql, bo_params = _blackout_exclusion("c")
|
|
937
|
-
sql += bo_sql
|
|
938
|
-
params.extend(bo_params)
|
|
939
943
|
ph = ",".join("?" * len(HIDDEN_TAG_NAMES))
|
|
940
944
|
sql += f" AND t.name NOT IN ({ph})"
|
|
941
945
|
params.extend(HIDDEN_TAG_NAMES)
|
|
946
|
+
if exclude_tokens:
|
|
947
|
+
tokens_list = [t for t in exclude_tokens if t]
|
|
948
|
+
for i in range(0, len(tokens_list), 500):
|
|
949
|
+
chunk = tokens_list[i : i + 500]
|
|
950
|
+
ph = ",".join("?" * len(chunk))
|
|
951
|
+
sql += f" AND c.friendly_token NOT IN ({ph}) "
|
|
952
|
+
params.extend(chunk)
|
|
942
953
|
if min_duration_sec > 0 or max_duration_sec is not None:
|
|
943
954
|
dur_sql, dur_params = _duration_range_filter(
|
|
944
955
|
"c", min_duration_sec or None, max_duration_sec
|
|
@@ -1331,6 +1342,30 @@ class _CatalogMixin:
|
|
|
1331
1342
|
async def delete_job_schedule(self, job_name: str) -> None:
|
|
1332
1343
|
await self._execute("DELETE FROM job_schedules WHERE job_name=?", [job_name])
|
|
1333
1344
|
|
|
1345
|
+
async def prune_job_run_logs(
|
|
1346
|
+
self, retention_days: int = 30, batch_size: int = 5000
|
|
1347
|
+
) -> int:
|
|
1348
|
+
total_deleted = 0
|
|
1349
|
+
cutoff = f"-{int(retention_days)} days"
|
|
1350
|
+
while True:
|
|
1351
|
+
cursor = await self._db.execute(
|
|
1352
|
+
"""
|
|
1353
|
+
DELETE FROM job_run_logs
|
|
1354
|
+
WHERE id IN (
|
|
1355
|
+
SELECT id FROM job_run_logs
|
|
1356
|
+
WHERE logged_at < datetime('now', ?)
|
|
1357
|
+
LIMIT ?
|
|
1358
|
+
)
|
|
1359
|
+
""",
|
|
1360
|
+
[cutoff, batch_size],
|
|
1361
|
+
)
|
|
1362
|
+
await self._db.commit()
|
|
1363
|
+
count = cursor.rowcount or 0
|
|
1364
|
+
total_deleted += count
|
|
1365
|
+
if count < batch_size:
|
|
1366
|
+
break
|
|
1367
|
+
return total_deleted
|
|
1368
|
+
|
|
1334
1369
|
# --- OTP ---
|
|
1335
1370
|
|
|
1336
1371
|
async def store_otp(self, username: str, code: str, expires_at: str):
|