From cbf8cd6516872c4d22ba5d516539f5c28140eb84 Mon Sep 17 00:00:00 2001 From: Decobus Date: Tue, 22 Jul 2025 14:25:46 -0400 Subject: [PATCH 1/6] Fix API response format compatibility on main page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The streams API now returns standardized format { success: true, data: [...] } but the frontend was still expecting the old direct array format. Added backward compatibility to handle both response formats: - New format: { success: true, data: streams } - Old format: streams (direct array) Fixes TypeError: streams.forEach is not a function 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/page.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/page.tsx b/app/page.tsx index 0e3ff66..a9125b7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -80,7 +80,9 @@ export default function Home() { activeRes.json() ]); - setStreams(streamsData); + // Handle both old and new API response formats + const streams = streamsData.success ? streamsData.data : streamsData; + setStreams(streams); setActiveSources(activeData); } catch (error) { console.error('Error fetching data:', error); -- 2.49.0 From 02cad6a319d59a2220c7569dfffce40a1b0656a1 Mon Sep 17 00:00:00 2001 From: Decobus Date: Tue, 22 Jul 2025 14:28:14 -0400 Subject: [PATCH 2/6] Add team and stream counts to footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created /api/counts endpoint for database statistics - Updated footer to display team and stream counts - Shows counts when OBS is connected (alongside OBS stats) - Shows database stats when OBS is disconnected (fallback display) - Polls counts every 60 seconds (less frequent than OBS status) - Maintains backward compatibility with API response formats Footer now shows: - Teams: X - Streams: Y 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/api/counts/route.ts | 25 ++++++++++++++++ components/Footer.tsx | 65 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 app/api/counts/route.ts diff --git a/app/api/counts/route.ts b/app/api/counts/route.ts new file mode 100644 index 0000000..86a638d --- /dev/null +++ b/app/api/counts/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from 'next/server'; +import { getDatabase } from '../../../lib/database'; +import { TABLE_NAMES } from '../../../lib/constants'; +import { createSuccessResponse, createDatabaseError, withErrorHandling } from '../../../lib/apiHelpers'; + +async function getCountsHandler() { + try { + const db = await getDatabase(); + + // Get counts in parallel + const [streamsResult, teamsResult] = await Promise.all([ + db.get(`SELECT COUNT(*) as count FROM ${TABLE_NAMES.STREAMS}`), + db.get(`SELECT COUNT(*) as count FROM ${TABLE_NAMES.TEAMS}`) + ]); + + return createSuccessResponse({ + streams: streamsResult.count, + teams: teamsResult.count + }); + } catch (error) { + return createDatabaseError('fetch counts', error); + } +} + +export const GET = withErrorHandling(getCountsHandler); \ No newline at end of file diff --git a/components/Footer.tsx b/components/Footer.tsx index c43a131..06a8cbe 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -19,8 +19,14 @@ type OBSStatus = { error?: string; }; +type Counts = { + streams: number; + teams: number; +}; + export default function Footer() { const [obsStatus, setObsStatus] = useState(null); + const [counts, setCounts] = useState(null); const [isLoading, setIsLoading] = useState(true); // Smart polling with performance monitoring and visibility detection @@ -40,9 +46,24 @@ export default function Footer() { } }; + const fetchCounts = async () => { + try { + const response = await fetch('/api/counts'); + const data = await response.json(); + // Handle both old and new API response formats + const countsData = data.success ? data.data : data; + setCounts(countsData); + } catch (error) { + console.error('Failed to fetch counts:', error); + } + }; + // Use smart polling that respects page visibility and adapts interval based on connection status const pollingInterval = obsStatus?.connected ? 15000 : 30000; // Poll faster when connected useSmartPolling(fetchOBSStatus, pollingInterval, [obsStatus?.connected]); + + // Poll counts less frequently (every 60 seconds) since they don't change as often + useSmartPolling(fetchCounts, 60000, []); if (isLoading) { return ( @@ -78,11 +99,27 @@ export default function Footer() { )} - {/* Live Status */} - {obsStatus?.connected && ( -
-

Live Status

- + {/* Live Status or Database Stats */} +
+

+ {obsStatus?.connected ? 'Live Status' : 'Database Stats'} +

+ + {/* Show counts when OBS is disconnected */} + {!obsStatus?.connected && counts && ( +
+
+ Teams: + {counts.teams} +
+
+ Streams: + {counts.streams} +
+
+ )} + + {obsStatus?.connected && (
{obsStatus.currentScene && (
@@ -98,6 +135,20 @@ export default function Footer() {
)} + {/* Database counts */} + {counts && ( + <> +
+ Teams: + {counts.teams} +
+
+ Streams: + {counts.streams} +
+ + )} +
@@ -110,8 +161,8 @@ export default function Footer() {
-
- )} + )} +
{/* Error Message */} -- 2.49.0 From 52f3051c82590b302c9fc6c78ac1ceffd2336ec3 Mon Sep 17 00:00:00 2001 From: Decobus Date: Tue, 22 Jul 2025 14:36:07 -0400 Subject: [PATCH 3/6] Fix status indicator dots visibility in footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added custom CSS for status dots using Solarized theme colors - Replaced Tailwind classes with custom .status-dot classes - Fixed connection status dot (green when connected, red when disconnected) - Fixed streaming/recording status dots (red when active, gray when idle) - Used proper Solarized color palette for consistency Status dots now properly display: - Connection: Green dot for connected, red for disconnected - Streaming: Red dot when live, gray when offline - Recording: Red dot when recording, gray when idle 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/globals.css | 24 ++++++++++++++++++++++++ components/Footer.tsx | 6 +++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/app/globals.css b/app/globals.css index 288b6ab..e1107df 100644 --- a/app/globals.css +++ b/app/globals.css @@ -40,6 +40,30 @@ body { line-height: 1.6; } +/* Status Indicator Dots */ +.status-dot { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; +} + +.status-dot.connected { + background-color: #859900; /* Solarized green */ +} + +.status-dot.disconnected { + background-color: #dc322f; /* Solarized red */ +} + +.status-dot.streaming { + background-color: #dc322f; /* Solarized red */ +} + +.status-dot.idle { + background-color: #586e75; /* Solarized base01 */ +} + /* Glass Card Component */ .glass { background: rgba(7, 54, 66, 0.4); diff --git a/components/Footer.tsx b/components/Footer.tsx index 06a8cbe..b4e585c 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -82,7 +82,7 @@ export default function Footer() { {/* Connection Status */}
-
+

OBS Studio

@@ -151,12 +151,12 @@ export default function Footer() {

-
+
{obsStatus.streaming ? 'LIVE' : 'OFFLINE'}
-
+
{obsStatus.recording ? 'REC' : 'IDLE'}
-- 2.49.0 From 3a0c34e5a0648b8ce8d6caf5f7ec2c4aa496b010 Mon Sep 17 00:00:00 2001 From: Decobus Date: Tue, 22 Jul 2025 14:37:48 -0400 Subject: [PATCH 4/6] Reorganize footer layout by moving streaming status to left side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Moved OFFLINE/IDLE streaming status from right to left column - Left side now shows: Connection + Host/Port + Streaming/Recording status - Right side now shows: Scene info + Team/Stream counts (less crowded) - Better balance of information between the two columns - Improves readability and visual hierarchy Footer layout now: Left: OBS Studio (connected) + 127.0.0.1:4455 + OFFLINE/IDLE status Right: Scene info + Teams/Streams counts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- components/Footer.tsx | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/components/Footer.tsx b/components/Footer.tsx index b4e585c..ce57d91 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -95,6 +95,21 @@ export default function Footer() {
{obsStatus.host}:{obsStatus.port}
{obsStatus.hasPassword &&
🔒 Authenticated
} + + {/* Streaming/Recording Status */} + {obsStatus.connected && ( +
+
+
+ {obsStatus.streaming ? 'LIVE' : 'OFFLINE'} +
+ +
+
+ {obsStatus.recording ? 'REC' : 'IDLE'} +
+
+ )}
)}
@@ -149,17 +164,6 @@ export default function Footer() { )} -
-
-
- {obsStatus.streaming ? 'LIVE' : 'OFFLINE'} -
- -
-
- {obsStatus.recording ? 'REC' : 'IDLE'} -
-
)}
-- 2.49.0 From d75f599711e84ef070b257aac8183630964e393f Mon Sep 17 00:00:00 2001 From: Decobus Date: Tue, 22 Jul 2025 16:11:49 -0400 Subject: [PATCH 5/6] Allow Twitch URL or username input in add stream form - Add extractTwitchUsername() function to parse various URL formats - Support https://twitch.tv/username, www.twitch.tv/username, and plain usernames - Real-time URL parsing - automatically extracts username as user types - Updated UI labels and placeholder to indicate both input options - Maintains existing validation and backend compatibility - Seamless UX - users can paste full URLs or type usernames directly --- app/streams/page.tsx | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/app/streams/page.tsx b/app/streams/page.tsx index 2f23e7c..2e4f52f 100644 --- a/app/streams/page.tsx +++ b/app/streams/page.tsx @@ -66,9 +66,37 @@ 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; - setFormData((prev) => ({ ...prev, [name]: value })); + + // 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 })); + } // Clear validation error when user starts typing if (validationErrors[name]) { @@ -213,7 +241,7 @@ export default function AddStream() { {/* Twitch Username */}
{validationErrors.twitch_username && (
-- 2.49.0 From b1215ea82c0a3411cc28c36eb0db4c662f29a648 Mon Sep 17 00:00:00 2001 From: Decobus Date: Tue, 22 Jul 2025 16:17:23 -0400 Subject: [PATCH 6/6] Fix unused variables and clean up codebase - Remove unused NextResponse imports from API routes - Remove unused result variable in teams DELETE route - Remove unused Link import from page.tsx - Remove unused inspectTextSourceProperties function from obsClient.js - Fix unused catch variables and response variables in test files - Clean up all ESLint warnings for unused variables --- app/api/__tests__/streams.test.ts | 8 -------- app/api/__tests__/teams.test.ts | 8 -------- app/api/counts/route.ts | 1 - app/api/getActive/route.ts | 1 - app/api/getTeamName/route.ts | 2 +- app/api/streams/route.ts | 1 - app/api/teams/[teamId]/route.ts | 2 +- app/page.tsx | 1 - lib/__tests__/apiHelpers.test.ts | 8 ++++---- lib/obsClient.js | 20 +++----------------- 10 files changed, 9 insertions(+), 43 deletions(-) diff --git a/app/api/__tests__/streams.test.ts b/app/api/__tests__/streams.test.ts index f57f060..41f6389 100644 --- a/app/api/__tests__/streams.test.ts +++ b/app/api/__tests__/streams.test.ts @@ -27,8 +27,6 @@ describe('/api/streams', () => { mockDb.all.mockResolvedValue(mockStreams); - const _response = await GET(); - expect(mockDb.all).toHaveBeenCalledWith( expect.stringContaining('SELECT * FROM') ); @@ -40,8 +38,6 @@ describe('/api/streams', () => { it('returns empty array when no streams exist', async () => { mockDb.all.mockResolvedValue([]); - const _response = await GET(); - const { NextResponse } = require('next/server'); expect(NextResponse.json).toHaveBeenCalledWith([]); }); @@ -50,8 +46,6 @@ describe('/api/streams', () => { const dbError = new Error('Database connection failed'); mockDb.all.mockRejectedValue(dbError); - const _response = await GET(); - const { NextResponse } = require('next/server'); expect(NextResponse.json).toHaveBeenCalledWith( { error: 'Failed to fetch streams' }, @@ -64,8 +58,6 @@ describe('/api/streams', () => { const { getDatabase } = require('@/lib/database'); getDatabase.mockRejectedValue(connectionError); - const _response = await GET(); - const { NextResponse } = require('next/server'); expect(NextResponse.json).toHaveBeenCalledWith( { error: 'Failed to fetch streams' }, diff --git a/app/api/__tests__/teams.test.ts b/app/api/__tests__/teams.test.ts index c84747a..bd09053 100644 --- a/app/api/__tests__/teams.test.ts +++ b/app/api/__tests__/teams.test.ts @@ -46,8 +46,6 @@ describe('/api/teams', () => { mockDb.all.mockResolvedValue(mockTeams); - const _response = await GET(); - expect(mockDb.all).toHaveBeenCalledWith( expect.stringContaining('SELECT * FROM') ); @@ -59,8 +57,6 @@ describe('/api/teams', () => { it('returns empty array when no teams exist', async () => { mockDb.all.mockResolvedValue([]); - const _response = await GET(); - const { createSuccessResponse } = require('@/lib/apiHelpers'); expect(createSuccessResponse).toHaveBeenCalledWith([]); }); @@ -69,8 +65,6 @@ describe('/api/teams', () => { const dbError = new Error('Table does not exist'); mockDb.all.mockRejectedValue(dbError); - const _response = await GET(); - const { createDatabaseError } = require('@/lib/apiHelpers'); expect(createDatabaseError).toHaveBeenCalledWith('fetch teams', dbError); }); @@ -80,8 +74,6 @@ describe('/api/teams', () => { const { getDatabase } = require('@/lib/database'); getDatabase.mockRejectedValue(connectionError); - const _response = await GET(); - const { createDatabaseError } = require('@/lib/apiHelpers'); expect(createDatabaseError).toHaveBeenCalledWith('fetch teams', connectionError); }); diff --git a/app/api/counts/route.ts b/app/api/counts/route.ts index 86a638d..a3ddc75 100644 --- a/app/api/counts/route.ts +++ b/app/api/counts/route.ts @@ -1,4 +1,3 @@ -import { NextResponse } from 'next/server'; import { getDatabase } from '../../../lib/database'; import { TABLE_NAMES } from '../../../lib/constants'; import { createSuccessResponse, createDatabaseError, withErrorHandling } from '../../../lib/apiHelpers'; diff --git a/app/api/getActive/route.ts b/app/api/getActive/route.ts index a47825b..59016e9 100644 --- a/app/api/getActive/route.ts +++ b/app/api/getActive/route.ts @@ -1,4 +1,3 @@ -import { NextResponse } from 'next/server'; import fs from 'fs'; import path from 'path'; import { createSuccessResponse, createErrorResponse, withErrorHandling } from '../../../lib/apiHelpers'; diff --git a/app/api/getTeamName/route.ts b/app/api/getTeamName/route.ts index c4c7cd9..cd5bef4 100644 --- a/app/api/getTeamName/route.ts +++ b/app/api/getTeamName/route.ts @@ -1,4 +1,4 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; import { getDatabase } from '../../../lib/database'; import { TABLE_NAMES } from '../../../lib/constants'; import { createErrorResponse, createSuccessResponse, createDatabaseError, withErrorHandling } from '../../../lib/apiHelpers'; diff --git a/app/api/streams/route.ts b/app/api/streams/route.ts index b52a74e..1df75ee 100644 --- a/app/api/streams/route.ts +++ b/app/api/streams/route.ts @@ -1,4 +1,3 @@ -import { NextResponse } from 'next/server'; import { getDatabase } from '../../../lib/database'; import { StreamWithTeam } from '@/types'; import { TABLE_NAMES } from '../../../lib/constants'; diff --git a/app/api/teams/[teamId]/route.ts b/app/api/teams/[teamId]/route.ts index 96dd077..e2128a2 100644 --- a/app/api/teams/[teamId]/route.ts +++ b/app/api/teams/[teamId]/route.ts @@ -126,7 +126,7 @@ export async function DELETE( ); // Delete the team - const result = await db.run( + await db.run( `DELETE FROM ${TABLE_NAMES.TEAMS} WHERE team_id = ?`, [teamId] ); diff --git a/app/page.tsx b/app/page.tsx index 24bfe97..ee8dcf2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,6 @@ 'use client'; import { useState, useEffect, useMemo, useCallback } from 'react'; -import Link from 'next/link'; import Dropdown from '@/components/Dropdown'; import { useToast } from '@/lib/useToast'; import { ToastContainer } from '@/components/Toast'; diff --git a/lib/__tests__/apiHelpers.test.ts b/lib/__tests__/apiHelpers.test.ts index 9bb97b4..1ad2609 100644 --- a/lib/__tests__/apiHelpers.test.ts +++ b/lib/__tests__/apiHelpers.test.ts @@ -32,7 +32,7 @@ describe('apiHelpers', () => { describe('createErrorResponse', () => { it('creates error response with default status 500', () => { - const _response = createErrorResponse('Test Error'); + createErrorResponse('Test Error'); expect(NextResponse.json).toHaveBeenCalledWith( expect.objectContaining({ @@ -44,7 +44,7 @@ describe('apiHelpers', () => { }); it('creates error response with custom status and message', () => { - const _response = createErrorResponse('Test Error', 400, 'Custom message', { detail: 'extra' }); + createErrorResponse('Test Error', 400, 'Custom message', { detail: 'extra' }); expect(NextResponse.json).toHaveBeenCalledWith( expect.objectContaining({ @@ -81,7 +81,7 @@ describe('apiHelpers', () => { describe('createSuccessResponse', () => { it('creates success response with default status 200', () => { const data = { test: 'data' }; - const _response = createSuccessResponse(data); + createSuccessResponse(data); expect(NextResponse.json).toHaveBeenCalledWith( expect.objectContaining({ @@ -95,7 +95,7 @@ describe('apiHelpers', () => { it('creates success response with custom status', () => { const data = { id: 1, name: 'test' }; - const _response = createSuccessResponse(data, 201); + createSuccessResponse(data, 201); expect(NextResponse.json).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/lib/obsClient.js b/lib/obsClient.js index 4402708..26dfbe4 100644 --- a/lib/obsClient.js +++ b/lib/obsClient.js @@ -244,20 +244,6 @@ async function getAvailableTextInputKind() { } } -async function inspectTextSourceProperties(inputKind) { - try { - const obsClient = await getOBSClient(); - - // Get the default properties for this input kind - const { inputProperties } = await obsClient.call('GetInputDefaultSettings', { inputKind }); - console.log(`Default properties for ${inputKind}:`, JSON.stringify(inputProperties, null, 2)); - - return inputProperties; - } catch (error) { - console.error('Error inspecting text source properties:', error.message); - return null; - } -} async function createTextSource(sceneName, textSourceName, text) { try { @@ -394,7 +380,7 @@ async function createStreamGroup(groupName, streamName, teamName, url) { try { await obsClient.call('CreateScene', { sceneName: streamGroupName }); console.log(`Created nested scene "${streamGroupName}" for stream grouping`); - } catch (sceneError) { + } catch (error) { console.log(`Nested scene "${streamGroupName}" might already exist`); } @@ -457,7 +443,7 @@ async function createStreamGroup(groupName, streamName, teamName, url) { sourceName: colorSourceName }); console.log(`Added color source background "${colorSourceName}" to nested scene`); - } catch (e) { + } catch (error) { console.log('Color source background might already be in nested scene'); } @@ -467,7 +453,7 @@ async function createStreamGroup(groupName, streamName, teamName, url) { sourceName: textSourceName }); console.log(`Added text source "${textSourceName}" to nested scene`); - } catch (e) { + } catch (error) { console.log('Text source might already be in nested scene'); } -- 2.49.0