Merge commit '5793617be3154877fa10e4f46b7608de86fa54ca' into dev

This commit is contained in:
2026-05-19 07:36:48 -04:00
23 changed files with 4335 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# RemoteRig Frontend Environment Variables
# Copy this file to .env and fill in your values
# Backend API URL (default: /api proxied through Vite dev server)
VITE_API_URL=http://localhost:8080/api
+33
View File
@@ -0,0 +1,33 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Dependencies
node_modules
dist
dist-ssr
*.local
# Environment files
.env
.env.local
.env.*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Vite
.vite
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/utils",
"hooks": "@/hooks",
"lib": "@/lib",
"types": "@/types"
}
}
+28
View File
@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
+15
View File
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RemoteRig — Camera Monitoring Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
</head>
<body class="bg-rig-dark-900 text-rig-dark-100">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+3860
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
{
"name": "remoterig-dashboard",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.469.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.13.0",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"typescript": "~5.7.2",
"typescript-eslint": "^8.18.0",
"vite": "^6.1.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+46
View File
@@ -0,0 +1,46 @@
import { Camera } from 'lucide-react'
function App() {
return (
<div className="min-h-screen bg-rig-dark-900">
{/* Header */}
<header className="border-b border-rig-dark-700 bg-rig-dark-800/50 backdrop-blur-sm">
<div className="mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:px-8">
<div className="flex items-center gap-3">
<Camera className="h-7 w-7 text-rig-accent" />
<h1 className="text-xl font-bold tracking-tight text-rig-dark-50">
RemoteRig
</h1>
<span className="ml-2 rounded-full bg-rig-accent/10 px-2.5 py-0.5 text-xs font-medium text-rig-accent">
Dashboard
</span>
</div>
</div>
</header>
{/* Main Content */}
<main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-rig-dark-600 bg-rig-dark-800/30 py-24 text-center">
<Camera className="mb-4 h-12 w-12 text-rig-dark-500" />
<h2 className="text-lg font-semibold text-rig-dark-200">
Dashboard Coming Soon
</h2>
<p className="mt-2 max-w-sm text-sm text-rig-dark-400">
Camera monitoring and remote control interface under construction.
</p>
</div>
</main>
{/* Footer */}
<footer className="border-t border-rig-dark-700 bg-rig-dark-800/30">
<div className="mx-auto max-w-7xl px-4 py-3 sm:px-6 lg:px-8">
<p className="text-center text-xs text-rig-dark-500">
RemoteRig v0.1.0 &mdash; Multi-Camera Remote Monitoring System
</p>
</div>
</footer>
</div>
)
}
export default App
+2
View File
@@ -0,0 +1,2 @@
export { useCameraStatus } from './useCameraStatus'
export { useSystemHealth } from './useSystemHealth'
+23
View File
@@ -0,0 +1,23 @@
import { useState, useEffect } from 'react'
import type { Camera } from '../types'
const MOCK_CAMERAS: Camera[] = [
{ id: 'cam-1', name: 'Front View', status: 'online', position: 'North', fps: 30, resolution: '1920x1080', streamUrl: '/api/cameras/cam-1/stream', recording: true },
{ id: 'cam-2', name: 'Side View', status: 'online', position: 'East', fps: 30, resolution: '1920x1080', streamUrl: '/api/cameras/cam-2/stream', recording: false },
{ id: 'cam-3', name: 'Top View', status: 'connecting', position: 'Center', fps: 15, streamUrl: '/api/cameras/cam-3/stream', recording: false },
]
export function useCameraStatus() {
const [cameras, setCameras] = useState<Camera[]>(MOCK_CAMERAS)
useEffect(() => {
const interval = setInterval(() => {
setCameras((prev) =>
prev.map((c) => ({ ...c })),
)
}, 5000)
return () => clearInterval(interval)
}, [])
return { cameras }
}
+28
View File
@@ -0,0 +1,28 @@
import { useState, useEffect } from 'react'
import type { SystemHealth } from '../types'
const MOCK_HEALTH: SystemHealth = {
cpuUsage: 23,
memoryUsage: 45,
gpuUsage: 12,
temperature: 58,
uptime: '2d 14h 32m',
}
export function useSystemHealth() {
const [health, setHealth] = useState<SystemHealth>(MOCK_HEALTH)
useEffect(() => {
const interval = setInterval(() => {
setHealth((prev) => ({
...prev,
cpuUsage: Math.min(100, Math.max(0, prev.cpuUsage + Math.round((Math.random() - 0.5) * 10))),
memoryUsage: Math.min(100, Math.max(0, prev.memoryUsage + Math.round((Math.random() - 0.5) * 4))),
temperature: Math.min(95, Math.max(30, prev.temperature + Math.round((Math.random() - 0.5) * 3))),
}))
}, 3000)
return () => clearInterval(interval)
}, [])
return { health }
}
+30
View File
@@ -0,0 +1,30 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Dashboard-specific resets */
html {
@apply antialiased;
}
body {
@apply bg-rig-dark-900 text-rig-dark-100;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
@apply bg-rig-dark-800;
}
::-webkit-scrollbar-thumb {
@apply bg-rig-dark-600 rounded;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-rig-dark-500;
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
+20
View File
@@ -0,0 +1,20 @@
const API_BASE = import.meta.env.VITE_API_URL || '/api'
async function request<T>(endpoint: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${API_BASE}${endpoint}`, {
headers: { 'Content-Type': 'application/json' },
...options,
})
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`)
}
return response.json()
}
export const api = {
getCameras: () => request<[]>('/cameras'),
getCameraStatus: (id: string) => request<[]>(`/cameras/${id}/status`),
getSystemHealth: () => request<[]>('/system/health'),
toggleRecording: (cameraId: string) =>
request<[]>(`/cameras/${cameraId}/recording`, { method: 'POST' }),
}
+35
View File
@@ -0,0 +1,35 @@
// RemoteRig TypeScript types
export interface Camera {
id: string
name: string
streamUrl: string
status: 'online' | 'offline' | 'error' | 'connecting'
position?: string
fps: number
resolution?: string
recording: boolean
}
export interface CameraFeed {
cameraId: string
thumbnailUrl?: string
frameRate: number
bitrate?: string
}
export interface SystemHealth {
cpuUsage: number
memoryUsage: number
gpuUsage: number
temperature: number
uptime: string
}
export interface StreamConfig {
width: number
height: number
fps: number
codec: string
bitrate: string
}
+25
View File
@@ -0,0 +1,25 @@
export function formatUptime(seconds: number): string {
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const parts: string[] = []
if (days > 0) parts.push(`${days}d`)
if (hours > 0) parts.push(`${hours}h`)
parts.push(`${minutes}m`)
return parts.join(' ')
}
export function statusColor(status: string): string {
switch (status) {
case 'online':
return 'bg-rig-success'
case 'offline':
return 'bg-rig-danger'
case 'error':
return 'bg-rig-danger'
case 'connecting':
return 'bg-rig-warning'
default:
return 'bg-rig-dark-500'
}
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+40
View File
@@ -0,0 +1,40 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
darkMode: 'class',
theme: {
extend: {
colors: {
// Dark dashboard palette
'rig-dark': {
50: '#f3f4f6',
100: '#e5e7eb',
200: '#d1d5db',
300: '#9ca3af',
400: '#6b7280',
500: '#4b5563',
600: '#374151',
700: '#1f2937',
800: '#111827',
900: '#030712',
},
'rig-accent': {
DEFAULT: '#22d3ee',
light: '#67e8f9',
dark: '#06b6d4',
},
'rig-danger': '#ef4444',
'rig-success': '#22c55e',
'rig-warning': '#eab308',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
},
},
},
plugins: [],
}
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
+13
View File
@@ -0,0 +1,13 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
})