Merge pull request 'Add OBS scene switching controls with dynamic button states' (#12) from scene-switching-controls into main
All checks were successful
Lint and Build / build (push) Successful in 2m40s
All checks were successful
Lint and Build / build (push) Successful in 2m40s
Reviewed-on: #12
This commit is contained in:
commit
fdf781dcf9
14 changed files with 6257 additions and 1668 deletions
18
CLAUDE.md
18
CLAUDE.md
|
@ -64,7 +64,7 @@ This is a Next.js web application (branded as "Live Stream Manager") that contro
|
|||
- Glass morphism effects with proper backdrop blur
|
||||
- Distinctive active navigation states for clear wayfinding
|
||||
|
||||
5. **Screen Position Management**: Seven distinct screen positions (large, left, right, topLeft, topRight, bottomLeft, bottomRight) with individual source control
|
||||
5. **Screen Position Management**: Seven distinct screen positions (large, left, right, top_left, top_right, bottom_left, bottom_right) with individual source control
|
||||
|
||||
6. **Real-time Status Monitoring**: Footer component polls OBS status every 30 seconds showing connection, streaming, and recording status
|
||||
|
||||
|
@ -74,6 +74,8 @@ This is a Next.js web application (branded as "Live Stream Manager") that contro
|
|||
|
||||
9. **Stream Deletion with Confirmation**: Safe deletion workflow that removes streams from both OBS and database with user confirmation prompts
|
||||
|
||||
10. **OBS Scene Control**: Direct scene switching controls with dynamic state tracking and real-time synchronization between UI and OBS
|
||||
|
||||
### Environment Configuration
|
||||
- `FILE_DIRECTORY`: Directory for database and text files (default: ./files)
|
||||
- `OBS_WEBSOCKET_HOST`: OBS WebSocket host (default: 127.0.0.1)
|
||||
|
@ -112,6 +114,10 @@ This is a Next.js web application (branded as "Live Stream Manager") that contro
|
|||
- `POST /api/syncGroups` - Synchronize all teams with OBS groups
|
||||
- `GET /api/verifyGroups` - Verify database groups exist in OBS with UUID tracking
|
||||
|
||||
#### OBS Scene Control
|
||||
- `POST /api/setScene` - Switch OBS to specified scene layout (1-Screen, 2-Screen, 4-Screen)
|
||||
- `GET /api/getCurrentScene` - Get currently active OBS scene for state synchronization
|
||||
|
||||
#### System Status
|
||||
- `GET /api/obsStatus` - Real-time OBS connection and streaming status
|
||||
|
||||
|
@ -222,6 +228,16 @@ See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructio
|
|||
- ⚠️ "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
|
||||
|
||||
### OBS Scene Control
|
||||
- **Dynamic Scene Switching**: Direct control of OBS scene layouts (1-Screen, 2-Screen, 4-Screen) from the main interface
|
||||
- **Real-time State Tracking**: Buttons dynamically show active/inactive states based on current OBS scene
|
||||
- **Visual State Indicators**:
|
||||
- Active buttons: Green/yellow gradient with "Active: X-Screen" text
|
||||
- Inactive buttons: Blue/cyan gradient with "Switch to X-Screen" text
|
||||
- **Optimistic UI Updates**: Immediate visual feedback when switching scenes
|
||||
- **Glass Morphism Integration**: Scene buttons styled consistently with existing design system
|
||||
- **Toast Feedback**: Success/error notifications for scene switching operations
|
||||
|
||||
### User Experience Improvements
|
||||
- **Toast Notifications**: Real-time feedback for all operations (success/error/info)
|
||||
- **Form Validation**: Client-side validation with immediate error feedback
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
A professional [Next.js](https://nextjs.org) web application for managing live streams and controlling multiple OBS [Source Switchers](https://github.com/exeldro/obs-source-switcher) with real-time WebSocket integration and modern glass morphism UI.
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **OBS Scene Control**: Switch between OBS layouts (1-Screen, 2-Screen, 4-Screen) with dynamic button states
|
||||
- **Multi-Screen Source Control**: Manage 7 different screen positions (large, left, right, and 4 corners)
|
||||
- **Real-time OBS Integration**: WebSocket connection with live status monitoring
|
||||
- **Enhanced Stream Management**: Create, edit, and delete streams with comprehensive OBS cleanup
|
||||
|
@ -145,6 +146,10 @@ npm run type-check # TypeScript validation
|
|||
- Identifies name mismatches
|
||||
- Shows sync status for all teams
|
||||
|
||||
### OBS Scene Control
|
||||
- `POST /api/setScene` - Switch OBS to specified scene (1-Screen, 2-Screen, 4-Screen)
|
||||
- `GET /api/getCurrentScene` - Get currently active OBS scene
|
||||
|
||||
### System Status
|
||||
- `GET /api/obsStatus` - Real-time OBS connection, streaming, and recording status
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { createSuccessResponse, createErrorResponse, withErrorHandling } from '../../../lib/apiHelpers';
|
||||
import { SCREEN_POSITIONS } from '../../../lib/constants';
|
||||
|
||||
const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files')
|
||||
// Ensure directory exists
|
||||
|
@ -11,31 +12,17 @@ console.log('using', FILE_DIRECTORY)
|
|||
|
||||
async function getActiveHandler() {
|
||||
try {
|
||||
const largePath = path.join(FILE_DIRECTORY, 'large.txt');
|
||||
const leftPath = path.join(FILE_DIRECTORY, 'left.txt');
|
||||
const rightPath = path.join(FILE_DIRECTORY, 'right.txt');
|
||||
const topLeftPath = path.join(FILE_DIRECTORY, 'topLeft.txt');
|
||||
const topRightPath = path.join(FILE_DIRECTORY, 'topRight.txt');
|
||||
const bottomLeftPath = path.join(FILE_DIRECTORY, 'bottomLeft.txt');
|
||||
const bottomRightPath = path.join(FILE_DIRECTORY, 'bottomRight.txt');
|
||||
const activeSources: Record<string, string | null> = {};
|
||||
|
||||
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;
|
||||
// Read each screen position file using the constant
|
||||
for (const screen of SCREEN_POSITIONS) {
|
||||
const filePath = path.join(FILE_DIRECTORY, `${screen}.txt`);
|
||||
activeSources[screen] = fs.existsSync(filePath)
|
||||
? fs.readFileSync(filePath, 'utf-8').trim()
|
||||
: null;
|
||||
}
|
||||
|
||||
return createSuccessResponse({
|
||||
large,
|
||||
left,
|
||||
right,
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomLeft,
|
||||
bottomRight
|
||||
});
|
||||
return createSuccessResponse(activeSources);
|
||||
} catch (error) {
|
||||
console.error('Error reading active sources:', error);
|
||||
return createErrorResponse('Failed to read active sources', 500, 'Could not read source files', error);
|
||||
|
|
30
app/api/getCurrentScene/route.ts
Normal file
30
app/api/getCurrentScene/route.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { getOBSClient } from '../../../lib/obsClient';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const obsClient = await getOBSClient();
|
||||
|
||||
// Get the current program scene
|
||||
const response = await obsClient.call('GetCurrentProgramScene');
|
||||
const { currentProgramSceneName } = response;
|
||||
|
||||
console.log(`Current OBS scene: ${currentProgramSceneName}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { sceneName: currentProgramSceneName },
|
||||
message: 'Current scene retrieved successfully'
|
||||
});
|
||||
} catch (obsError) {
|
||||
console.error('OBS WebSocket error:', obsError);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to get current scene from OBS',
|
||||
details: obsError instanceof Error ? obsError.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
66
app/api/setScene/route.ts
Normal file
66
app/api/setScene/route.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getOBSClient } from '../../../lib/obsClient';
|
||||
|
||||
// Valid scene names for this application
|
||||
const VALID_SCENES = ['1-Screen', '2-Screen', '4-Screen'] as const;
|
||||
type ValidScene = typeof VALID_SCENES[number];
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { sceneName } = body;
|
||||
|
||||
// Validate scene name
|
||||
if (!sceneName || typeof sceneName !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Scene name is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!VALID_SCENES.includes(sceneName as ValidScene)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid scene name',
|
||||
validScenes: VALID_SCENES
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const obsClient = await getOBSClient();
|
||||
|
||||
// Switch to the requested scene
|
||||
await obsClient.call('SetCurrentProgramScene', { sceneName });
|
||||
|
||||
console.log(`Successfully switched to scene: ${sceneName}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { sceneName },
|
||||
message: `Switched to ${sceneName} layout`
|
||||
});
|
||||
} catch (obsError) {
|
||||
console.error('OBS WebSocket error:', obsError);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to switch scene in OBS',
|
||||
details: obsError instanceof Error ? obsError.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error switching scene:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid request format'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
126
app/page.tsx
126
app/page.tsx
|
@ -5,24 +5,20 @@ import Dropdown from '@/components/Dropdown';
|
|||
import { useToast } from '@/lib/useToast';
|
||||
import { ToastContainer } from '@/components/Toast';
|
||||
import { useActiveSourceLookup, useDebounce, PerformanceMonitor } from '@/lib/performance';
|
||||
import { SCREEN_POSITIONS } from '@/lib/constants';
|
||||
|
||||
import { StreamWithTeam } from '@/types';
|
||||
|
||||
type ScreenType = 'large' | 'left' | 'right' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
||||
type ScreenType = typeof SCREEN_POSITIONS[number];
|
||||
|
||||
export default function Home() {
|
||||
const [streams, setStreams] = useState<StreamWithTeam[]>([]);
|
||||
const [activeSources, setActiveSources] = useState<Record<ScreenType, string | null>>({
|
||||
large: null,
|
||||
left: null,
|
||||
right: null,
|
||||
topLeft: null,
|
||||
topRight: null,
|
||||
bottomLeft: null,
|
||||
bottomRight: null,
|
||||
});
|
||||
const [activeSources, setActiveSources] = useState<Record<ScreenType, string | null>>(
|
||||
Object.fromEntries(SCREEN_POSITIONS.map(screen => [screen, null])) as Record<ScreenType, string | null>
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
||||
const [currentScene, setCurrentScene] = useState<string | null>(null);
|
||||
const { toasts, removeToast, showSuccess, showError } = useToast();
|
||||
|
||||
// Memoized active source lookup for performance
|
||||
|
@ -63,22 +59,27 @@ export default function Home() {
|
|||
const fetchData = useCallback(async () => {
|
||||
const endTimer = PerformanceMonitor.startTimer('fetchData');
|
||||
try {
|
||||
// Fetch streams and active sources in parallel
|
||||
const [streamsRes, activeRes] = await Promise.all([
|
||||
// Fetch streams, active sources, and current scene in parallel
|
||||
const [streamsRes, activeRes, sceneRes] = await Promise.all([
|
||||
fetch('/api/streams'),
|
||||
fetch('/api/getActive')
|
||||
fetch('/api/getActive'),
|
||||
fetch('/api/getCurrentScene')
|
||||
]);
|
||||
|
||||
const [streamsData, activeData] = await Promise.all([
|
||||
const [streamsData, activeData, sceneData] = await Promise.all([
|
||||
streamsRes.json(),
|
||||
activeRes.json()
|
||||
activeRes.json(),
|
||||
sceneRes.json()
|
||||
]);
|
||||
|
||||
// Handle both old and new API response formats
|
||||
const streams = streamsData.success ? streamsData.data : streamsData;
|
||||
const activeSources = activeData.success ? activeData.data : activeData;
|
||||
const sceneName = sceneData.success ? sceneData.data.sceneName : null;
|
||||
|
||||
setStreams(streams);
|
||||
setActiveSources(activeSources);
|
||||
setCurrentScene(sceneName);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
showError('Failed to Load Data', 'Could not fetch streams. Please refresh the page.');
|
||||
|
@ -114,14 +115,48 @@ export default function Home() {
|
|||
setOpenDropdown((prev) => (prev === screen ? null : screen));
|
||||
}, []);
|
||||
|
||||
const handleSceneSwitch = useCallback(async (sceneName: string) => {
|
||||
try {
|
||||
const response = await fetch('/api/setScene', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sceneName }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Update local state immediately for responsive UI
|
||||
setCurrentScene(sceneName);
|
||||
showSuccess('Scene Changed', `Switched to ${sceneName} layout`);
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to switch scene');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error switching scene:', error);
|
||||
showError('Scene Switch Failed', error instanceof Error ? error.message : 'Could not switch scene. Please try again.');
|
||||
}
|
||||
}, [showSuccess, showError]);
|
||||
|
||||
// Memoized corner displays to prevent re-renders
|
||||
const cornerDisplays = useMemo(() => [
|
||||
{ screen: 'topLeft' as const, label: 'Top Left' },
|
||||
{ screen: 'topRight' as const, label: 'Top Right' },
|
||||
{ screen: 'bottomLeft' as const, label: 'Bottom Left' },
|
||||
{ screen: 'bottomRight' as const, label: 'Bottom Right' },
|
||||
{ screen: 'top_left' as const, label: 'Top Left' },
|
||||
{ screen: 'top_right' as const, label: 'Top Right' },
|
||||
{ screen: 'bottom_left' as const, label: 'Bottom Left' },
|
||||
{ screen: 'bottom_right' as const, label: 'Bottom Right' },
|
||||
], []);
|
||||
|
||||
// Transform and sort streams for dropdown display
|
||||
const dropdownStreams = useMemo(() => {
|
||||
return streams
|
||||
.map(stream => ({
|
||||
id: stream.id,
|
||||
name: `${stream.team_name} - ${stream.name}`,
|
||||
originalStream: stream
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [streams]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="container section">
|
||||
|
@ -145,10 +180,23 @@ export default function Home() {
|
|||
|
||||
{/* Main Screen */}
|
||||
<div className="glass p-6 mb-6">
|
||||
<h2 className="card-title">Primary Display</h2>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="card-title mb-0">Primary Display</h2>
|
||||
<button
|
||||
onClick={() => handleSceneSwitch('1-Screen')}
|
||||
className={`btn ${currentScene === '1-Screen' ? 'active' : ''}`}
|
||||
style={{
|
||||
background: currentScene === '1-Screen'
|
||||
? 'linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow))'
|
||||
: 'linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan))'
|
||||
}}
|
||||
>
|
||||
{currentScene === '1-Screen' ? 'Active: 1-Screen' : 'Switch to 1-Screen'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="max-w-md mx-auto">
|
||||
<Dropdown
|
||||
options={streams}
|
||||
options={dropdownStreams}
|
||||
activeId={activeSourceIds.large}
|
||||
onSelect={(id) => handleSetActive('large', id)}
|
||||
label="Select Primary Stream..."
|
||||
|
@ -160,12 +208,25 @@ export default function Home() {
|
|||
|
||||
{/* Side Displays */}
|
||||
<div className="glass p-6 mb-6">
|
||||
<h2 className="card-title">Side Displays</h2>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="card-title mb-0">Side Displays</h2>
|
||||
<button
|
||||
onClick={() => handleSceneSwitch('2-Screen')}
|
||||
className={`btn ${currentScene === '2-Screen' ? 'active' : ''}`}
|
||||
style={{
|
||||
background: currentScene === '2-Screen'
|
||||
? 'linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow))'
|
||||
: 'linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan))'
|
||||
}}
|
||||
>
|
||||
{currentScene === '2-Screen' ? 'Active: 2-Screen' : 'Switch to 2-Screen'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid-2">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4 text-center">Left Display</h3>
|
||||
<Dropdown
|
||||
options={streams}
|
||||
options={dropdownStreams}
|
||||
activeId={activeSourceIds.left}
|
||||
onSelect={(id) => handleSetActive('left', id)}
|
||||
label="Select Left Stream..."
|
||||
|
@ -176,7 +237,7 @@ export default function Home() {
|
|||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4 text-center">Right Display</h3>
|
||||
<Dropdown
|
||||
options={streams}
|
||||
options={dropdownStreams}
|
||||
activeId={activeSourceIds.right}
|
||||
onSelect={(id) => handleSetActive('right', id)}
|
||||
label="Select Right Stream..."
|
||||
|
@ -189,13 +250,26 @@ export default function Home() {
|
|||
|
||||
{/* Corner Displays */}
|
||||
<div className="glass p-6">
|
||||
<h2 className="card-title">Corner Displays</h2>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="card-title mb-0">Corner Displays</h2>
|
||||
<button
|
||||
onClick={() => handleSceneSwitch('4-Screen')}
|
||||
className={`btn ${currentScene === '4-Screen' ? 'active' : ''}`}
|
||||
style={{
|
||||
background: currentScene === '4-Screen'
|
||||
? 'linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow))'
|
||||
: 'linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan))'
|
||||
}}
|
||||
>
|
||||
{currentScene === '4-Screen' ? 'Active: 4-Screen' : 'Switch to 4-Screen'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid-4">
|
||||
{cornerDisplays.map(({ screen, label }) => (
|
||||
<div key={screen}>
|
||||
<h3 className="text-md font-semibold mb-3 text-center">{label}</h3>
|
||||
<Dropdown
|
||||
options={streams}
|
||||
options={dropdownStreams}
|
||||
activeId={activeSourceIds[screen]}
|
||||
onSelect={(id) => handleSetActive(screen, id)}
|
||||
label="Select Stream..."
|
||||
|
|
|
@ -17,10 +17,10 @@ You must create **exactly 7 Source Switcher sources** in OBS with these specific
|
|||
| `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` |
|
||||
| `ss_top_left` | Top left corner | `top_left.txt` |
|
||||
| `ss_top_right` | Top right corner | `top_right.txt` |
|
||||
| `ss_bottom_left` | Bottom left corner | `bottom_left.txt` |
|
||||
| `ss_bottom_right` | Bottom right corner | `bottom_right.txt` |
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
|
|
BIN
docs/new_home.png
Normal file
BIN
docs/new_home.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 200 KiB |
1678
files/SaT.json
1678
files/SaT.json
File diff suppressed because it is too large
Load diff
3067
files/SaT.json.bak
3067
files/SaT.json.bak
File diff suppressed because it is too large
Load diff
|
@ -45,10 +45,10 @@ export const SCREEN_POSITIONS = [
|
|||
'large',
|
||||
'left',
|
||||
'right',
|
||||
'topLeft',
|
||||
'topRight',
|
||||
'bottomLeft',
|
||||
'bottomRight'
|
||||
'top_left',
|
||||
'top_right',
|
||||
'bottom_left',
|
||||
'bottom_right'
|
||||
] as const;
|
||||
|
||||
export const SOURCE_SWITCHER_NAMES = [
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Security utilities for input validation and sanitization
|
||||
|
||||
export const VALID_SCREENS = ['large', 'left', 'right', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight'] as const;
|
||||
export const VALID_SCREENS = ['large', 'left', 'right', 'top_left', 'top_right', 'bottom_left', 'bottom_right'] as const;
|
||||
export type ValidScreen = typeof VALID_SCREENS[number];
|
||||
|
||||
// Input validation functions
|
||||
|
|
|
@ -21,10 +21,6 @@ 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');
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
|
|
2878
package-lock.json
generated
2878
package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue