add platform tracking to dashboard

- track soundcloud/bandcamp/spotify/plyr.fm matches separately
- show platform breakdown with color-coded bar charts on dashboard
- fix terminology: turbostream is a stream provider, not a relay

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+38 -17
src/dashboard.zig
··· 1 1 const std = @import("std"); 2 - const posix = std.posix; 3 2 const stats = @import("stats.zig"); 4 3 const db = @import("db.zig"); 5 - 6 - const DEFAULT_TURBOSTREAM = "api.graze.social"; 7 4 8 5 fn formatNumber(buf: []u8, n: u64) []const u8 { 9 6 var temp: [32]u8 = undefined; ··· 32 29 const lag_ms = s.getPostLagMs(); 33 30 const lag_sec = @divTrunc(lag_ms, 1000); 34 31 const status = s.getStatus(); 35 - const turbostream_host = posix.getenv("TURBOSTREAM_HOST") orelse DEFAULT_TURBOSTREAM; 36 32 37 33 var buf: std.ArrayList(u8) = .{}; 38 34 const w = buf.writer(alloc); ··· 100 96 \\ } 101 97 \\ .stat-value { font-size: 18px; color: #fff; } 102 98 \\ .stat-label { font-size: 11px; color: #444; } 103 - \\ .criteria { 99 + \\ .criteria, .platforms { 104 100 \\ margin-bottom: 1.5rem; 105 101 \\ font-size: 12px; 106 102 \\ } 107 103 \\ .criteria-title { color: #444; margin-bottom: 0.25rem; } 108 104 \\ .criteria-list { color: #666; } 105 + \\ .platform-bars { margin-top: 0.5rem; } 106 + \\ .platform-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.3rem; } 107 + \\ .platform-name { width: 80px; color: #666; } 108 + \\ .bar { flex: 1; height: 8px; background: #1a1a1a; border-radius: 4px; overflow: hidden; } 109 + \\ .bar-fill { height: 100%; border-radius: 4px; } 110 + \\ .platform-pct { width: 32px; text-align: right; color: #555; font-size: 11px; } 109 111 \\ footer { 110 112 \\ padding-top: 1rem; 111 113 \\ border-top: 1px solid #1a1a1a; ··· 159 161 \\ </div> 160 162 \\ </div> 161 163 \\ 162 - \\ <div class="criteria"> 163 - \\ <div class="criteria-title">included</div> 164 - \\ <div class="criteria-list">soundcloud, bandcamp, spotify, plyr.fm links</div> 164 + \\ <div class="platforms"> 165 + \\ <div class="criteria-title">platforms</div> 166 + \\ <div class="platform-bars"> 167 + ); 168 + 169 + // get platform counts 170 + const platforms = s.getPlatformCounts(); 171 + const total = platforms.soundcloud + platforms.bandcamp + platforms.spotify + platforms.plyr; 172 + 173 + if (total > 0) { 174 + // calculate percentages 175 + const sc_pct = (platforms.soundcloud * 100) / total; 176 + const bc_pct = (platforms.bandcamp * 100) / total; 177 + const sp_pct = (platforms.spotify * 100) / total; 178 + const pl_pct = (platforms.plyr * 100) / total; 179 + 180 + try w.print( 181 + \\ <div class="platform-row"><span class="platform-name">soundcloud</span><div class="bar"><div class="bar-fill" style="width:{d}%;background:#ff5500"></div></div><span class="platform-pct">{d}%</span></div> 182 + \\ <div class="platform-row"><span class="platform-name">bandcamp</span><div class="bar"><div class="bar-fill" style="width:{d}%;background:#1da0c3"></div></div><span class="platform-pct">{d}%</span></div> 183 + \\ <div class="platform-row"><span class="platform-name">spotify</span><div class="bar"><div class="bar-fill" style="width:{d}%;background:#1db954"></div></div><span class="platform-pct">{d}%</span></div> 184 + \\ <div class="platform-row"><span class="platform-name">plyr.fm</span><div class="bar"><div class="bar-fill" style="width:{d}%;background:#7c3aed"></div></div><span class="platform-pct">{d}%</span></div> 185 + , .{ sc_pct, sc_pct, bc_pct, bc_pct, sp_pct, sp_pct, pl_pct, pl_pct }); 186 + } else { 187 + try w.writeAll( 188 + \\ <div class="platform-row"><span class="criteria-list">collecting data...</span></div> 189 + ); 190 + } 191 + 192 + try w.writeAll( 193 + \\ </div> 165 194 \\ </div> 166 195 \\ 167 196 \\ <div class="criteria"> ··· 171 200 \\ 172 201 \\ <footer> 173 202 \\ <a href="https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed">source</a> 174 - \\ <span>relay: <a href="https:// 175 - ); 176 - try w.writeAll(turbostream_host); 177 - try w.writeAll( 178 - \\" target="_blank"> 179 - ); 180 - try w.writeAll(turbostream_host); 181 - try w.writeAll( 182 - \\</a></span> 203 + \\ <span>stream: <a href="https://graze.social" target="_blank">turbostream</a></span> 183 204 \\ </footer> 184 205 \\ </div> 185 206 \\
+56
src/filter.zig
··· 1 1 const std = @import("std"); 2 2 const mem = std.mem; 3 3 const json = std.json; 4 + const stats = @import("stats.zig"); 4 5 5 6 const Record = json.ObjectMap; 6 7 const Filter = *const fn (Record) ?bool; ··· 116 117 if (mem.indexOf(u8, uri, domain) != null) return true; 117 118 } 118 119 return false; 120 + } 121 + 122 + /// detect which platform a URL belongs to 123 + pub fn detectPlatform(uri: []const u8) ?stats.Platform { 124 + if (mem.indexOf(u8, uri, "soundcloud.com") != null or 125 + mem.indexOf(u8, uri, "on.soundcloud.com") != null) 126 + { 127 + return .soundcloud; 128 + } 129 + if (mem.indexOf(u8, uri, "bandcamp.com") != null) return .bandcamp; 130 + if (mem.indexOf(u8, uri, "open.spotify.com") != null or 131 + mem.indexOf(u8, uri, "spotify.link") != null) 132 + { 133 + return .spotify; 134 + } 135 + if (mem.indexOf(u8, uri, "plyr.fm") != null) return .plyr; 136 + return null; 137 + } 138 + 139 + /// detect platform from post record (checks facets and embeds) 140 + pub fn detectPlatformFromRecord(record: Record) ?stats.Platform { 141 + // check facets 142 + if (record.get("facets")) |facets_val| { 143 + if (facets_val == .array) { 144 + for (facets_val.array.items) |facet| { 145 + if (facet != .object) continue; 146 + const features_val = facet.object.get("features") orelse continue; 147 + if (features_val != .array) continue; 148 + 149 + for (features_val.array.items) |feature| { 150 + if (feature != .object) continue; 151 + const uri_val = feature.object.get("uri") orelse continue; 152 + if (uri_val != .string) continue; 153 + if (detectPlatform(uri_val.string)) |p| return p; 154 + } 155 + } 156 + } 157 + } 158 + 159 + // check embed 160 + if (record.get("embed")) |embed_val| { 161 + if (embed_val == .object) { 162 + if (embed_val.object.get("external")) |external_val| { 163 + if (external_val == .object) { 164 + if (external_val.object.get("uri")) |uri_val| { 165 + if (uri_val == .string) { 166 + if (detectPlatform(uri_val.string)) |p| return p; 167 + } 168 + } 169 + } 170 + } 171 + } 172 + } 173 + 174 + return null; 119 175 } 120 176 121 177 /// simple text-based check for music links (used for backfill)
+6
src/jetstream.zig
··· 165 165 }; 166 166 167 167 stats.get().recordMatch(); 168 + 169 + // track platform 170 + if (filter.detectPlatformFromRecord(post_record.object)) |platform| { 171 + stats.get().recordPlatform(platform); 172 + } 173 + 168 174 std.debug.print("added: {s}\n", .{at_uri.string}); 169 175 } 170 176 };
+30
src/stats.zig
··· 6 6 7 7 const STATS_PATH = "/data/stats.json"; 8 8 9 + pub const Platform = enum { soundcloud, bandcamp, spotify, plyr }; 10 + 9 11 pub const Stats = struct { 10 12 started_at: i64, 11 13 prior_uptime: u64 = 0, ··· 17 19 connected_at: Atomic(i64), 18 20 last_post_time_ms: Atomic(i64), // actual post creation time from TID 19 21 22 + // platform counters 23 + soundcloud: Atomic(u64), 24 + bandcamp: Atomic(u64), 25 + spotify: Atomic(u64), 26 + plyr: Atomic(u64), 27 + 20 28 // for lag trend tracking 21 29 prev_lag_ms: Atomic(i64), 22 30 ··· 30 38 .last_match_time = Atomic(i64).init(0), 31 39 .connected_at = Atomic(i64).init(0), 32 40 .last_post_time_ms = Atomic(i64).init(0), 41 + .soundcloud = Atomic(u64).init(0), 42 + .bandcamp = Atomic(u64).init(0), 43 + .spotify = Atomic(u64).init(0), 44 + .plyr = Atomic(u64).init(0), 33 45 .prev_lag_ms = Atomic(i64).init(0), 34 46 }; 35 47 self.load(); ··· 110 122 pub fn recordMatch(self: *Stats) void { 111 123 _ = self.matches.fetchAdd(1, .monotonic); 112 124 self.last_match_time.store(std.time.milliTimestamp(), .monotonic); 125 + } 126 + 127 + pub fn recordPlatform(self: *Stats, platform: Platform) void { 128 + switch (platform) { 129 + .soundcloud => _ = self.soundcloud.fetchAdd(1, .monotonic), 130 + .bandcamp => _ = self.bandcamp.fetchAdd(1, .monotonic), 131 + .spotify => _ = self.spotify.fetchAdd(1, .monotonic), 132 + .plyr => _ = self.plyr.fetchAdd(1, .monotonic), 133 + } 134 + } 135 + 136 + pub fn getPlatformCounts(self: *const Stats) struct { soundcloud: u64, bandcamp: u64, spotify: u64, plyr: u64 } { 137 + return .{ 138 + .soundcloud = self.soundcloud.load(.monotonic), 139 + .bandcamp = self.bandcamp.load(.monotonic), 140 + .spotify = self.spotify.load(.monotonic), 141 + .plyr = self.plyr.load(.monotonic), 142 + }; 113 143 } 114 144 115 145 pub fn recordConnected(self: *Stats) void {