audio streaming app plyr.fm
38
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: moderation admin UI polish (#398)

- add favicon (shield emoji)
- fix flags list to load automatically on page load with saved token
- fix refresh button indicator styling (hide when not loading)
- add environment badges (dev/stg) for non-production namespace flags

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

Co-authored-by: Claude <noreply@anthropic.com>

authored by

nate nowack
Claude
and committed by
GitHub
8fe46e2a a2f9f52c

+62 -8
+31
moderation/src/admin.rs
··· 198 198 cards.join("\n") 199 199 } 200 200 201 + /// Extract namespace from AT URI (e.g., "fm.plyr.dev" from "at://did:plc:xxx/fm.plyr.dev.track/yyy") 202 + fn extract_namespace(uri: &str) -> Option<&str> { 203 + // URI format: at://did:plc:xxx/fm.plyr[.env].track/rkey 204 + let collection = uri.split('/').nth(3)?; 205 + // Strip ".track" suffix to get namespace 206 + collection.strip_suffix(".track") 207 + } 208 + 209 + /// Determine environment from namespace 210 + fn namespace_to_env(namespace: &str) -> Option<(&'static str, &'static str)> { 211 + match namespace { 212 + "fm.plyr" => None, // production - no badge needed 213 + "fm.plyr.stg" => Some(("staging", "stg")), 214 + "fm.plyr.dev" => Some(("development", "dev")), 215 + _ => Some(("unknown", "?")), 216 + } 217 + } 218 + 201 219 /// Render a single flag card as HTML. 202 220 fn render_flag_card(track: &FlaggedTrack) -> String { 203 221 let ctx = track.context.as_ref(); ··· 214 232 } else { 215 233 r#"<div class="no-context">no track info available</div>"#.to_string() 216 234 }; 235 + 236 + // Add environment badge for non-production namespaces 237 + let env_badge = extract_namespace(&track.uri) 238 + .and_then(namespace_to_env) 239 + .map(|(label, short)| { 240 + format!( 241 + r#"<span class="badge env" title="{}">{}</span>"#, 242 + label, short 243 + ) 244 + }) 245 + .unwrap_or_default(); 217 246 218 247 let score_badge = ctx 219 248 .and_then(|c| c.highest_score) ··· 282 311 <div class="flag-badges"> 283 312 {} 284 313 {} 314 + {} 285 315 </div> 286 316 </div> 287 317 {} ··· 292 322 resolved_class, 293 323 track_info, 294 324 html_escape(&track.uri), 325 + env_badge, 295 326 score_badge, 296 327 status_badge, 297 328 matches_html,
+16
moderation/static/admin.css
··· 210 210 color: var(--error); 211 211 } 212 212 213 + .badge.env { 214 + background: rgba(139, 92, 246, 0.15); 215 + color: #a78bfa; 216 + text-transform: uppercase; 217 + letter-spacing: 0.5px; 218 + } 219 + 213 220 /* matches section */ 214 221 .matches { 215 222 background: var(--bg-primary); ··· 314 321 } 315 322 .htmx-request.htmx-indicator { 316 323 opacity: 1; 324 + } 325 + 326 + /* refresh button with inline indicator */ 327 + .btn-secondary .htmx-indicator { 328 + display: none; 329 + margin-right: 6px; 330 + } 331 + .btn-secondary.htmx-request .htmx-indicator { 332 + display: inline; 317 333 } 318 334 319 335 /* mobile */
+1
moderation/static/admin.html
··· 4 4 <meta charset="utf-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1"> 6 6 <title>moderation · plyr.fm</title> 7 + <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🛡️</text></svg>"> 7 8 <script src="https://unpkg.com/htmx.org@1.9.10"></script> 8 9 <link rel="stylesheet" href="/static/admin.css"> 9 10 </head>
+14 -8
moderation/static/admin.js
··· 1 + // Set up auth header listener first (before any htmx requests) 2 + let currentToken = null; 3 + 4 + document.body.addEventListener('htmx:configRequest', function(evt) { 5 + if (currentToken) { 6 + evt.detail.headers['X-Moderation-Key'] = currentToken; 7 + } 8 + }); 9 + 1 10 // Check for saved token on load 2 11 const savedToken = localStorage.getItem('mod_token'); 3 12 if (savedToken) { 4 13 document.getElementById('auth-token').value = '••••••••'; 14 + currentToken = savedToken; 5 15 showMain(); 6 - setAuthHeader(savedToken); 16 + // Trigger load after DOM is ready and htmx is initialized 17 + setTimeout(() => htmx.trigger('#flags-list', 'load'), 0); 7 18 } 8 19 9 20 function authenticate() { 10 21 const token = document.getElementById('auth-token').value; 11 22 if (token && token !== '••••••••') { 12 23 localStorage.setItem('mod_token', token); 13 - setAuthHeader(token); 24 + currentToken = token; 14 25 showMain(); 15 26 htmx.trigger('#flags-list', 'load'); 16 27 } ··· 20 31 document.getElementById('main-content').style.display = 'block'; 21 32 } 22 33 23 - function setAuthHeader(token) { 24 - document.body.addEventListener('htmx:configRequest', function(evt) { 25 - evt.detail.headers['X-Moderation-Key'] = token; 26 - }); 27 - } 28 - 29 34 // Handle auth errors 30 35 document.body.addEventListener('htmx:responseError', function(evt) { 31 36 if (evt.detail.xhr.status === 401) { 32 37 localStorage.removeItem('mod_token'); 38 + currentToken = null; 33 39 showToast('invalid token', 'error'); 34 40 } 35 41 });