pyrtan / sourcev0.19.0 · main

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 &lt;checkout&gt; --owner &lt;your GitHub login&gt;</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>
41const esc = (s) => String(s).replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
42let trees = [], reports = [];
43const idOf = (t) => t.id || (t.namespace ? `${t.namespace}/${t.name}` : t.name);
44function 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}
60fetch("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>`; });
61fetch("../rendezvous/reports.json").then(r => r.ok ? r.json() : null).then(j => { if (j && j.reports) { reports = j.reports; render(); } }).catch(() => {});
62document.getElementById("q").addEventListener("input", render);
63</script>
64</body>
65</html>