Skip to content

Commit e43a29d

Browse files
lolimmlostclaude
andcommitted
refactor: Extract settings.html inline script + JSON data blocks
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>
1 parent a4fe1c2 commit e43a29d

2 files changed

Lines changed: 72 additions & 65 deletions

File tree

src/web/static/settings.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Alpine factory for the settings page. Initial state (current config + which
2+
// keys are overridden) is rendered server-side into data-* attributes on a
3+
// hidden #settings-init-data element so this script can stay CSP-clean —
4+
// no inline JSON, no inline scripts, no eval beyond Alpine's own expression
5+
// evaluator.
6+
function settingsPage() {
7+
const dataEl = document.getElementById('settings-init-data');
8+
return {
9+
config: JSON.parse(dataEl.dataset.config),
10+
overrides: JSON.parse(dataEl.dataset.overrides),
11+
message: '',
12+
messageError: false,
13+
init() {},
14+
isOverridden(key) {
15+
return key in this.overrides;
16+
},
17+
async saveGeneral(attr, value, event) {
18+
await this.saveOverride(`general.${attr}`, value, event);
19+
},
20+
async saveJob(jobName, attr, value, event) {
21+
await this.saveOverride(`jobs.${jobName}.${attr}`, value, event);
22+
},
23+
flashEl(event, cls) {
24+
if (!event) return;
25+
const el = event.target.closest('label') || event.target;
26+
el.classList.remove('flash-save', 'flash-error');
27+
void el.offsetWidth;
28+
el.classList.add(cls);
29+
el.addEventListener('animationend', () => el.classList.remove(cls), { once: true });
30+
},
31+
async saveOverride(key, value, event) {
32+
try {
33+
const res = await fetch(rootPath + '/api/config', {
34+
method: 'PATCH',
35+
headers: { 'Content-Type': 'application/json' },
36+
body: JSON.stringify({ updates: { [key]: value } }),
37+
});
38+
if (res.ok) {
39+
this.overrides[key] = value;
40+
this.flashEl(event, 'flash-save');
41+
} else {
42+
this.flashEl(event, 'flash-error');
43+
}
44+
} catch {
45+
this.flashEl(event, 'flash-error');
46+
}
47+
},
48+
async resetOverrides() {
49+
if (!confirm('Reset all runtime overrides to YAML defaults? This will reload settings from config file.')) return;
50+
try {
51+
const res = await fetch(rootPath + '/api/config/reload', { method: 'POST' });
52+
if (res.ok) {
53+
this.overrides = {};
54+
this.showMessage('Reset to defaults');
55+
setTimeout(() => location.reload(), 500);
56+
}
57+
} catch {
58+
this.showMessage('Error resetting', true);
59+
}
60+
},
61+
showMessage(msg, error = false) {
62+
this.message = msg;
63+
this.messageError = error;
64+
setTimeout(() => this.message = '', 3000);
65+
},
66+
};
67+
}

src/web/templates/settings.html

Lines changed: 5 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
{% extends "base.html" %}
22
{% block title %}Settings - Decluttarr{% endblock %}
33
{% block content %}
4+
<div id="settings-init-data"
5+
data-config="{{ config | tojson }}"
6+
data-overrides="{{ overrides | tojson }}"
7+
hidden></div>
48
<div x-data="settingsPage()" x-init="init()">
59
<h2>Settings</h2>
610

@@ -136,69 +140,5 @@ <h2>Settings</h2>
136140
{% endblock %}
137141

138142
{% block scripts %}
139-
<script type="application/json" id="init-config">{{ config | tojson }}</script>
140-
<script type="application/json" id="init-overrides">{{ overrides | tojson }}</script>
141-
<script>
142-
function settingsPage() {
143-
return {
144-
config: JSON.parse(document.getElementById('init-config').textContent),
145-
overrides: JSON.parse(document.getElementById('init-overrides').textContent),
146-
message: '',
147-
messageError: false,
148-
init() {},
149-
isOverridden(key) {
150-
return key in this.overrides;
151-
},
152-
async saveGeneral(attr, value, event) {
153-
await this.saveOverride(`general.${attr}`, value, event);
154-
},
155-
async saveJob(jobName, attr, value, event) {
156-
await this.saveOverride(`jobs.${jobName}.${attr}`, value, event);
157-
},
158-
flashEl(event, cls) {
159-
if (!event) return;
160-
const el = event.target.closest('label') || event.target;
161-
el.classList.remove('flash-save', 'flash-error');
162-
void el.offsetWidth;
163-
el.classList.add(cls);
164-
el.addEventListener('animationend', () => el.classList.remove(cls), { once: true });
165-
},
166-
async saveOverride(key, value, event) {
167-
try {
168-
const res = await fetch(rootPath + '/api/config', {
169-
method: 'PATCH',
170-
headers: { 'Content-Type': 'application/json' },
171-
body: JSON.stringify({ updates: { [key]: value } }),
172-
});
173-
if (res.ok) {
174-
this.overrides[key] = value;
175-
this.flashEl(event, 'flash-save');
176-
} else {
177-
this.flashEl(event, 'flash-error');
178-
}
179-
} catch {
180-
this.flashEl(event, 'flash-error');
181-
}
182-
},
183-
async resetOverrides() {
184-
if (!confirm('Reset all runtime overrides to YAML defaults? This will reload settings from config file.')) return;
185-
try {
186-
const res = await fetch(rootPath + '/api/config/reload', { method: 'POST' });
187-
if (res.ok) {
188-
this.overrides = {};
189-
this.showMessage('Reset to defaults');
190-
setTimeout(() => location.reload(), 500);
191-
}
192-
} catch {
193-
this.showMessage('Error resetting', true);
194-
}
195-
},
196-
showMessage(msg, error = false) {
197-
this.message = msg;
198-
this.messageError = error;
199-
setTimeout(() => this.message = '', 3000);
200-
},
201-
};
202-
}
203-
</script>
143+
<script src="{{ request.url_for('static', path='settings.js') }}"></script>
204144
{% endblock %}

0 commit comments

Comments
 (0)