diff --git a/F95_BRATR_Management_Ratings_Helper.user.js b/F95_BRATR_Management_Ratings_Helper.user.js
new file mode 100644
index 0000000..86e4279
--- /dev/null
+++ b/F95_BRATR_Management_Ratings_Helper.user.js
@@ -0,0 +1,415 @@
+// ==UserScript==
+// @name F95 BRATR Management Ratings Helper
+// @namespace Ryahn
+// @version 1.4.1
+// @description Triage panel for /bratr-ratings/management: highlight low-effort reviews, filter, export CSV.
+// @match https://f95zone.to/bratr-ratings/management*
+// @match https://f95zone.to/bratr-ratings/*/management*
+// @match https://f95zone.to/threads/*/br-reviews/*
+// @run-at document-idle
+// @grant GM_setClipboard
+// ==/UserScript==
+(function () {
+ const $ = (sel, root=document) => root.querySelector(sel);
+ const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel));
+ const text = el => (el ? el.textContent.replace(/\s+/g,' ').trim() : '');
+ const getRating = el => {
+ const rs = el.querySelector('.ratingStars.bratr-rating');
+ if (!rs) return null;
+ const t = rs.getAttribute('title') || '';
+ const m = t.match(/([\d.]+)\s*star/i);
+ if (m) return parseFloat(m[1]);
+ return rs.querySelectorAll('.ratingStars-star--full').length || null;
+ };
+ const csvEscape = s => `"${(s||'').replace(/"/g,'""')}"`;
+ const avg = arr => arr.length ? arr.reduce((a,b)=>a+b,0)/arr.length : 0;
+ const truncate = (s, n=80) => (s.length > n ? s.slice(0, n-1) + '…' : s);
+
+ // Scrape reviews on page; stash stable index i for row->DOM mapping
+ const reviews = $$('.message--review').map((msg, i) => {
+ const main = msg.querySelector('.contentRow-main');
+ const authorA = main?.querySelector('.contentRow-header a.username');
+ const author = text(authorA);
+ const rating = getRating(main);
+ const threadA = main?.querySelector('dl.pairs dd a');
+ const thread = text(threadA);
+ const threadUrl = threadA?.href || '';
+ const timeEl = main?.querySelector('time.u-dt');
+ const timeIso = timeEl?.getAttribute('datetime') || '';
+ const timeTxt = timeEl?.getAttribute('title') || text(timeEl);
+
+ // Body excluding spoiler contents
+ const bodyEl = main?.querySelector('article .bbWrapper');
+ let bodyTxt = '';
+ if (bodyEl) {
+ const clone = bodyEl.cloneNode(true);
+ clone.querySelectorAll('.bbCodeSpoiler-content').forEach(s => s.remove());
+ bodyTxt = text(clone);
+ }
+
+ const deleted = msg.classList.contains('message--deleted');
+ const links = {
+ report: main?.querySelector('a.actionBar-action--report')?.href || '',
+ edit: main?.querySelector('a.actionBar-action--edit')?.href || '',
+ del: main?.querySelector('a.actionBar-action--delete')?.href || '',
+ warn: main?.querySelector('a.actionBar-action--warn')?.href || ''
+ };
+
+ // Raw vs effective length (collapse punctuation carpets)
+ const rawLen = (bodyTxt || '').length;
+ const effBody = (bodyTxt || '')
+ .replace(/([.!?…,_\-])\1{2,}/g, '$1$1') // collapse carpets to 2
+ .replace(/\s{2,}/g, ' ')
+ .trim();
+ const effLen = effBody.length;
+
+ return { i, node: msg, author, rating, thread, threadUrl, timeIso, timeTxt, bodyTxt, rawLen, effLen, deleted, links };
+ });
+
+ // Cliché patterns with labels
+ const cliché = [
+ { label: 'good game', rx: /\bgood game\b/i },
+ { label: 'very good', rx: /\bit was very good\b/i },
+ { label: 'amazing', rx: /\bamazing!?(\b|$)/i },
+ { label: 'top N', rx: /\btop\s+\d+\b/i },
+ { label: 'pretty sex scenes', rx: /\bpretty sex scenes\b/i },
+ { label: 'downloads flex', rx: /\bdownload(ed)? hundreds of games\b/i }
+ ];
+
+ // Spam/bypass heuristics
+ function analyzeSpam(body) {
+ const reasons = [];
+ const s = body || '';
+ if (!s) return reasons;
+
+ // Punctuation carpets: 6+ of the same mark
+ const punctRuns = s.match(/([.!?…,_\-])\1{5,}/g) || [];
+ if (punctRuns.length) {
+ const totalRunChars = punctRuns.reduce((a,b)=>a+b.length,0);
+ reasons.push({ code: 'SPAM', text: `DOTCARPET ×${punctRuns.length}`, tip: `Punctuation runs total ${totalRunChars} chars` });
+ }
+
+ // Punctuation ratio
+ const punctChars = (s.match(/[^\w\s]/g) || []).length;
+ const pRatio = s.length ? punctChars / s.length : 0;
+ if (pRatio > 0.35 && s.length >= 120) {
+ reasons.push({ code: 'SPAM', text: `PUNC ${Math.round(pRatio*100)}%`, tip: `Non-alphanumeric ratio ${Math.round(pRatio*100)}%` });
+ }
+
+ // Tokenization
+ const tokens = s.toLowerCase().match(/[a-z0-9']+/g) || [];
+ const totalTok = tokens.length;
+ if (totalTok) {
+ const freq = new Map();
+ tokens.forEach(t => freq.set(t, (freq.get(t)||0)+1));
+ const uniq = freq.size;
+ const uniqRatio = uniq / totalTok;
+
+ if (uniqRatio < 0.5 && totalTok >= 10) {
+ reasons.push({ code: 'SPAM', text: `LOW_UNIQUE ${uniq}/${totalTok}`, tip: `Unique/token ratio ${uniqRatio.toFixed(2)}` });
+ }
+
+ // Single-word dominance
+ let maxWord = '', maxCount = 0;
+ for (const [w,c] of freq.entries()) if (c > maxCount) { maxCount = c; maxWord = w; }
+ if (maxCount / totalTok > 0.4 && totalTok >= 8) {
+ reasons.push({ code: 'SPAM', text: `REP "${truncate(maxWord,12)}" ×${maxCount}`, tip: `Word appears ${(maxCount/totalTok*100).toFixed(0)}% of tokens` });
+ }
+
+ // Repeated n-grams (bi/tri)
+ function ngrams(n) {
+ const m = new Map();
+ for (let i=0;i<=tokens.length-n;i++){
+ const g = tokens.slice(i,i+n).join(' ');
+ m.set(g,(m.get(g)||0)+1);
+ }
+ return m;
+ }
+ [2,3].forEach(n => {
+ const m = ngrams(n);
+ for (const [g,c] of m.entries()) {
+ if (c >= 3) {
+ reasons.push({ code: 'SPAM', text: `REP${n} "${truncate(g,20)}" ×${c}`, tip: `Repeated ${n}-gram ${c} times` });
+ break;
+ }
+ }
+ });
+
+ // Repeated very short sentences (<=4 words) 3+ times
+ const sentences = s.split(/(?<=[.!?])\s+/);
+ const shortFreq = new Map();
+ sentences.forEach(x => {
+ const toks = (x.toLowerCase().match(/[a-z0-9']+/g) || []);
+ if (toks.length && toks.length <= 4) {
+ const key = toks.join(' ');
+ shortFreq.set(key, (shortFreq.get(key)||0)+1);
+ }
+ });
+ for (const [key,c] of shortFreq.entries()) {
+ if (c >= 3) {
+ reasons.push({ code: 'SPAM', text: `SHORT_SENT "${truncate(key,18)}" ×${c}`, tip: `Short sentence repeated ${c} times` });
+ break;
+ }
+ }
+ }
+ return reasons;
+ }
+
+ // Diagnose low-effort
+ function diagnoseLowEffort(r, cutoff=220) {
+ const reasons = [];
+ const body = r.bodyTxt || '';
+
+ if (!r.deleted && (r.effLen || 0) < cutoff) {
+ reasons.push({
+ code: 'LEN',
+ text: `LEN ${r.effLen}<${cutoff}`,
+ tip: `Effective length ${r.effLen} is below cutoff ${cutoff}`
+ });
+ }
+
+ if (!r.deleted) {
+ cliché.forEach(c => {
+ const m = body.match(c.rx);
+ if (m) reasons.push({ code: 'CLICHÉ', text: `CLICHÉ: "${truncate(m[0], 28)}"`, tip: `Matched phrase: ${m[0]}` });
+ });
+ }
+
+ analyzeSpam(body).forEach(sr => reasons.push(sr));
+
+ if (r.deleted) reasons.push({ code: 'DELETED', text: 'DELETED', tip: 'Author deleted review' });
+
+ const low = reasons.some(x => x.code === 'LEN' || x.code === 'CLICHÉ' || x.code === 'SPAM');
+ return { low, reasons };
+ }
+
+ // Panel
+ const panel = document.createElement('div');
+ panel.id = 'bratr-helper';
+ panel.innerHTML = `
+
+ BRATR Helper
+
+
+
+
+ `;
+ document.body.appendChild(panel);
+
+ // Styles (+ clickable rows + pulse highlight)
+ const css = document.createElement('style');
+ css.textContent = `
+ #bratr-helper{position:fixed;right:12px;bottom:12px;width:780px;max-height:70vh;z-index:99999;
+ background:#111;border:1px solid #333;border-radius:12px;color:#ddd;font:12px/1.4 system-ui;box-shadow:0 8px 24px rgba(0,0,0,.4)}
+ #bratr-helper .bh-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid #2a2a2a}
+ #bratr-helper .bh-head strong{font-size:13px}
+ #bratr-helper .bh-metrics{opacity:.8}
+ #bratr-helper .bh-collapse{background:#222;border:1px solid #444;border-radius:8px;color:#ddd;padding:2px 8px;cursor:pointer}
+ #bratr-helper .bh-body{padding:8px}
+ #bratr-helper .bh-controls{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px}
+ #bratr-helper .bh-controls label{display:flex;gap:6px;align-items:center;background:#161616;border:1px solid #2a2a2a;border-radius:8px;padding:4px 8px}
+ #bratr-helper input{background:#0f0f0f;border:1px solid #333;color:#ddd;border-radius:6px;padding:3px 6px}
+ #bratr-helper button.bh-copy,#bratr-helper button.bh-reset{background:#1e1e1e;border:1px solid #444;color:#ddd;border-radius:8px;padding:4px 10px;cursor:pointer}
+ #bratr-helper .bh-table{overflow:auto;max-height:48vh;border:1px solid #2a2a2a;border-radius:8px}
+ #bratr-helper table{width:100%;border-collapse:collapse}
+ #bratr-helper th,#bratr-helper td{padding:6px 8px;border-bottom:1px solid #232323;vertical-align:top}
+ #bratr-helper tr[data-i]{cursor:pointer}
+ #bratr-helper tr[data-i]:hover td{background:#18130a}
+ #bratr-helper tr.low td{background:rgba(255,180,0,.08)}
+ #bratr-helper tr.deleted td{opacity:.65}
+ #bratr-helper .pill{font-size:11px;display:inline-block;padding:1px 6px;border-radius:999px;border:1px solid #444;margin-right:6px;white-space:nowrap}
+ #bratr-helper .pill.low{border-color:#f5b400;color:#f5b400}
+ #bratr-helper .pill.one{border-color:#ff5b5b;color:#ff5b5b}
+ #bratr-helper .pill.delet{border-color:#8aa;color:#8aa}
+ #bratr-helper .pill.spam{border-color:#ff9d00;color:#ff9d00}
+ #bratr-helper .links a{margin-right:8px}
+ @keyframes bh-pulse {
+ 0% { box-shadow: 0 0 0 0 rgba(245,180,0,.6); }
+ 100% { box-shadow: 0 0 0 12px rgba(245,180,0,0); }
+ }
+ .bh-target-pulse { animation: bh-pulse 1s ease-out 0s 2; outline: 2px solid #f5b400 !important; }
+ @media (max-width: 880px){#bratr-helper{left:8px;right:8px;width:auto}}
+ `;
+ document.head.appendChild(css);
+
+ const ui = {
+ min: panel.querySelector('.bh-minrating'),
+ maxlen: panel.querySelector('.bh-maxlen'),
+ user: panel.querySelector('.bh-user'),
+ lowonly: panel.querySelector('.bh-lowonly'),
+ from: panel.querySelector('.bh-from'),
+ to: panel.querySelector('.bh-to'),
+ table: panel.querySelector('.bh-table'),
+ metrics: panel.querySelector('.bh-metrics')
+ };
+
+ panel.querySelector('.bh-collapse').addEventListener('click', () => {
+ const body = panel.querySelector('.bh-body');
+ if (body.style.display === 'none') { body.style.display = ''; panel.querySelector('.bh-collapse').textContent = '–'; }
+ else { body.style.display = 'none'; panel.querySelector('.bh-collapse').textContent = '+'; }
+ });
+ panel.querySelector('.bh-reset').addEventListener('click', () => {
+ ui.min.value = 0; ui.maxlen.value = 220; ui.user.value = ''; ui.lowonly.checked = true; ui.from.value = ''; ui.to.value = '';
+ render();
+ });
+ ['input','change'].forEach(ev => {
+ [ui.min, ui.maxlen, ui.user, ui.lowonly, ui.from, ui.to].forEach(el => el.addEventListener(ev, render));
+ });
+
+ function withinDates(iso, from, to) {
+ if (!iso) return true;
+ const t = new Date(iso).getTime();
+ if (from && t < new Date(from).getTime()) return false;
+ if (to && t > new Date(to).getTime()) return false;
+ return true;
+ }
+
+ function currentRows() {
+ const min = parseFloat(ui.min.value || '0');
+ const maxlen = parseInt(ui.maxlen.value || '0', 10);
+ const user = (ui.user.value || '').toLowerCase();
+ const lowOnly = ui.lowonly.checked;
+ const from = ui.from.value;
+ const to = ui.to.value;
+
+ return reviews.filter(r => {
+ if (Number.isFinite(min) && r.rating != null && r.rating < min) return false;
+ if (user && !r.author.toLowerCase().includes(user)) return false;
+ if (from || to) { if (!withinDates(r.timeIso, from, to)) return false; }
+ const diag = diagnoseLowEffort(r, maxlen);
+ if (lowOnly && !diag.low) return false;
+ return true;
+ });
+ }
+
+ function render() {
+ const maxlen = parseInt(ui.maxlen.value || '220', 10);
+ const live = reviews.filter(r => !r.deleted);
+ const lowCount = live.filter(r => diagnoseLowEffort(r, maxlen).low).length;
+ const avgRating = avg(live.map(r => r.rating || 0)).toFixed(2);
+ ui.metrics.textContent = `on page: ${reviews.length} • avg ★ ${avgRating} • low-effort flagged: ${lowCount}`;
+
+ const rows = currentRows();
+ const html = [`
+ | Author | ★ | Thread | When | Len | Flags (why) | Actions |
`];
+
+ rows.forEach(r => {
+ const diag = diagnoseLowEffort(r, maxlen);
+ const low = diag.low;
+ const one = Math.round(r.rating||0) === 1;
+ const cls = `${low ? 'low' : ''} ${r.deleted ? 'deleted': ''}`.trim();
+ const flagHtml = diag.reasons.map(reason => {
+ let pill = 'pill';
+ if (reason.code === 'LEN' || reason.code === 'CLICHÉ') pill += ' low';
+ if (reason.code === 'SPAM') pill += ' spam';
+ if (reason.code === 'DELETED') pill += ' delet';
+ const tip = reason.tip ? ` title="${reason.tip.replace(/"/g, '"')}"` : '';
+ return `${reason.text}`;
+ }).join('');
+ html.push(`
+ | ${r.author || ''} |
+ ${r.rating != null ? r.rating.toFixed(2) : ''} |
+ ${r.thread || ''} |
+ ${r.timeTxt || ''} |
+ ${r.effLen} (raw ${r.rawLen}) |
+ ${flagHtml || ''}${one ? '1★' : ''} |
+
+ ${r.links.report ? `Report` : ''}
+ ${r.links.edit ? `Edit` : ''}
+ ${r.links.del ? `Delete` : ''}
+ ${r.links.warn ? `Warn` : ''}
+ |
+
`);
+ });
+
+ html.push(`
`);
+ ui.table.innerHTML = html.join('');
+
+ // Bind row click delegation exactly once and keep it
+ if (!ui.table._bhBound) {
+ ui.table.addEventListener('click', onRowClick);
+ ui.table._bhBound = true;
+ }
+ retint();
+ }
+
+ function onRowClick(e) {
+ if (e.target.closest('a, button')) return; // let links/buttons behave
+ const tr = e.target.closest('tr[data-i]');
+ if (!tr) return;
+ const idx = parseInt(tr.getAttribute('data-i'), 10);
+ const r = reviews[idx];
+ if (!r || !r.node) return;
+
+ // Smooth scroll and pulse
+ r.node.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ r.node.classList.remove('bh-target-pulse');
+ void r.node.offsetWidth; // reflow to restart animation
+ r.node.classList.add('bh-target-pulse');
+ setTimeout(() => r.node.classList.remove('bh-target-pulse'), 1800);
+ }
+
+ // Tint live page with reasons as tooltip
+ function retint() {
+ const maxlen = parseInt(ui.maxlen?.value || '220', 10);
+ reviews.forEach(r => {
+ if (r.deleted) {
+ r.node.style.outline = '';
+ r.node.removeAttribute('title');
+ return;
+ }
+ const diag = diagnoseLowEffort(r, maxlen);
+ r.node.style.outline = '';
+ r.node.removeAttribute('title');
+ if (diag.low) {
+ r.node.style.outline = '2px solid #f5b400';
+ const tips = diag.reasons
+ .filter(x => x.code === 'LEN' || x.code === 'CLICHÉ' || x.code === 'SPAM')
+ .map(x => x.text).join(' | ');
+ if (tips) r.node.setAttribute('title', tips);
+ }
+ });
+ }
+
+ render();
+
+ // Copy CSV of current view, with reasons column
+ panel.querySelector('.bh-copy').addEventListener('click', () => {
+ const rows = currentRows();
+ const maxlen = parseInt(ui.maxlen?.value || '220', 10);
+ const header = ['author','rating','thread','threadUrl','timeIso','effLen','rawLen','deleted','low','reasons','body'];
+ const lines = [header.join(',')];
+ rows.forEach(r => {
+ const diag = diagnoseLowEffort(r, maxlen);
+ const reasonFlat = diag.reasons.map(x => x.text).join(' | ');
+ lines.push([
+ csvEscape(r.author),
+ r.rating ?? '',
+ csvEscape(r.thread),
+ csvEscape(r.threadUrl),
+ csvEscape(r.timeIso),
+ r.effLen ?? '',
+ r.rawLen ?? '',
+ r.deleted ? '1' : '0',
+ diag.low ? '1' : '0',
+ csvEscape(reasonFlat),
+ csvEscape((r.bodyTxt || '').slice(0,3000))
+ ].join(','));
+ });
+ const csv = lines.join('\n');
+ try { GM_setClipboard(csv, { type: 'text', mimetype: 'text/csv' }); }
+ catch { navigator.clipboard?.writeText(csv); }
+ });
+})();
\ No newline at end of file