diff --git a/.github/workflows/upstream-release.yml b/.github/workflows/upstream-release.yml index 4b2321e..760231f 100644 --- a/.github/workflows/upstream-release.yml +++ b/.github/workflows/upstream-release.yml @@ -48,7 +48,7 @@ jobs: snapraid-daemon [v$newest](https://github.com/amadvance/snapraid-daemon/tree/v$newest) is out, the SnapRAID engine was last compared with v$reviewed. - [ ] Compare the API: [snapraidd.yaml v$reviewed...v$newest](https://github.com/amadvance/snapraid-daemon/compare/v$reviewed...v$newest) and the [HISTORY](https://github.com/amadvance/snapraid-daemon/blob/v$newest/HISTORY) - - [ ] An endpoint for something CLI-only today (\`dup\`, \`list\`, \`devices\`, config, ...)? Add a method to \`SnapRaidEngine\` in \`apps/server/src/engine/engine.ts\`, implement it in \`cli-engine.ts\`, move the callers and extend \`apps/server/src/__tests__/engine-contract.ts\` + - [ ] An endpoint for something CLI-only today (\`dup\`, \`list\`, \`devices\`, config, ...)? Add a method to \`SnapRaidEngine\` in \`apps/server/src/engine/engine.ts\`, implement it in \`cli-engine.ts\`, move the callers and extend \`apps/server/src/engine/__tests__/engine-contract.ts\` - [ ] Update the tables in [docs/architecture.md](https://github.com/${{ github.repository }}/blob/master/docs/architecture.md#snapraid-engine) - [ ] Put $newest into \`apps/server/src/engine/REVIEWED_DAEMON_VERSION\` - [ ] The end-to-end tests build and run against that version; if it needs a newer SnapRAID, raise \`SNAPRAID15_REF\` in \`e2e/scripts/setup-tools.sh\` diff --git a/apps/server/scripts/seed-demo.ts b/apps/server/scripts/seed-demo.ts index 56c54ff..17c9c71 100644 --- a/apps/server/scripts/seed-demo.ts +++ b/apps/server/scripts/seed-demo.ts @@ -10,8 +10,8 @@ import { readdir, readFile, rm, utimes, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Cron } from "croner"; import type { MaintenanceSettings, Schedule, UsagePoint } from "@shared/types.ts"; -import { parseSnapRaidConfig } from "../src/config-parser.ts"; -import { demoDataDiskUsage } from "../src/demo.ts"; +import { parseSnapRaidConfig } from "../src/config/config-parser.ts"; +import { demoDataDiskUsage } from "../src/system/demo.ts"; const sandbox = process.argv[2]; if (!sandbox) throw new Error("Usage: seed-demo.ts "); @@ -123,7 +123,7 @@ const schedules: Schedule[] = [ ]; await writeFile(join(sandbox, "schedules.json"), JSON.stringify({ schedules }, null, 2)); -// The demo answers for Docker itself (apps/server/src/demo.ts), spindown watches the demo disks +// The demo answers for Docker itself (apps/server/src/system/demo.ts), spindown watches the demo disks // The rest (metrics, MCP) takes its defaults when the backend loads the file const maintenance: Pick = { dockerPause: { diff --git a/apps/server/src/__tests__/auth.test.ts b/apps/server/src/auth/__tests__/auth.test.ts similarity index 99% rename from apps/server/src/__tests__/auth.test.ts rename to apps/server/src/auth/__tests__/auth.test.ts index f9fa9bb..72a7f55 100644 --- a/apps/server/src/__tests__/auth.test.ts +++ b/apps/server/src/auth/__tests__/auth.test.ts @@ -1,13 +1,13 @@ import { expect, test } from "bun:test"; -import { assertThrows } from "./assert-error.ts"; +import { assertThrows } from "../../__tests__/assert-error.ts"; import { join } from "node:path"; import type { Server } from "bun"; import { Hono } from "hono"; import { createAuth, SESSION_COOKIE } from "../auth.ts"; import { type CredentialStore, hashPassword, loadCredentials, verifyPassword } from "../credentials.ts"; import { AuthError, createPasskeys, relyingParty } from "../passkeys.ts"; -import { broadcast, closeAllClients, handleWebSocketUpgrade, websocket } from "../websocket.ts"; -import { setEngine } from "../engine/engine.ts"; +import { broadcast, closeAllClients, handleWebSocketUpgrade, websocket } from "../../http/websocket.ts"; +import { setEngine } from "../../engine/engine.ts"; import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; diff --git a/apps/server/src/auth.ts b/apps/server/src/auth/auth.ts similarity index 99% rename from apps/server/src/auth.ts rename to apps/server/src/auth/auth.ts index 0c0e96a..b6e7833 100644 --- a/apps/server/src/auth.ts +++ b/apps/server/src/auth/auth.ts @@ -6,11 +6,11 @@ import { sign, verify } from "hono/jwt"; import type { AuthenticationResponseJSON, RegistrationResponseJSON } from "@simplewebauthn/server"; import type { AuthSession, PasskeyInfo } from "@shared/types.ts"; import { msg } from "@shared/i18n.ts"; -import { BASE_PATH, resolveFromBase } from "./config.ts"; +import { BASE_PATH, resolveFromBase } from "../config/config.ts"; import { type CredentialStore, hashPassword, verifyPassword } from "./credentials.ts"; import { AuthError, createPasskeys } from "./passkeys.ts"; import { mkdir, readFile } from "node:fs/promises"; -import { writeTextFile } from "./fs.ts"; +import { writeTextFile } from "../system/fs.ts"; export const SESSION_COOKIE = "snapraid_session"; diff --git a/apps/server/src/credentials.ts b/apps/server/src/auth/credentials.ts similarity index 98% rename from apps/server/src/credentials.ts rename to apps/server/src/auth/credentials.ts index ad5ed56..6f7d01a 100644 --- a/apps/server/src/credentials.ts +++ b/apps/server/src/auth/credentials.ts @@ -5,9 +5,9 @@ // SNAPRAID_UI_USERNAME or SNAPRAID_UI_PASSWORD change, the file is dropped at startup: the // password from the environment applies again and the passkeys are gone. That is the way back // in after a forgotten password, and it removes a passkey someone else added. -import { writeTextFileAtomic } from "./atomic-write.ts"; +import { writeTextFileAtomic } from "../system/atomic-write.ts"; import { readFile } from "node:fs/promises"; -import { isNotFound, remove } from "./fs.ts"; +import { isNotFound, remove } from "../system/fs.ts"; export interface StoredPasskey { id: string; diff --git a/apps/server/src/passkeys.ts b/apps/server/src/auth/passkeys.ts similarity index 100% rename from apps/server/src/passkeys.ts rename to apps/server/src/auth/passkeys.ts diff --git a/apps/server/src/bin-cli.ts b/apps/server/src/bin-cli.ts index 491e777..a8bbf1d 100644 --- a/apps/server/src/bin-cli.ts +++ b/apps/server/src/bin-cli.ts @@ -1,7 +1,7 @@ // The command line of the standalone binary (bin.ts): --version and --help, and the data directory // when none is set, before main.ts's modules read the environment import { join } from "node:path"; -import { APP_VERSION } from "./version.ts"; +import { APP_VERSION } from "./system/version.ts"; const HELP = `SnapRAID UI ${APP_VERSION}, web interface for SnapRAID diff --git a/apps/server/src/__tests__/backup.test.ts b/apps/server/src/config/__tests__/backup.test.ts similarity index 95% rename from apps/server/src/__tests__/backup.test.ts rename to apps/server/src/config/__tests__/backup.test.ts index 466765f..5997d1b 100644 --- a/apps/server/src/__tests__/backup.test.ts +++ b/apps/server/src/config/__tests__/backup.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { assertThrows } from "./assert-error.ts"; +import { assertThrows } from "../../__tests__/assert-error.ts"; import { BackupError, planRestore } from "../backup.ts"; const configJson = JSON.stringify({ diff --git a/apps/server/src/__tests__/config.test.ts b/apps/server/src/config/__tests__/config.test.ts similarity index 100% rename from apps/server/src/__tests__/config.test.ts rename to apps/server/src/config/__tests__/config.test.ts diff --git a/apps/server/src/__tests__/maintenance-settings.test.ts b/apps/server/src/config/__tests__/maintenance-settings.test.ts similarity index 100% rename from apps/server/src/__tests__/maintenance-settings.test.ts rename to apps/server/src/config/__tests__/maintenance-settings.test.ts diff --git a/apps/server/src/__tests__/setup.test.ts b/apps/server/src/config/__tests__/setup.test.ts similarity index 100% rename from apps/server/src/__tests__/setup.test.ts rename to apps/server/src/config/__tests__/setup.test.ts diff --git a/apps/server/src/backup.ts b/apps/server/src/config/backup.ts similarity index 97% rename from apps/server/src/backup.ts rename to apps/server/src/config/backup.ts index 60fe256..5f2912b 100644 --- a/apps/server/src/backup.ts +++ b/apps/server/src/config/backup.ts @@ -1,9 +1,9 @@ -import { existsSync } from "./fs.ts"; +import { existsSync } from "../system/fs.ts"; import { dirname, isAbsolute, normalize } from "node:path"; import type { AppConfig, SettingsBackup } from "@shared/types.ts"; import { msg } from "@shared/i18n.ts"; import { resolveFromBase } from "./config.ts"; -import { writeTextFileAtomic } from "./atomic-write.ts"; +import { writeTextFileAtomic } from "../system/atomic-write.ts"; import { mkdir, readFile, writeFile } from "node:fs/promises"; // Settings and history next to config.json; logs and replacement runs stay behind diff --git a/apps/server/src/config-parser.ts b/apps/server/src/config/config-parser.ts similarity index 96% rename from apps/server/src/config-parser.ts rename to apps/server/src/config/config-parser.ts index 38ba62f..c187916 100644 --- a/apps/server/src/config-parser.ts +++ b/apps/server/src/config/config-parser.ts @@ -1,10 +1,10 @@ import { basename, dirname, join } from "node:path"; import type { AppConfig, ParityLevel, ParsedSnapRaidConfig } from "@shared/types.ts"; import { resolveFromBase } from "./config.ts"; -import { effectiveSmartIgnore, parseSmartIgnoreLine } from "./smart-ignore.ts"; -import { writeTextFileAtomic } from "./atomic-write.ts"; +import { effectiveSmartIgnore, parseSmartIgnoreLine } from "../smart/smart-ignore.ts"; +import { writeTextFileAtomic } from "../system/atomic-write.ts"; import { mkdir, readFile } from "node:fs/promises"; -import { isNotFound } from "./fs.ts"; +import { isNotFound } from "../system/fs.ts"; export const MAX_PARITY_LEVEL = 6; diff --git a/apps/server/src/config.ts b/apps/server/src/config/config.ts similarity index 100% rename from apps/server/src/config.ts rename to apps/server/src/config/config.ts diff --git a/apps/server/src/maintenance-settings.ts b/apps/server/src/config/maintenance-settings.ts similarity index 97% rename from apps/server/src/maintenance-settings.ts rename to apps/server/src/config/maintenance-settings.ts index 42944dc..30198c3 100644 --- a/apps/server/src/maintenance-settings.ts +++ b/apps/server/src/config/maintenance-settings.ts @@ -1,7 +1,7 @@ -import { existsSync } from "./fs.ts"; +import { existsSync } from "../system/fs.ts"; import type { MaintenanceSettings, SnapRaidCommand } from "@shared/types.ts"; import { resolveFromBase } from "./config.ts"; -import { writeTextFileAtomic } from "./atomic-write.ts"; +import { writeTextFileAtomic } from "../system/atomic-write.ts"; import { readFile } from "node:fs/promises"; const SETTINGS_FILE = "maintenance.json"; diff --git a/apps/server/src/setup.ts b/apps/server/src/config/setup.ts similarity index 98% rename from apps/server/src/setup.ts rename to apps/server/src/config/setup.ts index 0f144a7..7825f34 100644 --- a/apps/server/src/setup.ts +++ b/apps/server/src/config/setup.ts @@ -6,8 +6,8 @@ import type { MountCandidate } from "@shared/types.ts"; import { loadAppConfig, saveAppConfig } from "./config-parser.ts"; import { BASE_PATH, resolveFromBase } from "./config.ts"; import { readdir, stat, writeFile } from "node:fs/promises"; -import { isAlreadyExists } from "./fs.ts"; -import { runCommand } from "./command.ts"; +import { isAlreadyExists } from "../system/fs.ts"; +import { runCommand } from "../system/command.ts"; // Filesystems that never hold data disks const VIRTUAL_FS = new Set([ diff --git a/apps/server/src/__tests__/disk-check.test.ts b/apps/server/src/disks/__tests__/disk-check.test.ts similarity index 100% rename from apps/server/src/__tests__/disk-check.test.ts rename to apps/server/src/disks/__tests__/disk-check.test.ts diff --git a/apps/server/src/__tests__/disk-power.test.ts b/apps/server/src/disks/__tests__/disk-power.test.ts similarity index 100% rename from apps/server/src/__tests__/disk-power.test.ts rename to apps/server/src/disks/__tests__/disk-power.test.ts diff --git a/apps/server/src/__tests__/disk-removal.test.ts b/apps/server/src/disks/__tests__/disk-removal.test.ts similarity index 95% rename from apps/server/src/__tests__/disk-removal.test.ts rename to apps/server/src/disks/__tests__/disk-removal.test.ts index 4afbdb8..62e16c8 100644 --- a/apps/server/src/__tests__/disk-removal.test.ts +++ b/apps/server/src/disks/__tests__/disk-removal.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { assertRejects, assertThrows } from "./assert-error.ts"; +import { assertRejects, assertThrows } from "../../__tests__/assert-error.ts"; import { join } from "node:path"; import { DiskRemovalError, @@ -7,7 +7,7 @@ import { finalizeDataDiskRemoval, prepareDataDiskRemoval, } from "../disk-removal.ts"; -import { parseSnapRaidConfig } from "../config-parser.ts"; +import { parseSnapRaidConfig } from "../../config/config-parser.ts"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; diff --git a/apps/server/src/__tests__/disk-replacement.test.ts b/apps/server/src/disks/__tests__/disk-replacement.test.ts similarity index 97% rename from apps/server/src/__tests__/disk-replacement.test.ts rename to apps/server/src/disks/__tests__/disk-replacement.test.ts index c0f9604..00278cf 100644 --- a/apps/server/src/__tests__/disk-replacement.test.ts +++ b/apps/server/src/disks/__tests__/disk-replacement.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { assertThrows } from "./assert-error.ts"; +import { assertThrows } from "../../__tests__/assert-error.ts"; import { applyReplacementPath, DiskReplacementError, requiredDirectory } from "../disk-replacement.ts"; const CONFIG = `parity /mnt/parity1/snapraid.parity diff --git a/apps/server/src/__tests__/spindown.test.ts b/apps/server/src/disks/__tests__/spindown.test.ts similarity index 100% rename from apps/server/src/__tests__/spindown.test.ts rename to apps/server/src/disks/__tests__/spindown.test.ts diff --git a/apps/server/src/disk-check.ts b/apps/server/src/disks/disk-check.ts similarity index 97% rename from apps/server/src/disk-check.ts rename to apps/server/src/disks/disk-check.ts index 1029a69..e6fdaaa 100644 --- a/apps/server/src/disk-check.ts +++ b/apps/server/src/disks/disk-check.ts @@ -2,9 +2,9 @@ // An unmounted disk leaves an empty mount point behind; SnapRAID then sees all its // files as deleted, and a sync would drop them from parity. import type { DiskIssue } from "@shared/types.ts"; -import { parseLogTags, toInt, unescapeTagValue } from "./parsers/structured-log.ts"; +import { parseLogTags, toInt, unescapeTagValue } from "../parsers/structured-log.ts"; import { readdir, stat } from "node:fs/promises"; -import { isNotFound } from "./fs.ts"; +import { isNotFound } from "../system/fs.ts"; // Parity levels in the structured log, split parity files (`parity/1`) are not tag lines const PARITY_LEVEL = /^(?:[2-6]-)?parity(?:\/\d+)?$|^z-parity(?:\/\d+)?$/; diff --git a/apps/server/src/disk-power.ts b/apps/server/src/disks/disk-power.ts similarity index 93% rename from apps/server/src/disk-power.ts rename to apps/server/src/disks/disk-power.ts index 18ab292..7ced507 100644 --- a/apps/server/src/disk-power.ts +++ b/apps/server/src/disks/disk-power.ts @@ -1,7 +1,7 @@ // Spin disks up or down by hand, with SnapRAID's `up` and `down` (smartctl and hdparm under the // hood). Like the automatic spindown this calls SnapRAID directly, also in daemon mode. -import { deviceCommandSucceeded, executeSnapraidCommand } from "./executors/command-executor.ts"; -import { DEMO_MODE } from "./demo.ts"; +import { deviceCommandSucceeded, executeSnapraidCommand } from "../executors/command-executor.ts"; +import { DEMO_MODE } from "../system/demo.ts"; export type PowerAction = "up" | "down"; diff --git a/apps/server/src/disk-removal.ts b/apps/server/src/disks/disk-removal.ts similarity index 99% rename from apps/server/src/disk-removal.ts rename to apps/server/src/disks/disk-removal.ts index f0b9b91..a7a6033 100644 --- a/apps/server/src/disk-removal.ts +++ b/apps/server/src/disks/disk-removal.ts @@ -1,5 +1,5 @@ import { dirname, join } from "node:path"; -import { isPendingRemoval, parseParityLine, REMOVAL_DIR } from "./config-parser.ts"; +import { isPendingRemoval, parseParityLine, REMOVAL_DIR } from "../config/config-parser.ts"; import { msg } from "@shared/i18n.ts"; import { mkdir, readdir } from "node:fs/promises"; diff --git a/apps/server/src/disk-replacement.ts b/apps/server/src/disks/disk-replacement.ts similarity index 95% rename from apps/server/src/disk-replacement.ts rename to apps/server/src/disks/disk-replacement.ts index 2ee3bee..c8c232d 100644 --- a/apps/server/src/disk-replacement.ts +++ b/apps/server/src/disks/disk-replacement.ts @@ -1,10 +1,10 @@ import { dirname } from "node:path"; -import { existsSync } from "./fs.ts"; +import { existsSync } from "../system/fs.ts"; import type { DiskReplacement, ReplacementStep, ReplacementStepResult } from "@shared/types.ts"; -import { parseParityLine } from "./config-parser.ts"; -import { resolveFromBase } from "./config.ts"; +import { parseParityLine } from "../config/config-parser.ts"; +import { resolveFromBase } from "../config/config.ts"; import { msg } from "@shared/i18n.ts"; -import { writeTextFileAtomic } from "./atomic-write.ts"; +import { writeTextFileAtomic } from "../system/atomic-write.ts"; import { readFile } from "node:fs/promises"; /** diff --git a/apps/server/src/parity-usage.ts b/apps/server/src/disks/parity-usage.ts similarity index 92% rename from apps/server/src/parity-usage.ts rename to apps/server/src/disks/parity-usage.ts index 40e8cca..c285f90 100644 --- a/apps/server/src/parity-usage.ts +++ b/apps/server/src/disks/parity-usage.ts @@ -5,10 +5,10 @@ import type { ParityLevelUsage, ParsedSnapRaidConfig, } from "@shared/types.ts"; -import { type FsUsage, parseDfOutput } from "./parsers/df-parser.ts"; -import { DEMO_MODE, demoDataDiskUsage, demoParityUsage } from "./demo.ts"; +import { type FsUsage, parseDfOutput } from "../parsers/df-parser.ts"; +import { DEMO_MODE, demoDataDiskUsage, demoParityUsage } from "../system/demo.ts"; import { stat } from "node:fs/promises"; -import { runCommand } from "./command.ts"; +import { runCommand } from "../system/command.ts"; const toGB = (bytes: number): number => Math.round(bytes / 1e9 * 10) / 10; diff --git a/apps/server/src/spindown.ts b/apps/server/src/disks/spindown.ts similarity index 93% rename from apps/server/src/spindown.ts rename to apps/server/src/disks/spindown.ts index 6e1cb56..8d4d1b0 100644 --- a/apps/server/src/spindown.ts +++ b/apps/server/src/disks/spindown.ts @@ -3,13 +3,13 @@ // are preferred: SMART reads and power state probes count as I/O of the whole disk only, // they would otherwise keep it awake forever. import type { DeviceInfo, SpindownDisk, SpindownStatus } from "@shared/types.ts"; -import { loadAppConfig } from "./config-parser.ts"; -import { resolveFromBase } from "./config.ts"; -import { executeSnapraidCommand } from "./executors/command-executor.ts"; -import { parseDevicesOutput } from "./parsers/devices-parser.ts"; -import { loadMaintenanceSettings } from "./maintenance-settings.ts"; -import { DEMO_MODE, demoDevices, demoDiskstats } from "./demo.ts"; -import { selfTestRunning } from "./smart-selftest.ts"; +import { loadAppConfig } from "../config/config-parser.ts"; +import { resolveFromBase } from "../config/config.ts"; +import { executeSnapraidCommand } from "../executors/command-executor.ts"; +import { parseDevicesOutput } from "../parsers/devices-parser.ts"; +import { loadMaintenanceSettings } from "../config/maintenance-settings.ts"; +import { DEMO_MODE, demoDevices, demoDiskstats } from "../system/demo.ts"; +import { selfTestRunning } from "../smart/smart-selftest.ts"; import { readFile } from "node:fs/promises"; const TICK_MS = 60_000; diff --git a/apps/server/src/__tests__/daemon-engine.test.ts b/apps/server/src/engine/__tests__/daemon-engine.test.ts similarity index 99% rename from apps/server/src/__tests__/daemon-engine.test.ts rename to apps/server/src/engine/__tests__/daemon-engine.test.ts index 1a71019..1f8464e 100644 --- a/apps/server/src/__tests__/daemon-engine.test.ts +++ b/apps/server/src/engine/__tests__/daemon-engine.test.ts @@ -1,9 +1,9 @@ // The daemon engine against a stand-in for snapraid-daemon 2.0. The responses follow // what a real daemon answered (fields trimmed); a task advances one step per poll. import { expect, test } from "bun:test"; -import { assertRejects } from "./assert-error.ts"; +import { assertRejects } from "../../__tests__/assert-error.ts"; import type { JobProgress } from "@shared/types.ts"; -import { EngineBusyError } from "../engine/engine.ts"; +import { EngineBusyError } from "../engine.ts"; import { createDaemonEngine, type DaemonArray, @@ -17,7 +17,7 @@ import { toRunReport, toSmartDisks, toStatus, -} from "../engine/daemon-engine.ts"; +} from "../daemon-engine.ts"; import { assessSmart } from "@shared/smart-health.ts"; import { createFakeEngine } from "./fake-engine.ts"; diff --git a/apps/server/src/__tests__/engine-contract.test.ts b/apps/server/src/engine/__tests__/engine-contract.test.ts similarity index 89% rename from apps/server/src/__tests__/engine-contract.test.ts rename to apps/server/src/engine/__tests__/engine-contract.test.ts index 1543a5c..eb7dccd 100644 --- a/apps/server/src/__tests__/engine-contract.test.ts +++ b/apps/server/src/engine/__tests__/engine-contract.test.ts @@ -1,10 +1,10 @@ import { join } from "node:path"; -import { runCommand } from "../command.ts"; -import { SNAPRAID_BIN, SNAPRAID_EXTRA_ARGS } from "../config.ts"; -import { createCliEngine } from "../engine/cli-engine.ts"; -import { createDaemonEngine } from "../engine/daemon-engine.ts"; -import { parseSnapRaidConfig } from "../config-parser.ts"; -import { createLogManager } from "../log-manager.ts"; +import { runCommand } from "../../system/command.ts"; +import { SNAPRAID_BIN, SNAPRAID_EXTRA_ARGS } from "../../config/config.ts"; +import { createCliEngine } from "../cli-engine.ts"; +import { createDaemonEngine } from "../daemon-engine.ts"; +import { parseSnapRaidConfig } from "../../config/config-parser.ts"; +import { createLogManager } from "../../snapraid/log-manager.ts"; import { engineContract } from "./engine-contract.ts"; import { createFakeEngine } from "./fake-engine.ts"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; diff --git a/apps/server/src/__tests__/engine-contract.ts b/apps/server/src/engine/__tests__/engine-contract.ts similarity index 96% rename from apps/server/src/__tests__/engine-contract.ts rename to apps/server/src/engine/__tests__/engine-contract.ts index cc33418..cff3aa9 100644 --- a/apps/server/src/__tests__/engine-contract.ts +++ b/apps/server/src/engine/__tests__/engine-contract.ts @@ -2,8 +2,8 @@ // the fake one always, the CLI one when a SnapRAID binary is available. // A new engine, e.g. one on snapraid-daemon's REST API, gets the same tests. import { expect, test } from "bun:test"; -import { assertRejects } from "./assert-error.ts"; -import { EngineBusyError, type SnapRaidEngine } from "../engine/engine.ts"; +import { assertRejects } from "../../__tests__/assert-error.ts"; +import { EngineBusyError, type SnapRaidEngine } from "../engine.ts"; export interface ContractSetup { engine: SnapRaidEngine; diff --git a/apps/server/src/__tests__/engine-settings.test.ts b/apps/server/src/engine/__tests__/engine-settings.test.ts similarity index 99% rename from apps/server/src/__tests__/engine-settings.test.ts rename to apps/server/src/engine/__tests__/engine-settings.test.ts index 8005828..98f8de9 100644 --- a/apps/server/src/__tests__/engine-settings.test.ts +++ b/apps/server/src/engine/__tests__/engine-settings.test.ts @@ -6,7 +6,7 @@ import { DEFAULT_ENGINE_SETTINGS, maskEngineSecrets, normalizeEngineSettings, -} from "../engine/engine-settings.ts"; +} from "../engine-settings.ts"; import { createFakeEngine } from "./fake-engine.ts"; const target = (changes = {}) => ({ diff --git a/apps/server/src/__tests__/fake-engine.ts b/apps/server/src/engine/__tests__/fake-engine.ts similarity index 99% rename from apps/server/src/__tests__/fake-engine.ts rename to apps/server/src/engine/__tests__/fake-engine.ts index 36ef39f..6f8f3bf 100644 --- a/apps/server/src/__tests__/fake-engine.ts +++ b/apps/server/src/engine/__tests__/fake-engine.ts @@ -9,7 +9,7 @@ import type { SnapRaidCommand, SnapRaidStatus, } from "@shared/types.ts"; -import { EngineBusyError, type JobOutcome, type SnapRaidEngine } from "../engine/engine.ts"; +import { EngineBusyError, type JobOutcome, type SnapRaidEngine } from "../engine.ts"; export interface FakeEngineOptions { disks?: string[]; diff --git a/apps/server/src/engine/cli-engine.ts b/apps/server/src/engine/cli-engine.ts index b9814fa..01a00ec 100644 --- a/apps/server/src/engine/cli-engine.ts +++ b/apps/server/src/engine/cli-engine.ts @@ -1,8 +1,8 @@ // The engine that runs the SnapRAID CLI itself and reads its structured log import type { CommandOutput, SnapRaidCommand } from "@shared/types.ts"; import { msg } from "@shared/i18n.ts"; -import { snapraidCommand } from "../config.ts"; -import { runCommand } from "../command.ts"; +import { snapraidCommand } from "../config/config.ts"; +import { runCommand } from "../system/command.ts"; import { abortCommand, deviceCommandSucceeded, @@ -13,11 +13,11 @@ import { setLogManager, setProgressListener, } from "../executors/command-executor.ts"; -import type { LogManager } from "../log-manager.ts"; -import { readRunReport } from "../run-report.ts"; -import { findDiskIssues } from "../disk-check.ts"; -import { DEMO_MODE, demoProbeLog, demoSmartLog, demoStatusLog } from "../demo.ts"; -import { parseSnapRaidConfig } from "../config-parser.ts"; +import type { LogManager } from "../snapraid/log-manager.ts"; +import { readRunReport } from "../snapraid/run-report.ts"; +import { findDiskIssues } from "../disks/disk-check.ts"; +import { DEMO_MODE, demoProbeLog, demoSmartLog, demoStatusLog } from "../system/demo.ts"; +import { parseSnapRaidConfig } from "../config/config-parser.ts"; import { parseDiffOutput } from "../parsers/diff-parser.ts"; import { parseDegradedDisks, parseProbeOutput } from "../parsers/probe-parser.ts"; import { parseSmartArrayFailure, parseSmartOutput } from "../parsers/smart-parser.ts"; diff --git a/apps/server/src/engine/daemon-engine.ts b/apps/server/src/engine/daemon-engine.ts index 0b09dc1..f4899a1 100644 --- a/apps/server/src/engine/daemon-engine.ts +++ b/apps/server/src/engine/daemon-engine.ts @@ -20,7 +20,7 @@ import type { SnapRaidCommand, SnapRaidStatus, } from "@shared/types.ts"; -import { parseRunReport, type RunReport } from "../run-report.ts"; +import { parseRunReport, type RunReport } from "../snapraid/run-report.ts"; import { EngineBusyError, type JobOutcome, diff --git a/apps/server/src/engine/engine-settings.ts b/apps/server/src/engine/engine-settings.ts index 3d64a4c..6d093f1 100644 --- a/apps/server/src/engine/engine-settings.ts +++ b/apps/server/src/engine/engine-settings.ts @@ -1,10 +1,10 @@ // Which engine runs SnapRAID (engine.json), and the engine built from it -import { existsSync } from "../fs.ts"; +import { existsSync } from "../system/fs.ts"; import { type DaemonCheck, type DaemonTarget, type DaemonWarning, type EngineSettings, SECRET_MASK } from "@shared/types.ts"; -import { resolveFromBase } from "../config.ts"; +import { resolveFromBase } from "../config/config.ts"; import { createDaemonClient, createDaemonEngine, type DaemonArray, DaemonError } from "./daemon-engine.ts"; import type { SnapRaidEngine } from "./engine.ts"; -import { writeTextFileAtomic } from "../atomic-write.ts"; +import { writeTextFileAtomic } from "../system/atomic-write.ts"; import { readFile } from "node:fs/promises"; const SETTINGS_FILE = "engine.json"; diff --git a/apps/server/src/engine/engine.ts b/apps/server/src/engine/engine.ts index 794ffba..333ce0d 100644 --- a/apps/server/src/engine/engine.ts +++ b/apps/server/src/engine/engine.ts @@ -14,7 +14,7 @@ import type { SnapRaidCommand, SnapRaidStatus, } from "@shared/types.ts"; -import type { RunReport } from "../run-report.ts"; +import type { RunReport } from "../snapraid/run-report.ts"; export interface JobRequest { command: SnapRaidCommand; diff --git a/apps/server/src/executors/command-executor.ts b/apps/server/src/executors/command-executor.ts index f9192bd..e323453 100644 --- a/apps/server/src/executors/command-executor.ts +++ b/apps/server/src/executors/command-executor.ts @@ -1,13 +1,13 @@ import type { SnapRaidCommand, CommandOutput, RunningJob, FinishedJob, JobProgress } from "@shared/types.ts"; import { detectForceOption } from "@shared/force-option.ts"; -import type { LogManager } from "../log-manager.ts"; +import type { LogManager } from "../snapraid/log-manager.ts"; import { basename } from "node:path"; import type { Subprocess } from "bun"; -import { snapraidCommand } from "../config.ts"; -import { runCommand } from "../command.ts"; +import { snapraidCommand } from "../config/config.ts"; +import { runCommand } from "../system/command.ts"; import { parseRunPos } from "../parsers/progress-parser.ts"; import { createLogTail } from "./log-tail.ts"; -import { holdContainers } from "../container-pause.ts"; +import { holdContainers } from "../system/container-pause.ts"; // A running SnapRAID with its output piped type SnapraidProcess = Subprocess<"inherit", "pipe", "pipe">; diff --git a/apps/server/src/__tests__/frontend.test.ts b/apps/server/src/http/__tests__/frontend.test.ts similarity index 100% rename from apps/server/src/__tests__/frontend.test.ts rename to apps/server/src/http/__tests__/frontend.test.ts diff --git a/apps/server/src/frontend.ts b/apps/server/src/http/frontend.ts similarity index 96% rename from apps/server/src/frontend.ts rename to apps/server/src/http/frontend.ts index b17f194..aa7132d 100644 --- a/apps/server/src/frontend.ts +++ b/apps/server/src/http/frontend.ts @@ -3,7 +3,7 @@ // port next to /api and /ws, so the UI runs as a single process without a proxy in front. import { stat } from "node:fs/promises"; -import { appFile } from "./app-files.ts"; +import { appFile } from "../system/app-files.ts"; export type FetchHandler = (request: Request) => Response | Promise; diff --git a/apps/server/src/websocket.ts b/apps/server/src/http/websocket.ts similarity index 97% rename from apps/server/src/websocket.ts rename to apps/server/src/http/websocket.ts index 8413e53..90e4e17 100644 --- a/apps/server/src/websocket.ts +++ b/apps/server/src/http/websocket.ts @@ -1,5 +1,5 @@ import type { Server, ServerWebSocket, WebSocketHandler } from "bun"; -import { getEngine } from "./engine/engine.ts"; +import { getEngine } from "../engine/engine.ts"; // WebSocket connection management const wsClients = new Set>(); diff --git a/apps/server/src/__tests__/home-assistant.test.ts b/apps/server/src/integrations/__tests__/home-assistant.test.ts similarity index 100% rename from apps/server/src/__tests__/home-assistant.test.ts rename to apps/server/src/integrations/__tests__/home-assistant.test.ts diff --git a/apps/server/src/__tests__/mcp.test.ts b/apps/server/src/integrations/__tests__/mcp.test.ts similarity index 97% rename from apps/server/src/__tests__/mcp.test.ts rename to apps/server/src/integrations/__tests__/mcp.test.ts index e45a604..5f751ec 100644 --- a/apps/server/src/__tests__/mcp.test.ts +++ b/apps/server/src/integrations/__tests__/mcp.test.ts @@ -2,13 +2,13 @@ import { expect, test } from "bun:test"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import type { LogFile, MaintenanceSettings, Schedule, SnapRaidConfig } from "@shared/types.ts"; -import { DEFAULT_MAINTENANCE_SETTINGS } from "../maintenance-settings.ts"; +import { DEFAULT_MAINTENANCE_SETTINGS } from "../../config/maintenance-settings.ts"; import { createMcpServer, DOCS_DIR, fileNameOf, type McpDeps } from "../mcp.ts"; import type { ConfigMetrics } from "../metrics.ts"; -import type { LogManager } from "../log-manager.ts"; -import { createMcpRoutes, setMcpSources } from "../routes/mcp.ts"; -import type { Scheduler } from "../scheduler.ts"; -import { createFakeEngine } from "./fake-engine.ts"; +import type { LogManager } from "../../snapraid/log-manager.ts"; +import { createMcpRoutes, setMcpSources } from "../../routes/mcp.ts"; +import type { Scheduler } from "../../schedule/scheduler.ts"; +import { createFakeEngine } from "../../engine/__tests__/fake-engine.ts"; const ARRAYS: SnapRaidConfig[] = [ { name: "Media", path: "media.conf", enabled: true }, diff --git a/apps/server/src/__tests__/metrics.test.ts b/apps/server/src/integrations/__tests__/metrics.test.ts similarity index 100% rename from apps/server/src/__tests__/metrics.test.ts rename to apps/server/src/integrations/__tests__/metrics.test.ts diff --git a/apps/server/src/array-snapshot.ts b/apps/server/src/integrations/array-snapshot.ts similarity index 82% rename from apps/server/src/array-snapshot.ts rename to apps/server/src/integrations/array-snapshot.ts index 8a75b96..5140437 100644 --- a/apps/server/src/array-snapshot.ts +++ b/apps/server/src/integrations/array-snapshot.ts @@ -2,14 +2,14 @@ // the last runs from the logs (or the daemon), the last status read, the usage and SMART // histories. The Prometheus metrics and Home Assistant are built from it. import type { Schedule } from "@shared/types.ts"; -import { loadAppConfig } from "./config-parser.ts"; -import { resolveFromBase } from "./config.ts"; -import { getEngine } from "./engine/engine.ts"; -import type { LogManager } from "./log-manager.ts"; +import { loadAppConfig } from "../config/config-parser.ts"; +import { resolveFromBase } from "../config/config.ts"; +import { getEngine } from "../engine/engine.ts"; +import type { LogManager } from "../snapraid/log-manager.ts"; import type { ConfigMetrics } from "./metrics.ts"; -import { getSmartHistory } from "./smart-history.ts"; -import { lastStatus } from "./status-cache.ts"; -import { getUsageHistory } from "./usage-history.ts"; +import { getSmartHistory } from "../smart/smart-history.ts"; +import { lastStatus } from "../snapraid/status-cache.ts"; +import { getUsageHistory } from "../snapraid/usage-history.ts"; const sources = { logManager: null as LogManager | null, diff --git a/apps/server/src/home-assistant-service.ts b/apps/server/src/integrations/home-assistant-service.ts similarity index 98% rename from apps/server/src/home-assistant-service.ts rename to apps/server/src/integrations/home-assistant-service.ts index 0ef80bf..bc0cc91 100644 --- a/apps/server/src/home-assistant-service.ts +++ b/apps/server/src/integrations/home-assistant-service.ts @@ -1,8 +1,8 @@ // The MQTT connection to Home Assistant: publishes discovery and state, runs the buttons' commands import mqtt, { type MqttClient } from "mqtt"; -import { existsSync } from "./fs.ts"; +import { existsSync } from "../system/fs.ts"; import { type HomeAssistantSettings, type HomeAssistantStatus, SECRET_MASK } from "@shared/types.ts"; -import { resolveFromBase } from "./config.ts"; +import { resolveFromBase } from "../config/config.ts"; import type { ConfigMetrics } from "./metrics.ts"; import { arraySlugs, @@ -13,7 +13,7 @@ import { type MqttMessage, parseCommand, } from "./home-assistant.ts"; -import { writeTextFileAtomic } from "./atomic-write.ts"; +import { writeTextFileAtomic } from "../system/atomic-write.ts"; import { readFile } from "node:fs/promises"; const SETTINGS_FILE = "home-assistant.json"; diff --git a/apps/server/src/home-assistant.ts b/apps/server/src/integrations/home-assistant.ts similarity index 98% rename from apps/server/src/home-assistant.ts rename to apps/server/src/integrations/home-assistant.ts index b75b8c0..85e8c79 100644 --- a/apps/server/src/home-assistant.ts +++ b/apps/server/src/integrations/home-assistant.ts @@ -3,8 +3,8 @@ // disks, binary sensors for problems and a running job, and buttons for sync and scrub. // The values come from the array snapshot, publishing never wakes a disk. import type { ConfigMetrics } from "./metrics.ts"; -import { blockingIssues } from "./disk-check.ts"; -import { isSuccessful } from "./run-report.ts"; +import { blockingIssues } from "../disks/disk-check.ts"; +import { isSuccessful } from "../snapraid/run-report.ts"; export type ArrayHealth = "ok" | "errors" | "disk_missing" | "sync_incomplete" | "unknown"; diff --git a/apps/server/src/mcp.ts b/apps/server/src/integrations/mcp.ts similarity index 98% rename from apps/server/src/mcp.ts rename to apps/server/src/integrations/mcp.ts index ca17b38..ebf9e85 100644 --- a/apps/server/src/mcp.ts +++ b/apps/server/src/integrations/mcp.ts @@ -20,12 +20,12 @@ import type { import { diskFreeSeries, forecastFill } from "@shared/usage-forecast.ts"; import { arrayState } from "./home-assistant.ts"; import type { ConfigMetrics } from "./metrics.ts"; -import { EngineBusyError, type SnapRaidEngine } from "./engine/engine.ts"; -import type { LogManager } from "./log-manager.ts"; -import type { Scheduler } from "./scheduler.ts"; -import type { CreateArrayResult } from "./setup.ts"; +import { EngineBusyError, type SnapRaidEngine } from "../engine/engine.ts"; +import type { LogManager } from "../snapraid/log-manager.ts"; +import type { Scheduler } from "../schedule/scheduler.ts"; +import type { CreateArrayResult } from "../config/setup.ts"; import { readdir, readFile } from "node:fs/promises"; -import { appFile } from "./app-files.ts"; +import { appFile } from "../system/app-files.ts"; export const DOCS_DIR = fileURLToPath(appFile("docs/")); diff --git a/apps/server/src/metrics.ts b/apps/server/src/integrations/metrics.ts similarity index 97% rename from apps/server/src/metrics.ts rename to apps/server/src/integrations/metrics.ts index 5c6a51a..dc8e541 100644 --- a/apps/server/src/metrics.ts +++ b/apps/server/src/integrations/metrics.ts @@ -2,9 +2,9 @@ // runs in the logs, the last status read, the usage and SMART histories and the schedules. // Nothing here runs SnapRAID or smartctl, a scrape never wakes a disk. import type { LastRuns, RunningJob, Schedule, SmartHistoryPoint, UsagePoint } from "@shared/types.ts"; -import { isSuccessful } from "./run-report.ts"; -import { blockingIssues } from "./disk-check.ts"; -import type { CachedStatus } from "./status-cache.ts"; +import { isSuccessful } from "../snapraid/run-report.ts"; +import { blockingIssues } from "../disks/disk-check.ts"; +import type { CachedStatus } from "../snapraid/status-cache.ts"; export interface ConfigMetrics { name: string; diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index d49c256..959c13f 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -1,16 +1,16 @@ import type { Server } from "bun"; import { Hono } from "hono"; import { cors } from "hono/cors"; -import { loadAppConfig } from "./config-parser.ts"; -import { broadcast, closeAllClients, handleWebSocketUpgrade, websocket } from "./websocket.ts"; +import { loadAppConfig } from "./config/config-parser.ts"; +import { broadcast, closeAllClients, handleWebSocketUpgrade, websocket } from "./http/websocket.ts"; import { setBroadcast, setSnapraidLogManager } from "./routes/snapraid.ts"; import { activeEngine, type SnapRaidEngine, setEngine, wrapJobs } from "./engine/engine.ts"; import { createCliEngine } from "./engine/cli-engine.ts"; import { buildEngine, loadEngineSettings } from "./engine/engine-settings.ts"; import { engineRoutes, setEngineApplier } from "./routes/engine.ts"; -import { createLogManager } from "./log-manager.ts"; +import { createLogManager } from "./snapraid/log-manager.ts"; import { setLogManager } from "./routes/logs.ts"; -import { createScheduler } from "./scheduler.ts"; +import { createScheduler } from "./schedule/scheduler.ts"; import { setScheduler } from "./routes/schedules.ts"; import { configRoutes } from "./routes/config.ts"; import { filesystemRoutes } from "./routes/filesystem.ts"; @@ -22,18 +22,18 @@ import { maintenanceRoutes, setSpindownMonitor } from "./routes/maintenance.ts"; import { setupRoutes } from "./routes/setup.ts"; import { metricsRoutes } from "./routes/metrics.ts"; import { mcpRoutes, setMcpSources } from "./routes/mcp.ts"; -import { setSnapshotSources, snapshotConfigs } from "./array-snapshot.ts"; +import { setSnapshotSources, snapshotConfigs } from "./integrations/array-snapshot.ts"; import { homeAssistantRoutes, setHomeAssistant } from "./routes/home-assistant.ts"; -import { createHomeAssistant } from "./home-assistant-service.ts"; +import { createHomeAssistant } from "./integrations/home-assistant-service.ts"; import { startExternalJob } from "./routes/snapraid.ts"; -import { createSpindownMonitor } from "./spindown.ts"; -import { resumeLeftoverContainers } from "./container-pause.ts"; -import { resolveFromBase } from "./config.ts"; -import { withRestoredOwnership } from "./restore-ownership.ts"; -import { createAuth, disabledAuthRoutes, loadSessionSecret, readAuthEnv } from "./auth.ts"; -import { loadCredentials } from "./credentials.ts"; -import { APP_VERSION } from "./version.ts"; -import { type FetchHandler, isBackendPath, loadFrontend } from "./frontend.ts"; +import { createSpindownMonitor } from "./disks/spindown.ts"; +import { resumeLeftoverContainers } from "./system/container-pause.ts"; +import { resolveFromBase } from "./config/config.ts"; +import { withRestoredOwnership } from "./snapraid/restore-ownership.ts"; +import { createAuth, disabledAuthRoutes, loadSessionSecret, readAuthEnv } from "./auth/auth.ts"; +import { loadCredentials } from "./auth/credentials.ts"; +import { APP_VERSION } from "./system/version.ts"; +import { type FetchHandler, isBackendPath, loadFrontend } from "./http/frontend.ts"; // The env of each request is the Bun server that received it (Bun.serve passes it to fetch) const app = new Hono<{ Bindings: Server }>(); diff --git a/apps/server/src/__tests__/notifications.test.ts b/apps/server/src/notifications/__tests__/notifications.test.ts similarity index 97% rename from apps/server/src/__tests__/notifications.test.ts rename to apps/server/src/notifications/__tests__/notifications.test.ts index 7c77ef1..78a9a9f 100644 --- a/apps/server/src/__tests__/notifications.test.ts +++ b/apps/server/src/notifications/__tests__/notifications.test.ts @@ -8,10 +8,10 @@ import { validateNotificationSettings, } from "../notifications.ts"; import { buildRunNotification, buildSkipNotification, diffDegradedDisks, diffSmartProblems, heartbeatAfterRun } from "../notification-events.ts"; -import { failedReport, parseRunReport } from "../run-report.ts"; -import { combineResults, scheduleSteps } from "../scheduler.ts"; +import { failedReport, parseRunReport } from "../../snapraid/run-report.ts"; +import { combineResults, scheduleSteps } from "../../schedule/scheduler.ts"; import { assessSmart, attributeLevel, smartHints } from "@shared/smart-health.ts"; -import { parseSmartOutput } from "../parsers/smart-parser.ts"; +import { parseSmartOutput } from "../../parsers/smart-parser.ts"; import { readFileSync } from "node:fs"; const SYNC_LOG = `summary:added:12 @@ -248,7 +248,7 @@ test("combineResults - first failure, otherwise the worst of ok and warning", () }); test("assessSmart - a few percent failure probability is normal, sectors and status are not", () => { - const disks = parseSmartOutput(readFileSync(new URL("../parsers/__tests__/fixtures/smart.log", import.meta.url), "utf8")); + const disks = parseSmartOutput(readFileSync(new URL("../../parsers/__tests__/fixtures/smart.log", import.meta.url), "utf8")); const byName = Object.fromEntries(disks.map((d) => [d.name, assessSmart(d)])); // 4.46% per year, no errors diff --git a/apps/server/src/notification-events.ts b/apps/server/src/notifications/notification-events.ts similarity index 98% rename from apps/server/src/notification-events.ts rename to apps/server/src/notifications/notification-events.ts index 6e128b5..be4ca94 100644 --- a/apps/server/src/notification-events.ts +++ b/apps/server/src/notifications/notification-events.ts @@ -1,5 +1,5 @@ import { basename } from "node:path"; -import { existsSync } from "./fs.ts"; +import { existsSync } from "../system/fs.ts"; import type { NotificationSettings, ScheduleOutcome, @@ -7,11 +7,11 @@ import type { SnapRaidCommand, } from "@shared/types.ts"; import { assessSmart, type SmartAssessment, type SmartReason, smartHints, smartSignature } from "@shared/smart-health.ts"; -import { resolveFromBase } from "./config.ts"; +import { resolveFromBase } from "../config/config.ts"; import { loadNotificationSettings, type Notification, notify, pingHeartbeat } from "./notifications.ts"; -import { isSuccessful, type RunReport } from "./run-report.ts"; -import { parseLogTags, restOf, unescapeTagValue } from "./parsers/structured-log.ts"; -import { writeTextFileAtomic } from "./atomic-write.ts"; +import { isSuccessful, type RunReport } from "../snapraid/run-report.ts"; +import { parseLogTags, restOf, unescapeTagValue } from "../parsers/structured-log.ts"; +import { writeTextFileAtomic } from "../system/atomic-write.ts"; import { readFile } from "node:fs/promises"; type Language = NotificationSettings["language"]; diff --git a/apps/server/src/notifications.ts b/apps/server/src/notifications/notifications.ts similarity index 98% rename from apps/server/src/notifications.ts rename to apps/server/src/notifications/notifications.ts index f89121b..5c026b6 100644 --- a/apps/server/src/notifications.ts +++ b/apps/server/src/notifications/notifications.ts @@ -1,5 +1,5 @@ import nodemailer from "nodemailer"; -import { existsSync } from "./fs.ts"; +import { existsSync } from "../system/fs.ts"; import { type NotificationChannel, type NotificationEvent, @@ -7,8 +7,8 @@ import { type NotificationTestResult, SECRET_MASK, } from "@shared/types.ts"; -import { resolveFromBase } from "./config.ts"; -import { writeTextFileAtomic } from "./atomic-write.ts"; +import { resolveFromBase } from "../config/config.ts"; +import { writeTextFileAtomic } from "../system/atomic-write.ts"; import { readFile } from "node:fs/promises"; export type Severity = "info" | "warning" | "error"; diff --git a/apps/server/src/parsers/__tests__/parsers.test.ts b/apps/server/src/parsers/__tests__/parsers.test.ts index a772be2..e952c48 100644 --- a/apps/server/src/parsers/__tests__/parsers.test.ts +++ b/apps/server/src/parsers/__tests__/parsers.test.ts @@ -8,7 +8,7 @@ import { parseSmartArrayFailure, parseSmartOutput } from "../smart-parser.ts"; import { parseDegradedDisks, parseProbeOutput } from "../probe-parser.ts"; import { parseDfOutput } from "../df-parser.ts"; import { isLockedOutput, parseLogTags, splitStructuredOutput, unescapeTagValue } from "../structured-log.ts"; -import { parseParityLine } from "../../config-parser.ts"; +import { parseParityLine } from "../../config/config-parser.ts"; import { readFileSync } from "node:fs"; // Fixtures are the stderr of `snapraid --log ">&2" ` (SnapRAID 14.9) on a demo array. diff --git a/apps/server/src/__tests__/disk-management.test.ts b/apps/server/src/routes/__tests__/disk-management.test.ts similarity index 93% rename from apps/server/src/__tests__/disk-management.test.ts rename to apps/server/src/routes/__tests__/disk-management.test.ts index 4a14501..90a2dbd 100644 --- a/apps/server/src/__tests__/disk-management.test.ts +++ b/apps/server/src/routes/__tests__/disk-management.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test"; import { join } from "node:path"; -import { diskManagementRoutes } from "../routes/disk-management.ts"; +import { diskManagementRoutes } from "../disk-management.ts"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; diff --git a/apps/server/src/__tests__/heal.test.ts b/apps/server/src/routes/__tests__/heal.test.ts similarity index 93% rename from apps/server/src/__tests__/heal.test.ts rename to apps/server/src/routes/__tests__/heal.test.ts index 5fa7c4d..1845559 100644 --- a/apps/server/src/__tests__/heal.test.ts +++ b/apps/server/src/routes/__tests__/heal.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "bun:test"; import type { RunResult, SnapRaidCommand } from "@shared/types.ts"; -import type { JobOutcome } from "../engine/engine.ts"; -import { heal } from "../routes/snapraid.ts"; +import type { JobOutcome } from "../../engine/engine.ts"; +import { heal } from "../snapraid.ts"; const outcome = (command: SnapRaidCommand, result: RunResult): JobOutcome => ({ output: { command, output: "", timestamp: "", exitCode: result === "ok" ? 0 : 1 }, diff --git a/apps/server/src/routes/config-operations.ts b/apps/server/src/routes/config-operations.ts index efac12d..102e414 100644 --- a/apps/server/src/routes/config-operations.ts +++ b/apps/server/src/routes/config-operations.ts @@ -1,8 +1,8 @@ import { Hono } from "hono"; import { join } from "node:path"; -import { parseSnapRaidConfig } from "../config-parser.ts"; -import { resolveFromBase } from "../config.ts"; -import { editSmartIgnore, isSmartIgnoreAttribute, SMART_IGNORE_MAX, SmartIgnoreFullError } from "../smart-ignore.ts"; +import { parseSnapRaidConfig } from "../config/config-parser.ts"; +import { resolveFromBase } from "../config/config.ts"; +import { editSmartIgnore, isSmartIgnoreAttribute, SMART_IGNORE_MAX, SmartIgnoreFullError } from "../smart/smart-ignore.ts"; import { msg } from "@shared/i18n.ts"; import { readFile, writeFile } from "node:fs/promises"; diff --git a/apps/server/src/routes/config.ts b/apps/server/src/routes/config.ts index b2ddd06..f35263b 100644 --- a/apps/server/src/routes/config.ts +++ b/apps/server/src/routes/config.ts @@ -1,12 +1,12 @@ import { Hono } from "hono"; import type { ConfigFileCheck } from "@shared/types.ts"; -import { loadAppConfig, parseSnapRaidConfig, saveAppConfig } from "../config-parser.ts"; -import { BASE_PATH, resolveFromBase, toStoredPath } from "../config.ts"; +import { loadAppConfig, parseSnapRaidConfig, saveAppConfig } from "../config/config-parser.ts"; +import { BASE_PATH, resolveFromBase, toStoredPath } from "../config/config.ts"; import { msg } from "@shared/i18n.ts"; -import { BackupError, createBackup, restoreBackup } from "../backup.ts"; +import { BackupError, createBackup, restoreBackup } from "../config/backup.ts"; import { reloadSchedules } from "./schedules.ts"; import { stat, writeFile } from "node:fs/promises"; -import { isAlreadyExists } from "../fs.ts"; +import { isAlreadyExists } from "../system/fs.ts"; const config = new Hono(); diff --git a/apps/server/src/routes/disk-management.ts b/apps/server/src/routes/disk-management.ts index 1364cec..6003cdc 100644 --- a/apps/server/src/routes/disk-management.ts +++ b/apps/server/src/routes/disk-management.ts @@ -5,8 +5,8 @@ import { parityKeyword, parseParityLine, parseSnapRaidConfig, -} from "../config-parser.ts"; -import { resolveFromBase } from "../config.ts"; +} from "../config/config-parser.ts"; +import { resolveFromBase } from "../config/config.ts"; import { msg } from "@shared/i18n.ts"; import { readFile, writeFile } from "node:fs/promises"; diff --git a/apps/server/src/routes/disk-replacement.ts b/apps/server/src/routes/disk-replacement.ts index f7b9517..e03222e 100644 --- a/apps/server/src/routes/disk-replacement.ts +++ b/apps/server/src/routes/disk-replacement.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import type { ReplacementStep, SnapRaidCommand } from "@shared/types.ts"; -import { resolveFromBase } from "../config.ts"; +import { resolveFromBase } from "../config/config.ts"; import { applyReplacementPath, clearReplacement, @@ -9,7 +9,7 @@ import { recordReplacementStep, requiredDirectory, saveReplacement, -} from "../disk-replacement.ts"; +} from "../disks/disk-replacement.ts"; import type { JobOutcome } from "../engine/engine.ts"; import { msg } from "@shared/i18n.ts"; import { readFile, stat, writeFile } from "node:fs/promises"; diff --git a/apps/server/src/routes/filesystem.ts b/apps/server/src/routes/filesystem.ts index 075c477..efb1272 100644 --- a/apps/server/src/routes/filesystem.ts +++ b/apps/server/src/routes/filesystem.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { join } from "node:path"; -import { BASE_PATH, resolveFromBase } from "../config.ts"; +import { BASE_PATH, resolveFromBase } from "../config/config.ts"; import { readdir, readFile, writeFile } from "node:fs/promises"; const filesystem = new Hono(); diff --git a/apps/server/src/routes/hardware.ts b/apps/server/src/routes/hardware.ts index bf6a7c6..9f66707 100644 --- a/apps/server/src/routes/hardware.ts +++ b/apps/server/src/routes/hardware.ts @@ -1,12 +1,12 @@ import { Hono } from "hono"; -import { resolveFromBase } from "../config.ts"; -import { parseSnapRaidConfig } from "../config-parser.ts"; -import { getDataDiskUsage, getParityUsage } from "../parity-usage.ts"; -import { withCrcBaseline } from "../smart-baseline.ts"; -import { getSmartHistory, recordSmartHistory } from "../smart-history.ts"; +import { resolveFromBase } from "../config/config.ts"; +import { parseSnapRaidConfig } from "../config/config-parser.ts"; +import { getDataDiskUsage, getParityUsage } from "../disks/parity-usage.ts"; +import { withCrcBaseline } from "../smart/smart-baseline.ts"; +import { getSmartHistory, recordSmartHistory } from "../smart/smart-history.ts"; import { EngineCommandError, EngineUnsupportedError, getEngine } from "../engine/engine.ts"; -import { type PowerAction, setDiskPower } from "../disk-power.ts"; -import { controlSelfTests, readSelfTests } from "../smart-selftest.ts"; +import { type PowerAction, setDiskPower } from "../disks/disk-power.ts"; +import { controlSelfTests, readSelfTests } from "../smart/smart-selftest.ts"; import { msg } from "@shared/i18n.ts"; const hardware = new Hono(); diff --git a/apps/server/src/routes/home-assistant.ts b/apps/server/src/routes/home-assistant.ts index 2592caf..7c00ef8 100644 --- a/apps/server/src/routes/home-assistant.ts +++ b/apps/server/src/routes/home-assistant.ts @@ -7,7 +7,7 @@ import { normalizeHomeAssistantSettings, saveHomeAssistantSettings, testHomeAssistantConnection, -} from "../home-assistant-service.ts"; +} from "../integrations/home-assistant-service.ts"; const homeAssistant = new Hono(); diff --git a/apps/server/src/routes/logs.ts b/apps/server/src/routes/logs.ts index 365dd51..91dbac4 100644 --- a/apps/server/src/routes/logs.ts +++ b/apps/server/src/routes/logs.ts @@ -1,5 +1,5 @@ import { Hono } from "hono"; -import type { LogManager } from "../log-manager.ts"; +import type { LogManager } from "../snapraid/log-manager.ts"; import type { AppConfig } from "@shared/types.ts"; const logs = new Hono(); diff --git a/apps/server/src/routes/maintenance.ts b/apps/server/src/routes/maintenance.ts index 8ccefa1..d881ef6 100644 --- a/apps/server/src/routes/maintenance.ts +++ b/apps/server/src/routes/maintenance.ts @@ -4,9 +4,9 @@ import { loadMaintenanceSettings, normalizeMaintenanceSettings, saveMaintenanceSettings, -} from "../maintenance-settings.ts"; -import { dockerSocket, listContainers, ownContainerId } from "../docker.ts"; -import type { SpindownMonitor } from "../spindown.ts"; +} from "../config/maintenance-settings.ts"; +import { dockerSocket, listContainers, ownContainerId } from "../system/docker.ts"; +import type { SpindownMonitor } from "../disks/spindown.ts"; const maintenance = new Hono(); diff --git a/apps/server/src/routes/mcp.ts b/apps/server/src/routes/mcp.ts index 4bcd445..d1ac734 100644 --- a/apps/server/src/routes/mcp.ts +++ b/apps/server/src/routes/mcp.ts @@ -1,18 +1,18 @@ import { Hono } from "hono"; import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; -import { safeEqual } from "../auth.ts"; -import { snapshotConfigs } from "../array-snapshot.ts"; -import { loadAppConfig, parseSnapRaidConfig } from "../config-parser.ts"; -import { BASE_PATH, resolveFromBase } from "../config.ts"; -import { APP_VERSION } from "../version.ts"; +import { safeEqual } from "../auth/auth.ts"; +import { snapshotConfigs } from "../integrations/array-snapshot.ts"; +import { loadAppConfig, parseSnapRaidConfig } from "../config/config-parser.ts"; +import { BASE_PATH, resolveFromBase } from "../config/config.ts"; +import { APP_VERSION } from "../system/version.ts"; import { getEngine } from "../engine/engine.ts"; -import type { LogManager } from "../log-manager.ts"; -import { loadMaintenanceSettings } from "../maintenance-settings.ts"; -import { createMcpServer, DOCS_DIR } from "../mcp.ts"; -import type { Scheduler } from "../scheduler.ts"; -import { createArray, listMountCandidates, missingSetupPaths } from "../setup.ts"; -import { rememberStatus } from "../status-cache.ts"; -import { getUsageHistory, recordUsage } from "../usage-history.ts"; +import type { LogManager } from "../snapraid/log-manager.ts"; +import { loadMaintenanceSettings } from "../config/maintenance-settings.ts"; +import { createMcpServer, DOCS_DIR } from "../integrations/mcp.ts"; +import type { Scheduler } from "../schedule/scheduler.ts"; +import { createArray, listMountCandidates, missingSetupPaths } from "../config/setup.ts"; +import { rememberStatus } from "../snapraid/status-cache.ts"; +import { getUsageHistory, recordUsage } from "../snapraid/usage-history.ts"; import { startExternalJob } from "./snapraid.ts"; // Log manager and scheduler will be injected diff --git a/apps/server/src/routes/metrics.ts b/apps/server/src/routes/metrics.ts index 65479b2..90bd726 100644 --- a/apps/server/src/routes/metrics.ts +++ b/apps/server/src/routes/metrics.ts @@ -1,8 +1,8 @@ import { Hono } from "hono"; -import { snapshotConfigs, snapshotSchedules } from "../array-snapshot.ts"; +import { snapshotConfigs, snapshotSchedules } from "../integrations/array-snapshot.ts"; import { getEngine } from "../engine/engine.ts"; -import { loadMaintenanceSettings } from "../maintenance-settings.ts"; -import { renderMetrics } from "../metrics.ts"; +import { loadMaintenanceSettings } from "../config/maintenance-settings.ts"; +import { renderMetrics } from "../integrations/metrics.ts"; const metrics = new Hono(); diff --git a/apps/server/src/routes/notifications.ts b/apps/server/src/routes/notifications.ts index c07bbfb..3b2dded 100644 --- a/apps/server/src/routes/notifications.ts +++ b/apps/server/src/routes/notifications.ts @@ -8,7 +8,7 @@ import { resolveSecrets, saveNotificationSettings, validateNotificationSettings, -} from "../notifications.ts"; +} from "../notifications/notifications.ts"; import { msg } from "@shared/i18n.ts"; const notifications = new Hono(); diff --git a/apps/server/src/routes/reports.ts b/apps/server/src/routes/reports.ts index 8c89b21..c8da67e 100644 --- a/apps/server/src/routes/reports.ts +++ b/apps/server/src/routes/reports.ts @@ -1,9 +1,9 @@ import { Hono } from "hono"; -import type { SnapRaidRunner } from "../snapraid-runner.ts"; -import { resolveFromBase } from "../config.ts"; +import type { SnapRaidRunner } from "../snapraid/snapraid-runner.ts"; +import { resolveFromBase } from "../config/config.ts"; import { getEngine } from "../engine/engine.ts"; -import { parseSnapRaidConfig } from "../config-parser.ts"; -import { changedPaths, deleteDuplicates, insideDisk, markGone, MAX_DUPLICATE_DELETIONS } from "../duplicates.ts"; +import { parseSnapRaidConfig } from "../config/config-parser.ts"; +import { changedPaths, deleteDuplicates, insideDisk, markGone, MAX_DUPLICATE_DELETIONS } from "../snapraid/duplicates.ts"; import type { DiffReport, DuplicateDeletion } from "@shared/types.ts"; import { msg } from "@shared/i18n.ts"; import { stat } from "node:fs/promises"; diff --git a/apps/server/src/routes/schedules.ts b/apps/server/src/routes/schedules.ts index 8b5bf6c..afbc3a3 100644 --- a/apps/server/src/routes/schedules.ts +++ b/apps/server/src/routes/schedules.ts @@ -1,5 +1,5 @@ import { Hono } from "hono"; -import type { Scheduler } from "../scheduler.ts"; +import type { Scheduler } from "../schedule/scheduler.ts"; import { msg } from "@shared/i18n.ts"; const schedules = new Hono(); diff --git a/apps/server/src/routes/setup.ts b/apps/server/src/routes/setup.ts index dc63ec4..0af9fb9 100644 --- a/apps/server/src/routes/setup.ts +++ b/apps/server/src/routes/setup.ts @@ -1,7 +1,7 @@ import { Hono } from "hono"; import type { ArraySetup } from "@shared/array-setup.ts"; import { msg } from "@shared/i18n.ts"; -import { createArray, listMountCandidates } from "../setup.ts"; +import { createArray, listMountCandidates } from "../config/setup.ts"; const setup = new Hono(); diff --git a/apps/server/src/routes/snapraid.ts b/apps/server/src/routes/snapraid.ts index 7fd769f..235252f 100644 --- a/apps/server/src/routes/snapraid.ts +++ b/apps/server/src/routes/snapraid.ts @@ -1,29 +1,29 @@ import { Hono } from "hono"; -import { parseSnapRaidConfig } from "../config-parser.ts"; -import { createSnapRaidRunner } from "../snapraid-runner.ts"; -import type { LogManager } from "../log-manager.ts"; +import { parseSnapRaidConfig } from "../config/config-parser.ts"; +import { createSnapRaidRunner } from "../snapraid/snapraid-runner.ts"; +import type { LogManager } from "../snapraid/log-manager.ts"; import type { RestoreFile, SnapRaidCommand } from "@shared/types.ts"; -import { snapraidCommand, resolveFromBase } from "../config.ts"; +import { snapraidCommand, resolveFromBase } from "../config/config.ts"; import {diskManagementRoutes} from "./disk-management.ts"; -import { DiskRemovalError, ensureEmptyDir, finalizeDataDiskRemoval, prepareDataDiskRemoval } from "../disk-removal.ts"; +import { DiskRemovalError, ensureEmptyDir, finalizeDataDiskRemoval, prepareDataDiskRemoval } from "../disks/disk-removal.ts"; import {configOperationsRoutes} from "./config-operations.ts"; import {hardwareRoutes} from "./hardware.ts"; import { setReportsRunner, reportsRoutes } from "./reports.ts"; -import { getUsageHistory, recordUsage } from "../usage-history.ts"; -import { rememberStatus } from "../status-cache.ts"; +import { getUsageHistory, recordUsage } from "../snapraid/usage-history.ts"; +import { rememberStatus } from "../snapraid/status-cache.ts"; import { parseStatusOutput } from "../parsers/status-parser.ts"; import { parseCheckOutput } from "../parsers/check-parser.ts"; import { createDiskReplacementRoutes } from "./disk-replacement.ts"; -import { notifyManualRun, notifySkipped } from "../notification-events.ts"; -import { checkSyncGuard } from "../scheduler.ts"; -import { isSuccessful } from "../run-report.ts"; -import { MAX_RESTORE_FILES, restoreFiles } from "../recovery.ts"; -import { findDiskIssues } from "../disk-check.ts"; +import { notifyManualRun, notifySkipped } from "../notifications/notification-events.ts"; +import { checkSyncGuard } from "../schedule/scheduler.ts"; +import { isSuccessful } from "../snapraid/run-report.ts"; +import { MAX_RESTORE_FILES, restoreFiles } from "../snapraid/recovery.ts"; +import { findDiskIssues } from "../disks/disk-check.ts"; import { msg } from "@shared/i18n.ts"; import { EngineBusyError, getEngine, type JobOutcome } from "../engine/engine.ts"; import { readFile, writeFile } from "node:fs/promises"; -import { remove } from "../fs.ts"; -import { runCommand } from "../command.ts"; +import { remove } from "../system/fs.ts"; +import { runCommand } from "../system/command.ts"; const snapraid = new Hono(); diff --git a/apps/server/src/__tests__/scheduler.test.ts b/apps/server/src/schedule/__tests__/scheduler.test.ts similarity index 97% rename from apps/server/src/__tests__/scheduler.test.ts rename to apps/server/src/schedule/__tests__/scheduler.test.ts index 339f056..ee53096 100644 --- a/apps/server/src/__tests__/scheduler.test.ts +++ b/apps/server/src/schedule/__tests__/scheduler.test.ts @@ -1,9 +1,9 @@ import { expect, test } from "bun:test"; -import { assertRejects } from "./assert-error.ts"; +import { assertRejects } from "../../__tests__/assert-error.ts"; import { join } from "node:path"; import type { Schedule, ScheduleConfig } from "@shared/types.ts"; import { createScheduler, executeScheduledCommand } from "../scheduler.ts"; -import { createFakeEngine, type FakeEngineOptions } from "./fake-engine.ts"; +import { createFakeEngine, type FakeEngineOptions } from "../../engine/__tests__/fake-engine.ts"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; diff --git a/apps/server/src/scheduler.ts b/apps/server/src/schedule/scheduler.ts similarity index 95% rename from apps/server/src/scheduler.ts rename to apps/server/src/schedule/scheduler.ts index d41affd..f05626e 100644 --- a/apps/server/src/scheduler.ts +++ b/apps/server/src/schedule/scheduler.ts @@ -7,19 +7,19 @@ import type { ScheduleStepOutcome, SnapRaidCommand, } from "@shared/types.ts"; -import type { SnapRaidEngine } from "./engine/engine.ts"; -import { existsSync } from "./fs.ts"; -import { resolveFromBase } from "./config.ts"; -import { failedReport, isSuccessful, type RunReport } from "./run-report.ts"; -import { isReplacementInProgress } from "./disk-replacement.ts"; -import { withCrcBaseline } from "./smart-baseline.ts"; -import { recordSmartHistory } from "./smart-history.ts"; -import { heartbeatAfterRun, notifyRun, notifySkipped, notifySmart } from "./notification-events.ts"; +import type { SnapRaidEngine } from "../engine/engine.ts"; +import { existsSync } from "../system/fs.ts"; +import { resolveFromBase } from "../config/config.ts"; +import { failedReport, isSuccessful, type RunReport } from "../snapraid/run-report.ts"; +import { isReplacementInProgress } from "../disks/disk-replacement.ts"; +import { withCrcBaseline } from "../smart/smart-baseline.ts"; +import { recordSmartHistory } from "../smart/smart-history.ts"; +import { heartbeatAfterRun, notifyRun, notifySkipped, notifySmart } from "../notifications/notification-events.ts"; import { msg } from "@shared/i18n.ts"; -import { blockingIssues } from "./disk-check.ts"; -import { holdContainers } from "./container-pause.ts"; -import { rememberStatus } from "./status-cache.ts"; -import { writeTextFileAtomic } from "./atomic-write.ts"; +import { blockingIssues } from "../disks/disk-check.ts"; +import { holdContainers } from "../system/container-pause.ts"; +import { rememberStatus } from "../snapraid/status-cache.ts"; +import { writeTextFileAtomic } from "../system/atomic-write.ts"; import { readFile } from "node:fs/promises"; // Module-level storage for active jobs diff --git a/apps/server/src/__tests__/smart-baseline.test.ts b/apps/server/src/smart/__tests__/smart-baseline.test.ts similarity index 100% rename from apps/server/src/__tests__/smart-baseline.test.ts rename to apps/server/src/smart/__tests__/smart-baseline.test.ts diff --git a/apps/server/src/__tests__/smart-history.test.ts b/apps/server/src/smart/__tests__/smart-history.test.ts similarity index 100% rename from apps/server/src/__tests__/smart-history.test.ts rename to apps/server/src/smart/__tests__/smart-history.test.ts diff --git a/apps/server/src/__tests__/smart-ignore.test.ts b/apps/server/src/smart/__tests__/smart-ignore.test.ts similarity index 97% rename from apps/server/src/__tests__/smart-ignore.test.ts rename to apps/server/src/smart/__tests__/smart-ignore.test.ts index 2874ea7..b04b970 100644 --- a/apps/server/src/__tests__/smart-ignore.test.ts +++ b/apps/server/src/smart/__tests__/smart-ignore.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { assertThrows } from "./assert-error.ts"; +import { assertThrows } from "../../__tests__/assert-error.ts"; import { join } from "node:path"; import { editSmartIgnore, @@ -8,7 +8,7 @@ import { parseSmartIgnoreLine, SmartIgnoreFullError, } from "../smart-ignore.ts"; -import { parseSnapRaidConfig } from "../config-parser.ts"; +import { parseSnapRaidConfig } from "../../config/config-parser.ts"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; diff --git a/apps/server/src/__tests__/smart-selftest.test.ts b/apps/server/src/smart/__tests__/smart-selftest.test.ts similarity index 100% rename from apps/server/src/__tests__/smart-selftest.test.ts rename to apps/server/src/smart/__tests__/smart-selftest.test.ts diff --git a/apps/server/src/smart-baseline.ts b/apps/server/src/smart/smart-baseline.ts similarity index 95% rename from apps/server/src/smart-baseline.ts rename to apps/server/src/smart/smart-baseline.ts index 1a7d429..84a379b 100644 --- a/apps/server/src/smart-baseline.ts +++ b/apps/server/src/smart/smart-baseline.ts @@ -1,8 +1,8 @@ -import { existsSync } from "./fs.ts"; +import { existsSync } from "../system/fs.ts"; import type { SmartDiskInfo } from "@shared/types.ts"; import { CRC_ATTRIBUTE_ID } from "@shared/smart-health.ts"; -import { resolveFromBase } from "./config.ts"; -import { writeTextFileAtomic } from "./atomic-write.ts"; +import { resolveFromBase } from "../config/config.ts"; +import { writeTextFileAtomic } from "../system/atomic-write.ts"; import { readFile } from "node:fs/promises"; const STATE_FILE = "smart-baseline.json"; diff --git a/apps/server/src/smart-history.ts b/apps/server/src/smart/smart-history.ts similarity index 94% rename from apps/server/src/smart-history.ts rename to apps/server/src/smart/smart-history.ts index fc60992..099822b 100644 --- a/apps/server/src/smart-history.ts +++ b/apps/server/src/smart/smart-history.ts @@ -1,7 +1,7 @@ -import { existsSync } from "./fs.ts"; +import { existsSync } from "../system/fs.ts"; import type { SmartDiskInfo, SmartHistoryPoint } from "@shared/types.ts"; -import { resolveFromBase } from "./config.ts"; -import { writeTextFileAtomic } from "./atomic-write.ts"; +import { resolveFromBase } from "../config/config.ts"; +import { writeTextFileAtomic } from "../system/atomic-write.ts"; import { readFile } from "node:fs/promises"; const STATE_FILE = "smart-history.json"; diff --git a/apps/server/src/smart-ignore.ts b/apps/server/src/smart/smart-ignore.ts similarity index 100% rename from apps/server/src/smart-ignore.ts rename to apps/server/src/smart/smart-ignore.ts diff --git a/apps/server/src/smart-selftest.ts b/apps/server/src/smart/smart-selftest.ts similarity index 97% rename from apps/server/src/smart-selftest.ts rename to apps/server/src/smart/smart-selftest.ts index d85d2c6..9749508 100644 --- a/apps/server/src/smart-selftest.ts +++ b/apps/server/src/smart/smart-selftest.ts @@ -3,10 +3,10 @@ // runs it on its own; its status and the log of the last tests come from `smartctl -c -l selftest`. // smartctl's JSON output (-j) covers ATA and NVMe disks alike. import type { DeviceInfo, DiskSelfTest, SelfTestLogEntry, SelfTestType } from "@shared/types.ts"; -import { executeSnapraidCommand } from "./executors/command-executor.ts"; -import { parseDevicesOutput } from "./parsers/devices-parser.ts"; -import { runCommand } from "./command.ts"; -import { DEMO_MODE, demoDevices, demoDisks } from "./demo.ts"; +import { executeSnapraidCommand } from "../executors/command-executor.ts"; +import { parseDevicesOutput } from "../parsers/devices-parser.ts"; +import { runCommand } from "../system/command.ts"; +import { DEMO_MODE, demoDevices, demoDisks } from "../system/demo.ts"; // The image installs smartmontools; tests point it to a stand-in const smartctlBin = () => process.env.SMARTCTL_BIN ?? "smartctl"; diff --git a/apps/server/src/__tests__/duplicates.test.ts b/apps/server/src/snapraid/__tests__/duplicates.test.ts similarity index 100% rename from apps/server/src/__tests__/duplicates.test.ts rename to apps/server/src/snapraid/__tests__/duplicates.test.ts diff --git a/apps/server/src/__tests__/force-option.test.ts b/apps/server/src/snapraid/__tests__/force-option.test.ts similarity index 100% rename from apps/server/src/__tests__/force-option.test.ts rename to apps/server/src/snapraid/__tests__/force-option.test.ts diff --git a/apps/server/src/__tests__/log-manager.test.ts b/apps/server/src/snapraid/__tests__/log-manager.test.ts similarity index 98% rename from apps/server/src/__tests__/log-manager.test.ts rename to apps/server/src/snapraid/__tests__/log-manager.test.ts index 3b34c1d..94930b5 100644 --- a/apps/server/src/__tests__/log-manager.test.ts +++ b/apps/server/src/snapraid/__tests__/log-manager.test.ts @@ -55,7 +55,7 @@ test("parseRunResult - diff and dup that found something succeeded", () => { const fixture = (name: string) => - readFileSync(new URL(`../parsers/__tests__/fixtures/${name}`, import.meta.url), "utf8"); + readFileSync(new URL(`../../parsers/__tests__/fixtures/${name}`, import.meta.url), "utf8"); // The touch log of issue #48 ends with the saved content file, the smart log with the array failure probability const TOUCH_LOG = fixture("touch.log"); diff --git a/apps/server/src/__tests__/recovery.test.ts b/apps/server/src/snapraid/__tests__/recovery.test.ts similarity index 97% rename from apps/server/src/__tests__/recovery.test.ts rename to apps/server/src/snapraid/__tests__/recovery.test.ts index 5b60715..5e7bd0a 100644 --- a/apps/server/src/__tests__/recovery.test.ts +++ b/apps/server/src/snapraid/__tests__/recovery.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test"; import type { RunResult, SnapRaidCommand } from "@shared/types.ts"; -import type { JobOutcome } from "../engine/engine.ts"; +import type { JobOutcome } from "../../engine/engine.ts"; import { exactFilter, restoreFiles, restoreRuns } from "../recovery.ts"; test("exactFilter - anchored at the disk root, wildcards taken literally", () => { diff --git a/apps/server/src/__tests__/restore-ownership.test.ts b/apps/server/src/snapraid/__tests__/restore-ownership.test.ts similarity index 99% rename from apps/server/src/__tests__/restore-ownership.test.ts rename to apps/server/src/snapraid/__tests__/restore-ownership.test.ts index 90f2e74..c12ab75 100644 --- a/apps/server/src/__tests__/restore-ownership.test.ts +++ b/apps/server/src/snapraid/__tests__/restore-ownership.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { adoptFolderOwnership, fileMode, folderMode, recreatedFiles } from "../restore-ownership.ts"; import { chmod, chown, mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { writeTextFile } from "../fs.ts"; +import { writeTextFile } from "../../system/fs.ts"; // From a real `snapraid fix`: a/two was changed (fixed in place), a/one and a/we:ird were deleted const FIX_LOG = [ diff --git a/apps/server/src/__tests__/run-result.test.ts b/apps/server/src/snapraid/__tests__/run-result.test.ts similarity index 97% rename from apps/server/src/__tests__/run-result.test.ts rename to apps/server/src/snapraid/__tests__/run-result.test.ts index 464eb5c..d72944f 100644 --- a/apps/server/src/__tests__/run-result.test.ts +++ b/apps/server/src/snapraid/__tests__/run-result.test.ts @@ -4,7 +4,7 @@ import type { RunResult } from "@shared/types.ts"; import { readFileSync } from "node:fs"; const fixture = (name: string) => - readFileSync(new URL(`../parsers/__tests__/fixtures/${name}`, import.meta.url), "utf8"); + readFileSync(new URL(`../../parsers/__tests__/fixtures/${name}`, import.meta.url), "utf8"); const lines = (...tags: string[]) => tags.join("\n"); diff --git a/apps/server/src/__tests__/usage-history.test.ts b/apps/server/src/snapraid/__tests__/usage-history.test.ts similarity index 100% rename from apps/server/src/__tests__/usage-history.test.ts rename to apps/server/src/snapraid/__tests__/usage-history.test.ts diff --git a/apps/server/src/duplicates.ts b/apps/server/src/snapraid/duplicates.ts similarity index 98% rename from apps/server/src/duplicates.ts rename to apps/server/src/snapraid/duplicates.ts index 5f16b58..baabfc7 100644 --- a/apps/server/src/duplicates.ts +++ b/apps/server/src/snapraid/duplicates.ts @@ -5,7 +5,7 @@ import { join, normalize, sep } from "node:path"; import type { DiffReport, DuplicateDeletion, DuplicateSkip, DupReport } from "@shared/types.ts"; import { lstat } from "node:fs/promises"; -import { remove as removeFile } from "./fs.ts"; +import { remove as removeFile } from "../system/fs.ts"; // One request cleans up this many copies at most export const MAX_DUPLICATE_DELETIONS = 5000; diff --git a/apps/server/src/log-manager.ts b/apps/server/src/snapraid/log-manager.ts similarity index 99% rename from apps/server/src/log-manager.ts rename to apps/server/src/snapraid/log-manager.ts index bc30d8d..c516b14 100644 --- a/apps/server/src/log-manager.ts +++ b/apps/server/src/snapraid/log-manager.ts @@ -4,7 +4,7 @@ import { join, resolve } from "node:path"; import type { SnapRaidCommand, LogFile, LastRun, RunResult } from "@shared/types.ts"; import { detectForceOption } from "@shared/force-option.ts"; import { parseRunResult } from "@shared/run-result.ts"; -import { isAlreadyExists, isNotFound, remove } from "./fs.ts"; +import { isAlreadyExists, isNotFound, remove } from "../system/fs.ts"; export interface LogManager { ensureLogDirectory(): Promise; diff --git a/apps/server/src/recovery.ts b/apps/server/src/snapraid/recovery.ts similarity index 97% rename from apps/server/src/recovery.ts rename to apps/server/src/snapraid/recovery.ts index 16f1504..b9f2b9f 100644 --- a/apps/server/src/recovery.ts +++ b/apps/server/src/snapraid/recovery.ts @@ -3,7 +3,7 @@ // both, until the next sync records the new state. import type { RestoreFile, SnapRaidCommand } from "@shared/types.ts"; import { isSuccessful } from "./run-report.ts"; -import type { JobOutcome } from "./engine/engine.ts"; +import type { JobOutcome } from "../engine/engine.ts"; // More filters make the command line long and gain little, "all missing" (-m) covers the rest export const MAX_RESTORE_FILES = 1000; diff --git a/apps/server/src/restore-ownership.ts b/apps/server/src/snapraid/restore-ownership.ts similarity index 97% rename from apps/server/src/restore-ownership.ts rename to apps/server/src/snapraid/restore-ownership.ts index 63ec73a..6e93cf5 100644 --- a/apps/server/src/restore-ownership.ts +++ b/apps/server/src/snapraid/restore-ownership.ts @@ -7,9 +7,9 @@ import type { Stats } from "node:fs"; import { chmod, chown, lstat } from "node:fs/promises"; import { dirname, join } from "node:path"; import { msg } from "@shared/i18n.ts"; -import { parseSnapRaidConfig } from "./config-parser.ts"; -import { type SnapRaidEngine, wrapJobs } from "./engine/engine.ts"; -import { parseLogTags, unescapeTagValue } from "./parsers/structured-log.ts"; +import { parseSnapRaidConfig } from "../config/config-parser.ts"; +import { type SnapRaidEngine, wrapJobs } from "../engine/engine.ts"; +import { parseLogTags, unescapeTagValue } from "../parsers/structured-log.ts"; export interface RestoredFile { disk: string; diff --git a/apps/server/src/run-report.ts b/apps/server/src/snapraid/run-report.ts similarity index 97% rename from apps/server/src/run-report.ts rename to apps/server/src/snapraid/run-report.ts index f39d6f2..5074065 100644 --- a/apps/server/src/run-report.ts +++ b/apps/server/src/snapraid/run-report.ts @@ -1,7 +1,7 @@ import { basename } from "node:path"; import type { CommandOutput, RunResult, SnapRaidCommand } from "@shared/types.ts"; import { parseRunResult } from "./log-manager.ts"; -import { collectKeyValues, parseLogTags, toInt } from "./parsers/structured-log.ts"; +import { collectKeyValues, parseLogTags, toInt } from "../parsers/structured-log.ts"; import { readFile } from "node:fs/promises"; /** diff --git a/apps/server/src/snapraid-runner.ts b/apps/server/src/snapraid/snapraid-runner.ts similarity index 85% rename from apps/server/src/snapraid-runner.ts rename to apps/server/src/snapraid/snapraid-runner.ts index d0a295a..884605d 100644 --- a/apps/server/src/snapraid-runner.ts +++ b/apps/server/src/snapraid/snapraid-runner.ts @@ -1,11 +1,11 @@ // Reports only the SnapRAID CLI gives, whatever engine runs the jobs (engine/engine.ts): // they read the content file or the devices, snapraid-daemon has no API for them import type { DevicesReport, DupReport, ListReport } from "@shared/types.ts"; -import { executeSnapraidCommand } from "./executors/command-executor.ts"; -import { parseDevicesOutput } from "./parsers/devices-parser.ts"; -import { LIST_ARGS, parseListOutput } from "./parsers/list-parser.ts"; -import { parseDupOutput } from "./parsers/dup-parser.ts"; -import { STRUCTURED_LOG_ARGS, splitStructuredOutput } from "./parsers/structured-log.ts"; +import { executeSnapraidCommand } from "../executors/command-executor.ts"; +import { parseDevicesOutput } from "../parsers/devices-parser.ts"; +import { LIST_ARGS, parseListOutput } from "../parsers/list-parser.ts"; +import { parseDupOutput } from "../parsers/dup-parser.ts"; +import { STRUCTURED_LOG_ARGS, splitStructuredOutput } from "../parsers/structured-log.ts"; /** * Run a SnapRAID command with the structured log on stderr. diff --git a/apps/server/src/status-cache.ts b/apps/server/src/snapraid/status-cache.ts similarity index 100% rename from apps/server/src/status-cache.ts rename to apps/server/src/snapraid/status-cache.ts diff --git a/apps/server/src/usage-history.ts b/apps/server/src/snapraid/usage-history.ts similarity index 92% rename from apps/server/src/usage-history.ts rename to apps/server/src/snapraid/usage-history.ts index 7b6e743..ef6e6c8 100644 --- a/apps/server/src/usage-history.ts +++ b/apps/server/src/snapraid/usage-history.ts @@ -1,7 +1,7 @@ -import { existsSync } from "./fs.ts"; +import { existsSync } from "../system/fs.ts"; import type { SnapRaidStatus, UsagePoint } from "@shared/types.ts"; -import { resolveFromBase } from "./config.ts"; -import { writeTextFileAtomic } from "./atomic-write.ts"; +import { resolveFromBase } from "../config/config.ts"; +import { writeTextFileAtomic } from "../system/atomic-write.ts"; import { readFile } from "node:fs/promises"; const STATE_FILE = "usage-history.json"; diff --git a/apps/server/src/__tests__/atomic-write.test.ts b/apps/server/src/system/__tests__/atomic-write.test.ts similarity index 97% rename from apps/server/src/__tests__/atomic-write.test.ts rename to apps/server/src/system/__tests__/atomic-write.test.ts index 8935e84..9faada4 100644 --- a/apps/server/src/__tests__/atomic-write.test.ts +++ b/apps/server/src/system/__tests__/atomic-write.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { assertRejects } from "./assert-error.ts"; +import { assertRejects } from "../../__tests__/assert-error.ts"; import { readdirSync } from "node:fs"; import { join } from "node:path"; import { writeTextFileAtomic } from "../atomic-write.ts"; diff --git a/apps/server/src/__tests__/demo.test.ts b/apps/server/src/system/__tests__/demo.test.ts similarity index 93% rename from apps/server/src/__tests__/demo.test.ts rename to apps/server/src/system/__tests__/demo.test.ts index 7644857..c561288 100644 --- a/apps/server/src/__tests__/demo.test.ts +++ b/apps/server/src/system/__tests__/demo.test.ts @@ -2,10 +2,10 @@ import { expect, test } from "bun:test"; import type { ParsedSnapRaidConfig } from "@shared/types.ts"; import { demoDataDiskUsage, demoDocker, demoParityUsage, demoStatusLog } from "../demo.ts"; import { inspectContainer, listContainers, pauseContainer, unpauseContainer } from "../docker.ts"; -import { parseStatusOutput } from "../parsers/status-parser.ts"; +import { parseStatusOutput } from "../../parsers/status-parser.ts"; import { readFile } from "node:fs/promises"; -const STATUS_LOG = await readFile(new URL("../parsers/__tests__/fixtures/status.log", import.meta.url), "utf8"); +const STATUS_LOG = await readFile(new URL("../../parsers/__tests__/fixtures/status.log", import.meta.url), "utf8"); // The fixture's array: two data disks and one parity const CONFIG = { diff --git a/apps/server/src/__tests__/docker.test.ts b/apps/server/src/system/__tests__/docker.test.ts similarity index 98% rename from apps/server/src/__tests__/docker.test.ts rename to apps/server/src/system/__tests__/docker.test.ts index 72ee8ed..96c1db6 100644 --- a/apps/server/src/__tests__/docker.test.ts +++ b/apps/server/src/system/__tests__/docker.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { assertRejects } from "./assert-error.ts"; +import { assertRejects } from "../../__tests__/assert-error.ts"; import { containerIdFromMountinfo, type DockerRequest, listContainers, parseHttpResponse } from "../docker.ts"; import { pauseContainers, resumeContainers } from "../container-pause.ts"; diff --git a/apps/server/src/__tests__/version.test.ts b/apps/server/src/system/__tests__/version.test.ts similarity index 100% rename from apps/server/src/__tests__/version.test.ts rename to apps/server/src/system/__tests__/version.test.ts diff --git a/apps/server/src/app-files.ts b/apps/server/src/system/app-files.ts similarity index 94% rename from apps/server/src/app-files.ts rename to apps/server/src/system/app-files.ts index 0176d12..5e4d487 100644 --- a/apps/server/src/app-files.ts +++ b/apps/server/src/system/app-files.ts @@ -3,7 +3,7 @@ // (scripts/build-binary.sh) embeds them at the same paths in its own file system, /$bunfs/root/ const COMPILED = import.meta.url.startsWith("file:///$bunfs/"); -const ROOT = COMPILED ? new URL("file:///$bunfs/root/") : new URL("../../../", import.meta.url); +const ROOT = COMPILED ? new URL("file:///$bunfs/root/") : new URL("../../../../", import.meta.url); /** * A file of the app by its path from the repository's root, e.g. "apps/web/package.json" diff --git a/apps/server/src/atomic-write.ts b/apps/server/src/system/atomic-write.ts similarity index 100% rename from apps/server/src/atomic-write.ts rename to apps/server/src/system/atomic-write.ts diff --git a/apps/server/src/command.ts b/apps/server/src/system/command.ts similarity index 100% rename from apps/server/src/command.ts rename to apps/server/src/system/command.ts diff --git a/apps/server/src/container-pause.ts b/apps/server/src/system/container-pause.ts similarity index 97% rename from apps/server/src/container-pause.ts rename to apps/server/src/system/container-pause.ts index 195171c..adbf820 100644 --- a/apps/server/src/container-pause.ts +++ b/apps/server/src/system/container-pause.ts @@ -4,7 +4,7 @@ import { existsSync, remove } from "./fs.ts"; import type { SnapRaidCommand } from "@shared/types.ts"; import { msg } from "@shared/i18n.ts"; -import { resolveFromBase } from "./config.ts"; +import { resolveFromBase } from "../config/config.ts"; import { dockerSocket, type DockerRequest, @@ -13,7 +13,7 @@ import { pauseContainer, unpauseContainer, } from "./docker.ts"; -import { loadMaintenanceSettings } from "./maintenance-settings.ts"; +import { loadMaintenanceSettings } from "../config/maintenance-settings.ts"; import { writeTextFileAtomic } from "./atomic-write.ts"; import { readFile } from "node:fs/promises"; diff --git a/apps/server/src/demo.ts b/apps/server/src/system/demo.ts similarity index 99% rename from apps/server/src/demo.ts rename to apps/server/src/system/demo.ts index 4cfc11d..51477b9 100644 --- a/apps/server/src/demo.ts +++ b/apps/server/src/system/demo.ts @@ -4,7 +4,7 @@ // The sandbox disks also hold only a few KiB on the host's filesystem: `status` and the // disk usage get the sizes of the demo disks, so the dashboard looks like a real array. import type { DataDiskUsage, DeviceInfo, ParityLevelUsage, ParsedSnapRaidConfig } from "@shared/types.ts"; -import { parseSnapRaidConfig } from "./config-parser.ts"; +import { parseSnapRaidConfig } from "../config/config-parser.ts"; import type { DockerRequest } from "./docker.ts"; export const DEMO_MODE = process.env.SNAPRAID_DEMO === "1"; diff --git a/apps/server/src/docker.ts b/apps/server/src/system/docker.ts similarity index 100% rename from apps/server/src/docker.ts rename to apps/server/src/system/docker.ts diff --git a/apps/server/src/fs.ts b/apps/server/src/system/fs.ts similarity index 100% rename from apps/server/src/fs.ts rename to apps/server/src/system/fs.ts diff --git a/apps/server/src/version.ts b/apps/server/src/system/version.ts similarity index 100% rename from apps/server/src/version.ts rename to apps/server/src/system/version.ts diff --git a/apps/web/src/components/AccountDialog.tsx b/apps/web/src/components/AccountDialog.tsx index 9ccf010..81dd4e5 100644 --- a/apps/web/src/components/AccountDialog.tsx +++ b/apps/web/src/components/AccountDialog.tsx @@ -31,7 +31,7 @@ import { import { Input } from './ui/input' import { Label } from './ui/label' -// The backend's minimum (MIN_PASSWORD_LENGTH in apps/server/src/auth.ts) +// The backend's minimum (MIN_PASSWORD_LENGTH in apps/server/src/auth/auth.ts) const MIN_PASSWORD_LENGTH = 12 const MAX_NAME_LENGTH = 64 diff --git a/apps/web/src/components/RemoveDataDiskWizard.tsx b/apps/web/src/components/RemoveDataDiskWizard.tsx index ce47ef3..5db9413 100644 --- a/apps/web/src/components/RemoveDataDiskWizard.tsx +++ b/apps/web/src/components/RemoveDataDiskWizard.tsx @@ -26,7 +26,7 @@ import { DialogTitle, } from './ui/dialog' -// Must match REMOVAL_DIR in apps/server/src/config-parser.ts +// Must match REMOVAL_DIR in apps/server/src/config/config-parser.ts const REMOVAL_DIR = '.snapraid-removal' const JOB_POLL_MS = 2000 diff --git a/docker/Dockerfile b/docker/Dockerfile index e9a6e9b..e474a64 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -58,9 +58,9 @@ COPY --from=backend-builder /app/tsconfig.base.json /app/tsconfig.base.json COPY --from=backend-builder /app/node_modules /app/node_modules COPY --from=backend-builder /app/apps/server /app/apps/server COPY packages/shared/ /app/packages/shared/ -# The backend loads the frontend's server from here (apps/server/src/frontend.ts) +# The backend loads the frontend's server from here (apps/server/src/http/frontend.ts) COPY --from=frontend-builder /app/apps/web/.output /app/apps/web/.output -# The version, which the backend reads from it (apps/server/src/version.ts) +# The version, which the backend reads from it (apps/server/src/system/version.ts) COPY apps/web/package.json /app/apps/web/package.json # The documentation, which the MCP endpoint offers to AI agents COPY docs/*.md /app/docs/ diff --git a/docs/architecture.md b/docs/architecture.md index e27a64c..32ca653 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -45,7 +45,7 @@ The container runs a single process, the backend (in `/app/apps/server`), under bun src/main.ts ``` -It listens on port 80 (`PORT=80`) and answers `/api` and `/ws` itself. Every other path goes to the frontend: Nitro builds the server-side rendering and the static files (`apps/web/.output/`) with its `standard` preset into one fetch handler, which `apps/server/src/frontend.ts` loads from `apps/web/.output/server/index.mjs`. +It listens on port 80 (`PORT=80`) and answers `/api` and `/ws` itself. Every other path goes to the frontend: Nitro builds the server-side rendering and the static files (`apps/web/.output/`) with its `standard` preset into one fetch handler, which `apps/server/src/http/frontend.ts` loads from `apps/web/.output/server/index.mjs`. ```mermaid flowchart LR @@ -63,13 +63,13 @@ The image sets `SNAPRAID_BASE_PATH=/app/snapraid`, `SNAPRAID_BIN=/usr/local/bin/ ## Standalone binary -`scripts/build-binary.sh` compiles `apps/server/src/bin.ts` with `bun build --compile` into one executable per architecture (`snapraid-ui-linux-x86_64`, `snapraid-ui-linux-aarch64`), which the release attaches to the GitHub release. The binary embeds the Bun runtime, the backend bundled with its packages, and with `--asset` the built frontend (`apps/web/.output/`), `apps/web/package.json` and `docs/*.md`. Inside the binary these files lie under `/$bunfs/root/` at their paths in the repository (`apps/web/…`, `docs/…`); `apps/server/src/app-files.ts` finds them there, and at the same paths from the root in the repository and the image: +`scripts/build-binary.sh` compiles `apps/server/src/bin.ts` with `bun build --compile` into one executable per architecture (`snapraid-ui-linux-x86_64`, `snapraid-ui-linux-aarch64`), which the release attaches to the GitHub release. The binary embeds the Bun runtime, the backend bundled with its packages, and with `--asset` the built frontend (`apps/web/.output/`), `apps/web/package.json` and `docs/*.md`. Inside the binary these files lie under `/$bunfs/root/` at their paths in the repository (`apps/web/…`, `docs/…`); `apps/server/src/system/app-files.ts` finds them there, and at the same paths from the root in the repository and the image: | File | Read by | For | |---|---|---| -| `apps/web/.output/server/index.mjs` | `apps/server/src/frontend.ts` | The UI | -| `apps/web/package.json` | `apps/server/src/version.ts` | The version, for `--version`, Home Assistant and the MCP endpoint; `SNAPRAID_UI_VERSION` overrides it | -| `docs/*.md` | `apps/server/src/mcp.ts` | The documentation the MCP endpoint offers | +| `apps/web/.output/server/index.mjs` | `apps/server/src/http/frontend.ts` | The UI | +| `apps/web/package.json` | `apps/server/src/system/version.ts` | The version, for `--version`, Home Assistant and the MCP endpoint; `SNAPRAID_UI_VERSION` overrides it | +| `docs/*.md` | `apps/server/src/integrations/mcp.ts` | The documentation the MCP endpoint offers | `bin.ts` first imports `bin-cli.ts`, which answers `--version` and `--help` and sets the data directory to `./snapraid` when `SNAPRAID_BASE_PATH` is not set, and then `main.ts`. SnapRAID and `smartctl` are not inside: the binary runs the ones installed on the host. @@ -82,39 +82,40 @@ Middleware, in order: 1. **CORS**: credentials are only accepted from an origin with the same hostname as the request, e.g. the dev frontend on `localhost:3000` calling `localhost:8080`. 2. **Auth** (only when `SNAPRAID_UI_USERNAME` and `SNAPRAID_UI_PASSWORD` are both set): guards `/api/*` and `/ws`, except `/api/auth/*`, `/api/metrics` and `/api/mcp`, which check their own token. A missing or invalid session returns `401 {"error":"Unauthorized"}`. Sessions are HS256-signed JWTs in an HTTP-only `snapraid_session` cookie, see [Security](security.md). `auth.ts` has the routes, `credentials.ts` the password changed in the UI and the passkeys in `auth.json`, `passkeys.ts` WebAuthn with `@simplewebauthn/server`. -Module map: +Module map. `main.ts`, `bin.ts` and `bin-cli.ts` sit in `src/`, every other module in a folder of its domain (`auth/`, `config/`, `disks/`, `engine/`, `executors/`, `http/`, `integrations/`, `notifications/`, `parsers/`, `routes/`, `schedule/`, `smart/`, `snapraid/`, `system/`), with its unit tests in that folder's `__tests__/`: | Module | Responsibility | |---|---| | `main.ts` | App setup, middleware, route mounting, dependency injection, startup | -| `config.ts` | `SNAPRAID_BASE_PATH`, `SNAPRAID_BIN`, `SNAPRAID_EXTRA_ARGS`; `resolveFromBase()` (relative paths resolve against the data directory, absolute ones stay as they are); `snapraidCommand()` | -| `config-parser.ts` | Parses `snapraid.conf` (parity levels incl. split parity, content, data, exclude, pool, `autosave`, `blocksize`); loads and saves `config.json` | -| `auth.ts` | Login, logout, password change, passkey routes, session cookie, lockout, session secret | -| `credentials.ts` | `auth.json`: the password changed in the UI (PBKDF2) and the passkeys, dropped when the environment credentials change | -| `passkeys.ts` | WebAuthn registration and login (`@simplewebauthn/server`): relying party from the Origin, one-time challenges | -| `websocket.ts` | WebSocket client set, `broadcast()`, replay of the running job on connect | +| `auth/auth.ts` | Login, logout, password change, passkey routes, session cookie, lockout, session secret | +| `auth/credentials.ts` | `auth.json`: the password changed in the UI (PBKDF2) and the passkeys, dropped when the environment credentials change | +| `auth/passkeys.ts` | WebAuthn registration and login (`@simplewebauthn/server`): relying party from the Origin, one-time challenges | +| `config/config.ts` | `SNAPRAID_BASE_PATH`, `SNAPRAID_BIN`, `SNAPRAID_EXTRA_ARGS`; `resolveFromBase()` (relative paths resolve against the data directory, absolute ones stay as they are); `snapraidCommand()` | +| `config/config-parser.ts` | Parses `snapraid.conf` (parity levels incl. split parity, content, data, exclude, pool, `autosave`, `blocksize`); loads and saves `config.json` | +| `config/backup.ts` | Export and restore of settings, histories and SnapRAID configs | +| `config/maintenance-settings.ts` | Docker pause and spindown settings | +| `disks/parity-usage.ts` | Size and free space of data and parity disks | +| `disks/disk-replacement.ts`, `disks/disk-removal.ts` | Config rewriting and state for the replace-disk and remove-data-disk wizards | +| `disks/disk-check.ts` | Disks that are missing, empty or on another filesystem than the content file recorded, from the `status` log | +| `disks/spindown.ts` | Watches `/proc/diskstats` and spins idle disks down with `snapraid down` | +| `engine/engine.ts`, `engine/cli-engine.ts` | The [SnapRAID engine](#snapraid-engine) interface and its CLI implementation: jobs, status, diff, SMART, power state | | `executors/command-executor.ts` | Spawns long-running SnapRAID jobs, streams output, tracks the current and last job, follows progress, handles abort | | `executors/log-tail.ts` | Reads new lines that a running process appends to its log file | -| `engine/engine.ts`, `engine/cli-engine.ts` | The [SnapRAID engine](#snapraid-engine) interface and its CLI implementation: jobs, status, diff, SMART, power state | -| `snapraid-runner.ts` | Reports only the CLI gives (`devices`, `list`, `dup`) | +| `http/websocket.ts` | WebSocket client set, `broadcast()`, replay of the running job on connect | +| `notifications/notifications.ts` | Notification settings, secret masking, delivery via SMTP ([Nodemailer](https://nodemailer.com/)), ntfy and webhook | +| `notifications/notification-events.ts` | Builds notifications for runs, skipped schedules and SMART changes, in the configured language | | `parsers/*` | Parsers for SnapRAID's structured log: `status`, `diff`, `check`, `list`, `dup`, `smart`, `probe`, `devices`, progress (`run:pos`) | -| `run-report.ts` | Summarizes a finished run (result, duration, error counts, sync changes) from its log | -| `log-manager.ts` | Log file naming, listing, outcome detection, last run per config, rotation, deletion | -| `scheduler.ts` | Cron schedules (via [Croner](https://github.com/hexagon/croner)), multi-step sync routine, sync guard, skip logic | -| `notifications.ts` | Notification settings, secret masking, delivery via SMTP ([Nodemailer](https://nodemailer.com/)), ntfy and webhook | -| `notification-events.ts` | Builds notifications for runs, skipped schedules and SMART changes, in the configured language | -| `smart-baseline.ts`, `smart-history.ts` | CRC error baseline per disk; daily SMART history | -| `usage-history.ts`, `parity-usage.ts` | Daily array usage history; size and free space of data and parity disks | -| `disk-replacement.ts`, `disk-removal.ts` | Config rewriting and state for the replace-disk and remove-data-disk wizards | -| `backup.ts` | Export and restore of settings, histories and SnapRAID configs | -| `smart-selftest.ts` | SMART self-tests through `smartctl -j` (ATA and NVMe): start, stop, status and log per disk; keeps the spindown away from disks with a running test | -| `recovery.ts`, `restore-ownership.ts` | Restoring files from parity, one `fix` per disk; after every `fix`, files it recreated (from its log) and the folders made for them take the owner and permissions of their folder | -| `duplicates.ts` | Deleting duplicates safely: a copy goes only while it and the copy that stays are as at the last sync (not in the diff, the size `dup` reported); marks copies no longer on their disk | -| `disk-check.ts` | Disks that are missing, empty or on another filesystem than the content file recorded, from the `status` log | -| `maintenance-settings.ts` | Docker pause and spindown settings | -| `docker.ts`, `container-pause.ts` | Docker Engine API over the unix socket; pausing containers for the duration of jobs, resuming leftovers after a restart | -| `spindown.ts` | Watches `/proc/diskstats` and spins idle disks down with `snapraid down` | -| `demo.ts` | The demo sandbox (`SNAPRAID_DEMO=1`): fake `smart`, `probe` and device output, demo disk sizes in `status` and the disk usage, a Docker API with demo containers | +| `schedule/scheduler.ts` | Cron schedules (via [Croner](https://github.com/hexagon/croner)), multi-step sync routine, sync guard, skip logic | +| `smart/smart-baseline.ts`, `smart/smart-history.ts` | CRC error baseline per disk; daily SMART history | +| `smart/smart-selftest.ts` | SMART self-tests through `smartctl -j` (ATA and NVMe): start, stop, status and log per disk; keeps the spindown away from disks with a running test | +| `snapraid/snapraid-runner.ts` | Reports only the CLI gives (`devices`, `list`, `dup`) | +| `snapraid/run-report.ts` | Summarizes a finished run (result, duration, error counts, sync changes) from its log | +| `snapraid/log-manager.ts` | Log file naming, listing, outcome detection, last run per config, rotation, deletion | +| `snapraid/usage-history.ts` | Daily array usage history | +| `snapraid/recovery.ts`, `snapraid/restore-ownership.ts` | Restoring files from parity, one `fix` per disk; after every `fix`, files it recreated (from its log) and the folders made for them take the owner and permissions of their folder | +| `snapraid/duplicates.ts` | Deleting duplicates safely: a copy goes only while it and the copy that stays are as at the last sync (not in the diff, the size `dup` reported); marks copies no longer on their disk | +| `system/docker.ts`, `system/container-pause.ts` | Docker Engine API over the unix socket; pausing containers for the duration of jobs, resuming leftovers after a restart | +| `system/demo.ts` | The demo sandbox (`SNAPRAID_DEMO=1`): fake `smart`, `probe` and device output, demo disk sizes in `status` and the disk usage, a Docker API with demo containers | | `routes/*` | HTTP routes, see the [API reference](#api-reference) | ## Running SnapRAID @@ -174,7 +175,7 @@ The API was last compared with snapraid-daemon **`v2.1`**; the version is in `ap ### Tests -`apps/server/src/__tests__/engine-contract.ts` holds what every engine has to do: a sync succeeds, the status lists the disks, the diff finds a new file, `afterRun` runs while the job is still current, and so on. `engine-contract.test.ts` runs it against the fake engine (`__tests__/fake-engine.ts`) always, against the CLI engine with a real SnapRAID binary, and against the daemon engine with a running snapraid-daemon (see [Development](development.md#tests)). +`apps/server/src/engine/__tests__/engine-contract.ts` holds what every engine has to do: a sync succeeds, the status lists the disks, the diff finds a new file, `afterRun` runs while the job is still current, and so on. `engine-contract.test.ts` runs it against the fake engine (`__tests__/fake-engine.ts`) always, against the CLI engine with a real SnapRAID binary, and against the daemon engine with a running snapraid-daemon (see [Development](development.md#tests)). Without a daemon, `daemon-engine.test.ts` runs the daemon engine against a stand-in that answers like snapraid-daemon 2.0: queuing and following a task, progress, abort, the fallback, auth and errors, and the mapping of its JSON to the UI's types. `engine-settings.test.ts` covers the settings and which engine runs which config, `scheduler.test.ts` the scheduler against the fake engine. @@ -250,7 +251,7 @@ Everything lives in the data directory, `SNAPRAID_BASE_PATH`. In the image that | `auth.json` | Password changed in the UI (hash) and passkeys (only with login enabled) | | `logs/*.log` | One structured log per job (directory set in `config.json`) | -Writes go through `node:fs`: the JSON state files through `writeTextFileAtomic` (`apps/server/src/atomic-write.ts`, a temporary file renamed over the old one), other writes (the SnapRAID configs among them) are plain `writeFile` calls. Most files are read again whenever they are needed. Two things are read only at startup: the log directory and retention settings from `config.json`, and the cron timers, which are rebuilt from `schedules.json` on start, on changes through the API and after a restore. See [Configuration](configuration.md) for details on each file. +Writes go through `node:fs`: the JSON state files through `writeTextFileAtomic` (`apps/server/src/system/atomic-write.ts`, a temporary file renamed over the old one), other writes (the SnapRAID configs among them) are plain `writeFile` calls. Most files are read again whenever they are needed. Two things are read only at startup: the log directory and retention settings from `config.json`, and the cron timers, which are rebuilt from `schedules.json` on start, on changes through the API and after a restore. See [Configuration](configuration.md) for details on each file. ## Frontend @@ -275,7 +276,7 @@ UI texts live in `apps/web/messages/{en,de,it}.json` and are compiled by [Paragl The backend does not know the viewer's language. For errors a user can run into, it sends a marked key instead of text: `msg("server_error_job_running")` from `packages/shared/src/i18n.ts` produces `\u0002["server_error_job_running",{}]\u0003`. The frontend (`apps/web/src/lib/i18n.ts`, `localizeServer()`) replaces such markers with the translated message; plain text, such as SnapRAID's own output, passes through unchanged. Some validation errors for malformed requests are plain English strings. -Notifications are the exception: they leave the server by e-mail, ntfy or webhook in the language set in the notification settings (`apps/server/src/notification-events.ts`). +Notifications are the exception: they leave the server by e-mail, ntfy or webhook in the language set in the notification settings (`apps/server/src/notifications/notification-events.ts`). See [Development](development.md#translations) for how to add or change texts. diff --git a/docs/development.md b/docs/development.md index 63da7ee..5a463dc 100644 --- a/docs/development.md +++ b/docs/development.md @@ -59,7 +59,7 @@ pkill -f "vite dev" - runs sync, scrub, touch and sync again with their logs, then adds, changes, moves and deletes files and runs `diff` and `status`, so that every page has something to show; - gives the sandbox a past with `apps/server/scripts/seed-demo.ts` (run with Bun in `apps/server/`, whose packages it uses): the job logs move back in time (last sync 10 hours ago, scrub yesterday), `usage-history.json` gets two months of disk usage with one disk filling up, `schedules.json` a nightly sync with touch and scrub, a weekly scrub and a SMART check, and `maintenance.json` Docker pause and spindown. -All sandbox disks are directories on one filesystem, which SnapRAID only accepts with `--test-skip-device`. They hold a few KiB and have no SMART data, power state or Docker next to them, so `SNAPRAID_DEMO=1` makes the backend answer like a real home server (`apps/server/src/demo.ts`): +All sandbox disks are directories on one filesystem, which SnapRAID only accepts with `--test-skip-device`. They hold a few KiB and have no SMART data, power state or Docker next to them, so `SNAPRAID_DEMO=1` makes the backend answer like a real home server (`apps/server/src/system/demo.ts`): - `smart` and `probe`: generated results in the real structured log format: a healthy HDD, an SSD, an HDD with growing reallocated sectors, a sleeping 16 TB disk and an 18 TB parity disk; - `status`: the real log of the sandbox, with the sizes, file counts and fragmentation of those disks and a scrub that keeps up (64 % verified, the oldest block 17 days old) instead of the sandbox's figures; @@ -88,14 +88,24 @@ apps/server/ the backend src/ main.ts app setup, middleware, startup bin.ts entry point of the standalone binary; bin-cli.ts: --version, --help - app-files.ts where the frontend, its package.json and docs/ are (repository, image, binary) - fs.ts, command.ts file and process helpers (node:fs, Bun.spawn) - frontend.ts loads the built frontend, which the backend serves - routes/ HTTP routes per area + auth/ login, passkeys, the credentials in auth.json + config/ data directory and SnapRAID binary, config.json and snapraid.conf, setup + wizard, backup/restore, maintenance settings + disks/ disk check, power, spindown, removal and replacement wizards, disk usage + engine/ the SnapRAID engine: CLI and snapraid-daemon executors/ job runner (spawn, stream, progress, abort) - parsers/ structured log parsers, tests and real SnapRAID fixtures - __tests__/ backend unit tests - *.ts auth, scheduler, notifications, log manager, histories, wizards, backup, demo + http/ the built frontend the backend serves, the WebSocket + integrations/ array snapshot, Prometheus metrics, MCP server, Home Assistant over MQTT + notifications/ channels (SMTP, ntfy, webhook) and the notifications for runs and SMART + parsers/ structured log parsers + routes/ HTTP routes per area + schedule/ cron schedules and the sync routine + smart/ SMART baseline, history, smartignore, self-tests + snapraid/ run reports, logs, status cache, usage history, recovery, duplicates, + reports only the CLI gives + system/ file and process helpers, atomic writes, app files, version, Docker and + the container pause, the demo stand-ins + */__tests__/ unit tests next to the folder they test; __tests__/ holds assert-error.ts apps/web/ the frontend messages/ UI texts: en.json, de.json, it.json project.inlang/ Paraglide/inlang project settings @@ -224,7 +234,7 @@ Biome (`apps/web/biome.json`) uses the recommended rules, 2-space indentation, s The UI is available in English, German and Italian. All texts are in `apps/web/messages/{en,de,it}.json` in the inlang message format (`@inlang/plugin-message-format`); `en` is the base locale (`apps/web/project.inlang/settings.json`). Placeholders are written as `{name}`. - **Frontend:** import the messages as `import * as m from '../paraglide/messages'` (relative path) and call `m.key({ … })`. -- **Backend:** for errors a user can see, return `msg("server_error_…", { … })` from `@shared/i18n.ts` (`packages/shared/src/i18n.ts`) instead of text. The frontend renders the key in the UI language with `localizeServer()`. Notification texts are not in the message files; they are in `apps/server/src/notification-events.ts` and `apps/server/src/routes/notifications.ts`, one block per language. +- **Backend:** for errors a user can see, return `msg("server_error_…", { … })` from `@shared/i18n.ts` (`packages/shared/src/i18n.ts`) instead of text. The frontend renders the key in the UI language with `localizeServer()`. Notification texts are not in the message files; they are in `apps/server/src/notifications/notification-events.ts` and `apps/server/src/routes/notifications.ts`, one block per language. To add, change or remove a key in all three files at once, use the helper script from `apps/web/`: diff --git a/packages/shared/src/i18n.ts b/packages/shared/src/i18n.ts index 5133f32..fddae23 100644 --- a/packages/shared/src/i18n.ts +++ b/packages/shared/src/i18n.ts @@ -5,7 +5,7 @@ // SnapRAID's own output, passes through unchanged. // // Notifications are the exception: they leave the server by mail, ntfy or -// webhook in the language set for them (apps/server/src/notification-events.ts). +// webhook in the language set for them (apps/server/src/notifications/notification-events.ts). export type MsgInputs = Record; diff --git a/scripts/build-binary.sh b/scripts/build-binary.sh index a05f62c..50c3b59 100755 --- a/scripts/build-binary.sh +++ b/scripts/build-binary.sh @@ -27,7 +27,7 @@ fi # The files the backend reads at runtime, laid out as in the repository: --asset embeds a # directory under its own name, so the binary has them at /$bunfs/root/apps/web/ and -# /$bunfs/root/docs/ (apps/server/src/app-files.ts). The documentation as the MCP endpoint offers +# /$bunfs/root/docs/ (apps/server/src/system/app-files.ts). The documentation as the MCP endpoint offers # it: the Markdown pages without the pictures. stage="$(mktemp -d)" trap 'rm -rf "$stage"' EXIT