Skip to content

Releases: arediss/Oscarr

v0.8.1

10 May 02:26

Choose a tag to compare

⚠️ BREAKING — OSCARR_SECRET_KEY is now MANDATORY

Before upgrading, generate a 32-byte hex key and add it to your environment. Without it, Oscarr will refuse to boot.

Generate one (host with Node):

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

No Node on host? Just boot the container without the key — Oscarr exits with a friendly message and a freshly-generated key you can copy-paste:

docker compose up
# … look for the "Oscarr — secret key required" block in the logs:
#   OSCARR_SECRET_KEY=<copy this line into your .env>

Then add to your compose / env file:

environment:
  - JWT_SECRET=your_random_jwt_secret
  - OSCARR_SECRET_KEY=your_64_hex_chars_key

🔐 Lose this key = stored service credentials become unrecoverable and need to be re-entered manually. Treat it like a password — back it up.

Existing plaintext credentials get flagged in the admin (floating banner, bottom-right) and re-encrypted automatically on next save.


Compose gotcha

Compose reads .env for YAML interpolation only — vars don't reach the container unless declared in environment: or loaded via env_file:. The bundled docker-compose.yml was patched post-release to do both. If you maintain your own compose file, make sure it has either:

env_file:
  - .env

…or…

environment:
  - OSCARR_SECRET_KEY=${OSCARR_SECRET_KEY}

Verify with: docker compose config | grep OSCARR_SECRET_KEY (must show the real value, not the placeholder).


Highlights

New

  • Seerr-compatible API at /api/v1/* — third-party tools (Homarr, Doplarr) can talk to Oscarr without a custom integration. Endpoints not yet implemented return a clean 501 Not Implemented.
  • Per-app API keys with revocation (Admin → Access → API Keys) — each external integration gets its own scoped credential.
  • AES-256-GCM encryption at rest for service credentials. Master key derived from OSCARR_SECRET_KEY via HKDF-SHA256.
  • Secure password reveal/copy modal — replaces the native prompt() for credential reveal in the admin.
  • TMDB trending backdrop on the login page — random pick from the top 10 trending titles.

Improved

  • qBittorrent 5.x compatibility — provider now uses API-key auth (Authorization: Bearer). Drops username/password. Pause/resume hit the new /torrents/stop and /torrents/start endpoints.
  • Searchable plugin Discover with filters (Hide installed, Nouveau pill).
  • Reusable ConfirmModal — replaces confirm()/alert() across destructive admin actions.

Removed

  • *Drop arr tag-based request sync — replaced by the upcoming Seerr import path (#192).

Fixes

  • Bump better-sqlite3 to 12.9 for Node 24+ support.
  • Slim README + extract docs/installation.md.

CI

  • Advisory React Doctor job on push + PR.

Image

ghcr.io/arediss/oscarr:0.8.1
ghcr.io/arediss/oscarr:latest

Multi-arch (linux/amd64, linux/arm64), keyless-signed (Sigstore / cosign), SPDX SBOM attached.

v0.8.0 — Plugin self-update, generic storage, indexer + download-client connector pack

08 May 17:38

Choose a tag to compare

Highlights

Plugins now self-update from inside Oscarr with a permission diff and a reload/reboot split — no more SSH dance for routine bumps. They also gain a generic isolated storage API (KV + SQLite, gated by storage:plugin), so plugin state lives next to the plugin and is wiped with it. On the integrations side, a connector pack lands: Prowlarr (indexer) plus four torrent/Usenet clients (NZBGet, SABnzbd, Transmission, Deluge) — connection-test only for now, the action layer will ship as separate plugins.

Features

  • In-app plugin update flow with permission diff. Plugins self-update from inside Oscarr without SSH — the admin sees the upcoming version, the diff of requested permissions, and chooses between a hot reload (no downtime) and a full reboot (required when manifest perms or native deps changed). Reload vs reboot is decided by the engine, not guessed by the user.
  • Generic plugin storage API (KV + SQLite). Two isolated primitives — a key/value store and a per-plugin SQLite database — exposed via PluginContext.storage and gated by the new storage:plugin permission. State is scoped to the plugin and removed with it, so the core schema doesn't leak plugin concerns.
  • Indexer + download-client connector pack (Prowlarr, NZBGet, SABnzbd, Transmission, Deluge). Five new service connectors land on the Services tab — connection testing only. Actions (queue/history/pause/etc.) will be exposed by separate plugins talking to these connectors. Each ships with a typed test() that distinguishes auth failures from network errors and surfaces protocol-specific cases like Deluge's daemon-detached state and Transmission's CSRF challenge.
  • Searchable service-type picker with provider icons + 'Untested' badge. The service-creation modal now opens a Headless UI Combobox instead of a native <select>, with a search input, provider icons rendered inline, and an amber 'Untested' pill on connectors that haven't been validated against a real instance yet. Picking an untested connector shows a one-click GitHub feedback link prefilled with the connector name — the badge is removed in the same commit that confirms the integration works.
  • Support module extracted to a standalone plugin. The legacy in-core Support module is now shipped as Oscarr-Plugin-Support, removable like any other plugin. The migration carries an export of existing tickets so admins can install the plugin and keep their data, while keeping the core lean and aligned with the plugin-first vision.

Fixes

  • SonarCloud reliability sweep — 53 bugs cleared. Three refactor tiers landed in this cycle (mechanical sweep, cosmetic batch, redundant cast removal). Pure code-health work — no behavioural change expected.
  • Log injection sanitizer hardened. CodeQL was failing to recognise the log sanitizer; rewriting it as explicit per-channel branches with separate replace() calls satisfies the static analyzer and removes the dead suppression. Closes part of the security code-scanning queue (#182).

Other

  • Faster multi-arch (amd64 + arm64) release builds. The release pipeline now produces both linux/amd64 and linux/arm64 images noticeably faster. The lockfile's platform-native deps were also restored so installs on distroless / musl images stop failing.

Full changelog: v0.7.8...v0.8.0

Upgrading

```
docker pull ghcr.io/arediss/oscarr:0.8.0
```

v0.7.8 — Account modal redesign, Jackett indexer & qBittorrent plugin contract

03 May 20:32

Choose a tag to compare

Highlights

A redesigned account modal Discord-style replaces the old avatar dropdown, ships a new account.section plugin hook, and unlocks the first official plugin built on it: qBittorrent Manager. Admins gain user-defined topbar shortcuts, a quick admin/back jump button, and a Jackett connector for indexer integrations.

Features

  • Discord-like account modal with plugin sections. The avatar dropdown is replaced by a full-screen AccountModal with a sidebar (avatar header → section list → logout) and a content pane that swaps between built-in sections (Account, Preferences) and plugin-contributed ones via the new account.section hook point. Plugins declare an icon, label and React component; built-ins and plugin entries merge in a single sorted list.
  • User-selectable avatar source. #169 — pick which connected provider (Plex, Jellyfin, Emby, …) supplies your avatar. The new AvatarEditor surfaces every provider image you're linked to and lets you swap on the fly without touching the file system.
  • Admin-defined custom topbar links. #167 — admins can publish arbitrary URL shortcuts to the home topbar (label, icon, URL, target). Useful for pinning Tautulli, Overseerr, your dashboard, or any service users should reach in one click — without writing a plugin.
  • Admin jump button + tooltip system. A discreet shield/home button now sits in every topbar (admin-gated) for one-click jumps between the user app and the admin panel — replacing the slower path through the avatar dropdown. Backed by a new shared Tooltip component with a 100ms fade (vs the browser's ~1.5s native delay), reused on the avatar cog and the notification bell.
  • Jackett indexer connector. New indexer service category with Jackett as its first member. The connection test uses the public-with-apikey Torznab caps endpoint so it works on Jackett instances locked behind an admin password (where /server/config requires a cookie session). API key passes via query (not X-Api-Key — Jackett doesn't honour that header).
  • Curated icon set expanded. DynamicIcon (used by plugin manifests for nav/admin tabs/widgets) gains 8 new icons: Coins, CreditCard, Gift, Crown, Award, Trophy, Gauge, Rocket — to cover subscription/achievements/stats plugin patterns. Unknown icons now log a one-time dev-mode console warning instead of silently falling back to Puzzle, and the available list is documented in docs/plugins.md.

Fixes

  • qBittorrent connection test no longer 403s. The qBittorrent provider used to hit /api/v2/app/version directly without first authenticating, surfacing a misleading HTTP_FORBIDDEN. It now performs the full /auth/login flow, captures the SID cookie, and reports typed errors on bad creds (AUTH_FAILED), IP ban after too many failed attempts (AUTH_BANNED) or missing session cookie (AUTH_NO_SESSION) instead of the bare HTTP code.
  • Admin-only plugins no longer reachable by URL. Any plugin shipping a frontend field was previously mountable at /p/<id> whether or not it published a nav contribution — admin-only plugins (subscription, qbittorrent-manager…) were therefore reachable by typing the URL even though their data routes were permission-gated. The /p/<id> route now refuses to mount any plugin that hasn't contributed to hookPoint='nav', falling back to plugin.not_found.

New plugin available

Oscarr-Plugin-qBittorrent v0.1.0 ships alongside this release. Discover it via Admin → Plugins → Discover once the registry update lands. Read-only torrent queue + bulk pause/resume/delete + magnet add, with cross-check against Sonarr/Radarr import history so seeds already in the library are flagged for safe cleanup.


Full changelog: v0.7.7...v0.7.8

Upgrading

docker pull ghcr.io/arediss/oscarr:0.7.8

or

docker pull ghcr.io/arediss/oscarr:latest

v0.7.7 — Customisable dashboard, plugin install metrics & live nav refresh

27 Apr 21:52

Choose a tag to compare

Highlights

The admin dashboard becomes customisable per tab and per widget, the Discover tab surfaces install counts, and the nav refreshes live when you toggle a plugin. Plus the usual sweep of fixes for stale badges, broken caches, and stuck "Copy" buttons.

Features

  • Customisable dashboard tabs and widgets. Pick an icon for each tab from a curated 56-icon set. Each widget gets a Pencil edit button in edit mode → custom title, custom icon, or just toggle "show header" to surface the widget's default title. Custom overrides survive drag/resize.
  • Live install count on the Discover tab. Each plugin card now shows the total number of GitHub release-asset downloads next to the star count. GitHub increments the counter on every install — no extra infra. Plugins that ship a source tarball (instead of a Release asset) show 0 since GitHub doesn't track those.
  • Verbose request log toggle. New opt-in toggle in Admin → Système → Instance → Debug that persists every /api/* response to AppLog with method, path, status, redirect Location, duration, IP and User-Agent. Useful for tracing OAuth round-trips or plugin proxy redirects on a live instance. Off by default; URLs and Referer scrubbed via the existing scrubSecrets pipeline.
  • Plugin tab URL sync. Admin → Plugins → Installed / Discover sub-tab now syncs to the URL via ?sub=. Refresh, bookmark or share a link and you land on the right tab.

Fixes

  • Live nav refresh on plugin toggle. Toggling a plugin used to leave its nav entries / dashboard widgets stuck until F5. usePluginUI gains a pub/sub: invalidating the cache notifies every mounted hook so contributions refetch in place. The toggle pill itself shows a spinner with "Enabling…" / "Disabling…" copy while the request is in flight.
  • Stale "update available" badge. After installing a fresh version (say v0.1.2), the badge would still claim "v0.1.1 available" until the 1h cache TTL passed — it compared with latest !== installed instead of strict semver. The check is now latest > installed (semver-aware) AND the cache invalidates on install/uninstall so the badge clears immediately.
  • Update resolver follows /releases when /latest 404s. GitHub returns 404 on /releases/latest when no release is explicitly flagged as latest (some CI workflows skip the flag, plus transient API hiccups). The resolver now falls back to /releases?per_page=30 sorted by semver desc — same logic GitHub uses internally.
  • Bust stale browser caches on version flip. Stale HTML cached for /api/* URLs (from a past misconfig) could load the SPA shell on backend routes and produce blank pages. The SPA fallback now sets Cache-Control: no-store on index.html, and /api/app/version sends Clear-Site-Data: "cache" when the client's oscarr_v cookie doesn't match the running version — Chrome/Firefox/Edge purge their HTTP cache for the site on the spot. Sessions, settings and JWT survive.
  • Logs "Copy" button works on LAN dev. navigator.clipboard requires a secure context (HTTPS or localhost). Dev over a LAN IP had no clipboard, so the button silently failed. New helper falls back to document.execCommand('copy') via a hidden textarea so the copy works on plain HTTP origins.

Schema migration

New boolean column AppSettings.verboseRequestLog (default false). Idempotent on existing rows.


Full changelog: v0.7.6...v0.7.7

Upgrading

```
docker pull ghcr.io/arediss/oscarr:0.7.7
```
or
```
docker pull ghcr.io/arediss/oscarr:latest
```

v0.7.5 — Plugin install hotfix (CSP + bind-mount perms)

26 Apr 00:43

Choose a tag to compare

Hotfix

Two prod-blocking bugs uncovered while testing the 0.7.4 plugin install flow against a live self-hosted instance.

Fixes

  • Resolve plugin install URL server-side. 0.7.4 added a frontend fetch() to api.github.com to prefer a Release asset over the GitHub-generated source tarball. The prod CSP blocks api.github.com from connect-src, so the fetch silently failed and the install fell back to the source tarball — same broken end state as 0.7.3 for plugins like kedaewyn/Leonarr that don't commit dist/ to their repo. The resolution now lives backend-side (already trusted with GitHub calls for the update check), so the fix actually applies in prod. The /plugins/install endpoint accepts { repository } from the registry path and a raw { url } for manual installs.

  • Auto-chown the plugins directory at boot. Self-hosters with a host bind-mount for /app/packages/plugins (the docker-compose default) hit EACCES when Oscarr tried to mkdir the staging dir for an install — the host directory wasn't owned by uid 1001 (the in-container oscarr user). The entrypoint now chowns + chmod 2775 (setgid) the plugins dir at boot, the same way it already handles /data. Files dropped by the host maintainer keep the right group and both sides can write.

If GITHUB_TOKEN is set on the Oscarr container's env, the install resolution path uses it — useful if your registry's source is rate-limited.


Full changelog: v0.7.4...v0.7.5

Upgrading

```
docker pull ghcr.io/arediss/oscarr:0.7.5
```
or
```
docker pull ghcr.io/arediss/oscarr:latest
```

v0.7.4 — Multi-tab dashboard + plugin install via release assets

25 Apr 23:43

Choose a tag to compare

Highlights

The admin dashboard becomes multi-tab — name them, add new ones, drag your widgets where you want them. And plugin installs finally respect prebuilt release assets, unblocking the plugins that ship a clean source repo with dist/ shipped only on the GitHub Release page.

Features

  • Multi-tab dashboard. The dashboard hosts multiple named tabs above the grid. Add a tab with the + button, double-click a tab to rename, remove the active one in edit mode (the last remaining tab is locked so you're never empty). Widgets stay unique across the whole layout.
  • Reset confirm modal. The Reset-to-default action moved out of the toolbar into a proper a11y-aware modal (Escape, focus-trap, focus-return) and only renders in edit mode where it belongs.

Fixes

  • Plugin install respects release assets. The Discover-tab installer used to hardcode tarball/HEAD — GitHub's auto-generated source archive that omits gitignored build outputs. Plugins that ship dist/ as a Release asset (the recommended pattern) would land an unbuilt tree and fail with Cannot find module .../dist/index.js. Oscarr now resolves the install URL via the GitHub Releases API: the first .tar.gz asset on the latest release wins, with a fallback to tarball/HEAD for plugins that still commit dist/ to their repo.

Docs

  • Plugin release workflow. docs/plugins.md grows a Releasing a plugin section with a copy-paste GitHub Actions workflow that builds, packs and uploads a tarball asset on every v* tag — so future plugin authors land on the asset path by default and stop polluting their git history with built code.

Schema migration

The dashboard layout schema bumps to v2 (tabs). Existing v1 layouts are auto-migrated on the next GET into a single Main tab — the row is only rewritten on the next save. No DB migration required.


Full changelog: v0.7.3...v0.7.4

Upgrading

```
docker pull ghcr.io/arediss/oscarr:0.7.4
```
or
```
docker pull ghcr.io/arediss/oscarr:latest
```

v0.7.3 — Composable admin dashboard + plugin install fix

25 Apr 22:33

Choose a tag to compare

Highlights

The admin Dashboard tab is now a composable widget grid: drag-and-drop, three built-in widgets shipped, and plugins can contribute their own via a typed hook point. Plus the plugin install path is fixed for the Docker setups where /tmp and /app live on different filesystems.

Features

  • Composable admin dashboard. A drag-and-drop widget grid replaces the static DashboardTab. Three built-ins ship out of the box: stats counters (users / pending requests / services / plugins), service health (reachability + version per configured service), and system (Oscarr version + plugin update count). The layout is persisted globally per Oscarr instance.
  • Plugin-extensible. Plugins contribute widgets via the new admin.dashboard.widget hook point. A typed Zod sub-schema validates widget props (id, title, icon, defaultSize, minSize, maxSize) at plugin load — malformed manifests are rejected before the widget can reach the dashboard.
  • WYSIWYG edit mode. Editing the dashboard no longer wraps each widget in an extra card with a redundant title. The drag handle and remove button float in the corners with a dashed outline; what you see in edit is what you get in view.

Fixes

  • Plugin install across filesystems. Plugin tarballs used to be staged under os.tmpdir() and then renamed into <pluginsDir>/<id>. In Docker setups where /tmp is a tmpfs mount and /app lives on the persistent volume, fs.rename threw EXDEV ("cross-device link not permitted") and installs failed after a successful download. The installer now stages inside the plugins dir itself (under a hidden name the loader ignores) so the final rename is atomic.

Plugin contract

A new section in docs/plugins.md documents the admin.dashboard.widget hook point — manifest example, props schema, frontend contract.


Full changelog: v0.7.2...v0.7.3

Upgrading

```
docker pull ghcr.io/arediss/oscarr:0.7.3
```
or
```
docker pull ghcr.io/arediss/oscarr:latest
```

v0.7.2 — Prisma CLI resolution fix

25 Apr 18:49

Choose a tag to compare

Hotfix

Quick follow-up to v0.7.1 — the boot-time `prisma migrate deploy` was using a hardcoded path that worked in the prod image but failed in dev, since npm workspaces hoist the prisma binary to the monorepo root in dev (no packages/backend/node_modules/.bin/prisma).

Fix

  • ensureMigrated now resolves the prisma CLI via Node's module resolution (require.resolve('prisma/package.json') + pkg.bin) instead of a hardcoded path. Works in both setups:
    • Dev (npm workspaces): finds prisma in <root>/node_modules/prisma
    • Prod image: finds prisma in packages/backend/node_modules/prisma
  • No npx dependency reintroduced — the prod image stays free of npm/yarn/corepack.

Production deploys of v0.7.1 weren't affected (the old hardcoded path matched the prod layout). This release future-proofs against any layout change.


Full changelog: v0.7.1...v0.7.2

Upgrading

```
docker pull ghcr.io/arediss/oscarr:0.7.2
```
or
```
docker pull ghcr.io/arediss/oscarr:latest
```

v0.7.1 — CSP hotfix (self-host Inter, permissive img-src)

25 Apr 17:48

Choose a tag to compare

Hotfix

Quick follow-up to v0.7.0 — the CSP set in v0.7.0 was rejecting Google Fonts (Inter) and external avatar sources (Plex sync, Discord OAuth) in the browser console.

Fixes

  • Inter font is now self-hosted via @fontsource/inter — bundled with the frontend (weights 300/400/500/600/700/800), no more roundtrip to fonts.googleapis.com / fonts.gstatic.com. Faster boot, zero external tracking, zero CSP friction.
  • img-src relaxed to 'self' data: blob: https: — any HTTPS image source is allowed. Avatars synced from Plex, Discord, Jellyfin, Emby, or any future provider load without per-source allowlisting. <img> can't execute code, and on a self-hosted reverse-proxied app the alternative (endless allowlist updates) was friction with no real security benefit.
  • Everything else stays strict: script-src 'self' + the static importmap hash, connect-src limited to TMDB only, default-src 'self', no 'unsafe-inline' on scripts.

Full changelog: v0.7.0...v0.7.1

Upgrading

```
docker pull ghcr.io/arediss/oscarr:0.7.1
```
or
```
docker pull ghcr.io/arediss/oscarr:latest
```

v0.7.0 — Plugin engine v1.1, hardening waves & audit pass

25 Apr 17:20

Choose a tag to compare

What's New

Plugin Engine v1.1

  • Extended PluginContext — additive tmdb:read / requests:read / requests:write capability buckets, media.batchStatus, requests.create (shared pipeline with the HTTP route), getArrClients, findUserByEmail, listFolderRules, and an events bus with on/off/emit
  • Capability gating tightened — every ctx method that touches data now declares a capability, manifests are runtime-validated against the capability enum, and disabling a service in the admin panel immediately stops leaking its url+apiKey to plugins
  • Lifecycle cleanup — event-bus listeners and notification providers a plugin registered are now dropped on disable + uninstall (no more zombie handlers wired to the singleton)
  • Plug-and-play link flows — plugins can mint a JWT cookie via ctx.app.internalFetch, and a logged-out user hitting an OAuth ?action=link URL is redirected to /login?next=… with a same-origin guarded post-login bounce
  • Per-plugin Tailwind bundles — each plugin compiles its own CSS bundle so a plugin's classes can't conflict with the host app
  • Hot-installed plugins are immediately schedulable — manual triggers and cron ticks pick up newly registered jobs without a process restart

Auth Provider System

  • Discord OAuth — full sign-in / link / sync flow with single-use state nonces and intent binding
  • Per-provider signup gates — each AuthProvider has its own allowSignup toggle (including email), secure-by-default, no global master switch
  • Generic user sync — provider-agnostic sync interface with Plex as the first concrete adapter
  • Selective import via review modal — admins cherry-pick which provider users land in Oscarr instead of importing everyone
  • User.disabled flag — soft-disable accounts with a configurable login mode (friendly shows a localised banner, block returns generic invalid credentials)

Security Hardening (waves 1 + 2)

  • CSRF gate — every /api/admin/* request must carry the X-Requested-With: oscarr header
  • SSRF guardassertPublicUrl consistently applied on admin-typed URLs; plugin installer pins resolved IPs via Agent.connect.lookup to defeat DNS rebind
  • Open-redirect proof?next= and OAuth callback bounces use a URL-parser + origin match (rejects //evil.com, /\evil.com, data:, javascript:)
  • RBAC fail-closed — DB outage during user-state lookup no longer keeps a disabled user authorised; falls back to last-known-good cache or treats as disabled
  • view-as-role allow-list — header is now restricted to known roles
  • Login error tokens i18n — backend returns UPPER_SNAKE tokens (ACCOUNT_DISABLED, INVALID_CREDENTIALS) so the frontend can localise per-user
  • Webhook auth — timing-safe API key comparison
  • Process-level guardsuncaughtException / unhandledRejection log to AppLog then exit hard so the supervisor respawns cleanly

Performance

  • SQLite WAL mode enabled — readers no longer block while a write transaction holds the lock; the UI stays fluid during long syncs
  • Fire-and-forget admin job triggersPOST /admin/jobs/:key/run returns 202 immediately and the frontend polls for completion
  • Per-key job mutex — same job can't double-run when a manual trigger collides with a cron tick
  • Hero progress bars — switched from width: 0% → 100% (per-frame layout) to GPU-composited transform: scaleX for smooth animation under load
  • Backup asyncrunAutoBackup no longer blocks the event loop during scheduled backups

Sync Subsystem (audit fixes)

  • TV placeholder upgrade — Sonarr's real tmdbId is now written at sync time, so homepage availability pills match without a click. Existing placeholder rows are merged into their canonical positive-tmdbId row on the next full sync, deduping requests by userId
  • Race-safe upgradeprocessSingleMedia uses try-update / catch-P2002 / fall-back-to-merge instead of probe-then-act, so a concurrent findOrCreateMedia can no longer race the unique constraint
  • Webhook 'added' parity — webhooks now enrich freshly-created rows with poster / quality / seasons via getMediaById instead of leaving them poster-less until the next periodic sync
  • Notification fan-out — silent .catch(() => {}) on sendAvailabilityNotifications replaced with logEvent, so failed pushes for 'media available' surface in the admin Logs tab
  • Keyword sync — transient TMDB errors no longer poison keywordIds = '[]' permanently; the sentinel is written only on explicit not-found (404 / 422)

Architecture Refactors

  • Backend bootstrap splitindex.ts reduced to start orchestration; security / docs / routes / plugins / static / jobs each in its own bootstrap/ module
  • Routes decompositionroutes/requests and routes/admin/services each split into focused submodules (create, list, lifecycle, maintenance, …)
  • Admin tabs — Plugins and Services tabs separated into hooks + presentational components (usePluginsTab, useServicesTab)
  • TMDB submodule — large services/tmdb.ts split into list / details / helpers
  • Shared media helpersfindMediaByExternalId and resolveTvdbId extracted from three duplicated inline patterns
  • Notification translation — single source of truth in @oscarr/shared/notificationTemplates; backend pre-translates at emit time so plugin subscribers receive readable text

UI / UX

  • Admin layout — viewport-locked: top bar + sidebar fixed, only the content area scrolls (no more empty band at the bottom of the menu when scrolling)
  • Centered admin search — desktop search bar now matches the /home topbar layout (absolute centered, max-w-lg)
  • Mobile inputs — 16px font-size on iOS Safari to suppress focus-zoom
  • Sidebar search results — semantic groups + warning indicators for tabs needing attention

Backup

  • Restore + rollback double-failureapplyDbBuffer returns a structured { ok, safetyPath, rollbackFailed, error } shape; the route surfaces a dedicated 500 + AppLog entry pointing at the safety copy when the rollback also fails
  • HMAC-signed backups — restore gates HMAC + admin password re-auth + SQLite magic byte check

Image hardening

  • Stripped npm, corepack, and yarn from the production image — none are needed at runtime, and they regularly ship transitive CVEs the scanner picks up. prisma migrate deploy at boot now calls the local binary directly instead of going through npx

Full changelog: v0.6.3...v0.7.0

Upgrading

```
docker pull ghcr.io/arediss/oscarr:0.7.0
```
or
```
docker pull ghcr.io/arediss/oscarr:latest
```

WAL is enabled automatically on first boot. The next full_sync (manual or scheduled) will merge any duplicate TV rows created before this version.