diff --git a/CLAUDE.md b/CLAUDE.md index faca2d8..ad09d70 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -205,6 +205,7 @@ See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructio - **Audio Control**: Browser sources created with "Control Audio via OBS" enabled and auto-muted - **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 ### Team & Group Management - **UUID-based Tracking**: Robust OBS group synchronization using scene UUIDs @@ -213,12 +214,13 @@ See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructio - Shared team text source - All associated stream scenes and sources - All browser sources with team prefix -- **Sync Verification**: Real-time verification of database-OBS group synchronization +- **Sync Verification**: Real-time verification of database-OBS group synchronization with system scene bypass - **Conflict Resolution**: UI actions to resolve sync issues (missing groups, name changes) - **Visual Indicators**: Clear status indicators for group linking and sync problems - 🆔 "Linked by UUID" - Group tracked by reliable UUID - 📝 "Name changed in OBS" - Group renamed in OBS, database needs update - ⚠️ "Not found in OBS" - Group in database but missing from OBS +- **System Scene Protection**: Infrastructure scenes (1-Screen, 2-Screen, 4-Screen, Starting, Ending, Audio, Movies, Resources) excluded from orphaned cleanup ### User Experience Improvements - **Toast Notifications**: Real-time feedback for all operations (success/error/info) @@ -227,13 +229,22 @@ See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructio - **Responsive Design**: Mobile-friendly interface with glass morphism styling - **Loading States**: Clear indicators during API operations - **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 + +### 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 ### Developer Experience - **Type Safety**: Comprehensive TypeScript definitions throughout -- **API Documentation**: Well-documented endpoints with clear parameter validation +- **API Standardization**: Consistent response formats across all endpoints with proper error handling - **Migration Scripts**: Database migration tools for schema updates - **Security**: Input validation, sanitization, and secure API design -- **Testing**: Comprehensive error handling and edge case management +- **Performance Monitoring**: Smart polling with visibility detection and performance tracking +- **Code Optimization**: Eliminated redundancies and consolidated common patterns ## Known Issues diff --git a/README.md b/README.md index f83b714..f375aa9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ A professional [Next.js](https://nextjs.org) web application for managing live s - **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 and OBS connection status +- **Optimized Performance**: Reduced code duplication and standardized API responses ## Quick Start diff --git a/app/api/getActive/route.ts b/app/api/getActive/route.ts index a54b4b4..a47825b 100644 --- a/app/api/getActive/route.ts +++ b/app/api/getActive/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; import fs from 'fs'; import path from 'path'; -// import config from '../../../config'; +import { createSuccessResponse, createErrorResponse, withErrorHandling } from '../../../lib/apiHelpers'; const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files') // Ensure directory exists @@ -10,7 +10,7 @@ if (!fs.existsSync(FILE_DIRECTORY)) { } console.log('using', FILE_DIRECTORY) -export async function GET() { +async function getActiveHandler() { try { const largePath = path.join(FILE_DIRECTORY, 'large.txt'); const leftPath = path.join(FILE_DIRECTORY, 'left.txt'); @@ -20,38 +20,27 @@ export async function GET() { const bottomLeftPath = path.join(FILE_DIRECTORY, 'bottomLeft.txt'); const bottomRightPath = path.join(FILE_DIRECTORY, 'bottomRight.txt'); - const tankPath = path.join(FILE_DIRECTORY, 'tank.txt'); - const treePath = path.join(FILE_DIRECTORY, 'tree.txt'); - const kittyPath = path.join(FILE_DIRECTORY, 'kitty.txt'); - const chickenPath = path.join(FILE_DIRECTORY, 'chicken.txt'); + const large = fs.existsSync(largePath) ? fs.readFileSync(largePath, 'utf-8').trim() : null; + const left = fs.existsSync(leftPath) ? fs.readFileSync(leftPath, 'utf-8').trim() : null; + const right = fs.existsSync(rightPath) ? fs.readFileSync(rightPath, 'utf-8').trim() : null; + const topLeft = fs.existsSync(topLeftPath) ? fs.readFileSync(topLeftPath, 'utf-8').trim() : null; + const topRight = fs.existsSync(topRightPath) ? fs.readFileSync(topRightPath, 'utf-8').trim() : null; + const bottomLeft = fs.existsSync(bottomLeftPath) ? fs.readFileSync(bottomLeftPath, 'utf-8').trim() : null; + const bottomRight = fs.existsSync(bottomRightPath) ? fs.readFileSync(bottomRightPath, 'utf-8').trim() : null; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const large = fs.existsSync(largePath) ? fs.readFileSync(largePath, 'utf-8') : null; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const left = fs.existsSync(leftPath) ? fs.readFileSync(leftPath, 'utf-8') : null; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const right = fs.existsSync(rightPath) ? fs.readFileSync(rightPath, 'utf-8') : null; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const topLeft = fs.existsSync(topLeftPath) ? fs.readFileSync(topLeftPath, 'utf-8') : null; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const topRight = fs.existsSync(topRightPath) ? fs.readFileSync(topRightPath, 'utf-8') : null; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const bottomLeft = fs.existsSync(bottomLeftPath) ? fs.readFileSync(bottomLeftPath, 'utf-8') : null; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const bottomRight = fs.existsSync(bottomRightPath) ? fs.readFileSync(bottomRightPath, 'utf-8') : null; - - const tank = fs.existsSync(tankPath) ? fs.readFileSync(tankPath, 'utf-8') : null; - const tree = fs.existsSync(treePath) ? fs.readFileSync(treePath, 'utf-8') : null; - const kitty = fs.existsSync(kittyPath) ? fs.readFileSync(kittyPath, 'utf-8') : null; - const chicken = fs.existsSync(chickenPath) ? fs.readFileSync(chickenPath, 'utf-8') : null; - - // For SaT - return NextResponse.json({ large, left, right, topLeft, topRight, bottomLeft, bottomRight }, {status: 201}) - // return NextResponse.json({ tank, tree, kitty, chicken }, {status: 201}) - } catch (error) { + return createSuccessResponse({ + large, + left, + right, + topLeft, + topRight, + bottomLeft, + bottomRight + }); + } catch (error) { console.error('Error reading active sources:', error); - return NextResponse.json({ error: 'Failed to read active sources' }, {status: 500}); - } + return createErrorResponse('Failed to read active sources', 500, 'Could not read source files', error); + } +} -} \ No newline at end of file +export const GET = withErrorHandling(getActiveHandler); \ No newline at end of file diff --git a/app/api/streams/route.ts b/app/api/streams/route.ts index ab368e3..b52a74e 100644 --- a/app/api/streams/route.ts +++ b/app/api/streams/route.ts @@ -1,13 +1,20 @@ import { NextResponse } from 'next/server'; import { getDatabase } from '../../../lib/database'; -import { Stream } from '@/types'; +import { StreamWithTeam } from '@/types'; import { TABLE_NAMES } from '../../../lib/constants'; import { createSuccessResponse, createDatabaseError, withErrorHandling } from '../../../lib/apiHelpers'; async function getStreamsHandler() { try { const db = await getDatabase(); - const streams: Stream[] = await db.all(`SELECT * FROM ${TABLE_NAMES.STREAMS}`); + const streams: StreamWithTeam[] = await db.all(` + SELECT + s.*, + t.team_name, + t.group_name + FROM ${TABLE_NAMES.STREAMS} s + LEFT JOIN ${TABLE_NAMES.TEAMS} t ON s.team_id = t.team_id + `); return createSuccessResponse(streams); } catch (error) { return createDatabaseError('fetch streams', error); diff --git a/app/api/verifyGroups/route.ts b/app/api/verifyGroups/route.ts index 40b6df0..1ed5515 100644 --- a/app/api/verifyGroups/route.ts +++ b/app/api/verifyGroups/route.ts @@ -12,7 +12,8 @@ const SYSTEM_SCENES: string[] = [ 'Starting', 'Ending', 'Audio', - 'Movies' + 'Movies', + 'Resources' ]; interface OBSScene { diff --git a/app/edit/[id]/page.tsx b/app/edit/[id]/page.tsx index 4422a66..706fc12 100644 --- a/app/edit/[id]/page.tsx +++ b/app/edit/[id]/page.tsx @@ -65,8 +65,7 @@ export default function EditStream() { team_id: streamData.team_id, }); - // Handle both old and new API response formats - const teams = teamsData.success ? teamsData.data : teamsData; + const teams = teamsData.data; // Map teams for dropdown setTeams( diff --git a/app/page.tsx b/app/page.tsx index a9125b7..24bfe97 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,17 +7,12 @@ import { useToast } from '@/lib/useToast'; import { ToastContainer } from '@/components/Toast'; import { useActiveSourceLookup, useDebounce, PerformanceMonitor } from '@/lib/performance'; -type Stream = { - id: number; - name: string; - obs_source_name: string; - url: string; -}; +import { StreamWithTeam } from '@/types'; type ScreenType = 'large' | 'left' | 'right' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; export default function Home() { - const [streams, setStreams] = useState([]); + const [streams, setStreams] = useState([]); const [activeSources, setActiveSources] = useState>({ large: null, left: null, @@ -82,8 +77,9 @@ export default function Home() { // Handle both old and new API response formats const streams = streamsData.success ? streamsData.data : streamsData; + const activeSources = activeData.success ? activeData.data : activeData; setStreams(streams); - setActiveSources(activeData); + setActiveSources(activeSources); } catch (error) { console.error('Error fetching data:', error); showError('Failed to Load Data', 'Could not fetch streams. Please refresh the page.'); @@ -100,9 +96,9 @@ export default function Home() { const handleSetActive = useCallback(async (screen: ScreenType, id: number | null) => { const selectedStream = streams.find((stream) => stream.id === id); - // Generate stream group name for optimistic updates + // Generate stream group name for optimistic updates - must match obsClient.js format const streamGroupName = selectedStream - ? `${selectedStream.name.toLowerCase().replace(/\s+/g, '_')}_stream` + ? `${selectedStream.team_name?.toLowerCase().replace(/\s+/g, '_') || 'unknown'}_${selectedStream.name.toLowerCase().replace(/\s+/g, '_')}_stream` : null; // Update local state immediately for optimistic updates diff --git a/app/streams/page.tsx b/app/streams/page.tsx index 8520c19..2f23e7c 100644 --- a/app/streams/page.tsx +++ b/app/streams/page.tsx @@ -40,9 +40,8 @@ export default function AddStream() { const teamsData = await teamsResponse.json(); const streamsData = await streamsResponse.json(); - // Handle both old and new API response formats - const teams = teamsData.success ? teamsData.data : teamsData; - const streams = streamsData.success ? streamsData.data : streamsData; + const teams = teamsData.data; + const streams = streamsData.data; // Map the API data to the format required by the Dropdown setTeams( diff --git a/app/teams/page.tsx b/app/teams/page.tsx index 7b877e4..bb8684a 100644 --- a/app/teams/page.tsx +++ b/app/teams/page.tsx @@ -41,9 +41,7 @@ export default function Teams() { try { const res = await fetch('/api/teams'); const data = await res.json(); - // Handle both old and new API response formats - const teams = data.success ? data.data : data; - setTeams(teams); + setTeams(data.data); } catch (error) { console.error('Error fetching teams:', error); } finally { diff --git a/components/Footer.tsx b/components/Footer.tsx index ce57d91..9d2e03c 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -163,7 +163,6 @@ export default function Footer() { )} - )} diff --git a/lib/obsClient.js b/lib/obsClient.js index 9adfe80..4402708 100644 --- a/lib/obsClient.js +++ b/lib/obsClient.js @@ -323,7 +323,7 @@ async function createTextSource(sceneName, textSourceName, text) { text, font: { face: 'Arial', - size: 72, + size: 96, style: 'Bold' }, color: 0xFFFFFFFF, // White text @@ -352,7 +352,7 @@ async function createTextSource(sceneName, textSourceName, text) { text, font: { face: 'Arial', - size: 72, + size: 96, style: 'Bold' }, color: 0xFFFFFFFF, // White text diff --git a/lib/performance.ts b/lib/performance.ts index 4355393..8e71fca 100644 --- a/lib/performance.ts +++ b/lib/performance.ts @@ -38,13 +38,17 @@ export function useThrottle unknown>( } // Memoized stream lookup utilities -export function createStreamLookupMaps(streams: Array<{ id: number; obs_source_name: string; name: string }>) { +export function createStreamLookupMaps(streams: Array<{ id: number; obs_source_name: string; name: string; team_name?: string; group_name?: string | null }>) { const sourceToIdMap = new Map(); - const idToStreamMap = new Map(); + const idToStreamMap = new Map(); streams.forEach(stream => { // Generate stream group name to match what's written to files - const streamGroupName = `${stream.name.toLowerCase().replace(/\s+/g, '_')}_stream`; + // Format: {team_name}_{stream_name}_stream (matching obsClient.js logic) + const cleanTeamName = stream.team_name ? stream.team_name.toLowerCase().replace(/\s+/g, '_') : 'unknown'; + const cleanStreamName = stream.name.toLowerCase().replace(/\s+/g, '_'); + const streamGroupName = `${cleanTeamName}_${cleanStreamName}_stream`; + sourceToIdMap.set(streamGroupName, stream.id); idToStreamMap.set(stream.id, stream); }); @@ -53,7 +57,7 @@ export function createStreamLookupMaps(streams: Array<{ id: number; obs_source_n } // Hook version for React components -export function useStreamLookupMaps(streams: Array<{ id: number; obs_source_name: string; name: string }>) { +export function useStreamLookupMaps(streams: Array<{ id: number; obs_source_name: string; name: string; team_name?: string; group_name?: string | null }>) { return useMemo(() => { return createStreamLookupMaps(streams); }, [streams]); @@ -61,7 +65,7 @@ export function useStreamLookupMaps(streams: Array<{ id: number; obs_source_name // Efficient active source lookup export function useActiveSourceLookup( - streams: Array<{ id: number; obs_source_name: string; name: string }>, + streams: Array<{ id: number; obs_source_name: string; name: string; team_name?: string; group_name?: string | null }>, activeSources: Record ) { const { sourceToIdMap } = useStreamLookupMaps(streams);