pyrtan / site / catalog / index.html
5,896 bytes · last changed 2026-10-27 in Simplify the text on pyrtan.com
| 1 | <!doctype html> |
| 2 | <html lang="en"> |
| 3 | <head> |
| 4 | <meta charset="utf-8"> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 6 | <title>Pyrtan catalog</title> |
| 7 | <meta name="description" content="Ready-made Pyrtan trees you can download and run."> |
| 8 | <meta name="theme-color" content="#c2410c"> |
| 9 | <script src="../theme.js"></script> |
| 10 | <style> |
| 11 | :root { --bg:#fbfaf7; --fg:#1d1d1b; --mute:#6b6a64; --line:#e3e1da; --card:#ffffff; --pyr:#c2410c; --pyr-fg:#fff; --tan:#a8804f; --tan-fg:#fff; --soft:#f1efe9 } |
| 12 | @media (prefers-color-scheme: dark) { :root:not([data-theme=light]) { --bg:#141513; --fg:#ecebe6; --mute:#9d9c95; --line:#2b2c29; --card:#1c1d1b; --pyr:#fb8a4c; --pyr-fg:#101318; --tan:#d2b48c; --tan-fg:#101318; --soft:#222420 } } |
| 13 | :root[data-theme=dark] { --bg:#141513; --fg:#ecebe6; --mute:#9d9c95; --line:#2b2c29; --card:#1c1d1b; --pyr:#fb8a4c; --pyr-fg:#101318; --tan:#d2b48c; --tan-fg:#101318; --soft:#222420 } |
| 14 | :root { --accent:var(--pyr); --accent-fg:var(--pyr-fg) } |
| 15 | * { box-sizing:border-box } |
| 16 | body { margin:0; background:var(--bg); color:var(--fg); font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif } |
| 17 | main { max-width:920px; margin:0 auto; padding:32px 16px 64px } |
| 18 | h1 { font-size:24px; margin:0 0 4px } p.lede { color:var(--mute); margin:0 0 20px } |
| 19 | input { width:100%; font:inherit; padding:10px 12px; border:1px solid var(--line); border-radius:8px; background:var(--card); color:var(--fg); margin:0 0 14px } |
| 20 | .card { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:14px 16px; margin:0 0 12px } |
| 21 | .card h2 { font-size:17px; margin:0 0 4px } .card h2 small { color:var(--mute); font-weight:normal; font-size:13px; margin-left:8px } |
| 22 | .mute { color:var(--mute); font-size:14px } |
| 23 | code { font:13px ui-monospace,SFMono-Regular,Menlo,monospace; background:var(--soft); padding:1px 5px; border-radius:4px } |
| 24 | pre { margin:8px 0 0; padding:8px 12px; background:var(--soft); border-radius:8px; font:13px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; overflow:auto } |
| 25 | a { color:var(--accent) } |
| 26 | .pyr { color:var(--pyr) } .tan { color:var(--tan) } |
| 27 | :focus-visible { outline:2px solid var(--tan); outline-offset:2px } |
| 28 | ::selection { background:var(--tan); color:var(--tan-fg) } |
| 29 | footer { color:var(--mute); font-size:13px; margin-top:32px } |
| 30 | </style> |
| 31 | </head> |
| 32 | <body> |
| 33 | <main> |
| 34 | <h1><span class="pyr" data-theme-part="pyr">pyr</span><span class="tan" data-theme-part="tan">tan</span> catalog</h1> |
| 35 | <p class="lede">Ready-made trees you can download and run. Each one is a single tested TOML file, under its author's licence. <code>pyrtan pull NAMESPACE/NAME</code> checks the file against <a href="index.json">index.json</a> before saving it. Where hubs have hit a case a tree doesn't cover, its <a href="../rendezvous/reports.json">gap reports</a> are shown beside it.</p> |
| 36 | <input type="search" id="q" placeholder="filter: weather, github, read-only…" autocomplete="off"> |
| 37 | <div id="list"><p class="mute">Loading index.json…</p></div> |
| 38 | <footer>To publish a tree, run <code>pyrtan publish my-tree.toml --into <checkout> --owner <your GitHub login></code> and open a pull request. Once it passes the checks and is merged, it shows up here. <a href="../">pyrtan.com</a> · <a href="../source/blob/catalog/README.md.html">how</a> · <a href="../source/">source</a></footer> |
| 39 | </main> |
| 40 | <script> |
| 41 | const esc = (s) => String(s).replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c])); |
| 42 | let trees = [], reports = []; |
| 43 | const idOf = (t) => t.id || (t.namespace ? `${t.namespace}/${t.name}` : t.name); |
| 44 | function render() { |
| 45 | const q = document.getElementById("q").value.toLowerCase().split(/\s+/).filter(Boolean); |
| 46 | const list = document.getElementById("list"); list.innerHTML = ""; |
| 47 | const shown = trees.filter(t => { const hay = [idOf(t), t.description, ...(t.tags||[]), ...(t.hosts||[]), ...(t.sources||[]), ...(t.triggers||[])].join(" ").toLowerCase(); return q.every(w => hay.includes(w)); }); |
| 48 | for (const t of shown) { |
| 49 | const id = idOf(t); |
| 50 | const reach = (t.hosts||[]).concat((t.sources||[]).filter(s => s !== "http")); |
| 51 | const mine = reports.filter(r => String(r.id).split("@")[0] === id); |
| 52 | const seen = mine.reduce((n, r) => n + (r.count||1), 0), hubs = Math.max(0, ...mine.map(r => r.reporters||1)); |
| 53 | const gaps = mine.length ? `<details><summary class="mute">${seen} gap${seen === 1 ? "" : "s"} reported by ${hubs} hub${hubs === 1 ? "" : "s"}: cases this tree doesn't cover</summary>${mine.map(r => `<div><code>${esc(r.node)}</code> ${esc(r.on)} = <code>${esc(JSON.stringify(r.observed))}</code> <span class="mute">×${esc(r.count||1)}${r.id.includes("@") ? " · v" + esc(r.id.split("@")[1]) : ""}</span></div>`).join("")}</details>` : ""; |
| 54 | const d = document.createElement("div"); d.className = "card"; |
| 55 | d.innerHTML = `<h2><code>${esc(id)}</code><small>v${esc(t.version)} · ${esc(t.license||"no licence")}${reach.length ? " · " + esc(reach.join(", ")) : ""}</small></h2><div class="mute">${esc(t.description||"")}</div>${(t.triggers||[]).map(p => `<div><code>${esc(p)}</code></div>`).join("")}<pre>pyrtan pull ${esc(id)}</pre>${gaps}<div class="mute"><a href="${esc(t.path)}">${esc(t.path)}</a> · sha256 ${esc((t.sha256||"").slice(0, 12))}…</div>`; |
| 56 | list.appendChild(d); |
| 57 | } |
| 58 | if (!shown.length) list.innerHTML = `<p class="mute">Nothing matches.</p>`; |
| 59 | } |
| 60 | fetch("index.json").then(r => r.json()).then(i => { trees = i.trees || []; document.title = `Pyrtan catalog · ${trees.length} trees`; render(); }).catch(e => { document.getElementById("list").innerHTML = `<p class="mute">Could not load index.json: ${esc(e)}</p>`; }); |
| 61 | fetch("../rendezvous/reports.json").then(r => r.ok ? r.json() : null).then(j => { if (j && j.reports) { reports = j.reports; render(); } }).catch(() => {}); |
| 62 | document.getElementById("q").addEventListener("input", render); |
| 63 | </script> |
| 64 | </body> |
| 65 | </html> |