|
| 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>Pilot App Store — Catalogue</title> |
| 7 | +<meta name="description" content="Apps installable via pilotctl appstore install — the Pilot Protocol app store catalogue." /> |
| 8 | +<style> |
| 9 | + :root { |
| 10 | + --bg: #0b0e14; --panel: #131823; --panel2: #1b2230; --line: #273043; |
| 11 | + --fg: #e6e9ef; --muted: #93a0b5; --accent: #5b8cff; --ok: #46d39a; --code: #0f1420; |
| 12 | + } |
| 13 | + * { box-sizing: border-box; } |
| 14 | + body { |
| 15 | + margin: 0; background: var(--bg); color: var(--fg); |
| 16 | + font: 16px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
| 17 | + } |
| 18 | + header { padding: 56px 24px 28px; max-width: 1040px; margin: 0 auto; } |
| 19 | + h1 { font-size: 30px; margin: 0 0 8px; letter-spacing: -0.02em; } |
| 20 | + .sub { color: var(--muted); margin: 0; } |
| 21 | + .meta { color: var(--muted); font-size: 13px; margin-top: 14px; } |
| 22 | + main { max-width: 1040px; margin: 0 auto; padding: 0 24px 64px; } |
| 23 | + .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 18px; } |
| 24 | + .card { |
| 25 | + background: linear-gradient(180deg, var(--panel), var(--panel2)); |
| 26 | + border: 1px solid var(--line); border-radius: 14px; padding: 20px 20px 18px; |
| 27 | + display: flex; flex-direction: column; gap: 12px; |
| 28 | + } |
| 29 | + .card h2 { font-size: 18px; margin: 0; display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; } |
| 30 | + .ver { color: var(--ok); font-size: 13px; font-weight: 600; background: rgba(70,211,154,.12); padding: 2px 8px; border-radius: 999px; } |
| 31 | + .desc { color: var(--fg); margin: 0; } |
| 32 | + .label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; margin: 4px 0 2px; } |
| 33 | + .methods { display: flex; flex-wrap: wrap; gap: 6px; } |
| 34 | + .pill { background: var(--code); border: 1px solid var(--line); border-radius: 8px; padding: 2px 8px; font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--accent); } |
| 35 | + .muted { color: var(--muted); font-size: 13px; } |
| 36 | + pre { background: var(--code); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; margin: 0; overflow-x: auto; } |
| 37 | + code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; color: var(--fg); } |
| 38 | + .row { display: flex; align-items: center; justify-content: space-between; gap: 10px; } |
| 39 | + a { color: var(--accent); text-decoration: none; } |
| 40 | + a:hover { text-decoration: underline; } |
| 41 | + .foot { color: var(--muted); font-size: 13px; margin-top: 36px; border-top: 1px solid var(--line); padding-top: 18px; } |
| 42 | + .err { background: #2a1620; border: 1px solid #5a2740; color: #ffb4c4; padding: 16px; border-radius: 12px; } |
| 43 | + .sha { font-family: ui-monospace, monospace; font-size: 11px; color: var(--muted); word-break: break-all; } |
| 44 | +</style> |
| 45 | +</head> |
| 46 | +<body> |
| 47 | +<header> |
| 48 | + <h1>Pilot App Store</h1> |
| 49 | + <p class="sub">Apps installable with <code>pilotctl appstore install <id></code> over the Pilot overlay network.</p> |
| 50 | + <p class="meta" id="meta"></p> |
| 51 | +</header> |
| 52 | +<main> |
| 53 | + <div id="content"><p class="muted">Loading catalogue…</p></div> |
| 54 | + <p class="foot"> |
| 55 | + The catalogue is fetched from <code id="src"></code> and is signed (detached ed25519); |
| 56 | + <code>pilotctl</code> verifies the signature before any install. |
| 57 | + </p> |
| 58 | +</main> |
| 59 | +<script> |
| 60 | +// Same-origin catalogue copied next to this page at deploy time. Override |
| 61 | +// with ?src= for local testing against another catalogue URL. |
| 62 | +const params = new URLSearchParams(location.search); |
| 63 | +const SRC = params.get("src") || "./catalogue.json"; |
| 64 | + |
| 65 | +function el(tag, cls, text) { |
| 66 | + const e = document.createElement(tag); |
| 67 | + if (cls) e.className = cls; |
| 68 | + if (text != null) e.textContent = text; |
| 69 | + return e; |
| 70 | +} |
| 71 | + |
| 72 | +function card(app) { |
| 73 | + const c = el("div", "card"); |
| 74 | + const h = el("h2"); |
| 75 | + h.appendChild(el("span", null, app.id || "(unknown id)")); |
| 76 | + if (app.version) h.appendChild(el("span", "ver", "v" + app.version)); |
| 77 | + c.appendChild(h); |
| 78 | + |
| 79 | + if (app.description) c.appendChild(el("p", "desc", app.description)); |
| 80 | + |
| 81 | + // Methods aren't part of the v1 catalogue schema (they live in each |
| 82 | + // app's manifest); render them when an entry provides them. |
| 83 | + if (Array.isArray(app.methods) && app.methods.length) { |
| 84 | + c.appendChild(el("div", "label", "Methods")); |
| 85 | + const m = el("div", "methods"); |
| 86 | + app.methods.forEach(x => m.appendChild(el("span", "pill", x))); |
| 87 | + c.appendChild(m); |
| 88 | + } |
| 89 | + |
| 90 | + c.appendChild(el("div", "label", "Install")); |
| 91 | + const pre = el("pre"); |
| 92 | + pre.appendChild(el("code", null, "pilotctl appstore install " + (app.id || ""))); |
| 93 | + c.appendChild(pre); |
| 94 | + |
| 95 | + if (app.bundle_url) { |
| 96 | + const row = el("div", "row"); |
| 97 | + const a = el("a", null, "bundle ↗"); |
| 98 | + a.href = app.bundle_url; a.rel = "noopener"; a.target = "_blank"; |
| 99 | + row.appendChild(a); |
| 100 | + if (app.bundle_sha256) row.appendChild(el("span", "sha", app.bundle_sha256.slice(0, 16) + "…")); |
| 101 | + c.appendChild(row); |
| 102 | + } |
| 103 | + return c; |
| 104 | +} |
| 105 | + |
| 106 | +async function render() { |
| 107 | + const content = document.getElementById("content"); |
| 108 | + document.getElementById("src").textContent = SRC; |
| 109 | + try { |
| 110 | + const res = await fetch(SRC, { cache: "no-store" }); |
| 111 | + if (!res.ok) throw new Error("HTTP " + res.status); |
| 112 | + const cat = await res.json(); |
| 113 | + document.getElementById("meta").textContent = |
| 114 | + "Schema v" + cat.version + (cat.updated_at ? " · updated " + cat.updated_at : "") + |
| 115 | + " · " + (cat.apps ? cat.apps.length : 0) + " app(s)"; |
| 116 | + content.textContent = ""; |
| 117 | + if (!cat.apps || !cat.apps.length) { |
| 118 | + content.appendChild(el("p", "muted", "The catalogue is empty.")); |
| 119 | + return; |
| 120 | + } |
| 121 | + const grid = el("div", "grid"); |
| 122 | + cat.apps.forEach(a => grid.appendChild(card(a))); |
| 123 | + content.appendChild(grid); |
| 124 | + } catch (e) { |
| 125 | + content.innerHTML = ""; |
| 126 | + const box = el("div", "err", "Could not load the catalogue: " + e.message); |
| 127 | + content.appendChild(box); |
| 128 | + } |
| 129 | +} |
| 130 | +render(); |
| 131 | +</script> |
| 132 | +</body> |
| 133 | +</html> |
0 commit comments