diff --git a/app/globals.css b/app/globals.css
index ae33ffe..dac679e 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -73,7 +73,8 @@ body {
}
/* Glass Card Component */
-.glass {
+.glass,
+.glass-panel {
background: rgba(7, 54, 66, 0.4);
backdrop-filter: blur(10px);
border: 1px solid rgba(88, 110, 117, 0.3);
@@ -489,4 +490,59 @@ body {
background: rgba(7, 54, 66, 0.2);
border: 1px solid rgba(88, 110, 117, 0.2);
border-radius: 12px;
-}
\ No newline at end of file
+}
+
+/* Solarized Color Utilities */
+.text-base03 { color: var(--solarized-base03); }
+.text-base02 { color: var(--solarized-base02); }
+.text-base01 { color: var(--solarized-base01); }
+.text-base00 { color: var(--solarized-base00); }
+.text-base0 { color: var(--solarized-base0); }
+.text-base1 { color: var(--solarized-base1); }
+.text-base2 { color: var(--solarized-base2); }
+.text-base3 { color: var(--solarized-base3); }
+.text-blue { color: var(--solarized-blue); }
+.text-cyan { color: var(--solarized-cyan); }
+.text-green { color: var(--solarized-green); }
+.text-yellow { color: var(--solarized-yellow); }
+.text-orange { color: var(--solarized-orange); }
+.text-red { color: var(--solarized-red); }
+.text-magenta { color: var(--solarized-magenta); }
+.text-violet { color: var(--solarized-violet); }
+
+.bg-base03 { background-color: var(--solarized-base03); }
+.bg-base02 { background-color: var(--solarized-base02); }
+.bg-base01 { background-color: var(--solarized-base01); }
+.bg-base00 { background-color: var(--solarized-base00); }
+.bg-base0 { background-color: var(--solarized-base0); }
+.bg-base1 { background-color: var(--solarized-base1); }
+.bg-base2 { background-color: var(--solarized-base2); }
+.bg-base3 { background-color: var(--solarized-base3); }
+.bg-blue { background-color: var(--solarized-blue); }
+.bg-cyan { background-color: var(--solarized-cyan); }
+.bg-green { background-color: var(--solarized-green); }
+.bg-yellow { background-color: var(--solarized-yellow); }
+.bg-orange { background-color: var(--solarized-orange); }
+.bg-red { background-color: var(--solarized-red); }
+.bg-magenta { background-color: var(--solarized-magenta); }
+.bg-violet { background-color: var(--solarized-violet); }
+
+.border-base01 { border-color: var(--solarized-base01); }
+.border-base02 { border-color: var(--solarized-base02); }
+.border-blue { border-color: var(--solarized-blue); }
+.border-cyan { border-color: var(--solarized-cyan); }
+.border-green { border-color: var(--solarized-green); }
+.border-yellow { border-color: var(--solarized-yellow); }
+.border-orange { border-color: var(--solarized-orange); }
+.border-red { border-color: var(--solarized-red); }
+
+/* Border opacity utilities */
+.border-green\/30 { border-color: rgba(133, 153, 0, 0.3); }
+.border-yellow\/30 { border-color: rgba(181, 137, 0, 0.3); }
+.border-blue\/30 { border-color: rgba(38, 139, 210, 0.3); }
+.border-red\/30 { border-color: rgba(220, 50, 47, 0.3); }
+
+/* Focus utilities */
+.focus\:outline-none:focus { outline: none; }
+.focus\:ring-2:focus { box-shadow: 0 0 0 2px var(--solarized-blue); }
+.focus\:ring-blue:focus { box-shadow: 0 0 0 2px var(--solarized-blue); }
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index c3ab1bc..4b3dbbc 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -2,7 +2,7 @@ import './globals.css';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { ErrorBoundary } from '@/components/ErrorBoundary';
-import PerformanceDashboard from '@/components/PerformanceDashboard';
+import { ApiKeyProvider } from '@/contexts/ApiKeyContext';
export const metadata = {
title: 'Live Stream Manager',
@@ -13,14 +13,15 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
-
-
-
- {children}
-
-
-
-
+
+
+
+
+ {children}
+
+
+
+
);
diff --git a/app/performance/page.tsx b/app/performance/page.tsx
new file mode 100644
index 0000000..4db20db
--- /dev/null
+++ b/app/performance/page.tsx
@@ -0,0 +1,125 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { PerformanceMonitor } from '@/lib/performance';
+
+interface PerformanceMetrics {
+ [key: string]: {
+ avg: number;
+ min: number;
+ max: number;
+ count: number;
+ } | null;
+}
+
+export default function PerformancePage() {
+ const [metrics, setMetrics] = useState({});
+
+ useEffect(() => {
+ const updateMetrics = () => {
+ setMetrics(PerformanceMonitor.getAllMetrics());
+ };
+
+ // Update metrics every 2 seconds
+ updateMetrics();
+ const interval = setInterval(updateMetrics, 2000);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+
+
+
Performance Metrics
+
+ {Object.keys(metrics).length === 0 ? (
+
+
No metrics collected yet. Navigate around the app to see performance data.
+
+ ) : (
+
+ {Object.entries(metrics).map(([label, metric]) => {
+ if (!metric) return null;
+
+ return (
+
+
+ {label.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
+
+
+
+
+ Average
+ {metric.avg.toFixed(2)}ms
+
+
+
+ Min
+ {metric.min.toFixed(2)}ms
+
+
+
+ Max
+ 100 ? 'text-red' : 'text-yellow'}`}>
+ {metric.max.toFixed(2)}ms
+
+
+
+
+ Count
+ {metric.count}
+
+
+
+ {/* Performance indicator bar */}
+
+
+
+
+ {metric.avg < 50 ? 'Excellent' :
+ metric.avg < 100 ? 'Good' : 'Needs Optimization'}
+
+
+
+
+ );
+ })}
+
+ {/* Performance tips */}
+
+
đĄ Performance Tips
+
+
+
Response Times
+
+ < 50ms - Excellent user experience
+ 50-100ms - Good, barely noticeable
+ 100-300ms - Noticeable delay
+ > 300ms - Frustrating for users
+
+
+
+
Optimization Strategies
+
+ Monitor fetchData and setActive timings
+ High max values indicate performance spikes
+ Consider caching for frequently called APIs
+ Batch multiple requests when possible
+
+
+
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/settings/page.tsx b/app/settings/page.tsx
new file mode 100644
index 0000000..07f070b
--- /dev/null
+++ b/app/settings/page.tsx
@@ -0,0 +1,158 @@
+'use client';
+
+import { useState } from 'react';
+import { useApiKey } from '@/contexts/ApiKeyContext';
+
+export default function SettingsPage() {
+ const { apiKey, setApiKey, clearApiKey, isAuthenticated } = useApiKey();
+ const [inputValue, setInputValue] = useState('');
+ const [error, setError] = useState('');
+ const [success, setSuccess] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setSuccess('');
+ setIsLoading(true);
+
+ if (!inputValue.trim()) {
+ setError('API key is required');
+ setIsLoading(false);
+ return;
+ }
+
+ // Test the API key by making a simple request
+ try {
+ const response = await fetch('/api/obsStatus', {
+ headers: {
+ 'x-api-key': inputValue.trim()
+ }
+ });
+
+ if (response.ok) {
+ setApiKey(inputValue.trim());
+ setInputValue('');
+ setSuccess('API key saved successfully!');
+ } else {
+ setError('Invalid API key');
+ }
+ } catch {
+ setError('Failed to validate API key');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleClearKey = () => {
+ clearApiKey();
+ setInputValue('');
+ setError('');
+ setSuccess('API key cleared');
+ };
+
+ return (
+
+
+
Settings
+
+ {/* API Key Section */}
+
+
+
API Key Authentication
+
+ API keys are required when accessing this application from external networks.
+ The key is stored securely in your browser's local storage.
+
+
+
+ {/* Current Status */}
+
+
+
+
Current Status
+
+ {isAuthenticated ? (
+ <>
+
+
Authenticated
+ >
+ ) : (
+ <>
+
+
No API key set
+ >
+ )}
+
+
+ {isAuthenticated && (
+
+ Clear Key
+
+ )}
+
+
+
+ {/* API Key Form */}
+
+
+ {/* Information Section */}
+
+
âšī¸ Information
+
+ API keys are only required for external network access
+ Local network access bypasses authentication automatically
+ Keys are validated against the server before saving
+ Your API key is stored locally and never transmitted unnecessarily
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/ApiKeyPrompt.tsx b/components/ApiKeyPrompt.tsx
new file mode 100644
index 0000000..a42611a
--- /dev/null
+++ b/components/ApiKeyPrompt.tsx
@@ -0,0 +1,152 @@
+'use client';
+
+import React, { useState } from 'react';
+import { useApiKey } from '../contexts/ApiKeyContext';
+
+interface ApiKeyPromptProps {
+ show: boolean;
+ onClose?: () => void;
+}
+
+export function ApiKeyPrompt({ show, onClose }: ApiKeyPromptProps) {
+ const { setApiKey } = useApiKey();
+ const [inputValue, setInputValue] = useState('');
+ const [error, setError] = useState('');
+
+ if (!show) return null;
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+
+ if (!inputValue.trim()) {
+ setError('API key is required');
+ return;
+ }
+
+ // Test the API key by making a simple request
+ try {
+ const response = await fetch('/api/obsStatus', {
+ headers: {
+ 'x-api-key': inputValue.trim()
+ }
+ });
+
+ if (response.ok) {
+ setApiKey(inputValue.trim());
+ setInputValue('');
+ onClose?.();
+ } else {
+ setError('Invalid API key');
+ }
+ } catch {
+ setError('Failed to validate API key');
+ }
+ };
+
+ return (
+
+
+
API Key Required
+
+ This application requires an API key for access. Please enter your API key to continue.
+
+
+
+
+
+ );
+}
+
+export function ApiKeyBanner() {
+ const { isAuthenticated, clearApiKey } = useApiKey();
+ const [showPrompt, setShowPrompt] = useState(false);
+
+ if (isAuthenticated) {
+ return (
+
+
+ â
+ Authenticated
+
+
+ setShowPrompt(true)}
+ className="text-base1 hover:text-white underline transition-colors"
+ >
+ Change Key
+
+
+ Logout
+
+
+
setShowPrompt(false)} />
+
+ );
+ }
+
+ return (
+ <>
+
+
+ â ī¸
+ API key required for full access
+
+ setShowPrompt(true)}
+ className="text-base1 hover:text-white underline transition-colors"
+ >
+ Enter API Key
+
+
+ setShowPrompt(false)} />
+ >
+ );
+}
\ No newline at end of file
diff --git a/components/Header.tsx b/components/Header.tsx
index 44f8cc6..5c4d9ec 100644
--- a/components/Header.tsx
+++ b/components/Header.tsx
@@ -50,6 +50,24 @@ export default function Header() {
đĨ
Teams
+
+
+ âī¸
+ Settings
+
+
+ {process.env.NODE_ENV === 'development' && (
+
+ đ
+ Perf
+
+ )}
diff --git a/components/PerformanceDashboard.tsx b/components/PerformanceDashboard.tsx
index cfb96ed..b18c95c 100644
--- a/components/PerformanceDashboard.tsx
+++ b/components/PerformanceDashboard.tsx
@@ -36,22 +36,27 @@ export default function PerformanceDashboard() {
}
return (
-
- {!isVisible ? (
-
setIsVisible(true)}
- className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-lg text-sm font-medium shadow-lg"
- title="Show Performance Metrics"
- >
- đ Perf
-
- ) : (
-
+ <>
+ {!isVisible && (
+
+ setIsVisible(true)}
+ className="btn text-sm"
+ title="Show Performance Metrics"
+ >
+ đ Perf
+
+
+ )}
+
+ {isVisible && (
+
+
-
Performance Metrics
+ Performance Metrics
setIsVisible(false)}
- className="text-white/60 hover:text-white text-xl leading-none"
+ className="text-base1 hover:text-white text-xl leading-none transition-colors"
title="Close"
>
Ã
@@ -59,33 +64,33 @@ export default function PerformanceDashboard() {
{Object.keys(metrics).length === 0 ? (
-
No metrics collected yet.
+
No metrics collected yet.
) : (
{Object.entries(metrics).map(([label, metric]) => {
if (!metric) return null;
return (
-
-
+
+
{label.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
- Avg: {' '}
- {metric.avg.toFixed(2)}ms
+ Avg: {' '}
+ {metric.avg.toFixed(2)}ms
- Count: {' '}
- {metric.count}
+ Count: {' '}
+ {metric.count}
- Min: {' '}
- {metric.min.toFixed(2)}ms
+ Min: {' '}
+ {metric.min.toFixed(2)}ms
- Max: {' '}
- 100 ? 'text-red-400' : 'text-yellow-400'}>
+ Max: {' '}
+ 100 ? 'text-red' : 'text-yellow'}>
{metric.max.toFixed(2)}ms
@@ -96,11 +101,11 @@ export default function PerformanceDashboard() {
-
+
{metric.avg < 50 ? 'Excellent' :
metric.avg < 100 ? 'Good' : 'Needs Optimization'}
@@ -111,11 +116,11 @@ export default function PerformanceDashboard() {
})}
{/* Performance tips */}
-
-
+
+
đĄ Performance Tips
-
+
âĸ Keep API calls under 100ms for optimal UX
âĸ Monitor fetchData and setActive timings
âĸ High max values indicate performance spikes
@@ -124,8 +129,9 @@ export default function PerformanceDashboard() {
)}
+
)}
-
+ >
);
}
\ No newline at end of file
diff --git a/contexts/ApiKeyContext.tsx b/contexts/ApiKeyContext.tsx
new file mode 100644
index 0000000..829d266
--- /dev/null
+++ b/contexts/ApiKeyContext.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import React, { createContext, useContext, useState, useEffect } from 'react';
+
+interface ApiKeyContextType {
+ apiKey: string | null;
+ setApiKey: (key: string) => void;
+ clearApiKey: () => void;
+ isAuthenticated: boolean;
+}
+
+const ApiKeyContext = createContext(undefined);
+
+export function ApiKeyProvider({ children }: { children: React.ReactNode }) {
+ const [apiKey, setApiKeyState] = useState(null);
+ const [isLoaded, setIsLoaded] = useState(false);
+
+ // Load API key from localStorage on mount
+ useEffect(() => {
+ const stored = localStorage.getItem('obs-api-key');
+ if (stored) {
+ setApiKeyState(stored);
+ }
+ setIsLoaded(true);
+ }, []);
+
+ const setApiKey = (key: string) => {
+ localStorage.setItem('obs-api-key', key);
+ setApiKeyState(key);
+ };
+
+ const clearApiKey = () => {
+ localStorage.removeItem('obs-api-key');
+ setApiKeyState(null);
+ };
+
+ const isAuthenticated = Boolean(apiKey);
+
+ // Don't render children until we've loaded the API key from storage
+ if (!isLoaded) {
+ return Loading...
;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useApiKey() {
+ const context = useContext(ApiKeyContext);
+ if (context === undefined) {
+ throw new Error('useApiKey must be used within an ApiKeyProvider');
+ }
+ return context;
+}
\ No newline at end of file
diff --git a/lib/apiClient.ts b/lib/apiClient.ts
index 5d386c7..41170b6 100644
--- a/lib/apiClient.ts
+++ b/lib/apiClient.ts
@@ -1,14 +1,13 @@
// API client utility for making authenticated requests
-// Get API key from environment (client-side will need to be provided differently)
+// Get API key from environment or localStorage
function getApiKey(): string | null {
if (typeof window === 'undefined') {
// Server-side
return process.env.API_KEY || null;
} else {
- // Client-side - for now, return null to bypass auth in development
- // In production, this would come from a secure storage or context
- return null;
+ // Client-side - get from localStorage
+ return localStorage.getItem('obs-api-key') || null;
}
}
diff --git a/middleware.ts b/middleware.ts
index 8361919..a20b5db 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -8,8 +8,8 @@ export function middleware(request: NextRequest) {
return NextResponse.next();
}
- // Check for API key in header
- const apiKey = request.headers.get('x-api-key');
+ // Check for API key in header or URL parameter
+ const apiKey = request.headers.get('x-api-key') || request.nextUrl.searchParams.get('apikey');
const validKey = process.env.API_KEY;
// If API_KEY is not set in environment, skip authentication (development mode)