From 2420fdc4ce17fea58630cf2d6002f1595cffd9da Mon Sep 17 00:00:00 2001 From: ZhaiSoul <842607283@qq.com> Date: Tue, 21 Apr 2026 20:59:16 +0800 Subject: [PATCH 01/10] docs: add docker compose generator --- .gitignore | 5 + docs/docs/frigate/installation.md | 9 + docs/package-lock.json | 9 +- docs/package.json | 7 +- docs/scripts/build-config.mjs | 64 ++++ .../DockerComposeGenerator.tsx | 110 ++++++ .../components/DeviceSelector.tsx | 53 +++ .../components/GeneratedOutput.tsx | 60 ++++ .../components/HardwareOptions.tsx | 62 ++++ .../components/NvidiaGpuConfig.tsx | 63 ++++ .../components/OtherOptions.tsx | 89 +++++ .../components/PortConfig.tsx | 125 +++++++ .../components/StoragePaths.tsx | 66 ++++ .../DockerComposeGenerator/config/config.yaml | 276 +++++++++++++++ .../DockerComposeGenerator/config/index.ts | 12 + .../DockerComposeGenerator/config/types.ts | 125 +++++++ .../DockerComposeGenerator/generator/index.ts | 246 +++++++++++++ .../hooks/useConfigGenerator.ts | 207 +++++++++++ .../hooks/useCooldown.ts | 42 +++ .../DockerComposeGenerator/index.ts | 1 + .../DockerComposeGenerator/styles.module.css | 328 ++++++++++++++++++ 21 files changed, 1956 insertions(+), 3 deletions(-) create mode 100644 docs/scripts/build-config.mjs create mode 100644 docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx create mode 100644 docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx create mode 100644 docs/src/components/DockerComposeGenerator/components/GeneratedOutput.tsx create mode 100644 docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx create mode 100644 docs/src/components/DockerComposeGenerator/components/NvidiaGpuConfig.tsx create mode 100644 docs/src/components/DockerComposeGenerator/components/OtherOptions.tsx create mode 100644 docs/src/components/DockerComposeGenerator/components/PortConfig.tsx create mode 100644 docs/src/components/DockerComposeGenerator/components/StoragePaths.tsx create mode 100644 docs/src/components/DockerComposeGenerator/config/config.yaml create mode 100644 docs/src/components/DockerComposeGenerator/config/index.ts create mode 100644 docs/src/components/DockerComposeGenerator/config/types.ts create mode 100644 docs/src/components/DockerComposeGenerator/generator/index.ts create mode 100644 docs/src/components/DockerComposeGenerator/hooks/useConfigGenerator.ts create mode 100644 docs/src/components/DockerComposeGenerator/hooks/useCooldown.ts create mode 100644 docs/src/components/DockerComposeGenerator/index.ts create mode 100644 docs/src/components/DockerComposeGenerator/styles.module.css diff --git a/.gitignore b/.gitignore index c9db2929f..7c97a23a0 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,8 @@ core !/web/**/*.ts .idea/* .ipynb_checkpoints + +# Auto-generated Docker Compose Generator config files +docs/src/components/DockerComposeGenerator/config/devices.ts +docs/src/components/DockerComposeGenerator/config/hardware.ts +docs/src/components/DockerComposeGenerator/config/ports.ts diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index 5d228a609..1fc604c31 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -4,6 +4,8 @@ title: Installation --- import ShmCalculator from '@site/src/components/ShmCalculator' +import DockerComposeGenerator from '@site/src/components/DockerComposeGenerator' + Frigate is a Docker container that can be run on any Docker host including as a [Home Assistant App](https://www.home-assistant.io/apps/). Note that the Home Assistant App is **not** the same thing as the integration. The [integration](/integrations/home-assistant) is required to integrate Frigate into Home Assistant, whether you are running Frigate as a standalone Docker container or as a Home Assistant App. @@ -71,6 +73,13 @@ Users of the Snapcraft build of Docker cannot use storage locations outside your ::: +### Docker Compose Generator + +Generate a Frigate Docker Compose configuration based on your hardware and requirements. + + + + ### Calculating required shm-size Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is **64MB**. diff --git a/docs/package-lock.json b/docs/package-lock.json index 626d71dfd..110b0b26d 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -14,9 +14,11 @@ "@docusaurus/theme-mermaid": "^3.7.0", "@inkeep/docusaurus": "^2.0.16", "@mdx-js/react": "^3.1.0", + "@types/js-yaml": "^4.0.9", "clsx": "^2.1.1", "docusaurus-plugin-openapi-docs": "^4.5.1", "docusaurus-theme-openapi-docs": "^4.5.1", + "js-yaml": "^4.1.1", "prism-react-renderer": "^2.4.1", "raw-loader": "^4.0.2", "react": "^18.3.1", @@ -5747,6 +5749,11 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://mirrors.tencent.com/npm/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -12883,7 +12890,7 @@ }, "node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "resolved": "https://mirrors.tencent.com/npm/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { diff --git a/docs/package.json b/docs/package.json index 0ff76c473..e57d7a154 100644 --- a/docs/package.json +++ b/docs/package.json @@ -3,9 +3,10 @@ "version": "0.0.0", "private": true, "scripts": { + "build:config": "node scripts/build-config.mjs", "docusaurus": "docusaurus", - "start": "npm run regen-docs && docusaurus start --host 0.0.0.0", - "build": "npm run regen-docs && docusaurus build", + "start": "npm run build:config && npm run regen-docs && docusaurus start --host 0.0.0.0", + "build": "npm run build:config && npm run regen-docs && docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", @@ -23,9 +24,11 @@ "@docusaurus/theme-mermaid": "^3.7.0", "@inkeep/docusaurus": "^2.0.16", "@mdx-js/react": "^3.1.0", + "@types/js-yaml": "^4.0.9", "clsx": "^2.1.1", "docusaurus-plugin-openapi-docs": "^4.5.1", "docusaurus-theme-openapi-docs": "^4.5.1", + "js-yaml": "^4.1.1", "prism-react-renderer": "^2.4.1", "raw-loader": "^4.0.2", "react": "^18.3.1", diff --git a/docs/scripts/build-config.mjs b/docs/scripts/build-config.mjs new file mode 100644 index 000000000..78926bed5 --- /dev/null +++ b/docs/scripts/build-config.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node + +/** + * Build script: reads config.yaml and generates TypeScript files + * for the Docker Compose Generator. + * + * Usage: node scripts/build-config.mjs + */ + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import yaml from "js-yaml"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const CONFIG_DIR = path.resolve(__dirname, "../src/components/DockerComposeGenerator/config"); +const YAML_PATH = path.join(CONFIG_DIR, "config.yaml"); + +// Read & parse YAML +const raw = fs.readFileSync(YAML_PATH, "utf8"); +const config = yaml.load(raw); + +if (!config.devices || !config.hardware || !config.ports) { + console.error("config.yaml must contain 'devices', 'hardware', and 'ports' sections."); + process.exit(1); +} + +/** + * Generate a .ts file from a section of the YAML config. + */ +function generateTsFile(sectionName, items, typeName, varName, mapVarName, yamlFilename) { + const jsonItems = JSON.stringify(items, null, 2); + // Indent JSON to fit inside the array literal + const indented = jsonItems + .split("\n") + .map((line, i) => (i === 0 ? line : " " + line)) + .join("\n"); + + const content = `/** + * AUTO-GENERATED FILE — do not edit directly. + * Source: ${yamlFilename} + * To update, edit the YAML file and run: npm run build:config + */ + +import type { ${typeName} } from "./types"; + +export const ${varName}: ${typeName}[] = ${indented}; + +/** Lookup map for quick access by ID */ +export const ${mapVarName}: Map = new Map(${varName}.map((item) => [item.id, item])); +`; + + const outPath = path.join(CONFIG_DIR, `${sectionName}.ts`); + fs.writeFileSync(outPath, content, "utf8"); + console.log(` ✓ Generated ${sectionName}.ts (${items.length} items)`); +} + +console.log("Building config from config.yaml..."); + +generateTsFile("devices", config.devices, "DeviceConfig", "devices", "deviceMap", "config.yaml"); +generateTsFile("hardware", config.hardware, "HardwareOption", "hardwareOptions", "hardwareMap", "config.yaml"); +generateTsFile("ports", config.ports, "PortConfig", "ports", "portMap", "config.yaml"); + +console.log("Done!"); diff --git a/docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx b/docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx new file mode 100644 index 000000000..0779f4249 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import Admonition from "@theme/Admonition"; +import DeviceSelector from "./components/DeviceSelector"; +import HardwareOptions from "./components/HardwareOptions"; +import PortConfigSection from "./components/PortConfig"; +import StoragePaths from "./components/StoragePaths"; +import NvidiaGpuConfig from "./components/NvidiaGpuConfig"; +import OtherOptions from "./components/OtherOptions"; +import GeneratedOutput from "./components/GeneratedOutput"; +import { useConfigGenerator } from "./hooks/useConfigGenerator"; +import styles from "./styles.module.css"; + +/** + * Simple markdown-link-to-React renderer for help text. + * Only supports [text](url) syntax — no nested brackets. + */ +function renderHelpText(text: string): React.ReactNode { + const parts = text.split(/(\[[^\]]+\]\([^)]+\))/g); + return parts.map((part, i) => { + const match = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/); + if (match) { + return ( + + {match[1]} + + ); + } + return {part}; + }); +} + +export default function DockerComposeGenerator() { + const { + deviceId, device, hardwareEnabled, + portEnabled, port5000Confirmed, + nvidiaGpuCount, nvidiaGpuDeviceId, + configPath, mediaPath, rtspPassword, timezone, shmSize, + shmSizeError, gpuDeviceIdError, configPathError, mediaPathError, + hasAnyHardware, generatedYaml, + selectDevice, toggleHardware, togglePort, setPort5000Confirmed, + handleShmSizeChange, handleConfigPathChange, handleMediaPathChange, + handleNvidiaGpuCountChange, handleNvidiaGpuDeviceIdChange, + setRtspPassword, setTimezone, isHardwareDisabled, + } = useConfigGenerator(); + + return ( +
+
+ + + {device.helpText && ( + + {renderHelpText(device.helpText)} + + )} + + {device.needsNvidiaConfig && ( + + )} + + + + + + + + + + +
+
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx b/docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx new file mode 100644 index 000000000..81338db20 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { devices } from "../config"; +import type { DeviceConfig } from "../config"; +import styles from "../styles.module.css"; + +interface Props { + selectedId: string; + onSelect: (id: string) => void; +} + +function DeviceCard({ + device, + active, + onClick, +}: { + device: DeviceConfig; + active: boolean; + onClick: () => void; +}) { + return ( +
{ + if (e.key === "Enter" || e.key === " ") onClick(); + }} + > +
{device.icon}
+
{device.name}
+
{device.description}
+
+ ); +} + +export default function DeviceSelector({ selectedId, onSelect }: Props) { + return ( +
+

Device Type

+
+ {devices.map((d) => ( + onSelect(d.id)} + /> + ))} +
+
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/GeneratedOutput.tsx b/docs/src/components/DockerComposeGenerator/components/GeneratedOutput.tsx new file mode 100644 index 000000000..f170637aa --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/GeneratedOutput.tsx @@ -0,0 +1,60 @@ +import React, { useState, useCallback } from "react"; +import CodeBlock from "@theme/CodeBlock"; +import Admonition from "@theme/Admonition"; +import styles from "../styles.module.css"; + +interface Props { + yaml: string; + configPath: string; + mediaPath: string; + hasAnyHardware: boolean; + deviceId: string; +} + +export default function GeneratedOutput({ + yaml, + configPath, + mediaPath, + hasAnyHardware, + deviceId, +}: Props) { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(yaml).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }, [yaml]); + + return ( +
+
+

Generated Configuration

+ +
+ + {!configPath && ( + +

You haven't specified a config file directory. You may want to modify the default path.

+
+ )} + {!mediaPath && ( + +

You haven't specified a recording storage directory. You may want to modify the default path.

+
+ )} + {deviceId === "stable" && !hasAnyHardware && ( + +

You haven't selected any hardware acceleration. Please check if you have supported hardware available.

+
+ )} + + + {yaml} + +
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx b/docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx new file mode 100644 index 000000000..0162269b9 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { hardwareOptions } from "../config"; +import type { HardwareOption } from "../config"; +import styles from "../styles.module.css"; + +interface Props { + deviceId: string; + hardwareEnabled: Record; + onToggle: (hwId: string) => void; + isDisabled: (hwId: string) => boolean; +} + +function renderDescription(text: string): React.ReactNode { + const parts = text.split(/(\[[^\]]+\]\([^)]+\))/g); + return parts.map((part, i) => { + const match = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/); + if (match) { + return {match[1]}; + } + return {part}; + }); +} + +function HardwareCheckbox({ + hw, disabled, checked, onToggle, +}: { + hw: HardwareOption; disabled: boolean; checked: boolean; onToggle: () => void; +}) { + return ( +
+ + {checked && hw.description && ( +
{renderDescription(hw.description)}
+ )} +
+ ); +} + +export default function HardwareOptions({ deviceId, hardwareEnabled, onToggle, isDisabled }: Props) { + return ( +
+

Generic Hardware Acceleration

+ {deviceId !== "stable" && ( +

+ Some options have been auto-configured based on your device type. +

+ )} +
+ {hardwareOptions.map((hw) => { + const disabled = isDisabled(hw.id); + const checked = disabled ? false : !!hardwareEnabled[hw.id]; + return ( + onToggle(hw.id)} /> + ); + })} +
+
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/NvidiaGpuConfig.tsx b/docs/src/components/DockerComposeGenerator/components/NvidiaGpuConfig.tsx new file mode 100644 index 000000000..5863ac386 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/NvidiaGpuConfig.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import styles from "../styles.module.css"; + +interface Props { + gpuCount: string; + gpuDeviceId: string; + gpuDeviceIdError: boolean; + onGpuCountChange: (value: string) => void; + onGpuDeviceIdChange: (value: string) => void; +} + +export default function NvidiaGpuConfig({ + gpuCount, + gpuDeviceId, + gpuDeviceIdError, + onGpuCountChange, + onGpuDeviceIdChange, +}: Props) { + return ( +
+
+ + onGpuCountChange(e.target.value)} + /> +

+ Enter a number (e.g. 1, 2, 3) or "all" to use all GPUs +

+
+ {gpuCount !== "all" && ( +
+ + onGpuDeviceIdChange(e.target.value)} + /> + {gpuDeviceIdError ? ( +

+ ⚠️ GPU device IDs are required when GPU count is a number +

+ ) : ( +

+ Single GPU: 0  |  Multiple GPUs: 0,1,2 +

+ )} +
+ )} +
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/OtherOptions.tsx b/docs/src/components/DockerComposeGenerator/components/OtherOptions.tsx new file mode 100644 index 000000000..3a14a2c16 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/OtherOptions.tsx @@ -0,0 +1,89 @@ +import React from "react"; +import CodeInline from "@theme/CodeInline"; +import styles from "../styles.module.css"; + +interface Props { + rtspPassword: string; + timezone: string; + shmSize: string; + shmSizeError: boolean; + onRtspPasswordChange: (value: string) => void; + onTimezoneChange: (value: string) => void; + onShmSizeChange: (value: string) => void; +} + +export default function OtherOptions({ + rtspPassword, + timezone, + shmSize, + shmSizeError, + onRtspPasswordChange, + onTimezoneChange, + onShmSizeChange, +}: Props) { + return ( +
+

Other Options

+
+
+ + onRtspPasswordChange(e.target.value)} + /> +

+ Used as{" "} + {"{FRIGATE_RTSP_PASSWORD}"}{" "} + in the config file to reference camera stream passwords. This is NOT + the Frigate login password. +

+
+
+ + onTimezoneChange(e.target.value)} + /> +
+
+ + onShmSizeChange(e.target.value)} + /> + {shmSizeError ? ( +

+ ⚠️ Invalid format. Use a number followed by a unit (e.g. 512mb, 1gb) +

+ ) : ( +

+ See{" "} + + calculating required SHM size + {" "} + for the correct value. +

+ )} +
+
+
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/PortConfig.tsx b/docs/src/components/DockerComposeGenerator/components/PortConfig.tsx new file mode 100644 index 000000000..983a6b45e --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/PortConfig.tsx @@ -0,0 +1,125 @@ +import React from "react"; +import Admonition from "@theme/Admonition"; +import { ports } from "../config"; +import { useCooldown } from "../hooks/useCooldown"; +import styles from "../styles.module.css"; + +interface Props { + portEnabled: Record; + port5000Confirmed: boolean; + onTogglePort: (portId: string) => void; + onConfirm5000: (confirmed: boolean) => void; +} + +function Port5000Confirmation({ + portEnabled, + confirmed, + onToggle, + onConfirm, +}: { + portEnabled: boolean; + confirmed: boolean; + onToggle: () => void; + onConfirm: (confirmed: boolean) => void; +}) { + const { remaining, start, stop } = useCooldown(10); + + React.useEffect(() => { + if (portEnabled) { + start(); + } else { + stop(); + onConfirm(false); + } + return stop; + }, [portEnabled]); + + return ( +
+ {portEnabled && ( + +

+ Exposing port 5000 allows unauthenticated access to + your Frigate instance. Anyone on your network (or the internet if you + have a public IP) could access it without credentials. +

+

+ This may lead to unauthorized access,{" "} + privacy leaks, or further attacks. Ensure you have + proper firewall rules or VPN in place. +

+ +
+ )} + +
+ ); +} + +export default function PortConfigSection({ + portEnabled, + port5000Confirmed, + onTogglePort, + onConfirm5000, +}: Props) { + return ( +
+

Port Configuration

+ + {/* All ports except 5000 */} +
+ {ports + .filter((p) => p.id !== "5000") + .map((port) => ( +
+ + {port.description && ( +
{port.description}
+ )} +
+ ))} +
+ + {/* Port 5000 with special warning — placed last */} + onTogglePort("5000")} + onConfirm={onConfirm5000} + /> +
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/components/StoragePaths.tsx b/docs/src/components/DockerComposeGenerator/components/StoragePaths.tsx new file mode 100644 index 000000000..89581bf11 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/components/StoragePaths.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import styles from "../styles.module.css"; + +interface Props { + configPath: string; + mediaPath: string; + configPathError: boolean; + mediaPathError: boolean; + onConfigPathChange: (value: string) => void; + onMediaPathChange: (value: string) => void; +} + +export default function StoragePaths({ + configPath, + mediaPath, + configPathError, + mediaPathError, + onConfigPathChange, + onMediaPathChange, +}: Props) { + return ( +
+

Storage Paths

+
+
+ + onConfigPathChange(e.target.value)} + /> + {configPathError && ( +

+ ⚠️ Path contains invalid characters. Only letters, numbers, + underscores, hyphens, slashes, and dots are allowed. +

+ )} +
+
+ + onMediaPathChange(e.target.value)} + /> + {mediaPathError && ( +

+ ⚠️ Path contains invalid characters. Only letters, numbers, + underscores, hyphens, slashes, and dots are allowed. +

+ )} +
+
+
+ ); +} diff --git a/docs/src/components/DockerComposeGenerator/config/config.yaml b/docs/src/components/DockerComposeGenerator/config/config.yaml new file mode 100644 index 000000000..deebafdbf --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/config/config.yaml @@ -0,0 +1,276 @@ +# Unified configuration for Docker Compose Generator +# This file defines all devices, hardware options, and ports for Frigate Docker Compose generation + +devices: + - id: "stable" + name: "Standard x86_64" + description: "Generic PC / server" + icon: "💻" + imageTag: "stable" + autoHardware: [] + + - id: "intel" + name: "Intel Device" + description: "Intel GPU / NPU" + icon: "🖥️" + imageTag: "stable" + autoHardware: + - "gpu" + - "intelNpu" + helpText: "Intel Device automatically configures /dev/dri and /dev/accel device mappings." + helpType: "info" + + - id: "stable-tensorrt" + name: "NVIDIA GPU" + description: "NVIDIA acceleration" + icon: "🟢" + imageTag: "stable-tensorrt" + autoHardware: [] + helpText: "Requires the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker) to be installed. GPU deploy resources are configured automatically." + helpType: "warning" + needsNvidiaConfig: true + + - id: "stable-tensorrt-jp6" + name: "NVIDIA Jetson" + description: "Jetson development board" + icon: "🟢" + imageTag: "stable-tensorrt-jp6" + autoHardware: [] + helpText: "NVIDIA Jetson devices automatically configure runtime: nvidia." + helpType: "info" + runtime: "nvidia" + + - id: "stable-rocm" + name: "AMD GPU" + description: "ROCm acceleration" + icon: "🔴" + imageTag: "stable-rocm" + autoHardware: + - "gpu" + helpText: "AMD GPU automatically configures LIBVA_DRIVER_NAME environment variable and /dev/dri device mapping." + helpType: "info" + env: + LIBVA_DRIVER_NAME: "radeonsi" + + - id: "apple-silicon" + name: "Apple Silicon" + description: "Mac M-series processor" + icon: "🍎" + imageTag: "stable" + imageTagSuffix: "-standard-arm64" + autoHardware: [] + helpText: "Apple Silicon (M-series) requires an [external detector](/configuration/object_detectors#apple-silicon-detector) running on the host." + helpType: "warning" + extraHosts: + - "host.docker.internal:host-gateway" + + - id: "raspberry-pi" + name: "Raspberry Pi" + description: "ARM device" + icon: "🍓" + imageTag: "stable" + imageTagSuffix: "-standard-arm64" + autoHardware: + - "video11" + helpText: "Raspberry Pi automatically configures the Video11 device and uses the arm64 image." + helpType: "info" + + - id: "stable-rk" + name: "Rockchip" + description: "Rockchip SoC board" + icon: "🪨" + imageTag: "stable-rk" + autoHardware: + - "gpu" + helpText: "Rockchip devices automatically configure /dev/dri device mapping." + helpType: "info" + devices: + - host: "/dev/dma_heap" + comment: "Rockchip DMA heap" + - host: "/dev/rga" + comment: "Rockchip RGA" + - host: "/dev/mpp_service" + comment: "Rockchip MPP service" + volumes: + - host: "/sys/" + container: "/sys/" + readOnly: true + comment: "Rockchip system info" + securityOpt: + - "apparmor=unconfined" + - "systempaths=unconfined" + + - id: "stable-synaptics" + name: "Synaptics" + description: "Synaptics NPU" + icon: "🔷" + imageTag: "stable-synaptics" + autoHardware: [] + helpText: "Synaptics devices automatically configure /dev/synap and video devices." + helpType: "info" + devices: + - host: "/dev/synap" + comment: "Synaptics NPU" + - host: "/dev/video0" + comment: "Video device 0" + - host: "/dev/video1" + comment: "Video device 1" + +hardware: + - id: "usbCoral" + label: "USB Coral (TPU)" + description: "Enable this if you have a Google Coral USB TPU. Other Coral versions require different device paths." + disabledWhen: + - "apple-silicon" + - "stable-synaptics" + devices: + - host: "/dev/bus/usb" + container: "/dev/bus/usb" + comment: "USB Coral — modify for other versions" + + - id: "pcieCoral" + label: "PCIe Coral (TPU)" + description: "Enable this if you have a Google Coral PCIe/M.2 TPU. You also need to [install the driver](https://github.com/jnicolson/gasket-builder)." + disabledWhen: + - "apple-silicon" + - "stable-synaptics" + devices: + - host: "/dev/apex_0" + container: "/dev/apex_0" + comment: "PCIe Coral — follow driver instructions at https://github.com/jnicolson/gasket-builder" + + - id: "gpu" + label: "GPU Acceleration (/dev/dri)" + description: "Pass through /dev/dri for GPU hardware acceleration (Intel/AMD)." + disabledWhen: + - "stable-tensorrt-jp6" + - "apple-silicon" + devices: + - host: "/dev/dri" + container: "/dev/dri" + comment: "GPU hardware acceleration" + + - id: "intelNpu" + label: "Intel NPU (/dev/accel)" + description: "Pass through /dev/accel for Intel NPU acceleration." + disabledWhen: + - "stable-tensorrt-jp6" + - "apple-silicon" + - "stable-rocm" + - "stable-rk" + - "stable-synaptics" + devices: + - host: "/dev/accel" + container: "/dev/accel" + comment: "Intel NPU" + + - id: "hailo" + label: "Hailo NPU (/dev/hailo0)" + description: "Pass through /dev/hailo0 for Hailo-8 / Hailo-8L NPU acceleration." + disabledWhen: + - "apple-silicon" + - "stable-synaptics" + devices: + - host: "/dev/hailo0" + comment: "Hailo NPU" + + - id: "memryx" + label: "MemryX MX3 (/dev/memx0)" + description: "Pass through /dev/memx0 for MemryX MX3 NPU acceleration." + disabledWhen: + - "apple-silicon" + - "stable-synaptics" + devices: + - host: "/dev/memx0" + comment: "MemryX MX3 NPU" + volumes: + - host: "/run/mxa_manager" + container: "/run/mxa_manager" + comment: "MemryX manager" + + - id: "axera" + label: "AXERA Accelerator" + description: "Pass through AXERA accelerator devices. Requires the [AXCL driver](#axera) to be installed first." + disabledWhen: + - "apple-silicon" + - "stable-synaptics" + devices: + - host: "/dev/axcl_host" + comment: "AXERA accelerator device" + - host: "/dev/ax_mmb_dev" + comment: "AXERA MMB device" + - host: "/dev/msg_userdev" + comment: "AXERA message device" + volumes: + - host: "/usr/bin/axcl" + container: "/usr/bin/axcl" + comment: "AXERA binaries" + - host: "/usr/lib/axcl" + container: "/usr/lib/axcl" + comment: "AXERA libraries" + + - id: "video11" + label: "Raspberry Pi Video11" + description: "Pass through /dev/video11 for Raspberry Pi 4B hardware acceleration." + disabledWhen: + - "stable-tensorrt" + - "stable-tensorrt-jp6" + - "stable-rocm" + - "stable-rk" + - "stable-synaptics" + - "intel" + - "apple-silicon" + - "stable" + devices: + - host: "/dev/video11" + container: "/dev/video11" + comment: "Raspberry Pi 4B" + +ports: + - id: "8971" + host: 8971 + container: 8971 + protocol: "tcp" + description: "Authenticated UI and API access (default HTTPS)" + defaultEnabled: true + locked: true + + - id: "5000" + host: 5000 + container: 5000 + protocol: "tcp" + description: "Unauthenticated Web UI port (not recommended)" + defaultEnabled: false + requiresConfirmation: true + warningType: "danger" + warningContent: "Exposing port 5000 allows **unauthenticated access** to your Frigate instance. Anyone on your network (or the internet if you have a public IP) could access it without any credentials. This may lead to **unauthorized access**, **privacy leaks**, or further attacks. Ensure you have proper firewall rules or VPN in place before enabling this." + confirmationLabel: "I understand the risk and confirm enabling port 5000" + cooldownSeconds: 10 + + - id: "8554" + host: 8554 + container: 8554 + protocol: "tcp" + description: "RTSP feeds" + defaultEnabled: true + + - id: "8555-tcp" + host: 8555 + container: 8555 + protocol: "tcp" + description: "WebRTC over TCP" + defaultEnabled: true + + - id: "8555-udp" + host: 8555 + container: 8555 + protocol: "udp" + description: "WebRTC over UDP" + defaultEnabled: true + + - id: "1984" + host: 1984 + container: 1984 + protocol: "tcp" + description: "Go2RTC Web UIport" + defaultEnabled: false \ No newline at end of file diff --git a/docs/src/components/DockerComposeGenerator/config/index.ts b/docs/src/components/DockerComposeGenerator/config/index.ts new file mode 100644 index 000000000..5acaba9f1 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/config/index.ts @@ -0,0 +1,12 @@ +export { devices, deviceMap } from "./devices"; +export { hardwareOptions, hardwareMap } from "./hardware"; +export { ports, portMap } from "./ports"; + +export type { + DeviceConfig, + DeviceMapping, + VolumeMapping, + HardwareOption, + PortConfig, + NvidiaDeployConfig, +} from "./types"; diff --git a/docs/src/components/DockerComposeGenerator/config/types.ts b/docs/src/components/DockerComposeGenerator/config/types.ts new file mode 100644 index 000000000..5be12530a --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/config/types.ts @@ -0,0 +1,125 @@ +/** + * Type definitions for the Docker Compose Generator configuration. + * All device, hardware, and port options are declaratively defined + * so that adding a new device only requires editing config files. + */ + +/** A single device mapping entry (e.g. /dev/dri:/dev/dri) */ +export interface DeviceMapping { + /** Host device path */ + host: string; + /** Container device path (defaults to host if omitted) */ + container?: string; + /** Inline comment for this device line */ + comment?: string; +} + +/** A single volume mapping entry */ +export interface VolumeMapping { + /** Host path */ + host: string; + /** Container path */ + container: string; + /** Whether the mount is read-only */ + readOnly?: boolean; + /** Inline comment */ + comment?: string; +} + +/** NVIDIA deploy configuration for docker-compose */ +export interface NvidiaDeployConfig { + /** "all" or a specific number */ + count: string; + /** Specific GPU device IDs (when count is a number) */ + deviceIds?: string[]; +} + +/** Full device type definition */ +export interface DeviceConfig { + /** Unique identifier, e.g. "intel" */ + id: string; + /** Display name, e.g. "Intel GPU" */ + name: string; + /** Short description */ + description: string; + /** Icon emoji or identifier */ + icon: string; + /** Docker image tag, e.g. "stable" */ + imageTag: string; + /** + * Image tag suffix appended to the base tag. + * e.g. "-standard-arm64" produces "stable-standard-arm64" + */ + imageTagSuffix?: string; + /** Hardware option IDs to auto-enable when this device is selected */ + autoHardware: string[]; + /** Help text shown as an admonition when this device is selected */ + helpText?: string; + /** Admonition type for help text */ + helpType?: "info" | "warning" | "danger"; + /** Device mappings always added for this device type */ + devices?: DeviceMapping[]; + /** Volume mappings always added for this device type */ + volumes?: VolumeMapping[]; + /** Extra environment variables for this device type */ + env?: Record; + /** NVIDIA deploy config (only for tensorrt) */ + nvidiaDeploy?: NvidiaDeployConfig; + /** Runtime setting, e.g. "nvidia" for Jetson */ + runtime?: string; + /** Extra hosts entries, e.g. "host.docker.internal:host-gateway" */ + extraHosts?: string[]; + /** Security options, e.g. ["apparmor=unconfined"] */ + securityOpt?: string[]; + /** Whether this device type needs the NVIDIA GPU config UI */ + needsNvidiaConfig?: boolean; +} + +/** Generic hardware acceleration option definition */ +export interface HardwareOption { + /** Unique identifier, e.g. "usbCoral" */ + id: string; + /** Display label */ + label: string; + /** + * Description shown below the checkbox when this option is enabled. + * Supports markdown link syntax: [text](url) + */ + description?: string; + /** Device IDs that disable this option */ + disabledWhen?: string[]; + /** Device mappings added when this option is enabled */ + devices?: DeviceMapping[]; + /** Volume mappings added when this option is enabled */ + volumes?: VolumeMapping[]; + /** Extra environment variables */ + env?: Record; +} + +/** Port definition */ +export interface PortConfig { + /** Unique identifier (also the default host port as string) */ + id: string; + /** Host port number */ + host: number; + /** Container port number */ + container: number; + /** Protocol */ + protocol?: "tcp" | "udp"; + /** Description of the port's purpose */ + description: string; + /** Whether enabled by default */ + defaultEnabled: boolean; + /** Whether this port is locked (always enabled, cannot be toggled off) */ + locked?: boolean; + /** Whether this port requires a confirmation step before enabling */ + requiresConfirmation?: boolean; + /** Admonition type for the warning */ + warningType?: "warning" | "danger"; + /** Warning content (markdown) */ + warningContent?: string; + /** Confirmation checkbox label */ + confirmationLabel?: string; + /** Cooldown in seconds before the confirmation checkbox becomes available */ + cooldownSeconds?: number; +} diff --git a/docs/src/components/DockerComposeGenerator/generator/index.ts b/docs/src/components/DockerComposeGenerator/generator/index.ts new file mode 100644 index 000000000..24b8ab263 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/generator/index.ts @@ -0,0 +1,246 @@ +import type { + DeviceConfig, + DeviceMapping, + VolumeMapping, +} from "../config/types"; +import { hardwareMap } from "../config"; + +// --------------------------------------------------------------------------- +// Input type +// --------------------------------------------------------------------------- + +export interface GeneratorInput { + device: DeviceConfig; + selectedHardware: string[]; + enabledPorts: string[]; + configPath: string; + mediaPath: string; + rtspPassword: string; + timezone: string; + shmSize: string; + nvidiaGpuCount?: string; + nvidiaGpuDeviceId?: string; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function deviceLine(dm: DeviceMapping): string { + const host = dm.host; + const container = dm.container ?? dm.host; + const mapping = host === container ? host : `${host}:${container}`; + const comment = dm.comment ? ` # ${dm.comment}` : ""; + return ` - ${mapping}${comment}`; +} + +function volumeLine(vm: VolumeMapping): string { + const ro = vm.readOnly ? ":ro" : ""; + const comment = vm.comment ? ` # ${vm.comment}` : ""; + return ` - ${vm.host}:${vm.container}${ro}${comment}`; +} + +// --------------------------------------------------------------------------- +// YAML builder — each section returns an array of lines +// --------------------------------------------------------------------------- + +function buildImage(device: DeviceConfig): string[] { + const tag = device.imageTagSuffix + ? `${device.imageTag}${device.imageTagSuffix}` + : device.imageTag; + return [` image: ghcr.io/blakeblackshear/frigate:${tag}`]; +} + +function buildDevices( + device: DeviceConfig, + hwDevices: DeviceMapping[] +): string[] { + const all: DeviceMapping[] = [ + ...(device.devices ?? []), + ...hwDevices, + ]; + if (all.length === 0) return []; + return [ + " devices:", + ...all.map(deviceLine), + ]; +} + +function buildVolumes( + device: DeviceConfig, + hwVolumes: VolumeMapping[], + configPath: string, + mediaPath: string +): string[] { + const all: VolumeMapping[] = [ + ...(device.volumes ?? []), + ...hwVolumes, + ]; + return [ + " volumes:", + " - /etc/localtime:/etc/localtime:ro # Sync host time", + ` - ${configPath}:/config # Config file directory`, + ` - ${mediaPath}:/media/frigate # Recording storage directory`, + " - type: tmpfs # 1GB in-memory filesystem for recording segment storage", + " target: /tmp/cache", + " tmpfs:", + " size: 1000000000", + ...all.map(volumeLine), + ]; +} + +function buildPorts(enabledPorts: string[]): string[] { + return [ + " ports:", + ...enabledPorts, + ]; +} + +function buildEnvironment( + device: DeviceConfig, + hwEnv: Record, + rtspPassword: string, + timezone: string +): string[] { + const allEnv: Record = { + ...hwEnv, + ...(device.env ?? {}), + }; + + const lines: string[] = [ + " environment:", + ` FRIGATE_RTSP_PASSWORD: "${rtspPassword}" # RTSP password — change to your own`, + ` TZ: "${timezone}" # Timezone`, + ]; + + for (const [key, value] of Object.entries(allEnv)) { + lines.push(` ${key}: "${value}"`); + } + + return lines; +} + +function buildDeploy(device: DeviceConfig, input: GeneratorInput): string[] { + if (device.id === "stable-tensorrt") { + const count = input.nvidiaGpuCount || "all"; + const isAll = count.toLowerCase() === "all"; + const deviceId = input.nvidiaGpuDeviceId?.trim(); + + if (isAll) { + return [ + " deploy:", + " resources:", + " reservations:", + " devices:", + " - driver: nvidia", + " count: all # Use all GPUs", + " capabilities: [gpu]", + ]; + } + + if (deviceId) { + const ids = deviceId + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + .map((s) => `'${s}'`) + .join(", "); + return [ + " deploy:", + " resources:", + " reservations:", + " devices:", + " - driver: nvidia", + ` device_ids: [${ids}] # GPU device IDs`, + ` count: ${count} # GPU count`, + " capabilities: [gpu]", + ]; + } + + return [ + " deploy:", + " resources:", + " reservations:", + " devices:", + " - driver: nvidia", + ` count: ${count} # GPU count`, + " capabilities: [gpu]", + ]; + } + + return []; +} + +function buildRuntime(device: DeviceConfig): string[] { + if (device.runtime) { + return [` runtime: ${device.runtime}`]; + } + return []; +} + +function buildExtraHosts(device: DeviceConfig): string[] { + if (!device.extraHosts?.length) return []; + return [ + " extra_hosts:", + ...device.extraHosts.map( + (h, i) => + ` - "${h}"${i === 0 ? " # Required to talk to the NPU detector" : ""}` + ), + ]; +} + +function buildSecurityOpt(device: DeviceConfig): string[] { + if (!device.securityOpt?.length) return []; + return [ + " security_opt:", + ...device.securityOpt.map((s) => ` - ${s}`), + ]; +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Generate a docker-compose YAML string from the given input. + * The output is pure YAML with inline comments (no Shiki annotations). + */ +export function generateDockerCompose(input: GeneratorInput): string { + const { device } = input; + + // Collect hardware-level devices, volumes, and env + const hwDevices: DeviceMapping[] = []; + const hwVolumes: VolumeMapping[] = []; + const hwEnv: Record = {}; + + for (const hwId of input.selectedHardware) { + const hw = hardwareMap.get(hwId); + if (!hw) continue; + // Skip GPU device mapping for tensorrt images (it uses deploy instead) + if (hw.id === "gpu" && device.imageTag === "stable-tensorrt") continue; + hwDevices.push(...(hw.devices ?? [])); + hwVolumes.push(...(hw.volumes ?? [])); + Object.assign(hwEnv, hw.env ?? {}); + } + + const lines: string[] = [ + "services:", + " frigate:", + " container_name: frigate", + " privileged: true # This may not be necessary for all setups", + " restart: unless-stopped", + " stop_grace_period: 30s # Allow enough time to shut down the various services", + ...buildImage(device), + ` shm_size: "${input.shmSize || "512mb"}" # Update for your cameras based on SHM calculation`, + ...buildRuntime(device), + ...buildDeploy(device, input), + ...buildExtraHosts(device), + ...buildSecurityOpt(device), + ...buildDevices(device, hwDevices), + ...buildVolumes(device, hwVolumes, input.configPath, input.mediaPath), + ...buildPorts(input.enabledPorts), + ...buildEnvironment(device, hwEnv, input.rtspPassword, input.timezone), + ]; + + return lines.join("\n"); +} diff --git a/docs/src/components/DockerComposeGenerator/hooks/useConfigGenerator.ts b/docs/src/components/DockerComposeGenerator/hooks/useConfigGenerator.ts new file mode 100644 index 000000000..994ac631a --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/hooks/useConfigGenerator.ts @@ -0,0 +1,207 @@ +import { useState, useCallback, useMemo } from "react"; +import { deviceMap, hardwareMap, portMap } from "../config"; +import { generateDockerCompose } from "../generator"; +import type { GeneratorInput } from "../generator"; + +/** + * Main hook that holds all form state and generates the Docker Compose output. + * Configuration is loaded synchronously from build-time generated .ts files. + */ +export function useConfigGenerator() { + const [deviceId, setDeviceId] = useState("stable"); + + const [hardwareEnabled, setHardwareEnabled] = useState>(() => { + const defaultDevice = deviceMap.get("stable"); + const initial: Record = {}; + if (defaultDevice) { + for (const hwId of defaultDevice.autoHardware) { + initial[hwId] = true; + } + } + return initial; + }); + + const [portEnabled, setPortEnabled] = useState>(() => { + const initial: Record = {}; + for (const p of portMap.values()) { + initial[p.id] = p.defaultEnabled; + } + return initial; + }); + + const [port5000Confirmed, setPort5000Confirmed] = useState(false); + const [nvidiaGpuCount, setNvidiaGpuCount] = useState("all"); + const [nvidiaGpuDeviceId, setNvidiaGpuDeviceId] = useState(""); + const [configPath, setConfigPath] = useState(""); + const [mediaPath, setMediaPath] = useState(""); + const [rtspPassword, setRtspPassword] = useState("password"); + const [timezone, setTimezone] = useState( + () => Intl.DateTimeFormat().resolvedOptions().timeZone || "Etc/UTC" + ); + const [shmSize, setShmSize] = useState("512mb"); + const [shmSizeError, setShmSizeError] = useState(false); + const [gpuDeviceIdError, setGpuDeviceIdError] = useState(false); + const [configPathError, setConfigPathError] = useState(false); + const [mediaPathError, setMediaPathError] = useState(false); + + const device = useMemo(() => deviceMap.get(deviceId)!, [deviceId]); + + const selectDevice = useCallback((id: string) => { + const newDevice = deviceMap.get(id); + if (!newDevice) return; + setDeviceId(id); + setHardwareEnabled(() => { + const next: Record = {}; + for (const hwId of newDevice.autoHardware) { + next[hwId] = true; + } + return next; + }); + setNvidiaGpuCount("all"); + setNvidiaGpuDeviceId(""); + setGpuDeviceIdError(false); + }, []); + + const toggleHardware = useCallback((hwId: string) => { + setHardwareEnabled((prev) => ({ ...prev, [hwId]: !prev[hwId] })); + }, []); + + const togglePort = useCallback((portId: string) => { + const port = portMap.get(portId); + if (port?.locked) return; + setPortEnabled((prev) => { + const next = { ...prev, [portId]: !prev[portId] }; + if (portId === "5000" && !next[portId]) { + setPort5000Confirmed(false); + } + return next; + }); + }, []); + + const isHardwareDisabled = useCallback( + (hwId: string): boolean => { + const hw = hardwareMap.get(hwId); + if (!hw) return false; + return hw.disabledWhen?.includes(deviceId) ?? false; + }, + [deviceId] + ); + + const validateShmSize = useCallback((value: string): boolean => { + if (!value) return true; + return /^\d+(\.\d+)?[bkmgBKMG]{1,2}$/.test(value); + }, []); + + const validatePath = useCallback((value: string): boolean => { + if (!value) return true; + return /^[a-zA-Z0-9_\-/./]+$/.test(value); + }, []); + + const handleShmSizeChange = useCallback( + (value: string) => { + const filtered = value.replace(/[^0-9.bkmgBKMG]/g, ""); + const valid = validateShmSize(filtered); + setShmSize(filtered); + setShmSizeError(!valid && filtered !== ""); + }, + [validateShmSize] + ); + + const handleConfigPathChange = useCallback( + (value: string) => { + const filtered = value.replace(/[^a-zA-Z0-9_\-/./]/g, ""); + const valid = validatePath(filtered); + setConfigPath(filtered); + setConfigPathError(!valid && filtered !== ""); + }, + [validatePath] + ); + + const handleMediaPathChange = useCallback( + (value: string) => { + const filtered = value.replace(/[^a-zA-Z0-9_\-/./]/g, ""); + const valid = validatePath(filtered); + setMediaPath(filtered); + setMediaPathError(!valid && filtered !== ""); + }, + [validatePath] + ); + + const handleNvidiaGpuCountChange = useCallback((value: string) => { + const lower = value.trim().toLowerCase(); + if (lower === "all" || lower === "" || /^[0-9]+$/.test(lower)) { + setNvidiaGpuCount(lower || "all"); + if (lower === "all") { + setNvidiaGpuDeviceId(""); + setGpuDeviceIdError(false); + } else if (/^[0-9]+$/.test(lower)) { + setGpuDeviceIdError(false); + } + } + }, []); + + const handleNvidiaGpuDeviceIdChange = useCallback((value: string) => { + setNvidiaGpuDeviceId(value.trim()); + setGpuDeviceIdError(false); + }, []); + + const enabledPortLines = useMemo(() => { + const lines: string[] = []; + for (const [id, enabled] of Object.entries(portEnabled)) { + if (!enabled) continue; + if (id === "5000" && !port5000Confirmed) continue; + const p = portMap.get(id); + if (!p) continue; + const proto = p.protocol && p.protocol !== "tcp" ? `/${p.protocol}` : ""; + const comment = p.description ? ` # ${p.description}` : ""; + lines.push(` - "${p.host}:${p.container}${proto}"${comment}`); + } + return lines; + }, [portEnabled, port5000Confirmed]); + + const selectedHardwareIds = useMemo(() => { + return Object.entries(hardwareEnabled) + .filter(([id, enabled]) => { + if (!enabled) return false; + const hw = hardwareMap.get(id); + if (!hw) return false; + if (hw.disabledWhen?.includes(deviceId)) return false; + return true; + }) + .map(([id]) => id); + }, [hardwareEnabled, deviceId]); + + const generatedYaml = useMemo(() => { + const input: GeneratorInput = { + device, + selectedHardware: selectedHardwareIds, + enabledPorts: enabledPortLines, + configPath: configPath || "/path/to/your/config", + mediaPath: mediaPath || "/path/to/your/storage", + rtspPassword: rtspPassword || "password", + timezone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "Etc/UTC", + shmSize: shmSize || "512mb", + nvidiaGpuCount, + nvidiaGpuDeviceId, + }; + return generateDockerCompose(input); + }, [ + device, selectedHardwareIds, enabledPortLines, + configPath, mediaPath, rtspPassword, timezone, shmSize, + nvidiaGpuCount, nvidiaGpuDeviceId, + ]); + + const hasAnyHardware = selectedHardwareIds.length > 0 || !!device?.devices?.length; + + return { + deviceId, device, hardwareEnabled, portEnabled, + port5000Confirmed, nvidiaGpuCount, nvidiaGpuDeviceId, + configPath, mediaPath, rtspPassword, timezone, shmSize, + shmSizeError, gpuDeviceIdError, configPathError, mediaPathError, + hasAnyHardware, generatedYaml, + selectDevice, toggleHardware, togglePort, setPort5000Confirmed, + handleShmSizeChange, handleConfigPathChange, handleMediaPathChange, + handleNvidiaGpuCountChange, handleNvidiaGpuDeviceIdChange, + setRtspPassword, setTimezone, isHardwareDisabled, + }; +} diff --git a/docs/src/components/DockerComposeGenerator/hooks/useCooldown.ts b/docs/src/components/DockerComposeGenerator/hooks/useCooldown.ts new file mode 100644 index 000000000..253edca87 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/hooks/useCooldown.ts @@ -0,0 +1,42 @@ +import { useState, useEffect, useCallback, useRef } from "react"; + +/** + * Hook for a countdown timer (e.g. cooldown before confirming port 5000). + */ +export function useCooldown(initialSeconds: number) { + const [remaining, setRemaining] = useState(0); + const timerRef = useRef | null>(null); + + const start = useCallback(() => { + // Clear any existing timer + if (timerRef.current) clearInterval(timerRef.current); + setRemaining(initialSeconds); + timerRef.current = setInterval(() => { + setRemaining((prev) => { + if (prev <= 1) { + if (timerRef.current) clearInterval(timerRef.current); + timerRef.current = null; + return 0; + } + return prev - 1; + }); + }, 1000); + }, [initialSeconds]); + + const stop = useCallback(() => { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + setRemaining(0); + }, []); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (timerRef.current) clearInterval(timerRef.current); + }; + }, []); + + return { remaining, start, stop }; +} diff --git a/docs/src/components/DockerComposeGenerator/index.ts b/docs/src/components/DockerComposeGenerator/index.ts new file mode 100644 index 000000000..76dd58756 --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/index.ts @@ -0,0 +1 @@ +export { default } from "./DockerComposeGenerator"; diff --git a/docs/src/components/DockerComposeGenerator/styles.module.css b/docs/src/components/DockerComposeGenerator/styles.module.css new file mode 100644 index 000000000..0b7429ece --- /dev/null +++ b/docs/src/components/DockerComposeGenerator/styles.module.css @@ -0,0 +1,328 @@ +/* =================================================================== + Docker Compose Generator — styles + Uses Docusaurus / Infima CSS variables for theme compatibility. + =================================================================== */ + +.generator { + margin: 2rem 0; +} + +.card { + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-400); + border-radius: 12px; + padding: 2rem; + box-shadow: var(--ifm-global-shadow-lw); +} + +[data-theme="light"] .card { + background: var(--ifm-color-emphasis-100); + border: 1px solid var(--ifm-color-emphasis-300); +} + +/* --- Form sections --- */ + +.formSection { + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--ifm-color-emphasis-400); +} + +.formSection:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.formSection h4 { + margin: 0 0 1rem 0; + color: var(--ifm-font-color-base); + font-size: 1.1rem; + font-weight: var(--ifm-font-weight-semibold); +} + +/* --- Form controls --- */ + +.formGroup { + margin-bottom: 1rem; +} + +.formGroup:last-child { + margin-bottom: 0; +} + +.label { + display: block; + margin-bottom: 0.25rem; + color: var(--ifm-font-color-base); + font-weight: var(--ifm-font-weight-semibold); + font-size: 0.9rem; +} + +.input { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid var(--ifm-color-emphasis-400); + border-radius: 6px; + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + font-size: 0.95rem; + transition: border-color 0.2s, box-shadow 0.2s; +} + +[data-theme="light"] .input { + background: #fff; + border: 1px solid #d0d7de; +} + +.input:focus { + outline: none; + border-color: var(--ifm-color-primary); + box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest); +} + +[data-theme="dark"] .input { + border-color: var(--ifm-color-emphasis-300); +} + +.inputError { + border-color: #e74c3c; + animation: shake 0.3s ease-in-out; +} + +@keyframes shake { + 0%, + 100% { + transform: translateX(0); + } + 25% { + transform: translateX(-5px); + } + 75% { + transform: translateX(5px); + } +} + +.helpText { + margin: 0.5rem 0 0 0; + font-size: 0.85rem; + color: var(--ifm-font-color-secondary); + line-height: 1.5; +} + +.helpText a { + color: var(--ifm-color-primary); +} + +/* --- Device grid --- */ + +.deviceGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); + gap: 0.75rem; + margin-top: 0.5rem; +} + +.deviceCard { + padding: 0.75rem; + border: 2px solid var(--ifm-color-emphasis-400); + border-radius: 12px; + cursor: pointer; + transition: all 0.2s; + text-align: center; + background: var(--ifm-background-color); + display: flex; + flex-direction: column; + align-items: center; +} + +[data-theme="light"] .deviceCard { + border: 2px solid #d0d7de; + background: #fff; +} + +.deviceCard:hover { + border-color: var(--ifm-color-primary); + background: var(--ifm-color-emphasis-100); + transform: translateY(-2px); +} + +.deviceCardActive { + border-color: var(--ifm-color-primary); + background: var(--ifm-color-primary-lightest); + box-shadow: 0 0 0 1px var(--ifm-color-primary); +} + +[data-theme="light"] .deviceCardActive { + background: color-mix(in srgb, var(--ifm-color-primary) 12%, #fff); +} + +[data-theme="dark"] .deviceCardActive { + background: color-mix(in srgb, var(--ifm-color-primary) 25%, #1b1b1b); +} + +[data-theme="dark"] .deviceCardActive .deviceName { + color: var(--ifm-color-primary-light); +} + +[data-theme="dark"] .deviceCardActive .deviceDesc { + color: var(--ifm-color-primary-light); + opacity: 0.85; +} + +.deviceIcon { + font-size: 2rem; + margin-bottom: 0.25rem; + height: 40px; + width: 50px; + display: flex; + align-items: center; + justify-content: center; +} + +.deviceName { + font-weight: var(--ifm-font-weight-semibold); + color: var(--ifm-font-color-base); + margin-bottom: 0.15rem; + font-size: 0.9rem; +} + +.deviceDesc { + font-size: 0.75rem; + color: var(--ifm-font-color-secondary); + line-height: 1.3; +} + +/* --- Checkbox grid --- */ + +.checkboxGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; +} + +@media (max-width: 576px) { + .checkboxGrid { + grid-template-columns: 1fr; + } +} + +.hardwareItem { + margin-bottom: 0; +} + +.hardwareDescription { + margin: 0.15rem 0 0.4rem 1.6rem; + font-size: 0.8rem; + color: var(--ifm-font-color-secondary); + line-height: 1.5; +} + +.hardwareDescription a { + color: var(--ifm-color-primary); + text-decoration: underline; + text-underline-offset: 2px; +} + +.checkboxLabel { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + padding: 0.4rem 0.5rem; + border-radius: 6px; + transition: background-color 0.2s; + font-size: 0.9rem; +} + +.checkboxLabel:hover { + background: var(--ifm-color-emphasis-100); +} + +.checkboxLabel input[type="checkbox"] { + width: 1.1rem; + height: 1.1rem; + cursor: pointer; + flex-shrink: 0; +} + +.checkboxLabel span { + color: var(--ifm-font-color-base); +} + +.checkboxDisabled { + cursor: not-allowed; +} + +.checkboxDisabled:hover { + background: transparent; +} + +.checkboxDisabled input[type="checkbox"] { + cursor: not-allowed; + opacity: 0.5; +} + +/* --- Form grid (side-by-side) --- */ + +.formGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +@media (max-width: 576px) { + .formGrid { + grid-template-columns: 1fr; + } +} + +.formGrid .formGroup { + margin-bottom: 0; +} + +/* --- Port section --- */ + +.portSection { + margin-bottom: 0.75rem; +} + +.warningBadge { + margin-left: auto; + color: #e67e22; + font-size: 0.85rem; +} + +/* --- NVIDIA config --- */ + +.nvidiaConfig { + margin-top: 1rem; + margin-bottom: 1.5rem; + padding: 1rem; + background: var(--ifm-background-color); + border-radius: 8px; + border-left: 3px solid var(--ifm-color-primary); +} + +[data-theme="light"] .nvidiaConfig { + background: #f6f8fa; + border-left: 3px solid var(--ifm-color-primary); +} + +/* --- Result section --- */ + +.resultSection { + margin-top: 2rem; +} + +.resultHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.resultHeader h4 { + margin: 0; + color: var(--ifm-font-color-base); +} From d007bd0a6f1f4e2fefec521f1c8f83d424b930b4 Mon Sep 17 00:00:00 2001 From: ZhaiSoul <842607283@qq.com> Date: Tue, 21 Apr 2026 22:23:09 +0800 Subject: [PATCH 02/10] docs: add more icon support --- .../components/DeviceSelector.tsx | 96 ++++++++++++++++++- .../DockerComposeGenerator/config/config.yaml | 46 +++++++-- .../DockerComposeGenerator/config/types.ts | 35 ++++++- .../DockerComposeGenerator/styles.module.css | 34 +++++++ 4 files changed, 202 insertions(+), 9 deletions(-) diff --git a/docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx b/docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx index 81338db20..ddad16050 100644 --- a/docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx +++ b/docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useColorMode } from "@docusaurus/theme-common"; import { devices } from "../config"; import type { DeviceConfig } from "../config"; import styles from "../styles.module.css"; @@ -8,6 +9,99 @@ interface Props { onSelect: (id: string) => void; } +/** + * Determine the icon type from the icon string: + * - Starts with " tag. + */ +function hasBackgroundProps(style: React.CSSProperties | undefined): boolean { + if (!style) return false; + return Object.keys(style).some((key) => { + const k = key.toLowerCase().replace(/-/g, ""); + return k === "backgroundsize" || k === "backgroundposition" || k === "backgroundrepeat" || k === "backgroundimage"; + }); +} + +/** + * Convert a style object to CSS custom properties (e.g. { width: "24px" } → { "--svg-width": "24px" }) + * so they can be consumed by CSS rules targeting child elements like . + */ +function toCssVars(style: React.CSSProperties | undefined, prefix: string): React.CSSProperties { + if (!style) return {}; + const vars: Record = {}; + for (const [key, value] of Object.entries(style)) { + const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase(); + vars[`--${prefix}-${cssKey}`] = value; + } + return vars as React.CSSProperties; +} + +function DeviceIcon({ device }: { device: DeviceConfig }) { + const { isDarkTheme } = useColorMode(); + const iconStr = isDarkTheme && device.iconDark ? device.iconDark : device.icon; + const iconStyle = (isDarkTheme && device.iconDarkStyle + ? device.iconDarkStyle + : device.iconStyle) as React.CSSProperties | undefined; + const svgStyle = (isDarkTheme && device.svgDarkStyle + ? device.svgDarkStyle + : device.svgStyle) as React.CSSProperties | undefined; + + const iconType = getIconType(iconStr); + + if (iconType === "svg") { + return ( +
+ ); + } + + if (iconType === "image") { + // When iconStyle contains background-* properties, render as background-image + // on the container div instead of an tag, enabling background-size/position control. + if (hasBackgroundProps(iconStyle)) { + return ( +
+ ); + } + return ( +
+ {device.name} +
+ ); + } + + return ( +
+ {iconStr} +
+ ); +} + function DeviceCard({ device, active, @@ -27,7 +121,7 @@ function DeviceCard({ if (e.key === "Enter" || e.key === " ") onClick(); }} > -
{device.icon}
+
{device.name}
{device.description}
diff --git a/docs/src/components/DockerComposeGenerator/config/config.yaml b/docs/src/components/DockerComposeGenerator/config/config.yaml index deebafdbf..69058fc57 100644 --- a/docs/src/components/DockerComposeGenerator/config/config.yaml +++ b/docs/src/components/DockerComposeGenerator/config/config.yaml @@ -12,7 +12,7 @@ devices: - id: "intel" name: "Intel Device" description: "Intel GPU / NPU" - icon: "🖥️" + icon: '' imageTag: "stable" autoHardware: - "gpu" @@ -23,7 +23,12 @@ devices: - id: "stable-tensorrt" name: "NVIDIA GPU" description: "NVIDIA acceleration" - icon: "🟢" + icon: '' + svgStyle: + width: 50px + height: 50px + iconStyle: + padding-bottom: 15px imageTag: "stable-tensorrt" autoHardware: [] helpText: "Requires the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker) to be installed. GPU deploy resources are configured automatically." @@ -33,7 +38,12 @@ devices: - id: "stable-tensorrt-jp6" name: "NVIDIA Jetson" description: "Jetson development board" - icon: "🟢" + icon: '' + svgStyle: + width: 50px + height: 50px + iconStyle: + padding-bottom: 15px imageTag: "stable-tensorrt-jp6" autoHardware: [] helpText: "NVIDIA Jetson devices automatically configure runtime: nvidia." @@ -43,7 +53,18 @@ devices: - id: "stable-rocm" name: "AMD GPU" description: "ROCm acceleration" - icon: "🔴" + icon: "https://www.amd.com/content/dam/code/images/header/amd-header-logo.svg" + iconStyle: + filter: invert(1) + background-repeat: no-repeat + background-position: right center + background-size: 338% 90% + iconDark: "https://www.amd.com/content/dam/code/images/header/amd-header-logo.svg" + iconDarkStyle: + filter: invert(0) + background-repeat: no-repeat + background-position: right center + background-size: 338% 90% imageTag: "stable-rocm" autoHardware: - "gpu" @@ -55,7 +76,14 @@ devices: - id: "apple-silicon" name: "Apple Silicon" description: "Mac M-series processor" - icon: "🍎" + icon: '' + svgStyle: + width: 90px + height: 90px + svgDarkStyle: + width: 90px + height: 90px + fill: white imageTag: "stable" imageTagSuffix: "-standard-arm64" autoHardware: [] @@ -67,7 +95,11 @@ devices: - id: "raspberry-pi" name: "Raspberry Pi" description: "ARM device" - icon: "🍓" + icon: '' + svgStyle: + width: 40px + height: 40px + transform: translateX(-3px) imageTag: "stable" imageTagSuffix: "-standard-arm64" autoHardware: @@ -78,7 +110,7 @@ devices: - id: "stable-rk" name: "Rockchip" description: "Rockchip SoC board" - icon: "🪨" + icon: "https://www.rock-chips.com/favicon.ico" imageTag: "stable-rk" autoHardware: - "gpu" diff --git a/docs/src/components/DockerComposeGenerator/config/types.ts b/docs/src/components/DockerComposeGenerator/config/types.ts index 5be12530a..6d9b048f0 100644 --- a/docs/src/components/DockerComposeGenerator/config/types.ts +++ b/docs/src/components/DockerComposeGenerator/config/types.ts @@ -42,8 +42,41 @@ export interface DeviceConfig { name: string; /** Short description */ description: string; - /** Icon emoji or identifier */ + /** + * Icon for the device card. Supports: + * - Emoji string (e.g. "🖥️") + * - Image URL or static path (e.g. "/img/intel.svg", "https://example.com/icon.png") + * - Inline SVG markup (e.g. "...") + */ icon: string; + /** + * Additional CSS properties applied to the icon element. + * - For image-type icons: if any `background-*` property (e.g. `background-size`, + * `background-position`) is present, the image is rendered as a CSS `background-image` + * on the container div, enabling full background positioning control. + * Otherwise the image is rendered as an `` tag and styles apply to it. + * - For emoji/SVG icons: styles apply to the container div. + */ + iconStyle?: Record; + /** + * Additional CSS properties applied directly to the inner `` element + * when the icon is an inline SVG. Use this to override the default + * `width: 100%; height: 100%` or set `fill`, `transform`, etc. + * Ignored for emoji and image-type icons. + */ + svgStyle?: Record; + /** + * Icon for dark mode. Same format as `icon`. When provided, this icon + * replaces `icon` when the user is in dark mode. + */ + iconDark?: string; + /** Additional CSS properties for the dark mode icon container */ + iconDarkStyle?: Record; + /** + * SVG-specific styles for dark mode. Same as `svgStyle` but applied + * when dark mode is active. Merged over `svgStyle` in dark mode. + */ + svgDarkStyle?: Record; /** Docker image tag, e.g. "stable" */ imageTag: string; /** diff --git a/docs/src/components/DockerComposeGenerator/styles.module.css b/docs/src/components/DockerComposeGenerator/styles.module.css index 0b7429ece..62ecb1b6f 100644 --- a/docs/src/components/DockerComposeGenerator/styles.module.css +++ b/docs/src/components/DockerComposeGenerator/styles.module.css @@ -180,6 +180,40 @@ justify-content: center; } +.deviceIconSvg { + margin-bottom: 0.25rem; + height: 40px; + width: 50px; + display: flex; + align-items: center; + justify-content: center; + overflow: visible; + /* Allow iconStyle width/height to override */ + flex-shrink: 0; +} + +.deviceIconSvg svg { + width: var(--svg-width, 100%); + height: var(--svg-height, 100%); + fill: var(--svg-fill, currentColor); + transform: var(--svg-transform, none); +} + +.deviceIconImage { + margin-bottom: 0.25rem; + height: 40px; + width: 50px; + display: flex; + align-items: center; + justify-content: center; +} + +.deviceIconImage img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + .deviceName { font-weight: var(--ifm-font-weight-semibold); color: var(--ifm-font-color-base); From cef4355e28b79161513a78536d465d90c3c6328d Mon Sep 17 00:00:00 2001 From: GuoQing Liu <842607283@qq.com> Date: Wed, 29 Apr 2026 04:07:51 +0800 Subject: [PATCH 03/10] Update docs/src/components/DockerComposeGenerator/config/config.yaml Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> --- docs/src/components/DockerComposeGenerator/config/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/components/DockerComposeGenerator/config/config.yaml b/docs/src/components/DockerComposeGenerator/config/config.yaml index 69058fc57..eef54640a 100644 --- a/docs/src/components/DockerComposeGenerator/config/config.yaml +++ b/docs/src/components/DockerComposeGenerator/config/config.yaml @@ -180,7 +180,7 @@ hardware: devices: - host: "/dev/dri" container: "/dev/dri" - comment: "GPU hardware acceleration" + comment: "Intel/AMD GPU hardware acceleration" - id: "intelNpu" label: "Intel NPU (/dev/accel)" From deaf67fd8a8c60f4717ed238fbde603e1c486de4 Mon Sep 17 00:00:00 2001 From: GuoQing Liu <842607283@qq.com> Date: Wed, 29 Apr 2026 04:08:19 +0800 Subject: [PATCH 04/10] Update docs/src/components/DockerComposeGenerator/config/config.yaml Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> --- docs/src/components/DockerComposeGenerator/config/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/components/DockerComposeGenerator/config/config.yaml b/docs/src/components/DockerComposeGenerator/config/config.yaml index eef54640a..44f798b83 100644 --- a/docs/src/components/DockerComposeGenerator/config/config.yaml +++ b/docs/src/components/DockerComposeGenerator/config/config.yaml @@ -104,7 +104,7 @@ devices: imageTagSuffix: "-standard-arm64" autoHardware: - "video11" - helpText: "Raspberry Pi automatically configures the Video11 device and uses the arm64 image." + helpText: "Raspberry Pi automatically configures the video11 device (RPi 4) and uses the arm64 image." helpType: "info" - id: "stable-rk" From 9769e59e691362f126156b566ce216f10fc831f5 Mon Sep 17 00:00:00 2001 From: GuoQing Liu <842607283@qq.com> Date: Wed, 29 Apr 2026 04:08:40 +0800 Subject: [PATCH 05/10] Update docs/src/components/DockerComposeGenerator/config/config.yaml Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> --- docs/src/components/DockerComposeGenerator/config/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/components/DockerComposeGenerator/config/config.yaml b/docs/src/components/DockerComposeGenerator/config/config.yaml index 44f798b83..5711f66ec 100644 --- a/docs/src/components/DockerComposeGenerator/config/config.yaml +++ b/docs/src/components/DockerComposeGenerator/config/config.yaml @@ -242,7 +242,7 @@ hardware: comment: "AXERA libraries" - id: "video11" - label: "Raspberry Pi Video11" + label: "Raspberry Pi video11" description: "Pass through /dev/video11 for Raspberry Pi 4B hardware acceleration." disabledWhen: - "stable-tensorrt" From 25031618c7420a6108abf817388d84d95be877ec Mon Sep 17 00:00:00 2001 From: GuoQing Liu <842607283@qq.com> Date: Wed, 29 Apr 2026 04:11:35 +0800 Subject: [PATCH 06/10] Rename heading from 'Generic Hardware Acceleration' to 'Generic Hardware Devices' --- .../DockerComposeGenerator/components/HardwareOptions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx b/docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx index 0162269b9..9c261ed41 100644 --- a/docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx +++ b/docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx @@ -42,7 +42,7 @@ function HardwareCheckbox({ export default function HardwareOptions({ deviceId, hardwareEnabled, onToggle, isDisabled }: Props) { return (
-

Generic Hardware Acceleration

+

Generic Hardware Devices

{deviceId !== "stable" && (

Some options have been auto-configured based on your device type. From 39f94919719ad2b264538cfb248f8edca821e865 Mon Sep 17 00:00:00 2001 From: GuoQing Liu <842607283@qq.com> Date: Wed, 29 Apr 2026 04:23:56 +0800 Subject: [PATCH 07/10] Remove port 5000 configuration for security reasons Removed unauthenticated Web UI port 5000 from configuration due to security risks. --- .../DockerComposeGenerator/config/config.yaml | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/docs/src/components/DockerComposeGenerator/config/config.yaml b/docs/src/components/DockerComposeGenerator/config/config.yaml index 5711f66ec..09b538281 100644 --- a/docs/src/components/DockerComposeGenerator/config/config.yaml +++ b/docs/src/components/DockerComposeGenerator/config/config.yaml @@ -267,18 +267,6 @@ ports: defaultEnabled: true locked: true - - id: "5000" - host: 5000 - container: 5000 - protocol: "tcp" - description: "Unauthenticated Web UI port (not recommended)" - defaultEnabled: false - requiresConfirmation: true - warningType: "danger" - warningContent: "Exposing port 5000 allows **unauthenticated access** to your Frigate instance. Anyone on your network (or the internet if you have a public IP) could access it without any credentials. This may lead to **unauthorized access**, **privacy leaks**, or further attacks. Ensure you have proper firewall rules or VPN in place before enabling this." - confirmationLabel: "I understand the risk and confirm enabling port 5000" - cooldownSeconds: 10 - - id: "8554" host: 8554 container: 8554 @@ -305,4 +293,4 @@ ports: container: 1984 protocol: "tcp" description: "Go2RTC Web UIport" - defaultEnabled: false \ No newline at end of file + defaultEnabled: false From 6cc4db1103f03634ddf13fba4e82b4a458673fea Mon Sep 17 00:00:00 2001 From: ZhaiSoul <842607283@qq.com> Date: Wed, 29 Apr 2026 14:13:32 +0800 Subject: [PATCH 08/10] docs: remove 5000 port tips --- .../DockerComposeGenerator.tsx | 6 +- .../components/PortConfig.tsx | 122 +++++------------- .../DockerComposeGenerator/config/config.yaml | 3 +- .../DockerComposeGenerator/config/types.ts | 8 +- .../hooks/useConfigGenerator.ts | 16 +-- .../hooks/useCooldown.ts | 42 ------ 6 files changed, 44 insertions(+), 153 deletions(-) delete mode 100644 docs/src/components/DockerComposeGenerator/hooks/useCooldown.ts diff --git a/docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx b/docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx index 0779f4249..b8a8a8fc8 100644 --- a/docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx +++ b/docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx @@ -32,12 +32,12 @@ function renderHelpText(text: string): React.ReactNode { export default function DockerComposeGenerator() { const { deviceId, device, hardwareEnabled, - portEnabled, port5000Confirmed, + portEnabled, nvidiaGpuCount, nvidiaGpuDeviceId, configPath, mediaPath, rtspPassword, timezone, shmSize, shmSizeError, gpuDeviceIdError, configPathError, mediaPathError, hasAnyHardware, generatedYaml, - selectDevice, toggleHardware, togglePort, setPort5000Confirmed, + selectDevice, toggleHardware, togglePort, handleShmSizeChange, handleConfigPathChange, handleMediaPathChange, handleNvidiaGpuCountChange, handleNvidiaGpuDeviceIdChange, setRtspPassword, setTimezone, isHardwareDisabled, @@ -82,9 +82,7 @@ export default function DockerComposeGenerator() { ; - port5000Confirmed: boolean; onTogglePort: (portId: string) => void; - onConfirm5000: (confirmed: boolean) => void; } -function Port5000Confirmation({ - portEnabled, - confirmed, +function PortItem({ + port, + enabled, onToggle, - onConfirm, }: { - portEnabled: boolean; - confirmed: boolean; + port: typeof ports[number]; + enabled: boolean; onToggle: () => void; - onConfirm: (confirmed: boolean) => void; }) { - const { remaining, start, stop } = useCooldown(10); - - React.useEffect(() => { - if (portEnabled) { - start(); - } else { - stop(); - onConfirm(false); - } - return stop; - }, [portEnabled]); + const showWarning = port.warningContent && ( + port.warningWhen === "checked" ? enabled : + port.warningWhen === "unchecked" ? !enabled : enabled + ); return ( -

- {portEnabled && ( - -

- Exposing port 5000 allows unauthenticated access to - your Frigate instance. Anyone on your network (or the internet if you - have a public IP) could access it without credentials. -

-

- This may lead to unauthorized access,{" "} - privacy leaks, or further attacks. Ensure you have - proper firewall rules or VPN in place. -

- -
- )} -