Use relative asset and API URLs so the UI works under a path prefix

index.html and app.js requested /static/... and /api/... from the site
root, so serving the app behind a reverse proxy at a sub-path (e.g.
https://host/uj/ with the prefix stripped) 404'd every asset and call.
Relative URLs resolve against the page, which is the same at / and at
/uj/; direct access is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015UXA39Sh9coAx4FhRkd8R2
This commit is contained in:
2026-09-14 02:37:26 +09:00
parent 677d09524e
commit e50eff9122
2 changed files with 12 additions and 12 deletions
+8 -8
View File
@@ -100,7 +100,7 @@ async function api(path, options) {
} }
async function loadMetrics() { async function loadMetrics() {
const data = await api('/api/metrics'); const data = await api('api/metrics');
state.metrics = data.metrics; state.metrics = data.metrics;
state.metric = data.default; state.metric = data.default;
const select = el('metric-select'); const select = el('metric-select');
@@ -134,7 +134,7 @@ function renderFileList() {
check.addEventListener('click', (ev) => ev.stopPropagation()); check.addEventListener('click', (ev) => ev.stopPropagation());
check.addEventListener('change', async () => { check.addEventListener('change', async () => {
const ids = collectOverlayFromDOM(); const ids = collectOverlayFromDOM();
await api('/api/overlay', { await api('api/overlay', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_ids: ids }), body: JSON.stringify({ file_ids: ids }),
@@ -159,7 +159,7 @@ function renderFileList() {
remove.title = 'Forget this track (drops the analysis and removes it from browser storage)'; remove.title = 'Forget this track (drops the analysis and removes it from browser storage)';
remove.addEventListener('click', async (ev) => { remove.addEventListener('click', async (ev) => {
ev.stopPropagation(); ev.stopPropagation();
await api(`/api/file?file_id=${encodeURIComponent(f.file_id)}`, { method: 'DELETE' }); await api(`api/file?file_id=${encodeURIComponent(f.file_id)}`, { method: 'DELETE' });
// The server only ever held numbers; the audio is ours to delete. // The server only ever held numbers; the audio is ours to delete.
await store.remove(f.file_id); await store.remove(f.file_id);
releaseObjectUrl(f.file_id); releaseObjectUrl(f.file_id);
@@ -871,7 +871,7 @@ async function refreshPlot() {
if (state.yLog !== null) params.set('y_log', String(state.yLog)); if (state.yLog !== null) params.set('y_log', String(state.yLog));
if (state.xLog !== null) params.set('x_log', String(state.xLog)); if (state.xLog !== null) params.set('x_log', String(state.xLog));
const payload = await api(`/api/plot?${params.toString()}`); const payload = await api(`api/plot?${params.toString()}`);
state.payload = payload; state.payload = payload;
el('plot-title').textContent = payload.datasets.length el('plot-title').textContent = payload.datasets.length
? `${payload.metric_name}${payload.datasets.length > 1 ? ` — ${payload.datasets.length} tracks` : ` — ${payload.datasets[0].label}`}` ? `${payload.metric_name}${payload.datasets.length > 1 ? ` — ${payload.datasets.length} tracks` : ` — ${payload.datasets[0].label}`}`
@@ -933,7 +933,7 @@ async function ingest(fileList) {
} }
} }
const s = await api('/api/session'); const s = await api('api/session');
state.files = s.files; state.files = s.files;
state.overlay = s.overlay; state.overlay = s.overlay;
if (!state.selected && state.files.length) state.selected = state.files[0].file_id; if (!state.selected && state.files.length) state.selected = state.files[0].file_id;
@@ -952,7 +952,7 @@ async function ingest(fileList) {
* second round trip) and is recorded in `state.analysed`. * second round trip) and is recorded in `state.analysed`.
*/ */
async function analyseOne(fileId, name, metric = state.metric) { async function analyseOne(fileId, name, metric = state.metric) {
const url = `/api/analyse?` + new URLSearchParams({ const url = `api/analyse?` + new URLSearchParams({
file_id: fileId, file_id: fileId,
name, name,
metric: metric || state.metric || '', metric: metric || state.metric || '',
@@ -972,7 +972,7 @@ async function analyseOne(fileId, name, metric = state.metric) {
function ensureOverlay(fileId) { function ensureOverlay(fileId) {
if (!state.overlay.includes(fileId)) { if (!state.overlay.includes(fileId)) {
state.overlay = [...state.overlay, fileId]; state.overlay = [...state.overlay, fileId];
api('/api/overlay', { api('api/overlay', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_ids: state.overlay }), body: JSON.stringify({ file_ids: state.overlay }),
@@ -1129,7 +1129,7 @@ async function boot() {
} }
await loadMetrics(); await loadMetrics();
const s = await api('/api/session'); const s = await api('api/session');
state.files = s.files; state.files = s.files;
state.overlay = s.overlay; state.overlay = s.overlay;
state.selected = state.files.length ? state.files[0].file_id : null; state.selected = state.files.length ? state.files[0].file_id : null;
+4 -4
View File
@@ -4,8 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Audio Mastering Analysis Toolkit</title> <title>Audio Mastering Analysis Toolkit</title>
<link rel="stylesheet" href="/static/vendor/uPlot.min.css"> <link rel="stylesheet" href="static/vendor/uPlot.min.css">
<link rel="stylesheet" href="/static/app.css"> <link rel="stylesheet" href="static/app.css">
</head> </head>
<body> <body>
@@ -89,9 +89,9 @@
<div id="drop-overlay" class="drop-overlay" hidden><span>Drop to analyse</span></div> <div id="drop-overlay" class="drop-overlay" hidden><span>Drop to analyse</span></div>
<script src="/static/vendor/uPlot.iife.min.js"></script> <script src="static/vendor/uPlot.iife.min.js"></script>
<!-- app.js is an ES module: it imports ./audio-store.js, which owns the <!-- app.js is an ES module: it imports ./audio-store.js, which owns the
browser-side copy of every uploaded file. --> browser-side copy of every uploaded file. -->
<script type="module" src="/static/app.js"></script> <script type="module" src="static/app.js"></script>
</body> </body>
</html> </html>