tracks lexicons and how many times they appeared on the jetstream

Compare changes

Choose any two refs to compare.

+9 -9
client/src/app.html
··· 1 1 <!doctype html> 2 2 <html lang="en"> 3 - <head> 4 - <meta charset="utf-8" /> 5 - <link rel="icon" href="%sveltekit.assets%/favicon.svg" /> 6 - <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 - %sveltekit.head% 8 - </head> 9 - <body data-sveltekit-preload-data="hover"> 10 - <div style="display: contents">%sveltekit.body%</div> 11 - </body> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <link rel="icon" href="%sveltekit.assets%/favicon.svg" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 + %sveltekit.head% 8 + </head> 9 + <body class="bg-white dark:bg-gray-900" data-sveltekit-preload-data="hover"> 10 + <div style="display: contents">%sveltekit.body%</div> 11 + </body> 12 12 </html>
+2 -2
client/src/lib/components/BskyToggle.svelte
··· 11 11 <!-- svelte-ignore a11y_no_static_element_interactions --> 12 12 <button 13 13 onclick={onBskyToggle} 14 - class="wsbadge !mt-0 !font-normal bg-yellow-100 hover:bg-yellow-200 border-yellow-300" 14 + class="wsbadge !mt-0 !font-normal bg-blue-100 dark:bg-blue-900 hover:bg-blue-200 dark:hover:bg-blue-800 border-blue-300 dark:border-blue-700" 15 15 > 16 16 <input checked={dontShowBsky} type="checkbox" /> 17 - <span class="ml-0.5"> hide app.bsky.* </span> 17 + <span class="ml-0.5 text-black dark:text-gray-200"> hide app.bsky.* </span> 18 18 </button>
+8 -5
client/src/lib/components/EventCard.svelte
··· 104 104 </script> 105 105 106 106 <div 107 - class="group flex flex-col gap-2 p-1.5 md:p-3 min-h-64 bg-white border border-gray-200 rounded-lg hover:shadow-lg md:hover:-translate-y-1 transition-all duration-200 transform" 107 + class="group flex flex-col gap-2 p-1.5 md:p-3 min-h-64 bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-gray-950 rounded-lg hover:shadow-lg md:hover:-translate-y-1 transition-all duration-200 transform" 108 108 class:has-activity={isAnimating} 109 109 style="--border-thickness: {borderThickness}px" 110 110 > 111 111 <div class="flex items-start gap-2"> 112 112 <div 113 - class="text-sm font-bold text-blue-600 bg-blue-100 px-3 py-1 rounded-full" 113 + class="text-sm font-bold text-blue-600 bg-blue-100 dark:bg-indigo-950 px-3 py-1 rounded-full" 114 114 > 115 115 #{index + 1} 116 116 </div> 117 117 <div 118 118 title={event.nsid} 119 - class="font-mono text-sm text-gray-700 mt-0.5 leading-relaxed rounded-full text-nowrap text-ellipsis overflow-hidden group-hover:overflow-visible group-hover:bg-gray-50 border-gray-100 group-hover:border transition-all px-1" 119 + class="font-mono text-sm text-gray-700 dark:text-gray-300 mt-0.5 leading-relaxed rounded-full text-nowrap text-ellipsis overflow-hidden group-hover:overflow-visible group-hover:bg-gray-50 dark:group-hover:bg-gray-700 border-gray-100 dark:border-gray-900 group-hover:border transition-all px-1" 120 120 > 121 121 {event.nsid} 122 122 </div> ··· 136 136 </div> 137 137 </div> 138 138 139 - <style> 139 + <style lang="postcss"> 140 140 .has-activity { 141 141 position: relative; 142 142 transition: all 0.2s ease-out; 143 143 } 144 144 145 145 .has-activity::before { 146 + @reference "../../app.css"; 147 + @apply border-blue-500 dark:border-blue-800; 146 148 content: ""; 147 149 position: absolute; 148 150 top: calc(-1 * var(--border-thickness)); 149 151 left: calc(-1 * var(--border-thickness)); 150 152 right: calc(-1 * var(--border-thickness)); 151 153 bottom: calc(-1 * var(--border-thickness)); 152 - border: var(--border-thickness) solid rgba(59, 130, 246, 0.8); 154 + border-width: var(--border-thickness); 155 + border-style: solid; 153 156 border-radius: calc(0.5rem + var(--border-thickness)); 154 157 pointer-events: none; 155 158 transition: all 0.3s ease-out;
+5 -3
client/src/lib/components/FilterControls.svelte
··· 8 8 </script> 9 9 10 10 <div 11 - class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-blue-100 hover:bg-blue-200 border-blue-300" 11 + class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-blue-100 dark:bg-blue-900 hover:bg-blue-200 dark:hover:bg-blue-800 border-blue-300 dark:border-blue-700" 12 12 > 13 - <label for="filter-regex" class="text-blue-800 mr-1"> filter: </label> 13 + <label for="filter-regex" class="text-blue-800 dark:text-gray-200 mr-1"> 14 + filter: 15 + </label> 14 16 <input 15 17 id="filter-regex" 16 18 value={filterRegex} 17 19 oninput={(e) => onFilterChange((e.target as HTMLInputElement).value)} 18 20 type="text" 19 21 placeholder="regex..." 20 - class="bg-blue-50 text-blue-900 placeholder-blue-400 border border-blue-200 rounded-full px-1 outline-none focus:bg-white focus:border-blue-400 min-w-0 w-24" 22 + class="bg-blue-50 dark:bg-blue-950 text-blue-900 dark:text-gray-400 placeholder-blue-400 dark:placeholder-blue-700 border border-blue-200 dark:border-blue-700 rounded-full px-1 outline-none focus:border-blue-400 min-w-0 w-24" 21 23 /> 22 24 </div>
+6 -4
client/src/lib/components/RefreshControl.svelte
··· 8 8 </script> 9 9 10 10 <div 11 - class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-green-100 hover:bg-green-200 border-green-300" 11 + class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-lime-100 dark:bg-lime-900 dark:hover:bg-lime-800 hover:bg-lime-200 border-lime-300 dark:border-lime-700" 12 12 > 13 - <label for="refresh-rate" class="text-green-800 mr-1">refresh:</label> 13 + <label for="refresh-rate" class="text-lime-800 dark:text-lime-200 mr-1" 14 + >refresh:</label 15 + > 14 16 <input 15 17 id="refresh-rate" 16 18 value={refreshRate} ··· 24 26 pattern="[0-9]*" 25 27 min="0" 26 28 placeholder="real-time" 27 - class="bg-green-50 text-green-900 placeholder-green-400 border border-green-200 rounded-full px-1 outline-none focus:bg-white focus:border-green-400 min-w-0 w-20" 29 + class="bg-green-50 dark:bg-green-900 text-lime-900 dark:text-lime-200 placeholder-lime-600 dark:placeholder-lime-400 border border-lime-200 dark:border-lime-700 rounded-full px-1 outline-none focus:border-lime-400 min-w-0 w-20" 28 30 /> 29 - <span class="text-green-700">s</span> 31 + <span class="text-lime-800 dark:text-lime-200">s</span> 30 32 </div>
+31
client/src/lib/components/ShowControls.svelte
··· 1 + <script lang="ts"> 2 + import type { ShowOption } from "$lib/types"; 3 + 4 + interface Props { 5 + show: ShowOption; 6 + onShowChange: (value: ShowOption) => void; 7 + } 8 + 9 + let { show, onShowChange }: Props = $props(); 10 + 11 + const showOptions: ShowOption[] = ["server init", "stream start"]; 12 + </script> 13 + 14 + <div 15 + class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-pink-100 dark:bg-pink-800 hover:bg-pink-200 dark:hover:bg-pink-700 border-pink-300 dark:border-pink-700" 16 + > 17 + <label for="show" class="text-pink-800 dark:text-pink-100 mr-1"> 18 + show since: 19 + </label> 20 + <select 21 + id="show" 22 + value={show} 23 + onchange={(e) => 24 + onShowChange((e.target as HTMLSelectElement).value as ShowOption)} 25 + class="bg-pink-50 dark:bg-pink-900 text-pink-900 dark:text-pink-100 border border-pink-200 dark:border-pink-700 rounded-full px-1 outline-none focus:border-pink-400 min-w-0" 26 + > 27 + {#each showOptions as option} 28 + <option value={option}>{option}</option> 29 + {/each} 30 + </select> 31 + </div>
+5 -3
client/src/lib/components/SortControls.svelte
··· 17 17 </script> 18 18 19 19 <div 20 - class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-purple-100 hover:bg-purple-200 border-purple-300" 20 + class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-purple-100 dark:bg-purple-800 hover:bg-purple-200 dark:hover:bg-purple-700 border-purple-300 dark:border-purple-700" 21 21 > 22 - <label for="sort-by" class="text-purple-800 mr-1"> sort by: </label> 22 + <label for="sort-by" class="text-purple-800 dark:text-purple-300 mr-1"> 23 + sort by: 24 + </label> 23 25 <select 24 26 id="sort-by" 25 27 value={sortBy} 26 28 onchange={(e) => 27 29 onSortChange((e.target as HTMLSelectElement).value as SortOption)} 28 - class="bg-purple-50 text-purple-900 border border-purple-200 rounded-full px-1 outline-none focus:bg-white focus:border-purple-400 min-w-0" 30 + class="bg-purple-50 dark:bg-purple-900 text-purple-900 dark:text-purple-300 border border-purple-200 dark:border-purple-700 rounded-full px-1 outline-none focus:border-purple-400 min-w-0" 29 31 > 30 32 {#each sortOptions as option} 31 33 <option value={option.value}>{option.label}</option>
+16 -16
client/src/lib/components/StatsCard.svelte
··· 3 3 4 4 const colorClasses = { 5 5 green: { 6 - bg: "from-green-50 to-green-100", 7 - border: "border-green-200", 8 - titleText: "text-green-700", 9 - valueText: "text-green-900", 6 + bg: "from-green-50 to-green-100 dark:from-green-900 dark:to-green-800", 7 + border: "border-green-200 dark:border-green-800", 8 + titleText: "text-green-700 dark:text-green-400", 9 + valueText: "text-green-900 dark:text-green-200", 10 10 }, 11 11 red: { 12 - bg: "from-red-50 to-red-100", 13 - border: "border-red-200", 14 - titleText: "text-red-700", 15 - valueText: "text-red-900", 12 + bg: "from-red-50 to-red-100 dark:from-red-900 dark:to-red-800", 13 + border: "border-red-200 dark:border-red-800", 14 + titleText: "text-red-700 dark:text-red-400", 15 + valueText: "text-red-900 dark:text-red-200", 16 16 }, 17 17 turqoise: { 18 - bg: "from-teal-50 to-teal-100", 19 - border: "border-teal-200", 20 - titleText: "text-teal-700", 21 - valueText: "text-teal-900", 18 + bg: "from-teal-50 to-teal-100 dark:from-teal-900 dark:to-teal-800", 19 + border: "border-teal-200 dark:border-teal-800", 20 + titleText: "text-teal-700 dark:text-teal-400", 21 + valueText: "text-teal-900 dark:text-teal-200", 22 22 }, 23 23 orange: { 24 - bg: "from-orange-50 to-orange-100", 25 - border: "border-orange-200", 26 - titleText: "text-orange-700", 27 - valueText: "text-orange-900", 24 + bg: "from-orange-50 to-orange-100 dark:from-orange-900 dark:to-orange-800", 25 + border: "border-orange-200 dark:border-orange-800", 26 + titleText: "text-orange-700 dark:text-orange-400", 27 + valueText: "text-orange-900 dark:text-orange-200", 28 28 }, 29 29 }; 30 30
+9 -5
client/src/lib/components/StatusBadge.svelte
··· 8 8 const statusConfig = { 9 9 connected: { 10 10 text: "stream live", 11 - classes: "bg-green-100 text-green-800 border-green-200", 11 + classes: 12 + "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 border-green-200 dark:border-green-800", 12 13 }, 13 14 connecting: { 14 15 text: "stream connecting", 15 - classes: "bg-yellow-100 text-yellow-800 border-yellow-200", 16 + classes: 17 + "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 border-yellow-200 dark:border-yellow-800", 16 18 }, 17 19 error: { 18 20 text: "stream errored", 19 - classes: "bg-red-100 text-red-800 border-red-200", 21 + classes: 22 + "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 border-red-200 dark:border-red-800", 20 23 }, 21 24 disconnected: { 22 25 text: "stream offline", 23 - classes: "bg-gray-100 text-gray-800 border-gray-200", 26 + classes: 27 + "bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200 border-gray-200 dark:border-gray-800", 24 28 }, 25 29 }; 26 30 ··· 31 35 <!-- connecting spinner --> 32 36 {#if status === "connecting"} 33 37 <div 34 - class="animate-spin rounded-full h-4 w-4 border-b-2 border-yellow-800" 38 + class="animate-spin rounded-full h-4 w-4 border-b-2 border-yellow-800 dark:border-yellow-200" 35 39 ></div> 36 40 {/if} 37 41 <!-- status text -->
+1
client/src/lib/types.ts
··· 18 18 }; 19 19 20 20 export type SortOption = "total" | "created" | "deleted" | "date"; 21 + export type ShowOption = "server init" | "stream start";
+79 -26
client/src/routes/+page.svelte
··· 4 4 EventRecord, 5 5 Events, 6 6 NsidCount, 7 + ShowOption, 7 8 Since, 8 9 SortOption, 9 10 } from "$lib/types"; 10 11 import { onMount, onDestroy } from "svelte"; 11 - import { writable } from "svelte/store"; 12 + import { get, writable } from "svelte/store"; 12 13 import { PUBLIC_API_URL } from "$env/static/public"; 13 14 import { fetchEvents, fetchTrackingSince } from "$lib/api"; 14 15 import { createRegexFilter } from "$lib/filter"; ··· 20 21 import BskyToggle from "$lib/components/BskyToggle.svelte"; 21 22 import RefreshControl from "$lib/components/RefreshControl.svelte"; 22 23 import { formatTimestamp } from "$lib/format"; 24 + import ShowControls from "$lib/components/ShowControls.svelte"; 23 25 24 26 type Props = { 25 27 data: { events: Events; trackingSince: Since }; ··· 29 31 30 32 const events = writable( 31 33 new Map<string, EventRecord>(Object.entries(data.events.events)), 34 + ); 35 + const eventsStart = new Map<string, EventRecord>( 36 + Object.entries(data.events.events), 32 37 ); 33 38 const pendingUpdates = new Map<string, EventRecord>(); 34 - let eventsList: NsidCount[] = $state([]); 39 + 35 40 let updateTimer: NodeJS.Timeout | null = null; 36 - events.subscribe((value) => { 37 - eventsList = value 38 - .entries() 39 - .map(([nsid, event]) => ({ 40 - nsid, 41 - ...event, 42 - })) 43 - .toArray(); 44 - }); 45 41 let per_second = $state(data.events.per_second); 46 42 let tracking_since = $state(data.trackingSince.since); 47 43 44 + const diffEvents = ( 45 + oldEvents: Map<string, EventRecord>, 46 + newEvents: Map<string, EventRecord>, 47 + ): NsidCount[] => { 48 + const nsidCounts: NsidCount[] = []; 49 + for (const [nsid, event] of newEvents.entries()) { 50 + const oldEvent = oldEvents.get(nsid); 51 + if (oldEvent) { 52 + const counts = { 53 + nsid, 54 + count: event.count - oldEvent.count, 55 + deleted_count: event.deleted_count - oldEvent.deleted_count, 56 + last_seen: event.last_seen, 57 + }; 58 + if (counts.count > 0 || counts.deleted_count > 0) 59 + nsidCounts.push(counts); 60 + } else { 61 + nsidCounts.push({ 62 + nsid, 63 + ...event, 64 + }); 65 + } 66 + } 67 + return nsidCounts; 68 + }; 48 69 const applyEvents = (newEvents: Record<string, EventRecord>) => { 49 70 events.update((map) => { 50 71 for (const [nsid, event] of Object.entries(newEvents)) { ··· 54 75 }); 55 76 }; 56 77 78 + let error: string | null = $state(null); 79 + let filterRegex = $state(""); 80 + let dontShowBsky = $state(false); 81 + let sortBy: SortOption = $state("total"); 82 + let refreshRate = $state(""); 83 + let changedByUser = $state(false); 84 + let show: ShowOption = $state("server init"); 85 + let eventsList: NsidCount[] = $state([]); 86 + let updateEventsList = $derived((value: Map<string, EventRecord>) => { 87 + switch (show) { 88 + case "server init": 89 + eventsList = value 90 + .entries() 91 + .map(([nsid, event]) => ({ 92 + nsid, 93 + ...event, 94 + })) 95 + .toArray(); 96 + break; 97 + case "stream start": 98 + eventsList = diffEvents(eventsStart, value); 99 + break; 100 + } 101 + }); 102 + events.subscribe((value) => updateEventsList(value)); 57 103 let all: EventRecord = $derived( 58 104 eventsList.reduce( 59 105 (acc, event) => { ··· 73 119 }, 74 120 ), 75 121 ); 76 - let error: string | null = $state(null); 77 - let filterRegex = $state(""); 78 - let dontShowBsky = $state(false); 79 - let sortBy: SortOption = $state("total"); 80 - let refreshRate = $state(""); 81 - let changedByUser = $state(false); 82 122 83 123 let websocket: WebSocket | null = null; 84 124 let isStreamOpen = $state(false); ··· 227 267 /> 228 268 </svelte:head> 229 269 230 - <header class="border-gray-300 border-b mb-4 pb-2"> 270 + <header 271 + class="bg-white dark:bg-gray-900 border-gray-300 dark:border-gray-950 border-b mb-4 pb-2" 272 + > 231 273 <div 232 274 class="px-2 md:ml-[19vw] mx-auto flex flex-wrap items-center text-center" 233 275 > 234 - <h1 class="text-4xl font-bold mr-4 text-gray-900">lexicon tracker</h1> 235 - <p class="text-lg mt-1 text-gray-600"> 276 + <h1 class="text-4xl font-bold mr-4 text-gray-900 dark:text-gray-200"> 277 + lexicon tracker 278 + </h1> 279 + <p class="text-lg mt-1 text-gray-600 dark:text-gray-300"> 236 280 tracks lexicons seen on the jetstream {tracking_since === 0 237 281 ? "" 238 282 : `(since: ${formatTimestamp(tracking_since)})`} 239 283 </p> 240 284 </div> 241 285 </header> 242 - <div class="md:max-w-[61vw] mx-auto p-2"> 286 + <div class="bg-white dark:bg-gray-900 md:max-w-[61vw] mx-auto p-2"> 243 287 <div class="min-w-fit grid grid-cols-2 xl:grid-cols-4 gap-2 2xl:gap-6 mb-8"> 244 288 <StatsCard 245 289 title="total creation" ··· 265 309 266 310 {#if error} 267 311 <div 268 - class="bg-red-100 border border-red-300 text-red-700 px-4 py-3 rounded-lg mb-6" 312 + class="bg-red-100 dark:bg-red-900 border border-red-300 dark:border-red-700 text-red-700 dark:text-red-200 px-4 py-3 rounded-lg mb-6" 269 313 > 270 314 <p>Error: {error}</p> 271 315 </div> ··· 274 318 {#if eventsList.length > 0} 275 319 <div class="mb-8"> 276 320 <div class="flex flex-wrap items-center gap-3 mb-3"> 277 - <h2 class="text-2xl font-bold text-gray-900">seen lexicons</h2> 321 + <h2 class="text-2xl font-bold text-gray-900 dark:text-gray-200"> 322 + seen lexicons 323 + </h2> 278 324 <StatusBadge status={websocketStatus} /> 279 325 </div> 280 326 <div class="flex flex-wrap items-center gap-1.5 mb-6"> ··· 296 342 refreshRate = ""; 297 343 }} 298 344 /> 345 + <ShowControls 346 + {show} 347 + onShowChange={(value: ShowOption) => { 348 + show = value; 349 + updateEventsList(get(events)); 350 + }} 351 + /> 299 352 <RefreshControl 300 353 {refreshRate} 301 354 onRefreshChange={(value) => { ··· 320 373 {/if} 321 374 </div> 322 375 323 - <footer class="py-2 border-t border-gray-200 text-center"> 324 - <p class="text-gray-600 text-sm"> 376 + <footer class="py-2 border-t border-gray-200 dark:border-gray-800 text-center"> 377 + <p class="text-gray-600 dark:text-gray-200 text-sm"> 325 378 source code <a 326 379 href="https://tangled.sh/@poor.dog/nsid-tracker" 327 380 target="_blank" 328 381 rel="noopener noreferrer" 329 - class="text-blue-600 hover:text-blue-800 underline" 382 + class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-600 underline" 330 383 >@poor.dog/nsid-tracker</a 331 384 > 332 385 </p>
+2 -2
server/src/main.rs
··· 71 71 .expect("cant install rustls crypto provider"); 72 72 73 73 let urls = [ 74 + "wss://jetstream1.us-west.bsky.network/subscribe", 75 + "wss://jetstream2.us-west.bsky.network/subscribe", 74 76 "wss://jetstream2.fr.hose.cam/subscribe", 75 77 "wss://jetstream.fire.hose.cam/subscribe", 76 - "wss://jetstream1.us-west.bsky.network/subscribe", 77 - "wss://jetstream2.us-west.bsky.network/subscribe", 78 78 ]; 79 79 let mut jetstream = match JetstreamClient::new(urls) { 80 80 Ok(client) => client,