Skip to content

refactor: Code quality improvements (#385, #386, #395, #439)#525

Merged
filthyrake merged 3 commits intodevfrom
fix/code-quality-batch-385-386-395-439
Jan 27, 2026
Merged

refactor: Code quality improvements (#385, #386, #395, #439)#525
filthyrake merged 3 commits intodevfrom
fix/code-quality-batch-385-386-395-439

Conversation

@filthyrake
Copy link
Owner

Summary

This PR addresses 4 "good first issue" code quality improvements:

Changes

Issue #385 - Extract magic numbers to constants

Added named constants at the top of analytics_cache.py:

  • DEFAULT_CACHE_TTL_SECONDS = 60
  • DEFAULT_CACHE_MAX_SIZE = 1000
  • CACHE_EVICTION_DIVISOR = 10
  • REDIS_SOCKET_TIMEOUT = 5.0
  • REDIS_CONNECT_TIMEOUT = 5.0
  • REDIS_SCAN_BATCH_SIZE = 100

Issue #386 - Add return type hints

Added proper type hints to middleware dispatch methods:

  • RequestIDMiddleware.dispatch(request, call_next: Callable) -> Response
  • SecurityHeadersMiddleware.dispatch(request, call_next: Callable) -> Response

Issue #395 - Add error handling to start-transcription.sh

Updated script to match patterns in other startup scripts:

  • Added set -e and set -u for safer execution
  • Made virtual environment a hard requirement
  • Added validation for worker/transcription.py existence
  • Added check for whisper module installation

Issue #439 - Create slug validation helper

Added require_valid_slug(slug, resource_type) helper in common.py and replaced 10 duplicate validation patterns in public.py:

  • Video slug validation (7 occurrences)
  • Category slug validation (1 occurrence)
  • Tag slug validation (1 occurrence)
  • Playlist slug validation (2 occurrences)

Note: The embed endpoint at line 819 was intentionally left unchanged as it returns an HTML error page instead of raising an HTTPException.

Test plan

  • All analytics cache tests pass (47 tests)
  • Python syntax validation passes
  • Import verification passes
  • Manual testing of start-transcription.sh error conditions

Closes #385, #386, #395, #439

🤖 Generated with Claude Code

- Extract magic numbers to named constants in analytics_cache.py (#385)
  - DEFAULT_CACHE_TTL_SECONDS, DEFAULT_CACHE_MAX_SIZE
  - REDIS_SOCKET_TIMEOUT, REDIS_CONNECT_TIMEOUT, REDIS_SCAN_BATCH_SIZE
  - CACHE_EVICTION_DIVISOR

- Add return type hints to dispatch methods in common.py (#386)
  - RequestIDMiddleware.dispatch -> Response
  - SecurityHeadersMiddleware.dispatch -> Response

- Add error handling to start-transcription.sh (#395)
  - Add set -e and set -u for safer execution
  - Require virtual environment (fail if missing)
  - Validate transcription.py script exists
  - Verify whisper module is installed

- Create require_valid_slug() helper to eliminate repetition (#439)
  - Add helper function in common.py
  - Replace 10 duplicate validation patterns in public.py
  - Consistent error messages: "Invalid {resource_type} slug"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Clarity improvements (Conway):
- Rename CACHE_EVICTION_DIVISOR to CACHE_EVICTION_RATIO (0.10)
  for clearer intent - "evict 10%" is more intuitive than "divide by 10"
- Add rationale comments for REDIS_SOCKET_TIMEOUT and REDIS_SCAN_BATCH_SIZE
  explaining the tradeoffs (latency vs memory vs round-trips)
- Add comment explaining why start-transcription.sh changes to script dir

Reliability improvements (Margo):
- Add Redis reconnection with exponential backoff
  - _maybe_reconnect() attempts reconnection after transient failures
  - Backoff from 1s to 60s max prevents hammering failing Redis
  - Connection auto-recovers without requiring API restart
- Add timeout protection to bulk Redis operations
  - clear() and get_stats() now timeout after 5 seconds
  - Prevents blocking on large keyspaces
  - Logs warning with partial progress on timeout
- Mark connection as failed on operation errors to trigger reconnection
- Improve slug validation error messages with specific reasons:
  - "Missing {type} slug" for empty input
  - "path traversal not allowed" for .. attempts
  - "must be lowercase alphanumeric with hyphens" for format errors
- Shell script improvements:
  - Capture and display activation errors for debugging
  - Add BASH_SOURCE fallback for shell compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses four "good first issue" code quality improvements focusing on extracting magic numbers, adding type hints, improving shell script error handling, and eliminating code duplication. However, the PR includes significant scope creep in the analytics_cache.py changes that extends well beyond the stated goal of extracting magic numbers.

Changes:

  • Added named constants for cache TTL, sizes, and Redis connection parameters in analytics_cache.py
  • Added return type hints (-> Response) to middleware dispatch methods in api/common.py
  • Enhanced start-transcription.sh with error checking and validation (though with some implementation issues)
  • Created require_valid_slug() helper function and replaced 11 validation patterns across api/public.py

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

File Description
api/analytics_cache.py Extracted magic numbers to constants, but also added substantial new reconnection logic, timeouts, and error handling beyond issue scope
api/common.py Added return type hints to dispatch methods and created require_valid_slug() helper with duplicated validation logic
api/public.py Replaced 11 duplicate slug validation patterns with require_valid_slug() helper calls
start-transcription.sh Added set -e/set -u and validation checks, but uses incorrect command substitution patterns that differ from other startup scripts

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 34 to 39
import_error=$(python -c "from worker.transcription import main" 2>&1) || {
echo "Error: worker.transcription module not available for import"
echo "Details: $import_error"
echo "Ensure the package is installed: pip install -e ."
exit 1
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import validation pattern on lines 34-39 differs from the patterns used consistently in other startup scripts. Other scripts (start-admin.sh line 23, start-public.sh line 23, start-worker.sh line 29) use the more concise pattern with 2>/dev/null:

if ! python -c "from worker.transcription import main" 2>/dev/null; then
    echo "Error: Failed to import worker.transcription module"
    echo "Ensure the package is installed: pip install -e ."
    exit 1
fi

The current approach with command substitution and error capture is unnecessarily complex and deviates from the established convention in the codebase.

Copilot uses AI. Check for mistakes.
Comment on lines 42 to 47
whisper_error=$(python -c "import whisper" 2>&1) || {
echo "Error: whisper module not installed"
echo "Details: $whisper_error"
echo "Install with: pip install openai-whisper"
exit 1
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whisper module validation pattern on lines 42-47 differs from the patterns used consistently in other startup scripts. While checking for the whisper dependency is good, the error capture pattern is overly complex. Consider using the simpler pattern consistent with other scripts:

if ! python -c "import whisper" 2>/dev/null; then
    echo "Error: whisper module not installed"
    echo "Install with: pip install openai-whisper"
    exit 1
fi

This matches the validation pattern used in start-admin.sh line 23, start-public.sh line 23, and start-worker.sh line 29.

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +106
if not slug:
raise HTTPException(
status_code=400,
detail=f"Missing {resource_type} slug"
)
if '..' in slug:
raise HTTPException(
status_code=400,
detail=f"Invalid {resource_type} slug: path traversal not allowed"
)
if not SLUG_PATTERN.match(slug):
raise HTTPException(
status_code=400,
detail=f"Invalid {resource_type} slug: must be lowercase alphanumeric with hyphens"
)
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The require_valid_slug function duplicates validation logic that already exists in the validate_slug function (lines 54-75). This violates the DRY principle that this PR aims to improve. Consider refactoring to reuse the existing validate_slug function:

def require_valid_slug(slug: str, resource_type: str = "resource") -> None:
    """
    Validate slug or raise HTTPException with 400 status.
    
    Security: Prevents path traversal attacks by ensuring slug contains
    only safe characters (lowercase alphanumeric with hyphens).
    
    Args:
        slug: The slug string to validate
        resource_type: Type of resource for error message (e.g., "video", "category")
    
    Raises:
        HTTPException: 400 error if slug is invalid
    """
    if not validate_slug(slug):
        raise HTTPException(
            status_code=400,
            detail=f"Invalid {resource_type} slug"
        )

This approach provides a single source of truth for validation logic while still offering detailed error messages through the HTTPException.

Suggested change
if not slug:
raise HTTPException(
status_code=400,
detail=f"Missing {resource_type} slug"
)
if '..' in slug:
raise HTTPException(
status_code=400,
detail=f"Invalid {resource_type} slug: path traversal not allowed"
)
if not SLUG_PATTERN.match(slug):
raise HTTPException(
status_code=400,
detail=f"Invalid {resource_type} slug: must be lowercase alphanumeric with hyphens"
)
if not validate_slug(slug):
raise HTTPException(
status_code=400,
detail=f"Invalid {resource_type} slug"
)

Copilot uses AI. Check for mistakes.
Comment on lines +202 to +266
self._last_reconnect_attempt: Optional[float] = None
self._reconnect_backoff = REDIS_RECONNECT_MIN_INTERVAL

if enabled:
self._initialize_client()

def _initialize_client(self) -> None:
"""Initialize the Redis client."""
def _initialize_client(self) -> bool:
"""
Initialize the Redis client.

Returns:
True if connection successful, False otherwise.
"""
try:
import redis # Lazy import
self._client = redis.Redis.from_url(
self._redis_url,
socket_timeout=5.0,
socket_connect_timeout=5.0,
socket_timeout=REDIS_SOCKET_TIMEOUT,
socket_connect_timeout=REDIS_CONNECT_TIMEOUT,
decode_responses=True,
)
# Test connection
# Test connection (respects socket_timeout)
self._client.ping()
logger.info(f"Redis analytics cache connected: {self._redis_url.split('@')[-1]}")
self._connection_failed = False
self._reconnect_backoff = REDIS_RECONNECT_MIN_INTERVAL
return True
except Exception as e:
# Catch broad exceptions to avoid hard dependency on redis
logger.warning(f"Redis analytics cache connection failed: {e}")
self._client = None
self._connection_failed = True
return False

def _maybe_reconnect(self) -> bool:
"""
Attempt reconnection if enough time has passed since last attempt.

Uses exponential backoff to avoid hammering a failing Redis server.

Returns:
True if connected (either already or after reconnect), False otherwise.
"""
if not self._connection_failed:
return self._client is not None

now = time.time()
if self._last_reconnect_attempt is not None:
elapsed = now - self._last_reconnect_attempt
if elapsed < self._reconnect_backoff:
return False

self._last_reconnect_attempt = now
logger.info(f"Attempting Redis reconnection (backoff: {self._reconnect_backoff}s)")

if self._initialize_client():
logger.info("Redis analytics cache reconnected successfully")
return True

# Increase backoff for next attempt (exponential with cap)
self._reconnect_backoff = min(
self._reconnect_backoff * 2,
REDIS_RECONNECT_MAX_INTERVAL
)
return False
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR introduces substantial new functionality beyond the scope of issue #385 (extracting magic numbers). The additions include:

  1. New reconnection logic with exponential backoff (_maybe_reconnect method, lines 236-266)
  2. New instance variables (_last_reconnect_attempt, _reconnect_backoff, lines 202-203)
  3. Modified _initialize_client to return a boolean (line 208)
  4. Changes to _safe_redis_call to trigger reconnection (lines 292-295)
  5. Timeout logic in clear() and get_stats() methods (lines 355-361, 408-414)
  6. Connection failure tracking modifications (lines 226-227, 301-302)

These changes introduce complexity and risk that go well beyond a simple refactoring to extract magic numbers. While the changes may be beneficial, they should be in a separate PR with proper testing and review, as they fundamentally change the Redis cache's behavior and error handling.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +36
REDIS_BULK_OPERATION_TIMEOUT = 5.0
# Reconnection settings for transient failures
REDIS_RECONNECT_MIN_INTERVAL = 1.0 # Minimum seconds between reconnection attempts
REDIS_RECONNECT_MAX_INTERVAL = 60.0 # Maximum backoff interval
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constants REDIS_RECONNECT_MIN_INTERVAL, REDIS_RECONNECT_MAX_INTERVAL, and REDIS_BULK_OPERATION_TIMEOUT are introduced to support new functionality (reconnection logic and operation timeouts) that was not part of issue #385. Issue #385 specifically requested extracting existing magic numbers (5.0, 100, 10, 60, 1000) to constants, but these constants support entirely new features added in this PR.

These new constants and the features they support should be in a separate PR focused on adding reconnection and timeout capabilities.

Copilot uses AI. Check for mistakes.
# Cache configuration constants
DEFAULT_CACHE_TTL_SECONDS = 60 # Default time-to-live for cache entries
DEFAULT_CACHE_MAX_SIZE = 1000 # Maximum entries before triggering eviction
CACHE_EVICTION_RATIO = 0.10 # Evict 10% of entries when cache is full
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant name CACHE_EVICTION_RATIO deviates from the suggested name in issue #385, which recommended CACHE_EVICTION_DIVISOR. While CACHE_EVICTION_RATIO = 0.10 is mathematically equivalent to the original // 10 calculation, the issue specifically suggested CACHE_EVICTION_DIVISOR = 10 with a comment explaining "Evict 1/10th of cache when full".

The current implementation uses int(len(items) * CACHE_EVICTION_RATIO) while the issue suggested keeping the divisor pattern. Consider either:

  1. Using CACHE_EVICTION_DIVISOR = 10 with the calculation len(items) // CACHE_EVICTION_DIVISOR
  2. If preferring the ratio approach, update the constant name to clearly indicate it's a percentage or ratio

Copilot uses AI. Check for mistakes.
# Validate slug format (security: prevents log injection, ensures valid input)
if not validate_slug(slug):
raise HTTPException(status_code=400, detail="Invalid video slug")
require_valid_slug(slug, "video")
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description states "replaced 10 duplicate validation patterns" but the actual count in the code is 11 occurrences of require_valid_slug (lines 883, 1673, 1797, 1863, 1982, 2277, 2344, 2477, 2548, 3331, 3630). While the breakdown by resource type matches the description (7 video, 1 category, 1 tag, 2 playlist = 11 total), the summary says "10 duplicate validation patterns".

Copilot uses AI. Check for mistakes.
Comment on lines 19 to 23
# Activate virtual environment with error capture
activation_output=$(source venv/bin/activate 2>&1) || {
echo "Error: Failed to activate virtual environment"
echo "Details: $activation_output"
echo "Check permissions and virtual environment integrity"
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The activation output capture pattern on line 20 is problematic because source is a shell builtin that cannot be captured in a command substitution when used with set -e. The command substitution creates a subshell where source will fail silently. This pattern differs from other startup scripts (start-admin.sh, start-public.sh, start-worker.sh) which use the simpler and more reliable pattern:

source venv/bin/activate || {
    echo "Error: Failed to activate virtual environment"
    exit 1
}

This simpler pattern is more consistent with the codebase conventions and actually works correctly with set -e.

Suggested change
# Activate virtual environment with error capture
activation_output=$(source venv/bin/activate 2>&1) || {
echo "Error: Failed to activate virtual environment"
echo "Details: $activation_output"
echo "Check permissions and virtual environment integrity"
# Activate virtual environment
source venv/bin/activate || {
echo "Error: Failed to activate virtual environment"

Copilot uses AI. Check for mistakes.
Shell script changes:
- Revert to simpler patterns matching other startup scripts
- Use `source ... || {` instead of command substitution for activation
- Use `if ! python -c ... 2>/dev/null; then` for module checks
- Remove BASH_SOURCE fallback (all scripts use bash explicitly)

Documentation:
- Add note explaining intentional logic duplication in require_valid_slug()
  for providing specific error messages (missing vs path traversal vs format)

Copilot feedback addressed:
- [Fixed] Shell script patterns now match start-worker.sh exactly
- [Intentional] require_valid_slug() duplication: provides better UX with
  specific error messages per Margo's reliability review
- [Intentional] Reconnection/timeout logic: addresses critical reliability
  issues identified by Margo (Redis never recovering after failure)
- [Intentional] CACHE_EVICTION_RATIO naming: Conway specifically recommended
  this for clarity over the original DIVISOR suggestion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@filthyrake
Copy link
Owner Author

Response to Copilot Review

Thanks for the thorough review! Here's how I've addressed each point:

Fixed Issues

Feedback Resolution
Shell script patterns differ from other scripts ✅ Fixed - now matches start-worker.sh exactly
Command substitution problematic with set -e ✅ Fixed - using source ... || { and if ! ... 2>/dev/null
PR says "10 patterns" but actually 11 ✅ Acknowledged - the breakdown (7 video, 1 category, 1 tag, 2 playlist) totals 11

Intentional Design Decisions

1. require_valid_slug() duplicates validation logic

This is intentional per Margo's reliability review. The function provides specific error messages for each failure case:

  • "Missing video slug" - empty input
  • "path traversal not allowed" - security attempt
  • "must be lowercase alphanumeric with hyphens" - format error

A generic "Invalid slug" message doesn't help API consumers fix their request. Added a docstring note explaining this design decision.

2. Scope expansion (reconnection, timeouts)

The user explicitly requested we address Margo's reliability review, which identified critical issues:

  • Redis cache never recovers after transient failures (requires API restart)
  • SCAN operations can block indefinitely on large keyspaces

These fixes prevent production outages and are worth the scope expansion.

3. CACHE_EVICTION_RATIO vs CACHE_EVICTION_DIVISOR

Conway's clarity review specifically recommended this change:

"evict 10%" is more intuitive than "divide by 10"

The ratio approach (int(len * 0.10)) is clearer than divisor (len // 10). The issue suggestion was a starting point, not a requirement.

@filthyrake filthyrake merged commit 574c6da into dev Jan 27, 2026
2 of 3 checks passed
@filthyrake filthyrake deleted the fix/code-quality-batch-385-386-395-439 branch January 27, 2026 14:03
filthyrake added a commit that referenced this pull request Jan 30, 2026
* ci: bump actions/setup-node from 4 to 6

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* ci: bump actions/setup-node from 4 to 6

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* ci: bump actions/upload-artifact from 4 to 6

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Add API versioning and OpenAPI documentation improvements (Issue #218) (#505)

* Add API versioning and OpenAPI documentation improvements (Issue #218)

- Add versioned API routes with /api/v1 prefix across all three APIs
- Create api/versioning.py module with utilities for:
  - Versioned router creation
  - Deprecation header support
  - OpenAPI schema customization
  - Version header middleware
- Add configuration options in config.py:
  - API_VERSION, API_SUPPORTED_VERSIONS
  - API_DEPRECATION_NOTICE, API_DEPRECATION_SUNSET
  - API_INCLUDE_LEGACY_ROUTES (backwards compatibility)
  - OPENAPI_* settings for documentation customization
- Refactor public, admin, and worker APIs to use APIRouter
- Add enhanced OpenAPI endpoint descriptions and summaries
- Maintain backwards compatibility with legacy /api routes

Closes #218

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add security and reliability fixes to API versioning

Addresses feedback from code, security, and reliability reviews:

Security fixes:
- Add validate_config() with regex validation for API_VERSION format
- Add sanitize_header_value() to prevent CRLF header injection
- Validate all API_SUPPORTED_VERSIONS entries match expected format

Reliability fixes:
- Add error handling to VersionHeaderMiddleware.dispatch()
- Add error boundary to configure_openapi_schema() with fallback
- Add error handling to DeprecationHeadersRoute custom handler
- Fail fast on configuration errors at module load

Code quality:
- Remove unused datetime import
- Remove unused RESPONSE_EXAMPLES dict
- Update .env.example with new API versioning configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add GHSA-w853-jp5j-5j7f to security scan ignore list

The filelock TOCTOU vulnerability (CVE-2025-68146) fix requires
Python 3.10+, but CI runs on Python 3.9. The container images
use Python 3.11+ where the patched version is installed.

This is the same class of vulnerability as the already-ignored
GHSA-qmgc-5h2g-mvrw, just with a different advisory ID.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix jaraco-context version in container build

Add --upgrade step after pip install to ensure security-patched
packages (filelock, jaraco-context) are at their required versions
even if transitive dependencies initially resolve to older versions.

Fixes Trivy container scan failure for GHSA-58pv-8j8x-9vj2.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Use pip constraints file for security patches in container

Replace --upgrade approach with -c constraints.txt which forces
pip to respect minimum versions during dependency resolution.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Address Copilot review feedback

- Fix Link header to use /docs (FastAPI's default docs location)
- Clarify sunset date format comment in .env.example
- Add comprehensive tests for API versioning (18 tests covering
  version headers, versioned endpoints, legacy routes, config
  validation, header sanitization, and OpenAPI schema)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add runtime stage pip upgrade for security patches

The builder stage installs correct versions, but dnf packages in
runtime stage may have old transitive dependencies. Add explicit
pip upgrade after COPY to ensure security-patched versions are used.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Ignore setuptools vendored jaraco.context in Trivy scan

setuptools 80.9.0 vendors jaraco.context 5.3.0 internally. This
vendored copy is used only by setuptools for its own operations
and is not exposed to user input, making the path traversal
vulnerability (GHSA-58pv-8j8x-9vj2) unexploitable in this context.

The main jaraco-context package is correctly upgraded to 6.1.0.

Added .trivyignore file to skip this false positive in container
image scans.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(playback): Implement Up Next autoplay feature (Issue #211) (#506)

* feat(playback): Implement Up Next autoplay feature (Issue #211)

Add autoplay and "Up Next" functionality that automatically plays
the next recommended video when the current video ends.

Backend:
- Add playback configuration settings (VLOG_AUTOPLAY_ENABLED,
  VLOG_UPNEXT_ENABLED, VLOG_AUTOPLAY_COUNTDOWN_SECONDS)
- Create GET /api/config/playback endpoint for frontend config
- Create GET /api/videos/{slug}/next endpoint for single next video
- Add settings to KNOWN_SETTINGS and ENV_VAR_MAPPING for database
  settings migration support

Frontend:
- Add playback config and next video loading in watch.js
- Implement countdown overlay UI with cancel/play now buttons
- Add autoplay toggle checkbox (persisted to localStorage via
  VLogUtils.preferences)
- Hook video 'ended' event to trigger Up Next countdown
- Add responsive CSS for Up Next overlay

Features:
- Configurable countdown duration (default 10 seconds)
- User preference to enable/disable autoplay
- Global admin setting to enable/disable feature
- Cancel button to stop countdown and stay on current video
- Play Now button to skip countdown
- Fallback to related videos algorithm for next video selection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(playback): Address specialist review feedback (Issue #211)

Frontend reliability fixes:
- Add try/catch around localStorage in toggleAutoplay()
- Validate nextVideo._href before navigation
- Rename playNextVideo() to navigateToNextVideo() for clarity
- Add AbortController to loadNextVideo() for request cancellation

Backend improvements:
- Change settings fetch failure logging from DEBUG to WARNING
- Refactor get_next_video to share code with get_related_videos (DRY)
- Parallelize tier queries with asyncio.gather() for limit=1
- Increase cache TTL from 30s to 300s to reduce DB load

Tests:
- Add comprehensive unit tests for /api/v1/videos/{slug}/next endpoint
- Add unit tests for /api/config/playback endpoint
- Test invalid slug patterns, deleted videos, null responses

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(playback): Address Copilot review feedback (Issue #211)

Frontend fixes:
- Clear existing countdown in startUpNextCountdown to prevent duplicate timers
- Add @click.prevent on video link to avoid double navigation
- Add keyboard accessibility: focus on Cancel button, Escape key handler
- Add accessibility attrs: role="dialog", aria-live, aria-labelledby, sr-only description
- Use @click.prevent on autoplay checkbox to prevent flickering on localStorage errors
- Validate slug from API with SLUG_PATTERN before constructing URLs
- Use encodeURIComponent when building next video URL

Backend fixes:
- Make VIDEO_LIST_CACHE_TTL configurable via environment variable

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(live): Implement live streaming via HTTP segment push (#507)

* feat(live): Implement live streaming via HTTP segment push

Add live streaming capability without requiring RTMP/SRT infrastructure.
FFmpeg clients encode locally into HLS/CMAF segments and push via HTTP PUT.

Features:
- Stream key authentication with argon2id hashing
- HTTP segment push endpoints (PUT init.mp4, PUT seg_NNNN.m4s)
- Dynamic HLS playlist generation written to disk
- DVR window support with background cleanup
- Stale stream detection with grace period
- Automatic VOD recording on stream end (hardlink or copy segments)
- Rate limiting (global per-IP and segment-specific)
- Path containment verification for defense-in-depth

Admin API:
- POST /api/v1/live/streams - Create stream with stream key
- GET /api/v1/live/streams - List streams
- GET/PATCH/DELETE /api/v1/live/streams/{slug}
- POST /api/v1/live/streams/{slug}/regenerate-key
- POST /api/v1/live/streams/{slug}/end

Public API:
- GET /api/v1/live/streams - List active streams
- GET /live/{slug}/master.m3u8 - Master playlist
- GET /live/{slug}/{quality}/stream.m3u8 - Variant playlist

Ingest API:
- PUT /api/live/ingest/{slug}/{quality}/init.mp4
- PUT /api/live/ingest/{slug}/{quality}/seg_NNNN.m4s
- GET /api/live/ingest/{slug}/status

Includes vlog-live-push.sh script for secure FFmpeg streaming
with stream key passed via environment variable.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(live): Address Copilot review feedback

- Fix unused imports in live_ingest.py, live_playlist.py, live_tasks.py, live_vod.py
- Add comments to silent except clauses explaining why they're silent
- Replace lambda capture patterns with functools.partial for clarity
- Reduce upload timeout from 720s to 600s
- Fix VOD duration calculation (was using seq*duration, now sums durations)
- Implement VOD recording in stale stream detection
- Add playlist updates after successful segment upload
- Update stream status to 'live' on init segment upload
- Fix path containment to use is_relative_to (Python 3.9+) with fallback
- Improve duplicate detection to use IntegrityError instead of generic Exception
- Add fire-and-forget task error callback in admin.py VOD recording
- Fix shell script INPUT_OPTS to use bash arrays for proper quoting
- Simplify bufsize calculation in shell script
- Fix playlist DVR window limiting logic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(theme): Add UI theme customization and branding (Issue #214) (#508)

* feat(theme): Add UI theme customization and branding (Issue #214)

Implement comprehensive theme and branding customization features:

Backend:
- Add branding, theme, and layout settings to KNOWN_SETTINGS
- Create public API endpoint GET /api/v1/config/theme for theme config
- Add branding endpoints for logo/favicon upload in admin API
- Create public endpoints for serving logo and favicon

Frontend (Public):
- Add theme.js loader for dynamic CSS variable injection
- Update index.html to use dynamic site name and logo
- Update footer to support custom footer text and links
- Add logo image CSS styles

Frontend (Admin):
- Add Branding tab to Settings page
- Implement logo and favicon upload with progress tracking
- Add site name and footer text editing
- Create branding API client and store methods

Settings added:
- branding.site_name, branding.logo_path, branding.favicon_path
- branding.footer_text, branding.footer_links
- theme.primary_color, theme.secondary_color, theme.accent_color
- theme.mode (light/dark/auto), theme.custom_css
- layout.homepage_style, layout.videos_per_page, layout.grid_columns
- layout.show_sidebar, layout.show_related_videos

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Address security vulnerabilities in branding feature

Fixes identified by security review:

- Add path traversal protection to all file operations using validate_safe_path()
- Implement SVG sanitization for logo/favicon uploads to prevent XSS
- Add X-Content-Type-Options: nosniff header to all file responses
- Unify cache invalidation with asyncio.Lock to prevent race conditions
- Restrict custom CSS to CSS variables only via sanitize_custom_css()
- Add URL validation for footer links to block javascript: URLs
- Add 5-second fetch timeout to theme.js to prevent hanging

Issue #214

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address Copilot review comments on PR #508

- Fix :root regex pattern (was only matching bare colon)
- Remove redundant asyncio and re imports
- Add try/except around temp file cleanup to handle NameError
- Fix dictionary indentation in exception handler defaults
- Add aria-labels to delete buttons for accessibility
- Add for/id associations between labels and file inputs
- Update placeholder to remove hardcoded year
- Add reset_theme_settings_cache() to public.py
- Call cache reset when branding/theme settings are updated
- Invalidate public theme cache on all branding changes

Issue #214

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix(admin): Resolve CSP violations with constructable stylesheets (#509)

* fix(admin): Resolve CSP violations with constructable stylesheets

Migrate all web components to use constructable stylesheets instead of
inline <style> tags, which were being blocked by the strict Content
Security Policy (style-src 'self').

Changes:
- Migrate 18 web components to use adoptedStyleSheets API
- Add CSP-compatible navigation methods for Alpine.js
- Fix Alpine CSP parser errors for multi-statement @click expressions
- Update SettingsTab type to include branding and custom_fields tabs
- Add helper methods for CSP-compatible template expressions

Components migrated:
- vlog-alert, vlog-alert-container, vlog-badge, vlog-button
- vlog-card, vlog-dropzone, vlog-empty-state, vlog-filter
- vlog-hamburger, vlog-input, vlog-nav-drawer, vlog-search
- vlog-skeleton, vlog-tab-button, vlog-tab-panel, vlog-table
- vlog-tabs, vlog-video-card

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(admin): Address review feedback for CSP compliance PR

Changes based on code reviewer feedback:

1. Modal Component:
   - Migrate vlog-modal.ts to constructable stylesheets (CSP-compliant)
   - Replace inline body.style.overflow with classList.add/remove('modal-open')
   - Add .modal-open class to tokens.css for scroll locking

2. Navigation Methods (per Conway's clarity feedback):
   - Rename switchToWorkers -> openWorkersTab (clearer intent)
   - Rename switchToAnalytics -> openAnalyticsTab
   - Rename switchToSettings -> openSettingsTab
   - Rename switchToSettingsBranding -> openBrandingSettings
   - Rename switchToSettingsCustomFields -> openCustomFieldsSettings
   - Add comprehensive section header explaining CSP restrictions

3. Index.html Updates:
   - Use new navigation method names
   - Use tabClass() and settingsTabClass() helpers for CSP compliance
   - Remove semicolons from dropdown @click handlers

4. Type Updates:
   - Update SettingsTab type to include 'branding' and 'custom_fields'

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(admin): Complete CSP compliance for Alpine.js expressions

- Replace arrow functions with store helper methods:
  - hasActiveWorkers(), hasGlobalCustomFields(), getGlobalCustomFields()
  - hasCategoryCustomFields(), getCategoryCustomFields()
  - hasNullCategoryCustomFields(), getNullCategoryCustomFields()
  - handleFilesSelected() for dropzone events

- Replace regex literals with helper methods (CSP parser doesn't support /pattern/):
  - formatCategoryName(), formatCategoryTitle(), formatSettingLabel()

- Replace inline style attributes with CSS classes:
  - Add .w-pct-{0-100} percentage width classes
  - Add .grid-cols-5-minmax, .min-w-0, .aspect-16-9, .aspect-content utilities
  - Remove all style="" attributes from index.html

- Fix settings tab button styling consistency:
  - Update settingsTabClass() to match dynamic category buttons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(auth): Add multi-user authentication with RBAC (Issue #200) (#510)

* feat(auth): Add multi-user authentication with RBAC (Issue #200)

Implements comprehensive user authentication replacing the legacy single
admin secret with proper user accounts, sessions, and role-based access
control.

Key features:
- User accounts with username/email and argon2id password hashing
- Role-based access control (Admin, Editor, Viewer)
- Session-based browser auth with HTTP-only cookies
- Refresh token rotation with theft detection
- API keys for programmatic access
- OIDC integration for self-hosted identity providers
- Invite-only registration system
- Account lockout after failed login attempts
- CLI commands for user management (vlog auth migrate, create-admin, etc.)

Backend:
- New api/auth/ module with sessions, permissions, endpoints
- Database migration for users, sessions, api_keys, invites tables
- Session cleanup scheduled task for expired sessions
- Video ownership tracking (owner_id on videos table)

Frontend:
- Redesigned login page with username/password form
- User profile management (password change, API keys, sessions)
- User management page for admins (create, edit, disable users)
- Invite management for user registration

Testing & Documentation:
- 50 comprehensive tests covering auth flows
- Full documentation in docs/AUTHENTICATION.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(auth): Address comprehensive review feedback for authentication system

Security fixes (Bruce):
- Remove password reset token logging in endpoints.py
- Remove token from force password reset API response in users.py
- Add Depends() to OIDC link/unlink endpoints
- Implement OIDC nonce validation to prevent replay attacks
- Add SESSION_SECRET_KEY startup validation with minimum length

Performance fixes (Brendan):
- Use SHA-256 for session/API token hashing instead of argon2id
- Add token_prefix columns for O(1) indexed database lookup
- Support legacy argon2id hashes for backward compatibility

Reliability fixes (Margo):
- Add asyncio.Lock for thread-safe circuit breaker operations
- Wrap session refresh and invite acceptance in database transactions
- Extend cleanup task to purge expired OIDC states and password reset tokens

Code clarity improvements (Conway):
- Update migration to include token_prefix and last_used_at columns
- Add is_sha256_hash() helper for hash type detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(auth): Add VLOG_SESSION_SECRET_KEY to test configuration

Required after adding startup validation for session secret key.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(auth): Remove unused imports and fix code style

Address Copilot review comments:
- Remove unused imports across auth modules (Query, Depends, Set, etc.)
- Remove unused config imports (USER_REFRESH_TOKEN_EXPIRY_DAYS, etc.)
- Remove unused variable assignment in cli/main.py
- Fix test import style: use module reference pattern consistently
  instead of mixed 'import' and 'from import' for sessions module

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(test): Use non-secret placeholder for test session key

Replace string literal with generated value to avoid GitGuardian
false positive on test credentials.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(auth): Add setup wizard for first-time admin creation

Adds a web-based setup wizard that appears when no users exist,
allowing the first admin account to be created via the UI instead
of requiring CLI access.

Backend:
- GET /api/v1/auth/setup - Check if setup is needed
- POST /api/v1/auth/setup - Create initial admin (only works when
  no users exist, returns 403 afterward)

Frontend:
- Setup wizard modal with username, email, password fields
- Auto-login after successful account creation
- Integrated with existing auth flow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(auth): Resolve session persistence and login issues

- Remove legacy /auth/login and /auth/check endpoints in admin.py that
  were overriding the new user-based auth endpoints
- Update AuthCheckResponse to return nested user object with permissions,
  auth_required, auth_mode, and OIDC settings
- Update LoginRequest to accept username_or_email for flexible login
- Fix login handler to look up users by either username or email
- Add missing 'os' import in auth/endpoints.py
- Add failed_login_attempts=0 to setup wizard user creation
- Fix Alpine.js CSP compatibility: replace optional chaining (?.) with
  CSP-safe expressions throughout index.html
- Use <template x-if> instead of x-show for user-dependent UI sections
  to prevent evaluation errors when currentUser is null
- Fix TypeScript error: use undefined instead of null for optional fields

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(public-ui): Resolve Alpine.js CSP parser errors

Replace complex expressions with precomputed values for CSP compatibility:
- Add _showError, _showContent, _showEmptyState to category.js and tag.js
- Add _showFeaturedSection to home.js
- Add updateContentUIState() method to update UI state flags
- Update HTML files to use precomputed boolean values

The Alpine.js CSP build cannot parse negation (!), optional chaining (?.),
or complex boolean expressions (&&, ||) in x-show directives.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(auth): Address code review findings from specialized agents (#511)

* fix(auth): Address code review findings from specialized agents

Backend fixes (api/auth/endpoints.py):
- Move sqlalchemy or_ and func imports to top-level (was inline in function)
- Add case-insensitive username/email matching with func.lower()
- Hash identifier in security logs to avoid PII exposure (GDPR compliance)
- Cache OIDC env vars at module level to avoid repeated lookups
- Move validate_session_token and get_role_permissions imports to top-level

Frontend fixes:
- Fix broken HTML div nesting in admin/index.html (extra closing tag)
- Fix _showFeaturedSection race condition in home.js init()

Addresses findings from: Gafton, Bruce, Margo, Brendan, Conway reviews.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(auth): Address reliability findings from Margo's review

Reliability improvements:

1. Login flow atomicity (endpoints.py):
   - Move _reset_failed_login() to AFTER session creation succeeds
   - Ensures atomic state: if session creation fails, login count stays

2. Setup wizard TOCTOU fix (endpoints.py):
   - Wrap check-and-insert in database transaction
   - Handle unique constraint violations from concurrent requests
   - Session creation now within same transaction

3. Session limit race condition (sessions.py):
   - Use FOR UPDATE row locking in _enforce_session_limit()
   - Wrap session creation in transaction with limit enforcement
   - Prevents concurrent logins from exceeding limit

4. Database error handling (endpoints.py):
   - Add try/except around user lookup in login
   - Add try/except around setup status check
   - Return 503 with helpful message on database errors

5. Bare except:pass fix (sessions.py):
   - Add proper logging for last_used_at update failures
   - Helps detect database health issues in monitoring

6. Lockout state cleanup (endpoints.py):
   - Clear expired locked_until when user attempts login
   - Reset failed_login_attempts on expired lockout
   - Non-blocking with error logging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Update documentation for multi-user authentication features (#512)

* docs: Update documentation for multi-user authentication features

Updates documentation to reflect recent major features:
- Multi-user authentication with RBAC (Issue #200)
- Setup wizard for first-time admin creation
- API key management
- User invite system
- OIDC/SSO integration

Changes by document:
- API.md: Add comprehensive auth API section (setup wizard, login,
  user management, API keys, invites)
- AUTHENTICATION.md: Add setup wizard documentation, update migration
  guide for new multi-user system
- ADMIN_UI_GUIDE.md: Add Users tab section, update login/setup docs
- CONFIGURATION.md: Add VLOG_SESSION_SECRET_KEY and related auth
  settings, deprecate VLOG_ADMIN_API_SECRET
- DEPLOYMENT.md: Update production checklist for auth requirements
- TROUBLESHOOTING.md: Add comprehensive auth troubleshooting section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Fix critical accuracy issues found in code review

Addresses blockers identified by gafton-distinguished-engineer review:

1. Login response schema: Fixed to flat object (not nested under "user")
2. Profile update: Removed email field, only display_name and avatar_url
   are accepted
3. Environment variables: Fixed names to match actual implementation
   - VLOG_REFRESH_EXPIRY_DAYS (not VLOG_REFRESH_TOKEN_EXPIRY_DAYS)
   - VLOG_LOCKOUT_THRESHOLD (not VLOG_LOGIN_LOCKOUT_THRESHOLD)
   - VLOG_LOCKOUT_DURATION_MINUTES (not VLOG_LOGIN_LOCKOUT_DURATION_MINUTES)
4. Password reset expiry: Fixed default to 1 hour (not 24 hours)
5. Database settings table: Clarified these are env vars, not DB settings
6. Invite URL: Clarified it returns relative path, not absolute URL
7. Column name: Fixed failed_login_count to failed_login_attempts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* Chore(deps-dev): Bump lodash-es from 4.17.22 to 4.17.23 (#513)

Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.22 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits/4.17.23)

---
updated-dependencies:
- dependency-name: lodash-es
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Chore(deps-dev): Bump lodash from 4.17.21 to 4.17.23 (#514)

Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(logging): Add structured JSON logging with request context (#208) (#515)

* feat(logging): Add structured JSON logging with request context (#208)

Add centralized logging configuration with JSON output for production
and text format for development. Request context (request_id, client_ip,
user_agent) is automatically injected into all log messages.

Key features:
- python-json-logger for structured output
- SafeJSONEncoder handles non-serializable objects
- User-Agent sanitization prevents log injection
- try/finally ensures context cleanup on async exceptions
- Log file rotation with 0600 permissions for security
- Module-specific log level overrides via VLOG_LOG_LEVELS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address PR review feedback and security audit

- Fix import ordering to satisfy ruff/isort (E402, I001)
- Make setup_logging() idempotent with _logging_configured flag
- Close handlers before removing to prevent file descriptor leaks
- Add SecureRotatingFileHandler for 0o600 permissions on rotation
- Use record.getMessage() instead of str(record.msg) in fallback
- Add comprehensive tests for sanitize_user_agent and logging
- Add VLOG_LOG_* env vars to .env.example
- Ignore GHSA-7gcm-g887-7qv7 (protobuf DoS, no fix available)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Add protobuf GHSA-7gcm-g887-7qv7 to pip-audit ignores

No patched version available for this DoS vulnerability in protobuf's
json_format.ParseDict(). VLog doesn't use this function - protobuf is
a transitive dependency of faster-whisper.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Pin wheel>=0.46.2 and add CVE ID to trivyignore

- Add wheel>=0.46.2 to fix CVE-2026-24049 privilege escalation
- Add CVE-2026-0994 to trivyignore (Trivy finds protobuf by CVE, not GHSA)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Add wheel>=0.46.2 to Docker security patches

Add wheel to the security-patched packages in both Dockerfiles to fix
CVE-2026-24049 privilege escalation vulnerability detected by Trivy.

The wheel package is installed by pip as a build dependency. The existing
requirements.txt change doesn't affect the Docker image because it uses
explicit package installation during build.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Ignore wheel CVE in setuptools vendored copy

Add CVE-2026-24049/GHSA-8rrh-rw8j-w5fx to .trivyignore for the vendored
wheel inside setuptools. The vulnerability is in wheel.cli.unpack which
we don't use on untrusted files. The standalone wheel package is already
upgraded to 0.46.2 - this ignore is only for the vendored copy that
setuptools bundles internally.

Similar to the existing jaraco.context ignore - waiting for setuptools
upstream to update their vendored dependencies.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(embed): Add video embed functionality (#210) (#516)

* feat(embed): Add video embed functionality (#210)

Add the ability to generate iframe embed codes for videos, allowing them
to be embedded on external websites with a minimal, responsive player.

Security features:
- Domain whitelist with 'self' default (frame-ancestors CSP)
- Rate limiting: 500/min per IP for embed routes
- Query parameter validation (start, autoplay, controls)
- View count protection: 5+ seconds minimum playback

Backend:
- Add /embed/{slug} route with CSP headers
- Add /api/v1/videos/{slug}/embed-code endpoint
- Add embed configuration to config.py and settings_service.py
- Add EmbedCodeResponse schema

Frontend:
- Create embed.html minimal player template
- Create embed-error.html for graceful error display
- Create embed.js with analytics tracking (source='embed')
- Create embed.css with responsive styles
- Extend share modal with Link/Embed tabs
- Add start time input and autoplay checkbox options
- Add live embed code generation and copy functionality

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(embed): Address code review findings from specialized agents (#210)

Security fixes (Bruce):
- Add validate_slug() to embed routes to prevent log injection
- Skip X-Frame-Options for /embed/ routes (CSP takes precedence)
- Add CSP domain validation to prevent header injection
- Use crypto.randomUUID() for secure session UUID generation

Reliability fixes (Margo):
- Add fetchWithTimeout() with configurable timeouts for all network requests
- Add retry logic with exponential backoff for video data fetch
- Add circuit breaker for analytics heartbeats (max 3 failures)
- Add proper cleanup with EmbedPlayer.destroy() and EmbedAnalytics.destroy()
- Handle pagehide event for mobile browser cleanup

Performance fixes (Brendan):
- Debounce embed code input handler (300ms) to reduce DOM thrashing
- Add proper interval cleanup to prevent memory leaks

Clarity fixes (Conway):
- Rename shareLinkTabContent/shareEmbedTabContent to shareTabLink/shareTabEmbed
- Rename parseTimeInput() to convertTimeFormatToSeconds() with JSDoc
- Extract _is_video_embeddable() and _build_embed_error_response() helpers
- Add start time validation with 24-hour maximum cap
- Add picture-in-picture to client-side iframe generation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(embed): Address Copilot review comments (#210)

- Use DB-backed settings in build_embed_csp_frame_ancestors() instead of env vars
- Fix crypto check to use typeof to prevent ReferenceError
- Fix getCurrentQuality() to handle HLS.js with currentLevelIndex
- Fix quality level mapping bug: store originalIndex to map sorted display to unsorted player arrays
- Remove unsafe-inline from CSP by using CSS class for hidden state
- Replace inline style="display: none" with embed-hidden class

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(workers): Add API key expiration and rotation support (#226) (#517)

* feat(workers): Add API key expiration and rotation support (#226)

Implement secure API key lifecycle management for workers:

- Add configurable key expiration (default 90 days, 0 = never)
- Add grace period for expired keys (default 4 hours)
- Add key rotation with overlap period (default 2 hours)
- Add 5-minute cooldown between rotations
- Add rate limiting (10 rotations/hour per worker)

New API endpoints:
- POST /api/v1/workers/{worker_id}/rotate - rotate a worker's key
- GET /api/v1/workers/expiring-keys - list keys expiring soon
- POST /api/v1/workers/revoke-expired - bulk revoke expired keys

New CLI commands:
- vlog worker rotate <worker-id> [--revoke-old]
- vlog worker expire-warning [--days N] [--include-grace]

New settings:
- workers.api_key_expiration_days (default: 90)
- workers.api_key_grace_period_hours (default: 4)
- workers.api_key_rotation_overlap_hours (default: 2)
- workers.api_key_expiration_warning_days (default: 14)

Security considerations from code review:
- Reduced defaults per security review (grace 24h→4h, overlap 24h→2h)
- Dry-run mode for bulk revoke operations
- Rate limiting and cooldown to prevent abuse
- Audit logging for all rotation events

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(workers): Address code review findings for API key rotation (#226)

Critical fixes from code review:
- Add SELECT FOR UPDATE locking to prevent race condition in rotate_worker_key()
- Wrap bulk_revoke_expired_keys() in transaction for atomicity
- Fix return type annotation (datetime → Optional[datetime])
- Remove unused Response import and dead _key_expiring/_key_id code
- Only shorten old key expiration, never extend it (preserve longer lifetimes)
- Add error logging for failed rotation attempts

Other improvements:
- Fix rate limit comment to clarify per-IP (not per-worker) behavior
- Return 400 error for invalid 'days' parameter instead of silent clamping
- Add 429 handling in CLI with user-friendly Retry-After message
- Add index on expires_at column for efficient expiration queries
- Add 11 unit tests for _check_key_expiration_with_grace() edge cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(workers): Address Copilot review comments (#226)

- Add expires_at index to database.py table definition (matches migration)
- Fix CLI worker_name truncation to match column width (18 → 20 chars)
- Remove unused key_expiring variable and associated comment
- Remove unused WORKER_KEY_EXPIRED audit action
- Fix rate limit docstring: "per IP per hour" (not per worker)
- Remove unused AsyncMock and patch imports from tests

Note: Integration tests for new endpoints deferred to follow-up work.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(social): Add comments and ratings system (#213)

Implement a comprehensive comments and ratings system with:

Backend:
- Database migration 031 with ltree extension for threaded comments
- Comments table with materialized path threading (max depth 5)
- Ratings table with composite PK (video_id, user_id)
- Per-video social toggles (NULL = inherit from global settings)
- Denormalized aggregates on videos table with triggers
- RBAC permissions for comments and ratings
- Global settings for social features configuration
- HTML sanitization with bleach library
- Public API endpoints for comments/ratings CRUD
- Admin API endpoints for moderation queue and actions

Admin UI:
- Social Features section in video edit modal
- Comments/Ratings toggles with 3 states (On/Off/Inherit)

Public UI:
- Ratings section near video title (stars or thumbs)
- Comments section with threaded replies
- Comment form with character limit
- Reply functionality up to 5 levels
- Timestamp links to seek video
- Load more pagination

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(social): Fix critical bugs in comments/ratings endpoints (#213) (#518)

* fix(social): Fix critical bugs in comments/ratings endpoints (#213)

Fixes from code review findings:
- Change all comment/rating endpoints from video_id to slug parameter
  (fixes 422 validation errors - frontend sends slugs, not IDs)
- Add get_social_settings_by_video_id helper for internal lookups
- Add 'reason' field to CommentModerate schema for audit trail
- Add missing required fields to CommentResponse in admin endpoints:
  depth, path, parent_id, is_edited, reply_count
- Update SQL queries in admin.py to fetch all required comment fields

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(social): Don't set updated_at when moderating comments

Address Copilot review feedback:
- updated_at should only be set when content is edited, not during
  moderation status changes
- This ensures is_edited accurately reflects whether the user edited
  their comment content, not whether an admin moderated it
- Removes semantic confusion where moderated comments appeared as "edited"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Use restrictive CORS default for Admin API (#519)

* fix(security): Use restrictive CORS default for Admin API (#433)

Change Admin API CORS to default to same-origin only (empty list) instead
of allowing all origins (*). This improves defense-in-depth security while
maintaining functionality for typical deployments where the admin UI and
API are served from the same origin.

Users who need cross-origin access can explicitly set VLOG_ADMIN_CORS_ORIGINS.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Add CORS validation and improve reliability (#433)

Address review feedback for Admin API CORS security fix:

- Add origin format validation at startup (fail fast with clear errors)
  - Validates protocol prefix (http:// or https://)
  - Rejects trailing slashes
  - Warns about mixing wildcard with specific origins (breaks session auth)

- Fix allow_credentials logic to match public API pattern
  - Credentials only enabled when origins are configured AND not using wildcard
  - Clearer variable names for readability

- Add startup logging for CORS configuration
  - Logs allowed origins or "same-origin only" message
  - Helps troubleshoot CORS issues

- Add migration guide to UPGRADING.md
  - Explains who is affected by the change
  - Provides clear action steps for affected users

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(config): Add CORS validation and credentials tests (#433)

Add comprehensive test coverage for the new CORS validation logic
as requested by Copilot review:

TestCorsOriginValidation:
- Empty origins allowed (same-origin only)
- Valid HTTP/HTTPS origins parsed correctly
- Wildcard origin allowed
- Missing protocol rejected with clear error
- Trailing slash rejected with clear error
- Mixed wildcard warns but allows
- Whitespace stripped from origins
- Empty entries from extra commas filtered

TestAdminCorsCredentials:
- Empty origins: credentials disabled
- Specific origins: credentials enabled
- Wildcard: credentials disabled (per CORS spec)
- Mixed wildcard + specific: credentials disabled

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Apply consistent error sanitization across endpoints (#435) (#520)

* fix(security): Apply consistent error sanitization across endpoints (#435)

Apply sanitize_error_message() consistently to prevent information
disclosure through error messages. This addresses CWE-209 by ensuring
internal details (file paths, database errors, stack traces) are not
exposed to API clients.

Changes:
- worker_api.py: Sanitize 6 error paths in HLS/reencode uploads
- admin.py: Sanitize 10 error paths in thumbnails, settings, custom fields
- auth/endpoints.py: Sanitize session refresh error

All original errors are now logged for debugging while clients receive
safe, sanitized messages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address review feedback on error sanitization

- Add ErrorLogging.SKIP_LOGGING to prevent double-logging
- Add missing FFmpeg stderr sanitization (api/admin.py:2416)

Reviewers identified that sanitize_error_message() logs by default,
causing duplicate log entries. Now we log explicitly first, then
pass SKIP_LOGGING to avoid redundant logging.

Also addresses Bruce's security finding: FFmpeg stderr in thumbnail
upload was exposed without sanitization.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Use static message for thumbnail upload errors

Address Copilot review feedback: sanitize_error_message() on FFmpeg
stderr could produce confusing output like "Invalid image file: Video
transcoding failed" because the sanitizer detects "ffmpeg" keyword.

Use a static, contextually-appropriate message instead. The raw FFmpeg
stderr is still logged for debugging.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Require Admin API authentication by default (#431, #432) (#521)

* fix(security): Require Admin API authentication by default (#431, #432)

Issue #431: Admin API no longer allows unauthenticated access when
ADMIN_API_SECRET is not configured. Instead:
- If no users exist: only /api/auth/setup is accessible (503 for all else)
- If users exist: session authentication is required

Security improvements based on review feedback:
- Use permanent positive caching to prevent race condition attacks
- Fail-CLOSED on database errors (require auth, not allow setup)
- Replace /api/auth/* prefix skip with explicit public endpoint list
  to prevent bypass via /api/auth/users, /api/auth/invites, etc.
- Add logging for database errors in auth check
- Use efficient EXISTS query instead of COUNT(*)
- Normalize paths to handle trailing slashes

Issue #432: Worker API now logs a prominent warning at startup when
VLOG_WORKER_ADMIN_SECRET is not configured, informing operators that
management endpoints will return 503.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address PR review feedback

- Update startup log message to accurately describe which endpoints
  are accessible during setup mode (not just /api/auth/setup)
- Add comprehensive tests for Issue #431 authentication behavior:
  - No secret + no users: API returns 503, setup accessible
  - No secret + users exist: session auth required (401)
  - Public auth endpoints accessible in all states
  - Sensitive auth endpoints blocked in setup mode
  - DB errors fail closed (require auth, not allow setup)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(logging): Replace print statements with proper logging (#393, #380, #379) (#522)

* refactor(logging): Replace print statements with proper logging (#393, #380, #379)

Replace print() calls with Python's logging module across worker files
for consistent logging and better production observability.

Changes:
- api/database.py: Add logging import and replace 1 print statement
- worker/transcription.py: Add logging import and replace ~27 print statements
- worker/transcoder.py: Move logger definition earlier and replace ~80 print statements

All log messages use appropriate levels:
- logger.info() for normal operational messages
- logger.debug() for verbose/diagnostic messages
- logger.warning() for recoverable issues
- logger.error() for failures

Closes #393, #380, #379

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address code review feedback on import ordering and logging

- Move logger definitions after all imports in api/database.py and
  worker/transcription.py to comply with PEP 8 import ordering
- Combine split watchdog warning messages in transcoder.py
- Remove redundant TIMEOUT: and FAILURE: prefixes from log messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(backup): Implement backup and restore system (#216) (#523)

* feat(backup): Implement backup and restore system (#216)

Add comprehensive backup and restore functionality including:

- Database backup support for PostgreSQL (pg_dump) and SQLite
- Optional video file backup with incremental support
- S3 remote storage integration with multipart uploads
- Built-in scheduler daemon for automated backups
- Manifest signing with HMAC-SHA256 for integrity verification
- Restore with rollback guarantee (safety backup before restore)

New backup/ module with:
- service.py: Main BackupService orchestrator
- database.py: PostgreSQL/SQLite backup handlers
- files.py: Incremental video file backup with path validation
- s3.py: S3 storage with retry logic and server-side encryption
- manifest.py: Backup manifest with HMAC signing
- restore.py: Restoration with rollback guarantee
- verify.py: Integrity verification (checksums, signatures)
- scheduler.py: Backup scheduler daemon with health endpoint
- locking.py: File-based locking to prevent concurrent backups
- exceptions.py: Custom exception types

API endpoints:
- POST /api/v1/backups - Create backup
- GET /api/v1/backups - List backups
- GET /api/v1/backups/{id} - Get backup details
- POST /api/v1/backups/{id}/restore - Restore from backup
- POST /api/v1/backups/{id}/verify - Verify integrity
- DELETE /api/v1/backups/{id} - Delete backup

CLI commands:
- vlog backup create/list/restore/verify/delete/schedule

Security hardening:
- PGPASSWORD env var (never CLI args)
- Path validation to prevent traversal attacks
- Manifest signing for integrity
- Restore rate limiting (1/hour via API)
- Scheduler refuses to run as root

Reliability features:
- Pre-flight disk space checks
- Atomic operations (temp dir then rename)
- Configurable timeouts for all operations
- Exponential backoff retry for S3

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(backup): Address security, reliability, and performance issues (#216)

Security fixes:
- Fix tarfile extraction vulnerability (CVE-2007-4559 protection)
- Add backup ID format validation to prevent injection attacks
- Rename TimeoutError to BackupTimeoutError (avoid shadowing built-in)

Reliability fixes:
- Document file safety backup limitation with explicit acknowledgment
- Move restore rate limiting to database for multi-process safety
- Verify safety backup integrity before proceeding with restore
- Export all exceptions from backup module

Performance improvements:
- Parallelize S3 multipart uploads (4 concurrent workers)
- Combine statistics queries into single database query
- Increase checksum chunk size from 8KB to 1MB
- Update deprecated asyncio.get_event_loop() patterns

API changes:
- Add accept_no_file_rollback parameter to restore endpoints
- Add BackupRestoreRequest.accept_no_file_rollback field
- Add --accept-no-file-rollback CLI flag for restore command

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(deps): Pin urllib3>=2.6.3 to fix security vulnerabilities

Adds explicit pin for urllib3>=2.6.3 to fix:
- GHSA-38jv-5279-wg99
- GHSA-2xpw-w6gg-jr37
- GHSA-gm62-xv2j-4w53
- GHSA-pq67-6m6q-mj2v

Also adds wheel>=0.46.2 to pyproject.toml for consistency with
requirements.txt (CVE-2026-24049).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(backup): Address Copilot review comments (#216)

- Remove unused imports from backup modules
- Add Windows compatibility for file locking (msvcrt fallback)
- Add Windows compatibility for root/admin check in scheduler
- Use cross-platform shutil.disk_usage() for disk space checks
- Add comment explaining empty except block in scheduler
- Fix parameter name mismatch in admin API (delete_from_s3 -> delete_remote)
- Remove unused archive_checksum variable
- Improve path traversal check comment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: Code quality improvements (#385, #386, #395, #439) (#525)

* refactor: Code quality improvements (#385, #386, #395, #439)

- Extract magic numbers to named constants in analytics_cache.py (#385)
  - DEFAULT_CACHE_TTL_SECONDS, DEFAULT_CACHE_MAX_SIZE
  - REDIS_SOCKET_TIMEOUT, REDIS_CONNECT_TIMEOUT, REDIS_SCAN_BATCH_SIZE
  - CACHE_EVICTION_DIVISOR

- Add return type hints to dispatch methods in common.py (#386)
  - RequestIDMiddleware.dispatch -> Response
  - SecurityHeadersMiddleware.dispatch -> Response

- Add error handling to start-transcription.sh (#395)
  - Add set -e and set -u for safer execution
  - Require virtual environment (fail if missing)
  - Validate transcription.py script exists
  - Verify whisper module is installed

- Create require_valid_slug() helper to eliminate repetition (#439)
  - Add helper function in common.py
  - Replace 10 duplicate validation patterns in public.py
  - Consistent error messages: "Invalid {resource_type} slug"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: Address reviewer feedback (Conway, Margo)

Clarity improvements (Conway):
- Rename CACHE_EVICTION_DIVISOR to CACHE_EVICTION_RATIO (0.10)
  for clearer intent - "evict 10%" is more intuitive than "divide by 10"
- Add rationale comments for REDIS_SOCKET_TIMEOUT and REDIS_SCAN_BATCH_SIZE
  explaining the tradeoffs (latency vs memory vs round-trips)
- Add comment explaining why start-transcription.sh changes to script dir

Reliability improvements (Margo):
- Add Redis reconnection with exponential backoff
  - _maybe_reconnect() attempts reconnection after transient failures
  - Backoff from 1s to 60s max prevents hammering failing Redis
  - Connection auto-recovers without requiring API restart
- Add timeout protection to bulk Redis operations
  - clear() and get_stats() now timeout after 5 seconds
  - Prevents blocking on large keyspaces
  - Logs warning with partial progress on timeout
- Mark connection as failed on operation errors to trigger reconnection
- Improve slug validation error messages with specific reasons:
  - "Missing {type} slug" for empty input
  - "path traversal not allowed" for .. attempts
  - "must be lowercase alphanumeric with hyphens" for format errors
- Shell script improvements:
  - Capture and display activation errors for debugging
  - Add BASH_SOURCE fallback for shell compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address Copilot review feedback

Shell script changes:
- Revert to simpler patterns matching other startup scripts
- Use `source ... || {` instead of command substitution for activation
- Use `if ! python -c ... 2>/dev/null; then` for module checks
- Remove BASH_SOURCE fallback (all scripts use bash explicitly)

Documentation:
- Add note explaining intentional logic duplication in require_valid_slug()
  for providing specific error messages (missing vs path traversal vs format)

Copilot feedback addressed:
- [Fixed] Shell script patterns now match start-worker.sh exactly
- [Intentional] require_valid_slug() duplication: provides better UX with
  specific error messages per Margo's reliability review
- [Intentional] Reconnection/timeout logic: addresses critical reliability
  issues identified by Margo (Redis never recovering after failure)
- [Intentional] CACHE_EVICTION_RATIO naming: Conway specifically recommended
  this for clarity over the original DIVISOR suggestion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(live): Implement broadcaster dashboard/studio UI (#524)

Adds a comprehensive broadcaster dashboard for live stream management:

Backend:
- Stream health metrics infrastructure with Redis-based aggregation
- Real-time viewer tracking with server-generated session IDs
- Studio API endpoints with ownership checks
- SSE endpoint for real-time dashboard updates
- Background tasks for metrics aggregation and viewer cleanup
- Pub/sub channels for metrics and viewer count broadcasting

Database:
- Add owner_id to live_streams for ownership tracking
- Add live_stream_metrics table for aggregated health data
- Add live_stream_viewers table for viewer session tracking
- Add viewer count columns to live_streams

Frontend:
- Studio web app (Alpine.js/TypeScript/Vite)
- Real-time dashboard with SSE for live updates
- Stream health visualization and viewer counts
- Stream key regeneration with password re-entry

Security (per Bruce's review):
- Server-generated session IDs (256-bit entropy)
- HMAC-SHA256 IP hashing with per-instance secret
- Rate limiting on all public endpoints
- Password re-entry required for stream key operations

Observability (per Cid's review):
- Task health tracking with per-task intervals
- Redis availability logging
- Connection limit tracking for SSE

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: Update tailwindcss to v4.1.17

Vendor CSS library version bump.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: Suppress GitGuardian false positive in test file

The string "abcdefghijklmnop" is a test case for password validation,
not an actual secret. Added ggignore comment to suppress the warning.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: Add GitGuardian config to ignore test file false positives

Test files intentionally contain weak password strings to verify
password validation logic. These are not actual secrets.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: Address Copilot review feedback

- live_vod.py: Return immediately after JSON decode error for clearer flow
- analytics_cache.py: Clarify CACHE_EVICTION_RATIO comment with usage context

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
filthyrake added a commit that referenced this pull request Jan 30, 2026
* ci: bump actions/setup-node from 4 to 6

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* ci: bump actions/setup-node from 4 to 6

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* ci: bump actions/upload-artifact from 4 to 6

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Add API versioning and OpenAPI documentation improvements (Issue #218) (#505)

* Add API versioning and OpenAPI documentation improvements (Issue #218)

- Add versioned API routes with /api/v1 prefix across all three APIs
- Create api/versioning.py module with utilities for:
  - Versioned router creation
  - Deprecation header support
  - OpenAPI schema customization
  - Version header middleware
- Add configuration options in config.py:
  - API_VERSION, API_SUPPORTED_VERSIONS
  - API_DEPRECATION_NOTICE, API_DEPRECATION_SUNSET
  - API_INCLUDE_LEGACY_ROUTES (backwards compatibility)
  - OPENAPI_* settings for documentation customization
- Refactor public, admin, and worker APIs to use APIRouter
- Add enhanced OpenAPI endpoint descriptions and summaries
- Maintain backwards compatibility with legacy /api routes

Closes #218

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add security and reliability fixes to API versioning

Addresses feedback from code, security, and reliability reviews:

Security fixes:
- Add validate_config() with regex validation for API_VERSION format
- Add sanitize_header_value() to prevent CRLF header injection
- Validate all API_SUPPORTED_VERSIONS entries match expected format

Reliability fixes:
- Add error handling to VersionHeaderMiddleware.dispatch()
- Add error boundary to configure_openapi_schema() with fallback
- Add error handling to DeprecationHeadersRoute custom handler
- Fail fast on configuration errors at module load

Code quality:
- Remove unused datetime import
- Remove unused RESPONSE_EXAMPLES dict
- Update .env.example with new API versioning configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add GHSA-w853-jp5j-5j7f to security scan ignore list

The filelock TOCTOU vulnerability (CVE-2025-68146) fix requires
Python 3.10+, but CI runs on Python 3.9. The container images
use Python 3.11+ where the patched version is installed.

This is the same class of vulnerability as the already-ignored
GHSA-qmgc-5h2g-mvrw, just with a different advisory ID.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix jaraco-context version in container build

Add --upgrade step after pip install to ensure security-patched
packages (filelock, jaraco-context) are at their required versions
even if transitive dependencies initially resolve to older versions.

Fixes Trivy container scan failure for GHSA-58pv-8j8x-9vj2.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Use pip constraints file for security patches in container

Replace --upgrade approach with -c constraints.txt which forces
pip to respect minimum versions during dependency resolution.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Address Copilot review feedback

- Fix Link header to use /docs (FastAPI's default docs location)
- Clarify sunset date format comment in .env.example
- Add comprehensive tests for API versioning (18 tests covering
  version headers, versioned endpoints, legacy routes, config
  validation, header sanitization, and OpenAPI schema)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add runtime stage pip upgrade for security patches

The builder stage installs correct versions, but dnf packages in
runtime stage may have old transitive dependencies. Add explicit
pip upgrade after COPY to ensure security-patched versions are used.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Ignore setuptools vendored jaraco.context in Trivy scan

setuptools 80.9.0 vendors jaraco.context 5.3.0 internally. This
vendored copy is used only by setuptools for its own operations
and is not exposed to user input, making the path traversal
vulnerability (GHSA-58pv-8j8x-9vj2) unexploitable in this context.

The main jaraco-context package is correctly upgraded to 6.1.0.

Added .trivyignore file to skip this false positive in container
image scans.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(playback): Implement Up Next autoplay feature (Issue #211) (#506)

* feat(playback): Implement Up Next autoplay feature (Issue #211)

Add autoplay and "Up Next" functionality that automatically plays
the next recommended video when the current video ends.

Backend:
- Add playback configuration settings (VLOG_AUTOPLAY_ENABLED,
  VLOG_UPNEXT_ENABLED, VLOG_AUTOPLAY_COUNTDOWN_SECONDS)
- Create GET /api/config/playback endpoint for frontend config
- Create GET /api/videos/{slug}/next endpoint for single next video
- Add settings to KNOWN_SETTINGS and ENV_VAR_MAPPING for database
  settings migration support

Frontend:
- Add playback config and next video loading in watch.js
- Implement countdown overlay UI with cancel/play now buttons
- Add autoplay toggle checkbox (persisted to localStorage via
  VLogUtils.preferences)
- Hook video 'ended' event to trigger Up Next countdown
- Add responsive CSS for Up Next overlay

Features:
- Configurable countdown duration (default 10 seconds)
- User preference to enable/disable autoplay
- Global admin setting to enable/disable feature
- Cancel button to stop countdown and stay on current video
- Play Now button to skip countdown
- Fallback to related videos algorithm for next video selection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(playback): Address specialist review feedback (Issue #211)

Frontend reliability fixes:
- Add try/catch around localStorage in toggleAutoplay()
- Validate nextVideo._href before navigation
- Rename playNextVideo() to navigateToNextVideo() for clarity
- Add AbortController to loadNextVideo() for request cancellation

Backend improvements:
- Change settings fetch failure logging from DEBUG to WARNING
- Refactor get_next_video to share code with get_related_videos (DRY)
- Parallelize tier queries with asyncio.gather() for limit=1
- Increase cache TTL from 30s to 300s to reduce DB load

Tests:
- Add comprehensive unit tests for /api/v1/videos/{slug}/next endpoint
- Add unit tests for /api/config/playback endpoint
- Test invalid slug patterns, deleted videos, null responses

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(playback): Address Copilot review feedback (Issue #211)

Frontend fixes:
- Clear existing countdown in startUpNextCountdown to prevent duplicate timers
- Add @click.prevent on video link to avoid double navigation
- Add keyboard accessibility: focus on Cancel button, Escape key handler
- Add accessibility attrs: role="dialog", aria-live, aria-labelledby, sr-only description
- Use @click.prevent on autoplay checkbox to prevent flickering on localStorage errors
- Validate slug from API with SLUG_PATTERN before constructing URLs
- Use encodeURIComponent when building next video URL

Backend fixes:
- Make VIDEO_LIST_CACHE_TTL configurable via environment variable

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(live): Implement live streaming via HTTP segment push (#507)

* feat(live): Implement live streaming via HTTP segment push

Add live streaming capability without requiring RTMP/SRT infrastructure.
FFmpeg clients encode locally into HLS/CMAF segments and push via HTTP PUT.

Features:
- Stream key authentication with argon2id hashing
- HTTP segment push endpoints (PUT init.mp4, PUT seg_NNNN.m4s)
- Dynamic HLS playlist generation written to disk
- DVR window support with background cleanup
- Stale stream detection with grace period
- Automatic VOD recording on stream end (hardlink or copy segments)
- Rate limiting (global per-IP and segment-specific)
- Path containment verification for defense-in-depth

Admin API:
- POST /api/v1/live/streams - Create stream with stream key
- GET /api/v1/live/streams - List streams
- GET/PATCH/DELETE /api/v1/live/streams/{slug}
- POST /api/v1/live/streams/{slug}/regenerate-key
- POST /api/v1/live/streams/{slug}/end

Public API:
- GET /api/v1/live/streams - List active streams
- GET /live/{slug}/master.m3u8 - Master playlist
- GET /live/{slug}/{quality}/stream.m3u8 - Variant playlist

Ingest API:
- PUT /api/live/ingest/{slug}/{quality}/init.mp4
- PUT /api/live/ingest/{slug}/{quality}/seg_NNNN.m4s
- GET /api/live/ingest/{slug}/status

Includes vlog-live-push.sh script for secure FFmpeg streaming
with stream key passed via environment variable.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(live): Address Copilot review feedback

- Fix unused imports in live_ingest.py, live_playlist.py, live_tasks.py, live_vod.py
- Add comments to silent except clauses explaining why they're silent
- Replace lambda capture patterns with functools.partial for clarity
- Reduce upload timeout from 720s to 600s
- Fix VOD duration calculation (was using seq*duration, now sums durations)
- Implement VOD recording in stale stream detection
- Add playlist updates after successful segment upload
- Update stream status to 'live' on init segment upload
- Fix path containment to use is_relative_to (Python 3.9+) with fallback
- Improve duplicate detection to use IntegrityError instead of generic Exception
- Add fire-and-forget task error callback in admin.py VOD recording
- Fix shell script INPUT_OPTS to use bash arrays for proper quoting
- Simplify bufsize calculation in shell script
- Fix playlist DVR window limiting logic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(theme): Add UI theme customization and branding (Issue #214) (#508)

* feat(theme): Add UI theme customization and branding (Issue #214)

Implement comprehensive theme and branding customization features:

Backend:
- Add branding, theme, and layout settings to KNOWN_SETTINGS
- Create public API endpoint GET /api/v1/config/theme for theme config
- Add branding endpoints for logo/favicon upload in admin API
- Create public endpoints for serving logo and favicon

Frontend (Public):
- Add theme.js loader for dynamic CSS variable injection
- Update index.html to use dynamic site name and logo
- Update footer to support custom footer text and links
- Add logo image CSS styles

Frontend (Admin):
- Add Branding tab to Settings page
- Implement logo and favicon upload with progress tracking
- Add site name and footer text editing
- Create branding API client and store methods

Settings added:
- branding.site_name, branding.logo_path, branding.favicon_path
- branding.footer_text, branding.footer_links
- theme.primary_color, theme.secondary_color, theme.accent_color
- theme.mode (light/dark/auto), theme.custom_css
- layout.homepage_style, layout.videos_per_page, layout.grid_columns
- layout.show_sidebar, layout.show_related_videos

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Address security vulnerabilities in branding feature

Fixes identified by security review:

- Add path traversal protection to all file operations using validate_safe_path()
- Implement SVG sanitization for logo/favicon uploads to prevent XSS
- Add X-Content-Type-Options: nosniff header to all file responses
- Unify cache invalidation with asyncio.Lock to prevent race conditions
- Restrict custom CSS to CSS variables only via sanitize_custom_css()
- Add URL validation for footer links to block javascript: URLs
- Add 5-second fetch timeout to theme.js to prevent hanging

Issue #214

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address Copilot review comments on PR #508

- Fix :root regex pattern (was only matching bare colon)
- Remove redundant asyncio and re imports
- Add try/except around temp file cleanup to handle NameError
- Fix dictionary indentation in exception handler defaults
- Add aria-labels to delete buttons for accessibility
- Add for/id associations between labels and file inputs
- Update placeholder to remove hardcoded year
- Add reset_theme_settings_cache() to public.py
- Call cache reset when branding/theme settings are updated
- Invalidate public theme cache on all branding changes

Issue #214

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix(admin): Resolve CSP violations with constructable stylesheets (#509)

* fix(admin): Resolve CSP violations with constructable stylesheets

Migrate all web components to use constructable stylesheets instead of
inline <style> tags, which were being blocked by the strict Content
Security Policy (style-src 'self').

Changes:
- Migrate 18 web components to use adoptedStyleSheets API
- Add CSP-compatible navigation methods for Alpine.js
- Fix Alpine CSP parser errors for multi-statement @click expressions
- Update SettingsTab type to include branding and custom_fields tabs
- Add helper methods for CSP-compatible template expressions

Components migrated:
- vlog-alert, vlog-alert-container, vlog-badge, vlog-button
- vlog-card, vlog-dropzone, vlog-empty-state, vlog-filter
- vlog-hamburger, vlog-input, vlog-nav-drawer, vlog-search
- vlog-skeleton, vlog-tab-button, vlog-tab-panel, vlog-table
- vlog-tabs, vlog-video-card

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(admin): Address review feedback for CSP compliance PR

Changes based on code reviewer feedback:

1. Modal Component:
   - Migrate vlog-modal.ts to constructable stylesheets (CSP-compliant)
   - Replace inline body.style.overflow with classList.add/remove('modal-open')
   - Add .modal-open class to tokens.css for scroll locking

2. Navigation Methods (per Conway's clarity feedback):
   - Rename switchToWorkers -> openWorkersTab (clearer intent)
   - Rename switchToAnalytics -> openAnalyticsTab
   - Rename switchToSettings -> openSettingsTab
   - Rename switchToSettingsBranding -> openBrandingSettings
   - Rename switchToSettingsCustomFields -> openCustomFieldsSettings
   - Add comprehensive section header explaining CSP restrictions

3. Index.html Updates:
   - Use new navigation method names
   - Use tabClass() and settingsTabClass() helpers for CSP compliance
   - Remove semicolons from dropdown @click handlers

4. Type Updates:
   - Update SettingsTab type to include 'branding' and 'custom_fields'

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(admin): Complete CSP compliance for Alpine.js expressions

- Replace arrow functions with store helper methods:
  - hasActiveWorkers(), hasGlobalCustomFields(), getGlobalCustomFields()
  - hasCategoryCustomFields(), getCategoryCustomFields()
  - hasNullCategoryCustomFields(), getNullCategoryCustomFields()
  - handleFilesSelected() for dropzone events

- Replace regex literals with helper methods (CSP parser doesn't support /pattern/):
  - formatCategoryName(), formatCategoryTitle(), formatSettingLabel()

- Replace inline style attributes with CSS classes:
  - Add .w-pct-{0-100} percentage width classes
  - Add .grid-cols-5-minmax, .min-w-0, .aspect-16-9, .aspect-content utilities
  - Remove all style="" attributes from index.html

- Fix settings tab button styling consistency:
  - Update settingsTabClass() to match dynamic category buttons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(auth): Add multi-user authentication with RBAC (Issue #200) (#510)

* feat(auth): Add multi-user authentication with RBAC (Issue #200)

Implements comprehensive user authentication replacing the legacy single
admin secret with proper user accounts, sessions, and role-based access
control.

Key features:
- User accounts with username/email and argon2id password hashing
- Role-based access control (Admin, Editor, Viewer)
- Session-based browser auth with HTTP-only cookies
- Refresh token rotation with theft detection
- API keys for programmatic access
- OIDC integration for self-hosted identity providers
- Invite-only registration system
- Account lockout after failed login attempts
- CLI commands for user management (vlog auth migrate, create-admin, etc.)

Backend:
- New api/auth/ module with sessions, permissions, endpoints
- Database migration for users, sessions, api_keys, invites tables
- Session cleanup scheduled task for expired sessions
- Video ownership tracking (owner_id on videos table)

Frontend:
- Redesigned login page with username/password form
- User profile management (password change, API keys, sessions)
- User management page for admins (create, edit, disable users)
- Invite management for user registration

Testing & Documentation:
- 50 comprehensive tests covering auth flows
- Full documentation in docs/AUTHENTICATION.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(auth): Address comprehensive review feedback for authentication system

Security fixes (Bruce):
- Remove password reset token logging in endpoints.py
- Remove token from force password reset API response in users.py
- Add Depends() to OIDC link/unlink endpoints
- Implement OIDC nonce validation to prevent replay attacks
- Add SESSION_SECRET_KEY startup validation with minimum length

Performance fixes (Brendan):
- Use SHA-256 for session/API token hashing instead of argon2id
- Add token_prefix columns for O(1) indexed database lookup
- Support legacy argon2id hashes for backward compatibility

Reliability fixes (Margo):
- Add asyncio.Lock for thread-safe circuit breaker operations
- Wrap session refresh and invite acceptance in database transactions
- Extend cleanup task to purge expired OIDC states and password reset tokens

Code clarity improvements (Conway):
- Update migration to include token_prefix and last_used_at columns
- Add is_sha256_hash() helper for hash type detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(auth): Add VLOG_SESSION_SECRET_KEY to test configuration

Required after adding startup validation for session secret key.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(auth): Remove unused imports and fix code style

Address Copilot review comments:
- Remove unused imports across auth modules (Query, Depends, Set, etc.)
- Remove unused config imports (USER_REFRESH_TOKEN_EXPIRY_DAYS, etc.)
- Remove unused variable assignment in cli/main.py
- Fix test import style: use module reference pattern consistently
  instead of mixed 'import' and 'from import' for sessions module

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(test): Use non-secret placeholder for test session key

Replace string literal with generated value to avoid GitGuardian
false positive on test credentials.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(auth): Add setup wizard for first-time admin creation

Adds a web-based setup wizard that appears when no users exist,
allowing the first admin account to be created via the UI instead
of requiring CLI access.

Backend:
- GET /api/v1/auth/setup - Check if setup is needed
- POST /api/v1/auth/setup - Create initial admin (only works when
  no users exist, returns 403 afterward)

Frontend:
- Setup wizard modal with username, email, password fields
- Auto-login after successful account creation
- Integrated with existing auth flow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(auth): Resolve session persistence and login issues

- Remove legacy /auth/login and /auth/check endpoints in admin.py that
  were overriding the new user-based auth endpoints
- Update AuthCheckResponse to return nested user object with permissions,
  auth_required, auth_mode, and OIDC settings
- Update LoginRequest to accept username_or_email for flexible login
- Fix login handler to look up users by either username or email
- Add missing 'os' import in auth/endpoints.py
- Add failed_login_attempts=0 to setup wizard user creation
- Fix Alpine.js CSP compatibility: replace optional chaining (?.) with
  CSP-safe expressions throughout index.html
- Use <template x-if> instead of x-show for user-dependent UI sections
  to prevent evaluation errors when currentUser is null
- Fix TypeScript error: use undefined instead of null for optional fields

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(public-ui): Resolve Alpine.js CSP parser errors

Replace complex expressions with precomputed values for CSP compatibility:
- Add _showError, _showContent, _showEmptyState to category.js and tag.js
- Add _showFeaturedSection to home.js
- Add updateContentUIState() method to update UI state flags
- Update HTML files to use precomputed boolean values

The Alpine.js CSP build cannot parse negation (!), optional chaining (?.),
or complex boolean expressions (&&, ||) in x-show directives.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(auth): Address code review findings from specialized agents (#511)

* fix(auth): Address code review findings from specialized agents

Backend fixes (api/auth/endpoints.py):
- Move sqlalchemy or_ and func imports to top-level (was inline in function)
- Add case-insensitive username/email matching with func.lower()
- Hash identifier in security logs to avoid PII exposure (GDPR compliance)
- Cache OIDC env vars at module level to avoid repeated lookups
- Move validate_session_token and get_role_permissions imports to top-level

Frontend fixes:
- Fix broken HTML div nesting in admin/index.html (extra closing tag)
- Fix _showFeaturedSection race condition in home.js init()

Addresses findings from: Gafton, Bruce, Margo, Brendan, Conway reviews.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(auth): Address reliability findings from Margo's review

Reliability improvements:

1. Login flow atomicity (endpoints.py):
   - Move _reset_failed_login() to AFTER session creation succeeds
   - Ensures atomic state: if session creation fails, login count stays

2. Setup wizard TOCTOU fix (endpoints.py):
   - Wrap check-and-insert in database transaction
   - Handle unique constraint violations from concurrent requests
   - Session creation now within same transaction

3. Session limit race condition (sessions.py):
   - Use FOR UPDATE row locking in _enforce_session_limit()
   - Wrap session creation in transaction with limit enforcement
   - Prevents concurrent logins from exceeding limit

4. Database error handling (endpoints.py):
   - Add try/except around user lookup in login
   - Add try/except around setup status check
   - Return 503 with helpful message on database errors

5. Bare except:pass fix (sessions.py):
   - Add proper logging for last_used_at update failures
   - Helps detect database health issues in monitoring

6. Lockout state cleanup (endpoints.py):
   - Clear expired locked_until when user attempts login
   - Reset failed_login_attempts on expired lockout
   - Non-blocking with error logging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Update documentation for multi-user authentication features (#512)

* docs: Update documentation for multi-user authentication features

Updates documentation to reflect recent major features:
- Multi-user authentication with RBAC (Issue #200)
- Setup wizard for first-time admin creation
- API key management
- User invite system
- OIDC/SSO integration

Changes by document:
- API.md: Add comprehensive auth API section (setup wizard, login,
  user management, API keys, invites)
- AUTHENTICATION.md: Add setup wizard documentation, update migration
  guide for new multi-user system
- ADMIN_UI_GUIDE.md: Add Users tab section, update login/setup docs
- CONFIGURATION.md: Add VLOG_SESSION_SECRET_KEY and related auth
  settings, deprecate VLOG_ADMIN_API_SECRET
- DEPLOYMENT.md: Update production checklist for auth requirements
- TROUBLESHOOTING.md: Add comprehensive auth troubleshooting section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Fix critical accuracy issues found in code review

Addresses blockers identified by gafton-distinguished-engineer review:

1. Login response schema: Fixed to flat object (not nested under "user")
2. Profile update: Removed email field, only display_name and avatar_url
   are accepted
3. Environment variables: Fixed names to match actual implementation
   - VLOG_REFRESH_EXPIRY_DAYS (not VLOG_REFRESH_TOKEN_EXPIRY_DAYS)
   - VLOG_LOCKOUT_THRESHOLD (not VLOG_LOGIN_LOCKOUT_THRESHOLD)
   - VLOG_LOCKOUT_DURATION_MINUTES (not VLOG_LOGIN_LOCKOUT_DURATION_MINUTES)
4. Password reset expiry: Fixed default to 1 hour (not 24 hours)
5. Database settings table: Clarified these are env vars, not DB settings
6. Invite URL: Clarified it returns relative path, not absolute URL
7. Column name: Fixed failed_login_count to failed_login_attempts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* Chore(deps-dev): Bump lodash-es from 4.17.22 to 4.17.23 (#513)

Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.22 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits/4.17.23)

---
updated-dependencies:
- dependency-name: lodash-es
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Chore(deps-dev): Bump lodash from 4.17.21 to 4.17.23 (#514)

Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(logging): Add structured JSON logging with request context (#208) (#515)

* feat(logging): Add structured JSON logging with request context (#208)

Add centralized logging configuration with JSON output for production
and text format for development. Request context (request_id, client_ip,
user_agent) is automatically injected into all log messages.

Key features:
- python-json-logger for structured output
- SafeJSONEncoder handles non-serializable objects
- User-Agent sanitization prevents log injection
- try/finally ensures context cleanup on async exceptions
- Log file rotation with 0600 permissions for security
- Module-specific log level overrides via VLOG_LOG_LEVELS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address PR review feedback and security audit

- Fix import ordering to satisfy ruff/isort (E402, I001)
- Make setup_logging() idempotent with _logging_configured flag
- Close handlers before removing to prevent file descriptor leaks
- Add SecureRotatingFileHandler for 0o600 permissions on rotation
- Use record.getMessage() instead of str(record.msg) in fallback
- Add comprehensive tests for sanitize_user_agent and logging
- Add VLOG_LOG_* env vars to .env.example
- Ignore GHSA-7gcm-g887-7qv7 (protobuf DoS, no fix available)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Add protobuf GHSA-7gcm-g887-7qv7 to pip-audit ignores

No patched version available for this DoS vulnerability in protobuf's
json_format.ParseDict(). VLog doesn't use this function - protobuf is
a transitive dependency of faster-whisper.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Pin wheel>=0.46.2 and add CVE ID to trivyignore

- Add wheel>=0.46.2 to fix CVE-2026-24049 privilege escalation
- Add CVE-2026-0994 to trivyignore (Trivy finds protobuf by CVE, not GHSA)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Add wheel>=0.46.2 to Docker security patches

Add wheel to the security-patched packages in both Dockerfiles to fix
CVE-2026-24049 privilege escalation vulnerability detected by Trivy.

The wheel package is installed by pip as a build dependency. The existing
requirements.txt change doesn't affect the Docker image because it uses
explicit package installation during build.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Ignore wheel CVE in setuptools vendored copy

Add CVE-2026-24049/GHSA-8rrh-rw8j-w5fx to .trivyignore for the vendored
wheel inside setuptools. The vulnerability is in wheel.cli.unpack which
we don't use on untrusted files. The standalone wheel package is already
upgraded to 0.46.2 - this ignore is only for the vendored copy that
setuptools bundles internally.

Similar to the existing jaraco.context ignore - waiting for setuptools
upstream to update their vendored dependencies.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(embed): Add video embed functionality (#210) (#516)

* feat(embed): Add video embed functionality (#210)

Add the ability to generate iframe embed codes for videos, allowing them
to be embedded on external websites with a minimal, responsive player.

Security features:
- Domain whitelist with 'self' default (frame-ancestors CSP)
- Rate limiting: 500/min per IP for embed routes
- Query parameter validation (start, autoplay, controls)
- View count protection: 5+ seconds minimum playback

Backend:
- Add /embed/{slug} route with CSP headers
- Add /api/v1/videos/{slug}/embed-code endpoint
- Add embed configuration to config.py and settings_service.py
- Add EmbedCodeResponse schema

Frontend:
- Create embed.html minimal player template
- Create embed-error.html for graceful error display
- Create embed.js with analytics tracking (source='embed')
- Create embed.css with responsive styles
- Extend share modal with Link/Embed tabs
- Add start time input and autoplay checkbox options
- Add live embed code generation and copy functionality

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(embed): Address code review findings from specialized agents (#210)

Security fixes (Bruce):
- Add validate_slug() to embed routes to prevent log injection
- Skip X-Frame-Options for /embed/ routes (CSP takes precedence)
- Add CSP domain validation to prevent header injection
- Use crypto.randomUUID() for secure session UUID generation

Reliability fixes (Margo):
- Add fetchWithTimeout() with configurable timeouts for all network requests
- Add retry logic with exponential backoff for video data fetch
- Add circuit breaker for analytics heartbeats (max 3 failures)
- Add proper cleanup with EmbedPlayer.destroy() and EmbedAnalytics.destroy()
- Handle pagehide event for mobile browser cleanup

Performance fixes (Brendan):
- Debounce embed code input handler (300ms) to reduce DOM thrashing
- Add proper interval cleanup to prevent memory leaks

Clarity fixes (Conway):
- Rename shareLinkTabContent/shareEmbedTabContent to shareTabLink/shareTabEmbed
- Rename parseTimeInput() to convertTimeFormatToSeconds() with JSDoc
- Extract _is_video_embeddable() and _build_embed_error_response() helpers
- Add start time validation with 24-hour maximum cap
- Add picture-in-picture to client-side iframe generation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(embed): Address Copilot review comments (#210)

- Use DB-backed settings in build_embed_csp_frame_ancestors() instead of env vars
- Fix crypto check to use typeof to prevent ReferenceError
- Fix getCurrentQuality() to handle HLS.js with currentLevelIndex
- Fix quality level mapping bug: store originalIndex to map sorted display to unsorted player arrays
- Remove unsafe-inline from CSP by using CSS class for hidden state
- Replace inline style="display: none" with embed-hidden class

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(workers): Add API key expiration and rotation support (#226) (#517)

* feat(workers): Add API key expiration and rotation support (#226)

Implement secure API key lifecycle management for workers:

- Add configurable key expiration (default 90 days, 0 = never)
- Add grace period for expired keys (default 4 hours)
- Add key rotation with overlap period (default 2 hours)
- Add 5-minute cooldown between rotations
- Add rate limiting (10 rotations/hour per worker)

New API endpoints:
- POST /api/v1/workers/{worker_id}/rotate - rotate a worker's key
- GET /api/v1/workers/expiring-keys - list keys expiring soon
- POST /api/v1/workers/revoke-expired - bulk revoke expired keys

New CLI commands:
- vlog worker rotate <worker-id> [--revoke-old]
- vlog worker expire-warning [--days N] [--include-grace]

New settings:
- workers.api_key_expiration_days (default: 90)
- workers.api_key_grace_period_hours (default: 4)
- workers.api_key_rotation_overlap_hours (default: 2)
- workers.api_key_expiration_warning_days (default: 14)

Security considerations from code review:
- Reduced defaults per security review (grace 24h→4h, overlap 24h→2h)
- Dry-run mode for bulk revoke operations
- Rate limiting and cooldown to prevent abuse
- Audit logging for all rotation events

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(workers): Address code review findings for API key rotation (#226)

Critical fixes from code review:
- Add SELECT FOR UPDATE locking to prevent race condition in rotate_worker_key()
- Wrap bulk_revoke_expired_keys() in transaction for atomicity
- Fix return type annotation (datetime → Optional[datetime])
- Remove unused Response import and dead _key_expiring/_key_id code
- Only shorten old key expiration, never extend it (preserve longer lifetimes)
- Add error logging for failed rotation attempts

Other improvements:
- Fix rate limit comment to clarify per-IP (not per-worker) behavior
- Return 400 error for invalid 'days' parameter instead of silent clamping
- Add 429 handling in CLI with user-friendly Retry-After message
- Add index on expires_at column for efficient expiration queries
- Add 11 unit tests for _check_key_expiration_with_grace() edge cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(workers): Address Copilot review comments (#226)

- Add expires_at index to database.py table definition (matches migration)
- Fix CLI worker_name truncation to match column width (18 → 20 chars)
- Remove unused key_expiring variable and associated comment
- Remove unused WORKER_KEY_EXPIRED audit action
- Fix rate limit docstring: "per IP per hour" (not per worker)
- Remove unused AsyncMock and patch imports from tests

Note: Integration tests for new endpoints deferred to follow-up work.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(social): Add comments and ratings system (#213)

Implement a comprehensive comments and ratings system with:

Backend:
- Database migration 031 with ltree extension for threaded comments
- Comments table with materialized path threading (max depth 5)
- Ratings table with composite PK (video_id, user_id)
- Per-video social toggles (NULL = inherit from global settings)
- Denormalized aggregates on videos table with triggers
- RBAC permissions for comments and ratings
- Global settings for social features configuration
- HTML sanitization with bleach library
- Public API endpoints for comments/ratings CRUD
- Admin API endpoints for moderation queue and actions

Admin UI:
- Social Features section in video edit modal
- Comments/Ratings toggles with 3 states (On/Off/Inherit)

Public UI:
- Ratings section near video title (stars or thumbs)
- Comments section with threaded replies
- Comment form with character limit
- Reply functionality up to 5 levels
- Timestamp links to seek video
- Load more pagination

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(social): Fix critical bugs in comments/ratings endpoints (#213) (#518)

* fix(social): Fix critical bugs in comments/ratings endpoints (#213)

Fixes from code review findings:
- Change all comment/rating endpoints from video_id to slug parameter
  (fixes 422 validation errors - frontend sends slugs, not IDs)
- Add get_social_settings_by_video_id helper for internal lookups
- Add 'reason' field to CommentModerate schema for audit trail
- Add missing required fields to CommentResponse in admin endpoints:
  depth, path, parent_id, is_edited, reply_count
- Update SQL queries in admin.py to fetch all required comment fields

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(social): Don't set updated_at when moderating comments

Address Copilot review feedback:
- updated_at should only be set when content is edited, not during
  moderation status changes
- This ensures is_edited accurately reflects whether the user edited
  their comment content, not whether an admin moderated it
- Removes semantic confusion where moderated comments appeared as "edited"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Use restrictive CORS default for Admin API (#519)

* fix(security): Use restrictive CORS default for Admin API (#433)

Change Admin API CORS to default to same-origin only (empty list) instead
of allowing all origins (*). This improves defense-in-depth security while
maintaining functionality for typical deployments where the admin UI and
API are served from the same origin.

Users who need cross-origin access can explicitly set VLOG_ADMIN_CORS_ORIGINS.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Add CORS validation and improve reliability (#433)

Address review feedback for Admin API CORS security fix:

- Add origin format validation at startup (fail fast with clear errors)
  - Validates protocol prefix (http:// or https://)
  - Rejects trailing slashes
  - Warns about mixing wildcard with specific origins (breaks session auth)

- Fix allow_credentials logic to match public API pattern
  - Credentials only enabled when origins are configured AND not using wildcard
  - Clearer variable names for readability

- Add startup logging for CORS configuration
  - Logs allowed origins or "same-origin only" message
  - Helps troubleshoot CORS issues

- Add migration guide to UPGRADING.md
  - Explains who is affected by the change
  - Provides clear action steps for affected users

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(config): Add CORS validation and credentials tests (#433)

Add comprehensive test coverage for the new CORS validation logic
as requested by Copilot review:

TestCorsOriginValidation:
- Empty origins allowed (same-origin only)
- Valid HTTP/HTTPS origins parsed correctly
- Wildcard origin allowed
- Missing protocol rejected with clear error
- Trailing slash rejected with clear error
- Mixed wildcard warns but allows
- Whitespace stripped from origins
- Empty entries from extra commas filtered

TestAdminCorsCredentials:
- Empty origins: credentials disabled
- Specific origins: credentials enabled
- Wildcard: credentials disabled (per CORS spec)
- Mixed wildcard + specific: credentials disabled

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Apply consistent error sanitization across endpoints (#435) (#520)

* fix(security): Apply consistent error sanitization across endpoints (#435)

Apply sanitize_error_message() consistently to prevent information
disclosure through error messages. This addresses CWE-209 by ensuring
internal details (file paths, database errors, stack traces) are not
exposed to API clients.

Changes:
- worker_api.py: Sanitize 6 error paths in HLS/reencode uploads
- admin.py: Sanitize 10 error paths in thumbnails, settings, custom fields
- auth/endpoints.py: Sanitize session refresh error

All original errors are now logged for debugging while clients receive
safe, sanitized messages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address review feedback on error sanitization

- Add ErrorLogging.SKIP_LOGGING to prevent double-logging
- Add missing FFmpeg stderr sanitization (api/admin.py:2416)

Reviewers identified that sanitize_error_message() logs by default,
causing duplicate log entries. Now we log explicitly first, then
pass SKIP_LOGGING to avoid redundant logging.

Also addresses Bruce's security finding: FFmpeg stderr in thumbnail
upload was exposed without sanitization.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Use static message for thumbnail upload errors

Address Copilot review feedback: sanitize_error_message() on FFmpeg
stderr could produce confusing output like "Invalid image file: Video
transcoding failed" because the sanitizer detects "ffmpeg" keyword.

Use a static, contextually-appropriate message instead. The raw FFmpeg
stderr is still logged for debugging.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): Require Admin API authentication by default (#431, #432) (#521)

* fix(security): Require Admin API authentication by default (#431, #432)

Issue #431: Admin API no longer allows unauthenticated access when
ADMIN_API_SECRET is not configured. Instead:
- If no users exist: only /api/auth/setup is accessible (503 for all else)
- If users exist: session authentication is required

Security improvements based on review feedback:
- Use permanent positive caching to prevent race condition attacks
- Fail-CLOSED on database errors (require auth, not allow setup)
- Replace /api/auth/* prefix skip with explicit public endpoint list
  to prevent bypass via /api/auth/users, /api/auth/invites, etc.
- Add logging for database errors in auth check
- Use efficient EXISTS query instead of COUNT(*)
- Normalize paths to handle trailing slashes

Issue #432: Worker API now logs a prominent warning at startup when
VLOG_WORKER_ADMIN_SECRET is not configured, informing operators that
management endpoints will return 503.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address PR review feedback

- Update startup log message to accurately describe which endpoints
  are accessible during setup mode (not just /api/auth/setup)
- Add comprehensive tests for Issue #431 authentication behavior:
  - No secret + no users: API returns 503, setup accessible
  - No secret + users exist: session auth required (401)
  - Public auth endpoints accessible in all states
  - Sensitive auth endpoints blocked in setup mode
  - DB errors fail closed (require auth, not allow setup)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(logging): Replace print statements with proper logging (#393, #380, #379) (#522)

* refactor(logging): Replace print statements with proper logging (#393, #380, #379)

Replace print() calls with Python's logging module across worker files
for consistent logging and better production observability.

Changes:
- api/database.py: Add logging import and replace 1 print statement
- worker/transcription.py: Add logging import and replace ~27 print statements
- worker/transcoder.py: Move logger definition earlier and replace ~80 print statements

All log messages use appropriate levels:
- logger.info() for normal operational messages
- logger.debug() for verbose/diagnostic messages
- logger.warning() for recoverable issues
- logger.error() for failures

Closes #393, #380, #379

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address code review feedback on import ordering and logging

- Move logger definitions after all imports in api/database.py and
  worker/transcription.py to comply with PEP 8 import ordering
- Combine split watchdog warning messages in transcoder.py
- Remove redundant TIMEOUT: and FAILURE: prefixes from log messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(backup): Implement backup and restore system (#216) (#523)

* feat(backup): Implement backup and restore system (#216)

Add comprehensive backup and restore functionality including:

- Database backup support for PostgreSQL (pg_dump) and SQLite
- Optional video file backup with incremental support
- S3 remote storage integration with multipart uploads
- Built-in scheduler daemon for automated backups
- Manifest signing with HMAC-SHA256 for integrity verification
- Restore with rollback guarantee (safety backup before restore)

New backup/ module with:
- service.py: Main BackupService orchestrator
- database.py: PostgreSQL/SQLite backup handlers
- files.py: Incremental video file backup with path validation
- s3.py: S3 storage with retry logic and server-side encryption
- manifest.py: Backup manifest with HMAC signing
- restore.py: Restoration with rollback guarantee
- verify.py: Integrity verification (checksums, signatures)
- scheduler.py: Backup scheduler daemon with health endpoint
- locking.py: File-based locking to prevent concurrent backups
- exceptions.py: Custom exception types

API endpoints:
- POST /api/v1/backups - Create backup
- GET /api/v1/backups - List backups
- GET /api/v1/backups/{id} - Get backup details
- POST /api/v1/backups/{id}/restore - Restore from backup
- POST /api/v1/backups/{id}/verify - Verify integrity
- DELETE /api/v1/backups/{id} - Delete backup

CLI commands:
- vlog backup create/list/restore/verify/delete/schedule

Security hardening:
- PGPASSWORD env var (never CLI args)
- Path validation to prevent traversal attacks
- Manifest signing for integrity
- Restore rate limiting (1/hour via API)
- Scheduler refuses to run as root

Reliability features:
- Pre-flight disk space checks
- Atomic operations (temp dir then rename)
- Configurable timeouts for all operations
- Exponential backoff retry for S3

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(backup): Address security, reliability, and performance issues (#216)

Security fixes:
- Fix tarfile extraction vulnerability (CVE-2007-4559 protection)
- Add backup ID format validation to prevent injection attacks
- Rename TimeoutError to BackupTimeoutError (avoid shadowing built-in)

Reliability fixes:
- Document file safety backup limitation with explicit acknowledgment
- Move restore rate limiting to database for multi-process safety
- Verify safety backup integrity before proceeding with restore
- Export all exceptions from backup module

Performance improvements:
- Parallelize S3 multipart uploads (4 concurrent workers)
- Combine statistics queries into single database query
- Increase checksum chunk size from 8KB to 1MB
- Update deprecated asyncio.get_event_loop() patterns

API changes:
- Add accept_no_file_rollback parameter to restore endpoints
- Add BackupRestoreRequest.accept_no_file_rollback field
- Add --accept-no-file-rollback CLI flag for restore command

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(deps): Pin urllib3>=2.6.3 to fix security vulnerabilities

Adds explicit pin for urllib3>=2.6.3 to fix:
- GHSA-38jv-5279-wg99
- GHSA-2xpw-w6gg-jr37
- GHSA-gm62-xv2j-4w53
- GHSA-pq67-6m6q-mj2v

Also adds wheel>=0.46.2 to pyproject.toml for consistency with
requirements.txt (CVE-2026-24049).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(backup): Address Copilot review comments (#216)

- Remove unused imports from backup modules
- Add Windows compatibility for file locking (msvcrt fallback)
- Add Windows compatibility for root/admin check in scheduler
- Use cross-platform shutil.disk_usage() for disk space checks
- Add comment explaining empty except block in scheduler
- Fix parameter name mismatch in admin API (delete_from_s3 -> delete_remote)
- Remove unused archive_checksum variable
- Improve path traversal check comment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: Code quality improvements (#385, #386, #395, #439) (#525)

* refactor: Code quality improvements (#385, #386, #395, #439)

- Extract magic numbers to named constants in analytics_cache.py (#385)
  - DEFAULT_CACHE_TTL_SECONDS, DEFAULT_CACHE_MAX_SIZE
  - REDIS_SOCKET_TIMEOUT, REDIS_CONNECT_TIMEOUT, REDIS_SCAN_BATCH_SIZE
  - CACHE_EVICTION_DIVISOR

- Add return type hints to dispatch methods in common.py (#386)
  - RequestIDMiddleware.dispatch -> Response
  - SecurityHeadersMiddleware.dispatch -> Response

- Add error handling to start-transcription.sh (#395)
  - Add set -e and set -u for safer execution
  - Require virtual environment (fail if missing)
  - Validate transcription.py script exists
  - Verify whisper module is installed

- Create require_valid_slug() helper to eliminate repetition (#439)
  - Add helper function in common.py
  - Replace 10 duplicate validation patterns in public.py
  - Consistent error messages: "Invalid {resource_type} slug"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: Address reviewer feedback (Conway, Margo)

Clarity improvements (Conway):
- Rename CACHE_EVICTION_DIVISOR to CACHE_EVICTION_RATIO (0.10)
  for clearer intent - "evict 10%" is more intuitive than "divide by 10"
- Add rationale comments for REDIS_SOCKET_TIMEOUT and REDIS_SCAN_BATCH_SIZE
  explaining the tradeoffs (latency vs memory vs round-trips)
- Add comment explaining why start-transcription.sh changes to script dir

Reliability improvements (Margo):
- Add Redis reconnection with exponential backoff
  - _maybe_reconnect() attempts reconnection after transient failures
  - Backoff from 1s to 60s max prevents hammering failing Redis
  - Connection auto-recovers without requiring API restart
- Add timeout protection to bulk Redis operations
  - clear() and get_stats() now timeout after 5 seconds
  - Prevents blocking on large keyspaces
  - Logs warning with partial progress on timeout
- Mark connection as failed on operation errors to trigger reconnection
- Improve slug validation error messages with specific reasons:
  - "Missing {type} slug" for empty input
  - "path traversal not allowed" for .. attempts
  - "must be lowercase alphanumeric with hyphens" for format errors
- Shell script improvements:
  - Capture and display activation errors for debugging
  - Add BASH_SOURCE fallback for shell compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address Copilot review feedback

Shell script changes:
- Revert to simpler patterns matching other startup scripts
- Use `source ... || {` instead of command substitution for activation
- Use `if ! python -c ... 2>/dev/null; then` for module checks
- Remove BASH_SOURCE fallback (all scripts use bash explicitly)

Documentation:
- Add note explaining intentional logic duplication in require_valid_slug()
  for providing specific error messages (missing vs path traversal vs format)

Copilot feedback addressed:
- [Fixed] Shell script patterns now match start-worker.sh exactly
- [Intentional] require_valid_slug() duplication: provides better UX with
  specific error messages per Margo's reliability review
- [Intentional] Reconnection/timeout logic: addresses critical reliability
  issues identified by Margo (Redis never recovering after failure)
- [Intentional] CACHE_EVICTION_RATIO naming: Conway specifically recommended
  this for clarity over the original DIVISOR suggestion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(live): Implement broadcaster dashboard/studio UI (#524)

Adds a comprehensive broadcaster dashboard for live stream management:

Backend:
- Stream health metrics infrastructure with Redis-based aggregation
- Real-time viewer tracking with server-generated session IDs
- Studio API endpoints with ownership checks
- SSE endpoint for real-time dashboard updates
- Background tasks for metrics aggregation and viewer cleanup
- Pub/sub channels for metrics and viewer count broadcasting

Database:
- Add owner_id to live_streams for ownership tracking
- Add live_stream_metrics table for aggregated health data
- Add live_stream_viewers table for viewer session tracking
- Add viewer count columns to live_streams

Frontend:
- Studio web app (Alpine.js/TypeScript/Vite)
- Real-time dashboard with SSE for live updates
- Stream health visualization and viewer counts
- Stream key regeneration with password re-entry

Security (per Bruce's review):
- Server-generated session IDs (256-bit entropy)
- HMAC-SHA256 IP hashing with per-instance secret
- Rate limiting on all public endpoints
- Password re-entry required for stream key operations

Observability (per Cid's review):
- Task health tracking with per-task intervals
- Redis availability logging
- Connection limit tracking for SSE

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: Update tailwindcss to v4.1.17

Vendor CSS library version bump.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: Suppress GitGuardian false positive in test file

The string "abcdefghijklmnop" is a test case for password validation,
not an actual secret. Added ggignore comment to suppress the warning.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: Add GitGuardian config to ignore test file false positives

Test files intentionally contain weak password strings to verify
password validation logic. These are not actual secrets.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: Address Copilot review feedback

- live_vod.py: Return immediately after JSON decode error for clearer flow
- analytics_cache.py: Clarify CACHE_EVICTION_RATIO comment with usage context

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
filthyrake added a commit that referenced this pull request Jan 30, 2026
…528)

* ci: bump actions/setup-node from 4 to 6

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...



* ci: bump actions/setup-node from 4 to 6

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...



* ci: bump actions/upload-artifact from 4 to 6

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v4...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...



* Add API versioning and OpenAPI documentation improvements (Issue #218) (#505)

* Add API versioning and OpenAPI documentation improvements (Issue #218)

- Add versioned API routes with /api/v1 prefix across all three APIs
- Create api/versioning.py module with utilities for:
  - Versioned router creation
  - Deprecation header support
  - OpenAPI schema customization
  - Version header middleware
- Add configuration options in config.py:
  - API_VERSION, API_SUPPORTED_VERSIONS
  - API_DEPRECATION_NOTICE, API_DEPRECATION_SUNSET
  - API_INCLUDE_LEGACY_ROUTES (backwards compatibility)
  - OPENAPI_* settings for documentation customization
- Refactor public, admin, and worker APIs to use APIRouter
- Add enhanced OpenAPI endpoint descriptions and summaries
- Maintain backwards compatibility with legacy /api routes

Closes #218



* Add security and reliability fixes to API versioning

Addresses feedback from code, security, and reliability reviews:

Security fixes:
- Add validate_config() with regex validation for API_VERSION format
- Add sanitize_header_value() to prevent CRLF header injection
- Validate all API_SUPPORTED_VERSIONS entries match expected format

Reliability fixes:
- Add error handling to VersionHeaderMiddleware.dispatch()
- Add error boundary to configure_openapi_schema() with fallback
- Add error handling to DeprecationHeadersRoute custom handler
- Fail fast on configuration errors at module load

Code quality:
- Remove unused datetime import
- Remove unused RESPONSE_EXAMPLES dict
- Update .env.example with new API versioning configuration



* Add GHSA-w853-jp5j-5j7f to security scan ignore list

The filelock TOCTOU vulnerability (CVE-2025-68146) fix requires
Python 3.10+, but CI runs on Python 3.9. The container images
use Python 3.11+ where the patched version is installed.

This is the same class of vulnerability as the already-ignored
GHSA-qmgc-5h2g-mvrw, just with a different advisory ID.



* Fix jaraco-context version in container build

Add --upgrade step after pip install to ensure security-patched
packages (filelock, jaraco-context) are at their required versions
even if transitive dependencies initially resolve to older versions.

Fixes Trivy container scan failure for GHSA-58pv-8j8x-9vj2.



* Use pip constraints file for security patches in container

Replace --upgrade approach with -c constraints.txt which forces
pip to respect minimum versions during dependency resolution.



* Address Copilot review feedback

- Fix Link header to use /docs (FastAPI's default docs location)
- Clarify sunset date format comment in .env.example
- Add comprehensive tests for API versioning (18 tests covering
  version headers, versioned endpoints, legacy routes, config
  validation, header sanitization, and OpenAPI schema)



* Add runtime stage pip upgrade for security patches

The builder stage installs correct versions, but dnf packages in
runtime stage may have old transitive dependencies. Add explicit
pip upgrade after COPY to ensure security-patched versions are used.



* Ignore setuptools vendored jaraco.context in Trivy scan

setuptools 80.9.0 vendors jaraco.context 5.3.0 internally. This
vendored copy is used only by setuptools for its own operations
and is not exposed to user input, making the path traversal
vulnerability (GHSA-58pv-8j8x-9vj2) unexploitable in this context.

The main jaraco-context package is correctly upgraded to 6.1.0.

Added .trivyignore file to skip this false positive in container
image scans.



---------



* feat(playback): Implement Up Next autoplay feature (Issue #211) (#506)

* feat(playback): Implement Up Next autoplay feature (Issue #211)

Add autoplay and "Up Next" functionality that automatically plays
the next recommended video when the current video ends.

Backend:
- Add playback configuration settings (VLOG_AUTOPLAY_ENABLED,
  VLOG_UPNEXT_ENABLED, VLOG_AUTOPLAY_COUNTDOWN_SECONDS)
- Create GET /api/config/playback endpoint for frontend config
- Create GET /api/videos/{slug}/next endpoint for single next video
- Add settings to KNOWN_SETTINGS and ENV_VAR_MAPPING for database
  settings migration support

Frontend:
- Add playback config and next video loading in watch.js
- Implement countdown overlay UI with cancel/play now buttons
- Add autoplay toggle checkbox (persisted to localStorage via
  VLogUtils.preferences)
- Hook video 'ended' event to trigger Up Next countdown
- Add responsive CSS for Up Next overlay

Features:
- Configurable countdown duration (default 10 seconds)
- User preference to enable/disable autoplay
- Global admin setting to enable/disable feature
- Cancel button to stop countdown and stay on current video
- Play Now button to skip countdown
- Fallback to related videos algorithm for next video selection



* fix(playback): Address specialist review feedback (Issue #211)

Frontend reliability fixes:
- Add try/catch around localStorage in toggleAutoplay()
- Validate nextVideo._href before navigation
- Rename playNextVideo() to navigateToNextVideo() for clarity
- Add AbortController to loadNextVideo() for request cancellation

Backend improvements:
- Change settings fetch failure logging from DEBUG to WARNING
- Refactor get_next_video to share code with get_related_videos (DRY)
- Parallelize tier queries with asyncio.gather() for limit=1
- Increase cache TTL from 30s to 300s to reduce DB load

Tests:
- Add comprehensive unit tests for /api/v1/videos/{slug}/next endpoint
- Add unit tests for /api/config/playback endpoint
- Test invalid slug patterns, deleted videos, null responses



* fix(playback): Address Copilot review feedback (Issue #211)

Frontend fixes:
- Clear existing countdown in startUpNextCountdown to prevent duplicate timers
- Add @click.prevent on video link to avoid double navigation
- Add keyboard accessibility: focus on Cancel button, Escape key handler
- Add accessibility attrs: role="dialog", aria-live, aria-labelledby, sr-only description
- Use @click.prevent on autoplay checkbox to prevent flickering on localStorage errors
- Validate slug from API with SLUG_PATTERN before constructing URLs
- Use encodeURIComponent when building next video URL

Backend fixes:
- Make VIDEO_LIST_CACHE_TTL configurable via environment variable



---------



* feat(live): Implement live streaming via HTTP segment push (#507)

* feat(live): Implement live streaming via HTTP segment push

Add live streaming capability without requiring RTMP/SRT infrastructure.
FFmpeg clients encode locally into HLS/CMAF segments and push via HTTP PUT.

Features:
- Stream key authentication with argon2id hashing
- HTTP segment push endpoints (PUT init.mp4, PUT seg_NNNN.m4s)
- Dynamic HLS playlist generation written to disk
- DVR window support with background cleanup
- Stale stream detection with grace period
- Automatic VOD recording on stream end (hardlink or copy segments)
- Rate limiting (global per-IP and segment-specific)
- Path containment verification for defense-in-depth

Admin API:
- POST /api/v1/live/streams - Create stream with stream key
- GET /api/v1/live/streams - List streams
- GET/PATCH/DELETE /api/v1/live/streams/{slug}
- POST /api/v1/live/streams/{slug}/regenerate-key
- POST /api/v1/live/streams/{slug}/end

Public API:
- GET /api/v1/live/streams - List active streams
- GET /live/{slug}/master.m3u8 - Master playlist
- GET /live/{slug}/{quality}/stream.m3u8 - Variant playlist

Ingest API:
- PUT /api/live/ingest/{slug}/{quality}/init.mp4
- PUT /api/live/ingest/{slug}/{quality}/seg_NNNN.m4s
- GET /api/live/ingest/{slug}/status

Includes vlog-live-push.sh script for secure FFmpeg streaming
with stream key passed via environment variable.



* fix(live): Address Copilot review feedback

- Fix unused imports in live_ingest.py, live_playlist.py, live_tasks.py, live_vod.py
- Add comments to silent except clauses explaining why they're silent
- Replace lambda capture patterns with functools.partial for clarity
- Reduce upload timeout from 720s to 600s
- Fix VOD duration calculation (was using seq*duration, now sums durations)
- Implement VOD recording in stale stream detection
- Add playlist updates after successful segment upload
- Update stream status to 'live' on init segment upload
- Fix path containment to use is_relative_to (Python 3.9+) with fallback
- Improve duplicate detection to use IntegrityError instead of generic Exception
- Add fire-and-forget task error callback in admin.py VOD recording
- Fix shell script INPUT_OPTS to use bash arrays for proper quoting
- Simplify bufsize calculation in shell script
- Fix playlist DVR window limiting logic



---------



* feat(theme): Add UI theme customization and branding (Issue #214) (#508)

* feat(theme): Add UI theme customization and branding (Issue #214)

Implement comprehensive theme and branding customization features:

Backend:
- Add branding, theme, and layout settings to KNOWN_SETTINGS
- Create public API endpoint GET /api/v1/config/theme for theme config
- Add branding endpoints for logo/favicon upload in admin API
- Create public endpoints for serving logo and favicon

Frontend (Public):
- Add theme.js loader for dynamic CSS variable injection
- Update index.html to use dynamic site name and logo
- Update footer to support custom footer text and links
- Add logo image CSS styles

Frontend (Admin):
- Add Branding tab to Settings page
- Implement logo and favicon upload with progress tracking
- Add site name and footer text editing
- Create branding API client and store methods

Settings added:
- branding.site_name, branding.logo_path, branding.favicon_path
- branding.footer_text, branding.footer_links
- theme.primary_color, theme.secondary_color, theme.accent_color
- theme.mode (light/dark/auto), theme.custom_css
- layout.homepage_style, layout.videos_per_page, layout.grid_columns
- layout.show_sidebar, layout.show_related_videos



* fix(security): Address security vulnerabilities in branding feature

Fixes identified by security review:

- Add path traversal protection to all file operations using validate_safe_path()
- Implement SVG sanitization for logo/favicon uploads to prevent XSS
- Add X-Content-Type-Options: nosniff header to all file responses
- Unify cache invalidation with asyncio.Lock to prevent race conditions
- Restrict custom CSS to CSS variables only via sanitize_custom_css()
- Add URL validation for footer links to block javascript: URLs
- Add 5-second fetch timeout to theme.js to prevent hanging

Issue #214



* fix: Address Copilot review comments on PR #508

- Fix :root regex pattern (was only matching bare colon)
- Remove redundant asyncio and re imports
- Add try/except around temp file cleanup to handle NameError
- Fix dictionary indentation in exception handler defaults
- Add aria-labels to delete buttons for accessibility
- Add for/id associations between labels and file inputs
- Update placeholder to remove hardcoded year
- Add reset_theme_settings_cache() to public.py
- Call cache reset when branding/theme settings are updated
- Invalidate public theme cache on all branding changes

Issue #214



---------



* fix(admin): Resolve CSP violations with constructable stylesheets (#509)

* fix(admin): Resolve CSP violations with constructable stylesheets

Migrate all web components to use constructable stylesheets instead of
inline <style> tags, which were being blocked by the strict Content
Security Policy (style-src 'self').

Changes:
- Migrate 18 web components to use adoptedStyleSheets API
- Add CSP-compatible navigation methods for Alpine.js
- Fix Alpine CSP parser errors for multi-statement @click expressions
- Update SettingsTab type to include branding and custom_fields tabs
- Add helper methods for CSP-compatible template expressions

Components migrated:
- vlog-alert, vlog-alert-container, vlog-badge, vlog-button
- vlog-card, vlog-dropzone, vlog-empty-state, vlog-filter
- vlog-hamburger, vlog-input, vlog-nav-drawer, vlog-search
- vlog-skeleton, vlog-tab-button, vlog-tab-panel, vlog-table
- vlog-tabs, vlog-video-card



* fix(admin): Address review feedback for CSP compliance PR

Changes based on code reviewer feedback:

1. Modal Component:
   - Migrate vlog-modal.ts to constructable stylesheets (CSP-compliant)
   - Replace inline body.style.overflow with classList.add/remove('modal-open')
   - Add .modal-open class to tokens.css for scroll locking

2. Navigation Methods (per Conway's clarity feedback):
   - Rename switchToWorkers -> openWorkersTab (clearer intent)
   - Rename switchToAnalytics -> openAnalyticsTab
   - Rename switchToSettings -> openSettingsTab
   - Rename switchToSettingsBranding -> openBrandingSettings
   - Rename switchToSettingsCustomFields -> openCustomFieldsSettings
   - Add comprehensive section header explaining CSP restrictions

3. Index.html Updates:
   - Use new navigation method names
   - Use tabClass() and settingsTabClass() helpers for CSP compliance
   - Remove semicolons from dropdown @click handlers

4. Type Updates:
   - Update SettingsTab type to include 'branding' and 'custom_fields'



* fix(admin): Complete CSP compliance for Alpine.js expressions

- Replace arrow functions with store helper methods:
  - hasActiveWorkers(), hasGlobalCustomFields(), getGlobalCustomFields()
  - hasCategoryCustomFields(), getCategoryCustomFields()
  - hasNullCategoryCustomFields(), getNullCategoryCustomFields()
  - handleFilesSelected() for dropzone events

- Replace regex literals with helper methods (CSP parser doesn't support /pattern/):
  - formatCategoryName(), formatCategoryTitle(), formatSettingLabel()

- Replace inline style attributes with CSS classes:
  - Add .w-pct-{0-100} percentage width classes
  - Add .grid-cols-5-minmax, .min-w-0, .aspect-16-9, .aspect-content utilities
  - Remove all style="" attributes from index.html

- Fix settings tab button styling consistency:
  - Update settingsTabClass() to match dynamic category buttons



---------



* feat(auth): Add multi-user authentication with RBAC (Issue #200) (#510)

* feat(auth): Add multi-user authentication with RBAC (Issue #200)

Implements comprehensive user authentication replacing the legacy single
admin secret with proper user accounts, sessions, and role-based access
control.

Key features:
- User accounts with username/email and argon2id password hashing
- Role-based access control (Admin, Editor, Viewer)
- Session-based browser auth with HTTP-only cookies
- Refresh token rotation with theft detection
- API keys for programmatic access
- OIDC integration for self-hosted identity providers
- Invite-only registration system
- Account lockout after failed login attempts
- CLI commands for user management (vlog auth migrate, create-admin, etc.)

Backend:
- New api/auth/ module with sessions, permissions, endpoints
- Database migration for users, sessions, api_keys, invites tables
- Session cleanup scheduled task for expired sessions
- Video ownership tracking (owner_id on videos table)

Frontend:
- Redesigned login page with username/password form
- User profile management (password change, API keys, sessions)
- User management page for admins (create, edit, disable users)
- Invite management for user registration

Testing & Documentation:
- 50 comprehensive tests covering auth flows
- Full documentation in docs/AUTHENTICATION.md



* fix(auth): Address comprehensive review feedback for authentication system

Security fixes (Bruce):
- Remove password reset token logging in endpoints.py
- Remove token from force password reset API response in users.py
- Add Depends() to OIDC link/unlink endpoints
- Implement OIDC nonce validation to prevent replay attacks
- Add SESSION_SECRET_KEY startup validation with minimum length

Performance fixes (Brendan):
- Use SHA-256 for session/API token hashing instead of argon2id
- Add token_prefix columns for O(1) indexed database lookup
- Support legacy argon2id hashes for backward compatibility

Reliability fixes (Margo):
- Add asyncio.Lock for thread-safe circuit breaker operations
- Wrap session refresh and invite acceptance in database transactions
- Extend cleanup task to purge expired OIDC states and password reset tokens

Code clarity improvements (Conway):
- Update migration to include token_prefix and last_used_at columns
- Add is_sha256_hash() helper for hash type detection



* test(auth): Add VLOG_SESSION_SECRET_KEY to test configuration

Required after adding startup validation for session secret key.



* chore(auth): Remove unused imports and fix code style

Address Copilot review comments:
- Remove unused imports across auth modules (Query, Depends, Set, etc.)
- Remove unused config imports (USER_REFRESH_TOKEN_EXPIRY_DAYS, etc.)
- Remove unused variable assignment in cli/main.py
- Fix test import style: use module reference pattern consistently
  instead of mixed 'import' and 'from import' for sessions module



* fix(test): Use non-secret placeholder for test session key

Replace string literal with generated value to avoid GitGuardian
false positive on test credentials.



---------



* feat(auth): Add setup wizard for first-time admin creation

Adds a web-based setup wizard that appears when no users exist,
allowing the first admin account to be created via the UI instead
of requiring CLI access.

Backend:
- GET /api/v1/auth/setup - Check if setup is needed
- POST /api/v1/auth/setup - Create initial admin (only works when
  no users exist, returns 403 afterward)

Frontend:
- Setup wizard modal with username, email, password fields
- Auto-login after successful account creation
- Integrated with existing auth flow



* fix(auth): Resolve session persistence and login issues

- Remove legacy /auth/login and /auth/check endpoints in admin.py that
  were overriding the new user-based auth endpoints
- Update AuthCheckResponse to return nested user object with permissions,
  auth_required, auth_mode, and OIDC settings
- Update LoginRequest to accept username_or_email for flexible login
- Fix login handler to look up users by either username or email
- Add missing 'os' import in auth/endpoints.py
- Add failed_login_attempts=0 to setup wizard user creation
- Fix Alpine.js CSP compatibility: replace optional chaining (?.) with
  CSP-safe expressions throughout index.html
- Use <template x-if> instead of x-show for user-dependent UI sections
  to prevent evaluation errors when currentUser is null
- Fix TypeScript error: use undefined instead of null for optional fields



* fix(public-ui): Resolve Alpine.js CSP parser errors

Replace complex expressions with precomputed values for CSP compatibility:
- Add _showError, _showContent, _showEmptyState to category.js and tag.js
- Add _showFeaturedSection to home.js
- Add updateContentUIState() method to update UI state flags
- Update HTML files to use precomputed boolean values

The Alpine.js CSP build cannot parse negation (!), optional chaining (?.),
or complex boolean expressions (&&, ||) in x-show directives.



* fix(auth): Address code review findings from specialized agents (#511)

* fix(auth): Address code review findings from specialized agents

Backend fixes (api/auth/endpoints.py):
- Move sqlalchemy or_ and func imports to top-level (was inline in function)
- Add case-insensitive username/email matching with func.lower()
- Hash identifier in security logs to avoid PII exposure (GDPR compliance)
- Cache OIDC env vars at module level to avoid repeated lookups
- Move validate_session_token and get_role_permissions imports to top-level

Frontend fixes:
- Fix broken HTML div nesting in admin/index.html (extra closing tag)
- Fix _showFeaturedSection race condition in home.js init()

Addresses findings from: Gafton, Bruce, Margo, Brendan, Conway reviews.



* fix(auth): Address reliability findings from Margo's review

Reliability improvements:

1. Login flow atomicity (endpoints.py):
   - Move _reset_failed_login() to AFTER session creation succeeds
   - Ensures atomic state: if session creation fails, login count stays

2. Setup wizard TOCTOU fix (endpoints.py):
   - Wrap check-and-insert in database transaction
   - Handle unique constraint violations from concurrent requests
   - Session creation now within same transaction

3. Session limit race condition (sessions.py):
   - Use FOR UPDATE row locking in _enforce_session_limit()
   - Wrap session creation in transaction with limit enforcement
   - Prevents concurrent logins from exceeding limit

4. Database error handling (endpoints.py):
   - Add try/except around user lookup in login
   - Add try/except around setup status check
   - Return 503 with helpful message on database errors

5. Bare except:pass fix (sessions.py):
   - Add proper logging for last_used_at update failures
   - Helps detect database health issues in monitoring

6. Lockout state cleanup (endpoints.py):
   - Clear expired locked_until when user attempts login
   - Reset failed_login_attempts on expired lockout
   - Non-blocking with error logging



---------



* docs: Update documentation for multi-user authentication features (#512)

* docs: Update documentation for multi-user authentication features

Updates documentation to reflect recent major features:
- Multi-user authentication with RBAC (Issue #200)
- Setup wizard for first-time admin creation
- API key management
- User invite system
- OIDC/SSO integration

Changes by document:
- API.md: Add comprehensive auth API section (setup wizard, login,
  user management, API keys, invites)
- AUTHENTICATION.md: Add setup wizard documentation, update migration
  guide for new multi-user system
- ADMIN_UI_GUIDE.md: Add Users tab section, update login/setup docs
- CONFIGURATION.md: Add VLOG_SESSION_SECRET_KEY and related auth
  settings, deprecate VLOG_ADMIN_API_SECRET
- DEPLOYMENT.md: Update production checklist for auth requirements
- TROUBLESHOOTING.md: Add comprehensive auth troubleshooting section



* docs: Fix critical accuracy issues found in code review

Addresses blockers identified by gafton-distinguished-engineer review:

1. Login response schema: Fixed to flat object (not nested under "user")
2. Profile update: Removed email field, only display_name and avatar_url
   are accepted
3. Environment variables: Fixed names to match actual implementation
   - VLOG_REFRESH_EXPIRY_DAYS (not VLOG_REFRESH_TOKEN_EXPIRY_DAYS)
   - VLOG_LOCKOUT_THRESHOLD (not VLOG_LOGIN_LOCKOUT_THRESHOLD)
   - VLOG_LOCKOUT_DURATION_MINUTES (not VLOG_LOGIN_LOCKOUT_DURATION_MINUTES)
4. Password reset expiry: Fixed default to 1 hour (not 24 hours)
5. Database settings table: Clarified these are env vars, not DB settings
6. Invite URL: Clarified it returns relative path, not absolute URL
7. Column name: Fixed failed_login_count to failed_login_attempts



---------



* Chore(deps-dev): Bump lodash-es from 4.17.22 to 4.17.23 (#513)

Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.22 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits/4.17.23)

---
updated-dependencies:
- dependency-name: lodash-es
  dependency-version: 4.17.23
  dependency-type: indirect
...




* Chore(deps-dev): Bump lodash from 4.17.21 to 4.17.23 (#514)

Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](lodash/lodash@4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
...




* feat(logging): Add structured JSON logging with request context (#208) (#515)

* feat(logging): Add structured JSON logging with request context (#208)

Add centralized logging configuration with JSON output for production
and text format for development. Request context (request_id, client_ip,
user_agent) is automatically injected into all log messages.

Key features:
- python-json-logger for structured output
- SafeJSONEncoder handles non-serializable objects
- User-Agent sanitization prevents log injection
- try/finally ensures context cleanup on async exceptions
- Log file rotation with 0600 permissions for security
- Module-specific log level overrides via VLOG_LOG_LEVELS



* fix: Address PR review feedback and security audit

- Fix import ordering to satisfy ruff/isort (E402, I001)
- Make setup_logging() idempotent with _logging_configured flag
- Close handlers before removing to prevent file descriptor leaks
- Add SecureRotatingFileHandler for 0o600 permissions on rotation
- Use record.getMessage() instead of str(record.msg) in fallback
- Add comprehensive tests for sanitize_user_agent and logging
- Add VLOG_LOG_* env vars to .env.example
- Ignore GHSA-7gcm-g887-7qv7 (protobuf DoS, no fix available)



* fix(security): Add protobuf GHSA-7gcm-g887-7qv7 to pip-audit ignores

No patched version available for this DoS vulnerability in protobuf's
json_format.ParseDict(). VLog doesn't use this function - protobuf is
a transitive dependency of faster-whisper.



* fix(security): Pin wheel>=0.46.2 and add CVE ID to trivyignore

- Add wheel>=0.46.2 to fix CVE-2026-24049 privilege escalation
- Add CVE-2026-0994 to trivyignore (Trivy finds protobuf by CVE, not GHSA)



* fix(security): Add wheel>=0.46.2 to Docker security patches

Add wheel to the security-patched packages in both Dockerfiles to fix
CVE-2026-24049 privilege escalation vulnerability detected by Trivy.

The wheel package is installed by pip as a build dependency. The existing
requirements.txt change doesn't affect the Docker image because it uses
explicit package installation during build.



* fix(security): Ignore wheel CVE in setuptools vendored copy

Add CVE-2026-24049/GHSA-8rrh-rw8j-w5fx to .trivyignore for the vendored
wheel inside setuptools. The vulnerability is in wheel.cli.unpack which
we don't use on untrusted files. The standalone wheel package is already
upgraded to 0.46.2 - this ignore is only for the vendored copy that
setuptools bundles internally.

Similar to the existing jaraco.context ignore - waiting for setuptools
upstream to update their vendored dependencies.



---------



* feat(embed): Add video embed functionality (#210) (#516)

* feat(embed): Add video embed functionality (#210)

Add the ability to generate iframe embed codes for videos, allowing them
to be embedded on external websites with a minimal, responsive player.

Security features:
- Domain whitelist with 'self' default (frame-ancestors CSP)
- Rate limiting: 500/min per IP for embed routes
- Query parameter validation (start, autoplay, controls)
- View count protection: 5+ seconds minimum playback

Backend:
- Add /embed/{slug} route with CSP headers
- Add /api/v1/videos/{slug}/embed-code endpoint
- Add embed configuration to config.py and settings_service.py
- Add EmbedCodeResponse schema

Frontend:
- Create embed.html minimal player template
- Create embed-error.html for graceful error display
- Create embed.js with analytics tracking (source='embed')
- Create embed.css with responsive styles
- Extend share modal with Link/Embed tabs
- Add start time input and autoplay checkbox options
- Add live embed code generation and copy functionality



* fix(embed): Address code review findings from specialized agents (#210)

Security fixes (Bruce):
- Add validate_slug() to embed routes to prevent log injection
- Skip X-Frame-Options for /embed/ routes (CSP takes precedence)
- Add CSP domain validation to prevent header injection
- Use crypto.randomUUID() for secure session UUID generation

Reliability fixes (Margo):
- Add fetchWithTimeout() with configurable timeouts for all network requests
- Add retry logic with exponential backoff for video data fetch
- Add circuit breaker for analytics heartbeats (max 3 failures)
- Add proper cleanup with EmbedPlayer.destroy() and EmbedAnalytics.destroy()
- Handle pagehide event for mobile browser cleanup

Performance fixes (Brendan):
- Debounce embed code input handler (300ms) to reduce DOM thrashing
- Add proper interval cleanup to prevent memory leaks

Clarity fixes (Conway):
- Rename shareLinkTabContent/shareEmbedTabContent to shareTabLink/shareTabEmbed
- Rename parseTimeInput() to convertTimeFormatToSeconds() with JSDoc
- Extract _is_video_embeddable() and _build_embed_error_response() helpers
- Add start time validation with 24-hour maximum cap
- Add picture-in-picture to client-side iframe generation



* fix(embed): Address Copilot review comments (#210)

- Use DB-backed settings in build_embed_csp_frame_ancestors() instead of env vars
- Fix crypto check to use typeof to prevent ReferenceError
- Fix getCurrentQuality() to handle HLS.js with currentLevelIndex
- Fix quality level mapping bug: store originalIndex to map sorted display to unsorted player arrays
- Remove unsafe-inline from CSP by using CSS class for hidden state
- Replace inline style="display: none" with embed-hidden class



---------



* feat(workers): Add API key expiration and rotation support (#226) (#517)

* feat(workers): Add API key expiration and rotation support (#226)

Implement secure API key lifecycle management for workers:

- Add configurable key expiration (default 90 days, 0 = never)
- Add grace period for expired keys (default 4 hours)
- Add key rotation with overlap period (default 2 hours)
- Add 5-minute cooldown between rotations
- Add rate limiting (10 rotations/hour per worker)

New API endpoints:
- POST /api/v1/workers/{worker_id}/rotate - rotate a worker's key
- GET /api/v1/workers/expiring-keys - list keys expiring soon
- POST /api/v1/workers/revoke-expired - bulk revoke expired keys

New CLI commands:
- vlog worker rotate <worker-id> [--revoke-old]
- vlog worker expire-warning [--days N] [--include-grace]

New settings:
- workers.api_key_expiration_days (default: 90)
- workers.api_key_grace_period_hours (default: 4)
- workers.api_key_rotation_overlap_hours (default: 2)
- workers.api_key_expiration_warning_days (default: 14)

Security considerations from code review:
- Reduced defaults per security review (grace 24h→4h, overlap 24h→2h)
- Dry-run mode for bulk revoke operations
- Rate limiting and cooldown to prevent abuse
- Audit logging for all rotation events



* fix(workers): Address code review findings for API key rotation (#226)

Critical fixes from code review:
- Add SELECT FOR UPDATE locking to prevent race condition in rotate_worker_key()
- Wrap bulk_revoke_expired_keys() in transaction for atomicity
- Fix return type annotation (datetime → Optional[datetime])
- Remove unused Response import and dead _key_expiring/_key_id code
- Only shorten old key expiration, never extend it (preserve longer lifetimes)
- Add error logging for failed rotation attempts

Other improvements:
- Fix rate limit comment to clarify per-IP (not per-worker) behavior
- Return 400 error for invalid 'days' parameter instead of silent clamping
- Add 429 handling in CLI with user-friendly Retry-After message
- Add index on expires_at column for efficient expiration queries
- Add 11 unit tests for _check_key_expiration_with_grace() edge cases



* fix(workers): Address Copilot review comments (#226)

- Add expires_at index to database.py table definition (matches migration)
- Fix CLI worker_name truncation to match column width (18 → 20 chars)
- Remove unused key_expiring variable and associated comment
- Remove unused WORKER_KEY_EXPIRED audit action
- Fix rate limit docstring: "per IP per hour" (not per worker)
- Remove unused AsyncMock and patch imports from tests

Note: Integration tests for new endpoints deferred to follow-up work.



---------



* feat(social): Add comments and ratings system (#213)

Implement a comprehensive comments and ratings system with:

Backend:
- Database migration 031 with ltree extension for threaded comments
- Comments table with materialized path threading (max depth 5)
- Ratings table with composite PK (video_id, user_id)
- Per-video social toggles (NULL = inherit from global settings)
- Denormalized aggregates on videos table with triggers
- RBAC permissions for comments and ratings
- Global settings for social features configuration
- HTML sanitization with bleach library
- Public API endpoints for comments/ratings CRUD
- Admin API endpoints for moderation queue and actions

Admin UI:
- Social Features section in video edit modal
- Comments/Ratings toggles with 3 states (On/Off/Inherit)

Public UI:
- Ratings section near video title (stars or thumbs)
- Comments section with threaded replies
- Comment form with character limit
- Reply functionality up to 5 levels
- Timestamp links to seek video
- Load more pagination



* fix(social): Fix critical bugs in comments/ratings endpoints (#213) (#518)

* fix(social): Fix critical bugs in comments/ratings endpoints (#213)

Fixes from code review findings:
- Change all comment/rating endpoints from video_id to slug parameter
  (fixes 422 validation errors - frontend sends slugs, not IDs)
- Add get_social_settings_by_video_id helper for internal lookups
- Add 'reason' field to CommentModerate schema for audit trail
- Add missing required fields to CommentResponse in admin endpoints:
  depth, path, parent_id, is_edited, reply_count
- Update SQL queries in admin.py to fetch all required comment fields



* fix(social): Don't set updated_at when moderating comments

Address Copilot review feedback:
- updated_at should only be set when content is edited, not during
  moderation status changes
- This ensures is_edited accurately reflects whether the user edited
  their comment content, not whether an admin moderated it
- Removes semantic confusion where moderated comments appeared as "edited"



---------



* fix(security): Use restrictive CORS default for Admin API (#519)

* fix(security): Use restrictive CORS default for Admin API (#433)

Change Admin API CORS to default to same-origin only (empty list) instead
of allowing all origins (*). This improves defense-in-depth security while
maintaining functionality for typical deployments where the admin UI and
API are served from the same origin.

Users who need cross-origin access can explicitly set VLOG_ADMIN_CORS_ORIGINS.



* fix(security): Add CORS validation and improve reliability (#433)

Address review feedback for Admin API CORS security fix:

- Add origin format validation at startup (fail fast with clear errors)
  - Validates protocol prefix (http:// or https://)
  - Rejects trailing slashes
  - Warns about mixing wildcard with specific origins (breaks session auth)

- Fix allow_credentials logic to match public API pattern
  - Credentials only enabled when origins are configured AND not using wildcard
  - Clearer variable names for readability

- Add startup logging for CORS configuration
  - Logs allowed origins or "same-origin only" message
  - Helps troubleshoot CORS issues

- Add migration guide to UPGRADING.md
  - Explains who is affected by the change
  - Provides clear action steps for affected users



* test(config): Add CORS validation and credentials tests (#433)

Add comprehensive test coverage for the new CORS validation logic
as requested by Copilot review:

TestCorsOriginValidation:
- Empty origins allowed (same-origin only)
- Valid HTTP/HTTPS origins parsed correctly
- Wildcard origin allowed
- Missing protocol rejected with clear error
- Trailing slash rejected with clear error
- Mixed wildcard warns but allows
- Whitespace stripped from origins
- Empty entries from extra commas filtered

TestAdminCorsCredentials:
- Empty origins: credentials disabled
- Specific origins: credentials enabled
- Wildcard: credentials disabled (per CORS spec)
- Mixed wildcard + specific: credentials disabled



---------



* fix(security): Apply consistent error sanitization across endpoints (#435) (#520)

* fix(security): Apply consistent error sanitization across endpoints (#435)

Apply sanitize_error_message() consistently to prevent information
disclosure through error messages. This addresses CWE-209 by ensuring
internal details (file paths, database errors, stack traces) are not
exposed to API clients.

Changes:
- worker_api.py: Sanitize 6 error paths in HLS/reencode uploads
- admin.py: Sanitize 10 error paths in thumbnails, settings, custom fields
- auth/endpoints.py: Sanitize session refresh error

All original errors are now logged for debugging while clients receive
safe, sanitized messages.



* fix: Address review feedback on error sanitization

- Add ErrorLogging.SKIP_LOGGING to prevent double-logging
- Add missing FFmpeg stderr sanitization (api/admin.py:2416)

Reviewers identified that sanitize_error_message() logs by default,
causing duplicate log entries. Now we log explicitly first, then
pass SKIP_LOGGING to avoid redundant logging.

Also addresses Bruce's security finding: FFmpeg stderr in thumbnail
upload was exposed without sanitization.



* fix: Use static message for thumbnail upload errors

Address Copilot review feedback: sanitize_error_message() on FFmpeg
stderr could produce confusing output like "Invalid image file: Video
transcoding failed" because the sanitizer detects "ffmpeg" keyword.

Use a static, contextually-appropriate message instead. The raw FFmpeg
stderr is still logged for debugging.



---------



* fix(security): Require Admin API authentication by default (#431, #432) (#521)

* fix(security): Require Admin API authentication by default (#431, #432)

Issue #431: Admin API no longer allows unauthenticated access when
ADMIN_API_SECRET is not configured. Instead:
- If no users exist: only /api/auth/setup is accessible (503 for all else)
- If users exist: session authentication is required

Security improvements based on review feedback:
- Use permanent positive caching to prevent race condition attacks
- Fail-CLOSED on database errors (require auth, not allow setup)
- Replace /api/auth/* prefix skip with explicit public endpoint list
  to prevent bypass via /api/auth/users, /api/auth/invites, etc.
- Add logging for database errors in auth check
- Use efficient EXISTS query instead of COUNT(*)
- Normalize paths to handle trailing slashes

Issue #432: Worker API now logs a prominent warning at startup when
VLOG_WORKER_ADMIN_SECRET is not configured, informing operators that
management endpoints will return 503.



* fix: Address PR review feedback

- Update startup log message to accurately describe which endpoints
  are accessible during setup mode (not just /api/auth/setup)
- Add comprehensive tests for Issue #431 authentication behavior:
  - No secret + no users: API returns 503, setup accessible
  - No secret + users exist: session auth required (401)
  - Public auth endpoints accessible in all states
  - Sensitive auth endpoints blocked in setup mode
  - DB errors fail closed (require auth, not allow setup)



---------



* refactor(logging): Replace print statements with proper logging (#393, #380, #379) (#522)

* refactor(logging): Replace print statements with proper logging (#393, #380, #379)

Replace print() calls with Python's logging module across worker files
for consistent logging and better production observability.

Changes:
- api/database.py: Add logging import and replace 1 print statement
- worker/transcription.py: Add logging import and replace ~27 print statements
- worker/transcoder.py: Move logger definition earlier and replace ~80 print statements

All log messages use appropriate levels:
- logger.info() for normal operational messages
- logger.debug() for verbose/diagnostic messages
- logger.warning() for recoverable issues
- logger.error() for failures

Closes #393, #380, #379



* fix: Address code review feedback on import ordering and logging

- Move logger definitions after all imports in api/database.py and
  worker/transcription.py to comply with PEP 8 import ordering
- Combine split watchdog warning messages in transcoder.py
- Remove redundant TIMEOUT: and FAILURE: prefixes from log messages



---------



* feat(backup): Implement backup and restore system (#216) (#523)

* feat(backup): Implement backup and restore system (#216)

Add comprehensive backup and restore functionality including:

- Database backup support for PostgreSQL (pg_dump) and SQLite
- Optional video file backup with incremental support
- S3 remote storage integration with multipart uploads
- Built-in scheduler daemon for automated backups
- Manifest signing with HMAC-SHA256 for integrity verification
- Restore with rollback guarantee (safety backup before restore)

New backup/ module with:
- service.py: Main BackupService orchestrator
- database.py: PostgreSQL/SQLite backup handlers
- files.py: Incremental video file backup with path validation
- s3.py: S3 storage with retry logic and server-side encryption
- manifest.py: Backup manifest with HMAC signing
- restore.py: Restoration with rollback guarantee
- verify.py: Integrity verification (checksums, signatures)
- scheduler.py: Backup scheduler daemon with health endpoint
- locking.py: File-based locking to prevent concurrent backups
- exceptions.py: Custom exception types

API endpoints:
- POST /api/v1/backups - Create backup
- GET /api/v1/backups - List backups
- GET /api/v1/backups/{id} - Get backup details
- POST /api/v1/backups/{id}/restore - Restore from backup
- POST /api/v1/backups/{id}/verify - Verify integrity
- DELETE /api/v1/backups/{id} - Delete backup

CLI commands:
- vlog backup create/list/restore/verify/delete/schedule

Security hardening:
- PGPASSWORD env var (never CLI args)
- Path validation to prevent traversal attacks
- Manifest signing for integrity
- Restore rate limiting (1/hour via API)
- Scheduler refuses to run as root

Reliability features:
- Pre-flight disk space checks
- Atomic operations (temp dir then rename)
- Configurable timeouts for all operations
- Exponential backoff retry for S3



* fix(backup): Address security, reliability, and performance issues (#216)

Security fixes:
- Fix tarfile extraction vulnerability (CVE-2007-4559 protection)
- Add backup ID format validation to prevent injection attacks
- Rename TimeoutError to BackupTimeoutError (avoid shadowing built-in)

Reliability fixes:
- Document file safety backup limitation with explicit acknowledgment
- Move restore rate limiting to database for multi-process safety
- Verify safety backup integrity before proceeding with restore
- Export all exceptions from backup module

Performance improvements:
- Parallelize S3 multipart uploads (4 concurrent workers)
- Combine statistics queries into single database query
- Increase checksum chunk size from 8KB to 1MB
- Update deprecated asyncio.get_event_loop() patterns

API changes:
- Add accept_no_file_rollback parameter to restore endpoints
- Add BackupRestoreRequest.accept_no_file_rollback field
- Add --accept-no-file-rollback CLI flag for restore command



* fix(deps): Pin urllib3>=2.6.3 to fix security vulnerabilities

Adds explicit pin for urllib3>=2.6.3 to fix:
- GHSA-38jv-5279-wg99
- GHSA-2xpw-w6gg-jr37
- GHSA-gm62-xv2j-4w53
- GHSA-pq67-6m6q-mj2v

Also adds wheel>=0.46.2 to pyproject.toml for consistency with
requirements.txt (CVE-2026-24049).



* fix(backup): Address Copilot review comments (#216)

- Remove unused imports from backup modules
- Add Windows compatibility for file locking (msvcrt fallback)
- Add Windows compatibility for root/admin check in scheduler
- Use cross-platform shutil.disk_usage() for disk space checks
- Add comment explaining empty except block in scheduler
- Fix parameter name mismatch in admin API (delete_from_s3 -> delete_remote)
- Remove unused archive_checksum variable
- Improve path traversal check comment



---------



* refactor: Code quality improvements (#385, #386, #395, #439) (#525)

* refactor: Code quality improvements (#385, #386, #395, #439)

- Extract magic numbers to named constants in analytics_cache.py (#385)
  - DEFAULT_CACHE_TTL_SECONDS, DEFAULT_CACHE_MAX_SIZE
  - REDIS_SOCKET_TIMEOUT, REDIS_CONNECT_TIMEOUT, REDIS_SCAN_BATCH_SIZE
  - CACHE_EVICTION_DIVISOR

- Add return type hints to dispatch methods in common.py (#386)
  - RequestIDMiddleware.dispatch -> Response
  - SecurityHeadersMiddleware.dispatch -> Response

- Add error handling to start-transcription.sh (#395)
  - Add set -e and set -u for safer execution
  - Require virtual environment (fail if missing)
  - Validate transcription.py script exists
  - Verify whisper module is installed

- Create require_valid_slug() helper to eliminate repetition (#439)
  - Add helper function in common.py
  - Replace 10 duplicate validation patterns in public.py
  - Consistent error messages: "Invalid {resource_type} slug"



* refactor: Address reviewer feedback (Conway, Margo)

Clarity improvements (Conway):
- Rename CACHE_EVICTION_DIVISOR to CACHE_EVICTION_RATIO (0.10)
  for clearer intent - "evict 10%" is more intuitive than "divide by 10"
- Add rationale comments for REDIS_SOCKET_TIMEOUT and REDIS_SCAN_BATCH_SIZE
  explaining the tradeoffs (latency vs memory vs round-trips)
- Add comment explaining why start-transcription.sh changes to script dir

Reliability improvements (Margo):
- Add Redis reconnection with exponential backoff
  - _maybe_reconnect() attempts reconnection after transient failures
  - Backoff from 1s to 60s max prevents hammering failing Redis
  - Connection auto-recovers without requiring API restart
- Add timeout protection to bulk Redis operations
  - clear() and get_stats() now timeout after 5 seconds
  - Prevents blocking on large keyspaces
  - Logs warning with partial progress on timeout
- Mark connection as failed on operation errors to trigger reconnection
- Improve slug validation error messages with specific reasons:
  - "Missing {type} slug" for empty input
  - "path traversal not allowed" for .. attempts
  - "must be lowercase alphanumeric with hyphens" for format errors
- Shell script improvements:
  - Capture and display activation errors for debugging
  - Add BASH_SOURCE fallback for shell compatibility



* fix: Address Copilot review feedback

Shell script changes:
- Revert to simpler patterns matching other startup scripts
- Use `source ... || {` instead of command substitution for activation
- Use `if ! python -c ... 2>/dev/null; then` for module checks
- Remove BASH_SOURCE fallback (all scripts use bash explicitly)

Documentation:
- Add note explaining intentional logic duplication in require_valid_slug()
  for providing specific error messages (missing vs path traversal vs format)

Copilot feedback addressed:
- [Fixed] Shell script patterns now match start-worker.sh exactly
- [Intentional] require_valid_slug() duplication: provides better UX with
  specific error messages per Margo's reliability review
- [Intentional] Reconnection/timeout logic: addresses critical reliability
  issues identified by Margo (Redis never recovering after failure)
- [Intentional] CACHE_EVICTION_RATIO naming: Conway specifically recommended
  this for clarity over the original DIVISOR suggestion



---------



* feat(live): Implement broadcaster dashboard/studio UI (#524)

Adds a comprehensive broadcaster dashboard for live stream management:

Backend:
- Stream health metrics infrastructure with Redis-based aggregation
- Real-time viewer tracking with server-generated session IDs
- Studio API endpoints with ownership checks
- SSE endpoint for real-time dashboard updates
- Background tasks for metrics aggregation and viewer cleanup
- Pub/sub channels for metrics and viewer count broadcasting

Database:
- Add owner_id to live_streams for ownership tracking
- Add live_stream_metrics table for aggregated health data
- Add live_stream_viewers table for viewer session tracking
- Add viewer count columns to live_streams

Frontend:
- Studio web app (Alpine.js/TypeScript/Vite)
- Real-time dashboard with SSE for live updates
- Stream health visualization and viewer counts
- Stream key regeneration with password re-entry

Security (per Bruce's review):
- Server-generated session IDs (256-bit entropy)
- HMAC-SHA256 IP hashing with per-instance secret
- Rate limiting on all public endpoints
- Password re-entry required for stream key operations

Observability (per Cid's review):
- Task health tracking with per-task intervals
- Redis availability logging
- Connection limit tracking for SSE



* chore: Update tailwindcss to v4.1.17

Vendor CSS library version bump.



* chore: Suppress GitGuardian false positive in test file

The string "abcdefghijklmnop" is a test case for password validation,
not an actual secret. Added ggignore comment to suppress the warning.



* chore: Add GitGuardian config to ignore test file false positives

Test files intentionally contain weak password strings to verify
password validation logic. These are not actual secrets.



* refactor: Address Copilot review feedback

- live_vod.py: Return immediately after JSON decode error for clearer flow
- analytics_cache.py: Clarify CACHE_EVICTION_RATIO comment with usage context



---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
@filthyrake filthyrake mentioned this pull request Feb 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant