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' });
if (r.status === 401) {
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;
}
if (!r.ok) {
window.location.replace('/login.html');
window.location.replace((window.__BASE_PATH__ || '') + '/login.html');
return false;
}
const me = await r.json();
@ -27,7 +27,7 @@ window.__authReady = (async function authGate() {
delete document.documentElement.dataset.authPending;
return true;
} catch (e) {
window.location.replace('/login.html');
window.location.replace((window.__BASE_PATH__ || '') + '/login.html');
return false;
}
})();
@ -53,7 +53,7 @@ function renderUserBadge(me) {
btn.textContent = 'Sign out';
btn.addEventListener('click', async function () {
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);
slot.append(wrap);
@ -176,7 +176,7 @@ function renderUserBadge(me) {
async function requestJson(url, options) {
const response = await fetch(url, Object.assign({ credentials: 'same-origin' }, options || {}));
if (response.status === 401) {
window.location.replace('/login.html');
window.location.replace((window.__BASE_PATH__ || '') + '/login.html');
throw new Error('unauthenticated');
}
if (!response.ok) {
@ -1335,7 +1335,9 @@ function renderUserBadge(me) {
if (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>
<meta charset="utf-8" />
<title>Clearview — Sign in</title>
<link rel="stylesheet" href="/styles.css" />
<link rel="stylesheet" href="styles.css" />
</head>
<body class="auth-page">
<main class="auth-card">
@ -17,12 +17,12 @@
<p id="loginError" class="auth-error" hidden></p>
</form>
</main>
<script src="/auth.js"></script>
<script src="auth.js"></script>
<script>
(async function () {
const setup = await ClearviewAuth.getJson('/api/auth/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;
}
const form = document.getElementById('loginForm');
@ -37,7 +37,7 @@
remember: fd.get('remember') === 'on',
});
if (res.ok) {
window.location.replace('/');
window.location.replace((window.__BASE_PATH__ || '') + '/');
} else {
err.textContent = (res.data && res.data.detail) || 'Sign-in failed';
err.hidden = false;

View File

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

View File

@ -8,8 +8,8 @@ from __future__ import annotations
from pathlib import Path
from fastapi import Depends, FastAPI
from fastapi.responses import FileResponse
from fastapi import Depends, FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
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)
# ---------------------------------------------------------------------------
# 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("/")
def index() -> FileResponse:
return FileResponse(SITE_DIR / "index.html")
def index(request: Request) -> HTMLResponse:
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")

View File

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

View File

@ -2,6 +2,20 @@
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 — Release: drop unused `version.txt`