diff --git a/CLAUDE.md b/CLAUDE.md index ad09d70..faca2d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -205,7 +205,6 @@ 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 @@ -214,13 +213,12 @@ 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 with system scene bypass +- **Sync Verification**: Real-time verification of database-OBS group synchronization - **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) @@ -229,22 +227,13 @@ 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 Standardization**: Consistent response formats across all endpoints with proper error handling +- **API Documentation**: Well-documented endpoints with clear parameter validation - **Migration Scripts**: Database migration tools for schema updates - **Security**: Input validation, sanitization, and secure API design -- **Performance Monitoring**: Smart polling with visibility detection and performance tracking -- **Code Optimization**: Eliminated redundancies and consolidated common patterns +- **Testing**: Comprehensive error handling and edge case management ## Known Issues diff --git a/README.md b/README.md index f375aa9..f83b714 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,6 @@ 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 a47825b..a54b4b4 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 { createSuccessResponse, createErrorResponse, withErrorHandling } from '../../../lib/apiHelpers'; +// import config from '../../../config'; 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) -async function getActiveHandler() { +export async function GET() { try { const largePath = path.join(FILE_DIRECTORY, 'large.txt'); const leftPath = path.join(FILE_DIRECTORY, 'left.txt'); @@ -20,27 +20,38 @@ async function getActiveHandler() { const bottomLeftPath = path.join(FILE_DIRECTORY, 'bottomLeft.txt'); const bottomRightPath = path.join(FILE_DIRECTORY, 'bottomRight.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; + 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'); - return createSuccessResponse({ - large, - left, - right, - topLeft, - topRight, - bottomLeft, - bottomRight - }); - } catch (error) { + + // 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) { console.error('Error reading active sources:', error); - return createErrorResponse('Failed to read active sources', 500, 'Could not read source files', error); - } -} + return NextResponse.json({ error: 'Failed to read active sources' }, {status: 500}); + } -export const GET = withErrorHandling(getActiveHandler); \ No newline at end of file +} \ No newline at end of file diff --git a/app/api/streams/route.ts b/app/api/streams/route.ts index b52a74e..ab368e3 100644 --- a/app/api/streams/route.ts +++ b/app/api/streams/route.ts @@ -1,20 +1,13 @@ import { NextResponse } from 'next/server'; import { getDatabase } from '../../../lib/database'; -import { StreamWithTeam } from '@/types'; +import { Stream } 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: 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 - `); + const streams: Stream[] = await db.all(`SELECT * FROM ${TABLE_NAMES.STREAMS}`); 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 1ed5515..40b6df0 100644 --- a/app/api/verifyGroups/route.ts +++ b/app/api/verifyGroups/route.ts @@ -12,8 +12,7 @@ const SYSTEM_SCENES: string[] = [ 'Starting', 'Ending', 'Audio', - 'Movies', - 'Resources' + 'Movies' ]; interface OBSScene { diff --git a/app/edit/[id]/page.tsx b/app/edit/[id]/page.tsx index 706fc12..4422a66 100644 --- a/app/edit/[id]/page.tsx +++ b/app/edit/[id]/page.tsx @@ -65,7 +65,8 @@ export default function EditStream() { team_id: streamData.team_id, }); - const teams = teamsData.data; + // Handle both old and new API response formats + const teams = teamsData.success ? teamsData.data : teamsData; // Map teams for dropdown setTeams( diff --git a/app/page.tsx b/app/page.tsx index 24bfe97..a9125b7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,12 +7,17 @@ import { useToast } from '@/lib/useToast'; import { ToastContainer } from '@/components/Toast'; import { useActiveSourceLookup, useDebounce, PerformanceMonitor } from '@/lib/performance'; -import { StreamWithTeam } from '@/types'; +type Stream = { + id: number; + name: string; + obs_source_name: string; + url: string; +}; 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, @@ -77,9 +82,8 @@ 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(activeSources); + setActiveSources(activeData); } catch (error) { console.error('Error fetching data:', error); showError('Failed to Load Data', 'Could not fetch streams. Please refresh the page.'); @@ -96,9 +100,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 - must match obsClient.js format + // Generate stream group name for optimistic updates const streamGroupName = selectedStream - ? `${selectedStream.team_name?.toLowerCase().replace(/\s+/g, '_') || 'unknown'}_${selectedStream.name.toLowerCase().replace(/\s+/g, '_')}_stream` + ? `${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 2e4f52f..8520c19 100644 --- a/app/streams/page.tsx +++ b/app/streams/page.tsx @@ -40,8 +40,9 @@ export default function AddStream() { const teamsData = await teamsResponse.json(); const streamsData = await streamsResponse.json(); - const teams = teamsData.data; - const streams = streamsData.data; + // Handle both old and new API response formats + const teams = teamsData.success ? teamsData.data : teamsData; + const streams = streamsData.success ? streamsData.data : streamsData; // Map the API data to the format required by the Dropdown setTeams( @@ -66,37 +67,9 @@ export default function AddStream() { }, [fetchData]); - const extractTwitchUsername = (input: string): string => { - const trimmed = input.trim(); - - // If it's a URL, extract username - const urlPatterns = [ - /^https?:\/\/(www\.)?twitch\.tv\/([a-zA-Z0-9_]+)\/?$/, - /^(www\.)?twitch\.tv\/([a-zA-Z0-9_]+)\/?$/, - /^twitch\.tv\/([a-zA-Z0-9_]+)\/?$/ - ]; - - for (const pattern of urlPatterns) { - const match = trimmed.match(pattern); - if (match) { - return match[match.length - 1]; // Last capture group is always the username - } - } - - // Otherwise assume it's just a username - return trimmed; - }; - const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; - - // Special handling for twitch_username to extract from URL if needed - if (name === 'twitch_username') { - const username = extractTwitchUsername(value); - setFormData((prev) => ({ ...prev, [name]: username })); - } else { - setFormData((prev) => ({ ...prev, [name]: value })); - } + setFormData((prev) => ({ ...prev, [name]: value })); // Clear validation error when user starts typing if (validationErrors[name]) { @@ -241,7 +214,7 @@ export default function AddStream() { {/* Twitch Username */}
{validationErrors.twitch_username && (
diff --git a/app/teams/page.tsx b/app/teams/page.tsx index bb8684a..7b877e4 100644 --- a/app/teams/page.tsx +++ b/app/teams/page.tsx @@ -41,7 +41,9 @@ export default function Teams() { try { const res = await fetch('/api/teams'); const data = await res.json(); - setTeams(data.data); + // Handle both old and new API response formats + const teams = data.success ? data.data : data; + setTeams(teams); } catch (error) { console.error('Error fetching teams:', error); } finally { diff --git a/components/Footer.tsx b/components/Footer.tsx index 9d2e03c..ce57d91 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -163,6 +163,7 @@ export default function Footer() {
)} +
)} diff --git a/lib/obsClient.js b/lib/obsClient.js index 4402708..9adfe80 100644 --- a/lib/obsClient.js +++ b/lib/obsClient.js @@ -323,7 +323,7 @@ async function createTextSource(sceneName, textSourceName, text) { text, font: { face: 'Arial', - size: 96, + size: 72, style: 'Bold' }, color: 0xFFFFFFFF, // White text @@ -352,7 +352,7 @@ async function createTextSource(sceneName, textSourceName, text) { text, font: { face: 'Arial', - size: 96, + size: 72, style: 'Bold' }, color: 0xFFFFFFFF, // White text diff --git a/lib/performance.ts b/lib/performance.ts index 8e71fca..4355393 100644 --- a/lib/performance.ts +++ b/lib/performance.ts @@ -38,17 +38,13 @@ export function useThrottle unknown>( } // Memoized stream lookup utilities -export function createStreamLookupMaps(streams: Array<{ id: number; obs_source_name: string; name: string; team_name?: string; group_name?: string | null }>) { +export function createStreamLookupMaps(streams: Array<{ id: number; obs_source_name: string; name: string }>) { 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 - // 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`; - + const streamGroupName = `${stream.name.toLowerCase().replace(/\s+/g, '_')}_stream`; sourceToIdMap.set(streamGroupName, stream.id); idToStreamMap.set(stream.id, stream); }); @@ -57,7 +53,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; team_name?: string; group_name?: string | null }>) { +export function useStreamLookupMaps(streams: Array<{ id: number; obs_source_name: string; name: string }>) { return useMemo(() => { return createStreamLookupMaps(streams); }, [streams]); @@ -65,7 +61,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; team_name?: string; group_name?: string | null }>, + streams: Array<{ id: number; obs_source_name: string; name: string }>, activeSources: Record ) { const { sourceToIdMap } = useStreamLookupMaps(streams);