at main 5.1 kB view raw
1// Set up auth header listener first (before any htmx requests) 2let currentToken = null; 3let currentFilter = 'pending'; // track current filter state 4 5document.body.addEventListener('htmx:configRequest', function(evt) { 6 if (currentToken) { 7 evt.detail.headers['X-Moderation-Key'] = currentToken; 8 } 9}); 10 11// Track filter changes via htmx 12document.body.addEventListener('htmx:afterRequest', function(evt) { 13 const url = evt.detail.pathInfo?.requestPath || ''; 14 const match = url.match(/filter=(\w+)/); 15 if (match) { 16 currentFilter = match[1]; 17 } 18}); 19 20function showMain() { 21 document.getElementById('main-content').style.display = 'block'; 22} 23 24function authenticate() { 25 const token = document.getElementById('auth-token').value; 26 if (token && token !== '••••••••') { 27 localStorage.setItem('mod_token', token); 28 currentToken = token; 29 showMain(); 30 htmx.trigger('#flags-list', 'load'); 31 } 32} 33 34// Check for saved token on load 35const savedToken = localStorage.getItem('mod_token'); 36if (savedToken) { 37 document.getElementById('auth-token').value = '••••••••'; 38 currentToken = savedToken; 39 showMain(); 40 // Trigger load after DOM is ready and htmx is initialized 41 setTimeout(() => htmx.trigger('#flags-list', 'load'), 0); 42} 43 44// Handle auth errors 45document.body.addEventListener('htmx:responseError', function(evt) { 46 if (evt.detail.xhr.status === 401) { 47 localStorage.removeItem('mod_token'); 48 currentToken = null; 49 showToast('invalid token', 'error'); 50 } 51}); 52 53function showToast(message, type) { 54 const toast = document.getElementById('toast'); 55 toast.className = 'toast ' + type; 56 toast.textContent = message; 57 toast.style.display = 'block'; 58 setTimeout(() => { toast.style.display = 'none'; }, 3000); 59} 60 61// Reason options for false positive resolution 62const REASONS = [ 63 { value: 'original_artist', label: 'original artist' }, 64 { value: 'licensed', label: 'licensed' }, 65 { value: 'fingerprint_noise', label: 'fp noise' }, 66 { value: 'cover_version', label: 'cover/remix' }, 67 { value: 'other', label: 'other' } 68]; 69 70// Step 1 -> Step 2: Show reason selection buttons 71function showReasonSelect(btn) { 72 const flow = btn.closest('.resolve-flow'); 73 74 // Replace button with reason selection 75 flow.innerHTML = ` 76 <div class="reason-select"> 77 ${REASONS.map(r => ` 78 <button type="button" class="reason-btn" data-reason="${r.value}" onclick="selectReason(this, '${r.value}')"> 79 ${r.label} 80 </button> 81 `).join('')} 82 <button type="button" class="reason-btn cancel" onclick="cancelResolve(this)">✕</button> 83 </div> 84 `; 85} 86 87// Step 2 -> Step 3: Show confirmation 88function selectReason(btn, reason) { 89 const flow = btn.closest('.resolve-flow'); 90 const reasonLabel = REASONS.find(r => r.value === reason)?.label || reason; 91 92 // Replace with confirmation 93 flow.innerHTML = ` 94 <div class="confirm-step"> 95 <span class="confirm-text">resolve as <strong>${reasonLabel}</strong>?</span> 96 <button type="button" class="btn btn-confirm" onclick="confirmResolve(this, '${reason}')">confirm</button> 97 <button type="button" class="reason-btn cancel" onclick="cancelResolve(this)">cancel</button> 98 </div> 99 `; 100} 101 102// Step 3: Actually submit the resolution 103function confirmResolve(btn, reason) { 104 const flow = btn.closest('.resolve-flow'); 105 const uri = flow.dataset.uri; 106 const val = flow.dataset.val; 107 108 // Show loading state 109 btn.disabled = true; 110 btn.textContent = '...'; 111 112 // Submit via fetch (URLSearchParams for application/x-www-form-urlencoded) 113 const params = new URLSearchParams(); 114 params.append('uri', uri); 115 params.append('val', val); 116 params.append('reason', reason); 117 118 fetch('/admin/resolve-htmx', { 119 method: 'POST', 120 headers: { 121 'X-Moderation-Key': currentToken, 122 'Content-Type': 'application/x-www-form-urlencoded' 123 }, 124 body: params 125 }) 126 .then(response => { 127 if (response.ok) { 128 return response.text(); 129 } 130 throw new Error('Failed to resolve'); 131 }) 132 .then(html => { 133 // Parse and show toast from response 134 const match = html.match(/resolved: ([^<]+)/); 135 if (match) { 136 showToast(match[0], 'success'); 137 } 138 // Refresh flags list with current filter 139 refreshFlagsList(); 140 }) 141 .catch(err => { 142 showToast('failed to resolve: ' + err.message, 'error'); 143 cancelResolve(btn); 144 }); 145} 146 147// Refresh flags list preserving current filter 148function refreshFlagsList() { 149 htmx.ajax('GET', `/admin/flags-html?filter=${currentFilter}`, '#flags-list'); 150} 151 152// Cancel: restore original button 153function cancelResolve(btn) { 154 const flow = btn.closest('.resolve-flow'); 155 flow.innerHTML = ` 156 <button type="button" class="btn btn-warning" onclick="showReasonSelect(this)"> 157 mark false positive 158 </button> 159 `; 160}