Skip to content

Commit a840262

Browse files
committed
Add possibility to run this behind proxy (for instance, on code server)
1 parent 63bb8e6 commit a840262

7 files changed

Lines changed: 41 additions & 26 deletions

File tree

config/config_example.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ web:
6969
enabled: true # Set to false to disable the web UI
7070
# host: "0.0.0.0" # optional. Default: 0.0.0.0
7171
# port: 9999 # optional. Default: 9999
72+
# proxy_prefix: "proxy"
7273

7374
download_clients:
7475
qbittorrent:

src/settings/_general.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class General:
2020
web_enabled: bool = True
2121
web_host: str = "0.0.0.0"
2222
web_port: int = 9999
23+
proxy_prefix: str | None = None
2324

2425
def __init__(self, config):
2526
general_config = config.get("general", {})
@@ -45,6 +46,7 @@ def __init__(self, config):
4546
self.web_enabled = web_config.get("enabled", general_config.get("web_enabled", self.web_enabled))
4647
self.web_host = web_config.get("host", general_config.get("web_host", self.web_host))
4748
self.web_port = web_config.get("port", general_config.get("web_port", self.web_port))
49+
self.proxy_prefix = web_config.get("proxy_prefix", general_config.get("proxy_prefix", self.proxy_prefix))
4850
self.obsolete_tag = general_config.get("obsolete_tag", self.obsolete_tag)
4951
self.protected_tag = general_config.get("protected_tag", self.protected_tag)
5052

src/settings/_user_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
],
4343
"instances": ["SONARR", "RADARR", "READARR", "LIDARR", "WHISPARR"],
4444
"download_clients": ["QBITTORRENT"],
45-
"web": ["WEB_ENABLED", "WEB_HOST", "WEB_PORT"],
45+
"web": ["WEB_ENABLED", "WEB_HOST", "WEB_PORT", "PROXY_PREFIX"],
4646
}
4747

4848

src/web/app.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ async def start_web_server(settings, event_bus: EventBus, trigger_event: asyncio
4545
database = Database()
4646
await database.init()
4747

48+
host = getattr(settings.general, "web_host", "0.0.0.0")
49+
port = getattr(settings.general, "web_port", 9999)
50+
proxy_prefix = getattr(settings.general, "proxy_prefix", None)
51+
root_path = f"/{proxy_prefix}/{port}" if proxy_prefix else None
52+
4853
# Create app
4954
app = create_app(settings, event_bus, trigger_event)
5055
app.state.database = database
@@ -95,17 +100,17 @@ async def _periodic_cleanup():
95100
except Exception as e: # noqa: BLE001
96101
logger.debug(f"Activity log cleanup error: {e}")
97102

98-
host = getattr(settings.general, "web_host", "0.0.0.0")
99-
port = getattr(settings.general, "web_port", 9999)
100-
101103
logger.info(f"Web UI starting on http://{host}:{port}")
104+
if proxy_prefix:
105+
logger.debug(f"Web UI root path:{root_path}")
102106

103107
config = uvicorn.Config(
104108
app,
105109
host=host,
106110
port=port,
107-
log_level="warning",
111+
log_level="debug",
108112
access_log=False,
113+
root_path=root_path,
109114
)
110115
server = uvicorn.Server(config)
111-
await server.serve()
116+
await server.serve()

src/web/templates/base.html

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,28 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>{% block title %}Decluttarr{% endblock %}</title>
7+
<link rel="icon" href="data:,">
78
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
9+
<script>
10+
const rootPath = "{{ request.scope.get('root_path', '') }}";
11+
document.addEventListener('htmx:configRequest', function(evt) {
12+
if (rootPath) evt.detail.path = rootPath + evt.detail.path;
13+
});
14+
</script>
815
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
916
<script src="https://unpkg.com/alpinejs@3.14.8/dist/cdn.min.js" defer></script>
10-
<link rel="stylesheet" href="/static/style.css">
17+
<link rel="stylesheet" href="{{ request.url_for('static', path='style.css') }}">
1118
</head>
1219
<body>
1320
<nav class="container-fluid">
1421
<ul>
15-
<li><a href="/"><strong>Decluttarr</strong></a></li>
22+
<li><a href="{{ request.scope.get('root_path', '') }}/"><strong>Decluttarr</strong></a></li>
1623
</ul>
1724
<ul>
18-
<li><a href="/" {% if request.url.path == "/" %}class="active"{% endif %}>Dashboard</a></li>
19-
<li><a href="/activity" {% if request.url.path == "/activity" %}class="active"{% endif %}>Activity</a></li>
20-
<li><a href="/settings" {% if request.url.path == "/settings" %}class="active"{% endif %}>Settings</a></li>
21-
<li><a href="/api/docs" target="_blank">API</a></li>
25+
<li><a href="{{ request.scope.get('root_path', '') }}/" {% if request.url.path == "/" %}class="active"{% endif %}>Dashboard</a></li>
26+
<li><a href="{{ request.scope.get('root_path', '') }}/activity" {% if request.url.path == "/activity" %}class="active"{% endif %}>Activity</a></li>
27+
<li><a href="{{ request.scope.get('root_path', '') }}/settings" {% if request.url.path == "/settings" %}class="active"{% endif %}>Settings</a></li>
28+
<li><a href="{{ request.url_for('swagger_ui_html') }}" target="_blank">API</a></li>
2229
</ul>
2330
</nav>
2431

@@ -32,4 +39,4 @@
3239

3340
{% block scripts %}{% endblock %}
3441
</body>
35-
</html>
42+
</html>

src/web/templates/dashboard.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ <h3>Recent Activity</h3>
5959
function protectDownload(btn, downloadId, title, arrName) {
6060
btn.disabled = true;
6161
btn.setAttribute('aria-busy', 'true');
62-
fetch('/api/protected/' + encodeURIComponent(downloadId), {
62+
fetch(rootPath + '/api/protected/' + encodeURIComponent(downloadId), {
6363
method: 'POST',
6464
headers: {'Content-Type': 'application/json'},
6565
body: JSON.stringify({title: title, arr_name: arrName})
@@ -72,7 +72,7 @@ <h3>Recent Activity</h3>
7272
}
7373

7474
function unprotectDownload(downloadId) {
75-
fetch('/api/protected/' + encodeURIComponent(downloadId), {
75+
fetch(rootPath + '/api/protected/' + encodeURIComponent(downloadId), {
7676
method: 'DELETE'
7777
}).then(() => htmx.trigger('#queue-container', 'refresh'));
7878
}
@@ -83,7 +83,7 @@ <h3>Recent Activity</h3>
8383
message: '',
8484
evtSource: null,
8585
init() {
86-
this.evtSource = new EventSource('/api/events');
86+
this.evtSource = new EventSource(rootPath + '/api/events');
8787
this.evtSource.addEventListener('item_removed', () => {
8888
htmx.trigger('#queue-container', 'refresh');
8989
htmx.trigger('#activity-feed', 'refresh');
@@ -109,7 +109,7 @@ <h3>Recent Activity</h3>
109109
this.triggering = true;
110110
this.message = '';
111111
try {
112-
const res = await fetch('/api/trigger', { method: 'POST' });
112+
const res = await fetch(rootPath + '/api/trigger', { method: 'POST' });
113113
const data = await res.json();
114114
this.message = data.status === 'triggered' ? 'Cycle triggered!' : 'Could not trigger';
115115
} catch {
@@ -120,7 +120,7 @@ <h3>Recent Activity</h3>
120120
},
121121
async toggleTestRun() {
122122
try {
123-
const res = await fetch('/api/config/test-run', { method: 'POST' });
123+
const res = await fetch(rootPath + '/api/config/test-run', { method: 'POST' });
124124
const data = await res.json();
125125
this.message = `Test Run: ${data.test_run ? 'ON' : 'OFF'}`;
126126
htmx.trigger('[hx-get="/partials/status-bar"]', 'refresh');
@@ -132,4 +132,4 @@ <h3>Recent Activity</h3>
132132
}
133133
}
134134
</script>
135-
{% endblock %}
135+
{% endblock %}

src/web/templates/settings.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,13 @@ <h2>Settings</h2>
136136
{% endblock %}
137137

138138
{% block scripts %}
139+
<script type="application/json" id="init-config">{{ config | tojson }}</script>
140+
<script type="application/json" id="init-overrides">{{ overrides | tojson }}</script>
139141
<script>
140142
function settingsPage() {
141143
return {
142-
config: {{ config | tojson }},
143-
overrides: {{ overrides | tojson }},
144+
config: JSON.parse(document.getElementById('init-config').textContent),
145+
overrides: JSON.parse(document.getElementById('init-overrides').textContent),
144146
message: '',
145147
messageError: false,
146148
init() {},
@@ -157,14 +159,13 @@ <h2>Settings</h2>
157159
if (!event) return;
158160
const el = event.target.closest('label') || event.target;
159161
el.classList.remove('flash-save', 'flash-error');
160-
// Force reflow so re-adding the class restarts the animation
161162
void el.offsetWidth;
162163
el.classList.add(cls);
163164
el.addEventListener('animationend', () => el.classList.remove(cls), { once: true });
164165
},
165166
async saveOverride(key, value, event) {
166167
try {
167-
const res = await fetch('/api/config', {
168+
const res = await fetch(rootPath + '/api/config', {
168169
method: 'PATCH',
169170
headers: { 'Content-Type': 'application/json' },
170171
body: JSON.stringify({ updates: { [key]: value } }),
@@ -182,11 +183,10 @@ <h2>Settings</h2>
182183
async resetOverrides() {
183184
if (!confirm('Reset all runtime overrides to YAML defaults? This will reload settings from config file.')) return;
184185
try {
185-
const res = await fetch('/api/config/reload', { method: 'POST' });
186+
const res = await fetch(rootPath + '/api/config/reload', { method: 'POST' });
186187
if (res.ok) {
187188
this.overrides = {};
188189
this.showMessage('Reset to defaults');
189-
// Reload the page to get fresh config
190190
setTimeout(() => location.reload(), 500);
191191
}
192192
} catch {
@@ -201,4 +201,4 @@ <h2>Settings</h2>
201201
};
202202
}
203203
</script>
204-
{% endblock %}
204+
{% endblock %}

0 commit comments

Comments
 (0)