This repository has no description
2

Configure Feed

Select the types of activity you want to include in your feed.

v1

Robin Berjon (Jul 3, 2026, 10:36 AM +0200) 34dea6db 03a26a4a

+99 -46
+11
README.md
··· 126 126 - GCN `data` payloads can be tens of KB, so list/search responses strip them 127 127 server-side (`slim()` in the Lua scripts) and keep a human `summary` 128 128 extracted from the event JSON. 129 + - **`.env` is a deploy-time artifact.** `podium deploy` writes it from 130 + `.env.prod` or `.env.local`, and restores the local flavor after a prod 131 + deploy (the prod copy lives on in the server's service dir). If local 132 + `docker compose` ever fails on a `/srv/...` mount, `.env` has prod values — 133 + `cp .env.local .env`. 134 + - **A dashboard login poisons same-origin XRPC.** HappyView allows anonymous 135 + XRPC but hard-rejects *authenticated* calls without a client key — so once 136 + you've logged into the dashboard, your session cookie would break the 137 + frontend's fetches ("Missing client identification"). The frontend fetches 138 + with `credentials: 'omit'` to stay anonymous; keep that if you touch 139 + `api.ts`. 129 140 130 141 ## Adding a data source 131 142
+17 -3
backend/scripts/search.lua
··· 42 42 if not q or #q == 0 then error("missing required parameter: q") end 43 43 local limit = math.min(tonumber(params.limit) or 25, 100) 44 44 45 - local out, seen = {}, {} 45 + -- One bucket per source, then a round-robin merge so a huge collection 46 + -- can't crowd the others out of the results. 47 + local buckets = {} 46 48 for _, src in ipairs(SOURCES) do 47 49 if not params.collection or params.collection == src.collection then 50 + local bucket, seen = {}, {} 48 51 for _, field in ipairs(src.fields) do 49 52 local ok, res = pcall(db.search, { 50 53 collection = src.collection, ··· 53 56 limit = limit, 54 57 }) 55 58 if ok then 56 - collect(out, seen, src.collection, res) 59 + collect(bucket, seen, src.collection, res) 57 60 else 58 61 log("search failed on " .. src.collection .. "." .. field .. ": " .. tostring(res)) 59 62 end 60 63 end 64 + if #bucket > 0 then buckets[#buckets + 1] = bucket end 61 65 end 62 66 end 63 67 64 - while #out > limit do table.remove(out) end 68 + local out, rank, added = {}, 1, true 69 + while #out < limit and added do 70 + added = false 71 + for _, bucket in ipairs(buckets) do 72 + if bucket[rank] and #out < limit then 73 + out[#out + 1] = bucket[rank] 74 + added = true 75 + end 76 + end 77 + rank = rank + 1 78 + end 65 79 return { q = q, results = toarray(out) } 66 80 end
+11
bin/podium.js
··· 216 216 if (res.error?.code === 'ENOENT') fail('sm not found on PATH (npm link it from caddy-front)'); 217 217 if (res.status !== 0) fail(`sm deploy exited with ${res.status}`); 218 218 219 + // The prod .env has been rsynced to the server; restore the local flavor 220 + // so docker compose keeps working on this machine. 221 + if (env === 'prod') { 222 + try { 223 + await copyFile(resolve(root, '.env.local'), resolve(root, '.env')); 224 + log('.env ← .env.local (restored for local compose)'); 225 + } catch { 226 + // no .env.local — leave the prod copy in place 227 + } 228 + } 229 + 219 230 if (flags.has('--no-push')) return; 220 231 if (!envFile.vars.PODIUM_HV_KEY) { 221 232 log('\nNo PODIUM_HV_KEY yet, skipping backend push. First-boot bootstrap:');
+20 -26
frontend/index.html
··· 11 11 <link rel="icon" href="/favicon.svg" type="image/svg+xml" /> 12 12 <style> 13 13 :root { 14 - --bg: #0a0a0e; 15 - --panel: #121218; 16 - --panel-2: #17171f; 17 - --border: #26262f; 18 - --text: #ececf1; 19 - --muted: #8f8f9b; 20 - --faint: #5c5c68; 21 - --pink: #ff71ce; 22 - --cyan: #01cdfe; 23 - --green: #05ffa1; 24 - --purple: #b967ff; 25 - --grad: linear-gradient(100deg, var(--pink), var(--cyan) 35%, var(--green) 70%, var(--purple)); 14 + /* light page */ 15 + --bg: #ffffff; 16 + --panel: #ffffff; 17 + --panel-2: #f4f4f8; 18 + --border: #e3e3ea; 19 + --text: #1b1b22; 20 + --muted: #6b6b78; 21 + --faint: #9a9aa6; 22 + /* accent inks, readable on white */ 23 + --pink: #d63384; 24 + --cyan: #0b87b3; 25 + --green: #0a9e6b; 26 + --purple: #8a4fd0; 27 + /* the decorative disco gradient stays bright */ 28 + --grad: linear-gradient(100deg, #ff71ce, #01cdfe 35%, #05ffa1 70%, #b967ff); 29 + /* dark banner */ 30 + --banner-bg: #0a0a0e; 31 + --banner-text: #ececf1; 32 + --banner-muted: #8f8f9b; 26 33 --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace; 27 34 --sans: system-ui, -apple-system, "Segoe UI", sans-serif; 28 35 } 29 36 html { 30 - color-scheme: dark; 37 + color-scheme: light; 31 38 } 32 39 body { 33 40 margin: 0; ··· 35 42 color: var(--text); 36 43 font-family: var(--sans); 37 44 -webkit-font-smoothing: antialiased; 38 - } 39 - /* faint disco glow up top */ 40 - body::before { 41 - content: ""; 42 - position: fixed; 43 - inset: -40vh -20vw auto; 44 - height: 70vh; 45 - background: 46 - radial-gradient(40% 55% at 30% 30%, rgba(255, 113, 206, 0.1), transparent 70%), 47 - radial-gradient(35% 50% at 70% 25%, rgba(1, 205, 254, 0.1), transparent 70%), 48 - radial-gradient(30% 45% at 50% 60%, rgba(185, 103, 255, 0.08), transparent 70%); 49 - pointer-events: none; 50 - z-index: 0; 51 45 } 52 46 </style> 53 47 </head>
+5 -1
frontend/src/api.ts
··· 10 10 for (const [k, v] of Object.entries(params)) { 11 11 if (v !== undefined && v !== '') url.searchParams.set(k, String(v)); 12 12 } 13 - const res = await fetch(url); 13 + // credentials: 'omit' matters: a HappyView dashboard session cookie on the 14 + // same origin would otherwise ride along, and HappyView hard-rejects 15 + // authenticated XRPC calls that lack a client key. We're a public, 16 + // anonymous client — stay one. 17 + const res = await fetch(url, { credentials: 'omit' }); 14 18 if (!res.ok) { 15 19 let msg = `request failed (${res.status})`; 16 20 try {
+30 -13
frontend/src/components/podium-app.ts
··· 14 14 static styles = css` 15 15 :host { 16 16 display: block; 17 + } 18 + .banner { 17 19 position: relative; 18 - z-index: 1; 20 + overflow: hidden; 21 + background: var(--banner-bg); 22 + text-align: center; 23 + padding: 2.6rem 1.25rem 2.2rem; 24 + } 25 + /* faint disco glow, contained in the banner */ 26 + .banner::before { 27 + content: ''; 28 + position: absolute; 29 + inset: 0; 30 + background: 31 + radial-gradient(45% 90% at 25% 10%, rgba(255, 113, 206, 0.14), transparent 70%), 32 + radial-gradient(40% 80% at 75% 15%, rgba(1, 205, 254, 0.14), transparent 70%), 33 + radial-gradient(35% 70% at 50% 80%, rgba(185, 103, 255, 0.1), transparent 70%); 34 + pointer-events: none; 35 + } 36 + .banner .inner { 37 + position: relative; 19 38 } 20 39 .shell { 21 40 max-width: 44rem; 22 41 margin: 0 auto; 23 - padding: 4rem 1.25rem 5rem; 24 - } 25 - header { 26 - text-align: center; 27 - margin-bottom: 2.2rem; 42 + padding: 1.8rem 1.25rem 5rem; 28 43 } 29 44 .wordmark { 30 45 margin: 0; ··· 43 58 background-clip: text; 44 59 -webkit-background-clip: text; 45 60 color: transparent; 46 - animation: shimmer 9s linear infinite; 61 + animation: shimmer 50s linear infinite; 47 62 } 48 63 @keyframes shimmer { 49 64 to { ··· 57 72 } 58 73 .strap { 59 74 margin: 0.4rem 0 0; 60 - color: var(--muted); 75 + color: var(--banner-text); 76 + opacity: 0.85; 61 77 font-size: 0.95rem; 62 78 } 63 79 .stats { 64 80 margin-top: 0.6rem; 65 - color: var(--faint); 81 + color: var(--banner-muted); 66 82 font: 0.78rem var(--mono); 67 83 } 68 84 .filters { ··· 181 197 const searching = s.searching; 182 198 const hasQuery = s.query.length > 0; 183 199 return html` 184 - <div class="shell"> 185 - <header> 200 + <header class="banner"> 201 + <div class="inner"> 186 202 <h1 class="wordmark"><span class="ball">🪩</span><span class="text">podium</span></h1> 187 203 <p class="strap">a search engine for open data on the AT Protocol</p> 188 204 ${this.statsLine()} 189 - </header> 190 - 205 + </div> 206 + </header> 207 + <div class="shell"> 191 208 <podium-search-box></podium-search-box> 192 209 ${this.filters()} 193 210 ${s.error ? html`<div class="error">${s.error}</div>` : nothing}
+3 -1
frontend/src/components/podium-result-card.ts
··· 30 30 border-radius: 12px; 31 31 background: var(--panel); 32 32 padding: 0.9rem 1.1rem; 33 - transition: border-color 0.15s ease; 33 + box-shadow: 0 1px 2px rgba(20, 20, 30, 0.05); 34 + transition: border-color 0.15s ease, box-shadow 0.15s ease; 34 35 } 35 36 .card:hover { 36 37 border-color: transparent; 37 38 background: 38 39 linear-gradient(var(--panel), var(--panel)) padding-box, 39 40 var(--grad) border-box; 41 + box-shadow: 0 2px 10px rgba(20, 20, 30, 0.08); 40 42 } 41 43 .body { 42 44 min-width: 0;
+2 -2
frontend/src/components/podium-search-box.ts
··· 19 19 background: 20 20 linear-gradient(var(--panel), var(--panel)) padding-box, 21 21 var(--grad) border-box; 22 - box-shadow: 0 0 24px rgba(185, 103, 255, 0.15); 22 + box-shadow: 0 2px 18px rgba(185, 103, 255, 0.18); 23 23 } 24 24 input { 25 25 width: 100%; ··· 52 52 top: 50%; 53 53 translate: 0 -50%; 54 54 font-size: 0.9rem; 55 - animation: turn 1.2s linear infinite; 55 + animation: turn 2.5s linear infinite; 56 56 } 57 57 @keyframes turn { 58 58 to {