== Older changelog entries ==

TheGridIndex RSS Importer — version history prior to 1.0.70.
The current/recent versions live in readme.txt. This file is the archive
that keeps the plugin directory's readme parser inside its 5,000-char
changelog limit (see the v1.0.66 entry in readme.txt).

= 1.0.69 =
* Hero title now reads "TheGridIndex RSS Importer" — the full plugin display name — instead of the abbreviated "RSS Importer" that v1.0.68 shipped. The abbreviation read as a section heading rather than a plugin title once the eyebrow was removed; the full name makes it unambiguous what the user is looking at and matches the `Plugin Name:` header, the readme title, the credits card, and the WP.org listing. Title font-size tuned from 42px to 36px so the longer 25-character title fits comfortably on one line at typical desktop widths and wraps cleanly at the brand/product boundary on narrower screens.

= 1.0.68 =
* Admin UI polish — two fixes for visible white bleed and an unbalanced hero header reported from QA on the rendered page. **Background fill:** painted the WordPress admin container behind the plugin's wrap (`#wpbody`, `#wpbody-content`, `#wpcontent`) to match the active color mode, with a dedicated `gi-options-host--dark` body class. Previously the wrap div was dark but the WP admin chrome around and beneath it stayed white, producing a stripe of bright white below the card stack on dark mode. The fill is scoped strictly to the importer screen so other plugins' admin pages are untouched. **Hero rebalance:** dropped the "THEGRIDINDEX" eyebrow text above the title. The hero now leads with a single clean title ("RSS Importer") and a small standalone teal accent rule above it — a balanced wordmark with a single focal element instead of two competing text lines. The brand is still surfaced via the page badges, the WP sidebar menu, the readme, and the v-badge.

= 1.0.67 =
* Renamed the plugin to "TheGridIndex RSS Importer" (slug: `thegridindex-rss-importer`, textdomain: `thegridindex-rss-importer`). The WP.org trademark-clearance review flagged the prior name "Grid Index RSS Importer" because it could read as an official plugin for some unrelated "Grid Index" project, and the ownership signals on the submission didn't make affiliation clear. "TheGridIndex" is a coined, single-word brand name owned by Fifth Avenue Photographic and unambiguously this plugin's identity. Added a `Note: About the name` paragraph to the readme description explicitly disambiguating the plugin from any unrelated project. The internal code prefix remains `gip_` — that prefix has been the plugin's canonical project prefix since v1.0.0 and is referenced by the database option key, the cron hook name, the custom database table, the action hooks, and per-post meta. Renaming the internal prefix would require a one-time data migration with meaningful breakage risk and no functional benefit; keeping it stable preserves existing installs cleanly. User-facing strings ("Grid Index RSS Importer" anywhere it appeared) and the cron-schedule labels ("Every N Minutes (Grid Index RSS)") are updated to the new brand. The admin hero header's eyebrow/title pairing was reworked from "The Grid Index" → "Grid RSS" to "TheGridIndex" → "RSS Importer" so the visual hierarchy unambiguously names the plugin rather than the companion theme.

= 1.0.66 =
* Full pass against the WordPress.org Plugin Check linter (PHPCS WordPress + WordPress-Extra rulesets). Addressed all 82 reported issues from the most recent scan. Behavioral changes: replaced five `@unlink()` calls in the image-sideload path with `wp_delete_file()` (the WP.org-blessed wrapper; same silent-on-failure behavior); switched `uninstall.php` to use the canonical `gip_` prefix on every variable in its scope (renamed `$timestamp` to `$gip_next_cron_run`); split the changelog so this file stays under the 5000-character `readme.txt` parser limit (older entries are now in `changelog.txt` shipped alongside this readme). Annotation changes: documented intentional patterns the linter can't statically verify — direct `$wpdb` queries against our own custom seen-GUIDs ledger table, interpolated table identifiers in DDL, `meta_query` / `meta_key` lookups by our GUID-hash meta, `suppress_filters` in the dedup confirm-existing-post lookup, display-only `$_GET` reads of redirect-back-from-handler notice messages, and concatenated already-escaped data-attribute strings in the admin-notice block. Added `/* translators: */` comments above every translation call that contains placeholders, and renumbered the trim-to-cap confirmation message's `%d` placeholders to `%1$d` / `%2$d` per the i18n linter's strict-ordering rule.

= 1.0.65 =
* New: "Reset seen-GUIDs ledger" button in the Feeds tab. The ledger is what stops a deleted-then-redelivered RSS item from coming right back on the next cron tick — useful in normal operation, but it also means a feed can read "0 new, N skipped" for items you previously deleted and now want back. The new button wipes the ledger so the next import treats every item currently in every enabled feed as fresh. Live (un-deleted) imported posts are still detected via their post metadata, so the reset only re-enables import of GUIDs whose posts no longer exist. The button only appears when the ledger actually has rows, is labeled with a live entry count, requires two stacked confirmations (the second is intentionally worded as a hard "are you absolutely sure"), and reports back how many entries were cleared. Falls back from TRUNCATE to DELETE on hosts that strip TRUNCATE permission from the WordPress database user.

= 1.0.64 =
* Updated the `Plugin URI` header to point at the correct public GitHub repository (`https://github.com/Fifth-Ave-Photo/the-grid-index-rss-importer`). WP.org review flagged the previous URL as returning a 404; it referenced a repository path that no longer existed under that account.

= 1.0.63 =
* WP.org review compliance: extracted the inline `<style>` block (~1,070 lines) and inline `<script>` block (~432 lines) that the admin page render function emitted into standalone files at `/assets/admin/admin.css` and `/assets/admin/admin.js`. Both are now loaded via the WordPress enqueue APIs (`wp_enqueue_style()` and `wp_enqueue_script()`) from the existing `admin_enqueue_scripts` hook. Dynamic config that was previously inlined as `<script>window.gipRssCfg = {...}</script>` (AJAX URL, nonces, action names, i18n strings) is now passed via `wp_localize_script()`, which is the WP.org-blessed pattern for handing PHP values to JS. Behavior is unchanged — same styles, same interactions, same auto-save and progress polling — but the page now contains zero raw inline `<style>` or `<script>` tags.
* WP.org review compliance: removed the AP community-mirror S3 URL (`associated-press.s3-website-us-east-1.amazonaws.com/topnews.xml`) from the activation-time starter feeds. The reviewer flagged it as a remote-file-call dependency, which is fair: the mirror is unofficial, AP retired official RSS in 2020, and the mirror's uptime is outside the plugin author's control. A one-time activation migration also strips this URL from any existing user's saved feeds so upgrades don't keep silently pinging a third-party S3 bucket. Users who specifically want AP can add a bridge URL of their own choosing manually on the Feeds tab.
* Added `fifthavesupport` to the Contributors list in the readme.

= 1.0.62 =
* WP.org review compliance: added the required `== External services ==` disclosure to the readme. The plugin is an RSS importer, so its job is to fetch third-party feed URLs you opt into; the new section explains exactly what is sent (the feed URL, a browser-style User-Agent, standard HTTP headers, your server's outbound IP), when it is sent (on cron tick for enabled feeds and when you click Import Now / Fetch), and that no visitor data, analytics, or phone-home traffic is involved.
* The new section includes a per-publisher table covering every source the plugin can connect to (43 unique publishers across the 47-feed catalog plus the activation-time starter feeds), with the publisher home, terms of service URL, and privacy policy URL for each. Publishers are not affiliated with this plugin; their feeds are governed by their own respective policies.
* WP.org review compliance: moved the standalone-mode admin menu off its top-level position. When The Grid Index theme is active, the plugin continues to nest under **Grid Index → Grid RSS** (unchanged). When the theme is not active, the fallback now registers under **Settings → Grid RSS** via `add_options_page()` instead of as a top-level item at menu position 25. This follows the WP.org best-practice that configuration pages belong under Settings and avoids competing with core menu items for visibility.

= 1.0.61 =
* Fixes for WordPress.org submission scanner failures: removed the `Update URI: false` header (WP.org explicitly disallows this header in plugins they host — they manage updates themselves); removed the `Domain Path: /languages` header (the folder doesn't exist yet and the plugin ships with no translation files, so the header was a false promise); bumped readme `Tested up to:` from 6.7 to 6.9 to match current WordPress.

= 1.0.60 =
* Reverted the v1.0.59 theme CTA experiment. The pill is back to the v1.0.54 passive muted indicator: "Theme: The Grid Index — not active", no link, no button styling, no animation. The theme isn't on WordPress.org yet, so there's no useful destination to point at, and an external link to thegridindex.com wasn't the right call either.

= 1.0.59 =
* The "Theme: The Grid Index — not active" pill is now an active call-to-action instead of a passive status indicator. Reads "Get The Grid Index theme ↗", links to thegridindex.com (opens new tab), solid teal background with hover lift, and a brief pulse animation on page load (3 cycles, then stops) to draw attention without being annoying. Behavior unchanged: hidden entirely when the theme IS the active theme.

= 1.0.58 =
* Project link banner moved to the TOP of the Support tab, above the FAQ. The previous placement (inside the Credits card at the bottom of the tab) required scrolling past 15 FAQ entries before the user saw it — effectively buried. The banner is now the first thing visible when Support opens: title "The Grid Index" with description, plus a prominent primary "Visit The Grid Index ↗" button on the right. Teal gradient callout, can't miss it.

= 1.0.57 =
* Restored the "Visit The Grid Index" button on the Support tab's Credits card. Sits inside a soft-teal callout panel with a hint line beside it explaining the plugin/theme relationship. Inline text link to thegridindex.com stays in the Credits paragraph as well — both reachable.
* Logo treatment more distinctly redesigned. The eyebrow ("THE GRID INDEX") is now teal and has a short teal accent rule next to it — small, intentional wordmark element. Title bumped to 42px from 34px with tighter letter-spacing. The header now reads as a structured wordmark rather than two stacked text lines.

= 1.0.56 =
* Support tab simplified per design direction: removed the dedicated Contact card with its outline button. The thegridindex.com link is now a small inline text link inside the Credits paragraph instead. Cleaner, less promotional, less likely to draw WP.org review flags. Support tab now has two sections: FAQ at top, Credits at bottom.

= 1.0.55 =
* Support tab restructured: the FAQ knowledge base stays at the top, and two new sections sit beneath it. **Contact** card with the project home URL (thegridindex.com) and a ghost-style "Visit The Grid Index" button. **Credits** card naming Fifth Avenue Photographic as the parent company behind The Grid Index theme and this companion plugin, plus a small product grid showing both products side by side and the version of the plugin currently running.
* Links open in a new tab with `rel="noopener noreferrer"` for safety.

= 1.0.54 =
* Theme pairing pill now only shows when The Grid Index theme is INACTIVE. When the theme is active, no pill — the user already knows their own theme. Reduces hero noise to signal-only: visible when worth knowing, hidden when fine.

= 1.0.53 =
* Added a "Theme: The Grid Index" pill to the page header metadata row. Reads "active" (green) when The Grid Index theme is the current theme, "not active" (muted) otherwise. Hover for a longer tooltip explaining what the pairing enables. Honest acknowledgment of the companion theme, no outbound link or sales copy — just informational. Also doubles as a quick diagnostic: if you ever see unexpected behavior around source attribution, the pill tells you whether the theme is the cause.

= 1.0.52 =
* Hero header tightened: same elements, same colors, same fonts — just refined hierarchy. Eyebrow ("THE GRID INDEX") is smaller with tighter letter-spacing so it reads as a supertitle rather than a label. Vertical spacing between eyebrow / title / description tightened. Description max-width capped at 64 characters so long lines don't dilute the headline. Pill row (Mode / version / Category / Next run / Last run) reduced in size so the metadata supports the title visually instead of competing with it.
* Fixed: the version pill in the hero was reading from the theme's `GIP_VERSION` constant (or falling back to "1.0.0" when the theme was inactive) instead of the plugin's own version. Now reads from `GRID_INDEX_RSS_IMPORTER_VERSION` directly, so the pill always shows the plugin's actual version regardless of theme state.

= 1.0.51 =
* Fixed (the real cause, finally): the "Grid RSS" menu was never actually nesting under "Grid Index" even when the theme was active. The parent-detection check used `$GLOBALS['admin_page_hooks'][$parent_slug]`, but that global is keyed by HOOK SUFFIX (e.g. `toplevel_page_gridindex`), not by raw slug — so the check always returned false. Every install since v1.0.46 has been silently rendering the menu as a top-level item, even though I claimed it was nesting. Rewrote the detection to walk `$GLOBALS['menu']` directly, which is the authoritative source WordPress uses to render the sidebar.
* Added a defensive admin notice when the plugin's bundled stylesheet file is missing on disk. Some hosts' "Replace current plugin" upload path has been observed to skip subdirectories — leaving the plugin's PHP files in place but never extracting the `assets/` folder. Without the stylesheet, the entire admin UI renders unstyled. The notice makes the failure visible instead of silent. If you see "Bundled stylesheet missing at ..." after upgrading, the fix is to re-upload the zip or to manually unpack the assets folder via FTP/file-manager.

= 1.0.50 =
* Fixed: when The Grid Index theme was not the active theme, the entire plugin admin page rendered completely unstyled (plain WordPress admin with no card / pill / dark-mode styling). Root cause: the CSS enqueue loaded `theme-options.css` from `get_template_directory()`, which returns the currently active theme's path — not specifically The Grid Index. With a different theme active, the file didn't exist, the enqueue silently failed.
* Bundled `theme-options.css` inside the plugin at `/assets/admin/theme-options.css`. Plugin now always loads its own copy as the base layer, so the UI works standalone with any theme. If The Grid Index theme IS active, the theme's copy is layered on top so theme customizations still override the bundle.
* Moved the standalone top-level menu position from 3 to 25. Position 3 sat right under Dashboard (top-of-sidebar placement reserved for WordPress core areas); position 25 puts it below Comments and above Appearance — the conventional placement for plugin admin pages. Submenu placement under Grid Index (when the theme is active) is unchanged.

= 1.0.49 =
* WP.org submission compliance pass: text domain renamed from `grid-index-press` to `grid-index-rss-importer` (matches plugin slug as required); plugin header updated with proper License/License URI/Update URI fields; added an empty `index.php` to prevent directory listings; tightened readme short-description to comply with the 150-character limit.
* Flipped `keep_on_uninstall` default from TRUE to FALSE. WordPress.org guidelines require uninstall to remove plugin data unless the user explicitly opts in. Users who want preservation can check the box on Settings BEFORE uninstalling.
* Removed AP News and Reuters World from the catalog. Both publishers retired their official RSS years ago; the catalog had shipped community-bridge URLs (rsshub.app), which doesn't meet WP.org standards for pointing users at third-party services without disclosure. Users who specifically want either source can add their own bridge URL manually.
* One-time activation migration removes any rsshub.app URLs from active feed lists.
* Security pass: explicit `esc_url()` / `wp_kses_post()` wrappers added around a few echo sites where the source value was already escaped upstream (defense in depth for reviewer comfort).

= 1.0.48 =
* Added a green "Last run: [date + time]" pill to the page header, alongside the existing "Next run in X" pill. Shows the exact date and time of the most recent completed import using the site's configured date/time format from Settings → General, respecting the site's timezone. The relative time ("5 minutes ago") is exposed as a tooltip on hover so both representations are available. The pill only renders once at least one import has completed.

= 1.0.47 =
* Fixed: duplicate "Grid RSS" entry was appearing under Tools (and also under Settings), and clicking through either of those produced a completely unstyled admin page. Both bugs had the same root cause — leftover fallback menu registrations from before v1.0.46. The CSS enqueue was keyed to one specific `$hook_suffix` captured at registration time, so any path that arrived via a different parent menu (Tools, Settings) used a different hook suffix and the theme stylesheet silently failed to load. Removed the redundant Tools and Settings registrations entirely. There is now exactly one menu entry: Grid Index → Grid RSS (or top-level Grid RSS if the theme is inactive).
* Internal: rewrote the CSS-load + body-class hook conditions to match by the `?page=gip-rss-importer` query arg rather than a captured hook suffix. Robust to future menu relocations.

= 1.0.46 =
* Renamed "RSS Importer" to "Grid RSS" in the admin menu, the on-page hero heading, and the Tools / Settings fallback entries.
* Menu now nests under "Grid Index" (the theme's top-level menu) instead of sitting as its own top-level item. If The Grid Index theme isn't active, the plugin falls back to registering its own top-level "Grid RSS" menu so it's always reachable.
* Internal: bumped the menu registration's admin_menu priority from 1 to 15 so the theme's top-level menu (registered at priority 8) is guaranteed to exist by the time we try to nest under it.

= 1.0.45 =
* Renamed the companion theme from "Grid Index Press" to "The Grid Index" in all user-facing copy: the plugin description on the Plugins page, the eyebrow text above "RSS Importer" at the top of the admin screen, and the readme description. Internal code (text domain `grid-index-press`, class names, option keys, constants) is unchanged — changing those would break upgrades from existing installs.

= 1.0.44 =
* Author rename: plugin header now reads "By Fifth Avenue Photographic" instead of "By Grid Index Press." Updates the byline shown on the Plugins page. Mentions of the Grid Index Press THEME elsewhere in the description and changelog stay as-is — those refer to the companion theme this plugin pairs with, not the author.
* readme.txt Contributors slug updated to match.

= 1.0.43 =
* New "Support" tab with an embedded knowledge base. Fifteen FAQ entries grouped into five sections (Getting started, Imports & scheduling, Feed health & troubleshooting, Duplicates & deleted posts, Maintenance) covering the questions that come up most often: red-dot feeds, silent-failure feeds, force re-import, per-feed intervals, drafts vs publish, WP-Cron unreliability, the dedupe ledger, image filtering, uninstall behavior, restoring defaults. Each entry is an expandable accordion.
* Live search box at the top of the Support tab filters entries by question text + answer text, case-insensitive substring match. Matching entries auto-open; empty sections hide themselves. No external dependencies.
* The "Last updated" version stamp at the top of the FAQ pulls dynamically from GRID_INDEX_RSS_IMPORTER_VERSION so the knowledge base advertises which build it was written against. If a future version changes behavior and the FAQ falls out of sync, the version stamp will help spot it.

= 1.0.42 =
* New: duplicate-stories alert banner on the Feeds tab. When the dedupe detector finds duplicate groups in your RSS posts, a red banner appears at the top of the Feeds card with the count (e.g. "12 duplicate groups found across your RSS posts (47 extra posts can be merged)") and a "Review on Diagnostics" button. The button switches to the Diagnostics tab AND scrolls directly to the Duplicate detector card so you don't have to hunt for it.
* Counts use a lightweight summary helper that caches its result in a 3-minute transient, so the banner doesn't re-run the full grouping query on every Feeds-tab page load. The cache is invalidated after every successful merge AND after every import run that actually added posts, so the banner stays accurate.
* Banner only renders when there's actually at least one duplicate group — clean inboxes don't see scary red text.

= 1.0.41 =
* Removed CNN from the catalog. CNN deprecated their RSS feeds in 2024 — the URL that previously shipped (rss.cnn.com/rss/edition.rss) still resolves but returns empty or stale data, and cnn.com/services/rss/ now 302s to the homepage. No usable official replacement exists. Honest mistake on my part to have shipped it as "verified."
* One-time activation migration removes any rss.cnn.com URL from your active feeds list. If you'd want it back via a community bridge or third-party generator, you can paste a URL manually on the Feeds tab.
* New "Feed health check" card on the Diagnostics tab. For each active feed, counts posts imported in the last 48 hours and assigns a verdict: ✓ ok, ⚠ silent (fetched recently but zero posts imported — the same pattern CNN was showing), stale (no recent fetch), or never fetched. Sorted with problems first. Helps catch the "green status but actually broken" pattern that hid CNN's deprecation. Run it any time after the next cron tick.
* Catalog count text now shows the actual catalog size dynamically (49 after the CNN removal) rather than hardcoded "50."

= 1.0.40 =
* Per-feed granular categories. Imported posts now get tagged with both the existing "RSS" catch-all category AND a granular category (News, World, Tech, Business, Science) based on the feed's catalog category. So a BBC World story goes into both "RSS" and "World"; a TechCrunch story into "RSS" and "Tech." Theme code that queries Category:RSS keeps working, and users can also browse posts by topic.
* Granular categories are auto-created on first use — the plugin calls wp_insert_term() the first time it imports a post that needs each category. Slugs: news, world, tech, business, science. No category pollution on activation — terms are only created when actually needed.
* Per-feed category picker added to the Feeds tab, inline beside the Source Name input. Options: "RSS only" (default for custom URLs) plus the five granular categories. Catalog feeds get their category set automatically when added.
* One-time migration on activation: existing saved feeds get their category backfilled by URL-matching against the catalog. Custom (non-catalog) feeds get empty category (RSS-only) until the user assigns one manually.
* Note: this only affects NEW imports going forward. Posts imported under earlier versions still have only the RSS category. If you want to re-categorize existing posts in bulk, tell me and I'll add a Diagnostics tool for that.

= 1.0.39 =
* Fixed: clicking into a text/URL/number input field on the Settings or Feeds tab turned the text dark blue (WordPress admin's default focused-input color, #2c3338) — invisible against the dark-mode panel. Added explicit color/background overrides scoped to .gridindex-theme-options that beat WP's wp-admin/css/forms.css rules on focus. Also added a green focus ring so it's obvious which field has focus. Light-mode panels get a parallel set of overrides that keep the field text dark on a white background. Native select dropdown options use OS chrome but the inline <option> color is set to a dark color so open lists are legible on browsers that inherit element colors into the dropdown.

= 1.0.38 =
* Fixed: deleting an imported post (Trash OR permanent delete) no longer causes it to re-import on the next fetch. The root cause was the dedupe lookup querying `post_status='any'`, which is WP shorthand that excludes trash — and once a post was permanently deleted, the postmeta GUID-hash record went with it, leaving zero trace for dedupe to match against.
* Added a persistent seen-GUIDs ledger: a small custom table (`{prefix}gip_seen_guids`) that records every imported GUID hash. The table survives post deletion. On import, the plugin checks BOTH the new ledger and existing postmeta — trashed posts (postmeta still present) are blocked from re-import, and permanently-deleted posts (only ledger record left) are also blocked.
* One-time activation migration backfills the ledger from existing postmeta on first activation of 1.0.38. If you've imported posts under earlier versions, those GUID hashes get copied into the ledger so the same protections apply going forward.
* Force re-import still works, but now ONLY for posts that currently exist as publish/draft/pending — it will not undelete posts you've trashed or permanently removed. This is the intentional consequence of "permanently deleted = never re-import."
* Uninstall behavior: the ledger table follows the v1.0.37 "Keep my data" preference. If checked, table is preserved across delete-and-reinstall (your dedupe history is part of "your data"). If unchecked, the table is dropped.

= 1.0.37 =
* Added "Keep my data if I uninstall this plugin" checkbox to the Settings tab. Default ON. When enabled, deleting this plugin in WordPress preserves your feed list, schedule, image rules, post-status preference, AND the per-post GUID dedupe history. Reinstalling restores everything as it was — including the dedupe history, so already-imported posts aren't re-imported on the next fetch. Disable the checkbox before deletion if you want a fully clean uninstall (settings + dedupe meta wiped; imported posts kept either way).
* Honest tradeoff acknowledged: by leaving option rows in wp_options after deletion, we're deviating from WordPress's "delete should remove everything" contract. This is the same pattern used by Yoast, ACF, WP Rocket, and most established plugins because losing user configuration on a routine delete-and-reinstall is a worse failure mode than leaving a single option row behind. The checkbox makes the choice explicit.
* Cron event and SimplePie feed-cache transients are still cleared unconditionally on uninstall (a dead cron hook with no plugin to run it would log errors; stale feed transients are pointless to keep).

= 1.0.36 =
* Added a "Fetch now" action to the post-add toast. When you toggle on a feed from the Catalog tab, the success toast no longer auto-dismisses — instead it shows two buttons: "↻ Fetch now" runs an immediate AJAX fetch for that single feed (with live progress like "Fetching: New York Times…"), "Dismiss" closes the toast. After the fetch completes, the button reports the per-feed totals ("✓ 3 new, 12 already imported") and the toast then auto-dismisses after a few seconds. No page reload, no tab-switching — you can verify a newly-added feed works before adding the next.
* New AJAX endpoint wp_ajax_gip-rss-importer_ajax_fetch_one wraps run_import() in single-feed mode. Reuses the existing progress transient + polling system from v1.0.26, so the live status text inside the Fetch button comes from the same source as the Import Now progress label.

= 1.0.35 =
* Catalog expanded from 30 to 50 feeds: News 18 → 26 (added NBC News, CNN, The Hill, ProPublica, Time, Bloomberg Politics, LA Times, Reuters World via RSSHub bridge); Tech 6 → 9 (added 9to5Mac, MIT Tech Review, ZDNet); Business 6 → 8 (added MarketWatch, CNBC Top News); plus a new Science section (Science Daily, Ars Technica Science, NASA News) and a new World section (Deutsche Welle EN, France 24 EN, CBC News Canada, ABC News Australia). Honest note: a handful of these (Reuters via RSSHub, LA Times, CNBC's quirky feed format) may need swapping out if they don't resolve on first fetch — let me know which fail and I'll find replacements.
* Added a "Breaking News" virtual section at the top of the Catalog. Auto-populated from every feed with a 5-minute recommended interval (wire services and high-volume desks like Google News, AP, BBC World, Politico, Al Jazeera, NBC News, CNN, Reuters World). No new data — the same feeds also appear in their home category so you can see them in context.
* Added a Cards / List view toggle at the top of the Catalog. Cards is the default; List view is a denser table-style layout where you can see more feeds at once. The view choice persists in the URL (?view=list) and is preserved across Add/Remove actions so toggling a feed from List view doesn't bounce you back to Cards.
* Section order locked: Breaking News → News → World → Tech → Business → Science. Unknown categories fall to the end.

= 1.0.34 =
* Per-feed intervals. Each saved feed now has its own check interval (5min / 15min / 30min / hourly). The cron still runs at whatever rate you pick in the global Settings frequency — that rate is now the polling rate, and the cron uses each feed's individual interval to decide whether it's actually due. A feed set to 5-min interval but the cron polling hourly effectively runs hourly; a feed set to hourly but the cron polling every 5 min will be skipped on every poll until an hour has passed since its last fetch.
* Recommended intervals per catalog feed. Each catalog card shows the recommended interval based on the publisher's typical publishing volume: wire services (Google News, AP News, BBC World, Politico, Al Jazeera) → 5 min; major newspapers (NYT, WaPo, Guardian, NPR, USA Today, BBC US, ABC, CBS) → 15 min; tech sites (TechCrunch, Verge, Ars, Wired, Engadget, Hacker News) → 30 min; business / weekly (Bloomberg, FT, HBR, Fast Co., Forbes, WSJ) → hourly. Adding a feed from the catalog uses its recommendation as the starting interval; users can override on the Feeds tab.
* Interval picker column added to the Feeds tab between Source Name and Last Fetched. Picks any of the four intervals — no enforced minimum, you can run any feed at any rate, but be aware that aggressive polling of slow feeds wastes resources and can trigger rate-limits from publishers.
* One-time migration on activation: existing feeds without an interval get the catalog-recommended interval if their URL matches a catalog entry, otherwise hourly. Marker option means this fires once per install.

= 1.0.33 =
* Added "Every 5 minutes" and "Every 30 minutes" to the Check frequency dropdown on the Settings tab. The full set is now: Every 5 minutes / Every 15 minutes / Every 30 minutes / Hourly / Twice daily / Manual.
* Honest WP-Cron caveat surfaced in the UI: when a 5-min or 15-min interval is selected, an inline yellow note explains that WP-Cron only fires when someone visits the site, so for reliable short intervals you need a real cron job at your host (Hostinger → Cron Jobs → hit wp-cron.php every minute). Without that, short intervals can lag on quiet sites no matter what value is selected here.
* Caution before picking 5-min: most mainstream feeds (NYT, BBC, Google News) update slower than 5 minutes anyway, and aggressive polling can trigger rate-limits from some publishers. The existing 10-minute per-feed error backoff still applies, so a feed that starts failing won't be re-hit on every cron tick.

= 1.0.32 =
* Diagnostics tab readability — surgical fix. The duplicate-list date/source/hash text was at 0.6 opacity (too faded against dark backgrounds), the post-date caption under Recent Imports was at 0.55 opacity / 11px (smallest text on the page), and the source-URL cells were faded at 0.85 even though they're the most important content in those rows. All three bumped: dup-meta to 0.85, recent-imports date to 0.8 and 12px, source URLs to full opacity at 13px. Hash code chips bumped from 11px to 12px and the background lightened slightly so they don't visually disappear into the row. Layout, font family, font weight, padding, and overall page proportions unchanged — no visual redesign, just contrast.

= 1.0.31 =
* Added a "Merge duplicates" button to the duplicate detector. Keeps the oldest post in each group (lowest ID), moves the rest to Trash via wp_trash_post() so anything mis-grouped can be restored from Posts → Trash within 30 days. The detector now shows a green "KEEP" badge on the post that will survive each merge and a red "TRASH" badge on the ones that won't, so there are no surprises. Confirmation prompt names the exact count of posts that will be trashed.
* Refactored: the duplicate-finding logic is now in a shared find_duplicate_groups() method used by both the diagnostic UI and the merge handler, so the two can never disagree about what counts as a duplicate.
* Note: this fixes the existing duplicate posts. The upstream cause (why duplicates are being imported in the first place) is still under investigation — likely candidates are unstable item GUIDs in some feeds (where the publisher mints a new GUID each fetch) or repeated force-reimport runs. Once you've merged, watch the duplicate detector after the next few cron runs to see if it grows again, which will tell us if the underlying import logic also needs a fix.

= 1.0.30 =
* Added a read-only "Duplicate detector" card to the Diagnostics tab. Scans the most recent 2,000 RSS posts, normalizes titles (lowercase, strip punctuation, collapse whitespace), and groups posts whose normalized titles match. Lists groups with 2+ members in descending order by group size, showing post ID, date, source name, and the first 8 chars of the GUID dedupe hash for each member. No deletes — this build just exposes the problem so you can confirm whether what looks like duplicates on the front-end are actual database duplicates and what's causing them. The expected cause: the same story arriving via multiple feeds with different GUIDs (e.g., NYT Homepage, NYT World, and Google News all carrying the same wire story), which the GUID-hash dedupe doesn't catch because each publisher mints its own unique GUID. A merge tool can be added in a follow-up version once the diagnostic confirms what's actually duplicating.

= 1.0.29 =
* Fixed: deleting all your feeds and reinstalling the plugin caused the starter feed list to silently come back. The activation hook had been auto-seeding starter feeds on any "empty" state, which can't be distinguished from a deliberate reset after Deactivate → Delete → reinstall. Auto-seed has been removed entirely. Fresh installs now land on an empty Feeds tab; the Catalog tab is the path to populate. The "Restore default feeds" button on the Feeds tab still works for users who want the starter list back with one click — it's just no longer applied automatically.
* Added an empty-state nudge to the Feeds tab. When the saved feed list is empty, a green panel suggests the Catalog tab as the fastest way to get started.

= 1.0.28 =
* Removed the Run tab. The Import Now and Force re-import 24h buttons already moved to the Feeds tab toolbar in 1.0.26, so the Run tab was just hosting the import log. The log now lives in the Diagnostics tab as a "Last import log" card at the top, with the human-readable "Last run: 5 minutes ago" line underneath the title.
* Added a "Last Fetched" column to the Feeds tab table. Each feed shows a relative time like "2 minutes ago" or "3 hours ago" — hovering the cell reveals the exact timestamp ("May 10, 2026 4:11 PM"). Feeds that have never been fetched show "Never" in italic. Implemented with WordPress's native human_time_diff() and wp_date() helpers so timezones and translations work out of the box.
* One-time defensive cap clamp on activation: anyone arriving here with more than 15 saved feeds (because they upgraded from a pre-cap version, or because earlier cap enforcement only ran on save) gets trimmed to 15, keeping the first 15 in saved order. Gated by a marker option so it fires once per install. The Catalog tab's "Trim to 15" button still exists for the rare case where someone toggles back over the cap manually.

= 1.0.27 =
* Catalog reshaped to news-heavy. Removed the AI category (OpenAI, Google AI, DeepMind, Hugging Face, MIT Tech Review) and Culture category (The Atlantic, Vox, BuzzFeed, The New Yorker, Vulture) entirely. Added 10 mainstream news outlets focused on US national and world news with politics coverage: NYT Politics, BBC US & Canada, Guardian US, NPR Politics, USA Today, Washington Post, WaPo Politics, AP News, ABC News, CBS News. Tech reduced from 7 to 6 (dropped 9to5Google). Business expanded from 5 to 6 (added WSJ Markets). Final mix: 18 News / 6 Tech / 6 Business / 30 total.
* Two known caveats noted in the catalog source (and worth knowing): AP News uses the RSSHub community bridge because AP retired official RSS in 2020 — works most of the time but is outside AP's control. WSJ Markets feed publishes headlines and summaries, but article links require a paid WSJ subscription to read in full.
* One-time migration removes The Atlantic from active feed lists on activation, since it was dropped from the catalog. Gated by an option marker so it runs at most once per install. Atlantic loyalists can still re-add it manually via the Feeds tab.

= 1.0.26 =
* Live import progress. The import loop now writes its step-by-step state to a transient ("Fetching 3 of 11 — TechCrunch") and the admin UI polls it every second while a run is in progress. The Import Now / Force re-import buttons swap to "Fetching N of M — Source Name…" with the spinner, so during a 2+ minute import you can see exactly which feed is being worked on and how far through the queue you are. No more wondering if the page is stuck. Polling is read-only AJAX gated to manage_options, costs about one ~500-byte request per second while a run is active.
* Moved Import Now and Force re-import 24h buttons to the top of the Feeds tab, next to Add Feed. You no longer have to switch to the Run tab to trigger an import. The Run tab still works with its own copy of the buttons for users who want the full-page view with the import log underneath. Implemented via HTML5 `form="..."` attribute on the toolbar buttons so the buttons can submit hidden forms outside the main settings form (no nested-form bugs).

= 1.0.25 =
* Fixed an "I can't do anything" dead-end when the Catalog tab loaded with an existing over-cap feed list. v1.0.24 introduced the 15-feed cap but the defense-in-depth clamp in the save handler only fires when a save runs — anyone who already had 20 feeds saved kept all 20, and then couldn't add anything new from the Catalog because every inactive feed showed "Cap reached." The Catalog tab now detects an over-cap state, shows a warning banner with a clear "Trim to 15" button, and explains exactly what's happening. Trimming keeps the first 15 feeds in saved order (deterministic, recoverable — any feed you lose can be re-added via the Catalog).
* Catalog sub-header copy also fixed: it used to read "20 of 15 active feeds" in the over-cap case, which is nonsense. Now reads "20 active feeds — over the limit of 15" when applicable.

= 1.0.24 =
* Added a "Catalog" tab — 30 curated, verified-working feeds you can toggle on or off with one click. Grouped into News, Tech, AI, Business, and Culture. Each card shows feed name, host, and an Add / Remove button. Active feeds are highlighted with a green border and "Active" badge.
* Active-feeds cap of 15 enforced everywhere: catalog toggles refuse to add a 16th and show "Cap reached" on the disabled button; manual Feeds-tab saves get clamped to 15 as a defense-in-depth measure.
* Excluded from the catalog (known-broken or paywalled): Reuters (retired RSS in 2020), The Information (paywalled feed returns 403), legacy AP S3 bridge URL.

= 1.0.23 =
* Long-running action feedback. Clicking "Import Now," "Force re-import last 24h," or a per-row Fetch button now immediately swaps the button to a spinning indicator with a label like "Importing feeds — this can take 30-60 seconds…" and disables it so accidental double-clicks don't queue duplicate imports. The other long-action buttons on the page also dim so you can't fire two heavy imports in parallel. Honest activity indicator only — no fake percentage bar, because the synchronous import architecture can't truthfully report progress mid-run without a much larger background-job rewrite (which isn't worth it for 30-90s jobs). When the import returns, the toast notification (added in 1.0.22) tells you what happened.

= 1.0.22 =
* Auto-save while typing. Adding or editing feed URLs and names now saves silently in the background 800ms after you stop typing — no more "did this save?" anxiety, no more clicking Save Feeds. A small "Saving… / ✓ Saved" indicator appears next to the Add Feed button so you can see when each save lands. The "Save Feeds" button stays at the bottom for users who want explicit control. Implemented via a new AJAX endpoint (`wp_ajax_gip-rss-importer_ajax_save`) that wraps the same sanitization the manual save uses; gated to manage_options with a nonce.
* Toast notifications. Fetch results ("Engadget — 3 new, 7 skipped, 0 errors.") now slide in from the top-right corner and auto-dismiss after 5 seconds (errors stay for 9 seconds). Previously these messages sat at the top of the page as a static banner, easy to miss in the long admin layout.

= 1.0.21 =
* Fixed: clicking the × button to remove a feed was a client-side-only delete. The row vanished from the page but wasn't persisted until the user clicked "Save Feeds." Anyone who clicked × and then refreshed, switched tabs, or clicked any other button (like a per-row Fetch) before saving saw the deleted feed reappear. The × now removes the row AND immediately submits the form so the delete commits to the database. No more "I deleted it and it came back."

= 1.0.20 =
* The "Publish mode" status indicator no longer takes over the page. When the importer is set to Publish (the normal/correct state) it's now a small inline pill near the page header — present and tappable for the tooltip, but not dominating. The loud full-width yellow warning banner only renders when the importer is set to Draft or Pending, where it's actually warning about something. Removes about 80px of visual weight from the default healthy state.
* Moved the "+ Add Feed" button to the top of the feed list. With 13+ feeds, having it only at the bottom meant scrolling to add a row then scrolling back to fill it in. New rows now insert at the TOP of the list (right after the header) and the URL input auto-focuses, so adding a feed is one click and a paste. The bottom button strip keeps Save Feeds, Restore defaults, Publish drafts, and Clear all feeds — the destructive/maintenance actions stay grouped at the bottom where they belong.

= 1.0.19 =
* Readability pass across all four tabs. Card titles, field labels, and table headers now render closer to full opacity instead of the muted weights inherited from the theme stylesheet — they were dropping into illegible territory on the dark importer card backgrounds. Bumped padding inside cards and gaps between fields. Tab nav got a slightly larger font and higher resting opacity so inactive tabs are scannable.
* Diagnostics table redesigned. Source URL column now truncates with an ellipsis at the cell edge instead of breaking mid-word into a multi-line code block; full URL is in the link's hover tooltip. Status column changed from "✓ Button OK" / "✗ No button" (which read as a UI label) to "OK" / "Missing" with a clear hover description. Column header "Status" renamed to "Attribution" to match what the column actually represents (whether the theme can render the "Read at Source" button).

= 1.0.18 =
* Hotfix: the tabs added in 1.0.16 looked empty when switching to Settings, Run, or Diagnostics. Cause was a missing closing div on the Feeds tab panel — the other three panels were nesting inside it instead of being siblings, which made them invisible regardless of which tab you clicked. The Feeds tab still rendered correctly (which is why the regression slipped past my own pre-ship review), so it took a screenshot of the empty Settings tab to surface the bug. Sorry for the round-trip.

= 1.0.17 =
* Added uninstall cleanup. Previously, deleting the plugin via Plugins → Delete left the saved settings (including your feed list) sitting in wp_options indefinitely, so reinstalling brought every old feed back. The plugin now ships an uninstall.php that runs on actual deletion (not on simple deactivation) and removes: the settings option, the cron event, the SimplePie feed-cache transients, and the per-post GUID-hash meta on previously-imported posts. Imported posts THEMSELVES are not deleted — those are your content. Removing the meta just clears our fingerprint on them.
* Added a "Clear all feeds" button on the Feeds tab. Wipes the feed list in one click while preserving every other setting (post status, frequency, image rules). Already-imported posts are not touched. Confirms before running.

= 1.0.16 =
* Admin UI redesign. The single 4500px-tall page is now split into four tabs (Feeds / Settings / Run / Diagnostics) with the publish-status banner persisting across all of them. The Feeds list is no longer a stack of bordered cards — it's a compact table where each feed is a single row (URL, name, fetch button, remove button). A small color-coded status dot on the left of each row replaces the verbose status badges; click or hover the dot to reveal the previous "last status, message, last fetch X minutes ago" detail. Feed reliability checks happen the same way; only the presentation changed. Added a Save Feeds button to the Feeds tab so changes there don't require switching to the Settings tab to save. Tabs sync to the URL hash so refresh and shared links land on the same tab.

= 1.0.15 =
* Added a prominent status banner at the top of the RSS Importer admin page that shows the current "New post status" setting at a glance. When the setting is anything other than Publish, a single big button — "Switch to Publish & fix existing drafts" — flips the setting and republishes every existing imported draft in one click. The previous flow required scrolling to the Import Settings card, finding a dropdown, saving, then scrolling back up to find a separate Publish-drafts button. This consolidates both into one obvious action.

= 1.0.14 =
* Fixed the *real* draft bug. v1.0.13 fixed one source (form save fallback) but the underlying cause was a date-handling issue: SimplePie's get_date() returns a string without timezone info, which we were assigning to BOTH post_date and post_date_gmt. When a feed item's pubDate parsed as future relative to the site, WordPress would either reschedule the post or — with malformed/inconsistent date fields — silently demote 'publish' to 'draft'. We now derive both date fields from a single Unix timestamp clamped to "now," so the demotion path can't fire.
* Added a post-insert verification: if the intended status was 'publish' but the post landed as anything else (in case another plugin's wp_insert_post_data filter is intervening), re-assert publish. Belt-and-braces against problems we can't see from here.
* To fix existing drafts created by the bug, use the "Republish Imported Drafts" button on the RSS Importer settings page. It bulk-publishes every draft tagged with the importer's GUID hash meta.

= 1.0.13 =
* Added Google News (top stories, US English) to the curated starter feeds.
* Fixed: imported posts could land in Draft status even when "Publish immediately" was selected, if the saved settings shape was missing the post_status field for any reason. The save handler now falls back to Publish (matching the documented default), and get_settings() defensively normalizes any unknown value to Publish at read time so existing installs are repaired without a manual save.
* Improved feed reliability: bumped fetch timeout from WP's 5s default to 15s, switched to a real-looking User-Agent that doesn't get filtered as a bot by publisher WAFs, disabled SimplePie's HTTP cache during our own fetches so we never serve stale parses, added one automatic retry on transient failure, and added a 10-minute error backoff so a single broken feed stops eating timeout budget on every cron tick. Manual single-feed fetches always bypass the backoff.
* Activation/upgrade now flushes the SimplePie WordPress transient cache so fixes take effect immediately rather than waiting for stale entries to expire.
* Removed the experimental REST API (was briefly added in 1.0.12). It will return as a separate Pro add-on rather than living in the free plugin.

= 1.0.0 =
* Initial release. Extracted from Grid Index Press theme as a standalone plugin.
