What is happening on the site, newest first: the jev panel's activity
tab (website/src/jev/ActivityTab.jsx), from GET /jev/data/activity
(data.ts). Every source is a bounded query, newest first, from before a
cursor; the merge takes the newest limit of them all and answers the
cursor for the next page (next, the oldest item's time), so paging
never skips or repeats more than items sharing one millisecond.
revision a listener published a song or its next revision, with Jev's verdict as it stands (pending, fine, spam, abuse) scored the art critic scored a fine revision comment a comment on a song, with its verdict pitch a song idea, with its verdict take someone played a song as Jev arranged it (a stored performance): who (null when signed out), the song, how many sections it played and how it ended, and the take's id, for its replay link
Held and pending text is not in the feed: an item says Jev held it and why, and the data tab has its text; the feed carries the body of fine comments and pitches only.
24type Author = { id: string; name: string }; 25export type Activity = 26 | { kind: 'revision'; at: number; song: string; rev: number; title: string; author: Author; screen: string; art: number | null } 27 | { kind: 'scored'; at: number; song: string; rev: number; title: string; author: Author; art: number } 28 | { 29 kind: 'comment'; 30 at: number; 31 id: string; 32 author: Author; 33 on: { site: string } | { listener: string; title: string | null }; 34 screen: string; 35 body: string | null; 36 } 37 | { kind: 'pitch'; at: number; id: string; author: Author; screen: string; body: string | null } 38 | { 39 kind: 'take'; 40 at: number; 41 id: string; 42 song: string; 43 // a listener song's newest title (a site song's the page has) 44 title: string | null; 45 player: Author | null; 46 sections: number; 47 ended: string; 48 }; 49 50type Row = Record<string, unknown>; 51const author = (r: Row): Author => ({ id: r.author_id as string, name: r.author as string });
The newest limit of every source, before before.
62export async function activity(db: D1Database, before: number, limit: number) { 63 const [revisions, scored, comments, pitches, played] = (await db.batch([ 64 db 65 .prepare( 66 `SELECT r.song, r.rev, r.title, r.created_at AS at, r.screen, r.art, u.id AS author_id, u.display_name AS author 67 FROM song_revisions r JOIN listener_songs s ON s.id = r.song JOIN users u ON u.id = s.user_id 68 WHERE r.created_at < ? ORDER BY r.created_at DESC LIMIT ?`, 69 ) 70 .bind(before, limit), 71 db 72 .prepare( 73 `SELECT r.song, r.rev, r.title, r.scored_at AS at, r.art, u.id AS author_id, u.display_name AS author 74 FROM song_revisions r JOIN listener_songs s ON s.id = r.song JOIN users u ON u.id = s.user_id 75 WHERE r.scored_at IS NOT NULL AND r.scored_at < ? ORDER BY r.scored_at DESC LIMIT ?`, 76 ) 77 .bind(before, limit), 78 db 79 .prepare( 80 `SELECT c.id, c.site_song, c.listener_song, c.body, c.created_at AS at, c.screen, 81 u.id AS author_id, u.display_name AS author, 82 (SELECT title FROM song_revisions r WHERE r.song = c.listener_song AND r.screen = 'fine' 83 ORDER BY r.rev DESC LIMIT 1) AS listener_title 84 FROM comments c JOIN users u ON u.id = c.user_id 85 WHERE c.created_at < ? ORDER BY c.created_at DESC LIMIT ?`, 86 ) 87 .bind(before, limit), 88 db 89 .prepare( 90 `SELECT p.id, p.body, p.created_at AS at, p.screen, u.id AS author_id, u.display_name AS author 91 FROM pitches p JOIN users u ON u.id = p.user_id 92 WHERE p.created_at < ? ORDER BY p.created_at DESC LIMIT ?`, 93 ) 94 .bind(before, limit), 95 db 96 .prepare( 97 `SELECT p.id, p.song, p.recorded_at AS at, p.ended, u.id AS author_id, u.display_name AS author, 98 (SELECT count(*) FROM performance_segments s WHERE s.performance = p.id AND s.jev = coalesce(p.form_jev, 0)) AS sections, 99 (SELECT r.title FROM song_revisions r WHERE p.song = 'listener:' || r.song AND r.screen = 'fine' ORDER BY r.rev DESC LIMIT 1) AS title 100 FROM performances p LEFT JOIN users u ON u.id = p.user_id 101 WHERE p.recorded_at < ? ORDER BY p.recorded_at DESC LIMIT ?`, 102 ) 103 .bind(before, limit), 104 ])) as D1Result<Row>[]; 105 106 const sources: Activity[][] = [ 107 revisions.results.map((r) => ({ 108 kind: 'revision' as const, 109 at: r.at as number, 110 song: r.song as string, 111 rev: r.rev as number, 112 title: r.title as string, 113 author: author(r), 114 screen: r.screen as string, 115 art: (r.art as number | null) ?? null, 116 })), 117 scored.results.map((r) => ({ 118 kind: 'scored' as const, 119 at: r.at as number, 120 song: r.song as string, 121 rev: r.rev as number, 122 title: r.title as string, 123 author: author(r), 124 art: r.art as number, 125 })), 126 comments.results.map((r) => ({ 127 kind: 'comment' as const, 128 at: r.at as number, 129 id: r.id as string, 130 author: author(r), 131 on: r.site_song 132 ? { site: r.site_song as string } 133 : { listener: r.listener_song as string, title: (r.listener_title as string | null) ?? null }, 134 screen: r.screen as string, 135 body: r.screen === 'fine' ? (r.body as string) : null, 136 })), 137 pitches.results.map((r) => ({ 138 kind: 'pitch' as const, 139 at: r.at as number, 140 id: r.id as string, 141 author: author(r), 142 screen: r.screen as string, 143 body: r.screen === 'fine' ? (r.body as string) : null, 144 })), 145 played.results.map((r) => ({ 146 kind: 'take' as const, 147 at: r.at as number, 148 id: r.id as string, 149 song: r.song as string, 150 title: (r.title as string | null) ?? null, 151 player: typeof r.author_id === 'string' ? author(r) : null, 152 sections: r.sections as number, 153 ended: r.ended as string, 154 })), 155 ]; 156 return merge(sources, limit); 157}