(async () => { // Helper to get dynamic context const getContext = () => { const commentsEl = document.querySelector("#comments-container"); const favoEl = document.querySelector("#a_favo"); const infoEl = document.querySelector("#a_info"); const idLinkEl = document.querySelector("a.id-link"); const rawId = commentsEl?.dataset?.itemId || favoEl?.dataset?.itemId || infoEl?.dataset?.itemId || idLinkEl?.dataset?.itemId || idLinkEl?.innerText; if (!rawId) return null; const tagsContainer = document.querySelector("#tags"); const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null; const usernameEl = document.querySelector("a#a_username"); const currentSub = (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') ? window.albumGallery.getCurrentSubf0ck() : null; const subf0ck_id = currentSub ? (currentSub.slug || currentSub.id) : (tagsContainer?.dataset?.subf0ckSlug || tagsContainer?.dataset?.subf0ckId || null); return { postid: /^\d+$/.test(String(rawId).trim()) ? parseInt(rawId, 10) : rawId.trim(), subf0ck_id, // data-username holds the raw DB username; data-author-id holds the user's numeric ID. // Never fall back to innerText — it may be a display name or the literal string 'unknown'. poster: (usernameEl?.dataset?.username || '').trim() || null, authorId: (usernameEl?.dataset?.authorId || '').trim() || null, tags: inner ? [...inner.querySelectorAll(".badge")].map(t => t.innerText.slice(0, -2)) : [] }; }; const queryapi = async (url, data, method = 'GET') => { let req; if (method == 'POST') { req = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": window.f0ckSession?.csrf_token }, body: JSON.stringify(data) }); } else { let s = []; for (const [key, val] of Object.entries(data)) s.push(encodeURIComponent(key) + "=" + encodeURIComponent(val)); req = await fetch(url + '?' + s.join('&')); } return await req.json(); }; const get = async (url, data) => queryapi(url, data, 'GET'); const post = async (url, data) => queryapi(url, data, 'POST'); const renderTags = (_tags, highlightTag = null) => { const tagsContainer = document.querySelector("#tags"); if (!tagsContainer) return; const inner = tagsContainer.querySelector(".tags-inner") || tagsContainer; const activePostId = tagsContainer.dataset.itemId || (typeof window.getCurrentItemId === 'function' ? window.getCurrentItemId() : null); const canManage = !!( document.querySelector('#tags[data-can-manage="true"]') || (window.f0ckSession && (window.f0ckSession.is_admin || window.f0ckSession.is_moderator)) || (window.f0ckSession && window.f0ckSession.user && document.querySelector('#a_username[data-username]')?.dataset?.username?.toLowerCase() === window.f0ckSession.user.toLowerCase()) ); // Only remove existing dynamically generated tags [...inner.querySelectorAll(".badge")].forEach(tag => { // Don't remove the one containing the add/toggle buttons, and don't remove the autocomplete input itself if (!tag.querySelector('#a_addtag') && !tag.querySelector('#a_toggle') && !tag.classList.contains('tag-ac-wrapper')) { tag.parentElement.removeChild(tag); } }); // Deduplicate: ensure only at most ONE rating tag exists in the tags list const ratingTags = _tags.filter(t => ['sfw', 'nsfw', 'nsfl'].includes(t.normalized)); const lastRatingTag = ratingTags.length ? ratingTags[ratingTags.length - 1] : null; const cleanTags = _tags.filter(t => !['sfw', 'nsfw', 'nsfl'].includes(t.normalized) || t === lastRatingTag); const tagsCopy = [...cleanTags]; tagsCopy.reverse().forEach(tag => { const isRating = ['sfw', 'nsfw', 'nsfl'].includes(tag.normalized); let contentEl; if (isRating) { contentEl = document.createElement("span"); contentEl.className = "rating-label"; contentEl.textContent = tag.tag; } else { contentEl = document.createElement("a"); contentEl.href = "/tag/" + tag.normalized; contentEl.style = "color: inherit !important"; contentEl.textContent = tag.tag; } const span = document.createElement("span"); span.classList.add("badge"); if (highlightTag && (tag.tag === highlightTag || tag.normalized === highlightTag)) { span.classList.add('new-tag-glow'); } tag.badge.split(" ").forEach(b => span.classList.add(b)); if (isRating) { span.classList.add('rating-tag', `is-${tag.normalized}`); span.dataset.rating = tag.normalized; if (activePostId) span.dataset.itemId = activePostId; if (canManage) { span.classList.add('can-cycle'); } } else if (!window.f0ckSession?.is_anonymized && window.f0ckSession?.logged_in && !window.f0ckSession?.is_anon && (tag.display_name || tag.user)) { span.setAttribute("tooltip", tag.display_name || tag.user); } span.appendChild(contentEl); if (!isRating && tag.can_exclude) { const excludeBtn = document.createElement("button"); excludeBtn.type = "button"; excludeBtn.className = "tag-exclude-btn"; excludeBtn.setAttribute("title", tag.is_excluded ? "Excluded (click to unexclude)" : "Exclude tag"); excludeBtn.setAttribute("aria-label", "Exclude tag"); excludeBtn.innerHTML = ``; span.appendChild(excludeBtn); } if (!isRating) { const delbutton = document.createElement("a"); delbutton.href = "#"; delbutton.classList.add("admin-deltag", "removetag"); delbutton.innerHTML = ''; span.appendChild(document.createTextNode(" ")); span.appendChild(delbutton); } inner.insertAdjacentElement("afterbegin", span); }); const hasRating = !!lastRatingTag; if (!hasRating) { const untaggedSpan = document.createElement("span"); untaggedSpan.className = `badge badge-untagged rating-tag is-untagged${canManage ? ' can-cycle' : ''}`; untaggedSpan.dataset.rating = 'untagged'; if (activePostId) untaggedSpan.dataset.itemId = activePostId; const lbl = document.createElement("span"); lbl.className = "rating-label"; lbl.textContent = "untagged"; untaggedSpan.appendChild(lbl); inner.insertAdjacentElement("afterbegin", untaggedSpan); } // Safeguard: remove any extra rating tags in DOM const allRatingEls = inner.querySelectorAll('.rating-tag, .badge-success, .badge-danger, .badge-nsfl, .badge-untagged, [data-rating]'); if (allRatingEls.length > 1) { for (let i = 1; i < allRatingEls.length; i++) { allRatingEls[i].remove(); } } // Update thumbnail data-mode in background grid (ensures div.posts > a > p::before updates in Onara) if (activePostId) { const modeAttr = lastRatingTag ? lastRatingTag.normalized : 'null'; document.querySelectorAll(`.posts > a.thumb[data-item-id="${activePostId}"], .posts a[data-item-id="${activePostId}"], .posts a[href$="/${activePostId}"]`).forEach(th => { th.setAttribute('data-mode', modeAttr); }); if (document.body.classList.contains('onara-modal-open')) { const onaraThumb = document.querySelector('.posts > a.thumb.onara-active'); if (onaraThumb) onaraThumb.setAttribute('data-mode', modeAttr); } } // Handle show more/less toggle visibility and count const allBadges = [...inner.querySelectorAll(".badge")]; const realTags = allBadges.filter(b => !b.querySelector('#a_addtag') && !b.querySelector('#a_toggle') && !b.classList.contains('tag-ac-wrapper')); let toggle = tagsContainer.querySelector(".show-tags-toggle"); if (realTags.length > 10) { if (!toggle) { toggle = document.createElement("a"); toggle.href = "#"; toggle.className = "show-tags-toggle"; tagsContainer.appendChild(toggle); } const hiddenCount = realTags.length - 10; toggle.dataset.count = hiddenCount; // Auto-expand when rendering new tags (e.g. after adding one) as requested tagsContainer.classList.add('tags-expanded'); toggle.textContent = "show less"; } else if (toggle) { toggle.remove(); tagsContainer.classList.remove('tags-expanded'); } }; window.renderTags = renderTags; const deleteEvent = async e => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); const ctx = getContext(); if (!ctx) return; const { postid, subf0ck_id } = ctx; let target = e.target; if (target.nodeType === 3) target = target.parentElement; const badge = target.closest('.badge'); if (!badge) return; const tagLink = badge.querySelector('a[href*="/tag/"], a:first-of-type'); const tagname = tagLink ? tagLink.innerText.trim() : null; if (!tagname) return; if (typeof ModAction === 'undefined') return alert('Error: ModAction module not loaded'); ModAction.confirm((window.f0ckI18n && window.f0ckI18n.tag_delete_title) || 'Delete Tag', `${(window.f0ckI18n && window.f0ckI18n.tag_delete_confirm) || 'Are you sure you want to delete the tag'} ${tagname}?`, async (reason) => { const qs = new URLSearchParams(); if (reason) qs.set('reason', reason); if (subf0ck_id) qs.set('subf0ck_id', subf0ck_id); const qsStr = qs.toString(); const res = await (await fetch("/api/v2/tags/" + postid + "/" + encodeURIComponent(tagname) + (qsStr ? "?" + qsStr : ""), { method: 'DELETE', headers: { "X-CSRF-Token": window.f0ckSession?.csrf_token } })).json(); if (!res.success) { throw new Error(res.msg || "Error deleting tag"); } if (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') { const cur = window.albumGallery.getCurrentSubf0ck(); if (cur && res.tags) cur.tags = res.tags; } renderTags(res.tags); if (window.flashMessage) window.flashMessage((window.f0ckI18n?.tag_deleted_success) || 'Tag deleted', 2500, 'success'); }, { allowEmpty: window.f0ckSession?.is_admin }); }; const addtagClick = (e) => { if (e) e.preventDefault(); const ctx = getContext(); if (!ctx) return; const { postid, subf0ck_id, tags } = ctx; const anchor = document.querySelector("a#a_addtag"); if (!anchor) return; TagAutocomplete.open({ postid, existingTags: tags, anchorEl: anchor, onSubmit: async (tag) => { const payload = { tagname: tag }; if (subf0ck_id) payload.subf0ck_id = subf0ck_id; const res = await post("/api/v2/tags/" + postid, payload); if (res.success) { if (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') { const cur = window.albumGallery.getCurrentSubf0ck(); if (cur && res.tags) cur.tags = res.tags; } if (window.invalidateItemCache) { window.invalidateItemCache(postid); } } return res; }, renderTags: (newTags, hl) => { if (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') { const cur = window.albumGallery.getCurrentSubf0ck(); if (cur) cur.tags = newTags; } renderTags(newTags, hl); } }); }; const toggleFavEvent = async (e) => { if (e && typeof e.preventDefault === 'function') e.preventDefault(); if (window.f0ckSession?.is_anon && window.f0ckSession?.anon_permissions && window.f0ckSession.anon_permissions.favorite === false) { if (typeof window.flashMessage === 'function') { window.flashMessage('Anonymous favoriting is disabled.', 3000, 'error'); } return; } const ctx = getContext(); if (!ctx) return; const { postid } = ctx; // Read state BEFORE the API call so we have a fallback const favoBtns = document.querySelectorAll("#a_favo"); const wasAlreadyFav = favoBtns.length > 0 && favoBtns[0].classList.contains('fa-solid'); try { const res = await post('/api/v2/togglefav', { postid: postid }); if (res && res.success) { if (window.invalidateItemCache) { window.invalidateItemCache(postid); } // Authoritative server state takes priority; fallback to inverting previous UI state const isNowFav = (res.favorited !== undefined) ? !!res.favorited : !wasAlreadyFav; favoBtns.forEach(favoBtn => { favoBtn.classList.toggle('fa-solid', isNowFav); favoBtn.classList.toggle('fa-regular', !isNowFav); }); const favcontainer = document.querySelector('#favs'); if (favcontainer) { favcontainer.innerHTML = ""; if (res.favs && res.favs.length > 0) { const isAnonymized = !!(window.f0ckSession?.is_anonymized ?? (window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in) ?? (window.f0ckSession?.is_anon && window.f0ckSession?.anon_anonymize)); const fragment = document.createDocumentFragment(); res.favs.forEach(f => { const isSelf = window.f0ckSession && ( (window.f0ckSession.id && f.user_id && Number(window.f0ckSession.id) === Number(f.user_id)) || (window.f0ckSession.user && f.user && window.f0ckSession.user.toLowerCase() === f.user.toLowerCase()) || (window.f0ckSession.login && f.login && window.f0ckSession.login.toLowerCase() === f.login.toLowerCase()) ); const isFavAnon = f.is_anon || f.user === 'anonymous' || (typeof f.user === 'string' && f.user.startsWith('anon_')); if (f.hide_fav_badge && !isSelf) { const a = document.createElement('a'); a.className = 'ghost-fav'; a.setAttribute('tooltip', '?'); a.setAttribute('flow', 'up'); a.style.cursor = 'default'; const img = document.createElement('img'); img.src = '/s/img/ghost_fav.svg'; img.style.height = "32px"; img.style.width = "32px"; a.appendChild(img); fragment.appendChild(a); } else if (isAnonymized || isFavAnon) { const a = document.createElement('a'); a.className = 'ghost-fav'; a.setAttribute('tooltip', 'anonymous'); a.setAttribute('flow', 'up'); a.style.cursor = 'default'; const img = document.createElement('img'); img.src = '/a/default.png'; img.style.height = "32px"; img.style.width = "32px"; a.appendChild(img); fragment.appendChild(a); } else { const a = document.createElement('a'); a.href = `/user/${(f.user || '').toLowerCase()}`; a.setAttribute('tooltip', f.display_name || f.user || 'anonymous'); a.setAttribute('flow', 'up'); const img = document.createElement('img'); img.src = f.avatar_file ? `/a/${f.avatar_file}` : (f.avatar ? `/t/${f.avatar}.webp` : '/a/default.png'); img.style.height = "32px"; img.style.width = "32px"; if (f.username_color) img.style.borderColor = f.username_color; a.appendChild(img); fragment.appendChild(a); } }); favcontainer.appendChild(fragment); favcontainer.hidden = false; } else { favcontainer.hidden = true; } } window.flashMessage((window.f0ckI18n && (isNowFav ? window.f0ckI18n.fav_added : window.f0ckI18n.fav_removed)) || (isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES')); if (navigator.vibrate) navigator.vibrate(50); } else { const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.'; if (typeof window.flashMessage === 'function') { window.flashMessage(errMsg, 3000, 'error'); } } } catch (err) { if (typeof window.flashMessage === 'function') { window.flashMessage('Failed to update favorite.', 3000, 'error'); } } }; const deleteButtonEvent = async e => { if (e) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); } const ctx = getContext(); if (!ctx) return; const { postid, poster, authorId } = ctx; if (typeof ModAction === 'undefined') return alert('Error: ModAction module not loaded'); const i18n = window.f0ckI18n || {}; const confirmTitle = i18n.item_delete_title || 'Delete Item'; const posterStr = poster ? (authorId ? `${poster} (${authorId})` : poster) : 'unknown'; const confirmMsg = (i18n.item_delete_confirm || 'Are you sure you want to delete item {id} by {user}?') .replace('{id}', postid) .replace('{user}', posterStr); ModAction.confirm(confirmTitle, confirmMsg, async (reason) => { // Flag immediately so the SSE delete_item handler skips navigation window._adminJustDeletedItem = postid; const res = await post("/api/v2/admin/deletepost", { postid: postid, reason: reason }); if (!res.success) { window._adminJustDeletedItem = null; throw new Error(res.msg || "Error deleting item"); } const mediaObj = document.querySelector('.media-object'); if (mediaObj) { mediaObj.innerHTML = '

Item Deleted

The item has been successfully removed.

'; } if (window.flashMessage) window.flashMessage((window.f0ckI18n?.item_deleted_success) || 'Item deleted', 2500, 'success'); // Clear flag after a short delay (SSE has surely arrived by then) setTimeout(() => { window._adminJustDeletedItem = null; }, 3000); }, { allowEmpty: window.f0ckSession?.is_admin }); }; let tmptt = null; const editTagEvent = async e => { e.preventDefault(); if (e.detail === 2) { // Double click clearTimeout(tmptt); const old = e.target; const parent = e.target.parentElement; const oldtag = e.target.innerText; const textfield = document.createElement('input'); textfield.value = e.target.innerText; textfield.size = 10; parent.insertAdjacentElement('afterbegin', textfield); textfield.focus(); parent.removeChild(e.target); // Hide delete button while editing const delBtn = parent.querySelector('a:last-child'); if (delBtn) delBtn.style.display = 'none'; textfield.addEventListener("keydown", async e => { if (e.key === 'Enter' || e.keyCode === 13) { parent.removeChild(textfield); let res = await fetch('/api/v2/tags/rename/' + encodeURIComponent(oldtag), { method: 'PUT', headers: { "Content-Type": "application/json", "X-CSRF-Token": window.f0ckSession?.csrf_token }, body: JSON.stringify({ newtag: textfield.value }) }); const status = res.status; res = await res.json(); switch (status) { case 200: case 201: parent.insertAdjacentElement('afterbegin', old); if (delBtn) delBtn.style.display = ''; old.href = `/tag/${res.tag}`; old.innerText = res.tag.trim(); break; default: window.f0ckDebug(res); break; } } else if (e.key === 'Escape') { parent.removeChild(textfield); parent.insertAdjacentElement('afterbegin', old); if (delBtn) delBtn.style.display = ''; } }); } else tmptt = setTimeout(() => location.href = e.target.href, 250); return false; }; // Event Delegation document.addEventListener("click", e => { const target = e.target.nodeType === 3 ? e.target.parentElement : e.target; if (target.closest("a#a_addtag")) { addtagClick(e); } else if (target.closest("#a_delete")) { deleteButtonEvent(e); } else if (target.matches('#tags .badge > a[href*="/tag/"]')) { editTagEvent(e); } else if (target.closest('.admin-deltag') || target.closest('.removetag')) { deleteEvent(e); } else if (target.closest("#a_pin")) { pinButtonEvent(e); } else if (target.closest("#a_unavailable")) { unavailableButtonEvent(e); } else if (target.closest("#a_favo")) { toggleFavEvent(e); } }); const unavailableButtonEvent = async e => { if (e) e.preventDefault(); const ctx = getContext(); if (!ctx) return; const { postid } = ctx; const unavBtn = document.querySelector('#a_unavailable'); if (!unavBtn) return; const currentVis = parseInt(unavBtn.getAttribute('data-visibility') || '0', 10); const willBeUnavailable = currentVis !== 3; const targetVis = willBeUnavailable ? 3 : 0; const actionText = willBeUnavailable ? 'Make Unavailable (serves HTTP 451 to non-logged in visitors)' : 'Make Available (Public)'; const proceed = async () => { try { const res = await (await fetch('/api/v2/item/visibility', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: new URLSearchParams({ postid, id: postid, visibility: targetVis }) })).json(); if (res.success) { const isNowUnav = res.visibility === 3; unavBtn.setAttribute('data-visibility', res.visibility); unavBtn.classList.toggle('active', isNowUnav); unavBtn.style.color = isNowUnav ? 'var(--danger, #ff4444)' : ''; unavBtn.setAttribute('title', isNowUnav ? 'Make Available (Public)' : 'Make Unavailable (451)'); const infoVisLabel = document.getElementById('info-visibility-label'); if (infoVisLabel) { infoVisLabel.innerHTML = isNowUnav ? ' Unavailable (451)' : ' Public'; } const infoVisBtn = document.getElementById('info-visibility-edit-btn'); if (infoVisBtn) infoVisBtn.dataset.visibility = res.visibility; window.flashMessage(isNowUnav ? 'ITEM MARKED UNAVAILABLE (451)' : 'ITEM RESTORED TO PUBLIC'); } else { alert('Error: ' + (res.msg || 'Failed to update visibility')); } } catch (err) { console.error('Unavailable error:', err); } }; if (typeof ModAction !== 'undefined' && ModAction.confirm) { ModAction.confirm('Item Visibility', `${actionText} for post #${postid}?`, proceed); } else if (confirm(`${actionText} for post #${postid}?`)) { proceed(); } }; const pinButtonEvent = async e => { if (e) e.preventDefault(); const ctx = getContext(); if (!ctx) return; const { postid } = ctx; const pinBtn = document.querySelector('#a_pin'); if (!pinBtn) return; const isPinned = pinBtn.getAttribute('data-pinned') === 'true'; const url = isPinned ? `/mod/unpin?id=${postid}` : `/mod/pin?id=${postid}`; try { const res = await (await fetch(url, { method: 'POST', headers: { "X-CSRF-Token": window.f0ckSession?.csrf_token } })).json(); if (res.success) { const newState = res.pinned; const title = newState ? 'Unpin from main' : 'Pin to main'; const currentBtn = document.querySelector('#a_pin'); if (currentBtn) { currentBtn.setAttribute('data-pinned', newState); currentBtn.setAttribute('title', title); currentBtn.classList.toggle('active', newState); } window.flashMessage(newState ? (window.f0ckI18n?.item_pinned || 'ITEM PINNED') : (window.f0ckI18n?.item_unpinned || 'ITEM UNPINNED')); } else { alert('Error: ' + res.msg); } } catch (err) { console.error('Pin error:', err); } }; document.addEventListener("keyup", e => { if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return; const ctx = getContext(); if (!ctx) return; // 'f' and 'i' handled by f0ckm.js keybindings via programmatic click if (e.key === "x") deleteButtonEvent(); else if (e.key === "g") pinButtonEvent(); }); window.adminSetPassword = async (btn) => { const id = btn.dataset.id; const name = btn.dataset.name; if (typeof ModAction === 'undefined') return alert('Error: ModAction module not loaded'); const hint = 'Set a new password for ' + escHTML(name) + '. Must be at least 20 characters.

' + '' + ''; ModAction.confirm('Set Password', hint, async () => { const password = document.getElementById('admin-pw-new')?.value || ''; const confirm = document.getElementById('admin-pw-confirm')?.value || ''; if (password.length < 20) throw new Error('Password must be at least 20 characters.'); if (password !== confirm) throw new Error('Passwords do not match.'); const data = await post('/api/v2/admin/users/set-password', { user_id: id, password }); if (data.success) { showFlash(data.msg, 'success'); } else { throw new Error(data.msg || 'Failed to set password'); } }, { hideReason: true, confirmText: 'Set Password', unsafeContent: true }); }; window.adminRenameUser = async (btn) => { const id = btn.dataset.id; const currentName = btn.dataset.name; const currentUsername = btn.dataset.username; if (typeof ModAction === 'undefined') return alert('Error: ModAction module not loaded'); ModAction.confirm( 'Rename User', 'Enter a new login name for ' + escHTML(currentName) + '.
' + 'Current login: ' + escHTML(currentUsername) + ' — All uploads will be reassigned. User sessions will be invalidated. And the user has to login with the NEW name from now on.', async (newUsername) => { const data = await post('/api/v2/admin/users/rename', { user_id: id, new_username: newUsername }); if (data.success) { showFlash(data.msg, 'success'); // Update the row in-place: links, text, and all button data attributes const row = document.getElementById('user-row-' + id); if (row) { // Update the name link const link = row.querySelector('.user-info-cell a'); if (link) { link.href = '/user/' + data.new_login; // Only overwrite text if there's no display_name (plain username link) if (!link.querySelector('span[style*="accent"]')) { link.textContent = data.new_user; } } // Update all buttons in the row with the new name/username row.querySelectorAll('[data-username]').forEach(el => { el.dataset.username = data.new_login; }); row.querySelectorAll('[data-name]').forEach(el => { el.dataset.name = data.new_user; }); // Update activity stat links row.querySelectorAll('a[href^="/user/"]').forEach(a => { a.href = a.href.replace(/\/user\/[^/]+/, '/user/' + data.new_login); }); } } else { throw new Error(data.msg || 'Rename failed'); } }, { hideReason: false, singleLine: true, confirmText: 'Rename', placeholder: 'new username' } ); }; window.adminDeleteUser = async (btn) => { const id = btn.dataset.id; const name = btn.dataset.name; if (typeof ModAction === 'undefined') return alert('Error: ModAction module not loaded'); ModAction.confirm( 'Delete User', 'CRITICAL ACTION: Permanently delete user ' + escHTML(name) + '?

All their uploads and comments will be reassigned to deleted_user. This cannot be undone.', async () => { const data = await post('/api/v2/admin/users/delete', { user_id: id }); if (data.success) { showFlash(data.msg, 'success'); document.getElementById(`user-row-${id}`)?.remove(); } else { throw new Error(data.msg || 'Failed to delete user'); } }, { hideReason: true, confirmText: 'Delete User' } ); }; window.adminResetLoginAttempts = async (btn) => { const username = btn.dataset.username; if (!confirm(`Are you sure you want to reset login attempts for ${username}?`)) return; try { const data = await post('/api/v2/admin/users/reset-login-attempts', { username }); if (data.success) { showFlash(data.msg, 'success'); // Remove the failed attempt badges and reset button from the row in-place const row = btn.closest('tr'); if (row) { row.querySelectorAll('.status-badge[style*="ffcc00"], .status-badge[style*="ff4d4d"]').forEach(el => el.remove()); } btn.remove(); } else { alert(data.msg || 'Failed to reset attempts'); } } catch (err) { alert('Network error'); } }; window.adminBulkDeleteHalls = async (btn) => { const id = btn.dataset.id; const name = btn.dataset.name; if (typeof ModAction === 'undefined') return alert('Error: ModAction module not loaded'); ModAction.confirm( 'Delete All Halls', 'Permanently delete ALL halls for ' + escHTML(name) + '? This cannot be undone.', async () => { const data = await post('/api/v2/admin/users/bulk-delete-halls', { user_id: id }); if (data.success) { showFlash(data.msg, 'success'); } else { throw new Error(data.msg || 'Failed to delete halls'); } }, { hideReason: true, confirmText: 'Delete Everything' } ); }; window.adminReassignUploads = async (btn) => { const id = btn.dataset.id; const name = btn.dataset.name; const username = btn.dataset.username; if (typeof ModAction === 'undefined') return alert('Error: ModAction module not loaded'); ModAction.confirm( 'Reassign Uploads', 'Enter the target username to transfer all uploads from ' + escHTML(name) + ' to:', async (targetUsername) => { const payload = { target_username: targetUsername }; if (id) { payload.source_user_id = id; } else { payload.source_username = username; } const res = await post('/api/v2/admin/users/reassign-uploads', payload); if (res.success) { showFlash(res.msg, 'success'); } else { throw new Error(res.msg || 'Reassignment failed'); } }, { hideReason: false, singleLine: true, confirmText: 'Reassign', placeholder: 'target username' } ); }; })();