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 @@