eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1<!-- 🤖 The music ledger: what was playing on contributor machines while
2 Eleven was built — the soundtrack companion to the carbon and prompt
3 ledgers. Data is COMMITTED like the carbon tally and SHARDED BY MONTH:
4 stream-first: the page prefers the live ledger stream's derived views
5 (./ledgers/music/…, hash-chained appends — issue #1224) and falls back
6 to the committed files. Either way ./music.json is the index (months +
7 totals), ./music-YYYY-MM.json
8 the shards, accrued by tools/music-ledger.mjs (skips filtered, album
9 art deduped per album and inlined as data URIs — the pages allow no
10 external hosts). The newest month renders on load; earlier months fetch
11 on demand. Titles link to song.link, albums to album.link. -->
12<!doctype html>
13<html lang="en">
14<head>
15<meta charset="utf-8" />
16<meta name="viewport" content="width=device-width, initial-scale=1" />
17<meta name="color-scheme" content="light dark" />
18<title>Eleven — the music ledger</title>
19<link rel="icon" type="image/png" href="./icon-192.png" />
20<link rel="stylesheet" href="./status.css" />
21<style>
22 /* Music-only styles — track cards echo the access ledger's session
23 rows plus a small art tile. Everything shared is in status.css. */
24 .tracks { list-style: none; padding: 0; }
25 .tracks li {
26 display: flex; gap: 14px; align-items: flex-start;
27 background: var(--surface); border: 1px solid var(--border);
28 border-radius: 12px; padding: 14px 16px; margin: 10px 0;
29 }
30 .tracks .art { width: 56px; height: 56px; border-radius: 8px; flex: none; }
31 .tracks .art.placeholder {
32 display: flex; align-items: center; justify-content: center;
33 background: var(--grid); color: var(--ink-2); font-size: 22px;
34 }
35 .tracks .body { min-width: 0; }
36 .tracks .top { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: baseline; }
37 .tracks .who { font-weight: 650; }
38 .tracks .when { color: var(--ink-2); font-size: 14px; font-variant-numeric: tabular-nums; }
39 .tracks .facts { color: var(--ink-2); font-size: 14px; margin-top: 4px; }
40 .badge {
41 display: inline-block; font-size: 12px; font-weight: 650; border-radius: 999px;
42 padding: 1px 10px; border: 1px solid var(--border); color: var(--ink-2);
43 }
44 .month-h { font-size: 15px; font-weight: 650; margin: 18px 0 4px; }
45 #earlier[hidden] { display: none; } /* display:block below would defeat [hidden] */
46 #earlier {
47 display: block; width: 100%; font: inherit; font-weight: 650; cursor: pointer;
48 background: var(--surface); color: var(--link); border: 1px solid var(--border);
49 border-radius: 12px; padding: 12px 16px; margin: 10px 0;
50 }
51 #earlier:hover { background: var(--grid); }
52 #earlier:focus-visible { outline: 2px solid var(--series); outline-offset: 2px; }
53 .empty { color: var(--ink-2); background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; }
54</style>
55</head>
56<body>
57<div class="wrap">
58 <header data-title="The music ledger"></header>
59 <nav class="status-nav" aria-label="Status"></nav>
60 <script src="./status-nav.js"></script>
61
62 <p class="lede">What was playing on contributor machines while Eleven was
63 built — the soundtrack to the prompt ledger, attributed the same way.
64 Sessions sample Apple Music every 20 seconds; a track that held for under a
65 minute counts as a skip and never publishes. Rows carry the signals Music
66 exposes — <em>queued</em>, <em>radio</em>, <em>shuffle</em> — and a bare
67 catalog stream carries none, because Music can't tell a direct pick from
68 autoplay. Titles link to the song's <a href="https://song.link">song.link</a>
69 page, albums to album.link. One month loads at a time.</p>
70
71 <p class="asof">Last played <time id="asof" datetime="">—</time></p>
72
73 <p id="error" role="alert">Couldn’t load the music ledger. It may be mid-publish — this page retries every minute.</p>
74
75 <div class="tiles" id="totals"></div>
76
77 <section aria-labelledby="h-tracks">
78 <h2 id="h-tracks">Tracks, newest first</h2>
79 <div id="months"></div>
80 <button id="earlier" hidden></button>
81 <p class="empty" id="empty" hidden>Nothing in the ledger yet — the needle
82 hasn’t dropped.</p>
83 </section>
84
85 <footer>
86 <p>Accrued per session by <code>tools/music-ledger.mjs</code> from a
87 machine-local Apple Music log (song ids and art resolved once, at accrual —
88 this page makes no external requests). Convention: <a
89 href="https://github.com/elevenmessenger/messenger/blob/main/docs/status.md">docs/status.md</a>.</p>
90 </footer>
91</div>
92
93<script>
94(async function () {
95 "use strict";
96 const $ = (id) => document.getElementById(id);
97 const FIELDS = ["updated_at", "months[]", "totals.tracks", "totals.artists",
98 "totals.albums", "totals.listeners"];
99
100 const fmtInt = (n) => Number(n).toLocaleString();
101 const PLURAL = new Intl.PluralRules();
102 const plural = (n, one, many) => PLURAL.select(n) === "one" ? one : (many || one + "s");
103 const fmtDay = (iso) => new Date(iso).toLocaleDateString(undefined, { day: "numeric", month: "short", year: "numeric" });
104 const fmtTime = (iso) => new Date(iso).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
105 const fmtMonth = (m) => new Date(m + "-15T12:00:00Z").toLocaleDateString(undefined, { month: "long", year: "numeric" });
106 const artKey = (t) => t.album_id != null ? String(t.album_id) : t.track + " — " + t.artist;
107
108 let months = []; // index order, newest first
109 // Where the data lives: the live ledger stream's derived views when the
110 // bucket has them (issue #1224 — updated in seconds, no deploy), else the
111 // committed files that ride the release train.
112 let base = "./";
113 let shown = 0; // how many months are rendered
114
115 function trackRow(t, albums) {
116 const li = document.createElement("li");
117 let art;
118 const uri = albums[artKey(t)];
119 if (uri) {
120 art = document.createElement("img");
121 art.className = "art";
122 art.src = uri;
123 art.alt = "";
124 art.width = 56; art.height = 56;
125 } else {
126 art = document.createElement("span");
127 art.className = "art placeholder";
128 art.setAttribute("aria-hidden", "true");
129 art.textContent = "♪";
130 }
131 li.appendChild(art);
132
133 const body = document.createElement("div");
134 body.className = "body";
135 const top = document.createElement("div");
136 top.className = "top";
137 const who = document.createElement("span");
138 who.className = "who";
139 if (t.song_id) {
140 const a = document.createElement("a");
141 a.href = "https://song.link/i/" + t.song_id;
142 a.textContent = t.track;
143 who.appendChild(a);
144 } else {
145 who.textContent = t.track;
146 }
147 const when = document.createElement("span");
148 when.className = "when";
149 when.textContent = fmtDay(t.ts) + " · " + fmtTime(t.ts);
150 top.append(who, when);
151 for (const text of [t.listener, t.source].concat(t.signals || [])) {
152 const b = document.createElement("span");
153 b.className = "badge";
154 b.textContent = text;
155 top.appendChild(b);
156 }
157 body.appendChild(top);
158
159 const facts = document.createElement("div");
160 facts.className = "facts";
161 facts.append(t.artist + " · ");
162 if (t.album_id) {
163 const a = document.createElement("a");
164 a.href = "https://album.link/i/" + t.album_id;
165 a.textContent = t.album;
166 facts.appendChild(a);
167 } else {
168 facts.append(t.album);
169 }
170 body.appendChild(facts);
171 li.appendChild(body);
172 return li;
173 }
174
175 async function showMonth(m) {
176 const r = await fetch(base + "music-" + m + ".json");
177 if (!r.ok) throw 0;
178 const shard = await r.json();
179 const sec = document.createElement("section");
180 const h = document.createElement("h3");
181 h.className = "month-h";
182 h.textContent = fmtMonth(m);
183 const ol = document.createElement("ol");
184 ol.className = "tracks";
185 ol.setAttribute("reversed", "");
186 for (const t of shard.tracks.slice().sort((a, b) => b.ts.localeCompare(a.ts))) {
187 ol.appendChild(trackRow(t, shard.albums || {}));
188 }
189 sec.append(h, ol);
190 $("months").appendChild(sec);
191 shown++;
192 const btn = $("earlier");
193 if (shown < months.length) {
194 btn.hidden = false;
195 btn.textContent = "Show " + fmtMonth(months[shown]);
196 } else {
197 btn.hidden = true;
198 }
199 }
200
201 $("earlier").addEventListener("click", () => {
202 showMonth(months[shown]).catch(() => $("error").classList.add("show"));
203 });
204
205 function renderTiles(totals) {
206 const tiles = [
207 [plural(totals.tracks, "track"), fmtInt(totals.tracks)],
208 [plural(totals.artists, "artist"), fmtInt(totals.artists)],
209 [plural(totals.albums, "album"), fmtInt(totals.albums)],
210 [plural(totals.listeners, "listener"), fmtInt(totals.listeners)],
211 ];
212 const totalsEl = $("totals");
213 totalsEl.textContent = "";
214 for (const [label, val] of tiles) {
215 const tile = document.createElement("div");
216 tile.className = "tile";
217 const l = document.createElement("div"); l.className = "label"; l.textContent = label;
218 const v = document.createElement("div"); v.className = "value"; v.textContent = val;
219 tile.append(l, v);
220 totalsEl.appendChild(tile);
221 }
222 }
223
224 async function load() {
225 try {
226 let r = await fetch("./ledgers/music/music.json");
227 base = r.ok ? "./ledgers/music/" : "./";
228 if (!r.ok) r = await fetch("./music.json");
229 if (!r.ok) throw 0;
230 const doc = await r.json();
231 $("error").classList.remove("show");
232 const asof = $("asof");
233 asof.dateTime = doc.updated_at || "";
234 asof.textContent = doc.updated_at ? fmtDay(doc.updated_at) + " " + fmtTime(doc.updated_at) : "—";
235 renderTiles(doc.totals);
236 months = doc.months;
237 $("empty").hidden = months.length > 0;
238 // Re-render however many months the reader had open (at least one).
239 const keep = Math.max(1, shown);
240 $("months").textContent = "";
241 shown = 0;
242 for (let i = 0; i < Math.min(keep, months.length); i++) await showMonth(months[i]);
243 } catch {
244 $("error").classList.add("show");
245 }
246 }
247 await load();
248 setInterval(load, 60000); // same retry cadence as the other dashboards
249})();
250</script>
251</body>
252</html>
253