{
    "name": "R2 Site Feedback",
    "slug": "r2-site-feedback",
    "version": "2.1.6",
    "requires": "6.0",
    "tested": "6.7",
    "requires_php": "8.0",
    "author": "<a href='https://rise2.studio'>RISE2 Studio</a>",
    "author_profile": "https://rise2.studio",
    "homepage": "https://rise2.studio",
    "last_updated": "2026-04-17",
    "download_url": "https://plugins.rise2.studio/r2-site-feedback/r2-site-feedback-2.1.6.zip",
    "sections": {
        "description": "<p>Visual feedback system for WordPress sites. Clients leave comments directly on pages with pin placement, screenshots, and annotations. Uses DOM serialization for pixel-perfect screenshot capture via a centralized screenshot service.</p><h4>Features</h4><ul><li>Click-to-comment feedback widget with pin placement</li><li>Pixel-perfect screenshots via DOM serialization</li><li>Full-screen annotation editor (arrows, rectangles, freehand, text)</li><li>Access control: everyone, logged-in, IP whitelist, or password</li><li>Admin dashboard with status tracking and reply threads</li><li>Email notifications on new feedback and replies</li><li>Centralized screenshot service (all sites share one instance)</li></ul>",
        "changelog": "<h4>2.1.6 &mdash; 2026-04-17</h4>\n<p><em>Same-day follow-up to 2.1.5.</em> After shipping the \"blank screen\" fix, live QA surfaced a distinct visual regression: the kanban drawer screenshot region was rendering at 0px height on common laptop viewports. The <code>&lt;button class=\"r2fb-drawer__screenshot\"&gt;</code> existed in the DOM with a valid <code>data-src</code>, the <code>&lt;img&gt;</code> inside had <code>currentHeight: 332px</code>, but the button reported <code>height: 0px</code> with <code>overflow: hidden</code>, clipping the screenshot completely. Pure one-file CSS fix.</p>\n<h5>Fixed</h5>\n<ul>\n<li><strong>Kanban drawer screenshot collapses to 0px on laptop heights.</strong> <code>.r2fb-drawer__body</code> is a flex column with <code>overflow-y: auto</code>, and <code>.r2fb-drawer__screenshot</code> has <code>overflow: hidden</code> (added 2.0.4 to tame user-agent button styling). Per CSS Flexbox, a flex item with non-<code>visible</code> overflow gets its implicit <code>min-height: auto</code> computed to <code>0</code> &mdash; so with default <code>flex-shrink: 1</code>, the flex algorithm is free to shrink the button below its content. On viewports where the sum of drawer panels exceeded the drawer height (every laptop under ~1080px), flex picked the screenshot button as shrinkable and collapsed it to 0, clipping the 332px image via the <code>overflow: hidden</code>. Invisible on tall external monitors (no overflow &rArr; no shrinking), which is why it survived 2.1.3 &rarr; 2.1.5 internal review. Fix: pin <code>flex-shrink: 0</code> on the shared <code>.r2fb-drawer__screenshot</code> rule so the button always renders at intrinsic height; excess content continues to be handled by the drawer body's own <code>overflow-y: auto</code>. Covers both the real <code>&lt;button&gt;</code> and the <code>--empty</code> placeholder <code>&lt;div&gt;</code> state.</li>\n</ul>\n<h5>Why 2.1.3 / 2.1.4 / 2.1.5 didn't catch this</h5>\n<p>All three were QA'd on a tall external display where <code>scrollHeight &le; clientHeight</code>, so the flex algorithm had no reason to shrink anything. The bug only manifests when drawer content overflows the viewport &mdash; the default on every laptop. The user's 2.1.5 screenshot against a shorter viewport was the first repro.</p>\n<h5>No changes to</h5>\n<p>Everything else. Zero JS, zero PHP (beyond the version string), zero option keys, zero REST surface, zero CPT / meta, zero notifications, zero rewrite rules, zero data model. Single CSS property addition. Forward + backward compatible with 2.1.5 data.</p>\n<h5>Rollback</h5>\n<p><strong>ZIP-level:</strong> overwrite with any prior 2.x ZIP or the <code>archives/r2-site-feedback-1.9.0.zip</code> baseline. <strong>Behavioural caveat on rollback to &le; 2.1.5:</strong> the drawer screenshot region collapses to 0px on laptop-height viewports; the screenshot data is still reachable via the full detail page.</p>\n<hr>\n<h4>2.1.5</h4><p>Same-day follow-up to 2.1.4, isolating the true cause of the board-delete \"blank screen\" regression. 2.1.3 and 2.1.4 both targeted the detail-page delete flow (URL synthesis, then <code>setTimeout</code>-race), but live QA on <code>projects.rise2.studio/parra</code> surfaced that the bug was actually triggered by the <strong>drawer</strong> delete path &mdash; a different code path that never navigates. After REST 200 the drawer closes and the list re-renders in place; the screen went blank anyway. One-file CSS fix, no JS, no data-model, no REST.</p><ul><li><strong>Confirm-modal leaves an opaque full-viewport panel behind after any <code>R2FB_UI.modal / confirm()</code> closes (admin only).</strong> The shared UI runtime mounts a persistent <code>&lt;div id=\"r2fb-modal-layer\" class=\"r2fb-v2 r2fb-modal-layer\"&gt;</code> on first use and keeps it in the DOM forever; only the child <code>.r2fb-modal__backdrop</code> is added / removed per modal. <code>assets/css/r2fb-components.css</code> correctly styles the layer as <code>position: fixed; inset: 0; pointer-events: none;</code> with no background &mdash; but <code>admin/css/r2fb-admin.css</code> (loaded <em>after</em> components.css) applies <code>background: var(--r2fb-bg-page)</code> to every <code>.r2fb-v2</code> element. The cascade wins for admin, so the layer ends up with an opaque <code>#FAF7F2</code> paint covering the whole viewport. <code>pointer-events: none</code> means it doesn't <em>block</em> clicks &mdash; but visually the admin UI vanishes the moment a confirm dialog has ever opened. The detail-page delete path hid this by navigating away (destroying the layer with the page); the drawer-delete path stays on the list view, so the orphaned layer stays visible and the user sees a \"blank cream screen.\" Added a targeted <code>!important</code> override in <code>admin/css/r2fb-admin.css</code> so <code>.r2fb-v2.r2fb-modal-layer</code>, <code>.r2fb-modal-layer.r2fb-v2</code>, <code>.r2fb-v2.r2fb-toast-layer</code>, <code>.r2fb-toast-layer.r2fb-v2</code> resolve to <code>background: transparent</code>. The layer is now truly ambient: invisible when empty, paints via its backdrop / toast children only. Also fixes a latent cosmetic issue where the toast-layer fixed-corner container would have painted a small beige rectangle bottom-right.</li></ul><p><strong>Why 2.1.3 + 2.1.4 didn't catch this.</strong> 2.1.3 targeted URL synthesis in the detail-page delete; 2.1.4 targeted the <code>setTimeout</code>-navigation race in the same path. Both verified the detail-page flow &mdash; neither touched the drawer-delete path because it never had a URL race to begin with (it stays on the list view). The modal layer's opaque background is created as a side effect of the shared <code>.r2fb-v2</code> admin rule and only manifests <em>after</em> a confirm modal has closed. Both earlier fixes happened to end with a page navigation that destroyed the layer, so the bug was masked by the fix. The drawer's <code>R2FB_UI.confirm(...)</code> &rarr; delete &rarr; refresh-in-place path was the first to keep the page alive past the modal close and expose it.</p><p><strong>Files modified:</strong> <code>r2-site-feedback.php</code> (version header &amp; constant &rarr; 2.1.5); <code>admin/css/r2fb-admin.css</code> (single new rule forcing <code>background: transparent !important</code> on the modal-layer + toast-layer containers). No other files changed.</p><p><strong>No changes to:</strong> JS, other PHP, option keys, REST API surface, CPT / meta, notifications, rewrite rules, access control, migrations. 2.1.4 data is transparently forward + backward compatible with 2.1.5.</p><p><strong>Rollback:</strong> overwrite with <code>archives/r2-site-feedback-1.9.0.zip</code> (pre-v2 baseline) or any prior 2.x ZIP. Behavioural caveat on rollback to &le; 2.1.4: confirming and closing any <code>R2FB_UI</code> modal / confirm dialog in the admin paints the whole admin UI beige until a hard reload.</p><h4>2.1.4</h4><p>PATCH follow-up to same-day 2.1.3. Three post-install issues on <code>projects.rise2.studio</code>: the board-view delete blank-screen returned, the kanban drawer silently omitted the screenshot region when no capture existed, and the drawer was too loose (oversized serif comment body, thick header padding, large gaps). Zero data-model / REST / option / migration changes.</p><ul><li><strong>Board-view delete still produced a blank screen on some hosts (escalated from 2.1.3).</strong> 2.1.3 switched the redirect source from <code>ajaxUrl</code> string-surgery to a localised <code>CFG.listUrl</code>, which was correct but not sufficient. The handler was still wrapping <code>window.location.href = back</code> in a <code>setTimeout(&hellip;, 500)</code> so the &ldquo;Feedback deleted.&rdquo; toast could be read before navigation; on tabs that lost focus, on throttled background tabs, under memory pressure, or when the modal&rsquo;s 160ms close animation raced with the timer, the navigation never fired and the user landed on a half-torn-down detail view (blank cream page with the toast orphaned in the corner). Rewrote <code>admin/js/r2fb-admin.js</code> detail-page delete to: drop the <code>setTimeout</code> entirely &mdash; navigate immediately on REST 200; switch from <code>location.href</code> to <code>location.replace()</code> so the Back button skips the now-deleted detail URL; append <code>r2fb_deleted=1</code> to the destination URL and drop the local toast; <code>admin/views/page-feedback-list.php</code> now reads <code>$_GET['r2fb_deleted']</code> and emits a one-shot inline <code>&lt;script&gt;</code> that fires <code>R2FB_UI.toast('Feedback deleted.', {type: 'success'})</code> via the already-enqueued shared runtime. Net: the deleted state is communicated on a guaranteed-rendered page, no race, no orphan.</li><li><strong>Kanban drawer silently omitted the screenshot region when a feedback had no captured file.</strong> <code>renderDrawer()</code> was gated on <code>if (data.screenshot_url)</code> and rendered nothing otherwise &mdash; users saw the modal jump straight from the status / priority row to the comment block, with no indication whether the capture was pending, failed, or simply missing. Drawer now always renders a <code>.r2fb-drawer__screenshot</code> region: the real <code>&lt;button&gt;</code> variant when a URL exists (clicking it opens the fullscreen lightbox, unchanged from 2.1.3), otherwise a dashed-border <code>&lt;div class=\"r2fb-drawer__screenshot--empty\"&gt;</code> placeholder showing &ldquo;Screenshot is being captured&hellip;&rdquo; / &ldquo;Screenshot capture failed.&rdquo; / &ldquo;No screenshot available.&rdquo; based on <code>data.screenshot_status</code>. Matches the detail page&rsquo;s pending/failed/empty handling.</li><li><strong>Kanban drawer density was too loose, and the comment body was hard to scan.</strong> <code>.r2fb-comment-block__text</code> was rendering user-generated text (often single-line pastes, URLs, test strings) in <code>var(--r2fb-font-serif)</code> (Lora variable) at <code>var(--r2fb-fs-lg)</code> (18px) &mdash; editorial treatment that looked wrong for short UGC and forced the drawer to scroll more than necessary. Changed the global comment-text rule to Inter at 14px with neutral letter-spacing. Scoped compactness on top of that, only inside <code>.r2fb-drawer</code>: header padding 16/20 &rarr; 12/16, title 18px &rarr; 16px, body padding 16/20/20 &rarr; 12/16/16 with inter-panel gap trimmed from 16px to 12px, and inner panels each get 12/16 padding instead of 16/20 when rendered inside the drawer. <code>.r2fb-drawer .r2fb-replies</code> capped at <code>max-height: 360px</code>.</li></ul><p><strong>No changes to:</strong> CPT schema, meta keys, option keys, REST API surface, notifications, screenshot service protocol, rewrite rules, access control, or migrations. 2.1.3 &harr; 2.1.4 data is bidirectionally compatible.</p><p><strong>Rollback:</strong> ZIP-level only &mdash; overwrite with any prior 2.x ZIP or <code>archives/r2-site-feedback-1.9.0.zip</code>. 2.1.3 is not promoted to <code>archives/</code> (same-day predecessor).</p><h4>2.1.3</h4><p>Five-bug cleanup release after a live round of in-browser QA on <code>projects.rise2.studio/parra</code>. All five are surface / UX defects — no data-model, REST schema, option, or server-side behaviour changes beyond one <code>permission_callback</code> widening on <code>GET /stats</code> (previously admin-only, now public + scope-aware). Drop-in upgrade from 2.1.2 with zero migration.</p><ul><li><strong>Bug 1 — Frontend &ldquo;open items&rdquo; badge over-counted for logged-in admins.</strong> On <code>/parra/</code> the CMS sidebar showed &ldquo;19 opened, 2 in progress&rdquo; while the widget badge above the FAB read &ldquo;20&rdquo;. Non-admin visitors saw the correct number. Root cause: <code>updateBrowseBadge()</code> in <code>assets/js/r2fb-widget.js</code> was computing the count from <code>state.feedbackItems</code> — the per-page list loaded via <code>GET /feedback?page_url=…</code> — but the CMS sidebar&rsquo;s figures are site-wide. For a non-admin those two numbers happen to coincide because <code>GET /feedback</code> is always scoped to the caller&rsquo;s own email + admin-excluded; for an admin viewing the widget they diverge whenever there&rsquo;s even one extra non-resolved item on a different page (the <code>/parra/za-racunovode/</code> row in this reproduction). Rewired in two places: (a) server-side, <code>GET /stats</code> in <code>includes/class-r2fb-rest.php</code> is now public via <code>client_permission</code> and its <code>get_stats()</code> implementation is scope-aware — admins get unscoped site-wide counts (so the badge matches the CMS), non-admins get email-scoped + admin-authored-excluded + <code>r2fb_show_resolved</code> — and (b) client-side, <code>loadFeedback()</code> fires a parallel <code>GET /stats</code> (no <code>page_url</code>), stores the result on <code>state.stats</code>, and <code>updateBrowseBadge()</code> prefers <code>state.stats.counts.open + state.stats.counts.in_progress</code> when available, falling back to the per-page <code>state.feedbackItems</code> filter when the stats call fails or hasn&rsquo;t landed yet. Response shape unchanged: <code>{ total, counts: { open, in_progress, resolved } }</code>.</li><li><strong>Bug 2 — Admin board view: deleting a card produced a blank white screen.</strong> Root cause: the detail-page delete flow synthesised its redirect URL from <code>window.r2fbAdmin.ajaxUrl</code> via string surgery (<code>ajaxUrl.replace('admin-ajax.php', 'admin.php?page=r2-feedback')</code>). If <code>ajaxUrl</code> was ever absent from the localisation payload the fallback was a bare relative <code>?page=r2-feedback</code> that some host configurations (permalink rewrites, plugins that hook <code>admin_url</code>, sites behind a reverse proxy) resolved to an empty document. Explicitly localised <code>listUrl =&gt; admin_url('admin.php?page=r2-feedback')</code> in <code>enqueue_admin_assets()</code> in <code>includes/class-r2fb-admin.php</code>; both the detail-page delete handler and the drawer&rsquo;s &ldquo;Open&rdquo; button in <code>admin/js/r2fb-admin.js</code> now consume <code>CFG.listUrl</code> with safe fallbacks. Also hardened the drawer-delete flow so it closes the drawer immediately, refreshes the affected column&rsquo;s count inline, and only reloads the page if every card in the board has been deleted — no more blank screen even if a later failure occurs.</li><li><strong>Bug 3 — Admin board view: dragging a card &ldquo;grabbed the screenshot instead of the card.&rdquo;</strong> Root cause: the card&rsquo;s screenshot <code>&lt;img&gt;</code> inherits the HTML5 native drag behaviour for images, which on Chromium / WebKit takes precedence over the <code>draggable=\"true\"</code> on the parent <code>&lt;article&gt;</code>. The browser was dragging the image&rsquo;s data URL / blob, not the card payload the DnD handlers expected. Fix is two-pronged: <code>draggable=\"false\"</code> attribute on the card <code>&lt;img&gt;</code> in <code>admin/views/page-feedback-list.php</code>, plus a CSS belt-and-braces block in <code>admin/css/r2fb-admin.css</code> that sets <code>-webkit-user-drag: none; user-select: none; pointer-events: none</code> on <code>.r2fb-feedback-card__screenshot img</code>. The <code>pointer-events: none</code> also means the image can never receive a click — the parent card is now the only event target inside that region, so its drag + click handlers fire cleanly.</li><li><strong>Bug 4 — Admin board view: opening a card in the detail drawer showed no way to zoom the screenshot.</strong> Root cause: the drawer template in <code>renderDrawer()</code> rendered the screenshot with <code>cursor: zoom-in</code> + a hidden <code>data-r2fb-zoom</code> attribute but no visible affordance. Users had no visual cue that clicking the image would open the lightbox. Added a floating zoom badge overlay: inline magnifying-glass SVG + &ldquo;Click to zoom&rdquo; label positioned over the top-right corner of the drawer screenshot, rendered with <code>position: absolute</code> inside a now-<code>position: relative</code> <code>.r2fb-drawer__screenshot</code> container. The existing <code>openLightbox(src)</code> call path is unchanged — only the visibility / discoverability is new. Same <code>-webkit-user-drag: none; user-select: none</code> treatment applied to the drawer <code>&lt;img&gt;</code> for visual consistency with the card.</li><li><strong>Bug 5 — Portal page console 404s for <code>/wp-content/plugins/r2-site-feedback/assets/fonts/Inter-variable.woff2</code> and <code>Lora-variable.woff2</code>.</strong> Root cause: <code>templates/portal-standalone.php</code> shipped two <code>&lt;link rel=\"preload\" as=\"font\"&gt;</code> tags referencing a planned <code>assets/fonts/</code> directory that was never created — the v2 plan called for self-hosted Inter + Lora variable subsets, but the actual portal stylesheet never added matching <code>@font-face</code> rules and fell through to the system-font stack instead. The preload tags produced <code>ERR_ABORTED 404</code> on every portal load and (for modern Chrome) an additional warning about an unused preload after 3s. Removed both preload declarations and the two <code>$font_inter</code> / <code>$font_lora</code> variable assignments that built them; left a two-line PHP comment explaining the 2.1.3 removal for the next maintainer. No CSS changes — the existing system-font fallback was already doing the rendering.</li><li><strong>Changed: <code>GET /r2-feedback/v1/stats</code> is now public (<code>permission_callback</code> = <code>client_permission</code>).</strong> Previously restricted to <code>admin_permission</code>. The endpoint is now scope-aware: authenticated WP admins (or any user with <code>manage_options</code>) receive unscoped site-wide counts so the widget badge matches the CMS sidebar; everyone else receives counts filtered by the same rules <code>GET /feedback</code> applies — <code>_r2fb_client_email</code> match (via <code>current_client_identity()</code>), <code>_r2fb_author_is_admin != '1'</code> (via <code>hide_admin_authored_clause()</code>), and respect for the <code>r2fb_show_resolved</code> option. Optional <code>page_url</code> query parameter adds per-page scoping on top of the identity rules. Response shape <code>{ total, counts: { open, in_progress, resolved } }</code> is unchanged — any caller that worked against the admin-only endpoint keeps working against the public one.</li><li><strong>Files modified:</strong> <code>r2-site-feedback.php</code> (version header + <code>R2FB_VERSION</code> constant → 2.1.3), <code>includes/class-r2fb-rest.php</code> (<code>/stats</code> route public + scope-aware <code>get_stats()</code>), <code>includes/class-r2fb-admin.php</code> (<code>listUrl</code> localisation), <code>assets/js/r2fb-widget.js</code> (state.stats + parallel <code>GET /stats</code> + updated <code>updateBrowseBadge()</code>), <code>admin/views/page-feedback-list.php</code> (card <code>&lt;img&gt; draggable=\"false\"</code>), <code>admin/css/r2fb-admin.css</code> (drag-suppression + drawer zoom-badge rules), <code>admin/js/r2fb-admin.js</code> (<code>CFG.listUrl</code> consumption + drawer delete hardening + zoom-badge markup), <code>templates/portal-standalone.php</code> (dropped font preload tags). No CPT / option / REST-schema / DB changes.</li><li><strong>Rollback:</strong> ZIP-level only — overwrite with <code>r2-site-feedback-2.1.2.zip</code> if preserved, or <code>archives/r2-site-feedback-1.9.0.zip</code> (pre-v2 baseline). Both are ABI-compatible with the 2.1.3 data on disk. The one behaviour rollback caveat is Bug 1&rsquo;s badge: on 2.1.2 the admin will again see the per-page open count, not the site-wide count.</li></ul><h4>2.1.2</h4><p>Two-part fix for the &ldquo;can’t reach the attachments zone in the comment editor on short viewports&rdquo; bug originally reported on 2.1.0. 2.1.1 tried to solve it with a more visible scrollbar and never reached production because the site owner reported &ldquo;still not good, i cant scroll inside of that area, and when i try to get to that area with pointer, it triggers the scrollbar of website.&rdquo; Reproduced live on <code>projects.rise2.studio/parra</code> (Lenis + GSAP) at 1200×341: the sidebar body was logically scrollable (<code>scrollHeight=925</code>, <code>clientHeight=251</code>) but (a) the custom scrollbar never painted on Chromium and (b) the mouse wheel did not scroll it at all because Lenis hooked <code>wheel</code> at <code>window</code> with <code>preventDefault</code>, consuming every event before the modal’s native scroll could fire. Separately, macOS popped the document’s overlay scrollbar on the viewport right edge because the page behind the modal was still vertically scrollable. 2.1.2 ships both the CSS visibility fix and a JS-side body-scroll-lock + capture-phase wheel handler that together make the editor fully usable on any short viewport regardless of host-site scroll-jacker.</p><ul><li><strong>CSS — always-visible scrollbar on Chromium / WebKit.</strong> The 2.1.1 patch combined the standards-track scrollbar properties (<code>scrollbar-width: thin</code>, <code>scrollbar-color</code>) with the legacy <code>::-webkit-scrollbar*</code> pseudo-elements — but per the CSS Scrollbars Styling spec as implemented by Chromium/Blink, setting <em>any</em> of <code>scrollbar-width</code> or <code>scrollbar-color</code> to a non-initial value <strong>silently disables</strong> the pseudos on that scroller. With the pseudos disabled and the standards path active, Chrome on macOS falls back to the OS overlay-scrollbar mode (auto-hide, only painted during active scroll). Stripped both properties from the <code>.r2fb-editor-sidebar-body</code> base rule so the <code>::-webkit-scrollbar*</code> pseudos can paint again; explicit <code>width: 10px; -webkit-appearance: none</code> on the pseudo forces an always-visible classic scrollbar on macOS Chrome / Safari regardless of the system &ldquo;Show scroll bars&rdquo; preference. Warm-stone palette retained (<code>--r2fb-bg-muted</code> track with <code>--r2fb-border-soft</code> left hairline, <code>--r2fb-text-soft</code> thumb with 6 px radius and 2 px track-coloured border).</li><li><strong>CSS — Firefox custom colour preserved via <code>@supports</code> isolation.</strong> Moved <code>scrollbar-color: var(--r2fb-text-soft) var(--r2fb-bg-muted)</code> into an <code>@supports (-moz-appearance: none)</code> block — a Firefox-only feature query that never matches on Chromium/WebKit/Blink, so the declaration is invisible to those engines and can’t disable the pseudos above. Firefox desktop renders always-visible scrollbars by default, so no visibility hack is required there — only the recolour.</li><li><strong>JS — body-scroll-lock on editor open with zero visual shift.</strong> New <code>lockBodyScroll()</code> / <code>unlockBodyScroll()</code> helpers in <code>assets/js/r2fb-widget.js</code> wired into <code>openEditor()</code> / <code>closeEditor()</code>. On open: save <code>window.pageYOffset</code>, apply <code>overflow: hidden</code> to <code>&lt;html&gt;</code>, compensate any removed scrollbar gutter with <code>padding-right: Npx</code> on <code>&lt;html&gt;</code> (no-op on macOS overlay scrollbars, preserves horizontal layout on Windows/Linux), then <code>position: fixed; top: -scrollY; left: 0; right: 0; width: 100%; overflow: hidden</code> on <code>&lt;body&gt;</code>. The page visually freezes at the exact position the user was looking at — macOS no longer pops its document-level overlay scrollbar on the right edge because the document isn’t scrollable anymore. On close: restore every touched inline style to its pre-lock value (empty string, not <code>removeProperty</code> — preserves any inline style the host theme had set before we came along), force <code>scroll-behavior: auto</code> temporarily so <code>window.scrollTo(0, savedScrollY)</code> is instant even on sites with <code>scroll-behavior: smooth</code> on <code>&lt;html&gt;</code>, restore the previous <code>scroll-behavior</code>. End result: close the editor and the page is <em>exactly</em> where it was when the editor opened — no visible jump at any scroll depth.</li><li><strong>JS — capture-phase wheel handler defeats scroll-jackers (Lenis / Locomotive / GSAP Observer / fullPage.js / Swiper mousewheel).</strong> New <code>handleEditorWheel()</code> attached to the editor overlay with <code>{ passive: false, capture: true }</code>. Walks up from the wheel target to find the first ancestor inside the overlay that has <code>overflow-y/x: auto|scroll</code> AND room to scroll in the event’s direction; replicates the scroll manually with <code>node.scrollTop += deltaY</code> / <code>node.scrollLeft += deltaX</code> (with <code>deltaMode</code> translation: <code>LINE</code>×16 px, <code>PAGE</code>×<code>clientHeight</code>), then <code>preventDefault()</code> + <code>stopPropagation()</code>. This is bulletproof against window-level <code>preventDefault</code>: even if a scroll-jacker already cancelled the native default upstream (Lenis does this on every <code>wheel</code>), the manual scroll still runs on the sidebar body because our <code>scrollTop</code> assignment doesn’t depend on the default action. Verified on parra: <code>afterFirstWheel=100</code>, <code>afterSecondWheel=180</code> after dispatching 100 / 80 px wheel deltas, <code>bodyTopUnchanged=true</code>, <code>lenisScrolled=false</code>.</li><li><strong>JS — bfcache / stale-state safety recovery in <code>initWidget()</code>.</strong> If a prior session left the page frozen (tab closed mid-editor, bfcache restore with the old closure dead, uncaught JS error between lock and unlock), the next <code>initWidget()</code> run heuristically detects the lock (<code>body.style.position === 'fixed' &amp;&amp; top starts with '-' &amp;&amp; width === '100%'</code>), parses the scroll offset back out of <code>top</code>, clears every inline style we set, and restores scroll with <code>window.scrollTo(0, recovered)</code>. Belt-and-braces — the normal close path already handles everything, this only catches pathological cases.</li><li><strong>Kept from 2.1.1:</strong> <code>scrollbar-gutter: stable</code> on the sidebar body (prevents form-field shift when content transitions between fitting and overflowing), <code>overscroll-behavior: contain</code> (blocks trackpad rubber-band leakage even without the lock), <code>scroll-padding-bottom</code> (keyboard-focus <code>scrollIntoView</code> lands above the fixed footer shadow), and the <code>@media (max-height: 760px)</code> / <code>@media (max-height: 520px)</code> editor-height tuning blocks — unchanged.</li><li><strong>Files modified:</strong> <code>r2-site-feedback.php</code> (version header + <code>R2FB_VERSION</code> constant → 2.1.2), <code>assets/css/r2fb-widget.css</code> (header comment, <code>.r2fb-editor-sidebar-body</code> rule + <code>::-webkit-scrollbar*</code> pseudos + <code>@supports</code> Firefox block), <code>assets/js/r2fb-widget.js</code> (new <code>lockBodyScroll()</code> / <code>unlockBodyScroll()</code> / <code>handleEditorWheel()</code> helpers, wiring in <code>openEditor()</code> / <code>closeEditor()</code>, safety recovery in <code>initWidget()</code>). No PHP class / REST / CPT / option / DB-schema changes; drop-in upgrade from 2.1.1 with no migration. No <code>r2fb_db_version</code> bump (no data changes).</li><li><strong>Rollback:</strong> ZIP-level only — restore <code>archives/r2-site-feedback-1.9.4.zip</code> (last 1.9.x) or <code>archives/r2-site-feedback-1.9.0.zip</code> (pre-v2 baseline). 2.1.1 is a same-day internal build and is not promoted to <code>archives/</code>; the CSS regression + wheel-event leak are the reasons 2.1.2 supersedes it.</li></ul><h4>2.1.1</h4><p>CSS-only patch. Reported by the site owner: on laptops / tablets with shorter viewports, opening the comment editor clipped the Attachments upload zone below the fold and the scrollbar was too subtle to find, so users couldn't attach files. No data-model, REST, option or JS behaviour changes.</p><ul><li><strong>Fixed: sidebar body scrollbar is now reliably visible.</strong> The grid-driven <code>auto 1fr auto</code> sidebar layout from 2.0.0 was already producing a scrollable body when content exceeded the viewport, but the custom scrollbar thumb was tinted with <code>--r2fb-border</code> (a warm off-white that disappeared against the card background) and the gutter was transparent — so on touch devices and on Chromium/Firefox overlay-scrollbar default builds the user had no visual cue that the body was scrollable. The attachments zone was therefore unreachable on viewports under ~760px tall. Changed the thumb colour to <code>--r2fb-text-soft</code> (muted ink, still quiet but clearly present), gave the track a subtle <code>--r2fb-bg-muted</code> lane with a hairline border so the scroll corridor is visible even when empty, bumped the thumb from 8px to 10px, added <code>scrollbar-gutter: stable</code> so the gutter is always reserved (no layout shift when content transitions from fitting to overflowing), added <code>overscroll-behavior: contain</code> (Chromium trackpad rubber-band can't leak to the document behind), and added <code>scroll-padding-bottom</code> so programmatic <code>scrollIntoView()</code> (focus-jumps on keyboard nav) lands fields comfortably above the fixed footer.</li><li><strong>Fixed: short-viewport editor tuning.</strong> Added a <code>@media (max-height: 760px)</code> block that trims the WYSIWYG editor from <code>min-height: 140px / max-height: 260px</code> to <code>110px / 180px</code>, tightens the sidebar title / body / footer padding from <code>--r2fb-s-6</code> to <code>--r2fb-s-5</code>, and shrinks the body gap from <code>--r2fb-s-4</code> to <code>--r2fb-s-3</code>. Net effect on a 1366×768 laptop or a landscape tablet: the full form (name + comment + attachments + upload drop-zone) fits without any scroll in the common case; when it does scroll, the beefed-up scrollbar makes it obvious. A secondary <code>@media (max-height: 520px)</code> block further shrinks the WYSIWYG to <code>80px / 140px</code> and drops the sidebar title font down one step, for landscape-phone cases.</li><li><strong>Files modified:</strong> <code>assets/css/r2fb-widget.css</code> only. PHP, JS, REST, and data schema are untouched. Existing open editor sessions pick up the new styles on the next reload.</li><li><strong>Rollback:</strong> ZIP-level only — overwrite with <code>archives/r2-site-feedback-1.9.4.zip</code> or <code>archives/r2-site-feedback-1.9.0.zip</code>. No partial rollback to 2.1.0 (same-day internal build, not promoted to <code>archives/</code>).</li></ul><h4>2.1.0</h4><p>Minor version bump combining three tightly-coupled changes: closing the final non-admin-sees-admin-ticket leak, retiring the legacy v1 UI bundle, and styling the previously-unstyled WP dashboard widget in v2. Rollback surface simplifies — the in-plugin <em>Settings → Advanced → Interface Version</em> kill-switch and the <code>assets/{css,js}/legacy/</code> + <code>admin/{css,js}/legacy/</code> frozen bundles are gone; ZIP-level rollback to <code>archives/r2-site-feedback-1.9.4.zip</code> or <code>archives/r2-site-feedback-1.9.0.zip</code> remains the single escape hatch. No REST / option / CPT / data-model changes; existing feedback, screenshots, replies, notes, and API keys are preserved across upgrade and downgrade.</p><ul><li><strong>Security: every non-admin feedback query now AND-filters on a new <code>_r2fb_author_is_admin</code> meta flag.</strong> 2.0.4 / 2.0.6 hardened non-admin scoping to <em>email-only</em> across <code>R2FB_REST::get_feedback_list()</code>, <code>current_client_owns_post()</code>, and the <code>[r2fb_portal]</code> shortcode — but email-only cannot distinguish &ldquo;a ticket I submitted&rdquo; from &ldquo;an admin submitted a ticket with an email value that happens to match my cookie&rdquo; (shared test inboxes, admin's WP-profile email overlap, stale cookies set while browsing as admin, etc.). Reported by the site owner as &ldquo;as regular user, i still see the comment admin left.&rdquo; Fixed by: (a) registering a new CPT meta key <code>_r2fb_author_is_admin</code> (<code>'0'</code> or <code>'1'</code>); (b) stamping the flag at ticket-create time in <code>R2FB_REST::create_feedback()</code> via <code>current_user_can('manage_options')</code>; (c) short-circuiting <code>current_client_owns_post()</code> to return <code>false</code> for any post whose flag is <code>'1'</code>, regardless of email match; (d) inserting a new <code>hide_admin_authored_clause()</code> helper into the non-admin branch of <code>get_feedback_list()</code> so the <code>meta_query</code> now requires <code>_r2fb_client_email</code> exact match AND <code>_r2fb_author_is_admin = '0'</code>; (e) mirroring the same AND-clause in <code>class-r2fb-portal.php::render_portal()</code>. Because pre-2.1.0 rows have no meta at all, a synchronous <code>r2fb_backfill_author_is_admin()</code> migration walks every <code>r2_feedback</code> post on activation and on <code>plugins_loaded</code> priority 5 (before REST init at priority 10) and stamps the flag based on the stored author's current capabilities — <code>post_author = 0</code> (anonymous) → <code>'0'</code>; author has <code>manage_options</code> → <code>'1'</code>; otherwise <code>'0'</code>. Idempotent: skips any row already carrying <code>'0'</code> or <code>'1'</code>. Migration is gated by <code>R2FB_VERSION</code> vs the new <code>r2fb_db_version</code> option, so it runs once per upgrade and is a no-op on steady-state.</li><li><strong>Removed: legacy v1 UI bundle and the <code>r2fb_ui_version</code> kill-switch.</strong> Per user request to simplify the codebase and focus privacy work on a single surface. Deleted: <code>assets/css/legacy/</code> (1.9.4 <code>r2fb-widget.css</code>, <code>r2fb-portal-standalone.css</code>), <code>assets/js/legacy/</code> (1.9.4 <code>r2fb-widget.js</code>, <code>r2fb-portal-app.js</code>), <code>admin/css/legacy/</code> (1.9.4 <code>r2fb-admin.css</code>), <code>admin/js/legacy/</code> (1.9.4 <code>r2fb-admin.js</code>). <code>class-r2fb-widget-loader.php</code> and <code>class-r2fb-admin.php</code> collapse their per-request &ldquo;v1 or v2?&rdquo; branches — the v2 token→base→components→widget/admin cascade is now unconditionally enqueued. <code>R2FB_Admin::get_ui_version()</code> and <code>resolve_admin_assets()</code> methods are gone; <code>wp_localize_script</code> no longer ships the <code>uiVersion</code> key. <code>admin/views/page-settings.php</code> drops the entire <em>Advanced → Interface Version</em> dropdown; <code>save_settings()</code> drops the matching option-save handler. On upgrade from any &lt;2.1.0 install, <code>r2fb_maybe_upgrade()</code> deletes the orphan <code>r2fb_ui_version</code> option from <code>wp_options</code>. ZIP-level rollback remains supported via the preserved <code>archives/r2-site-feedback-1.9.4.zip</code> / <code>archives/r2-site-feedback-1.9.0.zip</code>.</li><li><strong>Fixed: WP dashboard widget was unstyled in v2.</strong> Same scoping-class pattern as the 2.0.1 admin drawer and 2.0.2 widget-surface fixes. <code>wp_add_dashboard_widget()</code> renders into WP core's <code>#dashboard-widgets</code> which sits outside any <code>.wrap.r2fb-v2</code> ancestor, so every <code>.r2fb-v2 .r2fb-dash-*</code> rule silently fell through to bare wp-admin defaults. Fixed by wrapping the render output in a <code>&lt;div class=&quot;r2fb-v2 r2fb-dashboard-widget&quot;&gt;</code> root and rewriting the admin CSS to scope under a compound <code>.r2fb-v2.r2fb-dashboard-widget</code> selector. Rewrote the markup so the three status counts (Open / In progress / Resolved) are individual clickable anchor cards that deep-link into pre-filtered list views (<code>admin.php?page=r2-feedback&amp;status=open</code>, etc.), each with a hover-lift <code>translateY(-1px)</code> + tinted focus ring driven by existing tokens (<code>--r2fb-warning-soft</code> / <code>--r2fb-info-soft</code> / <code>--r2fb-success-soft</code> / <code>--r2fb-shadow-2</code> / <code>--r2fb-shadow-focus</code>). Added a summary line (&ldquo;<em>N unresolved out of N total feedback items.</em>&rdquo;) and a friendlier empty state for fresh installs.</li><li><strong>Files modified:</strong> <code>r2-site-feedback.php</code> (version header + constant + activation defaults cleanup + new <code>r2fb_maybe_upgrade()</code> + <code>r2fb_backfill_author_is_admin()</code>), <code>includes/class-r2fb-cpt.php</code> (register new meta), <code>includes/class-r2fb-rest.php</code> (stamp flag at create-time, hide-admin clause helper, <code>current_client_owns_post()</code> short-circuit, list-query AND-filter), <code>includes/class-r2fb-portal.php</code> (list-query AND-filter), <code>includes/class-r2fb-widget-loader.php</code> (drop legacy branch), <code>includes/class-r2fb-admin.php</code> (drop legacy branch + rewrite <code>render_dashboard_widget()</code>), <code>admin/views/page-settings.php</code> (drop Advanced section), <code>admin/css/r2fb-admin.css</code> (dashboard widget scoping + styles).</li><li><strong>Files removed:</strong> <code>assets/css/legacy/r2fb-widget.css</code>, <code>assets/css/legacy/r2fb-portal-standalone.css</code>, <code>assets/js/legacy/r2fb-widget.js</code>, <code>assets/js/legacy/r2fb-portal-app.js</code>, <code>admin/css/legacy/r2fb-admin.css</code>, <code>admin/js/legacy/r2fb-admin.js</code>.</li><li><strong>Rollback:</strong> ZIP-level only in 2.1.0 — the in-plugin <em>Advanced → Interface Version</em> kill-switch is gone. Restore <code>archives/r2-site-feedback-1.9.4.zip</code> (last 1.9.x) or <code>archives/r2-site-feedback-1.9.0.zip</code> (pre-v2 baseline) and WP will accept the downgrade. All 2.1.0 privacy meta is ignored by pre-2.1.0 code; feedback rows stay intact and the email-OR-name matching leak returns (this is the expected pre-2.0.4 behaviour).</li></ul><h4>2.0.6</h4><p>Follow-up privacy hardening to 2.0.4. 2.0.4 closed the name-OR-email leak in <code>current_client_owns_post()</code> and the <code>own_feedback_only = ON</code> branch of <code>get_feedback_list()</code>, but field-testing surfaced three remaining paths where a regular visitor with an email cookie could still see admin-created tickets. Reported by the site owner as &ldquo;Privacy is still not good, when on feedback page, i can still see new tickets from admin as regular user who used email.&rdquo; No data-model or API-surface changes.</p><ul><li><strong>Security: legacy <code>[r2fb_portal]</code> shortcode now email-only for non-admins.</strong> <code>includes/class-r2fb-portal.php::render_portal()</code> was still running the pre-2.0.4 OR-based <code>meta_query</code> (<code>_r2fb_client_email</code> OR <code>_r2fb_client_name</code>), had no admin bypass, and gated the query only on &ldquo;email or name present&rdquo;. A non-admin whose cookie name matched an admin's stored <code>_r2fb_client_name</code> — or the widget default that every anonymous visitor shares — could see admin-filed tickets. The shortcode now (a) resolves <code>is_admin = current_user_can('manage_options')</code> and returns the unscoped list for admins, (b) requires a verifiable <code>client_email</code> for non-admins — empty returns an empty view with a friendly prompt, (c) hard-scopes the <code>meta_query</code> to <code>_r2fb_client_email</code> exact match (case-insensitive via MySQL's default <code>utf8mb4_*_ci</code> collation), and (d) keeps <code>client_name</code> <em>only</em> for the header greeting, never in the query. Matches the <code>R2FB_REST::get_feedback_list()</code> contract from 2.0.4.</li><li><strong>Security: <code>R2FB_REST::get_feedback_list()</code> else branch dropped the <code>client_name</code> OR fallback.</strong> The <code>own_feedback_only = ON</code> path was already email-only in 2.0.4, but the <code>own_feedback_only = OFF</code> branch still accepted <code>client_email || client_name</code> from the request and ran an OR <code>meta_query</code> — the same leak pattern, just gated by a different option. Non-admins in this branch now always resolve their identity server-side via <code>current_client_identity()</code>, drop any client-sent name param, and get an empty list when no email is resolvable. Admins remain unscoped.</li><li><strong>Security: <code>R2FB_REST::get_feedback()</code>, <code>add_reply()</code>, and <code>upload_attachments()</code> ownership checks are now unconditional.</strong> Pre-2.0.6 the <code>current_client_owns_post()</code> check was gated with <code>'1' === get_option( 'r2fb_own_feedback_only', '1' )</code> — meaning a non-admin with an email cookie could fetch, reply to, or upload to any feedback post by ID whenever that option was off. That option governs <em>list-view presentation</em> (breadth), not <em>per-ID access control</em> (the 2.0.2 help-text rewrite spells out the distinction). Ownership now applies to every non-admin request regardless of option state. 404 (not 403) is returned on failure so ID enumeration cannot probe for other users' records.</li><li><strong>Security / hygiene: portal SPA stops sending the <code>client_name</code> query param.</strong> <code>assets/js/r2fb-portal-app.js::loadFeedback()</code> previously pushed both <code>client_email</code> and <code>client_name</code> into the REST query string. The server now ignores <code>client_name</code> entirely, but shipping dead data was a vector risk — a future dev could accidentally re-introduce the name-based match on the server side. Only <code>client_email</code> is forwarded, as a hint; authoritative identity resolution stays server-side.</li><li><strong>Changed:</strong> in-code docblocks on <code>render_portal()</code>, <code>get_feedback()</code>, <code>add_reply()</code>, and <code>upload_attachments()</code> carry a 2.0.6 note documenting the &ldquo;email-only is strictly stronger than name-OR-email&rdquo; argument so future edits don't re-add the name fallback.</li><li><strong>Files modified:</strong> <code>includes/class-r2fb-portal.php</code>, <code>includes/class-r2fb-rest.php</code>, <code>assets/js/r2fb-portal-app.js</code>.</li><li><strong>Rollback:</strong> <em>Settings → Advanced → Interface Version → v1</em> restores the frozen 1.9.4 CSS/JS bundle but keeps 2.0.x privacy fixes (CSS is only presentation, PHP privacy stays in place). For a full rollback to pre-v2 privacy behaviour, upload <code>archives/r2-site-feedback-1.9.0.zip</code> — last build that accepted <code>name || email</code> matching (leak returns). Partial rollback to 2.0.5 re-introduces the legacy-shortcode and <code>own_feedback_only = OFF</code> branch leaks.</li></ul><h4>2.0.5</h4><p>UX follow-up to 2.0.4. The required-email validation added in 2.0.4 surfaced its error via <code>R2FB_UI.toast()</code>, which flashed in the bottom-right toast layer (visually disconnected from the email field) and auto-dismissed after ~4 seconds — user report: “jumping all around and hiding quickly”. No data-model, API, or option changes.</p><ul><li><strong>Changed: inline error for required email.</strong> Replaced the toast with a stable <code>&lt;small class=\"r2fb-field-error\" role=\"alert\" aria-live=\"polite\"&gt;</code> anchored directly under the email input inside its <code>.r2fb-editor-form-group</code>. The message stays put until the user edits the field — an <code>input</code> listener on the email field clears the error text, resets the red border, and removes <code>aria-invalid</code>. No auto-hide timer.</li><li><strong>Changed: red-bordered input state uses design tokens.</strong> New <code>.r2fb-editor-input.r2fb-has-error</code> and <code>.r2fb-has-error:focus</code> rules use <code>--r2fb-danger</code> + <code>--r2fb-danger-soft</code>, matching the rest of the v2 palette instead of the raw <code>#dc3232</code> fallback.</li><li><strong>Changed: shake + focus on submit failure.</strong> A short (420 ms) single shake animation plays once per submit attempt to draw attention without shifting layout, and honours the global <code>prefers-reduced-motion</code> override. Focus moves to the email input so keyboard users land on the field they need to correct and screen readers re-announce the <code>aria-describedby</code>-linked error.</li><li><strong>Files modified:</strong> <code>assets/js/r2fb-widget.js</code>, <code>assets/css/r2fb-widget.css</code>.</li><li><strong>Rollback:</strong> Settings → Appearance → <em>Use legacy UI</em> (v1) restores the 1.9.4 editor (which never required email). ZIP-level: <code>archives/r2-site-feedback-1.9.4.zip</code> or <code>archives/r2-site-feedback-1.9.0.zip</code>. 2.0.4 was a same-day internal build and was not promoted to archives.</li></ul><h4>2.0.4</h4><p>Bundled patch covering five live-site defects reported on <code>projects.rise2.studio/parra</code> after 2.0.3 was deployed. No data-model or API-surface changes; existing feedback, options and screenshots are preserved.</p><ul><li><strong>Fixed: host WAF / REST rate limit blocking the screenshot capture.</strong> The frontend widget's FAB fires <code>requestScreenshot</code> directly on every click, and the REST screenshot proxy is capped at 10 requests per IP per 5 minutes (plus host WAFs often cap lower). Rapid or impatient double-clicks — which happened routinely because the loading overlay takes ~300ms to appear — multiplied the request count and tripped the rate limit, surfacing as a silent \"server keeps blocking me\" with no captured screenshot. <code>assets/js/r2fb-widget.js</code> now guards <code>requestScreenshot</code> with a <code>screenshotInFlight</code> boolean plus <code>state.mode === 'loading' || 'editor'</code> early-return; the guard is released in the success, error, and \"screenshot service not configured\" branches. Net effect: a user clicking the FAB ten times in a row now fires exactly one capture.</li><li><strong>Fixed: admin list-view bulk-select master checkbox did nothing.</strong> The <code>.r2fb-table</code> thead in <code>admin/views/page-feedback-list.php</code> is a custom table (not <code>WP_List_Table</code>) so it never picked up WordPress's native <code>#cb-select-all-1</code> jQuery wiring. Clicking the select-all box left every row unchecked. Fixed by giving the master checkbox <code>id=\"r2fb-cb-all\"</code> and wiring a new <code>bindBulkSelect()</code> block in <code>admin/js/r2fb-admin.js</code> that (a) ticks/unticks every <code>input[name=\"r2fb_selected[]\"]</code> within the enclosing form when the master toggles, and (b) recomputes the master state (checked / unchecked / indeterminate) on every per-row change — matching the native WP list-table behaviour users already expect.</li><li><strong>Fixed: admin kanban drawer — screenshot completely missing.</strong> Opening a card's drawer rendered title, conversation and tech panels correctly but the screenshot area was blank. Root cause: the drawer screenshot is a native <code>&lt;button&gt;</code> (so <code>Enter</code> / <code>Space</code> open the lightbox), and the <code>.r2fb-drawer__screenshot</code> CSS rule set only <code>border-radius / overflow: hidden / background / cursor: zoom-in</code>. UA defaults on <code>&lt;button&gt;</code> (<code>inline-block</code>, buttonface background, <code>1px 6px</code> padding, <code>2px outset</code> border, middle baseline) were collapsing the inner <code>&lt;img&gt;</code> to zero visible height. <code>admin/css/r2fb-admin.css</code> now resets the button — <code>display: block; width: 100%; padding: 0; margin: 0; border: 0; font: inherit; color: inherit; text-align: left</code> — and enforces <code>max-width: 100%; height: auto</code> on the child <code>&lt;img&gt;</code>. Keyboard/lightbox semantics unchanged.</li><li><strong>Fixed: \"vor 2 Stunden\" for a just-submitted comment (relative-time off by timezone offset).</strong> On WP installs configured to UTC, <code>$post-&gt;post_date</code> equals <code>$post-&gt;post_date_gmt</code>, but the REST layer was returning that UTC value on the <code>date</code> field. The shared <code>R2FB_UI.time()</code> helper fed it into <code>new Date(\"YYYY-MM-DD HH:MM:SS\")</code> which modern Chromium parses as <strong>local</strong> time — so a UTC value got re-interpreted as browser-local, adding a CEST two-hour offset to every relative timestamp. Two-part fix: (1) <code>class-r2fb-rest.php</code> <code>format_feedback()</code>, <code>get_replies()</code> and <code>get_notes()</code> now return <code>$post-&gt;post_date_gmt</code> / <code>$comment-&gt;comment_date_gmt</code> on the canonical <code>date</code> field (with <code>date_gmt</code> preserved for any consumer that was relying on the explicit field). (2) <code>assets/js/r2fb-ui.js::time()</code> also defensively normalises any MySQL-format datetime string (<code>YYYY-MM-DD[ T]HH:MM:SS</code>) to a fully-qualified ISO UTC (<code>…THH:MM:SSZ</code>) before handing off to <code>new Date()</code>.</li><li><strong>Fixed / Privacy: anonymous clients could see one another's tickets.</strong> Since 1.9.3 the \"Own feedback only\" mode matched a non-admin client's identity on <strong>email OR name</strong> — but the widget ships a single <code>r2fb_default_client_name</code> that every anonymous visitor shares, and the name field is freely editable in the submit form. Two different anonymous visitors on the same site therefore saw each other's tickets in the browse panel and the portal, and could reply on each other's threads. <code>R2FB_REST::current_client_owns_post()</code> and <code>R2FB_REST::get_feedback_list()</code> now scope <strong>email-only</strong>. To make this workable for anonymous flows, <code>assets/js/r2fb-widget.js</code> promotes the email field from optional to required whenever <code>ownFeedbackOnly</code> is on and the visitor is not logged in — placeholder becomes <code>Email *</code>, <code>aria-required=\"true\"</code>, and <code>submitFromEditor()</code> validates a basic <code>x@y.z</code> pattern before enabling submission. A new <code>ownFeedbackOnly</code> flag is localised to <code>r2fbConfig</code> by <code>class-r2fb-widget-loader.php</code>. The settings-page help text under <em>Client Privacy → Scope to Client</em> carries a 2.0.4 note explaining that scoping is now email-only and recommending <em>Access mode = Logged-in users only</em> for the strongest guarantee.</li><li><strong>Rollback:</strong> restore <code>archives/r2-site-feedback-1.9.0.zip</code> (pre-v2 baseline) and set <code>version=1.9.0</code> in <code>info.json</code>; all 2.0.x data — feedback, meta, screenshots, API keys, options, replies, notes — re-renders correctly under 1.9.0. Or flip <em>Settings → Advanced → Interface Version</em> to <code>v1</code> for a style-only fallback while keeping 2.0.4's PHP fixes.</li></ul><h4>2.0.3</h4><ul><li><strong>Fixed: Frontend widget FAB rendered at ~82 px font-size on fluid-typography themes (follow-up to 2.0.2 on <code>dev.vrep.de</code>).</strong> After 2.0.2's scoping fix every widget rule correctly matched body-appended roots, but live inspection on one site still showed the \"Leave Feedback\" button as a full-width red banner. The v2 stylesheets <em>were</em> applying (position, height, padding, border-radius, z-index, design tokens all correct on the FAB) but the computed <code>font-size</code> came out as <code>82.3168px</code> and the <code>background</code> as an inline <code>rgb(234, 11, 11)</code>. Two distinct root causes: <strong>(1)</strong> The VREP theme sets <code>html { font-size: 101.313px }</code> (fluid <code>vw</code>-based root font-size, common for theme-wide fluid typography). All <code>--r2fb-fs-*</code> tokens were authored in <code>rem</code>, so <code>.8125rem × 101.313 ≈ 82.32px</code> — exactly the observed value. Fixed by authoring font-size tokens in <strong>px</strong> (<code>--r2fb-fs-sm: 13px</code>, etc.) in <code>assets/css/r2fb-tokens.css</code>; px still scales with browser zoom (browsers multiply) so accessibility is unchanged, and it's immune to fluid-root hosts. A contract comment was added to <code>r2fb-widget.css</code> so edits don't switch back to rem without first anchoring a font-size on every body-appended root. <strong>(2)</strong> The widget JS (<code>assets/js/r2fb-widget.js</code>) was hard-coding the admin-configured primary color as an <em>inline</em> <code>style.background</code> on the FAB, password gate submit, annotation-editor submit and ticket-reply buttons. Inline styles beat every <code>:hover</code>, focus-ring and <code>var(--r2fb-accent)</code> rule, so a configured red drowned out v2's coral hover. Fixed: the widget now forwards the admin color via <code>--r2fb-accent</code> CSS custom property (plus a JS-derived 12% darker <code>--r2fb-accent-hover</code>) on each root. The CSS owns <code>background: var(--r2fb-accent)</code> and <code>:hover { background: var(--r2fb-accent-hover) }</code>, so hover / focus / chip tints cascade from one source. New <code>accentStyle(extra)</code> helper built the style object at four call sites; the <code>el()</code> helper routes <code>--*</code> keys through <code>setProperty()</code> (since <code>Object.assign</code> is a no-op for custom properties). New <code>darkenHex(hex, amount)</code> utility parses <code>#rgb</code> / <code>#rrggbb</code> and returns a darker hex; safe to reuse.</li><li><strong>Rollback:</strong> 2.0.3 only touches the widget JS + token CSS — no data / REST / option changes. Revert by replacing with <code>r2-site-feedback-2.0.2.zip</code>, by restoring <code>archives/r2-site-feedback-1.9.0.zip</code> (pre-v2 baseline), or by flipping <em>Settings → Advanced → Interface Version</em> to <code>v1</code> to fall back to the frozen 1.9.4 bundle.</li></ul><h4>2.0.2</h4><ul><li><strong>Fixed: Frontend widget rendered unstyled on some themes.</strong> Reported on <code>dev.vrep.de</code> — with v2 enabled the \"Leave Feedback\" button appeared as a giant red unstyled block instead of the coral pill FAB. Same class of bug that 2.0.1 fixed for the admin kanban drawer, replayed across the whole widget surface. Seven root-level widget elements (FAB, browse badge, password gate, loading overlay, editor overlay, list panel, legacy toast) are appended directly to <code>document.body</code> and carry <code>.r2fb-v2</code> on themselves — but <code>r2fb-widget.css</code> was targeting them exclusively with descendant selectors (<code>.r2fb-v2 .r2fb-fab</code>), which require <code>.r2fb-v2</code> to be an ancestor. Since <code>&lt;body&gt;</code> isn't <code>.r2fb-v2</code>, zero rules matched and each element fell back to the host theme's bare styling. Fixed by pairing every root-level rule with a compound form (<code>.r2fb-v2 .r2fb-fab, .r2fb-v2.r2fb-fab { … }</code>) across FAB, browse badge, menu, password gate, loading / editor overlays, list panel, legacy toast, and all their <code>:hover</code> / <code>:active</code> / positional / media-query / reduced-motion variants. Children of these roots keep working via descendant selectors because the root itself is their <code>.r2fb-v2</code> ancestor. Added a scoping-contract docblock at the top of the file so future edits don't regress.</li><li><strong>Changed: Client Privacy settings help text rewritten.</strong> The two privacy toggles, <em>Hide Conversation</em> and <em>Scope to Client</em>, had descriptions that felt almost identical and users couldn't tell them apart. The real distinction is crisp but was buried: <strong>Scope to Client</strong> controls <em>breadth</em> (which tickets exist to the client at all — list-level filter), <strong>Hide Conversation</strong> controls <em>depth</em> (how much of each ticket the client can read / reply to — detail-level filter). Both toggles now have an italic sub-label under the main label stating which axis they control; the section header includes a short cheat-sheet with the same framing; help paragraphs were rewritten to stop restating each other. Toggles reordered: Scope to Client first (breadth), Hide Conversation second (depth) — matches how they evaluate server-side. Option keys, defaults, and server-side behaviour are unchanged. Documentation / labelling fix only.</li><li><strong>New admin CSS utility:</strong> <code>.r2fb-form-sublabel</code> — small italic muted line stacked under a form label, available for any settings row where the label alone is ambiguous.</li><li><strong>Rollback:</strong> 2.0.2 only touches CSS selectors and help-text wording — no data, REST, option, or JS behaviour changes from 2.0.1. Revert via <code>archives/r2-site-feedback-1.9.0.zip</code> (pre-v2.0.0 baseline) or flip <em>Settings → Advanced → Interface Version</em> to <code>v1</code> to fall back to the frozen 1.9.4 bundle.</li></ul><h4>2.0.1</h4><ul><li><strong>Fixed: Kanban card click / detail drawer in wp-admin.</strong> On 2.0.0 the board view felt dead — clicking a card produced no visible reaction. Root cause: the detail drawer, its overlay, and the screenshot lightbox are appended directly to <code>document.body</code> (so they escape the <code>.wrap.r2fb-v2</code> stacking context) and carry <code>.r2fb-v2</code> on themselves, but the admin stylesheet targeted them with descendant selectors like <code>.r2fb-v2 .r2fb-drawer</code>, which require <code>.r2fb-v2</code> to be an ancestor, not the same element. The drawer was created correctly, it just received zero positioning / z-index / background / transform, so it lived invisibly off-screen. Added compound selectors (<code>.r2fb-v2.r2fb-drawer</code>, <code>.r2fb-drawer.r2fb-v2</code>, same for <code>.r2fb-drawer-overlay</code>, <code>.r2fb-drawer.is-open</code>, <code>.r2fb-lightbox</code>) in <code>admin/css/r2fb-admin.css</code>.</li><li><strong>Fixed: Switch buttons in the settings page were rendered as raw native checkboxes.</strong> The shared <code>.r2fb-switch</code> component expected a class-prefixed <code>&lt;input class=\"r2fb-switch__input\"&gt;</code> and drove the thumb via <code>.r2fb-switch__track::before</code>, but every settings-page switch ships the native <code>&lt;input type=\"checkbox\"&gt;</code> (no class) with a real <code>&lt;span class=\"r2fb-switch__thumb\"&gt;</code> child. Result: the input wasn't hidden (visible native checkbox next to every switch), and the checked-state / thumb-slide rules never matched. Rewrote the switch CSS in <code>assets/css/r2fb-components.css</code> to target <code>input[type=\"checkbox\"]</code> directly and drive the real <code>.r2fb-switch__thumb</code> span; kept old <code>.r2fb-switch__input</code> selectors as aliases; added disabled + focus-visible styles.</li><li><strong>Changed: Settings page — options regrouped and reordered.</strong> The previous page mixed unrelated concerns (screenshot-service URL + API key lived under Portal, basic-auth for the service sat under Advanced, the v1/v2 kill-switch hid under Appearance, and the widget's Default Client Name pre-fill was grouped with Access). New order: <strong>Access → Appearance → Portal → Client Privacy → Notifications → Screenshot Service → Advanced</strong>. Moved Default Client Name → Appearance; Screenshot Service URL + API Key → new Screenshot Service section; Timeout + Max/hour + Basic Auth → Screenshot Service; Interface Version kill-switch → Advanced. Advanced now contains only the kill-switch. No option keys or form field names changed — nothing to migrate.</li></ul><h4>2.0.0</h4><ul><li><strong>Complete UI refresh — major visual update.</strong> New coral &amp; stone palette (<code>#E8624A</code> on <code>#FAF7F2</code>), Inter sans + Lora display serif, and a shared design-token system across all three surfaces: the frontend widget (FAB, browse badge, annotation editor, sidebar form, list panel), the standalone portal at <code>/rise2-feedback</code> (sticky header with search + filter tabs, two-pane list/detail on ≥960px, mobile push-over detail, skeleton loaders, empty states), and the WordPress admin (kanban board + list toggle, detail drawer with screenshot lightbox, settings page grouped by concern).</li><li><strong>Shared component runtime</strong> <code>R2FB_UI</code> (<code>assets/js/r2fb-ui.js</code>) — <code>modal()</code>, <code>confirm()</code>, <code>toast()</code>, <code>trapFocus()</code>, <code>time()</code>. Every native <code>alert()</code> / <code>confirm()</code> replaced across widget, portal, and admin.</li><li><strong>CSS token chain</strong> — <code>r2fb-tokens.css</code> → <code>r2fb-base.css</code> → <code>r2fb-components.css</code> → surface stylesheets. All v2 styles scoped to <code>.r2fb-v2</code> so host themes and wp-admin remain untouched.</li><li><strong>Interface-version kill-switch</strong> — new <code>r2fb_ui_version</code> option (default <code>'v2'</code>; flip to <code>'v1'</code> to restore the frozen 1.9.4 bundle from <code>assets/{css,js}/legacy/</code> and <code>admin/{css,js}/legacy/</code>). Configurable in <em>Settings → Appearance</em>. Applies to the widget + admin; the standalone portal is v2-only.</li><li><strong>Portal features</strong> — hash deep-linking <code>#/item/{id}</code> with <code>hashchange</code> + <code>popstate</code> wiring, debounced (150 ms) client-side search across <code>comment_text + page_path + client_name</code>, skeleton cards during initial fetch, optimistic reply submission with revert on failure, priority-dot indicator per card.</li><li><strong>Admin kanban</strong> — drag-and-drop between Open / In progress / Resolved columns (HTML5 drag API, no external lib), within-column reordering, keyboard-accessible status <code>&lt;select&gt;</code> on every card as the a11y fallback, <code>Esc</code> closes the detail drawer, <code>/</code> focuses search.</li><li><strong>Full ARIA pass</strong> — every dialog gets <code>role=\"dialog\"</code> + <code>aria-modal</code> + <code>aria-labelledby</code>; every icon-only button has <code>aria-label</code>; focus traps on open + focus restore on close; portal tab bar follows the WAI-ARIA tab pattern with Arrow/Home/End keys; live regions for toasts and async list updates.</li><li><strong>Skip link</strong> on the standalone portal pointing at <code>#r2fb-main</code>.</li><li><strong>Reduced-motion</strong> support — every fade-in / slide / shake is gated behind <code>@media (prefers-reduced-motion: no-preference)</code>.</li><li><strong>Fixed: Sidebar scrollbar now truly reliable.</strong> The 1.9.2 / 1.9.3 flex-column approach still failed on some browser-zoom / short-viewport combinations — on a real user report the attachments area became unreachable. Replaced with CSS Grid (<code>grid-template-rows: auto 1fr auto</code>) + <code>min-height: 0</code> on the body. The <code>1fr</code> track is always computed as the exact remaining space after the <code>auto</code> tracks — no content-size fallback loop. Attachments are reachable at any viewport size / zoom level.</li><li><strong>Rollback:</strong> the v1.9.4 bundle is preserved at <code>r2-commenter/archives/r2-site-feedback-1.9.4.zip</code>. To revert, restore that archive and set <code>version=1.9.4</code> in <code>info.json</code>; or, for a style-only rollback, flip the in-plugin <code>r2fb_ui_version</code> option to <code>'v1'</code>.</li><li><strong>No data changes.</strong> REST endpoints, hook names, option keys, database schema, and CPT structure are unchanged. Existing installations pick up the v2 UI automatically on upgrade; rollback preserves all feedback data.</li></ul><h4>1.9.4</h4><ul><li><strong>Privacy: Reply count no longer leaks to clients when the conversation is hidden.</strong> 1.9.2 / 1.9.3 hid the replies and notes themselves, but the REST payload still returned <code>reply_count</code> — so a client saw <code>💬 2</code> on their own card and could infer that someone had replied even though the thread was sealed. <code>R2FB_REST::format_feedback()</code> now zeros the count for non-admin viewers whenever <code>r2fb_hide_client_conversation</code> is on (default). The legacy <code>[r2fb_portal]</code> shortcode got the same guard. Admins still see the true count everywhere.</li></ul><h4>1.9.3</h4><ul><li><strong>Fixed: Sidebar scrollbar now reliably appears at zoom / low-resolution.</strong> The 1.9.2 flex-column rework used <code>flex: 1 1 auto</code> on the scrollable body, which on some browser-zoom / short-viewport combinations never triggered overflow. Switched to <code>flex: 1 1 0</code> + explicit <code>height: 100%</code> on the sidebar root so the body always sizes from available space and its custom scrollbar triggers every time.</li><li><strong>Privacy: Clients now see only their own feedback.</strong> New <code>r2fb_own_feedback_only</code> option (default ON). The widget browse panel and the standalone portal used to return all feedback submitted on the same page; now every client-facing REST path enforces ownership server-side (list / single-get / reply / attachments). Non-owners get a 404, not 403, so the existence of other clients' records isn't leaked. Identity comes from the logged-in WP user or the <code>r2fb_client_*</code> cookies. Admins are exempt.</li><li><strong>New \"Client Privacy\" setting</strong> (<em>Settings → Client Privacy</em>) with a plain-English description of the identity-resolution behaviour.</li></ul><h4>1.9.2</h4><ul><li><strong>Fixed: Portal client visibility — chat now truly hidden.</strong> The 1.9.1 \"Email Clients OFF\" toggle only silenced emails but clients could still read admin replies and post new replies in the portal. Corrected: both the REST payload and the portal SPA hide the conversation from non-admins. <code>POST &hellip;/reply</code> now returns 403 for non-admins when the new option is on. The client sees a short \"your feedback was received\" confirmation panel instead of the full conversation, so they can still verify the submission.</li><li><strong>Fixed: Sidebar no longer overlaps fields on low-resolution screens.</strong> The 1.9.1 sticky submit button could visually cover attachments / comment on short viewports. Sidebar is now a three-row flex column: fixed title, internally scrollable body (custom thin scrollbar), fixed footer hosting only the submit button. Every field is reachable regardless of viewport height or zoom.</li><li><strong>New: \"Hide conversation from clients in the portal\" setting</strong> (<em>Settings → Portal Conversation</em>, default ON). Clients see only their own submitted tickets — not admin replies, not the reply composer. Admins always see and interact with the full conversation. Pairs with the 1.9.1 \"Email Clients\" toggle for a full \"submission-only\" client mode.</li><li>Adds <code>r2fb_hide_client_conversation</code> option; added to activation defaults so fresh installs start in the safer hidden-by-default posture.</li><li>New <code>.r2fb-editor-sidebar-body</code> / <code>.r2fb-editor-sidebar-footer</code> structural classes — applied to both the current stylesheet and the dormant v2 bundle.</li></ul><h4>1.9.1</h4><ul><li><strong>Fixed: Submit button always visible in the feedback sidebar</strong> — the submit button is now pinned to the bottom (<code>position: sticky</code>), so it stays reachable on any desktop viewport or zoom level. Previously it could fall below the visible area, forcing users to tab to it.</li><li><strong>New: Email Clients toggle</strong> (Settings → Notifications, default OFF). When off, clients never receive any email about activity on their submissions — no reply notifications, no resolution notices, no status-change emails. Clients can still view their own submitted tickets in the portal to confirm the submission was saved; they just don't see replies, status changes, or internal notes.</li><li>Option <code>r2fb_notify_client_enabled</code> gates both <code>on_admin_reply()</code> and <code>on_status_resolved()</code> in the notifications class.</li><li>Groundwork for the upcoming v2.0.0 UI refresh is dormant behind a kill-switch (<code>r2fb_ui_version</code>) — no visual change ships in 1.9.1.</li></ul><h4>1.9.0</h4><ul><li>New: <strong>White-label portal URL</strong> — Portal URL Slug setting lets agencies rename the <code>/rise2-feedback</code> endpoint to any custom path without editing code</li><li>Slug validation: 2–64 characters, lowercase + hyphens, reserved WordPress paths rejected</li><li>Rewrite rules flush automatically on save — no manual Permalinks visit needed</li><li>Portal Viewing Mode description now shows the live portal URL as a clickable link</li><li>Changed: portal-standalone.php and widget-loader.php now resolve the portal URL dynamically via R2FB_Endpoint::get_slug()</li></ul><h4>1.8.5</h4><ul><li>Security: Removed SVG from allowed upload types (stored XSS prevention)</li><li>Security: HTML size limit and validation on screenshot endpoint (SSRF mitigation)</li><li>Security: Rate limiting on screenshot endpoint (10 per 5 min per IP)</li><li>Security: Explicit capability check on bulk action form</li></ul><h4>1.8.4</h4><ul><li>Fixed: WYSIWYG code toggle button now works correctly</li><li>Increased editor height</li></ul><h4>1.8.3</h4><ul><li>New: Separate \"Portal Viewing Mode\" setting — controls who can view /rise2-feedback independently from commenting access</li><li>Admins always have portal access regardless of viewing mode</li></ul><h4>1.8.2</h4><ul><li>Fixed: Portal no longer blocks anonymous visitors when mode is \"everyone\"</li></ul><h4>1.8.1</h4><ul><li>Portal: admins see all feedback, can change status and delete</li><li>Access control enforced on /rise2-feedback endpoint</li><li>Croatian translations for admin portal strings</li></ul><h4>1.8.0</h4><ul><li>FAB clicks directly to screenshot — menu removed</li><li>New browse badge above FAB links to /rise2-feedback portal with open count</li><li>Portal detail opens as sidebar panel instead of replacing list</li><li>All portal strings now translatable (Croatian included)</li></ul><h4>1.7.0</h4><ul><li>UI Modernization: complete visual refresh — SVG toolbar icons, filled color picker, Apple-style inputs, refined spacing</li><li>Dark annotation toolbar with proper SVG icons (pen, arrow, rectangle, text, undo, trash)</li><li>WYSIWYG active state now uses blue fill instead of outline</li></ul><h4>1.6.5</h4><ul><li>Fixed: Custom fonts (woff2) now render correctly in screenshots — relative URL resolution for @font-face during serialization</li></ul><h4>1.6.3</h4><ul><li>Fixed: Toolbar mousedown preventDefault — buttons no longer steal focus, all formatting toggles work reliably</li><li>Fixed: Code toggle works properly — enter/exit code mode, unwrap coded text</li></ul><h4>1.6.2</h4><ul><li>Fixed: Code button works as toggle — click to enter/exit code mode without text selection</li><li>Changed: Editor height doubled for more comfortable writing</li></ul><h4>1.6.1</h4><ul><li>Fixed: WYSIWYG toolbar shows active formatting state, all buttons toggle on/off without text selection</li><li>Changed: Croatian translations — 'Povratne informacije' → 'komentari' consistently across all UI</li></ul><h4>1.6.0</h4><ul><li>New: Standalone feedback portal at /rise2-feedback — independent from theme, no shortcode needed</li><li>New: Self-contained HTML page with own CSS/JS, same access control as widget</li><li>New: Status filter tabs, detail view with conversation and reply form</li><li>New: client_email/client_name REST API filtering for portal</li></ul><h4>1.5.6</h4><ul><li>Fixed: Screenshots now use correct browser engine (Firefox/WebKit) matching client browser instead of always Chromium</li><li>Fixed: Widget sends browser field in screenshot requests</li><li>Fixed: PHP proxy forwards browser field to screenshot service</li></ul><h4>1.5.5</h4><ul><li>Fixed: Root cause of lost newlines — sanitize_text_field() on comment_text meta was stripping all line breaks, switched to sanitize_textarea_field()</li><li>Fixed: Rewrote text extraction to DOM tree walking for reliable Enter/Shift+Enter handling</li></ul><h4>1.5.4</h4><ul><li>Fixed: Rewrote text extraction to DOM tree walking — Enter and Shift+Enter now produce correct newlines</li></ul><h4>1.5.3</h4><ul><li>Fixed: Line breaks render correctly in admin sidebar and widget detail view</li><li>Fixed: Internal Notes restyled to match Conversation section layout</li><li>Fixed: Markdown regex order — code processed first</li></ul><h4>1.5.2</h4><ul><li>New: Slack-style WYSIWYG comment editor with live formatting (Bold, Italic, Strikethrough, Link, Code)</li><li>New: 600-character limit with live countdown</li><li>New: Markdown rendering on admin detail page</li><li>Fixed: FAB button hidden from screenshots</li><li>Fixed: Newlines render correctly in comments</li><li>Removed: Tags/labels system</li><li>Changed: Admin UI deep modernization</li></ul><h4>1.5.0</h4><ul><li>New: Keyboard shortcuts in annotation editor (Escape, Ctrl+Z, tool switching)</li><li>New: Status notification email to client when feedback is resolved</li><li>New: Markdown support in comments (bold, italic, code, links)</li><li>New: Feedback portal page — [r2fb_portal] shortcode for clients</li><li>New: Internal notes — admin-only notes per feedback item</li><li>New: Tags/labels system with default &quot;bug&quot; tag</li><li>Changed: UI modernization — modern design for frontend widget and admin dashboard</li></ul><h4>1.4.1</h4><ul><li>Fixed: Text tool — clicking outside active text box no longer creates a new box, just closes current one</li><li>Added: Text tool — click existing text annotations to re-edit and reposition them</li></ul><h4>1.4.0</h4><ul><li>Added: Email notification to client when admin replies — HTML email with reply text, page link, and CTA button</li><li>Added: Page filter dropdown in admin board view — filter kanban by page path</li><li>Fixed: Path duplication on subdirectory WordPress installations (e.g. /parra/parra/ instead of /parra/)</li></ul><h4>1.3.6</h4><ul><li>Added: Attachments displayed in admin board sidebar (quick view panel)</li></ul><h4>1.3.5</h4><ul><li>Fixed: Attachments not deleted when feedback is deleted from admin dashboard</li><li>Added: before_delete_post hook for reliable file cleanup on any deletion path</li></ul><h4>1.3.4</h4><ul><li>Added: Attachment count badge in feedback list sidebar</li></ul><h4>1.3.3</h4><ul><li>Fixed: File uploads not saving — JS/PHP field name mismatch (attachments[] vs files)</li></ul><h4>1.3.2</h4><ul><li>Fixed: File upload and success toast broken due to variable scoping issue in widget JS</li></ul><h4>1.3.1</h4><ul><li>Added: Self-hosted update checker with Check for updates link on Plugins page</li><li>Added: Success notice after manual update check</li></ul><h4>1.3.0</h4><ul><li>New: File/image upload in feedback comments — multi-upload with drag-and-drop, 20 MB per file, 60 MB total</li><li>New: REST endpoint for uploading attachments (POST /feedback/{id}/attachments)</li><li>New: Attachments displayed on admin detail page and widget detail view</li><li>New: Inline text annotation tool — draggable/resizable text box replaces browser prompt</li><li>Changed: Default drawing tool is now Freehand (pen first in toolbar)</li><li>Attachments auto-deleted when feedback is deleted</li></ul><h4>1.2.0</h4><ul><li>Full Croatian (hr) translation for all frontend widget strings</li><li>All UI strings translatable — WPML compatible</li></ul><h4>1.1.0</h4><ul><li>Multiple notification email addresses support</li><li>Digest notification mode — bundles feedback into a summary email after cooldown period</li><li>Logged-in users: email field hidden, name auto-filled from WP profile</li></ul><h4>1.0.2</h4><ul><li>Fixed blank screenshots caused by double base64 encoding in REST proxy</li></ul><h4>1.0.1</h4><ul><li>Security: Screenshot API key no longer exposed to frontend — proxied through WP REST API</li><li>Security: Basic auth credentials removed from frontend</li><li>Security: Image validation before writing to disk</li><li>Security: REST access mode enforcement, email privacy, password rate limiting</li></ul><h4>1.0.0</h4><ul><li>Initial release with visual feedback widget, DOM serialization screenshots, annotations, admin dashboard</li></ul>"
    }
}