Skip to content

[NEEDS CODE REVIEWER] Add lightweight web UI for monitoring, activity history & runtime control - #337

Open
lolimmlost wants to merge 49 commits into
ManiMatter:devfrom
lolimmlost:feat/web-ui
Open

[NEEDS CODE REVIEWER] Add lightweight web UI for monitoring, activity history & runtime control#337
lolimmlost wants to merge 49 commits into
ManiMatter:devfrom
lolimmlost:feat/web-ui

Conversation

@lolimmlost

Copy link
Copy Markdown
Collaborator

Summary

Decluttarr currently has zero visibility into what it's doing — all config is YAML, all output is logs. This PR adds a lightweight web UI for monitoring, activity history, and runtime control without changing the existing daemon behavior.

  • Dashboard — real-time queue view across all arr instances, instance status cards, live activity feed, "Run Now" button
  • Activity Log — searchable, filterable, paginated history of every action (flags, removals, recoveries, strikes) stored in SQLite
  • Settings Editor — toggle test_run, enable/disable jobs, adjust max_strikes/min_speed at runtime without editing YAML or restarting
  • Download Protection — protect individual downloads from removal via the UI (supplements the qBit "Keep" tag)
  • REST API — full JSON API with auto-generated OpenAPI docs at /api/docs
  • SSE Live Updates — server-sent events push changes to the browser in real time

Tech Choices

Component Choice Why
Web framework FastAPI Async-native (shares existing asyncio loop), lightweight, built-in OpenAPI
Frontend Jinja2 + HTMX + Alpine.js No build step, no Node tooling in a Python project
Styling Pico CSS (dark theme) Classless CSS, minimal custom styles needed
Persistence SQLite via aiosqlite Zero config, file-based, auto-creates on first run
Real-time Server-Sent Events Simpler than WebSockets, unidirectional, HTMX-compatible

Architecture

The web server runs as a sibling asyncio task alongside the existing main loop — both share the same event loop and process memory. An EventBus class decouples the job system from the UI: jobs emit events at decision points, the web layer (ActivityRecorder + SSE) consumes them. When web is disabled, a NoOpEventBus is used with zero overhead.

Job System → EventBus → ActivityRecorder (writes SQLite)
                      → SSE endpoint (pushes to browser)

Browser → FastAPI API → reads Tracker state (queue/strikes)
                      → reads/writes SQLite (activity, config, protected)
                      → mutates Settings object (runtime config)

Database Schema (SQLite)

Three tables: activity_log (action history), protected_downloads (UI-managed protection), config_overrides (runtime config layered on top of YAML). Auto-created at ./data/decluttarr.db.

API Endpoints

Method Path Purpose
GET /api/status Uptime, test_run state, instance count
GET /api/queue Current queue across all arr instances with strike info
GET /api/activity Paginated activity log with filters
GET /api/strikes Current strike data across all trackers
POST/DELETE /api/protected/{id} Protect/unprotect a download
GET/PATCH /api/config Read/update runtime config
POST /api/config/test-run Toggle test_run on/off
POST /api/config/reload Reset overrides to YAML defaults
GET /api/events SSE stream for real-time updates
POST /api/trigger Manually trigger a job cycle

Configuration

Zero new required config. Defaults to enabled on port 9999.

# config.yaml (optional)
web:
  enabled: true    # or WEB_ENABLED=false to disable
  host: "0.0.0.0"
  port: 9999

Migration / Backward Compatibility

  • Defaults to enabled but works with zero config — existing YAML configs unaffected
  • No new required env vars — all web settings have sensible defaults
  • Event bus is no-op when web is disabled — zero overhead on existing behavior
  • Database auto-creates on first run
  • All 192 existing tests pass unchanged

New Dependencies

fastapi==0.115.6
uvicorn[standard]==0.34.0
aiosqlite==0.20.0
jinja2==3.1.5
python-multipart==0.0.20

Files Changed

New (15 files in src/web/): events.py, database.py, app.py, routes.py, config_manager.py, templates (base, dashboard, activity, settings, 4 partials), static/style.css

Modified (11 files): main.py, job_manager.py, removal_job.py, removal_handler.py, strikes_handler.py, _general.py, _user_config.py, _instances.py, Dockerfile, requirements.txt, config_example.yaml

Screenshots

The UI uses Pico CSS dark theme with color-coded badges for arr instances (Sonarr=blue, Radarr=yellow, etc.), action types (removed=red, recovered=green, flagged=amber), and strike counts.

Test Plan

  • pytest tests/ — all 192 existing tests pass
  • Web UI loads at http://localhost:9999
  • Job loop still runs on timer (verified via logs)
  • Queue table shows downloads with strike/protection status
  • Protect/unprotect buttons work and survive next cycle
  • test_run toggle via settings page takes immediate effect
  • "Run Now" button triggers early cycle
  • Activity log records and displays actions
  • Docker build succeeds with EXPOSE 9999
  • Verify with WEB_ENABLED=false that web is fully disabled
  • Test with multiple concurrent SSE clients

🤖 Generated with Claude Code

@Rubilmax

Copy link
Copy Markdown

Can we get this reviewed and merged please?

@lolimmlost

Copy link
Copy Markdown
Collaborator Author

Can we get this reviewed and merged please?

I appreciate your enthusiasm but definitively needs testing as I'm getting webui errors after a weeklong usage. I'll review the code once again this weekend.

@Rubilmax

Copy link
Copy Markdown

Amazing, thanks 🙏

@lolimmlost

Copy link
Copy Markdown
Collaborator Author

I'm attempting this fix for the crashing.

@lolimmlost

Copy link
Copy Markdown
Collaborator Author

Pushed a fix for a crash that was happening after ~1 week of uptime.

Root cause: When Sonarr/Radarr timed out (read timeout=15s), the unhandled exception propagated up through asyncio.gather(main_task, web_task), which cancelled the web server task too — killing the entire app.

Fix (commit 25f3e2f):

  • Wrapped per-instance job runs and download client jobs in try/except so timeouts log an error and continue to the next cycle
  • Added main_with_restart() wrapper so even unexpected failures auto-recover after 30s while the web UI stays up independently

Verified running 24hrs+ on production with multiple Sonarr/Radarr timeouts — all recovered cleanly on the next cycle, no crashes.

@lolimmlost

Copy link
Copy Markdown
Collaborator Author

I have tested the new fixes with 100% uptime after 48 hours. Ill be working on ui improvements; log pruning and api cache control.
Is pasword protections something we'd like for this but i assume the end user can secure it however they'd like. (cf access)

@ManiMatter

Copy link
Copy Markdown
Owner

hi, I am truly sorry I haven't looked into your PR in such a long time. I do appreciate very much that you took the time to contribute.

Unfortunately, I don't have the time to look into it still.
To overcome me being the bottleneck, I am looking to open this repo up to other people who help maintain it, and contributors can review each others code / merge.

Would you be willing to act as a formal contributor? If yes, I will add you, and if I find others (from open PRs), hopefully you can review each others PR and they can be merged.

Thanks for letting me know, and apologies again for my radio silence.

@lolimmlost

lolimmlost commented Apr 18, 2026 via email

Copy link
Copy Markdown
Collaborator Author

@ManiMatter

Copy link
Copy Markdown
Owner

Hey @ManiMatter. Thanks you for your honesty and I appreciate you wanting this project to continue. I personally would like to be a contributor. However I would need direction / goal in mind that we can work towards together. I am open to discussing what the future of the project may look like. Thanks for this opportunity.

On Sat, Apr 18, 2026 at 4:11 AM ManiMatter @.> wrote: ManiMatter left a comment (ManiMatter/decluttarr#337) <#337?email_source=notifications&email_token=ADBC4Q4G7LPLSTC5DZZ4AZL4WNPHLA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIMRXGM2TCMJVG44KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2LK4DSL5RW63LNMVXHIX3POBSW4X3DNRUWG2Y#issuecomment-4273511578> hi, I am truly sorry I haven't looked into your PR in such a long time. I do appreciate very much that you took the time to contribute. Unfortunately, I don't have the time to look into it still. To overcome me being the bottleneck, I am looking to open this repo up to other people who help maintain it, and contributors can review each others code / merge. Would you be willing to act as a formal contributor? If yes, I will add you, and if I find others (from open PRs), hopefully you can review each others PR and they can be merged. Thanks for letting me know, and apologies again for my radio silence. — Reply to this email directly, view it on GitHub <#337?email_source=notifications&email_token=ADBC4Q4G7LPLSTC5DZZ4AZL4WNPHLA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIMRXGM2TCMJVG44KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2LK4DSL5RW63LNMVXHIX3POBSW4X3DNRUWG2Y#issuecomment-4273511578>, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADBC4Q3BUVJF6NFQEKXD44T4WNPHLAVCNFSM6AAAAACWSAOXR2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DENZTGUYTCNJXHA . You are receiving this because you authored the thread.Message ID: @.>

hi @lolimmlost - Awesome, I am so glad you raise your hand to become a contributor.
I have just added you to the contributor list.

I also started a "Discussion", suggest we take the exchange there on what to focus on next / where to bring the tool from here. #345

@ManiMatter

ManiMatter commented Apr 19, 2026

Copy link
Copy Markdown
Owner

First of all - looks awesome. I think this is a massive improvement for the tool!
Some thoughts on this PR:

  1. Could you update the Readme to point out that there is a UI now, how to use it etc?
  2. This version will create an initial database; how do we deal with a future situation where changes to existing tables are needed? Would it make sense to already now use "alembic" (potentially in conjunction with sqlalchemy?).
    ̶3̶)̶ ̶/̶d̶a̶t̶a̶ ̶s̶h̶o̶u̶l̶d̶ ̶b̶e̶ ̶e̶x̶c̶l̶u̶d̶e̶d̶ ̶v̶i̶a̶ ̶.̶g̶i̶t̶i̶g̶n̶o̶r̶e̶/̶.̶d̶o̶c̶k̶e̶r̶i̶g̶n̶o̶r̶e̶ (done. see commits above)
    ̶4̶)̶ ̶a̶d̶d̶ ̶s̶u̶p̶p̶o̶r̶t̶ ̶t̶o̶ ̶r̶u̶n̶ ̶t̶h̶i̶s̶ ̶b̶e̶h̶i̶n̶d̶ ̶p̶r̶o̶x̶y̶ ̶(̶f̶o̶r̶ ̶i̶n̶s̶t̶a̶n̶c̶e̶ ̶w̶h̶e̶n̶ ̶o̶n̶ ̶c̶o̶d̶e̶s̶e̶r̶v̶e̶r̶)̶ (done. see commits above)
  3. the "web" variables are currently stored in the "general" section of the settings class. I don't think that's right; they should be in "web" - Could you please amend? the proxy variable (4) I added above I also put in general since the web-section doesn't exist yet, please also move it along.
  4. Do you think it could be possible to edit the instances also via the UI? The nested yaml structure to support the different instances is something that comes up in issues again and again. If this could be done more easily via UI, this would be a big win.
  5. Also, managing the settings per the different jobs would be great; which would eliminate the need for users to fight yaml as they could configure everything via UI

@lolimmlost

Copy link
Copy Markdown
Collaborator Author

Hey @ManiMatter, thanks for the detailed feedback! I've addressed your requests in the latest commits:

Done in this PR:

  • Container not starting #1 README — Added a Web UI section covering features, configuration (YAML + env vars), Docker port mapping, and how to disable it (d52a011)
  • Container does not restart after an error #2 Database migration strategy — Added a lightweight schema versioning system with a schema_version table and numbered migrations. Avoids the Alembic dependency (overkill for 3-table SQLite) while making future schema changes safe for existing databases (3685932)
  • Deleting automatically old DEV packages #5 Web settings in own section — Extracted web_enabled, web_host, web_port, and proxy_prefix from General into a dedicated Web settings class in src/settings/_web.py, following the same pattern as _jobs.py, _download_clients.py, etc. Updated all consumers (a05a812)

Already done (by you):

#6 (Instance editing via UI) and #7 (Full job config via UI):
The settings page already supports toggling jobs and adjusting max_strikes/min_speed at runtime (#7 partial). However, full instance editing (#6) and complete job config management (#7) would be significant additions — instance config involves nested YAML structures with API keys and multiple arr types.

Would it make sense to merge this PR as-is and open separate PRs for #6 and #7 as follow-up features? Happy to take those on as a contributor.

Also pushed a few additional fixes while testing:

  • Fixed XSS vulnerability in queue table onclick handlers (switched to data-* attributes with event delegation)
  • Fixed wait_and_exit() regression when web UI is disabled (non-web path now also gets restart-on-failure)
  • Added SRI integrity hashes to CDN-loaded assets (Pico CSS, HTMX, Alpine.js)
  • Fixed uvicorn root_path=None causing 400 Bad Request

@ManiMatter

ManiMatter commented Apr 21, 2026

Copy link
Copy Markdown
Owner

Thank you, @lolimmlost for the additional changes.
I think your proposal makes a lot of sense to keep 6) and 7) separate and this can be merged as first version.

Before merging though I think it would be good if somebody could review this code in more detail, as it is a relatively big addition.

Time wise I won‘t be able to do it myself. I hope somebody volunteers as additional maintainer to you & me and reviews/merges this.

@lolimmlost As you look into 6) and 7), would you be willing to review #311? The author there introduced per-instance overrides, which, if merged, would play into 7) (ie. full config via (UI) thus I see them related.

@ManiMatter ManiMatter changed the title feat: Add lightweight web UI for monitoring, activity history & runtime control [NEEDS CODE REVIEWER] Add lightweight web UI for monitoring, activity history & runtime control Apr 21, 2026
@ManiMatter

This comment was marked as off-topic.

@lolimmlost

This comment was marked as off-topic.

@ManiMatter

This comment was marked as off-topic.

@lolimmlost

lolimmlost commented Apr 28, 2026 via email

Copy link
Copy Markdown
Collaborator Author

@ManiMatter

Copy link
Copy Markdown
Owner

Cool. feel free to take on anything you want, for example open PRs, issues, or suggest new changes as you see fit.
There are definitely items where this tool would benefit from a code overhaul (it's my first python project ever...), thus feel free to take a go at anything you'd like and feel fully free to take any decisions (and tag me in tickets if you want a second opinion) :)

Suggest we update #345 for wider discussions if needed.

@lolimmlost

Copy link
Copy Markdown
Collaborator Author

Hey @ManiMatter, did a self-review pass on this PR and pushed fixes for what I found. One thing I'd like your call on before merging — it came from your a840262 commit so I didn't want to touch it unilaterally.

The thing: in src/web/app.py:51 the proxy support builds:

root_path = f"/{proxy_prefix}/{port}" if proxy_prefix else ""

Embedding the listen port matches code-server's /proxy/<port>/... convention. But users behind nginx/Traefik/Caddy expect a clean prefix like /decluttarr without the port — current code generates a root_path they don't want.

Options:

  1. Keep as-is, document that proxy_prefix is code-server-specific
  2. Drop the port: root_path = f"/{proxy_prefix}", code-server users set proxy_prefix: "proxy/9999"
  3. Add a flag like code_server_mode: true to switch formats

Leaning toward 2 — cleaner, and code-server still works with one extra path segment in config. What do you prefer?

@ManiMatter

Copy link
Copy Markdown
Owner

Hey @lolimmlost, thanks for asking. I agree with your proposal to go for option 2, so that the same setting can be used for any proxy

@Dark3clipse Dark3clipse self-assigned this May 2, 2026
@Dark3clipse

Copy link
Copy Markdown
Collaborator

Hi @lolimmlost, I just joined this project as a maintainer to help out, and I'd like to spend some time to fully review your PR.

@lolimmlost

lolimmlost commented May 2, 2026 via email

Copy link
Copy Markdown
Collaborator Author

@Dark3clipse

Dark3clipse commented May 2, 2026

Copy link
Copy Markdown
Collaborator

So, I've spend my evening reviewing your new GUI. I have deployed your branch in my environment and reviewed the frontend design, usability, and deployment. I have yet to review the code itself, I will do that at a later time.

However, I'd already like to post my initial review. It focuses on the architecture, deployment and usability, and frontend. I've used ai solely to format the review nicely and help me with better sentence structure.

PR Review Feedback

Architectural

1. Optional External Database Support (Future Consideration)

It may be worth considering support for an external database such as PostgreSQL (not required for this PR, but valuable long-term).

Benefits:

  • Keeps decluttarr deployments stateless (no need for persistent volumes)
  • Allows users to leverage existing infrastructure (backups, monitoring, optimization)
  • Many users already running a full arr stack + Plex/Jellyfin likely have PostgreSQL available

2. Configurable SQLite Database Location

It would be useful to allow users to specify where the SQLite database file is stored.

Benefits:

  • Enables better separation of persistent storage (e.g., dedicated PVCs)

Alternative:

  • Clearly document which directory should be mounted/bound for persistence

3. Frontend Dependency on External CDNs

The web app currently uses a no-build frontend approach, but still depends on runtime external resources (e.g., unpkg.com, jsDelivr for docs).

Concerns:

  • Introduces external runtime dependencies
  • Causes issues in restricted or offline environments
  • Requires additional CSP configuration (e.g., script-src 'self' https://unpkg.com)
  • Adds security considerations

Recommendation:
Vendor the required JS files directly into the repository (e.g., /web/static/*.min.js).

Advantages:

  • No CDN dependency
  • Works in airgapped or restricted environments
  • No CSP adjustments required for reverse proxies
  • Keeps the current lightweight, no-build approach

4. CSP Compatibility & Inline Scripts

Testing with a stricter Content Security Policy revealed that the frontend relies on inline scripts.

To make this work, CSP would require:

'unsafe-inline'

This is not recommended: https://content-security-policy.com/unsafe-inline/

Additionally, there are indications of eval usage:

Uncaught EvalError: Evaluating a string as JavaScript violates the following Content Security Policy directive...
csp

Recommendations:

  • Move inline scripts into separate JS files
  • Avoid patterns requiring unsafe-inline or unsafe-eval

Note:
This may be related to the use of Alpine.js. If so, it’s worth evaluating whether:

  • Alpine usage can be adjusted
  • Or a simpler approach (e.g., HTMX-only) would reduce CSP friction

5. Support for Read-Only Configurations

It would be beneficial to continue supporting users who provide configuration as read-only (e.g., via infrastructure-as-code workflows).

Use case:

  • Users define config in Git and mount it into the container
  • Prefer not to mutate config at runtime

Suggestion:

  • Detect read-only config files or introduce a readonly mode
  • Disable or grey out UI controls that modify configuration

This still allows the UI to provide value for visibility and monitoring.


⚙️ Usage

1. Historical Runs Overview (Future Feature)

It would be useful to:

  • View a list of past runs
  • Click into a run to inspect detailed logs/tracing

2. Download Queue Integrations (Future Feature)

Enhancements could include clickable links to:

  • Open the corresponding *arr instance (highlighting the item)
  • Open the download client (optionally with qui support)

Frontend

1. Responsiveness / Layout Issues

The Download Queue section currently introduces a horizontal scrollbar (especially noticeable on vertical monitors).
scrollbar

Observations:

  • The “Status” column appears too wide
  • Layout does not scale well on smaller screens
  • All table views are affected

Suggestions:

  • Improve responsiveness so rows wrap instead of overflowing
  • On small screens (e.g., mobile), allow rows to grow vertically instead of forcing horizontal scroll
  • Ensure all list views respect .container padding

2. Spacing Improvements

Some additional vertical spacing would improve readability:

  • Between instance cards
  • Between the “Run Now” / toggle controls
  • Above the download queue

3. Pagination for Large Lists

Currently, large lists (e.g., Download Queue) render all items at once.

Issue:

  • Requires excessive scrolling (e.g., 700+ items before reaching “Recent Activity”)

Suggestion:

  • Introduce pagination (as already done on the Activity page)
  • Apply consistently across list views

4. Project Branding / Favicon

It might be a good time for us to introduce:

  • A simple project logo
  • A favicon for browser tabs

This improves usability when multiple tabs are open. @ManiMatter how do you feel about this?


5. Pager Styling

The current pager UI feels slightly cramped.

Suggestion:

  • Consider using display: flex for better spacing/alignment

(This improved the layout in local testing, but open to preference.)
flex


✅ Summary

Overall, I really enjoy this new addition to decluttarr, and I think it is very valuable. I think it is a great next step for the project, and I'd like to thank you for your contribution.

The main concerns in my review center around:

  • Eliminating external frontend dependencies (CDNs)
  • Improving CSP compatibility
  • Maintaining compatibility with common deployment patterns (stateless, read-only config)
  • Enhancing usability for larger datasets and smaller screens

Addressing these will significantly improve robustness and maintainability, as well as deliver an even snappier frontend to our users.

@ManiMatter

ManiMatter commented May 2, 2026

Copy link
Copy Markdown
Owner

Really appreciate you take the time to review this thoroughly.
One item that got my attention is this one:

1. Optional External Database Support (Future Consideration)

It may be worth considering support for an external database such as PostgreSQL (not required for this PR, but valuable long-term).

Benefits:

  • Keeps decluttarr deployments stateless (no need for persistent volumes)
  • Allows users to leverage existing infrastructure (backups, monitoring, optimization)
  • Many users already running a full arr stack + Plex/Jellyfin likely have PostgreSQL available

Could you please elaborate why a external DB would be handy? My worry is that adding PostGresDB would create an additional external dependency which a) might increase application/development complexity, b) might add setup complexity for users (for instance, when running standalone python script outside docker enviornment)

Re your point on "Many users already running a full _arr stack" - That definitely applies to me (using Plex, Radarr, etc), but I for instance do not use PostGres for any of these applications but rely on their built-in DBs. Until you suggested this addtion here, I wasn't even aware an external DB could be used (and haven't really understood the point yet tbh, but since the feature exists and you ask for it, I'm sure there is good reasons for it)

I am not an expert in DBs at all, thus pls don't get discouraged by my thoughts, keen to hear your perspectives.


on this point:

  1. Project Branding / Favicon
    It might be a good time for us to introduce:

A simple project logo
A favicon for browser tabs
This improves usability when multiple tabs are open.

Like the idea. Feel free to create any icon you think makes sense; we can always change it in the future for something else if anybody has strong feelings.

@Dark3clipse

Copy link
Copy Markdown
Collaborator

Ah I can elaborate on the benefits of an external DB, certainly.

SQlite benefits

Sqlite is my no means a bad default. It serves a good purpose:

  • zero configuration
  • no additional services required, making it quicker and simpler to deploy decluttarr for most users
  • Perfect for small, single-instance setups

My recommendation is not to replace sqlite, rather I think it should be the default db option.

I'm kind of a power user myself, since I run a homelab with kubernetes. And specifically for such use cases, the benefits of external database support in applications becomes visible. So please don't consider my suggestion a must :) But I think it is not a lot of effort to implement external db support (I'd like to contribute myself) especially given that a db will be a new component for the project.

External database support is beneficial when users run more advanced setups

  • large docker compose stacks
  • Kubernetes
  • full homelabs

Drawbacks of sqlite

1. Stateful storage requirement

  • SQLite requires a local file
  • This means:
    • Docker → bind mounts or named volumes
    • Kubernetes → PersistentVolumeClaims (PVCs)
  • This introduces:
    • Storage lifecycle management
    • Backup complexity
    • Migration concerns

For example, in my k8s setup, I'd need to create a PVC and configure snapshot and backup schedule for it separately, whereas I already have a good snapshot/backup configuration for my postgres instance.
image

2. Reduced portability

  • A container is no longer self-contained
  • Deployments now depend on external storage configuration
  • Moving workloads between nodes (Kubernetes) becomes more complex

Again, not a problem at all for most users, but homelab users will need extra effort.

3. Concurrency limitations

  • SQLite has limited support for concurrent writes
  • While this may not be an issue today, the introduction of a web UI + background jobs increases the chance of:
    • overlapping operations
    • locking contention

I don't think we will run into this given the scope of this project.

4. Scaling constraints

  • SQLite does not scale beyond a single instance
  • Running multiple replicas (even accidentally) can lead to:
    • database corruption risks
    • undefined behavior

You can see for example Jellyfin is working towards migrating from a local db to support for external db:
https://jellyfin.org/posts/jellyfin-release-10.11.0/

Which will enable them to scale. In practice, this would enable users to run multiple instances of jellyfin with failover when one of them becomes unstable to bring more stability towards users of their Jellyfin deployment.

For decluttarr this is not relevant, but it serves of an example why external databases can scale better.

Benefits of External Databases (e.g., PostgreSQL)

Supporting an external database unlocks several operational advantages:

1. Truly stateless application containers

  • No local storage required
  • Containers can be:
    • ephemeral
    • easily rescheduled
    • horizontally scalable (if ever needed)

This aligns well with Kubernetes best practices.


2. Easier backups and disaster recovery

  • Many users already have:
    • automated backups
    • replication
    • monitoring
  • Decluttarr can integrate into existing database infrastructure instead of introducing a new backup surface

3. Reuse of existing infrastructure

  • Users running:
    • *arr stack
    • Plex / Jellyfin
    • other services

often already operate PostgreSQL or MariaDB instances.

Allowing reuse:

  • reduces duplication
  • simplifies operations
  • improves consistency across services

4. Better support for multi-instance or future growth

  • Even if not a current goal, external DBs enable:
    • multiple workers
    • separation of UI and worker components
    • safer concurrent access patterns

5. Improved reliability in orchestrated environments

  • Kubernetes scheduling becomes simpler:
    • no need to co-locate pods with storage
    • no risk of losing local state on rescheduling
  • Works better with:
    • rolling updates
    • scaling events
    • node failures

Docker Compose Perspective

Even in Docker Compose setups, external DBs can be beneficial:

  • Many users already define a shared postgres service
  • Centralized backups are easier than managing multiple SQLite files
  • Cleaner separation of concerns between:
    • application
    • data layer

I've had in the past when I was using docker-compose myself problems with sqlite corruption in my bazarr instance, forcing me to start over. For docker-compose users, it would be easier to configure postgres and make sure that that is properly backed up, than to make sure each and every application that uses sqlite has proper backup.

That said, SQLite should remain a first-class default for simple setups.

Recommended Approach

What I would recommend is the following:

  • don't spend more attention to this topic for this PR
  • Keep SQLite as the default (zero-config experience)

After this has been merged, I'll start a new branch and experiment with external db support:

  • Add optional support for an external database (e.g., PostgreSQL)
  • Allow configuration via environment variables
  • Clearly document both modes:
    • “simple mode” (SQLite)
    • “advanced mode” (external DB)

@ManiMatter

Copy link
Copy Markdown
Owner

Really appreciate the detailed answer and explanation, I enjoyed the thoughtful read and learnt something today.
Great idea to park this for now and thank you for looking into this once this PR is merged.

lolimmlost and others added 3 commits May 24, 2026 21:20
Two CSP-relevant changes here:

1. Move the settingsPage() Alpine factory + flash animation logic out
   of the inline <script> block in settings.html into static/settings.js.

2. Replace the two <script type="application/json"> blocks (which CSP
   blocks under script-src even though they aren't executable) with
   data-config / data-overrides attributes on a hidden
   #settings-init-data div. Same Jinja autoescape, just an HTML
   attribute instead of a script tag, so script-src doesn't apply.

settings.js reads the data attrs at factory-call time and parses with
JSON.parse, returning the populated state object so Alpine's initial
binding evaluation has the data it needs (avoids null-deref on
x-model="config.general.test_run" before init).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move the activityLog() Alpine factory (filter state + paginated
/api/activity fetch + timestamp formatting) out of the inline <script>
block into static/activity.js. Same load-order story as the other
extractions: external script in {% block scripts %} executes during
body parse, before Alpine's deferred init.

This is the last template-level inline <script>. Combined with the
previous extractions, the only remaining script tags in the rendered
HTML are <script src="..."> references — no inline content.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a CSP subsection to the web UI README documenting the policy the
UI works under after the Tier 2 changes (vendored deps, no inline
scripts, no inline JSON data blocks):

  default-src 'self';
  script-src  'self' 'unsafe-eval';
  style-src   'self';
  connect-src 'self';
  img-src     'self' data:;

Two notable properties:

- No external CDN allowlist needed — Pico/HTMX/Alpine are all vendored.
- 'unsafe-inline' is not required for script-src — all JS is in
  external .js files now.

The only relaxation we still need is 'unsafe-eval' because Alpine.js
compiles x-data / x-show / x-text expressions with new Function(...).
A follow-up PR can migrate to the alpinejs-csp build to drop that
requirement; this README note flags the dependency explicitly so users
deploying behind strict CSP know what to allow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ManiMatter

Copy link
Copy Markdown
Owner

@Dark3clipse - just checking if you are still planning on doing a review on this PR? would love this to be merged but unfortunately don't have the time to review it myself

Long status_messages lists (e.g. a full season of missing episodes)
rendered unbounded and stretched the row into a huge vertical strip.
Cap the message list with max-height + overflow-y:auto so it scrolls
within the cell, and break long filenames with overflow-wrap.

Title truncation moved off the <td> (where text-overflow is unreliable
under table-layout: auto) onto a block-level inner .truncate span.

Both inline style="" attributes removed and replaced with classes in
style.css, keeping the UI compliant with the documented style-src 'self'
CSP (no 'unsafe-inline').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ManiMatter ManiMatter changed the title Add lightweight web UI for monitoring, activity history & runtime control [NEEDS CODE REVIEWER] Add lightweight web UI for monitoring, activity history & runtime control Jun 20, 2026
lolimmlost and others added 2 commits June 22, 2026 20:22
Season packs with many episode files blew out row height. Status
messages now show only the first entry with a collapsible "+N more"
toggle for the rest, keeping the table compact by default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Title column now wraps to 2 lines before truncating instead of cutting
off on one line. Status column shows only the badge; hover it to see
the full status messages in a native tooltip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ManiMatter

Copy link
Copy Markdown
Owner

@jrhager84 would you be ok to review this?

@jrhager84

Copy link
Copy Markdown
Collaborator

@jrhager84 would you be ok to review this?

Yeah - I'm going to be working through the reviews and rebase etc.

@ManiMatter

Copy link
Copy Markdown
Owner

Awesome. Having a UI will improve the user experience significantly 🕺

@ManiMatter ManiMatter assigned jrhager84 and unassigned Dark3clipse Jul 13, 2026
@ManiMatter
ManiMatter requested review from jrhager84 and removed request for Dark3clipse July 13, 2026 17:25
@jrhager84

Copy link
Copy Markdown
Collaborator

Ok - I'm digging in and this is going to be a BIG change, IMO. I wanted to settle on something before I keep going:

I think the switch to sqlite is the better move, but I think the core should own the data layer as the source of truth, and the UI should derive from it. In that sense the UI could explode and the core UX wouldn't even notice.

So I think I should migrate the values into sqlite and get a nice deterministic path (so existing config users don't blow up) as a base on dev. then - I can make the UI changes around that. Thoughts, guys?

@ManiMatter @lolimmlost @Dark3clipse

@ManiMatter

Copy link
Copy Markdown
Owner

I am not sure I entirely understand the question; let me try to answer with my perspective based on how I interpret it.

  1. Choice of db
    Since you wrote „ think the switch to sqlite is the better move“, I believe the consensus discussed above was to use sqllite as default, with ability to use postgres optionally (future, separate PR)

  2. source of truth
    if you can change config in docker file (env vars), via config file, and via ui: which one counts? When the container restarts? Potentially, what user sees in config file/docker may be different than runtime config

i would propose that

  • env vars take precedence over config file
  • Config file over db
  • UI (from db) over built-in defaults

in other words, when user configures nothing, the defaults are pushed to db and used. If user overwrites in UI, this changes db. If user sets config via file or env var, this overrides db value (which will always reset when contsiner restarts).

this makes db golden source, with ability to override.

wdyt?

@lolimmlost

Copy link
Copy Markdown
Collaborator Author

+1 on ManiMatter's precedence model:

env vars > config file > DB > defaults

DB as the golden source with file/env overrides resetting on container restart makes sense. Users who only touch the UI get persistence, users who set env vars or config files get deterministic behavior across restarts. Clean separation.

Agreed on SQLite as default with optional Postgres down the road in a separate PR.

@jrhager84

Copy link
Copy Markdown
Collaborator

I am not sure I entirely understand the question; let me try to answer with my perspective based on how I interpret it.

  1. Choice of db
    Since you wrote „ think the switch to sqlite is the better move“, I believe the consensus discussed above was to use sqllite as default, with ability to use postgres optionally (future, separate PR)
  2. source of truth
    if you can change config in docker file (env vars), via config file, and via ui: which one counts? When the container restarts? Potentially, what user sees in config file/docker may be different than runtime config

i would propose that

  • env vars take precedence over config file
  • Config file over db
  • UI (from db) over built-in defaults

in other words, when user configures nothing, the defaults are pushed to db and used. If user overwrites in UI, this changes db. If user sets config via file or env var, this overrides db value (which will always reset when contsiner restarts).

this makes db golden source, with ability to override.

wdyt?

To your point @lolimmlost — I agree that env > config > DB > defaults is deterministic. My concern is what that looks like to the user.
If a config file or environment variable is taking priority, somebody could “change” a value in the UI and have it do absolutely nothing. Unless we surface why, that’s going to look broken.
SQLite seems like the right default. It’s lightweight, requires basically no setup, and I can’t think of anything Decluttarr currently needs that it wouldn’t handle. The consensus around SQLite also seemed pretty strong.
I don’t think we need to support multiple databases in this PR. We could keep the database access behind a small adapter/repository layer so someone can add a “bring your own database” option later without rewriting the core.
I think the proposed precedence makes sense for determining the effective value:
environment > config file > DB > defaults
I’d just want the core to track where that effective value came from and expose that information to the UI.
If a value is coming from the DB, the user can edit it. If it’s being overridden by config.yaml, the UI should say something like “Managed by config.yaml” and not pretend the field is editable. Same thing for environment variables, except those would always remain externally managed because Decluttarr can’t change its own container environment.
For existing users, the first startup could find their config, validate it, initialize the DB from those values, and continue without changing any behavior. The config would remain authoritative until they explicitly choose something like “Import config and manage through Decluttarr.”
That action would import the current file into SQLite and stop loading it as an active configuration source. We’d preserve the file, and the user could always re-import it later. A new installation with no config could just initialize the DB from defaults and be managed through the UI or eventually a CLI.
That’s what I meant by making the DB the source of truth “with an asterisk.” The core owns the values and their origin, and the UI is just another way to view or change them. No invisible overrides, no UI changes that silently do nothing, and no ambiguity about what Decluttarr is actually using.
Thoughts?

@ManiMatter

Copy link
Copy Markdown
Owner
  1. different db:
    I would argue this Should be in this repo (not separate) as this is also how the arr-apps work (opt-in to use postgres instead of default sqlite). but built in separate PR

  2. authoritative source
    I think we agree on precedence (env>config>db>defaults)
    Only question is UI whether user can see which items are editable vs externally managed. If we can disable certain controls & show (like a tooltip) why (managed in config, env var) that be great. Afraid how difficult it is; leave that to @lolimmlost to opine

@Dark3clipse hope you are still reading along. You brought great thinking to this PR, in case you want to chime in

@jrhager84

Copy link
Copy Markdown
Collaborator

I think we’re mostly aligned, but I want to clarify two things in case I confused anybody:

  1. The database layer would live in this repository. I just don’t think it should be tightly intertwined with the UI service itself. The core owns the data and the UI uses it.

SQLite seems completely reasonable as the default. It’s lightweight, requires no additional service, and I can’t think of anything Decluttarr currently needs that it wouldn’t handle. PostgreSQL support could absolutely be added here in a separate PR later, but I’m not sure it’s worth building the abstraction until somebody actually takes that on. We can keep the boundary clean now without maintaining multiple database implementations.

  1. I agree that the effective precedence should be:
    environment > config file > DB > defaults

The clarification I’ve arrived at is that we also need an explicit concept of who currently owns the configuration.
If an existing user upgrades and has a config file, they should stay in file-managed mode by default. Nothing changes for them. The core can normalize those values into SQLite, but the config remains authoritative. The UI would show that those values are managed by config.yaml and not pretend that changing them in the UI would do anything.
If there is no config file, a new install can default to Decluttarr-managed mode, where SQLite is authoritative and the UI or future CLI can edit it directly.
We could then provide an explicit action like:
“Import config and manage through Decluttarr.”
That wouldn’t just be a casual toggle. We’d validate the file, show the differences, import it transactionally, preserve the original file, and then switch ownership to the DB.
Once the DB is authoritative, I still think Decluttarr should notice if the config file changes. It shouldn’t automatically apply it, but it could show:
config.yaml has changed since it was last imported.
[Review changes] [Import changes] [Ignore this version]

If only comments or formatting changed and the normalized values are still identical, there’s nothing to warn about.
Environment variables would always remain externally managed and above either mode. The UI would show which environment variable controls the value and keep that field locked—we shouldn’t import those into SQLite, especially since they may contain secrets.
So the flow stays unidirectional and deterministic:
Existing config users keep their current behavior.
New users can be DB-managed from the start.
Nobody can make a UI change that silently does nothing.
Moving between file-managed and DB-managed configuration is explicit and reviewable.
The UI is still just another mechanism for interacting with core-owned state.
If that matches what you both mean, I think we have the basic model settled. Thoughts?

@ManiMatter

Copy link
Copy Markdown
Owner

For postgres:
@lolimmlost offered coding this abstaction separately and since he authors also this PR, I’d trust his judgement how to pave the way in this PR so that the postgres option later can be added with reasonable effort

For source of truth:
Are we overcomplicating by thinking of an „import and review mode“?
If we follow the principle that a) external config (env or file) overwrites db value and b) ui flags those as immutable, then upon removing the external config (whose values now live in db) they become editable.

Thus import becomes as simple as: run your decluttarr, first time your preexisting config gets written to db, you check UI that tells you the values are externally managed, you remove your preexisting config, now you can edit through UI and have essentially migrated.

Not covered: how you‘d migrate from sqllite to postgres (as at that point you dont have a yml file you csn feed to postgres) as you removed that when you introduced sqlite

wdyt?

@lolimmlost

lolimmlost commented Jul 18, 2026 via email

Copy link
Copy Markdown
Collaborator Author

@lolimmlost

Copy link
Copy Markdown
Collaborator Author

Agree with Mani -- the simpler model covers the real use cases without the cognitive overhead of import workflows and ownership modes.

Concrete proposal:

  1. On every startup, current effective config (env > file > defaults) gets written to DB
  2. UI reads from DB. Fields whose value came from env or config file are flagged as immutable with a tooltip ("Set by environment variable" / "Set by config file")
  3. Fields only living in DB (no external override) are editable in UI
  4. User removes their config file or env var, next restart those values stay in DB and become editable. That's the "migration" -- no explicit import action needed

jrhager84's point about UI changes silently doing nothing is valid, but the immutable flag solves that without needing ownership modes or change-detection between file and DB. If a field is locked, the user sees why. If it's unlocked, their change sticks.

For the postgres abstraction: I'll keep the DB access behind a thin repository layer in this PR so a future swap is straightforward, but won't build a multi-backend adapter until someone actually writes the postgres PR.

@ManiMatter

Copy link
Copy Markdown
Owner

Genuinely curious where you stand on this 😊 (no nudging intended)

@jrhager84

Copy link
Copy Markdown
Collaborator

Genuinely curious where you stand on this 😊 (no nudging intended)

I posted in another thread. Had some pretty significant family emergencies, so I got pulled away. I apologize. I'm trying to get back into here very shortly. I have most of a concept and will try to push through it this week or next. Apologies - It's been a rough few weeks.

@ManiMatter

Copy link
Copy Markdown
Owner

Yes I saw it, really hope things sort themselves out as best can be for you family @jrhager84 🙏

Reading above I thought @lolimmlost was working on it next, thus my question was more to him 😊

@jrhager84

Copy link
Copy Markdown
Collaborator

Ah - I must've misread. Either way.

@lolimmlost

Copy link
Copy Markdown
Collaborator Author

Reading above I thought @lolimmlost was working on it next, thus my question was more to him

That's right — I'm picking this up next week. I'll work from the punch list and the config-precedence model we landed on (env vars > config file > DB > defaults), and get the outstanding items pushed so this is ready for review.

@jrhager84 no rush at all on your end — take the time you need with your family. I'll ping here once I've got the changes up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants