diff --git a/CLAUDE.md b/CLAUDE.md index 17bd6d4..2bda7ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,11 +23,10 @@ This is a Next.js web application (branded as "Live Stream Manager") that contro ### Technology Stack - **Frontend**: Next.js 15.1.6 with React 19, TypeScript, and custom CSS with glass morphism design -- **Backend**: Next.js API routes with authentication middleware +- **Backend**: Next.js API routes - **Database**: SQLite with sqlite3 driver - **OBS Integration**: obs-websocket-js for WebSocket communication with OBS Studio -- **Styling**: Consolidated CSS architecture with Solarized Dark theme, CSS custom properties, Tailwind CSS utilities, and accessible glass morphism components -- **Security**: API key authentication middleware for production deployments +- **Styling**: Solarized Dark theme with CSS custom properties, Tailwind CSS utilities, and accessible glass morphism components ### Project Structure - `/app` - Next.js App Router pages and API routes @@ -35,8 +34,7 @@ This is a Next.js web application (branded as "Live Stream Manager") that contro - `/streams` - Streams management page (add new streams and view existing) - `/teams` - Team management page - `/edit/[id]` - Individual stream editing -- `/components` - Reusable React components (Header, Footer, Dropdown, Toast, CollapsibleGroup) -- `/middleware.ts` - API authentication middleware for security +- `/components` - Reusable React components (Header, Footer, Dropdown, Toast) - `/lib` - Core utilities and database connection - `database.ts` - SQLite database initialization and connection management - `obsClient.js` - OBS WebSocket client with persistent connection management @@ -78,10 +76,6 @@ This is a Next.js web application (branded as "Live Stream Manager") that contro 10. **OBS Scene Control**: Direct scene switching controls with dynamic state tracking and real-time synchronization between UI and OBS -11. **Studio Mode Support**: Full preview/program scene management with transition controls for professional broadcasting - -12. **Collapsible Stream Groups**: Organized stream display with expandable team groups for better UI management - ### Environment Configuration - `FILE_DIRECTORY`: Directory for database and text files (default: ./files) - `OBS_WEBSOCKET_HOST`: OBS WebSocket host (default: 127.0.0.1) @@ -123,10 +117,9 @@ This is a Next.js web application (branded as "Live Stream Manager") that contro #### OBS Scene Control - `POST /api/setScene` - Switch OBS to specified scene layout (1-Screen, 2-Screen, 4-Screen) - `GET /api/getCurrentScene` - Get currently active OBS scene for state synchronization -- `POST /api/triggerTransition` - Trigger studio mode transition from preview to program (requires studio mode enabled) #### System Status -- `GET /api/obsStatus` - Real-time OBS connection, streaming, recording, and studio mode status +- `GET /api/obsStatus` - Real-time OBS connection and streaming status ### Database Schema @@ -195,9 +188,6 @@ See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructio ### Security Architecture **Authentication**: API key-based authentication protects all API endpoints through Next.js middleware -- Middleware intercepts all API requests when `API_KEY` is set -- Bypasses authentication for localhost in development -- Returns 401 for unauthorized requests **Input Validation**: Comprehensive validation using centralized security utilities in `/lib/security.ts`: - Screen parameter allowlisting prevents path traversal attacks @@ -222,8 +212,6 @@ See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructio - **Visual Feedback**: Clear "View Stream" links with proper contrast for accessibility - **Team Association**: Streams organized under teams with proper naming conventions - **Active Source Detection**: Properly reads current active sources from text files on page load and navigation -- **Collapsible Organization**: Streams grouped by team in expandable sections for cleaner UI -- **Enhanced Stream Display**: Shows stream status with preview/program indicators in studio mode ### Team & Group Management - **UUID-based Tracking**: Robust OBS group synchronization using scene UUIDs @@ -259,15 +247,12 @@ See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructio - **Error Recovery**: Graceful error handling with user-friendly messages - **Enhanced Footer**: Real-time team/stream counts, OBS connection status with visual indicators - **Optimistic Updates**: Immediate UI feedback with proper stream group name matching -- **Studio Mode Status**: Footer displays studio mode state with preview/program scene information -- **Transition Controls**: "Cut to Preview" button available when studio mode is active ### OBS Integration Improvements - **Text Size**: Team name overlays use 96pt font for better visibility - **Color Display**: Fixed background color display (#002b4b) using proper ABGR format - **Standardized APIs**: All endpoints use consistent `{ success: true, data: [...] }` response format - **Performance Optimization**: Reduced code duplication and improved API response handling -- **CSS Consolidation**: Eliminated repetitive styles, centralized theming in globals.css ### Developer Experience - **Type Safety**: Comprehensive TypeScript definitions throughout diff --git a/README.md b/README.md index ce5b8e9..d44ace2 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,19 @@ A professional [Next.js](https://nextjs.org) web application for managing live s ## Features -- **Studio Mode Support**: Full preview/program scene management with transition controls for professional broadcasting - **OBS Scene Control**: Switch between OBS layouts (1-Screen, 2-Screen, 4-Screen) with dynamic button states - **Multi-Screen Source Control**: Manage 7 different screen positions (large, left, right, and 4 corners) - **Real-time OBS Integration**: WebSocket connection with live status monitoring - **Enhanced Stream Management**: Create, edit, and delete streams with comprehensive OBS cleanup - **Team Organization**: Organize streams by teams with full CRUD operations and scene synchronization -- **Collapsible Stream Groups**: Organized stream display with expandable team groups for better UI management - **Comprehensive Deletion**: Remove streams/teams with complete OBS component cleanup (scenes, sources, text files) - **Audio Control**: Browser sources created with muted audio and OBS control enabled - **Modern UI**: Glass morphism design with responsive layout and accessibility features - **Professional Broadcasting**: Audio routing, scene management, and live status indicators - **Dual Integration**: WebSocket API + text file monitoring for maximum compatibility - **UUID-based Tracking**: Robust OBS group synchronization with rename-safe tracking -- **Enhanced Footer**: Real-time team/stream counts, OBS connection status, and studio mode indicators -- **API Security**: Optional API key authentication for production deployments -- **Optimized Performance**: Consolidated CSS architecture and standardized API responses +- **Enhanced Footer**: Real-time team/stream counts and OBS connection status +- **Optimized Performance**: Reduced code duplication and standardized API responses ## Quick Start @@ -112,22 +109,54 @@ npm run type-check # TypeScript validation - **Styling**: Custom CSS with glass morphism and Tailwind utilities - **CI/CD**: Forgejo workflows with self-hosted runners -## API Documentation +## API Endpoints -The application provides a comprehensive REST API for managing streams, teams, and OBS integration. +### Stream Management +- `GET /api/streams` - List all streams with team information +- `GET /api/streams/[id]` - Get individual stream details +- `POST /api/addStream` - Create new stream with browser source and team association +- `PUT /api/streams/[id]` - Update stream information +- `DELETE /api/streams/[id]` - Delete stream with comprehensive OBS cleanup: + - Removes stream's nested scene + - Deletes browser source + - Removes from all source switchers + - Clears text files referencing the stream -**📚 [Complete API Documentation](docs/API.md)** +### Source Control +- `POST /api/setActive` - Set active stream for screen position (writes team-prefixed name to text file) +- `GET /api/getActive` - Get currently active sources for all screen positions -Key endpoints include: -- Stream management (CRUD operations) -- Source control for 7 screen positions -- Team and OBS group management -- Scene switching and studio mode controls -- Real-time status monitoring +### Team Management +- `GET /api/teams` - Get all teams with group information and sync status +- `POST /api/teams` - Create new team with optional OBS scene creation +- `PUT /api/teams/[teamId]` - Update team name, group_name, or group_uuid +- `DELETE /api/teams/[teamId]` - Delete team with comprehensive OBS cleanup: + - Deletes team scene/group + - Removes team text source + - Deletes all associated stream scenes + - Removes all browser sources with team prefix + - Clears all related text files +- `GET /api/getTeamName` - Get team name by ID -All endpoints support API key authentication for production deployments. +### OBS Group/Scene Management +- `POST /api/createGroup` - Create OBS scene from team and store UUID +- `POST /api/syncGroups` - Synchronize all teams with OBS groups +- `GET /api/verifyGroups` - Verify database groups exist in OBS with UUID tracking + - Detects orphaned groups (excludes system scenes) + - Identifies name mismatches + - Shows sync status for all teams -See [`CLAUDE.md`](CLAUDE.md) for detailed architecture documentation and [`docs/API.md`](docs/API.md) for complete endpoint specifications. +### OBS Scene Control +- `POST /api/setScene` - Switch OBS to specified scene (1-Screen, 2-Screen, 4-Screen) +- `GET /api/getCurrentScene` - Get currently active OBS scene + +### System Status +- `GET /api/obsStatus` - Real-time OBS connection, streaming, and recording status + +### Authentication +All endpoints require API key authentication when `API_KEY` environment variable is set. + +See `CLAUDE.md` for detailed architecture documentation and implementation details. ## Known Issues @@ -138,7 +167,7 @@ See [`CLAUDE.md`](CLAUDE.md) for detailed architecture documentation and [`docs/ ### System Scene Exclusion Infrastructure scenes containing source switchers are excluded from orphaned group detection: -- 1-Screen, 2-Screen, 4-Screen, Starting, Ending, Audio, Movies, Resources +- 1-Screen, 2-Screen, 4-Screen, Starting, Ending, Audio, Movies - Additional scenes can be added to the `SYSTEM_SCENES` array in `/app/api/verifyGroups/route.ts` diff --git a/app/api/__tests__/streams.test.ts b/app/api/__tests__/streams.test.ts index 1e1eea1..41f6389 100644 --- a/app/api/__tests__/streams.test.ts +++ b/app/api/__tests__/streams.test.ts @@ -1,4 +1,4 @@ -// import { GET } from '../streams/route'; +import { GET } from '../streams/route'; // Mock the database module jest.mock('@/lib/database', () => ({ diff --git a/app/api/__tests__/teams.test.ts b/app/api/__tests__/teams.test.ts index d7e431b..bd09053 100644 --- a/app/api/__tests__/teams.test.ts +++ b/app/api/__tests__/teams.test.ts @@ -1,4 +1,4 @@ -// import { GET } from '../teams/route'; +import { GET } from '../teams/route'; // Mock the database module jest.mock('@/lib/database', () => ({ @@ -13,7 +13,7 @@ jest.mock('@/lib/apiHelpers', () => ({ status, json: async () => ({ success: true, data }), })), - createDatabaseError: jest.fn((operation) => ({ + createDatabaseError: jest.fn((operation, error) => ({ error: 'Database Error', status: 500, json: async () => ({ diff --git a/app/api/addStream/route.ts b/app/api/addStream/route.ts index 75a62a3..9347106 100644 --- a/app/api/addStream/route.ts +++ b/app/api/addStream/route.ts @@ -63,7 +63,7 @@ function generateOBSSourceName(teamSceneName: string, streamName: string): strin } export async function POST(request: NextRequest) { - let name: string, url: string, team_id: number, obs_source_name: string, lockSources: boolean; + let name: string, url: string, team_id: number, obs_source_name: string; // Parse and validate request body try { @@ -78,7 +78,6 @@ export async function POST(request: NextRequest) { } ({ name, url, team_id } = validation.data!); - lockSources = body.lockSources !== false; // Default to true if not specified } catch { return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }); @@ -126,7 +125,7 @@ export async function POST(request: NextRequest) { if (!sourceExists) { // Create stream group with text overlay - await createStreamGroup(groupName, name, teamInfo.team_name, url, lockSources); + await createStreamGroup(groupName, name, teamInfo.team_name, url); // Update team with group UUID if not set if (!teamInfo.group_uuid) { diff --git a/app/api/obsStatus/route.ts b/app/api/obsStatus/route.ts index 91c85f6..13e8dc4 100644 --- a/app/api/obsStatus/route.ts +++ b/app/api/obsStatus/route.ts @@ -19,11 +19,9 @@ export async function GET() { obsWebSocketVersion: string; }; currentScene?: string; - currentPreviewScene?: string; sceneCount?: number; streaming?: boolean; recording?: boolean; - studioModeEnabled?: boolean; error?: string; } = { host: OBS_HOST, @@ -60,31 +58,15 @@ export async function GET() { // Get recording status const recordStatus = await obs.call('GetRecordStatus'); - // Get studio mode status - const studioModeStatus = await obs.call('GetStudioModeEnabled'); - - // Get preview scene if studio mode is enabled - let currentPreviewScene; - if (studioModeStatus.studioModeEnabled) { - try { - const previewSceneInfo = await obs.call('GetCurrentPreviewScene'); - currentPreviewScene = previewSceneInfo.sceneName; - } catch (previewError) { - console.log('Could not get preview scene:', previewError); - } - } - connectionStatus.connected = true; connectionStatus.version = { obsVersion: versionInfo.obsVersion, obsWebSocketVersion: versionInfo.obsWebSocketVersion }; connectionStatus.currentScene = currentSceneInfo.sceneName; - connectionStatus.currentPreviewScene = currentPreviewScene; connectionStatus.sceneCount = sceneList.scenes.length; connectionStatus.streaming = streamStatus.outputActive; connectionStatus.recording = recordStatus.outputActive; - connectionStatus.studioModeEnabled = studioModeStatus.studioModeEnabled; } catch (err) { connectionStatus.error = err instanceof Error ? err.message : 'Unknown error occurred'; diff --git a/app/api/setScene/route.ts b/app/api/setScene/route.ts index 2363d32..548e003 100644 --- a/app/api/setScene/route.ts +++ b/app/api/setScene/route.ts @@ -32,30 +32,16 @@ export async function POST(request: NextRequest) { try { const obsClient = await getOBSClient(); - // Check if studio mode is active - const { studioModeEnabled } = await obsClient.call('GetStudioModeEnabled'); + // Switch to the requested scene + await obsClient.call('SetCurrentProgramScene', { sceneName }); - if (studioModeEnabled) { - // In studio mode, switch the preview scene - await obsClient.call('SetCurrentPreviewScene', { sceneName }); - console.log(`Successfully switched preview to scene: ${sceneName} (Studio Mode)`); - - return NextResponse.json({ - success: true, - data: { sceneName, studioMode: true }, - message: `Preview set to ${sceneName} layout (Studio Mode) - ready to transition` - }); - } else { - // Normal mode, switch program scene directly - await obsClient.call('SetCurrentProgramScene', { sceneName }); - console.log(`Successfully switched to scene: ${sceneName}`); - - return NextResponse.json({ - success: true, - data: { sceneName, studioMode: false }, - message: `Switched to ${sceneName} layout` - }); - } + console.log(`Successfully switched to scene: ${sceneName}`); + + return NextResponse.json({ + success: true, + data: { sceneName }, + message: `Switched to ${sceneName} layout` + }); } catch (obsError) { console.error('OBS WebSocket error:', obsError); return NextResponse.json( diff --git a/app/api/triggerTransition/route.ts b/app/api/triggerTransition/route.ts deleted file mode 100644 index 7136863..0000000 --- a/app/api/triggerTransition/route.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getOBSClient } from '../../../lib/obsClient'; - -export async function POST() { - try { - const obsClient = await getOBSClient(); - - // Check if studio mode is active - const { studioModeEnabled } = await obsClient.call('GetStudioModeEnabled'); - - if (!studioModeEnabled) { - return NextResponse.json( - { - success: false, - error: 'Studio mode is not enabled', - message: 'Studio mode must be enabled to trigger transitions' - }, - { status: 400 } - ); - } - - try { - // Trigger the studio mode transition (preview to program) - await obsClient.call('TriggerStudioModeTransition'); - console.log('Successfully triggered studio mode transition'); - - // Get the updated scene information after transition - const [programResponse, previewResponse] = await Promise.all([ - obsClient.call('GetCurrentProgramScene'), - obsClient.call('GetCurrentPreviewScene') - ]); - - return NextResponse.json({ - success: true, - data: { - programScene: programResponse.currentProgramSceneName, - previewScene: previewResponse.currentPreviewSceneName - }, - message: 'Successfully transitioned preview to program' - }); - } catch (obsError) { - console.error('OBS WebSocket error during transition:', obsError); - return NextResponse.json( - { - success: false, - error: 'Failed to trigger transition in OBS', - details: obsError instanceof Error ? obsError.message : 'Unknown error' - }, - { status: 500 } - ); - } - } catch (error) { - console.error('Error triggering transition:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to connect to OBS or trigger transition' - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index dac679e..e1107df 100644 --- a/app/globals.css +++ b/app/globals.css @@ -20,14 +20,6 @@ --solarized-red: #dc322f; --solarized-magenta: #d33682; --solarized-violet: #6c71c4; - - /* Gradient Custom Properties */ - --gradient-primary: linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan)); - --gradient-active: linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow)); - --gradient-danger: linear-gradient(135deg, var(--solarized-red), var(--solarized-orange)); - --gradient-preview: linear-gradient(135deg, var(--solarized-yellow), var(--solarized-orange)); - --gradient-transition: linear-gradient(135deg, var(--solarized-red), var(--solarized-magenta)); - --gradient-body: linear-gradient(135deg, #002b36 0%, #073642 50%, #002b36 100%); } /* Modern CSS Foundation */ @@ -42,8 +34,8 @@ html { } body { - background: var(--gradient-body); - color: var(--solarized-base1); + background: linear-gradient(135deg, #002b36 0%, #073642 50%, #002b36 100%); + color: #93a1a1; min-height: 100vh; line-height: 1.6; } @@ -73,8 +65,7 @@ body { } /* Glass Card Component */ -.glass, -.glass-panel { +.glass { background: rgba(7, 54, 66, 0.4); backdrop-filter: blur(10px); border: 1px solid rgba(88, 110, 117, 0.3); @@ -84,8 +75,8 @@ body { /* Modern Button System */ .btn { - background: var(--gradient-primary); - color: var(--solarized-base3); + background: linear-gradient(135deg, #268bd2, #2aa198); + color: #fdf6e3; border: none; padding: 12px 24px; border-radius: 12px; @@ -102,10 +93,9 @@ body { text-decoration: none; } -.btn.active, -.btn-success { - background: var(--gradient-active); - color: var(--solarized-base3); +.btn.active { + background: linear-gradient(135deg, #859900, #b58900); + color: #fdf6e3; box-shadow: 0 0 0 3px rgba(133, 153, 0, 0.5); transform: translateY(-1px); font-weight: 700; @@ -127,7 +117,7 @@ body { .btn-secondary { background: rgba(88, 110, 117, 0.3); border: 1px solid rgba(131, 148, 150, 0.4); - color: var(--solarized-base1); + color: #93a1a1; backdrop-filter: blur(10px); } @@ -137,14 +127,25 @@ body { box-shadow: 0 6px 20px rgba(88, 110, 117, 0.3); } +/* Success Button */ +.btn-success { + background: linear-gradient(135deg, #859900, #b58900); + color: #fdf6e3; +} + +.btn-success:hover { + background: linear-gradient(135deg, #b58900, #859900); + box-shadow: 0 6px 20px rgba(133, 153, 0, 0.4); +} + /* Danger Button */ .btn-danger { - background: var(--gradient-danger); - color: var(--solarized-base3); + background: linear-gradient(135deg, #dc322f, #cb4b16); + color: #fdf6e3; } .btn-danger:hover { - background: linear-gradient(135deg, var(--solarized-orange), var(--solarized-red)); + background: linear-gradient(135deg, #cb4b16, #dc322f); box-shadow: 0 6px 20px rgba(220, 50, 47, 0.4); } @@ -168,18 +169,6 @@ body { flex-shrink: 0; } -/* Scene Button Variants */ -.btn-scene-preview { - background: var(--gradient-preview); - color: var(--solarized-base3); -} - -.btn-scene-transition { - background: var(--gradient-transition); - color: var(--solarized-base3); - min-width: 120px; -} - /* Form spacing fixes since Tailwind gap classes aren't working */ .form-row { display: flex; @@ -205,7 +194,7 @@ body { border: 1px solid rgba(88, 110, 117, 0.4); border-radius: 12px; padding: 12px 16px; - color: var(--solarized-base1); + color: #93a1a1; width: 100%; transition: all 0.3s ease; } @@ -216,7 +205,7 @@ body { .input:focus { outline: none; - border-color: var(--solarized-blue); + border-color: #268bd2; box-shadow: 0 0 0 3px rgba(38, 139, 210, 0.2); } @@ -226,7 +215,7 @@ body { border: 1px solid rgba(88, 110, 117, 0.4); border-radius: 12px; padding: 12px 16px; - color: var(--solarized-base1); + color: #93a1a1; width: 100%; text-align: left; cursor: pointer; @@ -252,28 +241,6 @@ body { position: absolute; transform: translateZ(0); will-change: transform; - max-height: 400px; - overflow-y: auto; -} - -/* Custom scrollbar for dropdown */ -.dropdown-menu::-webkit-scrollbar { - width: 8px; -} - -.dropdown-menu::-webkit-scrollbar-track { - background: rgba(7, 54, 66, 0.5); - border-radius: 4px; -} - -.dropdown-menu::-webkit-scrollbar-thumb { - background: rgba(88, 110, 117, 0.6); - border-radius: 4px; - transition: background 0.2s ease; -} - -.dropdown-menu::-webkit-scrollbar-thumb:hover { - background: rgba(88, 110, 117, 0.8); } .dropdown-item { @@ -281,7 +248,7 @@ body { cursor: pointer; transition: all 0.2s ease; border-bottom: 1px solid rgba(88, 110, 117, 0.2); - color: var(--solarized-base1); + color: #93a1a1; } .dropdown-item:last-child { @@ -294,7 +261,7 @@ body { .dropdown-item.active { background: rgba(38, 139, 210, 0.3); - color: var(--solarized-base3); + color: #fdf6e3; } /* Icon Sizes */ @@ -389,22 +356,6 @@ body { margin-bottom: 32px; } -.ml-3 { - margin-left: 12px; -} - -.mr-1 { - margin-right: 4px; -} - -.mr-2 { - margin-right: 8px; -} - -.mr-4 { - margin-right: 16px; -} - .p-4 { padding: 16px; } @@ -415,134 +366,4 @@ body { .p-8 { padding: 32px; -} - -/* Collapsible Group Styles */ -.collapsible-group { - margin-bottom: 16px; -} - -.collapsible-header { - width: 100%; - background: rgba(7, 54, 66, 0.3); - border: 1px solid rgba(88, 110, 117, 0.3); - border-radius: 12px; - padding: 16px 20px; - cursor: pointer; - transition: all 0.3s ease; - margin-bottom: 1px; -} - -.collapsible-header:hover { - background: rgba(7, 54, 66, 0.5); - border-color: rgba(131, 148, 150, 0.4); -} - -.collapsible-header-content { - display: flex; - align-items: center; - gap: 12px; -} - -.collapsible-icon { - flex-shrink: 0; - transition: transform 0.3s ease; - color: var(--solarized-base1); -} - -.collapsible-icon.open { - transform: rotate(90deg); -} - -.collapsible-title { - flex: 1; - font-size: 18px; - font-weight: 600; - color: var(--solarized-base3); - text-align: left; - margin: 0; -} - -.collapsible-count { - background: rgba(38, 139, 210, 0.2); - color: var(--solarized-blue); - padding: 4px 12px; - border-radius: 20px; - font-size: 14px; - font-weight: 600; -} - -.collapsible-content { - overflow: hidden; - transition: all 0.3s ease; - max-height: 0; - opacity: 0; -} - -.collapsible-content.open { - max-height: 5000px; - opacity: 1; - margin-top: 12px; -} - -.collapsible-content-inner { - padding: 20px; - background: rgba(7, 54, 66, 0.2); - border: 1px solid rgba(88, 110, 117, 0.2); - border-radius: 12px; -} - -/* Solarized Color Utilities */ -.text-base03 { color: var(--solarized-base03); } -.text-base02 { color: var(--solarized-base02); } -.text-base01 { color: var(--solarized-base01); } -.text-base00 { color: var(--solarized-base00); } -.text-base0 { color: var(--solarized-base0); } -.text-base1 { color: var(--solarized-base1); } -.text-base2 { color: var(--solarized-base2); } -.text-base3 { color: var(--solarized-base3); } -.text-blue { color: var(--solarized-blue); } -.text-cyan { color: var(--solarized-cyan); } -.text-green { color: var(--solarized-green); } -.text-yellow { color: var(--solarized-yellow); } -.text-orange { color: var(--solarized-orange); } -.text-red { color: var(--solarized-red); } -.text-magenta { color: var(--solarized-magenta); } -.text-violet { color: var(--solarized-violet); } - -.bg-base03 { background-color: var(--solarized-base03); } -.bg-base02 { background-color: var(--solarized-base02); } -.bg-base01 { background-color: var(--solarized-base01); } -.bg-base00 { background-color: var(--solarized-base00); } -.bg-base0 { background-color: var(--solarized-base0); } -.bg-base1 { background-color: var(--solarized-base1); } -.bg-base2 { background-color: var(--solarized-base2); } -.bg-base3 { background-color: var(--solarized-base3); } -.bg-blue { background-color: var(--solarized-blue); } -.bg-cyan { background-color: var(--solarized-cyan); } -.bg-green { background-color: var(--solarized-green); } -.bg-yellow { background-color: var(--solarized-yellow); } -.bg-orange { background-color: var(--solarized-orange); } -.bg-red { background-color: var(--solarized-red); } -.bg-magenta { background-color: var(--solarized-magenta); } -.bg-violet { background-color: var(--solarized-violet); } - -.border-base01 { border-color: var(--solarized-base01); } -.border-base02 { border-color: var(--solarized-base02); } -.border-blue { border-color: var(--solarized-blue); } -.border-cyan { border-color: var(--solarized-cyan); } -.border-green { border-color: var(--solarized-green); } -.border-yellow { border-color: var(--solarized-yellow); } -.border-orange { border-color: var(--solarized-orange); } -.border-red { border-color: var(--solarized-red); } - -/* Border opacity utilities */ -.border-green\/30 { border-color: rgba(133, 153, 0, 0.3); } -.border-yellow\/30 { border-color: rgba(181, 137, 0, 0.3); } -.border-blue\/30 { border-color: rgba(38, 139, 210, 0.3); } -.border-red\/30 { border-color: rgba(220, 50, 47, 0.3); } - -/* Focus utilities */ -.focus\:outline-none:focus { outline: none; } -.focus\:ring-2:focus { box-shadow: 0 0 0 2px var(--solarized-blue); } -.focus\:ring-blue:focus { box-shadow: 0 0 0 2px var(--solarized-blue); } \ No newline at end of file +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 4b3dbbc..c3ab1bc 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,7 +2,7 @@ import './globals.css'; import Header from '@/components/Header'; import Footer from '@/components/Footer'; import { ErrorBoundary } from '@/components/ErrorBoundary'; -import { ApiKeyProvider } from '@/contexts/ApiKeyContext'; +import PerformanceDashboard from '@/components/PerformanceDashboard'; export const metadata = { title: 'Live Stream Manager', @@ -13,15 +13,14 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( - -
-
- - {children} - -
-