Skip to content

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

Merged
filthyrake merged 5 commits intodevfrom
feature/200-user-authentication
Jan 19, 2026
Merged

feat(auth): Add multi-user authentication with RBAC (Issue #200)#510
filthyrake merged 5 commits intodevfrom
feature/200-user-authentication

Conversation

@filthyrake
Copy link
Owner

Summary

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

  • User accounts with username/email and argon2id password hashing
  • Role-based access control (Admin, Editor, Viewer) with permission matrix
  • Session-based browser auth with HTTP-only cookies and refresh token rotation
  • API keys for programmatic access (inherit user role permissions)
  • OIDC integration for self-hosted identity providers (Keycloak, Authentik, etc.)
  • Invite-only registration system (configurable)
  • Security features: account lockout, brute force protection, session cleanup

Changes

Backend (api/auth/)

  • sessions.py - Session creation, validation, refresh token rotation with theft detection
  • password.py - Argon2id password hashing and validation
  • permissions.py - RBAC permission definitions and role mappings
  • middleware.py - Auth middleware and permission dependencies
  • endpoints.py - Login, logout, refresh, password reset endpoints
  • users.py - User management (CRUD, force password reset)
  • api_keys.py - API key management
  • oidc.py - Generic OIDC with circuit breaker
  • invite.py - Invite-only registration system

Database

  • Migration 029_add_user_authentication.py adds:
    • users table with roles, status, lockout tracking
    • user_sessions table with refresh token family tracking
    • user_api_keys table
    • user_invites table
    • oidc_connections and oidc_states tables
    • password_reset_tokens table
    • owner_id column on videos table

CLI

  • vlog auth migrate - Migrate from admin secret
  • vlog auth create-admin - Create admin user
  • vlog auth create-user - Create user with role
  • vlog auth list-users - List all users
  • vlog auth reset-password - Force password reset
  • vlog auth disable-user - Disable user account

Frontend

  • Redesigned login page with username/password form
  • User profile tab (password change, API keys, active sessions)
  • User management tab for admins (create, edit, disable users, invites)
  • Updated auth store with user state management

Testing & Documentation

  • 50 comprehensive tests in tests/test_user_auth.py
  • Full documentation in docs/AUTHENTICATION.md

Test plan

  • Run pytest tests/test_user_auth.py - all 50 tests pass
  • Run database migration alembic upgrade head
  • Test CLI: vlog auth create-admin creates initial admin
  • Test login flow in admin panel with username/password
  • Verify role-based menu visibility (admin vs editor)
  • Test session management (view sessions, revoke session)
  • Test API key creation and authentication
  • Test invite flow for new user registration
  • Test password reset flow
  • Verify session cleanup runs on startup and periodically

Closes #200

🤖 Generated with Claude Code

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>
@gitguardian
Copy link

gitguardian bot commented Jan 19, 2026

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
25266048 Triggered Generic Password 668fd32 tests/test_user_auth.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

filthyrake and others added 2 commits January 19, 2026 12:43
…ystem

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>
Required after adding startup validation for session secret key.

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 implements a comprehensive multi-user authentication system with role-based access control (RBAC), replacing the legacy single admin secret. The implementation includes proper user accounts, sessions with refresh token rotation, API keys, OIDC integration, and invite-only registration.

Changes:

  • Complete authentication backend with password hashing (argon2id), session management, and permission system
  • Database migration adding 7 new tables for users, sessions, API keys, OIDC, invites, and password reset tokens
  • Frontend stores and UI components for user management, profiles, and authentication
  • CLI commands for user management and migration from legacy admin secret
  • 50 comprehensive tests covering password hashing, permissions, sessions, and API keys

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 20 comments.

Show a summary per file
File Description
api/auth/*.py Core authentication modules (sessions, passwords, permissions, middleware, endpoints, OIDC, invites)
api/database.py Database table definitions for user authentication system
migrations/versions/029_add_user_authentication.py Database migration creating user auth tables
web/admin/src/stores/*.ts Frontend state management for users and authentication
web/admin/src/api/endpoints/*.ts Frontend API client for user endpoints
web/admin/index.html UI components for login, user management, profiles, invites
tests/test_user_auth.py Comprehensive test suite (50 tests)
docs/AUTHENTICATION.md Complete documentation with API reference and migration guide
cli/main.py CLI commands for user management
config.py Configuration for session secrets, OIDC, and password policies
api/admin.py Integration of auth routers and session cleanup

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

cli/main.py Outdated
# Assign orphan videos
from api.database import videos

result = await database.execute(
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Variable result is not used.

Suggested change
result = await database.execute(
await database.execute(

Copilot uses AI. Check for mistakes.
Comment on lines 425 to 428
from api.auth.sessions import create_user_session

session_token, refresh_token, expires_at, refresh_expires_at = (
await create_user_session(
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Module 'api.auth.sessions' is imported with both 'import' and 'import from'.

Suggested change
from api.auth.sessions import create_user_session
session_token, refresh_token, expires_at, refresh_expires_at = (
await create_user_session(
session_token, refresh_token, expires_at, refresh_expires_at = (
await sessions_module.create_user_session(

Copilot uses AI. Check for mistakes.
@pytest.mark.asyncio
async def test_validate_session_valid(self, test_database, sample_user, monkeypatch):
"""validate_session_token should return user for valid session."""
import api.auth.sessions as sessions_module
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Module 'api.auth.sessions' is imported with both 'import' and 'import from'.

Copilot uses AI. Check for mistakes.
@pytest.mark.asyncio
async def test_validate_session_invalid_token(self, test_database, monkeypatch):
"""validate_session_token should return None for invalid token."""
import api.auth.sessions as sessions_module
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Module 'api.auth.sessions' is imported with both 'import' and 'import from'.

Copilot uses AI. Check for mistakes.
@pytest.mark.asyncio
async def test_validate_session_short_token(self, test_database, monkeypatch):
"""validate_session_token should reject short tokens."""
import api.auth.sessions as sessions_module
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Module 'api.auth.sessions' is imported with both 'import' and 'import from'.

Copilot uses AI. Check for mistakes.
api/auth/oidc.py Outdated

from api.auth.middleware import REFRESH_COOKIE_NAME, SESSION_COOKIE_NAME, require_auth
from api.auth.password import generate_token, hash_password, hash_token, verify_token
from api.auth.sessions import create_user_session, invalidate_user_sessions
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Import of 'invalidate_user_sessions' is not used.

Suggested change
from api.auth.sessions import create_user_session, invalidate_user_sessions
from api.auth.sessions import create_user_session

Copilot uses AI. Check for mistakes.
api/auth/oidc.py Outdated
Comment on lines 45 to 48
USER_REFRESH_TOKEN_EXPIRY_DAYS,
USER_SESSION_EXPIRY_HOURS,
)

Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Import of 'USER_REFRESH_TOKEN_EXPIRY_DAYS' is not used.
Import of 'USER_SESSION_EXPIRY_HOURS' is not used.

Suggested change
USER_REFRESH_TOKEN_EXPIRY_DAYS,
USER_SESSION_EXPIRY_HOURS,
)
)

Copilot uses AI. Check for mistakes.
from __future__ import annotations

from enum import Enum
from typing import FrozenSet, Set
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Import of 'Set' is not used.

Suggested change
from typing import FrozenSet, Set
from typing import FrozenSet

Copilot uses AI. Check for mistakes.
Comment on lines 15 to 23
from api.auth.password import (
generate_token,
get_token_prefix,
hash_token,
hash_token_fast,
is_sha256_hash,
verify_token,
verify_token_fast,
)
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Import of 'hash_token' is not used.

Copilot uses AI. Check for mistakes.
"""

import uuid
from datetime import datetime, timedelta, timezone
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Import of 'timedelta' is not used.

Suggested change
from datetime import datetime, timedelta, timezone
from datetime import datetime, timezone

Copilot uses AI. Check for mistakes.
filthyrake and others added 2 commits January 19, 2026 13:05
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>
Replace string literal with generated value to avoid GitGuardian
false positive on test credentials.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@filthyrake filthyrake merged commit a39b562 into dev Jan 19, 2026
3 checks passed
@filthyrake filthyrake deleted the feature/200-user-authentication branch January 19, 2026 21:12
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

User authentication system (JWT/OAuth)

1 participant