Dev build 2026-06-19 13:05

This commit is contained in:
Ivo Oskamp 2026-06-19 13:05:17 +02:00
parent 99f0e100df
commit 335bfd9591
6 changed files with 77 additions and 19 deletions

View File

@ -10,11 +10,11 @@ window.__authReady = (async function authGate() {
const r = await fetch('/api/auth/me', { credentials: 'same-origin' }); const r = await fetch('/api/auth/me', { credentials: 'same-origin' });
if (r.status === 401) { if (r.status === 401) {
const setup = await fetch('/api/auth/setup-required').then(function (x) { return x.json(); }).catch(function () { return { setup_required: false }; }); const setup = await fetch('/api/auth/setup-required').then(function (x) { return x.json(); }).catch(function () { return { setup_required: false }; });
window.location.replace(setup.setup_required ? '/setup.html' : '/login.html'); window.location.replace((window.__BASE_PATH__ || '') + (setup.setup_required ? '/setup.html' : '/login.html'));
return false; return false;
} }
if (!r.ok) { if (!r.ok) {
window.location.replace('/login.html'); window.location.replace((window.__BASE_PATH__ || '') + '/login.html');
return false; return false;
} }
const me = await r.json(); const me = await r.json();
@ -27,7 +27,7 @@ window.__authReady = (async function authGate() {
delete document.documentElement.dataset.authPending; delete document.documentElement.dataset.authPending;
return true; return true;
} catch (e) { } catch (e) {
window.location.replace('/login.html'); window.location.replace((window.__BASE_PATH__ || '') + '/login.html');
return false; return false;
} }
})(); })();
@ -53,7 +53,7 @@ function renderUserBadge(me) {
btn.textContent = 'Sign out'; btn.textContent = 'Sign out';
btn.addEventListener('click', async function () { btn.addEventListener('click', async function () {
await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }); await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' });
window.location.replace('/login.html'); window.location.replace((window.__BASE_PATH__ || '') + '/login.html');
}); });
wrap.append(btn); wrap.append(btn);
slot.append(wrap); slot.append(wrap);
@ -176,7 +176,7 @@ function renderUserBadge(me) {
async function requestJson(url, options) { async function requestJson(url, options) {
const response = await fetch(url, Object.assign({ credentials: 'same-origin' }, options || {})); const response = await fetch(url, Object.assign({ credentials: 'same-origin' }, options || {}));
if (response.status === 401) { if (response.status === 401) {
window.location.replace('/login.html'); window.location.replace((window.__BASE_PATH__ || '') + '/login.html');
throw new Error('unauthenticated'); throw new Error('unauthenticated');
} }
if (!response.ok) { if (!response.ok) {
@ -1335,7 +1335,9 @@ function renderUserBadge(me) {
if (siteFilter) { if (siteFilter) {
url += '?site_url=' + encodeURIComponent(siteFilter); url += '?site_url=' + encodeURIComponent(siteFilter);
} }
window.location.href = url; // Browser navigation (file download), not fetch — the fetch shim doesn't
// apply here, so prefix the path explicitly when behind the proxy.
window.location.href = (window.__BASE_PATH__ || '') + url;
}); });
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Clearview — Sign in</title> <title>Clearview — Sign in</title>
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="styles.css" />
</head> </head>
<body class="auth-page"> <body class="auth-page">
<main class="auth-card"> <main class="auth-card">
@ -17,12 +17,12 @@
<p id="loginError" class="auth-error" hidden></p> <p id="loginError" class="auth-error" hidden></p>
</form> </form>
</main> </main>
<script src="/auth.js"></script> <script src="auth.js"></script>
<script> <script>
(async function () { (async function () {
const setup = await ClearviewAuth.getJson('/api/auth/setup-required'); const setup = await ClearviewAuth.getJson('/api/auth/setup-required');
if (setup.ok && setup.data && setup.data.setup_required) { if (setup.ok && setup.data && setup.data.setup_required) {
window.location.replace('/setup.html'); window.location.replace((window.__BASE_PATH__ || '') + '/setup.html');
return; return;
} }
const form = document.getElementById('loginForm'); const form = document.getElementById('loginForm');
@ -37,7 +37,7 @@
remember: fd.get('remember') === 'on', remember: fd.get('remember') === 'on',
}); });
if (res.ok) { if (res.ok) {
window.location.replace('/'); window.location.replace((window.__BASE_PATH__ || '') + '/');
} else { } else {
err.textContent = (res.data && res.data.detail) || 'Sign-in failed'; err.textContent = (res.data && res.data.detail) || 'Sign-in failed';
err.hidden = false; err.hidden = false;

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Clearview — First-time setup</title> <title>Clearview — First-time setup</title>
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="styles.css" />
</head> </head>
<body class="auth-page"> <body class="auth-page">
<main class="auth-card"> <main class="auth-card">
@ -16,12 +16,12 @@
<p id="setupError" class="auth-error" hidden></p> <p id="setupError" class="auth-error" hidden></p>
</form> </form>
</main> </main>
<script src="/auth.js"></script> <script src="auth.js"></script>
<script> <script>
(async function () { (async function () {
const probe = await ClearviewAuth.getJson('/api/auth/setup-required'); const probe = await ClearviewAuth.getJson('/api/auth/setup-required');
if (!probe.ok || !probe.data || !probe.data.setup_required) { if (!probe.ok || !probe.data || !probe.data.setup_required) {
window.location.replace('/login.html'); window.location.replace((window.__BASE_PATH__ || '') + '/login.html');
return; return;
} }
const form = document.getElementById('setupForm'); const form = document.getElementById('setupForm');
@ -35,7 +35,7 @@
password: fd.get('password'), password: fd.get('password'),
}); });
if (res.ok) { if (res.ok) {
window.location.replace('/'); window.location.replace((window.__BASE_PATH__ || '') + '/');
} else { } else {
err.textContent = (res.data && res.data.detail) || 'Setup failed'; err.textContent = (res.data && res.data.detail) || 'Setup failed';
err.hidden = false; err.hidden = false;

View File

@ -8,8 +8,8 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from fastapi import Depends, FastAPI from fastapi import Depends, FastAPI, Request
from fastapi.responses import FileResponse from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from .api_jobs import router as jobs_router from .api_jobs import router as jobs_router
@ -67,9 +67,51 @@ app.include_router(onboarding_router, dependencies=_protected)
# Static files (mounted last so explicit API routes take precedence) # Static files (mounted last so explicit API routes take precedence)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Path-prefix support: when running behind a path-stripping proxy (landing on
# :8210), an X-Forwarded-Prefix header tells the page where it is mounted. We
# inject window.__BASE_PATH__ and a small fetch shim that prefixes same-origin
# absolute API calls (fetch('/api/...')); navigation/asset URLs are made
# prefix-relative in the site files. No header → empty prefix → no-op, so direct
# access behaves exactly as before.
_FETCH_SHIM = (
"(function(){var b=window.__BASE_PATH__;if(!b)return;"
"var f=window.fetch.bind(window);"
"window.fetch=function(i,o){"
'if(typeof i==="string"&&i.charAt(0)==="/")i=b+i;return f(i,o);};})();'
)
def _render_page(filename: str, prefix: str) -> HTMLResponse:
raw = (SITE_DIR / filename).read_text(encoding="utf-8")
head = (
"<head>\n<script>\n"
f'window.__BASE_PATH__ = "{prefix}";\n'
f"{_FETCH_SHIM}\n"
"</script>"
)
html = raw.replace("<head>", head, 1)
# no-store on the HTML shell so a browser never serves a stale page after a
# redeploy or a move behind/along the proxy.
return HTMLResponse(html, headers={"Cache-Control": "no-store"})
def _prefix(request: Request) -> str:
return request.headers.get("x-forwarded-prefix", "").rstrip("/")
@app.get("/") @app.get("/")
def index() -> FileResponse: def index(request: Request) -> HTMLResponse:
return FileResponse(SITE_DIR / "index.html") return _render_page("index.html", _prefix(request))
@app.get("/login.html")
def login_page(request: Request) -> HTMLResponse:
return _render_page("login.html", _prefix(request))
@app.get("/setup.html")
def setup_page(request: Request) -> HTMLResponse:
return _render_page("setup.html", _prefix(request))
app.mount("/", StaticFiles(directory=SITE_DIR, html=True), name="site") app.mount("/", StaticFiles(directory=SITE_DIR, html=True), name="site")

View File

@ -7,7 +7,7 @@ history, so operators can see exactly which image build is running.
from __future__ import annotations from __future__ import annotations
VERSION = "v0.2.0" VERSION = "v0.2.0"
BUILD = 0 BUILD = 1
def display_version() -> str: def display_version() -> str:

View File

@ -2,6 +2,20 @@
This file documents changes on the develop branch of this project. This file documents changes on the develop branch of this project.
## 2026-06-19 — Path-prefix routing support (run behind the landing proxy)
### Added
- Clearview can now be served under a path prefix (e.g. `http://10.19.3.32:8210/clearview/`) by the landing reverse proxy, which strips the prefix and passes it in `X-Forwarded-Prefix` — part of the shared path-prefix routing effort. With no header the app behaves exactly as on direct access.
- `main.py` now serves the three HTML pages (`index.html`, `login.html`, `setup.html`) through `_render_page`, which injects `<script>window.__BASE_PATH__ = "{prefix}"</script>` plus a small **fetch shim** that prefixes same-origin absolute API calls (`fetch('/api/...')`). This is a vanilla-JS app (no build step) with ~35 absolute `/api` calls across `app.js`/`auth.js`/inline scripts, so shimming `fetch` in one place is far less invasive than rewriting each call. `index.html`/`login.html`/`setup.html` got explicit routes (registered before the `StaticFiles` mount) and the pages are now sent with `Cache-Control: no-store` so a stale shell is never served after a redeploy.
### Changed
- Made the few **non-fetch** URLs prefix-aware in the site files, since the fetch shim only covers `fetch()`:
- `login.html`/`setup.html`: stylesheet/`auth.js` references made relative (`/styles.css` → `styles.css`, `/auth.js` → `auth.js`) so they resolve under the prefix via the document URL.
- Page navigations (`window.location.replace('/login.html' | '/setup.html' | '/')` in `app.js`, `login.html`, `setup.html`) now prepend `window.__BASE_PATH__`.
- The scan-job export download (`window.location.href = '/api/scan-jobs/.../export'`, a browser navigation, not `fetch`) prepends `window.__BASE_PATH__`.
- `index.html` needed no changes — its asset refs were already relative and it uses hash routing. `auth.js` needed none — its `fetch()` calls go through the shim.
- Verified: `main.py` compiles, the injection produces a single `window.__BASE_PATH__` assignment + shim per page (empty-prefix variant is a no-op), and no breaking absolute asset/nav refs remain in the site.
## 2026-05-28 — Released as v0.2.0 ## 2026-05-28 — Released as v0.2.0
## 2026-05-28 — Release: drop unused `version.txt` ## 2026-05-28 — Release: drop unused `version.txt`