fix: actually strip stale SESSION_SECRET mentions from README

The gap-g-readme merge commit (57a8914) staged this fix but never
re-staged it after editing, so the merge landed with the pre-fix content —
the working tree had the correction but git didn't. No functional change,
just closing the gap between what was intended and what was committed.
This commit is contained in:
Indiana
2026-07-23 03:32:21 +00:00
parent 57a8914fdc
commit 761d6171b5
12 changed files with 143 additions and 4 deletions

View File

@@ -184,7 +184,7 @@ sudo -u postgres psql -c "CREATE DATABASE quantumancy_test OWNER quantumancy ENC
```bash
cp .env.example .env
# edit .env: set a real SESSION_SECRET, confirm DATABASE_URL and OLLAMA_BASE_URL
# edit .env: confirm DATABASE_URL and OLLAMA_BASE_URL
```
**3. Backend:**
@@ -256,13 +256,12 @@ Any Ollama tag works — override with `OLLAMA_FAST_MODEL` / `OLLAMA_CHAT_MODEL`
All settings live in `backend/app/config.py` and are read from the environment
or `.env` (repo root when run via systemd; CWD otherwise). Required:
`DATABASE_URL`, `OLLAMA_BASE_URL`, `SESSION_SECRET`.
`DATABASE_URL`, `OLLAMA_BASE_URL`.
| Env var | Default | Purpose |
|---|---|---|
| `DATABASE_URL` | — (required) | asyncpg connection string, e.g. `postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy` |
| `OLLAMA_BASE_URL` | — (required) | Ollama REST endpoint, e.g. `http://10.30.20.107:11434` |
| `SESSION_SECRET` | — (required) | present in `Settings` but not currently read anywhere else in the app — session auth actually runs on random per-login tokens hashed into `auth_sessions` (`backend/app/models/auth_session.py`), not a signing secret. Still required to boot; set it to any random string. |
| `PORT` | `7777` | HTTP listen port |
| `OLLAMA_FAST_MODEL` | `granite4.1:3b` | fast tier: fragments, wire whispers |
| `OLLAMA_CHAT_MODEL` | `minicpm-v4.5:latest` | chat tier: direct contact, entity minting |
@@ -328,7 +327,6 @@ concurrent producers never interleave):
cd backend && source venv/bin/activate
DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy_test \
OLLAMA_BASE_URL=http://10.30.20.107:11434 \
SESSION_SECRET=test-secret \
python -m pytest -v
```

View File

@@ -86,4 +86,17 @@ async def serve_spa(full_path: str):
status_code=503,
detail="Frontend not built. Run `npm run build` in frontend/ and restart.",
)
# Vite emits root-level static files (favicon.ico, favicon.svg,
# apple-touch-icon.png, og-image.png, …) straight into dist/ rather than
# dist/assets/ — the only mounted static dir. Without this, requests for
# them fall through to the SPA fallback below and get index.html back
# instead of the actual file (browsers silently ignore it; social-media
# link-preview crawlers fetching og:image get an HTML page).
if full_path:
dist_root = FRONTEND_DIST.resolve()
candidate = (dist_root / full_path).resolve()
if candidate.is_file() and dist_root in candidate.parents:
return FileResponse(candidate)
return FileResponse(index_file)

View File

@@ -24,3 +24,38 @@ async def test_missing_frontend_build_returns_clear_error(client, monkeypatch, t
response = await client.get("/some/route")
assert response.status_code == 503
assert "npm run build" in response.json()["detail"]
@pytest.mark.asyncio
async def test_root_level_static_file_is_served_directly(client, monkeypatch, tmp_path):
# Vite emits favicon.ico, og-image.png, etc. straight into dist/, not
# dist/assets/ (the only mounted static dir) — these must be served as
# themselves, not swallowed by the SPA fallback.
import app.main as main_module
monkeypatch.setattr(main_module, "FRONTEND_DIST", tmp_path)
(tmp_path / "index.html").write_text('<div id="root"></div>')
(tmp_path / "favicon.svg").write_text("<svg>fake favicon</svg>")
response = await client.get("/favicon.svg")
assert response.status_code == 200
assert response.text == "<svg>fake favicon</svg>"
assert "html" not in response.headers["content-type"]
@pytest.mark.asyncio
async def test_static_file_lookup_cannot_escape_dist_directory(monkeypatch, tmp_path):
# Bypasses the HTTP client, which normalizes ".." segments out of URLs
# before they're ever sent — this exercises the route function's own
# guard directly against a full_path value an unusual client could send.
import app.main as main_module
dist_dir = tmp_path / "dist"
dist_dir.mkdir()
(dist_dir / "index.html").write_text('<div id="root"></div>')
secret = tmp_path / "secret.txt"
secret.write_text("should never be served")
monkeypatch.setattr(main_module, "FRONTEND_DIST", dist_dir)
response = await main_module.serve_spa("../secret.txt")
assert response.path == dist_dir / "index.html"

View File

@@ -4,6 +4,30 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Quantumancy</title>
<meta name="description" content="A self-hosted séance. Talk to spirits through your microphone, an RTL-SDR dongle, your network's jitter, your phone's motion sensors, or a text box — a locally-run LLM gives each anomaly a voice." />
<meta name="theme-color" content="#0b0a10" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png" />
<link rel="icon" type="image/png" sizes="512x512" href="/icon-512.png" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Quantumancy" />
<meta property="og:url" content="https://spirit.thetempleofdoom.com/" />
<meta property="og:title" content="Quantumancy — an instrument for speaking with the dead bandwidth" />
<meta property="og:description" content="A self-hosted séance. Talk to spirits through your microphone, an RTL-SDR dongle, your network's jitter, your phone's motion sensors, or a text box." />
<meta property="og:image" content="https://spirit.thetempleofdoom.com/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Quantumancy — an instrument for speaking with the dead bandwidth" />
<meta name="twitter:description" content="A self-hosted séance. Talk to spirits through your microphone, an RTL-SDR dongle, your network's jitter, your phone's motion sensors, or a text box." />
<meta name="twitter:image" content="https://spirit.thetempleofdoom.com/og-image.png" />
</head>
<body>
<div id="root"></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,69 @@
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="bgGlow" cx="50%" cy="42%" r="65%">
<stop offset="0%" stop-color="#2a2040" />
<stop offset="55%" stop-color="#140f20" />
<stop offset="100%" stop-color="#0b0a10" />
</radialGradient>
<radialGradient id="ghostFill" cx="50%" cy="30%" r="75%">
<stop offset="0%" stop-color="#d9c8f7" stop-opacity="0.98" />
<stop offset="45%" stop-color="#a687e0" stop-opacity="0.95" />
<stop offset="100%" stop-color="#6b4fa8" stop-opacity="0.9" />
</radialGradient>
<radialGradient id="eyeGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#fdf8ff" />
<stop offset="100%" stop-color="#c9b6f0" />
</radialGradient>
<filter id="softBlur" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="10" />
</filter>
<filter id="wideBlur" x="-80%" y="-80%" width="260%" height="260%">
<feGaussianBlur stdDeviation="22" />
</filter>
</defs>
<!-- background -->
<rect width="512" height="512" fill="url(#bgGlow)" />
<!-- faint signal arcs, spirit-radio motif -->
<g stroke="#9b7fd4" fill="none" opacity="0.28">
<path d="M 96 168 A 220 220 0 0 1 416 168" stroke-width="3" />
<path d="M 128 122 A 270 270 0 0 1 384 122" stroke-width="2.5" opacity="0.7" />
</g>
<!-- sigil ring -->
<circle cx="256" cy="272" r="176" stroke="#9b7fd4" stroke-width="1.5" fill="none" opacity="0.35" />
<circle cx="256" cy="272" r="176" stroke="#9b7fd4" stroke-width="1" fill="none" opacity="0.5"
stroke-dasharray="2 14" />
<!-- outer glow behind the ghost -->
<path
d="M256 108 C182 108 140 168 140 244 C140 322 172 372 196 408 C214 384 224 392 256 412
C288 392 298 384 316 408 C340 372 372 322 372 244 C372 168 330 108 256 108 Z"
fill="#9b7fd4" opacity="0.55" filter="url(#wideBlur)"
/>
<!-- ghost body -->
<path
d="M256 116 C188 116 150 172 150 242 C150 316 180 364 202 398 C218 376 228 384 256 402
C284 384 294 376 310 398 C332 364 362 316 362 242 C362 172 324 116 256 116 Z"
fill="url(#ghostFill)" stroke="#efe6ff" stroke-width="2" opacity="0.98"
/>
<!-- inner soft highlight -->
<ellipse cx="222" cy="188" rx="46" ry="60" fill="#ffffff" opacity="0.16" filter="url(#softBlur)" />
<!-- eyes -->
<circle cx="222" cy="230" r="15" fill="#0b0a10" />
<circle cx="290" cy="230" r="15" fill="#0b0a10" />
<circle cx="222" cy="230" r="6" fill="url(#eyeGlow)" />
<circle cx="290" cy="230" r="6" fill="url(#eyeGlow)" />
<!-- drifting motes -->
<g fill="#d9c8f7">
<circle cx="118" cy="330" r="4" opacity="0.85" />
<circle cx="140" cy="366" r="2.5" opacity="0.6" />
<circle cx="394" cy="322" r="3.5" opacity="0.75" />
<circle cx="378" cy="360" r="2" opacity="0.55" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB