diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 5a7d0b0..a5313f1 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -9,12 +9,21 @@ on: jobs: build: runs-on: self-hosted - # Note: Node.js is pre-installed on self-hosted runners + + strategy: + matrix: + node-version: [ 20, 22 ] steps: - name: Checkout Repository uses: actions/checkout@v4 + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - name: Clean NextJS cache run: rm -rf .next @@ -31,7 +40,8 @@ jobs: run: npm run build - name: Upload Build Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: obs-ss-build + name: obs-ss-${{ matrix.node-version }} + include-hidden-files: 'true' path: ./.next/* \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index b1cbf06..e9b1a8b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,8 +84,6 @@ This is a Next.js web application that controls multiple OBS Source Switchers. I #### Team Management - `GET /api/teams` - Get all teams - `GET /api/getTeamName` - Get team name by ID -- `POST /api/createGroup` - Create OBS group from team -- `POST /api/syncGroups` - Synchronize all teams with OBS groups #### System Status - `GET /api/obsStatus` - Real-time OBS connection and streaming status @@ -94,7 +92,7 @@ This is a Next.js web application that controls multiple OBS Source Switchers. I Dynamic table names with seasonal configuration: - `streams_YYYY_SEASON_SUFFIX`: id, name, obs_source_name, url, team_id -- `teams_YYYY_SEASON_SUFFIX`: team_id, team_name, group_name +- `teams_YYYY_SEASON_SUFFIX`: team_id, team_name ### OBS Integration Pattern @@ -102,18 +100,6 @@ The app uses a sophisticated dual integration approach: 1. **WebSocket Connection**: Direct OBS control using obs-websocket-js with persistent connection management 2. **Text File System**: Each screen position has a corresponding text file that OBS Source Switcher monitors -3. **Group Management**: Teams can be mapped to OBS groups (implemented as scenes) for organized source management - -**Required OBS Source Switchers** (must be created with these exact names): -- `ss_large` - Large screen source switcher -- `ss_left` - Left screen source switcher -- `ss_right` - Right screen source switcher -- `ss_top_left` - Top left screen source switcher -- `ss_top_right` - Top right screen source switcher -- `ss_bottom_left` - Bottom left screen source switcher -- `ss_bottom_right` - Bottom right screen source switcher - -See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructions. **Source Control Workflow**: 1. User selects stream in React UI diff --git a/app/api/__tests__/streams.test.ts b/app/api/__tests__/streams.test.ts index f57f060..d4ef664 100644 --- a/app/api/__tests__/streams.test.ts +++ b/app/api/__tests__/streams.test.ts @@ -6,7 +6,7 @@ jest.mock('@/lib/database', () => ({ })); describe('/api/streams', () => { - let mockDb: { all: jest.Mock }; + let mockDb: any; beforeEach(() => { // Create mock database @@ -27,7 +27,7 @@ describe('/api/streams', () => { mockDb.all.mockResolvedValue(mockStreams); - const _response = await GET(); + const response = await GET(); expect(mockDb.all).toHaveBeenCalledWith( expect.stringContaining('SELECT * FROM') @@ -40,7 +40,7 @@ describe('/api/streams', () => { it('returns empty array when no streams exist', async () => { mockDb.all.mockResolvedValue([]); - const _response = await GET(); + const response = await GET(); const { NextResponse } = require('next/server'); expect(NextResponse.json).toHaveBeenCalledWith([]); @@ -50,7 +50,7 @@ describe('/api/streams', () => { const dbError = new Error('Database connection failed'); mockDb.all.mockRejectedValue(dbError); - const _response = await GET(); + const response = await GET(); const { NextResponse } = require('next/server'); expect(NextResponse.json).toHaveBeenCalledWith( @@ -64,7 +64,7 @@ describe('/api/streams', () => { const { getDatabase } = require('@/lib/database'); getDatabase.mockRejectedValue(connectionError); - const _response = await GET(); + const response = await GET(); const { NextResponse } = require('next/server'); expect(NextResponse.json).toHaveBeenCalledWith( diff --git a/app/api/__tests__/teams.test.ts b/app/api/__tests__/teams.test.ts index c84747a..4f26d27 100644 --- a/app/api/__tests__/teams.test.ts +++ b/app/api/__tests__/teams.test.ts @@ -24,7 +24,7 @@ jest.mock('@/lib/apiHelpers', () => ({ })); describe('/api/teams', () => { - let mockDb: { all: jest.Mock }; + let mockDb: any; beforeEach(() => { // Create mock database @@ -46,7 +46,7 @@ describe('/api/teams', () => { mockDb.all.mockResolvedValue(mockTeams); - const _response = await GET(); + const response = await GET(); expect(mockDb.all).toHaveBeenCalledWith( expect.stringContaining('SELECT * FROM') @@ -59,7 +59,7 @@ describe('/api/teams', () => { it('returns empty array when no teams exist', async () => { mockDb.all.mockResolvedValue([]); - const _response = await GET(); + const response = await GET(); const { createSuccessResponse } = require('@/lib/apiHelpers'); expect(createSuccessResponse).toHaveBeenCalledWith([]); @@ -69,7 +69,7 @@ describe('/api/teams', () => { const dbError = new Error('Table does not exist'); mockDb.all.mockRejectedValue(dbError); - const _response = await GET(); + const response = await GET(); const { createDatabaseError } = require('@/lib/apiHelpers'); expect(createDatabaseError).toHaveBeenCalledWith('fetch teams', dbError); @@ -80,7 +80,7 @@ describe('/api/teams', () => { const { getDatabase } = require('@/lib/database'); getDatabase.mockRejectedValue(connectionError); - const _response = await GET(); + const response = await GET(); const { createDatabaseError } = require('@/lib/apiHelpers'); expect(createDatabaseError).toHaveBeenCalledWith('fetch teams', connectionError); diff --git a/app/api/addStream/route.ts b/app/api/addStream/route.ts index 750962e..940fcfe 100644 --- a/app/api/addStream/route.ts +++ b/app/api/addStream/route.ts @@ -1,19 +1,24 @@ import { NextRequest, NextResponse } from 'next/server'; import { getDatabase } from '../../../lib/database'; -import { connectToOBS, getOBSClient, disconnectFromOBS, addSourceToSwitcher, createGroupIfNotExists, addSourceToGroup } from '../../../lib/obsClient'; -import { open } from 'sqlite'; -import sqlite3 from 'sqlite3'; -import path from 'path'; -import { getTableName, BASE_TABLE_NAMES } from '../../../lib/constants'; +import { connectToOBS, getOBSClient, disconnectFromOBS, addSourceToSwitcher } from '../../../lib/obsClient'; interface OBSClient { call: (method: string, params?: Record) => Promise>; } +interface OBSScene { +sceneName: string; +} + interface OBSInput { inputName: string; } +interface GetSceneListResponse { +currentProgramSceneName: string; +currentPreviewSceneName: string; +scenes: OBSScene[]; +} interface GetInputListResponse { inputs: OBSInput[]; @@ -28,38 +33,85 @@ const screens = [ 'ss_bottom_right', ]; -async function fetchTeamInfo(teamId: number) { - const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files'); +async function fetchTeamName(teamId: number) { try { - const dbPath = path.join(FILE_DIRECTORY, 'sources.db'); - const db = await open({ - filename: dbPath, - driver: sqlite3.Database, - }); - - const teamsTableName = getTableName(BASE_TABLE_NAMES.TEAMS, { - year: 2025, - season: 'summer', - suffix: 'sat' - }); - - const teamInfo = await db.get( - `SELECT team_name, group_name FROM ${teamsTableName} WHERE team_id = ?`, - [teamId] - ); - - await db.close(); - return teamInfo; - } catch (error) { - if (error instanceof Error) { - console.error('Error fetching team info:', error.message); - } else { - console.error('An unknown error occurred:', error); + const baseUrl = process.env.BASE_URL || 'http://localhost:3000'; + const response = await fetch(`${baseUrl}/api/getTeamName?team_id=${teamId}`); + if (!response.ok) { + throw new Error('Failed to fetch team name'); } - return null; - } + const data = await response.json(); + return data.team_name; +} catch (error) { +if (error instanceof Error) { + console.error('Error:', error.message); +} else { + console.error('An unknown error occurred:', error); +} +return null; +} } +async function addBrowserSourceWithAudioControl(obs: OBSClient, sceneName: string, inputName: string, url: string) { + try { + // Step 1: Create the browser source input + await obs.call('CreateInput', { + sceneName, + inputName, + inputKind: 'browser_source', + inputSettings: { + width: 1600, + height: 900, + url, + }, + }); + + console.log(`Browser source "${inputName}" created successfully.`); + + // Step 2: Wait for the input to initialize + let inputReady = false; + for (let i = 0; i < 10; i++) { + try { + await obs.call('GetInputSettings', { inputName }); + inputReady = true; + break; + } catch { + console.log(`Waiting for input "${inputName}" to initialize...`); + await new Promise((resolve) => setTimeout(resolve, 500)); // Wait 500ms before retrying + } + } + + if (!inputReady) { + throw new Error(`Input "${inputName}" did not initialize in time.`); + } + + + // Step 3: Enable "Reroute audio" + await obs.call('SetInputSettings', { + inputName, + inputSettings: { + reroute_audio: true, + }, + overlay: true, // Keep existing settings and apply changes + }); + + console.log(`Audio rerouted for "${inputName}".`); + + // Step 4: Mute the input + await obs.call('SetInputMute', { + inputName, + inputMuted: true, + }); + + console.log(`Audio muted for "${inputName}".`); +} catch (error) { +if (error instanceof Error) { + console.error('Error adding browser source with audio control:', error.message); +} else { + console.error('An unknown error occurred while adding browser source:', error); +} +} +} import { validateStreamInput } from '../../../lib/security'; @@ -108,23 +160,20 @@ export async function POST(request: NextRequest) { } throw new Error('GetInputList failed.'); } - - const teamInfo = await fetchTeamInfo(team_id); - if (!teamInfo) { - throw new Error('Team not found'); + const teamName = await fetchTeamName(team_id); + console.log('Team Name:', teamName) + const response = await obs.call('GetSceneList'); + const sceneListResponse = response as unknown as GetSceneListResponse; + const { scenes } = sceneListResponse; + const groupExists = scenes.some((scene: OBSScene) => scene.sceneName === teamName); + if (!groupExists) { + await obs.call('CreateScene', { sceneName: teamName }); } - console.log('Team Info:', teamInfo); - - // Use group_name if it exists, otherwise use team_name - const groupName = teamInfo.group_name || teamInfo.team_name; - const sourceExists = inputs.some((input: OBSInput) => input.inputName === obs_source_name); if (!sourceExists) { - // Create/ensure group exists and add source to it - await createGroupIfNotExists(groupName); - await addSourceToGroup(groupName, obs_source_name, url); + await addBrowserSourceWithAudioControl(obs, teamName, obs_source_name, url) console.log(`OBS source "${obs_source_name}" created.`); @@ -147,12 +196,7 @@ export async function POST(request: NextRequest) { } const db = await getDatabase(); - const streamsTableName = getTableName(BASE_TABLE_NAMES.STREAMS, { - year: 2025, - season: 'summer', - suffix: 'sat' - }); - const query = `INSERT INTO ${streamsTableName} (name, obs_source_name, url, team_id) VALUES (?, ?, ?, ?)`; + const query = `INSERT INTO streams_2025_spring_adr (name, obs_source_name, url, team_id) VALUES (?, ?, ?, ?)`; db.run(query, [name, obs_source_name, url, team_id]) await disconnectFromOBS(); return NextResponse.json({ message: 'Stream added successfully' }, {status: 201}) diff --git a/app/api/createGroup/route.ts b/app/api/createGroup/route.ts deleted file mode 100644 index 10c3c44..0000000 --- a/app/api/createGroup/route.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { open } from 'sqlite'; -import sqlite3 from 'sqlite3'; -import path from 'path'; -import { getTableName, BASE_TABLE_NAMES } from '@/lib/constants'; -import { validateInteger } from '@/lib/security'; - -const { createGroupIfNotExists } = require('@/lib/obsClient'); - -const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files'); - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { teamId, groupName } = body; - - // Validate input - if (!teamId || !groupName) { - return NextResponse.json({ error: 'Team ID and group name are required' }, { status: 400 }); - } - - const validTeamId = validateInteger(teamId); - if (!validTeamId) { - return NextResponse.json({ error: 'Invalid team ID' }, { status: 400 }); - } - - // Sanitize group name (only allow alphanumeric, spaces, dashes, underscores) - const sanitizedGroupName = groupName.replace(/[^a-zA-Z0-9\s\-_]/g, ''); - if (!sanitizedGroupName || sanitizedGroupName.length === 0) { - return NextResponse.json({ error: 'Invalid group name' }, { status: 400 }); - } - - // Open database connection - const dbPath = path.join(FILE_DIRECTORY, 'sources.db'); - const db = await open({ - filename: dbPath, - driver: sqlite3.Database, - }); - - const teamsTableName = getTableName(BASE_TABLE_NAMES.TEAMS, { - year: 2025, - season: 'summer', - suffix: 'sat' - }); - - // Update team with group name - await db.run( - `UPDATE ${teamsTableName} SET group_name = ? WHERE team_id = ?`, - [sanitizedGroupName, validTeamId] - ); - - // Create group in OBS - const result = await createGroupIfNotExists(sanitizedGroupName); - - await db.close(); - - return NextResponse.json({ - success: true, - message: 'Group created/updated successfully', - groupName: sanitizedGroupName, - obsResult: result - }); - } catch (error) { - console.error('Error creating group:', error); - return NextResponse.json({ error: 'Failed to create group' }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/streams/[id]/route.ts b/app/api/streams/[id]/route.ts index 1a8974e..89cdb70 100644 --- a/app/api/streams/[id]/route.ts +++ b/app/api/streams/[id]/route.ts @@ -1,16 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { getDatabase } from '../../../../lib/database'; import { TABLE_NAMES } from '../../../../lib/constants'; -import { getOBSClient } from '../../../../lib/obsClient'; - -interface OBSInput { - inputName: string; - inputUuid: string; -} - -interface GetInputListResponse { - inputs: OBSInput[]; -} // GET single stream export async function GET( @@ -116,38 +106,7 @@ export async function DELETE( ); } - // Try to delete from OBS first - try { - const obs = await getOBSClient(); - console.log('OBS client obtained:', !!obs); - - if (obs && existingStream.obs_source_name) { - console.log(`Attempting to remove OBS source: ${existingStream.obs_source_name}`); - - // Get the input UUID first - const response = await obs.call('GetInputList'); - const inputs = response as GetInputListResponse; - console.log(`Found ${inputs.inputs.length} inputs in OBS`); - - const input = inputs.inputs.find((i: OBSInput) => i.inputName === existingStream.obs_source_name); - - if (input) { - console.log(`Found input with UUID: ${input.inputUuid}`); - await obs.call('RemoveInput', { inputUuid: input.inputUuid }); - console.log(`Successfully removed OBS source: ${existingStream.obs_source_name}`); - } else { - console.log(`Input not found in OBS: ${existingStream.obs_source_name}`); - console.log('Available inputs:', inputs.inputs.map((i: OBSInput) => i.inputName)); - } - } else { - console.log('OBS client not available or no source name provided'); - } - } catch (obsError) { - console.error('Error removing source from OBS:', obsError); - // Continue with database deletion even if OBS removal fails - } - - // Delete stream from database + // Delete stream await db.run( `DELETE FROM ${TABLE_NAMES.STREAMS} WHERE id = ?`, [resolvedParams.id] diff --git a/app/api/syncGroups/route.ts b/app/api/syncGroups/route.ts deleted file mode 100644 index 7e0a5c4..0000000 --- a/app/api/syncGroups/route.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { NextResponse } from 'next/server'; -import { open } from 'sqlite'; -import sqlite3 from 'sqlite3'; -import path from 'path'; -import { getTableName, BASE_TABLE_NAMES } from '@/lib/constants'; - -const { createGroupIfNotExists } = require('@/lib/obsClient'); - -const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files'); - -export async function POST() { - try { - // Open database connection - const dbPath = path.join(FILE_DIRECTORY, 'sources.db'); - const db = await open({ - filename: dbPath, - driver: sqlite3.Database, - }); - - const teamsTableName = getTableName(BASE_TABLE_NAMES.TEAMS, { - year: 2025, - season: 'summer', - suffix: 'sat' - }); - - // Get all teams without groups - const teamsWithoutGroups = await db.all( - `SELECT team_id, team_name FROM ${teamsTableName} WHERE group_name IS NULL` - ); - - const syncResults = []; - - for (const team of teamsWithoutGroups) { - try { - // Create group in OBS using team name - const obsResult = await createGroupIfNotExists(team.team_name); - - // Update database with group name - await db.run( - `UPDATE ${teamsTableName} SET group_name = ? WHERE team_id = ?`, - [team.team_name, team.team_id] - ); - - syncResults.push({ - teamId: team.team_id, - teamName: team.team_name, - groupName: team.team_name, - success: true, - obsResult - }); - } catch (error) { - console.error(`Error syncing team ${team.team_id}:`, error); - syncResults.push({ - teamId: team.team_id, - teamName: team.team_name, - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }); - } - } - - await db.close(); - - const successCount = syncResults.filter(r => r.success).length; - const failureCount = syncResults.filter(r => !r.success).length; - - return NextResponse.json({ - success: true, - message: `Sync completed: ${successCount} successful, ${failureCount} failed`, - results: syncResults, - summary: { - total: syncResults.length, - successful: successCount, - failed: failureCount - } - }); - } catch (error) { - console.error('Error syncing groups:', error); - return NextResponse.json({ error: 'Failed to sync groups' }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/teams/route.ts b/app/api/teams/route.ts index 36addd1..ce99d57 100644 --- a/app/api/teams/route.ts +++ b/app/api/teams/route.ts @@ -1,3 +1,4 @@ +import { NextResponse } from 'next/server'; import { getDatabase } from '../../../lib/database'; import { Team } from '@/types'; import { TABLE_NAMES } from '@/lib/constants'; @@ -38,14 +39,14 @@ function validateTeamInput(data: unknown): { return { valid: true, - data: { team_name: (team_name as string).trim() } + data: { team_name: team_name.trim() } }; } export const GET = withErrorHandling(async () => { try { const db = await getDatabase(); - const teams: Team[] = await db.all(`SELECT team_id, team_name, group_name FROM ${TABLE_NAMES.TEAMS} ORDER BY team_name ASC`); + const teams: Team[] = await db.all(`SELECT * FROM ${TABLE_NAMES.TEAMS} ORDER BY team_name ASC`); return createSuccessResponse(teams); } catch (error) { @@ -85,8 +86,7 @@ export const POST = withErrorHandling(async (request: Request) => { const newTeam: Team = { team_id: result.lastID!, - team_name: team_name, - group_name: null + team_name: team_name }; return createSuccessResponse(newTeam, 201); diff --git a/app/edit/[id]/page.tsx b/app/edit/[id]/page.tsx index 4422a66..6fe9463 100644 --- a/app/edit/[id]/page.tsx +++ b/app/edit/[id]/page.tsx @@ -86,7 +86,7 @@ export default function EditStream() { if (streamId) { fetchData(); } - }, [streamId, showError]); + }, [streamId]); const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; diff --git a/app/globals.css b/app/globals.css index 288b6ab..955ad62 100644 --- a/app/globals.css +++ b/app/globals.css @@ -213,10 +213,6 @@ body { margin-top: 4px; overflow: hidden; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); - z-index: 99999 !important; - position: absolute; - transform: translateZ(0); - will-change: transform; } .dropdown-item { diff --git a/app/page.tsx b/app/page.tsx index 2dc242d..2d18dfe 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -35,7 +35,7 @@ export default function Home() { const activeSourceIds = useActiveSourceLookup(streams, activeSources); // Debounced API calls to prevent excessive requests - const setActiveFunction = useCallback(async (screen: ScreenType, id: number | null) => { + const debouncedSetActive = useDebounce(async (screen: ScreenType, id: number | null) => { if (id) { const selectedStream = streams.find(stream => stream.id === id); try { @@ -62,9 +62,7 @@ export default function Home() { })); } } - }, [streams, showError, showSuccess]); - - const debouncedSetActive = useDebounce(setActiveFunction, 300); + }, 300); const fetchData = useCallback(async () => { const endTimer = PerformanceMonitor.startTimer('fetchData'); diff --git a/app/streams/page.tsx b/app/streams/page.tsx index 75aa281..e92feeb 100644 --- a/app/streams/page.tsx +++ b/app/streams/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect } from 'react'; import Dropdown from '@/components/Dropdown'; import { Team } from '@/types'; import { useToast } from '@/lib/useToast'; @@ -18,7 +18,7 @@ export default function AddStream() { const [formData, setFormData] = useState({ name: '', obs_source_name: '', - twitch_username: '', + url: '', team_id: null, }); const [teams, setTeams] = useState<{id: number; name: string}[]>([]); @@ -26,11 +26,14 @@ export default function AddStream() { const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({}); - const [deleteConfirm, setDeleteConfirm] = useState<{id: number; name: string} | null>(null); - const [isDeleting, setIsDeleting] = useState(false); const { toasts, removeToast, showSuccess, showError } = useToast(); - const fetchData = useCallback(async () => { + // Fetch teams and streams on component mount + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { setIsLoading(true); try { const [teamsResponse, streamsResponse] = await Promise.all([ @@ -60,13 +63,7 @@ export default function AddStream() { } finally { setIsLoading(false); } - }, [showError]); - - // Fetch teams and streams on component mount - useEffect(() => { - fetchData(); - }, [fetchData]); - + }; const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; @@ -88,32 +85,6 @@ export default function AddStream() { } }; - const handleDelete = async () => { - if (!deleteConfirm) return; - - setIsDeleting(true); - try { - const response = await fetch(`/api/streams/${deleteConfirm.id}`, { - method: 'DELETE', - }); - - const data = await response.json(); - if (response.ok) { - showSuccess('Stream Deleted', `"${deleteConfirm.name}" has been deleted successfully`); - setDeleteConfirm(null); - // Refetch the streams list - await fetchData(); - } else { - showError('Failed to Delete Stream', data.error || 'Unknown error occurred'); - } - } catch (error) { - console.error('Error deleting stream:', error); - showError('Failed to Delete Stream', 'Network error or server unavailable'); - } finally { - setIsDeleting(false); - } - }; - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -129,10 +100,14 @@ export default function AddStream() { errors.obs_source_name = 'OBS source name is required'; } - if (!formData.twitch_username.trim()) { - errors.twitch_username = 'Twitch username is required'; - } else if (!/^[a-zA-Z0-9_]{4,25}$/.test(formData.twitch_username.trim())) { - errors.twitch_username = 'Twitch username must be 4-25 characters and contain only letters, numbers, and underscores'; + if (!formData.url.trim()) { + errors.url = 'Stream URL is required'; + } else { + try { + new URL(formData.url); + } catch { + errors.url = 'Please enter a valid URL'; + } } if (!formData.team_id) { @@ -148,21 +123,16 @@ export default function AddStream() { setIsSubmitting(true); try { - const submissionData = { - ...formData, - url: `https://www.twitch.tv/${formData.twitch_username.trim()}` - }; - const response = await fetch('/api/addStream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(submissionData), + body: JSON.stringify(formData), }); const data = await response.json(); if (response.ok) { showSuccess('Stream Added', `"${formData.name}" has been added successfully`); - setFormData({ name: '', obs_source_name: '', twitch_username: '', team_id: null }); + setFormData({ name: '', obs_source_name: '', url: '', team_id: null }); setValidationErrors({}); fetchData(); } else { @@ -177,15 +147,14 @@ export default function AddStream() { }; return ( - <> -
- {/* Title */} -
-

Streams

-

- Organize your content by creating and managing stream sources -

-
+
+ {/* Title */} +
+

Streams

+

+ Organize your content by creating and managing stream sources +

+
{/* Add New Stream */}
@@ -237,25 +206,25 @@ export default function AddStream() { )}
- {/* Twitch Username */} + {/* URL */}
- {validationErrors.twitch_username && ( + {validationErrors.url && (
- {validationErrors.twitch_username} + {validationErrors.url}
)}
@@ -266,7 +235,7 @@ export default function AddStream() { Team
-
+
Team: {team?.name || 'Unknown'}
-
+
ID: {stream.id}
-
- - - - - View Stream - - -
+ + View Stream +
@@ -360,51 +310,8 @@ export default function AddStream() { )}
- {/* Toast Notifications */} - - - - {/* Delete Confirmation Modal */} - {deleteConfirm && ( -
-
-

Confirm Deletion

-

- Are you sure you want to delete the stream “{deleteConfirm.name}”? This will remove it from both the database and OBS. -

-
- - -
-
-
- )} - + {/* Toast Notifications */} + + ); } \ No newline at end of file diff --git a/app/teams/page.tsx b/app/teams/page.tsx index bbbc38a..223e73a 100644 --- a/app/teams/page.tsx +++ b/app/teams/page.tsx @@ -11,8 +11,6 @@ export default function Teams() { const [newTeamName, setNewTeamName] = useState(''); const [editingTeam, setEditingTeam] = useState(null); const [editingName, setEditingName] = useState(''); - const [creatingGroupForTeam, setCreatingGroupForTeam] = useState(null); - const [isSyncing, setIsSyncing] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [updatingTeamId, setUpdatingTeamId] = useState(null); const [deletingTeamId, setDeletingTeamId] = useState(null); @@ -148,64 +146,6 @@ export default function Teams() { } }; - const handleSyncAllGroups = async () => { - if (!confirm('This will create OBS groups for all teams that don\'t have one. Continue?')) { - return; - } - - setIsSyncing(true); - try { - const res = await fetch('/api/syncGroups', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }); - - if (res.ok) { - const result = await res.json(); - fetchTeams(); - showSuccess('Groups Synced', `${result.summary.successful} groups created successfully`); - if (result.summary.failed > 0) { - showError('Some Failures', `${result.summary.failed} groups failed to create`); - } - } else { - const error = await res.json(); - showError('Failed to Sync Groups', error.error || 'Unknown error occurred'); - } - } catch (error) { - console.error('Error syncing groups:', error); - showError('Failed to Sync Groups', 'Network error or server unavailable'); - } finally { - setIsSyncing(false); - } - }; - - const handleCreateGroup = async (teamId: number, teamName: string) => { - const groupName = prompt(`Enter group name for team "${teamName}":`, teamName); - if (!groupName) return; - - setCreatingGroupForTeam(teamId); - try { - const res = await fetch('/api/createGroup', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ teamId, groupName }), - }); - - if (res.ok) { - fetchTeams(); - showSuccess('Group Created', `OBS group "${groupName}" created for team "${teamName}"`); - } else { - const error = await res.json(); - showError('Failed to Create Group', error.error || 'Unknown error occurred'); - } - } catch (error) { - console.error('Error creating group:', error); - showError('Failed to Create Group', 'Network error or server unavailable'); - } finally { - setCreatingGroupForTeam(null); - } - }; - const startEditing = (team: Team) => { setEditingTeam(team); setEditingName(team.team_name); @@ -269,18 +209,7 @@ export default function Teams() { {/* Teams List */}
-
-

Existing Teams

- -
+

Existing Teams

{isLoading ? (
@@ -298,7 +227,7 @@ export default function Teams() { ) : (
{teams.map((team) => ( -
+
{editingTeam?.team_id === team.team_id ? (
{team.team_name}
ID: {team.team_id}
- {team.group_name ? ( -
OBS Group: {team.group_name}
- ) : ( -
No OBS Group
- )}
- {!team.group_name && ( - - )} + + {(controlledIsOpen ?? isOpen) && ( +
+ {options.length === 0 ? ( +
+ No streams available +
+ ) : ( + options.map((option) => ( +
handleSelect(option)} + className={`dropdown-item ${activeOption?.id === option.id ? 'active' : ''}`} + > + {option.name} +
+ )) + )}
- ) : ( - options.map((option) => ( -
handleSelect(option)} - className={`dropdown-item ${activeOption?.id === option.id ? 'active' : ''}`} - > - {option.name} -
- )) )}
- ) : null; - - return ( - <> -
- -
- - {mounted && typeof document !== 'undefined' && dropdownMenu ? - createPortal(dropdownMenu, document.body) : null - } - ); } \ No newline at end of file diff --git a/components/__tests__/ErrorBoundary.test.tsx b/components/__tests__/ErrorBoundary.test.tsx index 4459422..170521f 100644 --- a/components/__tests__/ErrorBoundary.test.tsx +++ b/components/__tests__/ErrorBoundary.test.tsx @@ -10,7 +10,7 @@ const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => { }; // Mock window.location.reload using jest.spyOn -// const mockReload = jest.fn(); // Defined but not used in current tests +const mockReload = jest.fn(); describe('ErrorBoundary', () => { // Suppress console.error for these tests since we expect errors @@ -63,7 +63,7 @@ describe('ErrorBoundary', () => { it('calls window.location.reload when refresh button is clicked', () => { // Skip this test in jsdom environment as window.location.reload cannot be easily mocked // In a real browser environment, this would work as expected - // const originalReload = window.location.reload; // Not used in jsdom test + const originalReload = window.location.reload; // Simple workaround for jsdom limitation if (typeof window.location.reload !== 'function') { @@ -119,17 +119,11 @@ describe('ErrorBoundary', () => { const originalEnv = process.env.NODE_ENV; beforeAll(() => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'development', - writable: true - }); + process.env.NODE_ENV = 'development'; }); afterAll(() => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: originalEnv, - writable: true - }); + process.env.NODE_ENV = originalEnv; }); it('shows error details in development mode', () => { @@ -153,17 +147,11 @@ describe('ErrorBoundary', () => { const originalEnv = process.env.NODE_ENV; beforeAll(() => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'production', - writable: true - }); + process.env.NODE_ENV = 'production'; }); afterAll(() => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: originalEnv, - writable: true - }); + process.env.NODE_ENV = originalEnv; }); it('hides error details in production mode', () => { diff --git a/components/__tests__/Toast.test.tsx b/components/__tests__/Toast.test.tsx index a5dbbee..d277e6c 100644 --- a/components/__tests__/Toast.test.tsx +++ b/components/__tests__/Toast.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent, act } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import { ToastComponent, ToastContainer, Toast, ToastType } from '../Toast'; // Mock timer functions diff --git a/docs/OBS_SETUP.md b/docs/OBS_SETUP.md deleted file mode 100644 index c4cc603..0000000 --- a/docs/OBS_SETUP.md +++ /dev/null @@ -1,99 +0,0 @@ -# OBS Source Switcher Setup Guide - -This document explains how to configure OBS Studio to work with the Source Switcher Plugin UI. - -## Prerequisites - -1. OBS Studio installed -2. [OBS WebSocket plugin](https://github.com/obsproject/obs-websocket) (usually included with OBS 28+) -3. [OBS Source Switcher plugin](https://obsproject.com/forum/resources/source-switcher.1090/) installed - -## Required Source Switcher Names - -You must create **exactly 7 Source Switcher sources** in OBS with these specific names: - -| Source Switcher Name | Screen Position | Text File | -|---------------------|-----------------|-----------| -| `ss_large` | Main/Large screen | `large.txt` | -| `ss_left` | Left screen | `left.txt` | -| `ss_right` | Right screen | `right.txt` | -| `ss_top_left` | Top left corner | `topLeft.txt` | -| `ss_top_right` | Top right corner | `topRight.txt` | -| `ss_bottom_left` | Bottom left corner | `bottomLeft.txt` | -| `ss_bottom_right` | Bottom right corner | `bottomRight.txt` | - -## Setup Instructions - -### 1. Configure OBS WebSocket - -1. In OBS, go to **Tools → WebSocket Server Settings** -2. Enable the WebSocket server -3. Set a port (default: 4455) -4. Optionally set a password -5. Note these settings for your `.env.local` file - -### 2. Create Source Switcher Sources - -For each screen position: - -1. In OBS, click the **+** button in Sources -2. Select **Source Switcher** -3. Name it exactly as shown in the table above (e.g., `ss_large`) -4. Configure the Source Switcher: - - **Mode**: Text File - - **File Path**: Point to the corresponding text file in your `files` directory - - **Switch Behavior**: Choose your preferred transition - -### 3. Configure Text File Monitoring - -Each Source Switcher should monitor its corresponding text file: - -- `ss_large` → monitors `{FILE_DIRECTORY}/large.txt` -- `ss_left` → monitors `{FILE_DIRECTORY}/left.txt` -- etc. - -Where `{FILE_DIRECTORY}` is the path configured in your `.env.local` file (default: `./files`) - -### 4. Add Browser Sources - -When you add streams through the UI, browser sources are automatically created in OBS with these settings: -- **Width**: 1600px -- **Height**: 900px -- **Audio**: Controlled via OBS (muted by default) - -## How It Works - -1. **Stream Selection**: When you select a stream for a screen position in the UI -2. **File Update**: The app writes the OBS source name to the corresponding text file -3. **Source Switch**: The Source Switcher detects the file change and switches to that source -4. **Group Organization**: Streams are organized into OBS groups based on their teams - -## Troubleshooting - -### Source Switcher not switching -- Verify the text file path is correct -- Check that the file is being updated (manually open the .txt file) -- Ensure Source Switcher is set to "Text File" mode - -### Sources not appearing -- Check OBS WebSocket connection in the footer -- Verify WebSocket credentials in `.env.local` -- Ensure the source name doesn't already exist in OBS - -### Missing screen positions -- Verify all 7 Source Switchers are created with exact names -- Check for typos in source names (they must match exactly) - -## Environment Variables - -Configure these in your `.env.local` file: - -```env -# OBS WebSocket Settings -OBS_WEBSOCKET_HOST=127.0.0.1 -OBS_WEBSOCKET_PORT=4455 -OBS_WEBSOCKET_PASSWORD=your_password_here - -# File Directory (where text files are stored) -FILE_DIRECTORY=./files -``` \ No newline at end of file diff --git a/files/SaT.json b/files/SaT.json deleted file mode 100644 index d898caf..0000000 --- a/files/SaT.json +++ /dev/null @@ -1,998 +0,0 @@ -{ - "current_scene": "Scene", - "current_program_scene": "Scene", - "scene_order": [ - { - "name": "Scene" - }, - { - "name": "1-Screen" - }, - { - "name": "2-Screen" - }, - { - "name": "4-Screen" - }, - { - "name": "Testers" - } - ], - "name": "SaT", - "groups": [], - "quick_transitions": [ - { - "name": "Cut", - "duration": 300, - "hotkeys": [], - "id": 4, - "fade_to_black": false - }, - { - "name": "Fade", - "duration": 300, - "hotkeys": [], - "id": 5, - "fade_to_black": false - }, - { - "name": "Fade", - "duration": 300, - "hotkeys": [], - "id": 6, - "fade_to_black": true - } - ], - "transitions": [], - "saved_projectors": [], - "canvases": [], - "current_transition": "Fade", - "transition_duration": 300, - "preview_locked": false, - "scaling_enabled": false, - "scaling_level": -8, - "scaling_off_x": 0.0, - "scaling_off_y": 0.0, - "modules": { - "scripts-tool": [], - "output-timer": { - "streamTimerHours": 0, - "streamTimerMinutes": 0, - "streamTimerSeconds": 30, - "recordTimerHours": 0, - "recordTimerMinutes": 0, - "recordTimerSeconds": 30, - "autoStartStreamTimer": false, - "autoStartRecordTimer": false, - "pauseRecordTimer": true - }, - "auto-scene-switcher": { - "interval": 300, - "non_matching_scene": "", - "switch_if_not_matching": false, - "active": false, - "switches": [] - }, - "captions": { - "source": "", - "enabled": false, - "lang_id": 1033, - "provider": "mssapi" - } - }, - "version": 2, - "sources": [ - { - "prev_ver": 520159233, - "name": "1-Screen", - "uuid": "a147f8e6-092d-4271-b1f6-8677b0644699", - "id": "scene", - "versioned_id": "scene", - "settings": { - "id_counter": 1, - "custom_size": false, - "items": [ - { - "name": "ss_large", - "source_uuid": "e5727f76-0f05-4747-93fe-190d0e27fad8", - "visible": true, - "locked": false, - "rot": 0.0, - "scale_ref": { - "x": 1920.0, - "y": 1080.0 - }, - "align": 5, - "bounds_type": 0, - "bounds_align": 0, - "bounds_crop": false, - "crop_left": 0, - "crop_top": 0, - "crop_right": 0, - "crop_bottom": 0, - "id": 1, - "group_item_backup": false, - "pos": { - "x": 0.0, - "y": 0.0 - }, - "pos_rel": { - "x": -1.7777777910232544, - "y": -1.0 - }, - "scale": { - "x": 1.0, - "y": 1.0 - }, - "scale_rel": { - "x": 1.0, - "y": 1.0 - }, - "bounds": { - "x": 0.0, - "y": 0.0 - }, - "bounds_rel": { - "x": 0.0, - "y": 0.0 - }, - "scale_filter": "disable", - "blend_method": "default", - "blend_type": "normal", - "show_transition": { - "duration": 0 - }, - "hide_transition": { - "duration": 0 - }, - "private_settings": {} - } - ] - }, - "mixers": 0, - "sync": 0, - "flags": 0, - "volume": 1.0, - "balance": 0.5, - "enabled": true, - "muted": false, - "push-to-mute": false, - "push-to-mute-delay": 0, - "push-to-talk": false, - "push-to-talk-delay": 0, - "hotkeys": { - "OBSBasic.SelectScene": [], - "libobs.show_scene_item.1": [], - "libobs.hide_scene_item.1": [] - }, - "deinterlace_mode": 0, - "deinterlace_field_order": 0, - "monitoring_type": 0, - "canvas_uuid": "6c69626f-6273-4c00-9d88-c5136d61696e", - "private_settings": {} - }, - { - "prev_ver": 520159233, - "name": "2-Screen", - "uuid": "e3fa43b2-84ff-46cf-b56c-6110a31df1ad", - "id": "scene", - "versioned_id": "scene", - "settings": { - "id_counter": 0, - "custom_size": false, - "items": [] - }, - "mixers": 0, - "sync": 0, - "flags": 0, - "volume": 1.0, - "balance": 0.5, - "enabled": true, - "muted": false, - "push-to-mute": false, - "push-to-mute-delay": 0, - "push-to-talk": false, - "push-to-talk-delay": 0, - "hotkeys": { - "OBSBasic.SelectScene": [] - }, - "deinterlace_mode": 0, - "deinterlace_field_order": 0, - "monitoring_type": 0, - "canvas_uuid": "6c69626f-6273-4c00-9d88-c5136d61696e", - "private_settings": {} - }, - { - "prev_ver": 520159233, - "name": "4-Screen", - "uuid": "ec2099f7-a728-49fc-95ee-c19ca5987bda", - "id": "scene", - "versioned_id": "scene", - "settings": { - "id_counter": 1, - "custom_size": false, - "items": [ - { - "name": "ss_bottom_left", - "source_uuid": "e3c901fa-60da-4e62-9104-f6e5c8c2dabd", - "visible": true, - "locked": false, - "rot": 0.0, - "scale_ref": { - "x": 1920.0, - "y": 1080.0 - }, - "align": 5, - "bounds_type": 0, - "bounds_align": 0, - "bounds_crop": false, - "crop_left": 0, - "crop_top": 0, - "crop_right": 0, - "crop_bottom": 0, - "id": 1, - "group_item_backup": false, - "pos": { - "x": 0.0, - "y": 0.0 - }, - "pos_rel": { - "x": -1.7777777910232544, - "y": -1.0 - }, - "scale": { - "x": 1.0, - "y": 1.0 - }, - "scale_rel": { - "x": 1.0, - "y": 1.0 - }, - "bounds": { - "x": 0.0, - "y": 0.0 - }, - "bounds_rel": { - "x": 0.0, - "y": 0.0 - }, - "scale_filter": "disable", - "blend_method": "default", - "blend_type": "normal", - "show_transition": { - "duration": 0 - }, - "hide_transition": { - "duration": 0 - }, - "private_settings": {} - } - ] - }, - "mixers": 0, - "sync": 0, - "flags": 0, - "volume": 1.0, - "balance": 0.5, - "enabled": true, - "muted": false, - "push-to-mute": false, - "push-to-mute-delay": 0, - "push-to-talk": false, - "push-to-talk-delay": 0, - "hotkeys": { - "OBSBasic.SelectScene": [], - "libobs.show_scene_item.1": [], - "libobs.hide_scene_item.1": [] - }, - "deinterlace_mode": 0, - "deinterlace_field_order": 0, - "monitoring_type": 0, - "canvas_uuid": "6c69626f-6273-4c00-9d88-c5136d61696e", - "private_settings": {} - }, - { - "prev_ver": 520159233, - "name": "Scene", - "uuid": "9a7e85d1-30bd-4e1a-8836-2d20b385820c", - "id": "scene", - "versioned_id": "scene", - "settings": { - "id_counter": 7, - "custom_size": false, - "items": [ - { - "name": "ss_top_left", - "source_uuid": "cd24ff7b-7e4b-4aef-a224-6af032de6247", - "visible": true, - "locked": false, - "rot": 0.0, - "scale_ref": { - "x": 1920.0, - "y": 1080.0 - }, - "align": 5, - "bounds_type": 0, - "bounds_align": 0, - "bounds_crop": false, - "crop_left": 0, - "crop_top": 0, - "crop_right": 0, - "crop_bottom": 0, - "id": 1, - "group_item_backup": false, - "pos": { - "x": 0.0, - "y": 0.0 - }, - "pos_rel": { - "x": -1.7777777910232544, - "y": -1.0 - }, - "scale": { - "x": 1.0, - "y": 1.0 - }, - "scale_rel": { - "x": 1.0, - "y": 1.0 - }, - "bounds": { - "x": 0.0, - "y": 0.0 - }, - "bounds_rel": { - "x": 0.0, - "y": 0.0 - }, - "scale_filter": "disable", - "blend_method": "default", - "blend_type": "normal", - "show_transition": { - "duration": 0 - }, - "hide_transition": { - "duration": 0 - }, - "private_settings": {} - }, - { - "name": "ss_top_right", - "source_uuid": "b38b1134-7040-44b4-a397-1a1385911403", - "visible": true, - "locked": false, - "rot": 0.0, - "scale_ref": { - "x": 1920.0, - "y": 1080.0 - }, - "align": 5, - "bounds_type": 0, - "bounds_align": 0, - "bounds_crop": false, - "crop_left": 0, - "crop_top": 0, - "crop_right": 0, - "crop_bottom": 0, - "id": 2, - "group_item_backup": false, - "pos": { - "x": 0.0, - "y": 0.0 - }, - "pos_rel": { - "x": -1.7777777910232544, - "y": -1.0 - }, - "scale": { - "x": 1.0, - "y": 1.0 - }, - "scale_rel": { - "x": 1.0, - "y": 1.0 - }, - "bounds": { - "x": 0.0, - "y": 0.0 - }, - "bounds_rel": { - "x": 0.0, - "y": 0.0 - }, - "scale_filter": "disable", - "blend_method": "default", - "blend_type": "normal", - "show_transition": { - "duration": 0 - }, - "hide_transition": { - "duration": 0 - }, - "private_settings": {} - }, - { - "name": "ss_bottom_left", - "source_uuid": "e3c901fa-60da-4e62-9104-f6e5c8c2dabd", - "visible": true, - "locked": false, - "rot": 0.0, - "scale_ref": { - "x": 1920.0, - "y": 1080.0 - }, - "align": 5, - "bounds_type": 0, - "bounds_align": 0, - "bounds_crop": false, - "crop_left": 0, - "crop_top": 0, - "crop_right": 0, - "crop_bottom": 0, - "id": 3, - "group_item_backup": false, - "pos": { - "x": 0.0, - "y": 0.0 - }, - "pos_rel": { - "x": -1.7777777910232544, - "y": -1.0 - }, - "scale": { - "x": 1.0, - "y": 1.0 - }, - "scale_rel": { - "x": 1.0, - "y": 1.0 - }, - "bounds": { - "x": 0.0, - "y": 0.0 - }, - "bounds_rel": { - "x": 0.0, - "y": 0.0 - }, - "scale_filter": "disable", - "blend_method": "default", - "blend_type": "normal", - "show_transition": { - "duration": 0 - }, - "hide_transition": { - "duration": 0 - }, - "private_settings": {} - }, - { - "name": "ss_bottom_right", - "source_uuid": "c2164d12-5a03-4372-acdd-f93a7db2a166", - "visible": true, - "locked": false, - "rot": 0.0, - "scale_ref": { - "x": 1920.0, - "y": 1080.0 - }, - "align": 5, - "bounds_type": 0, - "bounds_align": 0, - "bounds_crop": false, - "crop_left": 0, - "crop_top": 0, - "crop_right": 0, - "crop_bottom": 0, - "id": 4, - "group_item_backup": false, - "pos": { - "x": 0.0, - "y": 0.0 - }, - "pos_rel": { - "x": -1.7777777910232544, - "y": -1.0 - }, - "scale": { - "x": 1.0, - "y": 1.0 - }, - "scale_rel": { - "x": 1.0, - "y": 1.0 - }, - "bounds": { - "x": 0.0, - "y": 0.0 - }, - "bounds_rel": { - "x": 0.0, - "y": 0.0 - }, - "scale_filter": "disable", - "blend_method": "default", - "blend_type": "normal", - "show_transition": { - "duration": 0 - }, - "hide_transition": { - "duration": 0 - }, - "private_settings": {} - }, - { - "name": "ss_large", - "source_uuid": "e5727f76-0f05-4747-93fe-190d0e27fad8", - "visible": true, - "locked": false, - "rot": 0.0, - "scale_ref": { - "x": 1920.0, - "y": 1080.0 - }, - "align": 5, - "bounds_type": 0, - "bounds_align": 0, - "bounds_crop": false, - "crop_left": 0, - "crop_top": 0, - "crop_right": 0, - "crop_bottom": 0, - "id": 5, - "group_item_backup": false, - "pos": { - "x": 0.0, - "y": 0.0 - }, - "pos_rel": { - "x": -1.7777777910232544, - "y": -1.0 - }, - "scale": { - "x": 1.0, - "y": 1.0 - }, - "scale_rel": { - "x": 1.0, - "y": 1.0 - }, - "bounds": { - "x": 0.0, - "y": 0.0 - }, - "bounds_rel": { - "x": 0.0, - "y": 0.0 - }, - "scale_filter": "disable", - "blend_method": "default", - "blend_type": "normal", - "show_transition": { - "duration": 0 - }, - "hide_transition": { - "duration": 0 - }, - "private_settings": {} - }, - { - "name": "ss_left", - "source_uuid": "0f02c57d-41fd-40c3-8e05-de44edc52361", - "visible": true, - "locked": false, - "rot": 0.0, - "scale_ref": { - "x": 1920.0, - "y": 1080.0 - }, - "align": 5, - "bounds_type": 0, - "bounds_align": 0, - "bounds_crop": false, - "crop_left": 0, - "crop_top": 0, - "crop_right": 0, - "crop_bottom": 0, - "id": 6, - "group_item_backup": false, - "pos": { - "x": 0.0, - "y": 0.0 - }, - "pos_rel": { - "x": -1.7777777910232544, - "y": -1.0 - }, - "scale": { - "x": 1.0, - "y": 1.0 - }, - "scale_rel": { - "x": 1.0, - "y": 1.0 - }, - "bounds": { - "x": 0.0, - "y": 0.0 - }, - "bounds_rel": { - "x": 0.0, - "y": 0.0 - }, - "scale_filter": "disable", - "blend_method": "default", - "blend_type": "normal", - "show_transition": { - "duration": 0 - }, - "hide_transition": { - "duration": 0 - }, - "private_settings": {} - }, - { - "name": "ss_right", - "source_uuid": "a90a0e5b-4443-4ac1-981c-6598b92c47fe", - "visible": true, - "locked": false, - "rot": 0.0, - "scale_ref": { - "x": 1920.0, - "y": 1080.0 - }, - "align": 5, - "bounds_type": 0, - "bounds_align": 0, - "bounds_crop": false, - "crop_left": 0, - "crop_top": 0, - "crop_right": 0, - "crop_bottom": 0, - "id": 7, - "group_item_backup": false, - "pos": { - "x": 0.0, - "y": 0.0 - }, - "pos_rel": { - "x": -1.7777777910232544, - "y": -1.0 - }, - "scale": { - "x": 1.0, - "y": 1.0 - }, - "scale_rel": { - "x": 1.0, - "y": 1.0 - }, - "bounds": { - "x": 0.0, - "y": 0.0 - }, - "bounds_rel": { - "x": 0.0, - "y": 0.0 - }, - "scale_filter": "disable", - "blend_method": "default", - "blend_type": "normal", - "show_transition": { - "duration": 0 - }, - "hide_transition": { - "duration": 0 - }, - "private_settings": {} - } - ] - }, - "mixers": 0, - "sync": 0, - "flags": 0, - "volume": 1.0, - "balance": 0.5, - "enabled": true, - "muted": false, - "push-to-mute": false, - "push-to-mute-delay": 0, - "push-to-talk": false, - "push-to-talk-delay": 0, - "hotkeys": { - "OBSBasic.SelectScene": [], - "libobs.show_scene_item.1": [], - "libobs.hide_scene_item.1": [], - "libobs.show_scene_item.2": [], - "libobs.hide_scene_item.2": [], - "libobs.show_scene_item.3": [], - "libobs.hide_scene_item.3": [], - "libobs.show_scene_item.4": [], - "libobs.hide_scene_item.4": [], - "libobs.show_scene_item.5": [], - "libobs.hide_scene_item.5": [], - "libobs.show_scene_item.6": [], - "libobs.hide_scene_item.6": [], - "libobs.show_scene_item.7": [], - "libobs.hide_scene_item.7": [] - }, - "deinterlace_mode": 0, - "deinterlace_field_order": 0, - "monitoring_type": 0, - "canvas_uuid": "6c69626f-6273-4c00-9d88-c5136d61696e", - "private_settings": {} - }, - { - "prev_ver": 520159233, - "name": "ss_bottom_left", - "uuid": "e3c901fa-60da-4e62-9104-f6e5c8c2dabd", - "id": "source_switcher", - "versioned_id": "source_switcher", - "settings": { - "current_index": -1 - }, - "mixers": 0, - "sync": 0, - "flags": 0, - "volume": 1.0, - "balance": 0.5, - "enabled": true, - "muted": false, - "push-to-mute": false, - "push-to-mute-delay": 0, - "push-to-talk": false, - "push-to-talk-delay": 0, - "hotkeys": { - "none": [], - "next": [], - "previous": [], - "random": [], - "shuffle": [], - "first": [], - "last": [] - }, - "deinterlace_mode": 0, - "deinterlace_field_order": 0, - "monitoring_type": 0, - "private_settings": {} - }, - { - "prev_ver": 520159233, - "name": "ss_bottom_right", - "uuid": "c2164d12-5a03-4372-acdd-f93a7db2a166", - "id": "source_switcher", - "versioned_id": "source_switcher", - "settings": { - "current_index": -1 - }, - "mixers": 0, - "sync": 0, - "flags": 0, - "volume": 1.0, - "balance": 0.5, - "enabled": true, - "muted": false, - "push-to-mute": false, - "push-to-mute-delay": 0, - "push-to-talk": false, - "push-to-talk-delay": 0, - "hotkeys": { - "none": [], - "next": [], - "previous": [], - "random": [], - "shuffle": [], - "first": [], - "last": [] - }, - "deinterlace_mode": 0, - "deinterlace_field_order": 0, - "monitoring_type": 0, - "private_settings": {} - }, - { - "prev_ver": 520159233, - "name": "ss_large", - "uuid": "e5727f76-0f05-4747-93fe-190d0e27fad8", - "id": "source_switcher", - "versioned_id": "source_switcher", - "settings": { - "current_index": -1, - "current_source_file": true, - "current_source_file_path": "C:/Users/derek/OBS/SaT Summer 2025/ss/ss-large.txt", - "current_source_file_interval": 1000 - }, - "mixers": 0, - "sync": 0, - "flags": 0, - "volume": 1.0, - "balance": 0.5, - "enabled": true, - "muted": false, - "push-to-mute": false, - "push-to-mute-delay": 0, - "push-to-talk": false, - "push-to-talk-delay": 0, - "hotkeys": { - "none": [], - "next": [], - "previous": [], - "random": [], - "shuffle": [], - "first": [], - "last": [] - }, - "deinterlace_mode": 0, - "deinterlace_field_order": 0, - "monitoring_type": 0, - "private_settings": {} - }, - { - "prev_ver": 520159233, - "name": "ss_left", - "uuid": "0f02c57d-41fd-40c3-8e05-de44edc52361", - "id": "source_switcher", - "versioned_id": "source_switcher", - "settings": { - "current_index": -1 - }, - "mixers": 0, - "sync": 0, - "flags": 0, - "volume": 1.0, - "balance": 0.5, - "enabled": true, - "muted": false, - "push-to-mute": false, - "push-to-mute-delay": 0, - "push-to-talk": false, - "push-to-talk-delay": 0, - "hotkeys": { - "none": [], - "next": [], - "previous": [], - "random": [], - "shuffle": [], - "first": [], - "last": [] - }, - "deinterlace_mode": 0, - "deinterlace_field_order": 0, - "monitoring_type": 0, - "private_settings": {} - }, - { - "prev_ver": 520159233, - "name": "ss_right", - "uuid": "a90a0e5b-4443-4ac1-981c-6598b92c47fe", - "id": "source_switcher", - "versioned_id": "source_switcher", - "settings": { - "current_index": -1 - }, - "mixers": 0, - "sync": 0, - "flags": 0, - "volume": 1.0, - "balance": 0.5, - "enabled": true, - "muted": false, - "push-to-mute": false, - "push-to-mute-delay": 0, - "push-to-talk": false, - "push-to-talk-delay": 0, - "hotkeys": { - "none": [], - "next": [], - "previous": [], - "random": [], - "shuffle": [], - "first": [], - "last": [] - }, - "deinterlace_mode": 0, - "deinterlace_field_order": 0, - "monitoring_type": 0, - "private_settings": {} - }, - { - "prev_ver": 520159233, - "name": "ss_top_left", - "uuid": "cd24ff7b-7e4b-4aef-a224-6af032de6247", - "id": "source_switcher", - "versioned_id": "source_switcher", - "settings": { - "current_index": -1 - }, - "mixers": 0, - "sync": 0, - "flags": 0, - "volume": 1.0, - "balance": 0.5, - "enabled": true, - "muted": false, - "push-to-mute": false, - "push-to-mute-delay": 0, - "push-to-talk": false, - "push-to-talk-delay": 0, - "hotkeys": { - "none": [], - "next": [], - "previous": [], - "random": [], - "shuffle": [], - "first": [], - "last": [] - }, - "deinterlace_mode": 0, - "deinterlace_field_order": 0, - "monitoring_type": 0, - "private_settings": {} - }, - { - "prev_ver": 520159233, - "name": "ss_top_right", - "uuid": "b38b1134-7040-44b4-a397-1a1385911403", - "id": "source_switcher", - "versioned_id": "source_switcher", - "settings": { - "current_index": -1 - }, - "mixers": 0, - "sync": 0, - "flags": 0, - "volume": 1.0, - "balance": 0.5, - "enabled": true, - "muted": false, - "push-to-mute": false, - "push-to-mute-delay": 0, - "push-to-talk": false, - "push-to-talk-delay": 0, - "hotkeys": { - "none": [], - "next": [], - "previous": [], - "random": [], - "shuffle": [], - "first": [], - "last": [] - }, - "deinterlace_mode": 0, - "deinterlace_field_order": 0, - "monitoring_type": 0, - "private_settings": {} - }, - { - "prev_ver": 520159233, - "name": "Testers", - "uuid": "10ab7435-3634-4d18-ab64-18385c68a106", - "id": "scene", - "versioned_id": "scene", - "settings": { - "id_counter": 0, - "custom_size": false, - "items": [] - }, - "mixers": 0, - "sync": 0, - "flags": 0, - "volume": 1.0, - "balance": 0.5, - "enabled": true, - "muted": false, - "push-to-mute": false, - "push-to-mute-delay": 0, - "push-to-talk": false, - "push-to-talk-delay": 0, - "hotkeys": { - "OBSBasic.SelectScene": [] - }, - "deinterlace_mode": 0, - "deinterlace_field_order": 0, - "monitoring_type": 0, - "canvas_uuid": "6c69626f-6273-4c00-9d88-c5136d61696e", - "private_settings": {} - } - ] -} \ No newline at end of file diff --git a/lib/__tests__/apiHelpers.test.ts b/lib/__tests__/apiHelpers.test.ts index 9bb97b4..8b51764 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'); + const response = 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' }); + const response = 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); + const response = 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); + const response = createSuccessResponse(data, 201); expect(NextResponse.json).toHaveBeenCalledWith( expect.objectContaining({ @@ -151,9 +151,9 @@ describe('apiHelpers', () => { }); describe('parseRequestBody', () => { - const mockRequest = (body: unknown): Request => ({ + const mockRequest = (body: any): Request => ({ json: jest.fn().mockResolvedValue(body), - } as unknown as Request); + } as any); it('parses valid JSON body without validator', async () => { const body = { name: 'test', value: 123 }; @@ -170,7 +170,7 @@ describe('apiHelpers', () => { it('handles invalid JSON', async () => { const request = { json: jest.fn().mockRejectedValue(new Error('Invalid JSON')), - } as unknown as Request; + } as any; const result = await parseRequestBody(request); @@ -218,17 +218,11 @@ describe('apiHelpers', () => { const originalEnv = process.env.NODE_ENV; afterAll(() => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: originalEnv, - writable: true - }); + process.env.NODE_ENV = originalEnv; }); it('includes error details in development', () => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'development', - writable: true - }); + process.env.NODE_ENV = 'development'; const originalError = new Error('Test error'); createDatabaseError('test operation', originalError); @@ -242,10 +236,7 @@ describe('apiHelpers', () => { }); it('excludes error details in production', () => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'production', - writable: true - }); + process.env.NODE_ENV = 'production'; const originalError = new Error('Test error'); createDatabaseError('test operation', originalError); diff --git a/lib/__tests__/useToast.test.ts b/lib/__tests__/useToast.test.ts index 991b621..8d96eec 100644 --- a/lib/__tests__/useToast.test.ts +++ b/lib/__tests__/useToast.test.ts @@ -178,7 +178,7 @@ describe('useToast', () => { it('returns unique IDs for each toast', () => { const { result } = renderHook(() => useToast()); - let id1: string = '', id2: string = ''; + let id1: string, id2: string; act(() => { // Mock different random values for unique IDs diff --git a/lib/apiClient.ts b/lib/apiClient.ts index 5d386c7..a388761 100644 --- a/lib/apiClient.ts +++ b/lib/apiClient.ts @@ -18,7 +18,7 @@ export async function apiCall(url: string, options: RequestInit = {}): Promise = { 'Content-Type': 'application/json', - ...(options.headers as Record || {}), + ...options.headers, }; // Add API key if available diff --git a/lib/apiHelpers.ts b/lib/apiHelpers.ts index d6c5687..a85150d 100644 --- a/lib/apiHelpers.ts +++ b/lib/apiHelpers.ts @@ -140,7 +140,7 @@ export async function parseRequestBody( } return { success: true, data: body as T }; - } catch (_error) { + } catch (error) { return { success: false, response: createErrorResponse( diff --git a/lib/obsClient.js b/lib/obsClient.js index 5465302..4a6d673 100644 --- a/lib/obsClient.js +++ b/lib/obsClient.js @@ -120,74 +120,48 @@ async function addSourceToSwitcher(inputName, newSources) { } } -async function createGroupIfNotExists(groupName) { - try { - const obsClient = await getOBSClient(); - - // Check if the group (scene) exists - const { scenes } = await obsClient.call('GetSceneList'); - const groupExists = scenes.some((scene) => scene.sceneName === groupName); +// async function addSourceToGroup(obs, teamName, obs_source_name, url) { +// try { +// // Step 1: Check if the group exists +// const { scenes } = await obs.call('GetSceneList'); +// const groupExists = scenes.some((scene) => scene.sceneName === teamName); - if (!groupExists) { - console.log(`Creating group "${groupName}"`); - await obsClient.call('CreateScene', { sceneName: groupName }); - return { created: true, message: `Group "${groupName}" created successfully` }; - } else { - console.log(`Group "${groupName}" already exists`); - return { created: false, message: `Group "${groupName}" already exists` }; - } - } catch (error) { - console.error('Error creating group:', error.message); - throw error; - } -} +// // Step 2: Create the group if it doesn't exist +// if (!groupExists) { +// console.log(`Group "${teamName}" does not exist. Creating it.`); +// await obs.call('CreateScene', { sceneName: teamName }); +// } else { +// console.log(`Group "${teamName}" already exists.`); +// } -async function addSourceToGroup(groupName, sourceName, url) { - try { - const obsClient = await getOBSClient(); - - // Ensure group exists - await createGroupIfNotExists(groupName); - - // Check if source already exists in the group - const { sceneItems } = await obsClient.call('GetSceneItemList', { sceneName: groupName }); - const sourceExists = sceneItems.some(item => item.sourceName === sourceName); - - if (!sourceExists) { - // Create the browser source in the group - console.log(`Adding source "${sourceName}" to group "${groupName}"`); - await obsClient.call('CreateInput', { - sceneName: groupName, - inputName: sourceName, - inputKind: 'browser_source', - inputSettings: { - width: 1600, - height: 900, - url, - control_audio: true, - }, - }); - - // Ensure audio control is enabled - await obsClient.call('SetInputSettings', { - inputName: sourceName, - inputSettings: { - control_audio: true, - }, - overlay: true, - }); - - console.log(`Source "${sourceName}" successfully added to group "${groupName}"`); - return { success: true, message: `Source added to group successfully` }; - } else { - console.log(`Source "${sourceName}" already exists in group "${groupName}"`); - return { success: false, message: `Source already exists in group` }; - } - } catch (error) { - console.error('Error adding source to group:', error.message); - throw error; - } -} +// // Step 3: Add the source to the group +// console.log(`Adding source "${obs_source_name}" to group "${teamName}".`); +// await obs.call('CreateInput', { +// sceneName: teamName, +// inputName: obs_source_name, +// inputKind: 'browser_source', +// inputSettings: { +// width: 1600, +// height: 900, +// url, +// control_audio: true, +// }, +// }); + +// // Step 4: Enable "Control audio via OBS" +// await obs.call('SetInputSettings', { +// inputName: obs_source_name, +// inputSettings: { +// control_audio: true, // Enable audio control +// }, +// overlay: true, // Keep existing settings and apply changes +// }); + +// console.log(`Source "${obs_source_name}" successfully added to group "${teamName}".`); +// } catch (error) { +// console.error('Error adding source to group:', error.message); +// } +// } // Export all functions @@ -197,7 +171,5 @@ module.exports = { disconnectFromOBS, addSourceToSwitcher, ensureConnected, - getConnectionStatus, - createGroupIfNotExists, - addSourceToGroup + getConnectionStatus }; \ No newline at end of file diff --git a/lib/performance.ts b/lib/performance.ts index c523293..8643f0c 100644 --- a/lib/performance.ts +++ b/lib/performance.ts @@ -3,12 +3,11 @@ import React, { useMemo, useCallback, useRef } from 'react'; // Debounce hook for preventing excessive API calls -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function useDebounce any>( callback: T, delay: number ): T { - const timeoutRef = useRef(null); + const timeoutRef = useRef(); return useCallback((...args: Parameters) => { if (timeoutRef.current) { @@ -22,7 +21,7 @@ export function useDebounce any>( } // Throttle hook for limiting function calls -export function useThrottle unknown>( +export function useThrottle any>( callback: T, delay: number ): T { @@ -39,21 +38,16 @@ export function useThrottle unknown>( // Memoized stream lookup utilities export function createStreamLookupMaps(streams: Array<{ id: number; obs_source_name: string; name: string }>) { - const sourceToIdMap = new Map(); - const idToStreamMap = new Map(); - - streams.forEach(stream => { - sourceToIdMap.set(stream.obs_source_name, stream.id); - idToStreamMap.set(stream.id, stream); - }); - - return { sourceToIdMap, idToStreamMap }; -} - -// Hook version for React components -export function useStreamLookupMaps(streams: Array<{ id: number; obs_source_name: string; name: string }>) { return useMemo(() => { - return createStreamLookupMaps(streams); + const sourceToIdMap = new Map(); + const idToStreamMap = new Map(); + + streams.forEach(stream => { + sourceToIdMap.set(stream.obs_source_name, stream.id); + idToStreamMap.set(stream.id, stream); + }); + + return { sourceToIdMap, idToStreamMap }; }, [streams]); } @@ -62,7 +56,7 @@ export function useActiveSourceLookup( streams: Array<{ id: number; obs_source_name: string; name: string }>, activeSources: Record ) { - const { sourceToIdMap } = useStreamLookupMaps(streams); + const { sourceToIdMap } = createStreamLookupMaps(streams); return useMemo(() => { const activeSourceIds: Record = {}; @@ -110,7 +104,7 @@ export class PerformanceMonitor { } static getAllMetrics() { - const result: Record> = {}; + const result: Record = {}; this.metrics.forEach((_, label) => { result[label] = this.getMetrics(label); }); @@ -161,11 +155,11 @@ export function usePageVisibility() { export function useSmartPolling( callback: () => void | Promise, interval: number, - dependencies: unknown[] = [] + dependencies: any[] = [] ) { const isVisible = usePageVisibility(); const callbackRef = useRef(callback); - const intervalRef = useRef(null); + const intervalRef = useRef(); // Update callback ref React.useEffect(() => { @@ -173,24 +167,22 @@ export function useSmartPolling( }, [callback]); React.useEffect(() => { - // Clear any existing interval before setting up a new one - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - if (isVisible) { // Start polling when visible callbackRef.current(); intervalRef.current = setInterval(() => { callbackRef.current(); }, interval); + } else { + // Stop polling when not visible + if (intervalRef.current) { + clearInterval(intervalRef.current); + } } return () => { if (intervalRef.current) { clearInterval(intervalRef.current); - intervalRef.current = null; } }; }, [interval, isVisible, ...dependencies]); diff --git a/lib/security.ts b/lib/security.ts index e38cecf..e25f00d 100644 --- a/lib/security.ts +++ b/lib/security.ts @@ -18,15 +18,7 @@ export function isValidUrl(url: string): boolean { } export function isPositiveInteger(value: unknown): value is number { - return Number.isInteger(value) && Number(value) > 0; -} - -export function validateInteger(value: unknown): number | null { - const num = Number(value); - if (Number.isInteger(num) && num > 0) { - return num; - } - return null; + return Number.isInteger(value) && value > 0; } // String sanitization diff --git a/middleware.ts b/middleware.ts index 10f2ea0..910dd00 100644 --- a/middleware.ts +++ b/middleware.ts @@ -21,10 +21,7 @@ export function middleware(request: NextRequest) { // Skip authentication for localhost/internal requests (optional security) const host = request.headers.get('host'); if (host && (host.startsWith('localhost') || host.startsWith('127.0.0.1') || host.startsWith('192.168.'))) { - // Don't log for frequently polled endpoints to reduce noise - if (!request.nextUrl.pathname.includes('/api/obsStatus')) { - console.log('Allowing internal network access without API key'); - } + console.log('Allowing internal network access without API key'); return NextResponse.next(); } diff --git a/scripts/addGroupNameToTeams.ts b/scripts/addGroupNameToTeams.ts deleted file mode 100644 index 8a24fba..0000000 --- a/scripts/addGroupNameToTeams.ts +++ /dev/null @@ -1,57 +0,0 @@ -import sqlite3 from 'sqlite3'; -import { open } from 'sqlite'; -import path from 'path'; -import { getTableName, BASE_TABLE_NAMES } from '../lib/constants'; - -const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files'); - -const addGroupNameToTeams = async () => { - try { - const dbPath = path.join(FILE_DIRECTORY, 'sources.db'); - - // Open database connection - const db = await open({ - filename: dbPath, - driver: sqlite3.Database, - }); - - console.log('Database connection established.'); - - // Generate table name for teams - const teamsTableName = getTableName(BASE_TABLE_NAMES.TEAMS, { - year: 2025, - season: 'summer', - suffix: 'sat' - }); - - console.log(`Adding group_name column to ${teamsTableName}`); - - // Check if column already exists - const tableInfo = await db.all(`PRAGMA table_info(${teamsTableName})`); - const hasGroupName = tableInfo.some(col => col.name === 'group_name'); - - if (!hasGroupName) { - // Add group_name column - await db.exec(` - ALTER TABLE ${teamsTableName} - ADD COLUMN group_name TEXT - `); - - console.log(`✅ Added group_name column to ${teamsTableName}`); - } else { - console.log(`â„šī¸ group_name column already exists in ${teamsTableName}`); - } - - // Close database connection - await db.close(); - console.log('Database connection closed.'); - console.log('✅ Successfully updated teams table schema!'); - - } catch (error) { - console.error('Error updating table:', error); - process.exit(1); - } -}; - -// Run the script -addGroupNameToTeams(); \ No newline at end of file diff --git a/scripts/createSatSummer2025Tables.ts b/scripts/createSatSummer2025Tables.ts index c60ec68..d2324bb 100644 --- a/scripts/createSatSummer2025Tables.ts +++ b/scripts/createSatSummer2025Tables.ts @@ -60,8 +60,7 @@ const createSatSummer2025Tables = async () => { await db.exec(` CREATE TABLE IF NOT EXISTS ${teamsTableName} ( team_id INTEGER PRIMARY KEY, - team_name TEXT NOT NULL, - group_name TEXT + team_name TEXT NOT NULL ) `); diff --git a/tsconfig.json b/tsconfig.json index 5a35e12..5e7c74b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,6 +24,6 @@ "@/*": ["./*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "lib/obsService.js", "types/**/*.d.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "lib/obsService.js"], "exclude": ["node_modules"] } diff --git a/types/index.ts b/types/index.ts index fcb17f1..1ba7870 100644 --- a/types/index.ts +++ b/types/index.ts @@ -14,5 +14,4 @@ export type Screen = { export type Team = { team_id: number; team_name: string; - group_name?: string | null; }; \ No newline at end of file diff --git a/types/jest-dom.d.ts b/types/jest-dom.d.ts deleted file mode 100644 index 56fa56b..0000000 --- a/types/jest-dom.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Jest DOM type definitions -import '@testing-library/jest-dom'; - -declare global { - namespace NodeJS { - interface ProcessEnv { - NODE_ENV: 'development' | 'production' | 'test'; - } - } -} \ No newline at end of file