Architecture¶
This page explains how SnapRAID UI is put together: the processes in the container, the backend modules, how SnapRAID is run and its output parsed, where state is stored, and how translations flow. It ends with a reference of the REST API and the WebSocket messages.
It is written for contributors and for anyone who wants to script against the backend. For setting up a development environment, see Development.
- Overview
- Container layout
- Backend
- Running SnapRAID
- Structured log parsing
- Job runner and live output
- Scheduler
- Storage
- Frontend
- Internationalization
- API reference
- WebSocket
Overview¶
SnapRAID UI has three parts:
| Part | Technology | Location |
|---|---|---|
| Backend | Deno + Hono, REST API and WebSocket on port 8080 | backend/src/ |
| Frontend | React 19, TanStack Start / Router / Query, Tailwind CSS, shadcn/ui, Paraglide JS; server-rendered by Nitro on Node.js | frontend/src/ |
| Shared code | Types and logic used by both sides (types, i18n markers, SMART assessment, fill-up forecast, --force-* detection) |
shared/ |
There is no database. All state lives in JSON files and log files in the data directory (SNAPRAID_BASE_PATH), next to your snapraid.conf files. The backend runs the snapraid binary as a child process and reads its structured log (available since SnapRAID 14.0) instead of scraping the human-readable console output.
Container layout¶
The Docker image (docker/Dockerfile) is a multi-stage build:
- snapraid-builder (Debian bookworm-slim) downloads the pinned SnapRAID release (
ARG SNAPRAID_VERSION=14.10) and compiles it. - backend-builder (
denoland/deno:2.5.6) installs the backend dependencies and warms the Deno module cache. - frontend-builder (
node:22-bookworm-slim) runsnpm ciandnpm run build, which producesfrontend/.output/. - The final image is based on
denoland/deno:2.5.6, addsnginx,supervisor,curlandsmartmontools(SnapRAID callssmartctlforsmartandprobe), and copies in the SnapRAID binary, the Node.js binary, the backend with its module cache and the built frontend.
Inside the container, Supervisor (docker/supervisord.conf) runs three programs, and docker/entrypoint.sh simply starts supervisord:
| Program | Command | Listens on |
|---|---|---|
nginx |
nginx -g "daemon off;" |
80 |
frontend |
node .output/server/index.mjs |
3000 |
backend |
deno run --allow-net --allow-read --allow-write --allow-run --allow-env --allow-sys=networkInterfaces,hostname src/main.ts |
8080 |
flowchart LR
B[Browser] -->|":80"| N[nginx]
N -->|"/"| F["frontend (Node, SSR) :3000"]
N -->|"/api"| A["backend (Deno + Hono) :8080"]
N -->|"/ws"| A
A -->|child process| S[snapraid]
S -->|smartctl| D[(disks)]
A <--> V[("/app/snapraid<br/>data directory")]
Nginx (docker/nginx.conf) routes / to the frontend, /api and /ws to the backend, passes WebSocket upgrades through, and sets X-Real-IP, X-Forwarded-For and X-Forwarded-Proto. The backend uses X-Real-IP to tell clients apart for the login lockout and X-Forwarded-Proto to decide whether the session cookie gets the Secure flag (see Security).
The image sets SNAPRAID_BASE_PATH=/app/snapraid and SNAPRAID_BIN=/usr/local/bin/snapraid, declares /app/snapraid as a volume, and has a health check that requests http://localhost/ through nginx. See Installation and Configuration for how to run it.
Backend¶
backend/src/main.ts builds the Hono app, wires the modules together and starts Deno.serve on 0.0.0.0:8080. On start it loads config.json (creating a default one if it is missing), creates the log directory, loads the schedules and runs one log rotation.
Middleware, in order:
- CORS: credentials are only accepted from an origin with the same hostname as the request, e.g. the dev frontend on
localhost:3000callinglocalhost:8080. - Auth (only when
SNAPRAID_UI_USERNAMEandSNAPRAID_UI_PASSWORDare both set): guards/api/*and/ws, except/api/auth/*and/api/metrics, which checks its own token. A missing or invalid session returns401 {"error":"Unauthorized"}. Sessions are HS256-signed JWTs in an HTTP-onlysnapraid_sessioncookie, see Security.
Module map:
| 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, session cookie, lockout, session secret |
websocket.ts |
WebSocket client set, broadcast(), replay of the running job on connect |
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 interface and its CLI implementation: jobs, status, diff, SMART, power state |
snapraid-runner.ts |
Reports only the CLI gives (devices, list, dup) |
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), multi-step sync routine, sync guard, skip logic |
notifications.ts |
Notification settings, secret masking, delivery via SMTP (Nodemailer), 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 |
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 |
routes/* |
HTTP routes, see the API reference |
Running SnapRAID¶
Every SnapRAID call goes through snapraidCommand() in config.ts: it runs SNAPRAID_BIN (default snapraid, in the image /usr/local/bin/snapraid) with the arguments from SNAPRAID_EXTRA_ARGS prepended. There are two ways a command runs:
- Jobs (
POST /api/snapraid/execute, the wizards, the scheduler) run through the engine'srunJob, in the CLI engine the command executor. Only one job runs at a time: a second start returns409. The executor runssnapraid <command> -c <config> -l <logs>/<command>-YYYYMMDD-HHMMSS.log [--gui] [args…].--guiis added forsync,scrub,checkandfix, which makes SnapRAID write its progress (run:postags) into the log instead of drawing a progress bar on the console. The log file name uses UTC. - Reports (
status,diff,list,dup,smart,probe,devices,validate) run directly and return their result in the HTTP response. They add--log ">&2", so the structured log goes to stderr while the human-readable report stays on stdout.GET /api/snapraid/statusrefuses to run while a job is running (SnapRAID's lock would make it fail) and also reports409when another SnapRAID process, e.g. a host cron job, holds the lock.
Aborting sends SIGINT, which SnapRAID treats like Ctrl+C: it stops at the next block and saves its state. The job stays current until the process has exited.
When a job fails, the executor checks its output for SnapRAID's suggestion 'snapraid --force-zero|empty|uuid …' (shared/force-option.ts) and reports it as forceOption, so the UI can offer a confirmed retry. See Usage for the user side.
SnapRAID engine¶
Everything the UI could get from snapraid-daemon's REST API instead of calling the CLI itself sits behind one interface, SnapRaidEngine in backend/src/engine/engine.ts. Routes, WebSocket and main.ts call getEngine(); scheduler and spindown keep activeEngine, which always passes on to the engine in place, so the engine can be switched while the backend runs.
There are two implementations:
createCliEngine()(engine/cli-engine.ts) wraps the command executor and the parsers.createDaemonEngine()(engine/daemon-engine.ts) talks to one snapraid-daemon (tested with 2.0rc2). Jobs the daemon has no task for go to its fallback, the CLI engine.
buildEngine() in engine/engine-settings.ts puts them together from engine.json: in daemon mode each listed config runs on its daemon (a daemon serves one array, several run as snapraidd@<name>), the other configs on the CLI. One job runs at a time across all of them. GET/PUT /api/engine read and switch it (not while a job runs), POST /api/engine/test checks one daemon.
| Engine method | CLI engine | Daemon engine |
|---|---|---|
runJob |
snapraid <command> -l <log> through the executor |
POST /snapraid/v1/schedule with {tasks: [{command, args}]} for sync, scrub, check, fix, diff, smart, probe, then /v1/tasks and /v1/activity polled once a second for messages and progress. touch and status go to the CLI fallback, the daemon has no such task. The report comes from the task's SnapRAID log when the daemon runs on the same machine, from the task's figures otherwise |
abortJob |
SIGINT to the process |
POST /snapraid/v1/stop |
currentJob, currentOutput, lastJob, onProgress |
Executor state, run:pos tags from the log |
The task followed by runJob |
readStatus |
snapraid status plus the disk check (disk-check.ts) |
GET /snapraid/v1/array and /v2/disks; a disk the daemon rates degraded becomes a missing disk issue |
readDiff |
snapraid diff |
A diff task, then GET /snapraid/v1/array?limit_diffs=… |
readSmart |
snapraid smart |
GET /snapraid/v2/disks; attribute names mapped to SMART ids |
readPowerStates |
snapraid probe |
GET /snapraid/v2/disks (power state) |
readLastRuns |
null: the log manager reads the UI's log files |
Newest finished sync and scrub in /v1/tasks |
Stays with the UI whatever the engine, because the daemon has no API for it or it is the UI's own feature:
- Editing and validating
snapraid.conf, the disk wizards andpool: the daemon only readssnapraid.conf; its config API coverssnapraidd.conf. dup,listanddevices(snapraid-runner.ts).- Schedules, notifications, Docker pause and spindown (
scheduler.ts,notifications.ts,container-pause.ts,spindown.ts). The daemon's ownmaintenance_schedule, notifications,hook_docker_pauseand spindown have to stay off;POST /api/engine/testwarns when they are set.
Known gaps of the daemon engine:
- The daemon does not count files per disk (
DiskStatusInfo.filesis missing). - Its runs are not in the UI's logs:
LastRun.logFileis empty and the Logs page does not list them. - SMART attributes come by name only; unknown names get id
0. touch,dup,listand config validation run the CLI next to the daemon;touchas a job, one at a time as usual. Jobs the daemon starts by itself (its own web UI, another client) are not seen by the UI.
Keeping the engine in line with snapraid-daemon¶
The API was last compared with snapraid-daemon v2.0rc2; the version is in backend/src/engine/REVIEWED_DAEMON_VERSION. A weekly workflow (job snapraid-daemon in upstream-release.yml) opens an issue when a newer tag is out. For each new version:
- Compare
snapraidd.yaml(the OpenAPI spec) between the reviewed and the new tag. - If the daemon gained an endpoint for something in the "stays with the UI" list (for example
duporlist), add a method toSnapRaidEngine, implement it in the CLI engine, move the callers to it and add a step to the contract tests. - Update the tables above.
- Put the new version into
REVIEWED_DAEMON_VERSION.
Tests¶
backend/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).
Without a daemon, daemon-engine.test.ts runs the daemon engine against a stand-in that answers like snapraid-daemon 2.0rc2: 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.
The end-to-end tests run in CI against a real snapraid-daemon at the version in REVIEWED_DAEMON_VERSION: they switch to daemon mode in the UI, test the connection and check that a sync runs as a task of the daemon.
Structured log parsing¶
SnapRAID 14 writes a machine-readable log: one tag per line, name:value:value…, with colons, newlines and backslashes in paths escaped (\d, \n, \r, \\). The format is documented in doc/snapraid_log.txt in the SnapRAID sources. parsers/structured-log.ts splits captured output into tag lines and text lines, unescapes values and collects summary:key:value tags.
The outcome of a run (RunResult: ok, warning, error, aborted, incomplete) is derived from the log in log-manager.ts:
- a
sigint:tag meansaborted; summary:exit:ok/warningmap directly;equal,diff,nodup,dupandrecoveredcount asok;recoverableaswarning; anything else aserror;- no
summary:exitat all meanserrorif there is amsg:fatal:line, otherwiseincomplete(still running or killed).
Each log's conf:file tag links it to a config, which is how the dashboard finds the last sync and scrub of the selected config. The frontend has its own reader for the log viewer (frontend/src/lib/log-parse.ts). Parser tests use real SnapRAID output in backend/src/parsers/__tests__/fixtures/.
Job runner and live output¶
sequenceDiagram
participant UI
participant API as Backend
participant S as snapraid
UI->>API: POST /api/snapraid/execute
API-->>UI: {"success": true}
API->>S: spawn with -l <log> [--gui]
loop while running
S-->>API: stdout / stderr
API-->>UI: WS output
API->>API: poll log every 1 s for run:pos
API-->>UI: WS progress
end
S-->>API: exit
API-->>UI: WS complete
- Output chunks from stdout and stderr are broadcast to every connected WebSocket client as
outputmessages. The last 64 KiB are buffered, so a client that connects while a job runs first gets areplaymessage with the output so far. - For progress commands, the executor tails the job's log once per second, parses the newest
run:postag and broadcasts it asprogress. The current job (GET /api/snapraid/current-job) carries the latest progress too. - When the job ends, the backend broadcasts
complete(orerrorif the process could not run at all) and remembers the outcome as the last job. Clients that missed the message (reload, reconnect) learn about it fromGET /api/snapraid/last-job. - A job can carry an
afterRunstep that runs before the job is released, e.g. removing the data disk from the config after a successfulsync -E, or recording a replacement step. Nothing else can start in between. - After a manual
sync,scrub,fixorcheck, a notification is sent if the settings ask for manual jobs. Afterstatusordiffvia/execute, astatusmessage with the parsed status is broadcast.
The frontend (frontend/src/hooks/useJob.tsx) combines the WebSocket with polling GET /api/snapraid/current-job every 5 seconds, so it picks up jobs started elsewhere (another tab, a schedule) and notices jobs that ended while it was disconnected. frontend/src/lib/job-tracker.ts makes sure the end of a job is handled exactly once. The WebSocket client reconnects 3 seconds after the connection drops.
Scheduler¶
scheduler.ts keeps one Croner job per enabled schedule; schedules are stored in schedules.json. When a schedule fires:
- It is skipped if a job is already running (
job_running), if a disk replacement is in progress for its config (recovery_in_progress), or, for a sync with a maximum deleted files limit, ifdifffails (diff_failed) or reports more deleted files than allowed (too_many_deleted). A skip is stored as the schedule's last outcome and can trigger a notification. - Otherwise the steps run one after another:
touch(iftouchBefore), the command itself, thenscrubwith thescrubAfterarguments. A step that does not end inokorwarningstops the routine. If a manual job starts between two steps, the next step is not run. - For
smartschedules the result also updates the SMART history and can send a SMART notification. - The combined outcome (first failure, otherwise
warningif any step warned) and per-step results are written back toschedules.json, and a notification is sent if configured.
Scheduled jobs run through the same executor as manual ones, so their output, progress and completion reach the UI over the WebSocket. Their output messages use command: "scheduled" and prefix each chunk with [Schedule: <id>]. See Scheduling for the user side.
Storage¶
Everything lives in the data directory, SNAPRAID_BASE_PATH. In the image that is /app/snapraid; without the variable the backend uses ../snapraid relative to its working directory. Relative paths in API requests (e.g. configPath: "snapraid.conf") are resolved against it; absolute paths are used as they are.
| File | Written by |
|---|---|
config.json |
SnapRAID configs known to the UI, log settings |
schedules.json |
Schedules and their last outcome |
notifications.json |
Notification settings |
notifications-state.json |
Which SMART problems were already reported |
maintenance.json |
Docker pause and spindown settings |
paused-containers.json |
Containers paused for the running job, resumed on the next start after a crash |
smart-baseline.json, smart-history.json |
CRC baseline and daily SMART values (up to 365 points per disk) |
usage-history.json |
Daily array usage (up to 730 points per config) |
replacements.json |
Disk replacements in progress |
.session-secret |
Random key that signs sessions (only with login enabled) |
logs/*.log |
One structured log per job (directory set in config.json) |
Writes are plain Deno.writeTextFile 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 for details on each file.
Frontend¶
The frontend is a TanStack Start app with file-based routing. Nitro builds it into frontend/.output/, a Node.js server that renders pages on the server and serves the client bundle.
| Path | Content |
|---|---|
src/routes/ |
Pages: index.tsx (dashboard), smart.tsx, schedules.tsx, logs.tsx, notifications.tsx, __root.tsx (shell, providers) |
src/components/ |
Feature components; components/ui/ holds the shadcn/ui primitives |
src/hooks/ |
queries.ts (TanStack Query hooks for every endpoint), useJob.tsx (running job state), useSelectedConfig.tsx, useAppShell.tsx |
src/lib/api/ |
Thin fetch wrappers per area, the WebSocket client and error localization |
src/lib/ |
Command definitions, log parsing, progress, theme, i18n helper, navigation |
src/server.ts |
Server entry; runs the Paraglide middleware so SSR renders the user's language |
In production the API base is /api and the WebSocket URL is /ws (same origin, through nginx). In development the frontend calls http://localhost:8080/api and ws://localhost:8080/ws directly (src/lib/api/constants.ts). Every request is sent with credentials: 'include'; a 401 fires a snapraid:unauthorized event that brings back the login page.
Server state is managed with TanStack Query. Only the array status is persisted to localStorage, so the dashboard shows the last known status immediately after a reload. Theme and animation preferences are stored in localStorage and applied by an inline script before the first paint. The app is also an installable PWA (vite-plugin-pwa); pages and API data are deliberately not cached.
Internationalization¶
UI texts live in frontend/messages/{en,de,it}.json and are compiled by Paraglide JS into frontend/src/paraglide/ (generated, not committed). Components call m.key(). The language is resolved in this order: an explicit choice (cookie), the browser language, English.
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 shared/i18n.ts produces \u0002["server_error_job_running",{}]\u0003. The frontend (frontend/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 (backend/src/notification-events.ts).
See Development for how to add or change texts.
API reference¶
All endpoints are under /api and exchange JSON unless noted otherwise. With the login enabled, every endpoint except /api/auth/* requires the session cookie. Errors come back as {"error": "…"} with a 4xx or 5xx status; the message may be a marked i18n key (see above).
Most endpoints that work on a SnapRAID config take its path: as the path query parameter for GET/DELETE, or as configPath in the JSON body for POST. The path is the one stored in config.json (relative to the data directory, or absolute). Response types refer to the interfaces in shared/types.ts.
[!NOTE] The API is the internal interface between the frontend and the backend, not a versioned public API. It can change between releases.
General and auth¶
| Method | Path | Purpose | Key params / response |
|---|---|---|---|
| GET | / |
Backend health check (not under /api; only reachable on port 8080 directly, nginx sends / to the frontend) |
{status: "ok", service: "SnapRAID Backend"} |
| GET | /api/auth/session |
Current login state | AuthSession: {enabled, authenticated, username?}. With login disabled: {enabled: false, authenticated: true} |
| POST | /api/auth/login |
Log in, sets the session cookie | Body {username, password}. 401 on wrong credentials, 429 with Retry-After header and {retryAfter} after 5 failures within 15 minutes. Only exists with login enabled |
| POST | /api/auth/logout |
Clear the session cookie | Only exists with login enabled |
App configuration and backup¶
| Method | Path | Purpose | Key params / response |
|---|---|---|---|
| GET | /api/config |
Load config.json |
AppConfig |
| POST | /api/config |
Overwrite config.json |
Body: AppConfig |
| POST | /api/config/add |
Add an existing SnapRAID config file | Body {name, path, enabled?}. 400 if the file does not exist or is already added. Returns {success, config} |
| POST | /api/config/create |
Create a new config file in the data directory from a template and add it | Body {name, fileName} (letters, digits, _, ., -; .conf is appended if missing). 409 if the file exists. Returns {success, config, path} |
| POST | /api/config/update |
Rename or enable/disable a config | Body {path, name?, enabled?} |
| POST | /api/config/remove |
Remove a config from the UI; the file is kept | Body {path} |
| GET | /api/config/check |
Whether each config file exists and what it contains | ConfigFileCheck[]: {path, exists, dataDisks, parityLevels, contentFiles, error?} |
| GET | /api/config/base-path |
The data directory | {basePath} |
| GET | /api/config/backup |
Download settings, histories and SnapRAID configs as one file | SettingsBackup as attachment snapraid-ui-backup-YYYY-MM-DD.json |
| POST | /api/config/restore |
Write a backup back and reload the schedules | Body: SettingsBackup. Returns {restored, skipped}. Only configs inside the data directory are restored |
Filesystem¶
| Method | Path | Purpose | Key params / response |
|---|---|---|---|
| GET | /api/filesystem/browse |
List a directory (for the file and directory pickers) | Query path (default: data directory), filter = conf (directories and .conf files, default) or directories. Returns {path, entries: [{name, isDirectory, path}]} |
| GET | /api/filesystem/read |
Read a text file | Query path. Returns {content} |
| POST | /api/filesystem/write |
Write a text file (used by the config text editor) | Body {path, content} |
Jobs¶
| Method | Path | Purpose | Key params / response |
|---|---|---|---|
| POST | /api/snapraid/execute |
Start a SnapRAID command as a background job | Body {command, configPath, args?}. command is a SnapRaidCommand (sync, scrub, check, fix, touch, status, diff, …), args are extra SnapRAID arguments, e.g. ["-p", "new"], ["-h"] or ["--force-empty"]. 409 if a job is running. Output arrives over the WebSocket |
| GET | /api/snapraid/current-job |
The running job, or null |
RunningJob: {command, configPath, startTime, processId, aborting?, logFile?, progress?} |
| GET | /api/snapraid/last-job |
Outcome of the last finished job, or null |
FinishedJob: {command, processId, exitCode, aborted, error?, forceOption?, finishedAt} |
| POST | /api/snapraid/abort |
Abort the running job (SIGINT) | 404 if no job runs. Returns {success} |
| POST | /api/snapraid/restore |
Bring files back as they were at the last sync (fix -d <disk> -f /<path>), one job per disk |
Body {configPath, files: [{disk, path}]} with the paths from diff, at most 1000. 202; 409 if a job is running. Stops after a failed fix |
| POST | /api/snapraid/heal |
Repair the bad blocks (fix -e), then check them again (scrub -p bad), two jobs one after the other |
Body {configPath}. 202; 409 if a job is running. The scrub only follows a successful fix and only when no other job started in between |
| GET | /api/snapraid/history |
Results of the last 50 jobs started through /execute or the wizards since the backend started (in memory) |
CommandOutput[] |
Status and reports¶
| Method | Path | Purpose | Key params / response |
|---|---|---|---|
| GET | /api/snapraid/status |
Run snapraid status and parse it; also records the daily usage point |
Query path. Returns {status: SnapRaidStatus, timestamp, exitCode}. 409 with busy: true while a job runs or another process holds SnapRAID's lock. Without path: the parsed last status from the in-memory history |
| GET | /api/snapraid/last-runs |
Last sync and scrub of a config, from the logs | Query path. Returns {sync: LastRun \| null, scrub: LastRun \| null} |
| GET | /api/snapraid/diff |
Run snapraid diff (sync preview) |
Query path. Returns DiffReport with file list and counts |
| GET | /api/snapraid/list |
Run snapraid list |
Query path. Returns ListReport |
| GET | /api/snapraid/dup |
Duplicate files from the content file's hashes | Query path. Returns DupReport |
| GET | /api/snapraid/devices |
Run snapraid devices |
Query path. Returns DevicesReport |
| GET | /api/snapraid/check-report |
Files the last check of a config found, read from its log |
Query path. Returns CheckReport; 404 if there was no check yet |
| GET | /api/snapraid/usage-history |
Daily usage of the array | Query path. Returns UsagePoint[] |
| GET | /api/snapraid/parity-usage |
Size and free space of the parity disks | Query path. Returns ParityLevelUsage[] |
| GET | /api/snapraid/data-disk-usage |
Size and free space of the data disks | Query path. Returns DataDiskUsage[] |
SMART and power state¶
| Method | Path | Purpose | Key params / response |
|---|---|---|---|
| GET | /api/snapraid/smart |
Run snapraid smart; applies the CRC baseline and records the daily SMART history |
Query path. Returns SmartReport: {disks, arrayFailureProbability, timestamp, rawOutput} |
| GET | /api/snapraid/smart-history |
Daily SMART values per disk | Query path |
| GET | /api/snapraid/probe |
Power state of the disks (snapraid probe) |
Query path. Returns ProbeReport. 400 with unsupported: true if probing is not supported |
| POST | /api/snapraid/power |
Spin disks up or down (snapraid up / down, always through the CLI) |
Body {configPath, action: "up" \| "down", disks?}, no disks for the whole array. 409 while a job runs; 500 with SnapRAID's last line when it failed |
In demo mode (SNAPRAID_DEMO=1), smart and probe return generated values instead of running SnapRAID, and status gets the sizes and scrub state of the demo disks.
SnapRAID config editing¶
All of these take configPath in the body, edit the snapraid.conf line by line and return {success, config: ParsedSnapRaidConfig}.
| Method | Path | Purpose | Key params |
|---|---|---|---|
| GET | /api/snapraid/parse |
Parse a config file | Query path. Returns ParsedSnapRaidConfig (no wrapper) |
| POST | /api/snapraid/validate |
Check a config by running snapraid status |
Body {configPath}. Returns {valid, exitCode, output} |
| POST | /api/snapraid/add-data-disk |
Add a data line, plus a content line in the disk's root |
diskName, diskPath |
| POST | /api/snapraid/add-parity-disk |
Add the next parity level (parity, 2-parity … up to 6) |
parityPath (must end in .parity) |
| POST | /api/snapraid/remove-disk |
Remove the highest parity level | diskType: "parity", level?. Data disks use remove-data-disk |
| POST | /api/snapraid/add-exclude |
Add an exclude pattern |
pattern |
| POST | /api/snapraid/remove-exclude |
Remove an exclude pattern |
pattern |
| POST | /api/snapraid/add-content |
Add a content file |
contentPath |
| POST | /api/snapraid/remove-content |
Remove a content file |
contentPath |
| POST | /api/snapraid/set-pool |
Set or remove (empty value) the pool directory |
poolPath |
| POST | /api/snapraid/set-option |
Set or remove (value: null) autosave or blocksize |
option, value (positive integer or null) |
Disk wizards¶
| Method | Path | Purpose | Key params / response |
|---|---|---|---|
| POST | /api/snapraid/remove-data-disk |
Point the disk at an empty directory and start sync -E; after a successful sync the disk is removed from the config |
Body {configPath, diskName}. 409 if a job runs |
| GET | /api/snapraid/replace-disk |
Replacement in progress for a config, or null |
Query path. Returns DiskReplacement |
| POST | /api/snapraid/replace-disk |
Point the disk at its new location and start fix -d <disk> |
Body {configPath, diskName, newPath}. The new directory must exist. 409 if a job or another replacement runs |
| POST | /api/snapraid/replace-disk/step |
Run a step again or the next one | Body {configPath, step} with step = fix, check (check -a -d, data disks only) or sync; fix must have run first |
| DELETE | /api/snapraid/replace-disk |
Close a finished replacement or give up; the config keeps the new path | Query path |
Logs¶
| Method | Path | Purpose | Key params / response |
|---|---|---|---|
| GET | /api/logs |
List log files, newest first | LogFile[]: {filename, path, command, timestamp, size, modified?, result?, configPath?} |
| GET | /api/logs/:filename |
Content of a log file | Returns text/plain; 404 if missing |
| DELETE | /api/logs/:filename |
Delete a log file | |
| POST | /api/logs/rotate |
Apply the retention from config.json now |
Returns {success, deleted} |
Schedules¶
| Method | Path | Purpose | Key params / response |
|---|---|---|---|
| GET | /api/schedules |
All schedules | Schedule[] |
| GET | /api/schedules/:id |
One schedule | 404 if unknown |
| POST | /api/schedules |
Create a schedule | Body: name, command, configPath, cronExpression (required); args, maxDeletedFiles, maxUpdatedFiles, touchBefore, scrubAfter, enabled (optional). Returns 201 with the Schedule; 400 on an invalid cron expression |
| PUT | /api/schedules/:id |
Update fields of a schedule | Body: partial Schedule, e.g. {skipNext: true} to skip the next timed run once |
| POST | /api/schedules/:id/run |
Run a schedule once now, with its routine and checks, also a disabled one | 202; 404 if unknown, 409 if a job is running |
| DELETE | /api/schedules/:id |
Delete a schedule | |
| POST | /api/schedules/:id/toggle |
Enable or disable a schedule | Returns the updated Schedule |
| GET | /api/schedules/next-runs |
Next run time per active schedule | {[id]: ISO string \| null}. Registered after /:id, so requests currently resolve to GET /api/schedules/:id; use the nextRun field of each schedule instead |
Notifications¶
| Method | Path | Purpose | Key params / response |
|---|---|---|---|
| GET | /api/notifications |
Notification settings with the SMTP password and ntfy token masked | NotificationSettings |
| PUT | /api/notifications |
Save settings; a masked secret keeps the stored value | Body: NotificationSettings. 400 if invalid |
| POST | /api/notifications/test |
Send a test message with the given, possibly unsaved settings | Body {settings, channel?} with channel = email, ntfy or webhook (default: all enabled channels). Returns NotificationTestResult[] |
| POST | /api/notifications/heartbeat |
Ping the heartbeat URL of the given, possibly unsaved settings | Body {settings}. Returns {ok, error?} |
Automation¶
| Method | Path | Description | Notes |
|---|---|---|---|
| GET | /api/maintenance |
Docker pause, spindown and metrics settings | MaintenanceSettings |
| PUT | /api/maintenance |
Save settings | Body: MaintenanceSettings. 400 if invalid |
| GET | /api/maintenance/containers |
Containers on the Docker socket | Query socket (default: the saved one). Returns DockerContainersReport, available: false with the error when the socket can't be reached |
| GET | /api/maintenance/spindown |
Disks the spindown watches, their last activity and when they were spun down | SpindownStatus |
| GET | /api/setup/mounts |
Mounted filesystems the setup wizard offers as disks | MountCandidate[]: {path, device, fstype, totalBytes, usedBytes, freeBytes, empty, snapraidFiles}, from df, without virtual filesystems, system directories and files |
| POST | /api/setup/create |
Write the snapraid.conf of a new array into the data folder and add it |
Body {name, fileName, setup: {dataDisks: [{name, path}], parityPaths}}. 400 with problems if the setup is incomplete or a path is no directory, 409 if the file exists. Returns 201 with {path} |
| GET | /api/metrics |
Prometheus metrics, see Automation | Text exposition format. Not behind the login; Authorization: Bearer <token> when a token is set (401 otherwise), 404 while switched off |
See Notifications for the settings and the webhook payload.
WebSocket¶
Connect to /ws (through nginx) or ws://<host>:8080/ws. With the login enabled, the session cookie is required for the upgrade. The connection is server-to-client only: the backend ignores anything a client sends. Every message is a JSON object with a type:
type |
When | Fields |
|---|---|---|
replay |
Right after connecting, if a job is running | command, processId, output (the last 64 KiB of output so far) |
output |
A chunk of stdout/stderr from the running job | command ("scheduled" for scheduled jobs, whose chunks start with [Schedule: <id>]), chunk, timestamp |
progress |
New progress of sync, scrub, check or fix, at most once per second |
command, processId, progress: {percent, processedMB, speedMBs?, etaMinutes?, temperature?} |
complete |
A job finished (also aborted or failed runs) | command, processId, exitCode, aborted, forceOption? (zero, empty or uuid), timestamp |
error |
A job could not be run at all (e.g. the binary is missing) | command, processId?, error, timestamp |
status |
After a status or diff started through /api/snapraid/execute |
status (SnapRaidStatus). The current UI does not use it |
Example: