diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 2354f2e..2545297 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,7 +13,8 @@ { "label": "dev", "type": "shell", - "command": "./node_modules/.bin/dotenv -e .dev.env -e .public.env -e .secret.env -- 'turbo run dev --filter=${input:project}-frontend --filter=${input:project}-backend'" + "command": "./node_modules/.bin/dotenv -e .dev.env -e .public.env -e .secret.env -- 'turbo run dev --filter=${input:project}-frontend --filter=${input:project}-backend'", + "problemMatcher": [] }, { "label": "serve", @@ -23,7 +24,10 @@ { "label": "deploy", "type": "shell", - "dependsOn": ["build", "serve"], + "dependsOn": [ + "build", + "serve" + ], "dependsOrder": "sequence" }, { @@ -42,7 +46,7 @@ "id": "project", "description": "Full Stack Projects worth running", "type": "pickString", - "options": ["template", "test"] + "options": ["template", "test", "gb-limelight-recorder"] }, { "id": "workspace", diff --git a/apps/gb-limelight-recorder/backend/Dockerfile b/apps/gb-limelight-recorder/backend/Dockerfile new file mode 100644 index 0000000..7f398e6 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/Dockerfile @@ -0,0 +1,5 @@ +FROM node:20-alpine +WORKDIR /usr/src/app +COPY ./dist /usr/src/app +EXPOSE 4590 +CMD ["node","bundle.js"] \ No newline at end of file diff --git a/apps/gb-limelight-recorder/backend/RecordingProcess.ts b/apps/gb-limelight-recorder/backend/RecordingProcess.ts new file mode 100644 index 0000000..b963e7a --- /dev/null +++ b/apps/gb-limelight-recorder/backend/RecordingProcess.ts @@ -0,0 +1,62 @@ +//בס"ד +import type { ChildProcess } from "child_process"; +import { spawn } from "child_process"; +import ffmpegPath from "ffmpeg-static"; + +class RecordingProcess { + public ffmpegProcess: ChildProcess | null = null; + public cameraUrl: string; + public outputFile: string; + + // --- CONSTRUCTOR --- + public constructor(cameraUrl: string, outputFile: string) { + this.outputFile = outputFile; + + this.cameraUrl = + cameraUrl === "left" ? "http://limelight-left.local:5800" + : cameraUrl === "object" ? "http://limelight-object.local:5800" + : cameraUrl === "right" ? "http://limelight.local:5800" + : cameraUrl; + } + + + // --- START RECORDING --- + public startRecording(): string { + if (this.ffmpegProcess) { + return "Recording already running"; + } + console.log(ffmpegPath); + + // Process initiations + this.ffmpegProcess = spawn(ffmpegPath as unknown as string, [ + "-i", + this.cameraUrl, + "-c:v", + "copy", + this.outputFile, + ]); + + // Logging + this.ffmpegProcess.stderr?.on("data", (data) => { + console.log(data.toString()); + }); + + // Send response + return "Recording started"; + } + + // --- STOP RECORDING --- + public stopRecording(): string { + if (!this.ffmpegProcess) { + return "No recording running"; + } + + this.ffmpegProcess.stdin?.write("q"); + this.ffmpegProcess.stdin?.end(); + this.ffmpegProcess = null; + + return "Recording stopped" + } +} + +export { RecordingProcess }; \ No newline at end of file diff --git a/apps/gb-limelight-recorder/backend/build.ts b/apps/gb-limelight-recorder/backend/build.ts new file mode 100644 index 0000000..3802a64 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/build.ts @@ -0,0 +1,37 @@ +// בס"ד +import { build, context } from "esbuild"; +import { spawn } from "child_process"; + +const isDev = process.env.NODE_ENV === "DEV"; + +const bundlePath = "dist/bundle.js"; + +const buildSettings = { + entryPoints: ["src/main.ts"], + outfile: "dist/bundle.js", + bundle: true, + plugins: [], + minify: true, + platform: "node", + target: ["ES2022"], + format: "cjs", + external: ["@repo/config-env"], +} satisfies Parameters[0]; + +const buildDev = async () => + context(buildSettings) + .then(async (ctx) => ctx.watch()) + .then(() => { + console.log("Starting nodemon to manage execution of bundle.js"); + spawn( + "nodemon", + [bundlePath, "--watch", bundlePath, "--ext", "js", "--exec", "node"], + { stdio: "inherit", shell: true } + ); + }); + +const buildedProject = isDev ? buildDev() : build(buildSettings); + +buildedProject.catch((error: unknown) => { + console.warn(error); +}); diff --git a/apps/gb-limelight-recorder/backend/docker-compose.yml b/apps/gb-limelight-recorder/backend/docker-compose.yml new file mode 100644 index 0000000..dc82686 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/docker-compose.yml @@ -0,0 +1,11 @@ +services: + backend: + build: + context: . + dockerfile: Dockerfile + + env_file: + - ../../../.public.env + - ../../../.secret.env + ports: + - "${BACKEND_PORT}:4590" # Maps host:${FRONTEND_PORT} to container:4590 diff --git a/apps/gb-limelight-recorder/backend/package.json b/apps/gb-limelight-recorder/backend/package.json new file mode 100644 index 0000000..355ac1f --- /dev/null +++ b/apps/gb-limelight-recorder/backend/package.json @@ -0,0 +1,20 @@ +{ + "name": "gb-limelight-recorder-backend", + "version": "1.0.0", + "description": "Backend for the application", + "main": "index.js", + "scripts": { + "test": "echo Backend Test Succeeded && exit 0", + "build": "tsx build.ts", + "serve": "node dist/bundle.js", + "dev": "tsx build.ts" + }, + "author": "", + "license": "ISC", + "dependencies": { + "process": "^0.11.10" + }, + "devDependencies": { + "@types/node": "^24.8.1" + } +} diff --git a/apps/gb-limelight-recorder/backend/server.ts b/apps/gb-limelight-recorder/backend/server.ts new file mode 100644 index 0000000..f71b214 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/server.ts @@ -0,0 +1,124 @@ +//בס"ד +import express from "express"; +import { RecordingProcess } from "./RecordingProcess.js"; +import cors from "cors"; +import ping from "ping"; +import fs from "fs"; +import path from "path"; + +const app = express(); +const port = 5000; +app.use(cors()); + +let ffmpegProcessLeft: RecordingProcess | null = null; +let ffmpegProcessObject: RecordingProcess | null = null; +let ffmpegProcessRight: RecordingProcess | null = null; +const USB_ROOT = "E:/"; // CHANGE if needed + +function createSessionFolder(): string { + if (!fs.existsSync(USB_ROOT)) { + throw new Error("USB drive not connected"); + } + + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const sessionDir = path.join(USB_ROOT, `recording-${timestamp}`); + + fs.mkdirSync(sessionDir, { recursive: true }); + return sessionDir; +} + + +// --- HELLO --- +app.get("/", (req, res) => { + console.log("GET / route hit"); + res.send("Welcome to the Limelight Recorder API"); +}); + +// --- START THE SERVER --- +app.listen(port, () => { + console.log(`Server listening on http://localhost:${port}`); +}); + +function startRecording() { + if (ffmpegProcessLeft || ffmpegProcessObject || ffmpegProcessRight) { + return; + } + + let sessionDir: string; + + try { + sessionDir = createSessionFolder(); + } catch (err) { + console.error(err); + return; + } + + ffmpegProcessLeft = new RecordingProcess( + "left", + path.join(sessionDir, "left.mp4") + ); + ffmpegProcessLeft.startRecording(); + + ffmpegProcessObject = new RecordingProcess( + "object", + path.join(sessionDir, "object.mp4") + ); + ffmpegProcessObject.startRecording(); + + ffmpegProcessRight = new RecordingProcess( + "right", + path.join(sessionDir, "right.mp4") + ); + ffmpegProcessRight.startRecording(); + + console.log(`Recording started in ${sessionDir}`); +} + +function stopRecording() { + // Stop left camera + if (ffmpegProcessLeft) { + ffmpegProcessLeft.stopRecording(); + ffmpegProcessLeft = null; + console.log("Stopped recording: left"); + } + + // Stop object camera + if (ffmpegProcessObject) { + ffmpegProcessObject.stopRecording(); + ffmpegProcessObject = null; + console.log("Stopped recording: object"); + } + + // Stop right camera + if (ffmpegProcessRight) { + ffmpegProcessRight.stopRecording(); + ffmpegProcessRight = null; + console.log("Stopped recording: right"); + } +} + +const oneSecond = 1000; +async function pingRobot(robotIp: string) { + const result = await ping.promise.probe(robotIp, { timeout: 10 }); + return result; +} +// --- PING CAMERAS --- +setInterval(() => { + async function pingCameras () { + const robotIp = "10.45.90.2"; + const isUp = await pingRobot(robotIp).then((res) => res); + + if (isUp.alive) { + console.log(`Robot at ${robotIp} is online.`); + startRecording(); + } + + if (!isUp.alive) { + console.log(`Robot at ${robotIp} is offline.`); + stopRecording(); + } + } + pingCameras().catch(() => { + console.error("Couldnt ping cameras"); + }) +}, oneSecond); \ No newline at end of file diff --git a/apps/gb-limelight-recorder/backend/src/main.ts b/apps/gb-limelight-recorder/backend/src/main.ts new file mode 100644 index 0000000..9d5aaa3 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/src/main.ts @@ -0,0 +1,14 @@ +// בס"ד +import express from "express"; +import { apiRouter } from "./routes"; + +const app = express(); + +const defaultPort = 4590; +const port = process.env.BACKEND_PORT ?? defaultPort; + +app.use("/api/v1", apiRouter); + +app.listen(port, () => { + console.log(`Production server running at http://localhost:${port}`); +}); diff --git a/apps/gb-limelight-recorder/backend/src/routes/index.ts b/apps/gb-limelight-recorder/backend/src/routes/index.ts new file mode 100644 index 0000000..b62ad50 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/src/routes/index.ts @@ -0,0 +1,9 @@ +// בס"ד +import { Router } from "express"; +import { StatusCodes } from "http-status-codes"; + +export const apiRouter = Router(); + +apiRouter.get("/health", (req, res) => { + res.status(StatusCodes.OK).send({ message: "Healthy!" }); +}); \ No newline at end of file diff --git a/apps/gb-limelight-recorder/backend/turbo.json b/apps/gb-limelight-recorder/backend/turbo.json new file mode 100644 index 0000000..52e8c76 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/turbo.json @@ -0,0 +1,12 @@ +{ + "extends": [ + "//" + ], + "tasks": { + "build": { + "outputs": [ + "dist/**" + ] + } + } +} diff --git a/apps/gb-limelight-recorder/frontend/index.html b/apps/gb-limelight-recorder/frontend/index.html new file mode 100644 index 0000000..de79403 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + GreenBlitz 4590 + + +
+ + + diff --git a/apps/gb-limelight-recorder/frontend/package.json b/apps/gb-limelight-recorder/frontend/package.json new file mode 100644 index 0000000..570f109 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/package.json @@ -0,0 +1,32 @@ +{ + "name": "gb-limelight-recorder-frontend", + "version": "1.0.0", + "description": "Frontend for the application", + "main": "index.js", + "scripts": { + "test": "echo Frontend Test Succeeded && exit 0", + "dev": "vite", + "build": "tsc -b && vite build", + "serve": " tsx start.ts", + "lint": "eslint .", + "preview": "vite preview" + }, + "author": "", + "license": "ISC", + "dependencies": { + "react": "^19.1.1", + "react-dom": "^19.1.1", + "tailwindcss": "^4.1.16", + "@tailwindcss/vite": "^4.1.16" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "babel-plugin-react-compiler": "^19.1.0-rc.3", + "globals": "^16.4.0", + "vite": "^7.1.7" + } +} diff --git a/apps/gb-limelight-recorder/frontend/src/App.css b/apps/gb-limelight-recorder/frontend/src/App.css new file mode 100644 index 0000000..08c2a34 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/src/App.css @@ -0,0 +1,44 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; + align-items: center; + align-content: center; +} + +.logo { + height: 9em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #dc42d4); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/apps/gb-limelight-recorder/frontend/src/App.tsx b/apps/gb-limelight-recorder/frontend/src/App.tsx new file mode 100644 index 0000000..4f9548b --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/src/App.tsx @@ -0,0 +1,53 @@ +// בס"ד +import { useEffect, useState } from "react"; +import "./App.css"; +import LimelightTable from "./LimelightTable"; + +const App = () => { + const [message, setMessage] = useState("Loading..."); + const [isRobotOnline, setIsRobotOnline] = useState(false); + const twoSeconds = 2000; + + useEffect(() => { + void (async () => { + try { + const res = await fetch("http://localhost:5000/"); + const text = await res.text(); + setMessage(text); + } catch (e) { + setMessage("Error connecting to server"); + console.error(e); + } + })(); + }, []); + + + useEffect(() => { + const interval = setInterval(async () => { + try { + const res = await fetch("http://localhost:4590/"); + const text = await res.text(); + setMessage(text); + setIsRobotOnline(text.includes("Welcome")); + } catch { + setIsRobotOnline(false); + } + }, twoSeconds); + return () => {clearInterval(interval)}; + }, []); + + return ( + <> +
+ Logo +

{message}

+
+ + + ); +} + +export default App; diff --git a/apps/gb-limelight-recorder/frontend/src/Dockerfile b/apps/gb-limelight-recorder/frontend/src/Dockerfile new file mode 100644 index 0000000..d625b35 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/src/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20-alpine +WORKDIR /app +COPY ./start.ts /app/start.ts +COPY ./dist /app/dist +EXPOSE 443 +RUN ["npm","install","express"] +CMD ["npm","exec","--","tsx","start.ts"] \ No newline at end of file diff --git a/apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx b/apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx new file mode 100644 index 0000000..318a492 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx @@ -0,0 +1,95 @@ +//בס"ד +import { useEffect, useState, useRef } from "react"; +import type React from "react"; +import type { HTMLAttributes } from "react"; + +interface LimelightTableProps { + robotOnline: boolean; + cameras: boolean[]; +} + +declare module "react" { + interface InputHTMLAttributes extends HTMLAttributes { + webkitdirectory?: boolean; + } +} + +const zero = 0; +const one = 1; + +async function doThingy( + robotOnline: boolean, + cameraStatus: boolean, + index: number +) { + const camera = index === zero ? "left" : index === one ? "object" : "right"; + if (robotOnline && cameraStatus) { + await fetch(`http://localhost:5000/record/start/${camera}`, { + method: "POST", + }); + } else if (!robotOnline) { + await fetch(`http://localhost:5000/record/stop/${camera}`, { + method: "POST", + }); + } +} + +const LimelightTable: React.FC = ({ robotOnline }) => { + const cameraStatuses = [false, false, false]; + const [fileLocation, setFileLocation] = useState(""); + const locationPickerRef = useRef(null); + + useEffect(() => { + cameraStatuses.forEach((cameraStatus, index) => { + doThingy(robotOnline, cameraStatus, index).catch(() => { + console.error("Couldnt do the thingy"); + }); + }); + }, [robotOnline]); + + return ( + <> + + + + + + + + + + + + + + + + + + + +
LeftObjectRight
limelight-left.local:5800limelight-object.local:5800limelight.local:5800
+ + + + + +
+ +
+ +
+

Location: {fileLocation}

+
+ + ); +}; + +export default LimelightTable; diff --git a/apps/gb-limelight-recorder/frontend/src/assets/greenblitz.png b/apps/gb-limelight-recorder/frontend/src/assets/greenblitz.png new file mode 100644 index 0000000..b10ccc9 Binary files /dev/null and b/apps/gb-limelight-recorder/frontend/src/assets/greenblitz.png differ diff --git a/apps/gb-limelight-recorder/frontend/src/docker-compose.yml b/apps/gb-limelight-recorder/frontend/src/docker-compose.yml new file mode 100644 index 0000000..a27c44f --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/src/docker-compose.yml @@ -0,0 +1,11 @@ +services: + frontend: + build: + context: . + dockerfile: Dockerfile + + env_file: + - ../../../.public.env + - ../../../.secret.env + ports: + - "${FRONTEND_PORT}:443" # Maps host:${FRONTEND_PORT} to container:443 diff --git a/apps/gb-limelight-recorder/frontend/src/index.css b/apps/gb-limelight-recorder/frontend/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/apps/gb-limelight-recorder/frontend/src/main.tsx b/apps/gb-limelight-recorder/frontend/src/main.tsx new file mode 100644 index 0000000..e37945f --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/src/main.tsx @@ -0,0 +1,11 @@ +// בס"ד +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +createRoot(document.getElementById("root")!).render( + + + +); diff --git a/apps/gb-limelight-recorder/frontend/start.ts b/apps/gb-limelight-recorder/frontend/start.ts new file mode 100644 index 0000000..a99498b --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/start.ts @@ -0,0 +1,24 @@ +// בס"ד +import express from "express"; +import path from "path"; + +const app = express(); + +const defaultPort = 80; +const securePort = 443; +const port = parseInt(process.env.FRONTEND_PORT ?? defaultPort.toString()); +const protocol = port === securePort ? "https" : "http"; + +const dirname = path.dirname(__filename); +const distDirectory = path.join(dirname, "dist"); +const indexHTML = path.join(distDirectory, "index.html"); + +app.use(express.static(distDirectory)); + +app.get(/^(.*)$/, (req, res) => { + res.sendFile(indexHTML); +}); + +app.listen(port, () => { + console.log(`Production server running at ${protocol}://localhost:${port}`); +}); diff --git a/apps/gb-limelight-recorder/frontend/tsconfig.app.json b/apps/gb-limelight-recorder/frontend/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/apps/gb-limelight-recorder/frontend/tsconfig.json b/apps/gb-limelight-recorder/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/gb-limelight-recorder/frontend/tsconfig.node.json b/apps/gb-limelight-recorder/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/gb-limelight-recorder/frontend/turbo.json b/apps/gb-limelight-recorder/frontend/turbo.json new file mode 100644 index 0000000..52e8c76 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/turbo.json @@ -0,0 +1,12 @@ +{ + "extends": [ + "//" + ], + "tasks": { + "build": { + "outputs": [ + "dist/**" + ] + } + } +} diff --git a/apps/gb-limelight-recorder/frontend/vite.config.ts b/apps/gb-limelight-recorder/frontend/vite.config.ts new file mode 100644 index 0000000..37eb678 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/vite.config.ts @@ -0,0 +1,18 @@ +// בס"ד +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + + +// https://vite.dev/config/ +export default defineConfig({ + server: { port: 80 }, + plugins: [ + react({ + babel: { + plugins: [["babel-plugin-react-compiler"]], + }, + }), + tailwindcss(), + ], +}); diff --git a/package-lock.json b/package-lock.json index 43dd24c..8ebb25c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,15 @@ ], "dependencies": { "@tailwindcss/vite": "^4.1.17", + "cors": "^2.8.5", "dotenv": "^17.2.3", "dotenv-cli": "^11.0.0", "eslint-plugin-bsd": "^1.0.0", "eslint-plugin-react": "^7.37.5", "express": "^5.1.0", + "ffmpeg-static": "^5.3.0", "http-status-codes": "^2.3.0", + "ping": "^1.0.0", "tailwindcss": "^4.1.17", "tsx": "^4.21.0" }, @@ -95,6 +98,51 @@ "vite": "^7.1.7" } }, + "apps/gb-limelight-recorder/backend": { + "name": "gb-limelight-recorder-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "process": "^0.11.10" + }, + "devDependencies": { + "@types/node": "^24.8.1" + } + }, + "apps/gb-limelight-recorder/frontend": { + "name": "gb-limelight-recorder-frontend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@tailwindcss/vite": "^4.1.16", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "tailwindcss": "^4.1.16" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "babel-plugin-react-compiler": "^19.1.0-rc.3", + "globals": "^16.4.0", + "vite": "^7.1.7" + } + }, + "apps/gb-limelight-recorder/frontend/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "apps/template/backend": { "name": "template-backend", "version": "1.0.0", @@ -441,6 +489,21 @@ "node": ">=6.9.0" } }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "license": "MIT", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@dotenv-run/cli": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/@dotenv-run/cli/-/cli-1.3.6.tgz", @@ -2417,6 +2480,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2672,23 +2747,27 @@ "license": "(MIT OR Apache-2.0)" }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { @@ -2748,6 +2827,12 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2835,6 +2920,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2924,6 +3015,21 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -2970,6 +3076,19 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -3261,6 +3380,15 @@ "node": ">=10.13.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -3944,6 +4072,22 @@ "reusify": "^1.0.4" } }, + "node_modules/ffmpeg-static": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz", + "integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==", + "hasInstallScript": true, + "license": "GPL-3.0-or-later", + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4109,6 +4253,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gb-limelight-recorder-backend": { + "resolved": "apps/gb-limelight-recorder/backend", + "link": true + }, + "node_modules/gb-limelight-recorder-frontend": { + "resolved": "apps/gb-limelight-recorder/frontend", + "link": true + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -4373,22 +4525,54 @@ "node": ">= 0.8" } }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "license": "MIT", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, "node_modules/http-status-codes": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", "license": "MIT" }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -5729,6 +5913,11 @@ "node": ">=6" } }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5791,6 +5980,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/ping": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ping/-/ping-1.0.0.tgz", + "integrity": "sha512-3dxdgGtV+7P/EVo42JhkGSomeO/0GGicSz3mI/yK+AI+VGNAOfakw5XfcbGI4IjyBY+ZZwvuRXdhnNF2uliKew==", + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5835,6 +6033,15 @@ "node": ">= 0.6.0" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5936,22 +6143,6 @@ "node": ">= 0.10" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -5989,6 +6180,20 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6513,6 +6718,15 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-ts": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/string-ts/-/string-ts-2.2.1.tgz", @@ -7039,6 +7253,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -7152,6 +7372,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 35155bd..b4e11a7 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,15 @@ ], "dependencies": { "@tailwindcss/vite": "^4.1.17", + "cors": "^2.8.5", "dotenv": "^17.2.3", "dotenv-cli": "^11.0.0", "eslint-plugin-bsd": "^1.0.0", "eslint-plugin-react": "^7.37.5", "express": "^5.1.0", + "ffmpeg-static": "^5.3.0", "http-status-codes": "^2.3.0", + "ping": "^1.0.0", "tailwindcss": "^4.1.17", "tsx": "^4.21.0" }