diff --git a/README.md b/README.md index 95a2cf3..b25752a 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,17 @@ becomes super user) **provided `PODIUM_PUBLIC_URL` is the bare origin** — see - GCN `data` payloads can be tens of KB, so list/search responses strip them server-side (`slim()` in the Lua scripts) and keep a human `summary` extracted from the event JSON. +- **`.env` is a deploy-time artifact.** `podium deploy` writes it from + `.env.prod` or `.env.local`, and restores the local flavor after a prod + deploy (the prod copy lives on in the server's service dir). If local + `docker compose` ever fails on a `/srv/...` mount, `.env` has prod values — + `cp .env.local .env`. +- **A dashboard login poisons same-origin XRPC.** HappyView allows anonymous + XRPC but hard-rejects *authenticated* calls without a client key — so once + you've logged into the dashboard, your session cookie would break the + frontend's fetches ("Missing client identification"). The frontend fetches + with `credentials: 'omit'` to stay anonymous; keep that if you touch + `api.ts`. ## Adding a data source diff --git a/backend/scripts/search.lua b/backend/scripts/search.lua index 77fbb97..406dd5e 100644 --- a/backend/scripts/search.lua +++ b/backend/scripts/search.lua @@ -42,9 +42,12 @@ function handle() if not q or #q == 0 then error("missing required parameter: q") end local limit = math.min(tonumber(params.limit) or 25, 100) - local out, seen = {}, {} + -- One bucket per source, then a round-robin merge so a huge collection + -- can't crowd the others out of the results. + local buckets = {} for _, src in ipairs(SOURCES) do if not params.collection or params.collection == src.collection then + local bucket, seen = {}, {} for _, field in ipairs(src.fields) do local ok, res = pcall(db.search, { collection = src.collection, @@ -53,14 +56,25 @@ function handle() limit = limit, }) if ok then - collect(out, seen, src.collection, res) + collect(bucket, seen, src.collection, res) else log("search failed on " .. src.collection .. "." .. field .. ": " .. tostring(res)) end end + if #bucket > 0 then buckets[#buckets + 1] = bucket end end end - while #out > limit do table.remove(out) end + local out, rank, added = {}, 1, true + while #out < limit and added do + added = false + for _, bucket in ipairs(buckets) do + if bucket[rank] and #out < limit then + out[#out + 1] = bucket[rank] + added = true + end + end + rank = rank + 1 + end return { q = q, results = toarray(out) } end diff --git a/bin/podium.js b/bin/podium.js index 32abf0c..e525332 100755 --- a/bin/podium.js +++ b/bin/podium.js @@ -216,6 +216,17 @@ async function cmdDeploy() { if (res.error?.code === 'ENOENT') fail('sm not found on PATH (npm link it from caddy-front)'); if (res.status !== 0) fail(`sm deploy exited with ${res.status}`); + // The prod .env has been rsynced to the server; restore the local flavor + // so docker compose keeps working on this machine. + if (env === 'prod') { + try { + await copyFile(resolve(root, '.env.local'), resolve(root, '.env')); + log('.env ← .env.local (restored for local compose)'); + } catch { + // no .env.local — leave the prod copy in place + } + } + if (flags.has('--no-push')) return; if (!envFile.vars.PODIUM_HV_KEY) { log('\nNo PODIUM_HV_KEY yet, skipping backend push. First-boot bootstrap:'); diff --git a/frontend/index.html b/frontend/index.html index 90eb2f3..1d11062 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -11,23 +11,30 @@ diff --git a/frontend/src/api.ts b/frontend/src/api.ts index dba0fed..2acc970 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -10,7 +10,11 @@ async function query( for (const [k, v] of Object.entries(params)) { if (v !== undefined && v !== '') url.searchParams.set(k, String(v)); } - const res = await fetch(url); + // credentials: 'omit' matters: a HappyView dashboard session cookie on the + // same origin would otherwise ride along, and HappyView hard-rejects + // authenticated XRPC calls that lack a client key. We're a public, + // anonymous client — stay one. + const res = await fetch(url, { credentials: 'omit' }); if (!res.ok) { let msg = `request failed (${res.status})`; try { diff --git a/frontend/src/components/podium-app.ts b/frontend/src/components/podium-app.ts index db48a48..2dcf425 100644 --- a/frontend/src/components/podium-app.ts +++ b/frontend/src/components/podium-app.ts @@ -14,17 +14,32 @@ export class PodiumApp extends SignalWatcher(LitElement) { static styles = css` :host { display: block; + } + .banner { + position: relative; + overflow: hidden; + background: var(--banner-bg); + text-align: center; + padding: 2.6rem 1.25rem 2.2rem; + } + /* faint disco glow, contained in the banner */ + .banner::before { + content: ''; + position: absolute; + inset: 0; + background: + radial-gradient(45% 90% at 25% 10%, rgba(255, 113, 206, 0.14), transparent 70%), + radial-gradient(40% 80% at 75% 15%, rgba(1, 205, 254, 0.14), transparent 70%), + radial-gradient(35% 70% at 50% 80%, rgba(185, 103, 255, 0.1), transparent 70%); + pointer-events: none; + } + .banner .inner { position: relative; - z-index: 1; } .shell { max-width: 44rem; margin: 0 auto; - padding: 4rem 1.25rem 5rem; - } - header { - text-align: center; - margin-bottom: 2.2rem; + padding: 1.8rem 1.25rem 5rem; } .wordmark { margin: 0; @@ -43,7 +58,7 @@ export class PodiumApp extends SignalWatcher(LitElement) { background-clip: text; -webkit-background-clip: text; color: transparent; - animation: shimmer 9s linear infinite; + animation: shimmer 50s linear infinite; } @keyframes shimmer { to { @@ -57,12 +72,13 @@ export class PodiumApp extends SignalWatcher(LitElement) { } .strap { margin: 0.4rem 0 0; - color: var(--muted); + color: var(--banner-text); + opacity: 0.85; font-size: 0.95rem; } .stats { margin-top: 0.6rem; - color: var(--faint); + color: var(--banner-muted); font: 0.78rem var(--mono); } .filters { @@ -181,13 +197,14 @@ export class PodiumApp extends SignalWatcher(LitElement) { const searching = s.searching; const hasQuery = s.query.length > 0; return html` -
-
+ - +
+ +
${this.filters()} ${s.error ? html`
${s.error}
` : nothing} diff --git a/frontend/src/components/podium-result-card.ts b/frontend/src/components/podium-result-card.ts index 04781bd..ef2a7e7 100644 --- a/frontend/src/components/podium-result-card.ts +++ b/frontend/src/components/podium-result-card.ts @@ -30,13 +30,15 @@ export class PodiumResultCard extends LitElement { border-radius: 12px; background: var(--panel); padding: 0.9rem 1.1rem; - transition: border-color 0.15s ease; + box-shadow: 0 1px 2px rgba(20, 20, 30, 0.05); + transition: border-color 0.15s ease, box-shadow 0.15s ease; } .card:hover { border-color: transparent; background: linear-gradient(var(--panel), var(--panel)) padding-box, var(--grad) border-box; + box-shadow: 0 2px 10px rgba(20, 20, 30, 0.08); } .body { min-width: 0; diff --git a/frontend/src/components/podium-search-box.ts b/frontend/src/components/podium-search-box.ts index 3e5cf80..0e1e46f 100644 --- a/frontend/src/components/podium-search-box.ts +++ b/frontend/src/components/podium-search-box.ts @@ -19,7 +19,7 @@ export class PodiumSearchBox extends SignalWatcher(LitElement) { background: linear-gradient(var(--panel), var(--panel)) padding-box, var(--grad) border-box; - box-shadow: 0 0 24px rgba(185, 103, 255, 0.15); + box-shadow: 0 2px 18px rgba(185, 103, 255, 0.18); } input { width: 100%; @@ -52,7 +52,7 @@ export class PodiumSearchBox extends SignalWatcher(LitElement) { top: 50%; translate: 0 -50%; font-size: 0.9rem; - animation: turn 1.2s linear infinite; + animation: turn 2.5s linear infinite; } @keyframes turn { to {