diff --git a/scripts/browser_smoke.cjs b/scripts/browser_smoke.cjs index 9ccde56..b26fa96 100644 --- a/scripts/browser_smoke.cjs +++ b/scripts/browser_smoke.cjs @@ -115,7 +115,7 @@ async function copy(id) { page.on('response', r => { if (r.status() >= 400) report.http_errors.push({ url: r.url(), status: r.status() }); }); page.on('dialog', async dialog => { report.page_errors.push(`Dialog: ${dialog.message()}`); await dialog.dismiss(); }); await step('Application identity, assets and initial rendering', async () => { - const r = await page.goto(base, { waitUntil: 'networkidle', timeout: 120000 }); + const r = await page.goto(base + '/studio/', { waitUntil: 'networkidle', timeout: 120000 }); assert.equal(r.status(), 200); assert.match(await page.title(), /Simplex Normal Forms Studio/); await rendered('ai-source-tokens', '62'); @@ -125,7 +125,7 @@ async function copy(id) { assert.equal(health.status(), 200); report.health = await health.json(); assert.equal(report.health.service, 'smplxnf-studio'); - for (const [urlPath, local] of [['/', 'web/templates/index.html'], ['/static/app.js', 'web/static/app.js'], ['/static/style.css', 'web/static/style.css']]) { + for (const [urlPath, local] of [['/studio/', 'web/templates/index.html'], ['/', 'web/templates/overview.html'], ['/static/app.js', 'web/static/app.js'], ['/static/style.css', 'web/static/style.css']]) { const response = await context.request.get(base + urlPath); assert.equal(response.status(), 200); const bytes = await response.body(); diff --git a/tests/test_web_api.py b/tests/test_web_api.py index c6952a6..f19d8d0 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -45,8 +45,8 @@ def test_healthz_endpoint(self) -> None: self.assertEqual(payload.get("version"), "0.1.0") def test_index_html_renders_all_tabs(self) -> None: - """Verifies GET / renders the landing SPA with all five workbench tabs.""" - response = self.client.get("/") + """Verifies GET /studio/ renders the studio SPA with all five workbench tabs.""" + response = self.client.get("/studio/") self.assertEqual(response.status_code, 200) self.assertIn("text/html", response.headers.get("content-type", "")) html = response.text diff --git a/web/app.py b/web/app.py index 7d73e93..0b766db 100644 --- a/web/app.py +++ b/web/app.py @@ -22,7 +22,7 @@ from fastapi import Body, FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel, Field @@ -942,6 +942,17 @@ def project_chunks(chunks: Sequence[Mapping[str, Any]]) -> List[Dict[str, Any]]: # Frontend SPA Route # ============================================================================= -@app.get("/", response_class=HTMLResponse) +@app.get("/", include_in_schema=False) +async def serve_overview() -> FileResponse: + """One-screen BQNF overview. Plain HTML, not rendered through Jinja.""" + return FileResponse(templates_dir / "overview.html", media_type="text/html") + + +@app.get("/studio", include_in_schema=False) +async def redirect_studio() -> RedirectResponse: + return RedirectResponse("/studio/", status_code=308) + + +@app.get("/studio/", response_class=HTMLResponse) async def serve_index(request: Request): return templates.TemplateResponse(request=request, name="index.html") diff --git a/web/templates/index.html b/web/templates/index.html index 7ec831d..a14d5fe 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -46,7 +46,7 @@
diff --git a/web/templates/overview.html b/web/templates/overview.html new file mode 100644 index 0000000..1b0a255 --- /dev/null +++ b/web/templates/overview.html @@ -0,0 +1,302 @@ + + + + + + +Change one row of sales. Watch which parts of the stored answer can be fixed from the change alone, and which need extra state.
| id | region | amount | status |
|---|
| Region | SUM stored | MAX stored | Database says |
|---|
| System | What it does | Rows read |
|---|---|---|
| SQL Server indexed view | Can't hold this answer: an indexed view may not contain MAX. | n/a |
| Postgres materialized view | Keeps the old answer until REFRESH reruns the whole query. | 0 |
| BQNF | Pick a change on the left. Every answer is checked against the table. | 0 |
The rows and moves match the simplex-normal-forms BQNF simulator (financial ledger preset). The materialized view is counted as refreshed after each change. Rows read are for this demo only, not a benchmark. SmplCache is the cache engine built on BQNF.
+