Skip to content

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

Open
lolimmlost wants to merge 35 commits intoManiMatter:latestfrom
lolimmlost:feat/web-ui
Open

[NEEDS CODE REVIEWER] Add lightweight web UI for monitoring, activity history & runtime control#337
lolimmlost wants to merge 35 commits intoManiMatter:latestfrom
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

ManiMatter and others added 6 commits November 1, 2025 17:32
Added note about Decluttar V2 release and breaking changes.
AttributeError: 'Response' object has no attribute 'get'
…ntime control

Adds a FastAPI-based web interface that runs alongside the existing job loop
as a sibling asyncio task. Zero new required config — defaults to enabled on
port 9999 and can be disabled via `web.enabled: false` or `WEB_ENABLED=false`.

Key features:
- Dashboard with real-time queue view, instance cards, and live activity feed
- Activity log with search, filtering (by job/arr/action/date), and pagination
- Runtime settings editor (toggle test_run, enable/disable jobs, adjust strikes)
- Download protection via UI (supplements qBit "Keep" tag)
- "Run Now" button to manually trigger a cycle
- SSE-powered live updates — no polling needed for real-time state
- Full REST API with auto-generated OpenAPI docs at /api/docs

Architecture:
- EventBus decouples job system from web layer (no-op when web disabled)
- SQLite via aiosqlite for activity history and config overrides
- Jinja2 + HTMX + Alpine.js frontend — no build step, no Node tooling
- Pico CSS for dark-theme styling

New files: src/web/ (events, database, app, routes, config_manager, templates)
Modified: main.py, job_manager, removal_job, removal_handler, strikes_handler,
          settings (_general, _user_config, _instances), Dockerfile, requirements

All 192 existing tests pass unchanged.

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

Inline Jinja2 tojson in @click attributes was getting double-escaped,
causing raw JS to render as button text. Moved to a queueRow() Alpine
component function instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@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.

Wrap per-instance job runs and download client jobs in try/except so
a Sonarr/Radarr timeout logs an error and continues instead of crashing.
Add main_with_restart() wrapper so even unexpected failures auto-recover
after 30s while the web server stays up independently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@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.

Juan and others added 2 commits March 28, 2026 16:18
When qBittorrent times out during startup, retry up to 5 times (30s apart)
instead of immediately calling sys.exit() which kills the web UI too.
Only exit after all retries are exhausted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…og cleanup

- Add Cache-Control headers to API responses (status, queue, config) to
  reduce unnecessary browser re-requests
- Replace hard-coded ALLOWED_GENERAL_KEYS and ALLOWED_JOB_ATTRS with
  dynamic derivation from General and JobParams classes so they stay in
  sync automatically when settings change
- Add automatic activity log cleanup (entries older than 90 days) on
  startup and daily to prevent disk space exhaustion
- Refactor queue fetching into reusable _fetch_queue helper

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@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)

lolimmlost and others added 7 commits April 5, 2026 17:27
…able

wait_and_exit() called time.sleep() (blocking the async event loop) then
sys.exit() which raised SystemExit — a BaseException that main_with_restart()
didn't catch. This killed the entire process including the web UI.

Now main_with_restart() catches non-zero SystemExit and restarts gracefully,
and wait_and_exit() no longer blocks the event loop with time.sleep().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Partials now return "Waiting for first cycle to complete…" instantly
during startup instead of calling arr APIs that may hang. SSE cycle_end
already triggers auto-refresh so no manual page reload is needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Protected rows now have a green left border and subtle green background.
Protected badge and Unprotect button use green (#2e7d32) instead of blue
for clear visual feedback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The inline <script> in the queue_table partial wasn't executing after
HTMX swaps, so Alpine couldn't find protect()/unprotect(). Moving the
function to the persistent dashboard script block fixes this.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Alpine x-data="queueRow({{ item | tojson }})" broke because tojson
double quotes conflicted with the HTML attribute quotes, and HTMX
swaps don't reinitialize Alpine components. Switched to plain
onclick handlers that work reliably with HTMX partial swaps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a setting is saved, its label flashes green briefly (1.5s ease-out).
On error, it flashes red instead. Replaces the generic top-of-page
"Saved" text with per-field visual feedback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The label background-color alone left visible gaps on the left/right
since labels tightly wrap their content. Added a box-shadow ring in
the same color to fill the surrounding area for a cleaner flash effect.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@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
Copy link
Copy Markdown
Collaborator Author

lolimmlost commented Apr 18, 2026 via email

@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
Copy link
Copy Markdown
Owner

ManiMatter commented Apr 19, 2026

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

ManiMatter and others added 4 commits April 19, 2026 17:28
Move web_enabled, web_host, web_port, and proxy_prefix out of General
into a dedicated Web settings class in src/settings/_web.py, matching
the project's existing pattern for settings sections. Updates all
consumers (main.py, app.py, routes.py, config_manager.py).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document the web UI feature including dashboard, activity log, settings
editor, download protection, REST API, and SSE live updates. Covers
configuration (YAML and env vars), Docker port mapping, and how to
disable the web UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
lolimmlost and others added 6 commits April 19, 2026 11:01
Replace raw SCHEMA string with a MIGRATIONS list and a schema_version
table. Database.init() now runs only pending migrations, making future
schema changes safe for existing databases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace inline onclick handlers with data-* attributes and event
delegation. download_id and arr_name were rendered unescaped in JS
string context — a crafted value could break out of the string.
Event delegation also works correctly with HTMX-swapped content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When web_enabled=False, sys.exit(1) from wait_and_exit() propagated
directly and killed the process. Now both paths use main_with_restart()
so unreachable services trigger a 30s retry instead of a hard crash.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add integrity and crossorigin attributes to Pico CSS, HTMX, and
Alpine.js script/link tags to prevent supply-chain tampering via
compromised CDNs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
uvicorn.Config expects root_path to be a string (default ''), not None.
Passing None caused all HTTP requests to return 400 Bad Request.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous hash was generated without following the unpkg redirect,
resulting in a hash of the redirect page instead of the actual JS file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@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
Copy link
Copy Markdown
Owner

ManiMatter commented Apr 21, 2026

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
Copy link
Copy Markdown
Collaborator Author

lolimmlost commented Apr 28, 2026 via email

@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 and others added 9 commits April 29, 2026 19:26
The activity page fetched /api/activity directly without prefixing
rootPath, breaking the page when running behind a reverse proxy.
Every other fetch in the codebase already uses rootPath.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
api_protect compared content-type with strict equality, so a request
with the standard 'application/json; charset=utf-8' header fell through
and the body was silently dropped — protected_downloads ended up with
title='Unknown' and an empty arr_name. Use startswith() to accept the
charset suffix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
_validate_config_key only checked the attribute portion of a 'jobs.X.attr'
key, so a key like 'jobs.fakefoo.max_strikes' passed validation, was
persisted to config_overrides, then silently no-op'd at apply time
because settings.jobs has no 'fakefoo'. The bogus row stayed in the DB
and was re-read on every restart, polluting the override set forever.

Validate that parts[1] resolves to an actual job object on settings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously the migration ran via executescript(), and the
schema_version row was only updated after the whole loop finished.
If a future migration failed halfway through, partial DDL would be
applied but schema_version would still point at the old version,
making the next startup either re-apply already-applied statements
(fine for CREATE TABLE IF NOT EXISTS, broken for ALTER TABLE) or
silently skip the rest.

Seed the schema_version row up front, then append the version bump
to each migration's script so the version is updated as part of the
same executescript call. SQLite commits each script implicitly, so
either the migration applies and version moves, or neither does.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The SQL fetch for UI-protected download IDs was inside the for-loop
over arr instances, so it ran N times per request even though the
result is identical for every iteration. Hoist it to a single query
before the loop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
uvicorn was hardcoded to log_level=debug regardless of the app's
configured log level, flooding production logs with framework noise.
Translate settings.general.log_level into the uvicorn equivalent and
default to 'info' for unknowns (including the app's custom VERBOSE
level, which uvicorn doesn't recognize).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
asyncio.create_task() returns a Task that the event loop only holds a
weak reference to (per the docs). Discarding the return value lets the
GC collect the task, which can cause it to disappear mid-flight. Both
_mark_first_cycle_done and _periodic_cleanup were fire-and-forget here.

Store the Task handles on app.state so they live as long as the app.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
api_unprotect deleted the protected_downloads row but never emitted an
event, so other browser tabs subscribed to the SSE stream stayed out
of date and the activity log silently dropped unprotect actions
(only protect was recorded).

Add an ITEM_UNPROTECTED EventType, emit it from api_unprotect with
the title/arr_name we look up before the delete, record it in
ActivityRecorder's action_map as 'unprotected', and add the option
to the activity page filter dropdown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The web UI exposes runtime mutation endpoints (toggle test_run,
disable jobs, change min_speed, trigger cycles, delete protected rows)
with no built-in authentication. The PR thread treats this as the
user's responsibility, but the README didn't surface that.

Add a Security subsection recommending bind to localhost + SSH tunnel,
reverse proxy auth, or disabling the UI; warn against exposing
port 9999 directly to the public internet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@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

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.

3 participants