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.
@@ -184,7 +184,7 @@ sudo -u postgres psql -c "CREATE DATABASE quantumancy_test OWNER quantumancy ENC
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
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:**
|
**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
|
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:
|
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 |
|
| Env var | Default | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `DATABASE_URL` | — (required) | asyncpg connection string, e.g. `postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy` |
|
| `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` |
|
| `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 |
|
| `PORT` | `7777` | HTTP listen port |
|
||||||
| `OLLAMA_FAST_MODEL` | `granite4.1:3b` | fast tier: fragments, wire whispers |
|
| `OLLAMA_FAST_MODEL` | `granite4.1:3b` | fast tier: fragments, wire whispers |
|
||||||
| `OLLAMA_CHAT_MODEL` | `minicpm-v4.5:latest` | chat tier: direct contact, entity minting |
|
| `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
|
cd backend && source venv/bin/activate
|
||||||
DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy_test \
|
DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy_test \
|
||||||
OLLAMA_BASE_URL=http://10.30.20.107:11434 \
|
OLLAMA_BASE_URL=http://10.30.20.107:11434 \
|
||||||
SESSION_SECRET=test-secret \
|
|
||||||
python -m pytest -v
|
python -m pytest -v
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -86,4 +86,17 @@ async def serve_spa(full_path: str):
|
|||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Frontend not built. Run `npm run build` in frontend/ and restart.",
|
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)
|
return FileResponse(index_file)
|
||||||
|
|||||||
@@ -24,3 +24,38 @@ async def test_missing_frontend_build_returns_clear_error(client, monkeypatch, t
|
|||||||
response = await client.get("/some/route")
|
response = await client.get("/some/route")
|
||||||
assert response.status_code == 503
|
assert response.status_code == 503
|
||||||
assert "npm run build" in response.json()["detail"]
|
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"
|
||||||
|
|||||||
@@ -4,6 +4,30 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Quantumancy</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
BIN
frontend/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
frontend/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 571 B |
BIN
frontend/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
frontend/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
69
frontend/public/favicon.svg
Normal 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 |
BIN
frontend/public/icon-192.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
frontend/public/icon-512.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
frontend/public/og-image.png
Normal file
|
After Width: | Height: | Size: 165 KiB |