From 731206be8e38f15728df295b7342e506e61962ee Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Sat, 19 Sep 2026 19:35:50 -0500 Subject: [PATCH] Resolve data and binary folders before starting the server (0.1.2) onvif-dashboard install-video put MediaMTX in ./bin relative to where it was run, but start launches the server from inside the package, so the app looked for it there and reported it missing. serverEnvironment() now settles CAMERAS_DATA_DIR and CAMERAS_BIN_DIR while still in the user's directory, and start passes them on. Checked from an installed tarball: MediaMTX in ./bin is found and the video bridge comes up. Co-Authored-By: Claude Opus 5 (1M context) --- cli/onvif-dashboard.mjs | 5 +++-- package.json | 2 +- src/lib/first-run.test.ts | 21 +++++++++++++++++++++ src/lib/first-run.ts | 14 +++++++++++++- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/cli/onvif-dashboard.mjs b/cli/onvif-dashboard.mjs index bf3fbb8..0f51a25 100755 --- a/cli/onvif-dashboard.mjs +++ b/cli/onvif-dashboard.mjs @@ -6,7 +6,7 @@ import { access } from "node:fs/promises"; import { createInterface } from "node:readline/promises"; import { parseArgs } from "node:util"; import path from "node:path"; -import { applyEnvironment, firstRun, SECRETS_FILE } from "../dist/lib/first-run.js"; +import { applyEnvironment, firstRun, SECRETS_FILE, serverEnvironment } from "../dist/lib/first-run.js"; import { installMediamtx } from "../dist/lib/mediamtx-install.js"; import { adminFilePath, readAdminFile } from "../dist/lib/admin-file.js"; @@ -70,8 +70,9 @@ async function start() { if (!process.env.CAMERA_CREDENTIALS_KEY) { // Never started before: take the defaults quietly rather than refuse to run. await firstRun(); - await applyEnvironment(); } + // Settle every path now: the server runs from inside the package, not from here. + await serverEnvironment(); const server = path.join(import.meta.dirname, "..", ".next", "standalone", "server.js"); if (!(await access(server).then(() => true, () => false))) { diff --git a/package.json b/package.json index e16aa48..f850e62 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "onvif-dashboard", - "version": "0.1.1", + "version": "0.1.2", "description": "Web dashboard for ONVIF cameras (Hikvision, Annke): discovery, live video over WebRTC, motion recording and settings, all on your own network.", "keywords": [ "onvif", diff --git a/src/lib/first-run.test.ts b/src/lib/first-run.test.ts index 808e57b..33f5bdf 100644 --- a/src/lib/first-run.test.ts +++ b/src/lib/first-run.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { applyEnvironment, + serverEnvironment, configFile, ensureSecrets, firstRun, @@ -188,3 +189,23 @@ describe("applyEnvironment", () => { expect(env.CAMERA_CREDENTIALS_KEY).toBeUndefined(); }); }); + +describe("serverEnvironment", () => { + it("pins the data and binary folders before the server starts elsewhere", async () => { + await writeConfig(work, {}, home); + await ensureSecrets(work); + const env: Record = {}; + await serverEnvironment(env, home); + + expect(env.CAMERAS_DATA_DIR).toBe(work); + // Resolved from where the command was typed, not from wherever the server will run. + expect(env.CAMERAS_BIN_DIR).toBe(path.join(cwd, "bin")); + expect(env.CAMERA_CREDENTIALS_KEY).toMatch(/^[0-9a-f]{64}$/); + }); + + it("leaves anything the environment already set alone", async () => { + const env = { CAMERAS_DATA_DIR: "/srv/data", CAMERAS_BIN_DIR: "/opt/bin" }; + await serverEnvironment(env, home); + expect(env).toMatchObject({ CAMERAS_DATA_DIR: "/srv/data", CAMERAS_BIN_DIR: "/opt/bin" }); + }); +}); diff --git a/src/lib/first-run.ts b/src/lib/first-run.ts index a318b1c..a32169f 100644 --- a/src/lib/first-run.ts +++ b/src/lib/first-run.ts @@ -14,7 +14,7 @@ import { randomBytes } from "node:crypto"; import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { dataDir as defaultDataDir, mediamtxBinary } from "./paths.ts"; +import { binDir as defaultBinDir, dataDir as defaultDataDir, mediamtxBinary } from "./paths.ts"; type Env = Record; @@ -151,3 +151,15 @@ export async function applyEnvironment(env: Env = process.env, home = os.homedir const stored = parseEnvFile(await readFile(path.join(dir, SECRETS_FILE), "utf8").catch(() => "")); for (const [key, value] of Object.entries(stored)) env[key] ??= value; } + +/** + * The environment the server is started with. The server runs from inside the package, so + * anything resolved from the working directory has to be settled here, while we are still + * in the directory the user typed the command in (vrek iss-9zcawn7). + */ +export async function serverEnvironment(env: Env = process.env, home = os.homedir()): Promise { + await applyEnvironment(env, home); + env.CAMERAS_DATA_DIR ??= defaultDataDir(env); + env.CAMERAS_BIN_DIR ??= defaultBinDir(env); + return env; +}