[NEEDS CODE REVIEWER] Add lightweight web UI for monitoring, activity history & runtime control - #337
[NEEDS CODE REVIEWER] Add lightweight web UI for monitoring, activity history & runtime control#337lolimmlost wants to merge 49 commits into
Conversation
|
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. |
|
Amazing, thanks 🙏 |
|
I'm attempting this fix for the crashing. |
|
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 Fix (commit 25f3e2f):
Verified running 24hrs+ on production with multiple Sonarr/Radarr timeouts — all recovered cleanly on the next cycle, no crashes. |
|
I have tested the new fixes with 100% uptime after 48 hours. Ill be working on ui improvements; log pruning and api cache control. |
|
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. 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. |
|
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 also started a "Discussion", suggest we take the exchange there on what to focus on next / where to bring the tool from here. #345 |
|
First of all - looks awesome. I think this is a massive improvement for the tool!
|
|
Hey @ManiMatter, thanks for the detailed feedback! I've addressed your requests in the latest commits: Done in this PR:
Already done (by you):
#6 (Instance editing via UI) and #7 (Full job config via UI): 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:
|
|
Thank you, @lolimmlost for the additional changes. 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. |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
|
Confirmed! I have accepted your request!
…On Tue, Apr 28, 2026 at 10:05 AM ManiMatter ***@***.***> wrote:
*ManiMatter* left a comment (ManiMatter/decluttarr#337)
<#337 (comment)>
Hey there,
I just re-invited you to become a collaborator, here is what I see on my
end:
image.png (view on web)
<https://github.com/user-attachments/assets/f963153e-948b-441a-a956-4693b8134c79>
It's the first time I'm doing it, please let me know in case you haven't
received an invite.
—
Reply to this email directly, view it on GitHub
<#337 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ADBC4Q6UDWKN76MEV65DY7T4YDQGDAVCNFSM6AAAAACWSAOXR2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DGMZXGQ4TIOBYHA>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
Cool. feel free to take on anything you want, for example open PRs, issues, or suggest new changes as you see fit. Suggest we update #345 for wider discussions if needed. |
|
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 The thing: in root_path = f"/{proxy_prefix}/{port}" if proxy_prefix else ""Embedding the listen port matches code-server's Options:
Leaning toward 2 — cleaner, and code-server still works with one extra path segment in config. What do you prefer? |
|
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 |
|
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. |
|
Yes finallly! I’m excited to see the review.
…On Sat, May 2, 2026 at 8:35 AM darkeclipse ***@***.***> wrote:
*Dark3clipse* left a comment (ManiMatter/decluttarr#337)
<#337 (comment)>
Hi @lolimmlost <https://github.com/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.
—
Reply to this email directly, view it on GitHub
<#337 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ADBC4Q72NXARPHBROYQR5KL4YYISPAVCNFSM6AAAAACWSAOXR2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DGNRUGE2DMMZTGU>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
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 FeedbackArchitectural1. 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:
2. Configurable SQLite Database LocationIt would be useful to allow users to specify where the SQLite database file is stored. Benefits:
Alternative:
3. Frontend Dependency on External CDNsThe web app currently uses a no-build frontend approach, but still depends on runtime external resources (e.g., Concerns:
Recommendation: Advantages:
4. CSP Compatibility & Inline ScriptsTesting with a stricter Content Security Policy revealed that the frontend relies on inline scripts. To make this work, CSP would require: This is not recommended: https://content-security-policy.com/unsafe-inline/ Additionally, there are indications of
Recommendations:
Note:
5. Support for Read-Only ConfigurationsIt would be beneficial to continue supporting users who provide configuration as read-only (e.g., via infrastructure-as-code workflows). Use case:
Suggestion:
This still allows the UI to provide value for visibility and monitoring. ⚙️ Usage1. Historical Runs Overview (Future Feature)It would be useful to:
2. Download Queue Integrations (Future Feature)Enhancements could include clickable links to:
Frontend1. Responsiveness / Layout IssuesThe Download Queue section currently introduces a horizontal scrollbar (especially noticeable on vertical monitors). Observations:
Suggestions:
2. Spacing ImprovementsSome additional vertical spacing would improve readability:
3. Pagination for Large ListsCurrently, large lists (e.g., Download Queue) render all items at once. Issue:
Suggestion:
4. Project Branding / FaviconIt might be a good time for us to introduce:
This improves usability when multiple tabs are open. @ManiMatter how do you feel about this? 5. Pager StylingThe current pager UI feels slightly cramped. Suggestion:
(This improved the layout in local testing, but open to preference.) ✅ SummaryOverall, 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:
Addressing these will significantly improve robustness and maintainability, as well as deliver an even snappier frontend to our users. |
|
Really appreciate you take the time to review this thoroughly.
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:
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. |
|
Ah I can elaborate on the benefits of an external DB, certainly. SQlite benefitsSqlite is my no means a bad default. It serves a good purpose:
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
Drawbacks of sqlite1. Stateful storage requirement
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. 2. Reduced portability
Again, not a problem at all for most users, but homelab users will need extra effort. 3. Concurrency limitations
I don't think we will run into this given the scope of this project. 4. Scaling constraints
You can see for example Jellyfin is working towards migrating from a local db to support for external db: 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
This aligns well with Kubernetes best practices. 2. Easier backups and disaster recovery
3. Reuse of existing infrastructure
often already operate PostgreSQL or MariaDB instances. Allowing reuse:
4. Better support for multi-instance or future growth
5. Improved reliability in orchestrated environments
Docker Compose PerspectiveEven in Docker Compose setups, external DBs can be beneficial:
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 ApproachWhat I would recommend is the following:
After this has been merged, I'll start a new branch and experiment with external db support:
|
|
Really appreciate the detailed answer and explanation, I enjoyed the thoughtful read and learnt something today. |
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>
|
@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>
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>
|
@jrhager84 would you be ok to review this? |
Yeah - I'm going to be working through the reviews and rebase etc. |
|
Awesome. Having a UI will improve the user experience significantly 🕺 |
|
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? |
|
I am not sure I entirely understand the question; let me try to answer with my perspective based on how I interpret it.
i would propose that
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? |
|
+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. |
To your point @lolimmlost — I agree that env > config > DB > defaults is deterministic. My concern is what that looks like to the user. |
@Dark3clipse hope you are still reading along. You brought great thinking to this PR, in case you want to chime in |
|
I think we’re mostly aligned, but I want to clarify two things in case I confused anybody:
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.
The clarification I’ve arrived at is that we also need an explicit concept of who currently owns the configuration. If only comments or formatting changed and the normalized values are still identical, there’s nothing to warn about. |
|
For postgres: For source of truth: 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? |
|
I’d like to make a complete response but I’ve been afk this weekend. Mani
is a genius though. I’ll get back to you on Monday
…On Thu, Jul 16, 2026 at 12:43 PM ManiMatter ***@***.***> wrote:
*ManiMatter* left a comment (ManiMatter/decluttarr#337)
<#337 (comment)>
For postgres:
@lolimmlost <https://github.com/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?
—
Reply to this email directly, view it on GitHub
<#337?email_source=notifications&email_token=ADBC4Q4P2S7CQTCN4I6TSWD5FEV7TA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIOJZGU4TCNZTHE32M4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-4995917397>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ADBC4Q7ZDKUYKABBPNSYHST5FEV7TAVCNFSNUABFKJSXA33TNF2G64TZHM3DSOBVHA2TKMRRHNEXG43VMU5TIMBXGYZTQMRUHEZ2C5QC>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
Agree with Mani -- the simpler model covers the real use cases without the cognitive overhead of import workflows and ownership modes. Concrete proposal:
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. |
|
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. |
|
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 😊 |
|
Ah - I must've misread. Either way. |
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. |




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.
test_run, enable/disable jobs, adjustmax_strikes/min_speedat runtime without editing YAML or restarting/api/docsTech Choices
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
EventBusclass decouples the job system from the UI: jobs emit events at decision points, the web layer (ActivityRecorder + SSE) consumes them. When web is disabled, aNoOpEventBusis used with zero overhead.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
/api/status/api/queue/api/activity/api/strikes/api/protected/{id}/api/config/api/config/test-run/api/config/reload/api/events/api/triggerConfiguration
Zero new required config. Defaults to enabled on port 9999.
Migration / Backward Compatibility
New Dependencies
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.cssModified (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 passhttp://localhost:9999test_runtoggle via settings page takes immediate effectEXPOSE 9999WEB_ENABLED=falsethat web is fully disabled🤖 Generated with Claude Code