kryten-webqueue 0.23.0__tar.gz → 0.24.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 (107) hide show
  1. kryten_webqueue-0.24.0/.github/workflows/python-publish.yml +93 -0
  2. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/.github/workflows/release.yml +6 -12
  3. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/CHANGELOG.md +17 -0
  4. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/PKG-INFO +1 -1
  5. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/static/css/main.css +40 -0
  6. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/templates/user/dashboard.html +96 -2
  7. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/pyproject.toml +1 -1
  8. kryten_webqueue-0.23.0/.github/workflows/python-publish.yml +0 -75
  9. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/.gitignore +0 -0
  10. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/README.md +0 -0
  11. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/config.example.json +0 -0
  12. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/deploy/kryten-webqueue.service +0 -0
  13. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/deploy/nginx-queue.conf +0 -0
  14. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
  15. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/docs/IMPL_API_GATE.md +0 -0
  16. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/docs/IMPL_ECONOMY.md +0 -0
  17. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/docs/IMPL_KRYTEN_PY.md +0 -0
  18. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/docs/IMPL_ROBOT.md +0 -0
  19. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
  20. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/docs/PRE_PLAN_GAPS.md +0 -0
  21. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/docs/PRODUCT_PLAN.md +0 -0
  22. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
  23. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/docs/UX_POLISH_PLAN.md +0 -0
  24. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/__init__.py +0 -0
  25. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/__main__.py +0 -0
  26. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/api_gate/__init__.py +0 -0
  27. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/api_gate/client.py +0 -0
  28. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/app.py +0 -0
  29. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/auth/__init__.py +0 -0
  30. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/auth/otp.py +0 -0
  31. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/auth/rate_limit.py +0 -0
  32. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/auth/session.py +0 -0
  33. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/catalog/__init__.py +0 -0
  34. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/catalog/db.py +0 -0
  35. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/catalog/images.py +0 -0
  36. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/catalog/mediacms.py +0 -0
  37. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/catalog/sync.py +0 -0
  38. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/config.py +0 -0
  39. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/integrations/__init__.py +0 -0
  40. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
  41. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
  42. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
  43. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
  44. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
  45. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
  46. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
  47. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
  48. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/jobs/__init__.py +0 -0
  49. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
  50. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/jobs/manager.py +0 -0
  51. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/jobs/tasks.py +0 -0
  52. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/logging_config.py +0 -0
  53. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/playlists/__init__.py +0 -0
  54. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
  55. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/playlists/fire.py +0 -0
  56. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/playlists/importer.py +0 -0
  57. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/playlists/ordering.py +0 -0
  58. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/playlists/scheduler.py +0 -0
  59. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/promos/__init__.py +0 -0
  60. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/promos/director.py +0 -0
  61. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/queue/__init__.py +0 -0
  62. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/queue/ordering.py +0 -0
  63. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/queue/poller.py +0 -0
  64. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/queue/presence.py +0 -0
  65. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/queue/shadow.py +0 -0
  66. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/routes/__init__.py +0 -0
  67. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
  68. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
  69. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
  70. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/routes/admin_promos.py +0 -0
  71. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/routes/admin_queue.py +0 -0
  72. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
  73. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/routes/auth.py +0 -0
  74. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/routes/catalog.py +0 -0
  75. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/routes/pages.py +0 -0
  76. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/routes/queue.py +0 -0
  77. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/routes/user.py +0 -0
  78. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/static/js/main.js +0 -0
  79. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/templates/admin/index.html +0 -0
  80. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
  81. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/templates/admin/promos.html +0 -0
  82. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
  83. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
  84. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/templates/auth/login.html +0 -0
  85. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/templates/base.html +0 -0
  86. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
  87. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
  88. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
  89. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/templates/queue/index.html +0 -0
  90. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/ws/__init__.py +0 -0
  91. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/ws/handler.py +0 -0
  92. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/kryten_webqueue/ws/manager.py +0 -0
  93. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/__init__.py +0 -0
  94. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_config_persistence.py +0 -0
  95. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_fetchurls_sharepoint.py +0 -0
  96. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_phase1.py +0 -0
  97. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_phase2_jobs.py +0 -0
  98. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_phase3_jobs.py +0 -0
  99. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_phase4_live_fixes.py +0 -0
  100. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_playlist_import.py +0 -0
  101. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_presence_refund.py +0 -0
  102. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_promo_director.py +0 -0
  103. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_promo_pool_exclusion.py +0 -0
  104. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_queue_announce.py +0 -0
  105. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_save_results_to_playlist.py +0 -0
  106. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_schedule_lock.py +0 -0
  107. {kryten_webqueue-0.23.0 → kryten_webqueue-0.24.0}/tests/test_search_facets.py +0 -0
@@ -0,0 +1,93 @@
1
+ name: Publish Python Package to PyPI
2
+
3
+ # Builds and publishes to PyPI. This workflow MUST run top-level (never invoked
4
+ # via a reusable `uses:` call). PyPI Trusted Publishing matches the OIDC
5
+ # `job_workflow_ref` claim — the filename of the workflow that contains the
6
+ # running job — against the configured publisher (python-publish.yml). Running
7
+ # this as a reusable workflow would change that filename and break publishing
8
+ # (and is explicitly unsupported by PyPI).
9
+ on:
10
+ # Primary automated path: fire after "Release Automation" (release.yml)
11
+ # finishes creating the tag + GitHub Release. workflow_run keeps this workflow
12
+ # top-level, unlike a reusable `uses:` invocation.
13
+ workflow_run:
14
+ workflows: ["Release Automation"]
15
+ types: [completed]
16
+ # Manual fallbacks: publishing a GitHub Release by hand, or pushing a tag.
17
+ release:
18
+ types: [published]
19
+ push:
20
+ tags:
21
+ - 'kryten-webqueue-v*'
22
+ - 'v*'
23
+
24
+ permissions:
25
+ contents: read
26
+
27
+ jobs:
28
+ release-build:
29
+ name: Build distribution packages
30
+ runs-on: ubuntu-latest
31
+ # When triggered by release.yml's completion, only proceed on success.
32
+ # release/tag triggers aren't workflow_run events, so they always build.
33
+ if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
34
+
35
+ steps:
36
+ - name: Checkout repository
37
+ uses: actions/checkout@v7
38
+ with:
39
+ # For a workflow_run trigger, build the exact commit release.yml
40
+ # processed; otherwise build the tag/release commit.
41
+ ref: ${{ github.event.workflow_run.head_sha || github.sha }}
42
+
43
+ - name: Install uv
44
+ uses: astral-sh/setup-uv@v8.2.0
45
+ with:
46
+ version: "latest"
47
+
48
+ - name: Set up Python
49
+ uses: actions/setup-python@v6
50
+ with:
51
+ python-version: "3.12"
52
+
53
+ - name: Build release distributions
54
+ run: |
55
+ uv build
56
+ echo "📦 Built packages:"
57
+ ls -lh dist/
58
+
59
+ - name: Upload distributions
60
+ uses: actions/upload-artifact@v7
61
+ with:
62
+ name: release-dists
63
+ path: dist/
64
+
65
+ pypi-publish:
66
+ name: Publish to PyPI
67
+ runs-on: ubuntu-latest
68
+ needs:
69
+ - release-build
70
+ environment:
71
+ name: pypi
72
+ permissions:
73
+ id-token: write
74
+
75
+ steps:
76
+ - name: Retrieve release distributions
77
+ uses: actions/download-artifact@v8
78
+ with:
79
+ name: release-dists
80
+ path: dist/
81
+
82
+ - name: Publish release distributions to PyPI
83
+ uses: pypa/gh-action-pypi-publish@release/v1
84
+ with:
85
+ packages-dir: dist/
86
+ skip-existing: true
87
+ attestations: false
88
+
89
+ - name: Success notification
90
+ if: success()
91
+ run: |
92
+ echo "✅ Successfully published to PyPI!"
93
+ echo "🔗 Package URL: https://pypi.org/project/kryten-webqueue/"
@@ -1,6 +1,10 @@
1
1
  name: Release Automation
2
2
 
3
- # Automatically create GitHub releases when version in pyproject.toml changes
3
+ # Automatically create GitHub releases when version in pyproject.toml changes.
4
+ # Publishing to PyPI is handled separately by python-publish.yml, which triggers
5
+ # on this workflow's completion (see its workflow_run trigger). Publishing is
6
+ # intentionally NOT inlined here: PyPI Trusted Publishing matches the OIDC
7
+ # job_workflow_ref filename, which is configured as python-publish.yml.
4
8
  on:
5
9
  push:
6
10
  branches:
@@ -11,8 +15,6 @@ on:
11
15
 
12
16
  permissions:
13
17
  contents: write
14
- pull-requests: read
15
- id-token: write
16
18
 
17
19
  jobs:
18
20
  create-release:
@@ -20,7 +22,7 @@ jobs:
20
22
  runs-on: ubuntu-latest
21
23
 
22
24
  steps:
23
- - uses: actions/checkout@v4
25
+ - uses: actions/checkout@v7
24
26
  with:
25
27
  fetch-depth: 0 # Full history for changelog
26
28
 
@@ -93,11 +95,3 @@ jobs:
93
95
  if: steps.check_tag.outputs.exists == 'true'
94
96
  run: |
95
97
  echo "ℹ️ Release ${{ steps.version.outputs.tag }} already exists - skipping"
96
-
97
- publish-to-pypi:
98
- name: Publish to PyPI
99
- needs: [create-release]
100
- uses: ./.github/workflows/python-publish.yml
101
- permissions:
102
- id-token: write
103
- contents: read
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.24.0] — 2026-06-21
9
+
10
+ ### Added
11
+
12
+ - **Live readability preview for the chat-color picker.** The dashboard's chat-color editor now previews the chosen color exactly as it renders in chat — a sample chat line (gray timestamp, username, message) on the near-black chat background (`#111`) — and scores it as you pick, mirroring kryten-economy's server-side guard (combined APCA lightness contrast × red-chroma penalty). A contrast badge reports **ok / warn / reject**: colors that fail the readability threshold (very dark colors and harsh pure reds) show a **reject** badge and the **Save button is disabled**, so users can't buy a color the server would refuse; borderline colors get a non-blocking **warn**. The thresholds (`reject < 30`, `warn < 40`) and background match the economy service so the dashboard and server always agree.
13
+
14
+ [0.24.0]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.24.0
15
+
16
+ ## [0.23.1] — 2026-06-21
17
+
18
+ ### Changed
19
+
20
+ - **Release CI hardening (no runtime changes).** PyPI publishing was moved out of a reusable workflow into a top-level one. PyPI Trusted Publishing matches the OIDC `job_workflow_ref` claim — the filename of the workflow containing the running job — against the configured publisher (`python-publish.yml`); invoking that workflow via a reusable `uses:` call is unsupported by PyPI and raised a "workflow misconfiguration" warning. `python-publish.yml` now triggers via `workflow_run` after `release.yml` completes (keeping it top-level so the publisher filename stays `python-publish.yml`), and `release.yml` no longer calls it reusably. All GitHub Actions were bumped off the deprecated Node 20 runtime: `actions/checkout@v7`, `actions/setup-python@v6`, `actions/upload-artifact@v7`, `actions/download-artifact@v8`, and `astral-sh/setup-uv@v8.2.0`.
21
+
22
+ [0.23.1]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.23.1
23
+
7
24
  ## [0.23.0] — 2026-06-21
8
25
 
9
26
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kryten-webqueue
3
- Version: 0.23.0
3
+ Version: 0.24.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
@@ -1009,6 +1009,46 @@ a.np-chip {
1009
1009
  text-transform: uppercase;
1010
1010
  }
1011
1011
 
1012
+ /* Chat-color live preview + readability badge */
1013
+ .color-preview {
1014
+ background: #111;
1015
+ border: 1px solid var(--border);
1016
+ border-radius: var(--radius);
1017
+ padding: 0.6rem 0.75rem;
1018
+ margin: 0 0 0.6rem;
1019
+ font-size: 0.95rem;
1020
+ line-height: 1.5;
1021
+ }
1022
+ .color-preview .cp-ts { color: #AAA; }
1023
+ .color-preview .cp-name { font-weight: 700; }
1024
+ .color-preview .cp-msg { /* color set inline to the chosen value */ }
1025
+ .contrast-badge {
1026
+ display: flex;
1027
+ align-items: flex-start;
1028
+ gap: 0.5rem;
1029
+ font-size: 0.82rem;
1030
+ padding: 0.5rem 0.6rem;
1031
+ border-radius: var(--radius);
1032
+ margin-bottom: 0.75rem;
1033
+ border: 1px solid var(--border);
1034
+ }
1035
+ .contrast-badge .cb-icon { flex-shrink: 0; line-height: 1.3; }
1036
+ .contrast-badge.cb-ok {
1037
+ background: rgba(0, 184, 148, 0.10);
1038
+ border-color: var(--success, #00b894);
1039
+ color: var(--success, #00b894);
1040
+ }
1041
+ .contrast-badge.cb-warn {
1042
+ background: rgba(253, 203, 110, 0.12);
1043
+ border-color: var(--warning, #fdcb6e);
1044
+ color: var(--warning, #fdcb6e);
1045
+ }
1046
+ .contrast-badge.cb-reject {
1047
+ background: rgba(225, 112, 85, 0.12);
1048
+ border-color: var(--danger, #e17055);
1049
+ color: var(--danger, #e17055);
1050
+ }
1051
+
1012
1052
  /* Transaction filter toggle */
1013
1053
  .tx-toggle {
1014
1054
  display: flex;
@@ -293,15 +293,92 @@ async function saveGreeting() {
293
293
  }
294
294
  }
295
295
 
296
+ // ── Chat-color readability scoring (mirrors kryten-economy contrast.py) ──
297
+ // The economy server is authoritative and re-checks on purchase; this gives
298
+ // instant local preview/feedback. Defaults match the economy config defaults.
299
+ const CONTRAST_BG = '#111111';
300
+ const CONTRAST_MIN = 30.0; // reject below this combined score
301
+ const CONTRAST_WARN = 40.0; // warn below this
302
+ const CHROMA_KNEE = 160; // green+blue at which the red penalty clears
303
+
304
+ function _hexRgb(h) {
305
+ let s = h.replace('#', '');
306
+ if (s.length === 3) s = s.split('').map(c => c + c).join('');
307
+ return [parseInt(s.slice(0,2),16), parseInt(s.slice(2,4),16), parseInt(s.slice(4,6),16)];
308
+ }
309
+ function _screenY(h) {
310
+ const [r,g,b] = _hexRgb(h);
311
+ return 0.2126729*Math.pow(r/255,2.4) + 0.7151522*Math.pow(g/255,2.4) + 0.0721750*Math.pow(b/255,2.4);
312
+ }
313
+ function _apcaLc(textHex, bgHex) {
314
+ let yt = _screenY(textHex), yb = _screenY(bgHex);
315
+ if (yt <= 0.022) yt += Math.pow(0.022 - yt, 1.414);
316
+ if (yb <= 0.022) yb += Math.pow(0.022 - yb, 1.414);
317
+ if (Math.abs(yb - yt) < 0.0005) return 0;
318
+ let out;
319
+ if (yb > yt) { const s = (Math.pow(yb,0.56)-Math.pow(yt,0.57))*1.14; out = s < 0.1 ? 0 : s - 0.027; }
320
+ else { const s = (Math.pow(yb,0.65)-Math.pow(yt,0.62))*1.14; out = s > -0.1 ? 0 : s + 0.027; }
321
+ return Math.abs(out * 100);
322
+ }
323
+ function _chromaFactor(h) {
324
+ const [, g, b] = _hexRgb(h);
325
+ return Math.min(g + b, CHROMA_KNEE) / CHROMA_KNEE;
326
+ }
327
+ function classifyColor(hex) {
328
+ const lc = _apcaLc(hex, CONTRAST_BG);
329
+ const score = Math.round(lc * _chromaFactor(hex) * 10) / 10;
330
+ let level;
331
+ if (score < CONTRAST_MIN) level = 'reject';
332
+ else if (score < CONTRAST_WARN) level = 'warn';
333
+ else level = 'ok';
334
+ return { score, level };
335
+ }
336
+
337
+ function updateColorPreview(hex) {
338
+ const msg = document.getElementById('cp-msg');
339
+ const sw = document.getElementById('color-input');
340
+ const badge = document.getElementById('contrast-badge');
341
+ const save = document.getElementById('color-save');
342
+ const valid = /^#[0-9A-Fa-f]{6}$/.test(hex);
343
+ if (msg) msg.style.color = valid ? hex : '#FFFFFF';
344
+ if (!valid || !badge || !save) return;
345
+ const { score, level } = classifyColor(hex);
346
+ if (level === 'reject') {
347
+ badge.className = 'contrast-badge cb-reject';
348
+ badge.innerHTML = '<span class="cb-icon">⛔</span><span>Too hard to read on the dark chat background. ' +
349
+ 'Try a lighter shade, or add some green/blue (e.g. pink instead of pure red).</span>';
350
+ save.disabled = true;
351
+ save.title = 'This color is too low-contrast to use';
352
+ } else if (level === 'warn') {
353
+ badge.className = 'contrast-badge cb-warn';
354
+ badge.innerHTML = '<span class="cb-icon">⚠️</span><span>Readable, but low-contrast — viewers with vision ' +
355
+ 'issues may struggle to read your chat lines.</span>';
356
+ save.disabled = false;
357
+ save.title = '';
358
+ } else {
359
+ badge.className = 'contrast-badge cb-ok';
360
+ badge.innerHTML = '<span class="cb-icon">✅</span><span>Good contrast on the chat background.</span>';
361
+ save.disabled = false;
362
+ save.title = '';
363
+ }
364
+ }
365
+
296
366
  function editColor() {
297
367
  if (!accountState) return;
298
368
  const sym = accountState.currency_symbol || 'Z';
299
369
  const cur = (accountState.vanity && accountState.vanity.custom_color) || '';
300
370
  const cost = (accountState.vanity_costs && accountState.vanity_costs.custom_color) || 0;
301
371
  const seed = /^#[0-9A-Fa-f]{6}$/.test(cur) ? cur : '#6C5CE7';
372
+ const uname = (accountState.username) || 'You';
302
373
  showModal(`
303
374
  <h3>Chat color</h3>
304
375
  <p class="muted">Pick a color for your chat messages. Cost: ${fmt(cost, sym)} per update.</p>
376
+ <div class="color-preview">
377
+ <span class="cp-ts">[12:34:56]</span>
378
+ <span class="cp-name" id="cp-name">${escapeHtml(uname)}:</span>
379
+ <span class="cp-msg" id="cp-msg">This is how your chat messages will look.</span>
380
+ </div>
381
+ <div class="contrast-badge cb-ok" id="contrast-badge"></div>
305
382
  <div class="color-picker-row">
306
383
  <input type="color" id="color-input" value="${seed}">
307
384
  <input type="text" id="color-hex" class="color-hex" value="${escapeHtml(cur || seed)}" maxlength="7" placeholder="#RRGGBB">
@@ -312,14 +389,25 @@ function editColor() {
312
389
  </div>`);
313
390
  const picker = document.getElementById('color-input');
314
391
  const hex = document.getElementById('color-hex');
315
- picker.addEventListener('input', () => { hex.value = picker.value.toUpperCase(); });
392
+ const normalize = (v) => {
393
+ v = v.trim();
394
+ if (!v.startsWith('#')) v = '#' + v;
395
+ return v.toUpperCase();
396
+ };
397
+ picker.addEventListener('input', () => {
398
+ hex.value = picker.value.toUpperCase();
399
+ updateColorPreview(picker.value.toUpperCase());
400
+ });
316
401
  hex.addEventListener('input', () => {
317
402
  const val = hex.value.trim();
318
403
  if (/^#?[0-9A-Fa-f]{6}$/.test(val)) {
319
- picker.value = (val.startsWith('#') ? '' : '#') + val;
404
+ const norm = normalize(val);
405
+ picker.value = norm;
406
+ updateColorPreview(norm);
320
407
  }
321
408
  });
322
409
  document.getElementById('color-save').addEventListener('click', saveColor);
410
+ updateColorPreview(normalize(cur || seed));
323
411
  }
324
412
 
325
413
  async function saveColor() {
@@ -329,6 +417,12 @@ async function saveColor() {
329
417
  return;
330
418
  }
331
419
  if (!value.startsWith('#')) value = '#' + value;
420
+ value = value.toUpperCase();
421
+ // Block locally on reject (the server enforces this too).
422
+ if (classifyColor(value).level === 'reject') {
423
+ showToast('That color is too low-contrast to use on the chat background', 'error');
424
+ return;
425
+ }
332
426
  const btn = document.getElementById('color-save');
333
427
  btn.disabled = true;
334
428
  let resp;
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryten-webqueue"
3
- version = "0.23.0"
3
+ version = "0.24.0"
4
4
  description = "Netflix/Tubi-style catalog browser and pay-to-play queue management for CyTube"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,75 +0,0 @@
1
- name: Publish Python Package to PyPI
2
-
3
- on:
4
- workflow_call:
5
- release:
6
- types: [published]
7
- push:
8
- tags:
9
- - 'kryten-webqueue-v*'
10
- - 'v*'
11
-
12
- permissions:
13
- contents: read
14
- id-token: write
15
-
16
- jobs:
17
- release-build:
18
- name: Build distribution packages
19
- runs-on: ubuntu-latest
20
-
21
- steps:
22
- - name: Checkout repository
23
- uses: actions/checkout@v4
24
-
25
- - name: Install uv
26
- uses: astral-sh/setup-uv@v4
27
- with:
28
- version: "latest"
29
-
30
- - name: Set up Python
31
- uses: actions/setup-python@v5
32
- with:
33
- python-version: "3.12"
34
-
35
- - name: Build release distributions
36
- run: |
37
- uv build
38
- echo "📦 Built packages:"
39
- ls -lh dist/
40
-
41
- - name: Upload distributions
42
- uses: actions/upload-artifact@v4
43
- with:
44
- name: release-dists
45
- path: dist/
46
-
47
- pypi-publish:
48
- name: Publish to PyPI
49
- runs-on: ubuntu-latest
50
- needs:
51
- - release-build
52
- environment:
53
- name: pypi
54
- permissions:
55
- id-token: write
56
-
57
- steps:
58
- - name: Retrieve release distributions
59
- uses: actions/download-artifact@v4
60
- with:
61
- name: release-dists
62
- path: dist/
63
-
64
- - name: Publish release distributions to PyPI
65
- uses: pypa/gh-action-pypi-publish@release/v1
66
- with:
67
- packages-dir: dist/
68
- skip-existing: true
69
- attestations: false
70
-
71
- - name: Success notification
72
- if: success()
73
- run: |
74
- echo "✅ Successfully published to PyPI!"
75
- echo "🔗 Package URL: https://pypi.org/project/kryten-webqueue/"