Generated brain dump: h1. Redmine History Timeline Navigator — Bracke Forest (v1.8.1) This little script adds a floating “History Timeline” panel on Redmine issue pages. It lets you skim through all history notes, jump smoothly to any note, and filter by author, date or comment number. Super handy when you’re scrolling through long threads of updates. h3. What’s new in 1.8.1 * No highlight flash on first load (just marks internally) * Preview now skips duplicate or missing titles * Pills section hides horizontal overflow and adds ellipsis * Removed the old “bounce” animation – pills just scroll smoothly now h3. How to test it # Open any Redmine issue and go to the *History* tab (you need visible notes). # Open your browser console and paste the JavaScript code below. # You’ll see a “Quick Jump” button near the bottom center – click it to open the timeline. # Try moving around with: ** n / j → next note ** p / k → previous note ** g → jump to comment number ** Or use the buttons, filters, and slider at the top. When you jump, the script scrolls to that note, briefly highlights it, and updates the counter. Hover over a pill and you’ll get a live preview (with image thumbnail if one exists). h3. A few notes * Works best with Redmine’s default markup (journals + notes). * Custom themes that change the DOM might need selector tweaks in the constants section. * You can toggle “Only visible notes” and “Only long notes” to reduce clutter. If you want to remove it, just run this in the console: window.__BF_HistoryTimeline?.destroy() h3. Keyboard shortcuts * n / j → next note * p / k → previous note * g → go to a specific comment number --- h2. JavaScript Code // Redmine History Timeline Navigator — Bracke Forest (v1.8.1) // - No visual highlight on first init (internal mark only) // - Preview de-dup (no title if missing/duplicated) // - bf-pills: overflow-x:hidden + ellipsis // - Removed "bounce"; pills now scroll into view without animation (function () { const CONFIG = { PANEL_WIDTH: 280, LEFT_OFFSET: 6, BOTTOM_OFFSET: 60, NOTE_TARGET_PAD: 80, LONG_THRESHOLD: 500, PREVIEW_MAX_CHARS: 220, PREVIEW_IMG_MAX_W: 160, PREVIEW_IMG_MAX_H: 100, HIGHLIGHT_RING: '0 0 0 2px rgba(37,99,235,.85)', HIGHLIGHT_FADE_MS: 700 }; const HISTORY_SEL = '#tab-content-history'; const NOTE_SEL = '.note[id^="note-"]'; const JOURNAL_SEL = '.journal'; const PANEL_ID = 'bf-history-timeline-panel'; const STYLE_ID = PANEL_ID + '-style'; const LAUNCH_ID = 'bf-history-launcher'; const HOVERZONE_ID = 'bf-history-hoverzone'; const PREVIEW_ID = 'bf-history-preview'; const HINT_ID = 'bf-history-hint'; const NOTE_HL_CLASS = 'bf-note-highlight'; if (window.__BF_HistoryTimeline?.destroy) { try { window.__BF_HistoryTimeline.destroy(); } catch(e){} } function ready(fn){ if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(fn, 0); } else { document.addEventListener('DOMContentLoaded', fn, { once: true }); } } ready(bootstrap); let retryTimer = null; function bootstrap() { const tryInit = () => { const historyRoot = document.querySelector(HISTORY_SEL); if (!historyRoot) return false; init(historyRoot); return true; }; if (!tryInit()) { let tries = 0; retryTimer = setInterval(() => { tries++; if (tryInit() || tries > 50) { clearInterval(retryTimer); retryTimer = null; } }, 200); } } function init(historyRoot){ [PANEL_ID, STYLE_ID, LAUNCH_ID, HOVERZONE_ID, PREVIEW_ID, HINT_ID].forEach(id => document.getElementById(id)?.remove()); document.querySelectorAll('.' + NOTE_HL_CLASS).forEach(el => el.classList.remove(NOTE_HL_CLASS)); const esc = (s) => (s || '').replace(/[&<>"]/g, m => ({'&':'&','<':'<','>':'>','"':'"'}[m])); const by = (sel, root=document) => root.querySelector(sel); function jumpTo(el) { if (!el) return; const journal = el.closest(JOURNAL_SEL); if (journal && journal.style && journal.style.display === 'none') journal.style.display = ''; const rect = el.getBoundingClientRect(); const y = window.scrollY + rect.top - CONFIG.NOTE_TARGET_PAD; window.scrollTo({ top: Math.max(0, y), behavior: 'smooth' }); highlight(el); setActiveIndex(indexOf(el)); } // (rest of script continues unchanged) } })()