Skip to content

Operations

Domekologe edited this page Aug 7, 2026 · 1 revision

Operations

🌐 English · Deutsch

The Operations tab (Settings → Operations, admin only) is the one page that answers operational questions: what are the background workers doing, what state is the database schema in, can this installation be rolled back, and what may run at which time of day.

It exists because those answers used to be spread across six tables and three log prefixes. Each of those is the right place for a worker's data — none of them answers the question an operator actually asks.


Background workers

Every worker reports a heartbeat: what it is doing, when it last ran, when it runs next, and what it last failed with.

State Meaning
running Working right now
idle Alive, waiting for work or for its next scheduled run
error The last run ended with an error (shown below the card)
stale Claimed to be running but has not reported in 15 minutes
unknown Has never reported at all

unknown and "not running" are deliberately different. A list that only shows what it found cannot distinguish "the encoder has never started" from "there is no encoder", and those are very different problems.

The error text is sticky: it stays visible after the worker recovers and goes back to idle, so a failure that happened at 4 a.m. is still there in the morning.

Workers covered: download queue, encoding, upscaling, AutoSync, library scan, UpTime monitor, MediaScan, TMDB keywords, cache eviction, Dev infos, audit retention. A module that runs its own worker and reports a heartbeat appears here too.


Database schema & snapshots

Schema version

MediaForge tracks its schema in a schema_migrations table. The panel shows the current version, the latest version this build knows, and any migration that is still pending.

Migrations run once, at startup, before anything else touches the database. Each one runs in its own transaction: if one fails, every earlier migration stays committed and the failing one is fully undone, so a fixed release resumes from exactly that point. A failed migration is not fatal — the app still starts, because refusing to start would leave you without the interface you need to roll back.

An installation that predates the migration engine is baselined, not migrated: the schema it already has is recorded as version 1 without re-running anything, and only migrations added after that actually execute on it.

Snapshots

A snapshot is a complete, self-contained copy of the database file.

  • One is taken automatically before any pending migration runs.
  • One is taken automatically before a restore, so the restore itself is undoable.
  • You can take one manually at any time.

Automatic snapshots are pruned to the ten most recent. Manual ones are never pruned — you asked for those. They live in ~/.mediaforge/db_snapshots/ next to the database, deliberately: a rollback is most needed exactly when the app will not start, and you have to be able to find them by hand.

Snapshots use SQLite's online backup API, not a file copy. The database runs in WAL mode, so part of the committed state lives in the -wal file at any moment. Copying only mediaforge.db would produce a database that is valid but silently missing the most recent transactions — the worst possible failure mode for a rollback target.

Verify

Verify opens a snapshot read-only and proves it is restorable: integrity check, foreign-key check, schema version, and row counts for the tables that would hurt to lose. An unverified backup is not a backup.

Restore

Restore replaces the live database with the snapshot. It refuses to run unless the snapshot passes verification first — restoring a corrupt file over a working database turns a recoverable situation into an unrecoverable one.

After a restore you must restart the server: open connections (workers, request contexts) still point at the replaced file.

The restore is recorded in the audit log before and after it happens. That is one of the reasons the audit log lives in its own database file — the record of who performed the restore is exactly the record you least want the restore to erase.


Quiet hours

Called "maintenance windows" in earlier releases. Nothing here maintains anything — it throttles — so the name changed. The database table, the /api/ops/maintenance routes and the maintenance_window_* audit actions deliberately keep the old identifiers, so existing setups, scripts and audit history are untouched.

A quiet period says: on these weekdays, between these two times, allow at most N downloads, and optionally forbid encoding, upscaling and library scans entirely.

Field Meaning
Days Which weekdays the period applies on
From / To Clock times. An end before the start wraps over midnight — "22:00 to 06:00" is one period, not two
Max downloads Upper bound on parallel downloads. 0 pauses downloads entirely
Encoding / Upscaling / Library scans Whether each is allowed during the period

Quiet periods are restrictions. Outside every period the normal settings apply unchanged, so an installation with no quiet hours behaves exactly as it did before.

Overlapping periods resolve to the strictest combination, not to whichever was found first. Two periods that each forbid encoding cannot combine into one that permits it.

The panel shows which period (if any) is active right now and what it currently allows.


Diagnostics

Download diagnostic bundle builds a ZIP you can attach to a bug report.

Included: version and platform info, which settings are set (sensitive values replaced with <set>), table row counts, worker states, schema version and snapshot list, installed modules, the most recent queue errors, and the tail of the log.

Excluded: passwords, API keys, tokens, session cookies, media files, and library paths beyond their shortened form. The audit log is not included either.

The log and the queue errors are scrubbed with the same sanitizer telemetry uses — home directories collapsed, URL paths stripped, secret-shaped tokens redacted. The ZIP contains a manifest.json spelling this out. Please still skim it before sharing.

Nothing is sent anywhere. The bundle is generated on request, downloaded once, and never stored.


Settings profile

A settings profile is the small sibling of Backup: a plain-JSON export of the configuration itself — naming template, provider order, quality defaults, path layout — and deliberately nothing else.

No users, no history, no queues, no API keys. It is meant to be pasted into a support ticket or shared with somebody setting up the same thing, and anything that could not survive that is excluded rather than encrypted.

Which keys go in is an explicit allowlist. A denylist would mean every newly added sensitive setting is exposed until somebody remembers to add it — the failure mode that turns "share your settings" into an incident. On top of that, anything the database considers sensitive is refused even inside an allowed prefix, which is what stays correct when a module registers a new secret at runtime.

Importing shows a diff first: every key that would change, with its current and incoming value, each individually deselectable. Nothing is written until you apply it. Keys outside the allowlist are listed as refused rather than silently dropped.


Rollback after a self-update

A database snapshot is taken automatically before the update helper takes over, and its id is kept in the update status even after you dismiss the "update finished" banner — a problem introduced by an update is usually noticed days later.

This is a different snapshot from the one the migration engine takes. That one is written by the new code, just before it migrates; if the new version fails before it gets that far, the pre-update snapshot is the only copy of the old state that exists.

If the snapshot cannot be written (unwritable directory, full disk) the update still proceeds — it just means there is no automatic way back. That is logged as a warning rather than blocking an update you asked for.



Running the workers in their own process

By default the download, encoding, upscale, Auto-Sync and TMDB-keyword workers run as threads inside the web process. That is the right answer for a single-user install, and it is what happens if you configure nothing.

It stops being the right answer when the workers and the UI compete for the same process: Python's GIL means a worker doing CPU-bound work slows down request handling, a scraper stuck in a blocking call makes the UI feel broken, and a worker that wedges a subprocess takes down the page you would have used to cancel it.

Set MEDIAFORGE_WORKER_MODE=external on the web process and start a second one:

python -m mediaforge.web.worker_host

Both must see the same MEDIAFORGE_CONFIG_DIR. They coordinate purely through the database — the queue claim has always been an atomic UPDATE ... WHERE status='queued', written precisely so two workers can never take the same job, which is what makes this a deployment choice rather than a different execution model.

MEDIAFORGE_WORKERS=queue,encoding runs only some of them there and leaves the rest in the web process. An unknown name in that list falls back to running all of them and logs an error — a typo must not silently mean "run nothing", because the symptom would be downloads sitting there with nothing in the log.

The host waits for the web process to finish migrating the schema before starting, so the start order does not matter. Exactly one process may own the schema, and that is the web process.

Docker Compose users: docker-compose.yaml ships the second service commented out, with the volumes it has to share.

The Operations view labels each worker with the mode it reported (inprocess / external) and the pid it is running under, so you can see at a glance where a worker actually lives.


Health endpoints

Two endpoints answer without a login, for Docker HEALTHCHECK, Kubernetes probes and external monitors such as Uptime Kuma:

Endpoint Meaning
GET /healthz Liveness. {"status": "ok"} while the process is up
GET /readyz Readiness. 200 when the database answers, 503 when it does not

Both return a single status string and nothing else — no version, no worker names, no error text. A status endpoint that leaks that to an unauthenticated caller is a reconnaissance endpoint; the detailed version lives behind the admin gate at GET /api/ops/workers.


See also

Clone this wiki locally