=== MetaWrench ===
Contributors:           metawrench
Tags:                   seo, schema, backup, migrate, security, performance, local business, structured data, service business, json-ld
Requires at least:      5.8
Tested up to:           6.8
Requires PHP:           7.4
Stable tag:             1.3.0
License:                GPLv2 or later
License URI:            https://www.gnu.org/licenses/gpl-2.0.html

A complete all-in-one SEO, schema, performance, security, and backup/migration toolkit for WordPress sites of any kind — built with a particular focus on blue collar and service-based businesses. No bloat, no conflicts — just clean, powerful tools that work.

== Description ==

**MetaWrench** is built from the ground up for service-based businesses — plumbers, electricians, HVAC companies, roofers, landscapers, contractors, and anyone else who serves customers in a specific area.

Most SEO plugins are built for bloggers and eCommerce stores. MetaWrench is different. It understands that your business has service areas, physical locations, business hours, and local customers who are searching for exactly what you do — and it helps Google understand all of that automatically.

= What It Does =

**Global Business Settings**
Set your business name, address, phone, email, logo, hours, and social profiles once. Everything flows through to your schema markup automatically — no retyping on every page.

**Single or Multiple Locations**
Running one shop or ten? Toggle between single and multi-location mode. Each location gets its own address, phone number, and hours. Service pages can be tied to a specific location.

**Global Service Areas**
Add every city, town, or region you serve once in the settings. Then on any service page, just check the areas that page covers. No retyping, no inconsistencies.

**Per-Page SEO Fields**
Every page and post gets its own meta title (60-char limit with live counter), meta description (120-char limit with live counter), canonical URL, noindex/nofollow toggles, and social share fields.

**Multi-Schema Support**
Select multiple schema types per page — because a service page might be both a Service and an FAQPage. Each selected type expands its own set of fields:

* **WebPage** — automatic, no extra fields needed
* **LocalBusiness** — pulls from your location settings automatically
* **Service** — service name, description, price range, location, and service area checkboxes
* **FAQPage** — dynamic question and answer builder, add as many as you need
* **Article / Blog Post** — author, publish date, modified date (auto-pulled from WordPress)
* **Product** — name, brand, price, currency, SKU, availability
* **About Page** — pulls from global business settings automatically
* **Contact Page** — pulls from global business settings automatically

**Social Sharing**
Set an Open Graph image (Facebook, LinkedIn), Twitter/X card type, and optional Twitter-specific title and description per page. Falls back to the featured image automatically if no OG image is set.

**Organization Schema**
Output on every page automatically. Establishes your brand entity in Google's Knowledge Graph — business name, website, address, phone, logo, and all social profiles.

= Who It's For =

* Plumbers, HVAC, electricians, roofers, and other trades
* Landscapers, painters, contractors, and home service companies
* Any local service business that wants clean SEO without fighting a bloated plugin
* Web designers and developers managing client sites in the service industry

Alongside SEO and schema, MetaWrench includes backups (with scheduled and cloud copies), restore and site migration, performance tools (page cache, minify, defer, WebP), security (login protection, firewall, file protection, malware scan), SMTP email, and local Business Profile and analytics reporting.

= No Conflicts =
MetaWrench does not use global plugin settings that interfere with other plugins. It only outputs tags on singular pages/posts, leaving archives, search results, and other page types untouched.

== Installation ==

1. Download the plugin zip file
2. In your WordPress dashboard go to **Plugins → Add New → Upload Plugin**
3. Choose the zip file and click **Install Now**
4. Click **Activate Plugin**
5. Go to **MetaWrench** in the left dashboard menu
6. Work through the **Setup** section: business information, locations, service areas, hours and social profiles
7. Edit any page or post to set its meta title, description and schema in the MetaWrench box below the editor

== Frequently Asked Questions ==

= Will this conflict with Yoast SEO or other SEO plugins? =

It can. If you are switching from another SEO plugin, deactivate and delete it first. Running two SEO plugins at the same time will result in duplicate meta tags in your page's head section, which can confuse search engines.

= Does it work with the Gutenberg block editor? =

Yes. The MetaWrench box (SEO, schema and social settings for the page) appears below the editor in both the block editor and the classic editor.

= Will my SEO data survive if I deactivate the plugin? =

Yes. All per-page SEO data is stored as post meta in your WordPress database. If you deactivate and reactivate the plugin your data will still be there. The global settings are only deleted if you fully uninstall (delete) the plugin.

= Can I use it on a site with multiple locations? =

Yes. Enable multi-location mode on the Locations tab. Add each location with its own address, phone, and hours. When editing a service page you can select which location provides that service.

= Does it output schema on every page automatically? =

The site-wide business schema outputs automatically. Page-level types (Service, FAQPage, Article and so on) are added per page from the MetaWrench box in the editor.

= Can I add FAQ schema to a service page that already has Service schema? =

Yes. That is exactly what the multi-schema system is designed for. Check both Service and FAQPage on the same page and both JSON-LD blocks will output independently. Google reads and processes each one separately.

= Is this plugin suitable for non-service businesses? =

It works for any local business, but the feature set is specifically designed around service businesses. If you run a restaurant, retail store, or other non-service business some schema types and field labels may not be a perfect fit.

= Does it generate a sitemap? =

MetaWrench uses WordPress's built-in XML sitemap and controls what goes in it: you can leave out post types and taxonomies, and pages marked noindex are left out automatically.

= Is it translation ready? =

The plugin uses the text domain `metawrench` and all user-facing strings are wrapped in translation functions. Full internationalization support is planned for a future release.

== Screenshots ==

1. The global settings page — Business Info tab
2. The settings page — Locations tab with multi-location builder
3. The settings page — Service Areas tab
4. The settings page — Business Hours tab
5. The per-page meta box — SEO tab with character counters
6. The per-page meta box — Technical tab with schema type selector
7. The per-page meta box — Service schema fields expanded
8. The per-page meta box — FAQ builder
9. The per-page meta box — Social tab

== External services ==

MetaWrench Pro connects to MetaWrench's own server, api.metawrench.io, for features that need outside data. For "Known security holes" it sends, once a day and when plugins are updated: your licence, your site address, and the names (folder names) and version numbers of your installed plugins, themes and WordPress. For "Block known attack networks" it sends your licence and site address and receives a list of address ranges. For malware scanning it downloads an updated list of malware patterns from the same server once a day (sending your licence and site address). No visitor data or site content is sent. Vulnerability data comes from Wordfence Intelligence (Defiant, Inc.); blocklist data from The Spamhaus Project (DROP) and Emerging Threats (GPL-2.0).

The malware scan (Pro) also asks WordPress.org for the official file fingerprints of each installed plugin that comes from WordPress.org (https://downloads.wordpress.org/plugin-checksums/), once per plugin version, sending only the plugin's name and version. This is the same service WP-CLI uses to verify plugins. WordPress.org privacy policy: https://wordpress.org/about/privacy/

== Changelog ==

= 1.3.0 =
* New (Pro): the malware scan compares every plugin from wordpress.org with its original files. A changed file, or a PHP file the original doesn't have, is flagged - this is how most infections of real plugins show up, and it catches malware nobody has seen before. "Reinstall plugin" puts the originals back. Readme and translation edits are ignored.
* New (Pro): a much larger malware pattern list (35 patterns, up from 5) covering hidden code, backdoors, known hacker control panels, secret admin accounts, login backdoors, spam links and script injections. It now also checks must-use plugins. Tested against tens of thousands of files from popular plugins with no false alarms in real plugin code.
* New (Pro): the pattern list updates itself daily from MetaWrench's server, so new malware is recognised without waiting for a plugin update. The Malware tab shows which list you have.
* Faster: files that match their wordpress.org originals are known clean and skip the pattern check.

= 1.2.0 =
* New (Pro): Known security holes. Once a day MetaWrench checks the exact versions of your plugins, themes and WordPress against a database of publicly known security holes, and tells you what to update: on Security > Malware, in a notice on the Dashboard and Plugins screens, under the affected plugin, and by email when something new is found. Only plugin and theme names and version numbers are sent. Vulnerability data provided by Wordfence Intelligence.
* New (Pro): Block known attack networks. A switch on Security > Firewall that turns away addresses on public lists of criminal networks and computers known to be attacking other sites (Spamhaus DROP and Emerging Threats), refreshed daily. Your Allowed IPs are never blocked.
* Both use MetaWrench's own server, so your site never contacts the list providers itself and stays fast.

= 1.1.14 =
* Fix: the firewall's attack filter blocked ordinary visitors - a comment or contact message containing a YouTube embed or the word "<script>", and even a password containing "sleep(". It now checks web addresses only, not what people type into forms, and never blocks logged-in editors.
* Fix: on hosts with a proxy in front (nginx in front of Apache, a load balancer, Cloudflare), every visitor looked like the same address. A few wrong passwords locked everyone out, and the rate limit could block the whole site. MetaWrench now finds the real visitor address behind your own server's proxy and behind Cloudflare automatically, and never locks out or rate-limits a proxy address.
* Fix: the private login address could be found through /login, /wp-signup.php and addresses like /./wp-login.php, and could take over a real page with the same name. A failed test of a new address no longer switches your private login address off.
* Fix: the malware scan said "All core files match" when it could not reach WordPress.org to check. It now says it couldn't check, and uses your site's language.
* Fix: fewer false alarms in the malware scan ($this->retrieval( is not eval(, and a check for a feature removed in PHP 7 is gone).
* Fix: the Quarantine button now works for files the scan flagged, only takes single files, and puts a file straight back if the site stops working without it. Restore always returns a file to where it came from.
* Fix: "Hide WordPress version" no longer stops visitors getting updated CSS and JavaScript after an update. The version is replaced by a code instead of removed.
* Safer: File protection checks your site still loads after writing its rules and undoes them if not; also blocks .phtml and .phar files in uploads.
* Safer: a failed plugin reinstall switches the plugin back on; the restore guard renames a broken file instead of deleting it; WP-CLI is never blocked by the firewall.

= 1.1.13 =
* Fix: anything typed in the Tag Manager box (even "none") switched off your Google Analytics and Google Ads tags. Only a real GTM- ID counts now, and a wrong-shaped Tag Manager, Analytics or Ads ID is refused with a message instead of saving.
* New: a Google Analytics 4 box under Connections > Tracking pixels, so the tag MetaWrench fills in for you can be changed or removed.
* Change: tracking tags are no longer added for logged-in staff, in previews or in the Customizer, so your own visits aren't counted. (Developers: the mw_track_logged_in filter turns this back on.)
* Fix: a short Bing outage no longer disconnects Bing for good, and a problem on MetaWrench's side can no longer disconnect Google on every site.
* Fix: on a Google account with several Business Profiles, none listing this website, the first one (possibly another business's) was shown as yours. It now asks you to add the website to the right profile. Updating clears a profile picked this way.
* Fix: disconnecting Google now also clears leads and rankings from that account.
* Safer: the Google auto-setup only runs straight after you sign in, not from any link; ranking fetches no longer run inside background admin requests; IndexNow skips password-protected pages.

= 1.1.12 =
* Fix: a redirect pointing at its own address no longer sends visitors round in a loop (with "From: /" that took the home page offline). It is skipped instead.
* Fix: redirects now match when "From" is a full address or has accented letters, and keep tracking tags like ?utm_source on the way through.
* Fix: a bot trying hundreds of random addresses no longer pushes your real 404s out of the log. One-off hits are dropped first.
* Fix: the weekly broken-link check now actually checks your links. It used to only update "Checked just now".
* Fix: links starting with // and javascript:, mailto: or tel: links are no longer reported as broken.
* Fix: breadcrumbs and the search box in your schema no longer get a double slash (site.com//page) when your website address ends in /, and a blank business name or website falls back to your site's own.
* Fix: a rating with 0 reviews is no longer published (Google rejects it).
* Security: the site-wide custom schema can no longer end its script tag early.
* Removed: the "Archive pages in search" audit check, which could never pass.
* Faster admin: page SEO reports no longer refresh in the background on every Heartbeat request, or on the free plan.

= 1.1.11 =
* Fix: a custom schema typed in the page editor's Schema box lost its backslashes when saved (and again on every later save), which broke it and left the page with no schema at all. It now saves exactly as typed. A page whose saved schema was already broken uses the automatic schema again, and the editor says so.
* Fix: editing the Schema box when a page has more than one schema block no longer throws the edit away. The blocks are saved together.
* Fix: in the classic editor, setting a featured image in WordPress's own panel was undone when the page was updated. MetaWrench now only changes the featured image when you change it in the MetaWrench box.
* New: a Remove button for the Facebook and Pinterest images, and the preview updates as soon as you pick one.
* Safer: the page editor box only saves to the page it belongs to, and the Page Type preview checks you can edit that page.

= 1.1.10 =
* Fix: HTML minify could send a blank page on very large pages. It now sends the page unminified instead, only touches HTML (not robots.txt or sitemaps), and no longer joins words that sit in separate tags ("Save 20%" stayed "Save 20%").
* Fix: the bulk image tool with only "Compress" switched on converted images to WebP and deleted the originals. It now only compresses, and keeps your JPG and PNG files.
* Clearer: the "Convert new uploads to WebP" switch now says that the WebP file replaces the original.
* Safer database cleanup: auto-drafts are only removed after a week (as WordPress does); "Keep N revisions" keeps the newest N of each page; trashed WooCommerce orders are never deleted; the admin-bar "File Cleanup" no longer empties the trash.
* Fix: security headers no longer ask browsers to upgrade to https on a site without https.

= 1.1.9 =
* Fix: the robots.txt MetaWrench writes no longer blocks /wp-includes/, which stopped Google loading the scripts and styles it needs to see your pages, and keeps WordPress's own admin-ajax.php exception. Updating repairs a robots.txt MetaWrench wrote earlier.
* Safer: "Block all search engines" now asks before hiding your site from search.
* Fix: the robots.txt raw editor no longer throws away your edits when you click away; it saves with its own "Save this robots.txt" button.
* Fix: blocked paths like /file.pdf or /*.pdf$ are no longer broken by an added slash.
* Fix: robots.txt messages ("saved", "could not write") now show.
* Fix: deactivating or removing MetaWrench deletes the robots.txt it wrote, so a "block all" file can't outlive the plugin. A robots.txt someone else wrote is left alone.
* Fix: "Discourage search engines" in Settings > Reading is respected by MetaWrench's robots.txt.

= 1.1.8 =
* Fix: saving My Business added a backslash before every apostrophe or quote, and more on each save ("Joe's" became "Joe\'s", then "Joe\\\'s"). Those backslashes also went out to Google in your schema. Saving is fixed, and updating removes the extra backslashes from text that already has them.

= 1.1.7 =
* Back: "Move this licence to another site" on MetaWrench > Account, under Your sites. It asks you to confirm first, and if the licence server can't be reached nothing changes.

= 1.1.6 =
* Redesigned: MetaWrench > Account now matches the rest of the plugin. Four boxes along the top (plan, sites used, renewal date, version), a warning strip when the licence is ending, expired or cancelled, one-click tiles for Billing, Check licence, Updates and Support, and your licence details beside a picture of your sites.
* Change: the Account screen no longer has a "Move licence" button.

= 1.1.5 =
* New: MetaWrench > Account. One clear screen for your licence: plan and site count, sites used, renewal or expiry date, the last four characters of the key, and buttons to move the licence, manage billing, check the licence or renew. It replaces the Freemius account page.
* Change: the Pro badge in the top bar now opens the Account screen. It used to deactivate the licence when clicked.
* Fix: moving a licence to another site could leave the licensing module running on the old, now free, site.
* Fix: if the licence server can't be reached when moving a licence, nothing changes and Pro stays on, instead of switching Pro off here while the licence still counted this site.
* Removed: leftover code from the old licence provider.

= 1.1.4 =
* Faster: MetaWrench screens and the post editor no longer force all of the plugin's files to be recompiled on every page load. The refresh now happens once, after the plugin is updated.
* Setup: the local-area question now asks "I also sell or serve customers online", instead of turning Online on or off behind the scenes.
* Setup: on Pro sites the licence step shows only the last four characters of the key.
* Setup: tidier spacing on the licence step and on phones.
* Fix: settings screens could show default values on databases that reject an old MySQL query option; the read now falls back safely.

= 1.1.3 =
* Fix: answering "Yes" to the local-area setup question saved only "customers come to me", so businesses that travel to customers lost their service areas. Setup now asks how customers reach you (they come to me, I go to them, or both) and keeps "Online" for local businesses that also sell online.
* Fix: reopening Setup on a Pro site showed an empty licence box. It now shows that Pro is active.
* Fix: after activating a licence in Setup, the page now reloads so Pro shows straight away.
* Fix: turning the monthly report off in Setup no longer clears the saved email address or resets the report's chosen sections.
* Fix: Setup now shows the real reason a save failed (for example a mistyped email address) instead of "Could not save".
* Fix: a licence key is only stored once it has activated.
* Fix: moving between Setup questions with the side list now saves the answer on screen.
* Fix: the first-install redirect no longer fires when several plugins are activated at once, and is no longer lost to a background request.
* Change: activation refreshes only MetaWrench's own cached files instead of clearing the whole server's PHP cache.
* Improved: Setup closes with Esc, keeps the keyboard inside the pop-up, and fits phone screens.

= 1.1.2 =
* Fix: connecting Google or Bing with a Freemius licence failed with "A valid https site URL is required". Freemius keys contain characters such as & and #, and the sign-in address was built without encoding them, so the site address was cut off. All addresses sent to api.metawrench.io are now fully encoded (needs the matching server update).

= 1.1.1 =
* The free version no longer loads Freemius at all: no opt-in screen, no licence prompt, every free tool works straight after activation. Freemius starts only when a Pro licence key is entered, and stops again if the licence is deactivated.

= 1.1.0 =
* Licensing, checkout and updates now run through Freemius. Licence keys are activated through the Freemius SDK (bundled in /freemius), Pro status comes from Freemius, and licensed sites get automatic updates in the WordPress dashboard.
* api.metawrench.io checks the licence with Freemius for this exact install (needs the matching server update).
* Uninstall runs through Freemius's after_uninstall hook, so both clean-ups happen.

= 1.0.2 =
* Google: a visible "disconnect" link in every Google panel (Search Console, Analytics, Tag Manager, Ads). Disconnecting also forgets the Business Profile location, and connecting a different account afterwards drops the old account's Analytics property.
* Google: "Connect Google account" now always shows Google's account chooser, so the right account can be picked.
* Search Console: a site that was added but never verified no longer shows as "Tracking"; MetaWrench now verifies it automatically instead.
* Business Profile: the row now opens a panel like the others. It shows the listing being used, or says why none was found, with "Create a Business Profile" and "Look again" buttons. The old text pointing to a location setting in Setup (which does not exist) is gone, and the listing is now looked for straight after connecting.
* Google Ads: new "Connect Google Ads" button in the Google Ads panel. It finds the Ads account (including accounts under a manager/agency account), fills in the AW- conversion ID, and shows the last 30 days of clicks, spend and conversions on the dashboard. Needs the updated api.metawrench.io.
* Google sign-in now keeps permissions given earlier (reconnecting no longer drops the "set up for me" or Google Ads permissions).
* Bing panel: shows the site being tracked and a "Go to Bing Webmaster Tools" button once connected.
* Tag Manager panel: warns that entering a container ID hands the Analytics and Ads tags over to Tag Manager.
* Tag Manager: "Connect Tag Manager" lists every website container by name and account and shows whether each already has the site's Google tags. Picking one that is missing them (or "Create a new container") makes MetaWrench add the Google Analytics tag, the Google Ads tag and the Conversion Linker, then publish the container - one click, nothing to do inside Tag Manager.
* Works with the updated api.metawrench.io sign-in service (the one-time sign-in code is now passed back correctly).

= 1.0.1 =
Bug-fix release. Every change was checked against the 1.0.0 behaviour with a before/after recording of every admin screen (Pro and Free), every settings save, backups, deactivation and the MetaWrench Doctor diagnostics; the only differences are the fixes below.

Critical
* Deactivation no longer leaves a broken line in .htaccess.
* Scheduled backups no longer crash in WP-Cron ("Call to undefined function wp_tempnam").
* Backups, migration uploads and job files are private (web access denied, unguessable names, job files removed when finished).
* A restore's logged-out batches must carry that restore's own secret token; job IDs are validated.
* Restore now puts back the backup's files (themes, plugins, uploads), not only the database - with a per-file rollback if anything fails - and the Full / Database only / Files only buttons do what they say.
* Five schema fields in the editor box are escaped (stored XSS).
* The failed-login log is no longer written to a public file.
* "Discourage search engines" now always results in noindex, nofollow.
* Connecting an outside service requires a sign-in this site started (no more account-swap by link).

High
* Saving on an unlicensed site (free, expired or licence server unreachable) no longer wipes Pro settings; saves no longer freeze defaults or reset fields that weren't on the form; the Images tab saves through WordPress properly.
* Minify/defer exclusion lists work (by handle or file name); Pro performance features stay off without a licence; defer no longer breaks the login page or scripts that need a library first.
* A Tag Manager ID no longer switches off the social pixels.
* Search & replace no longer crashes (and retries forever) on data from inactive plugins.
* A database restore that hits a SQL error stops and rolls back instead of reporting success.
* One job batch runs at a time; the browser waits and backs off instead of piling on.
* Combined CSS/JS rebuilds when files change; inline CSS is kept; Clear cache empties the right folder.
* WebP conversion is all-or-nothing per image, never overwrites a same-name image, and bulk conversion no longer skips half the library.
* "Allowed IPs" now bypass the firewall and login lockouts.
* SMTP presets save the credentials you typed.

Medium
* One canonical tag per page; the posts page keeps a robots tag; og:type and og:image size are correct; llms.txt and the IndexNow key are served without redirects.
* noindex pages are left out of the sitemap and IndexNow; IndexNow is silent on staging and discouraged sites.
* Breadcrumb schema describes the page actually shown; example opening hours are not published until you save your own; closed days no longer read as "open 24h"; only valid schema.org types are published; addressCountry comes from your location.
* AI-crawler blocks keep working after saving the Robots form.
* Saving settings no longer re-runs the malware scan; reactivating restores the scan and backup schedules; recurring scheduled backups keep recurring.
* Firewall runs on the private login page; /wp-admin/ no longer reveals the private login URL; rate-limit and lockout numbers are validated; login protection sees the real visitor IP behind a proxy/CDN (with "Trust proxy headers" on).
* Quarantined files can never execute; the quarantine list is escaped.
* Security .htaccess rules follow instant toggles, stop growing on each save, are removed when turned off, and use Apache 2.2 and 2.4 syntax.
* The page-cache folder keeps its protection; cache rules are removed when the cache is turned off by a licence lapse or Safe Mode.
* Backups include only this site's tables; restores translate table prefixes (users keep their roles) and bring back plugins that were active before the restore.
* A crashed backup can no longer push a good one out of retention.
* "Remove emojis" and "Disable heartbeat on the front end" fully work.
* Business Profile reviews use the address Google requires; Q&A errors aren't cached as "0".
* Visitors never wait on the licence server.
* Deactivation removes every file, rule and scheduled job MetaWrench added.

Low
* Business Profile connection status shown correctly; PHP 7.4 compatibility; guard incidents and missed scheduled backups are shown; 404 "Fix" and empty redirect rows handled; SMTP relays without a login work; email failures logged on every tier; "Send now" reports failures and the monthly report stays on the 1st; FTP/S3 "Test connection" no longer overwrites saved settings, and passwords/secrets are no longer printed into the page or wiped by a blank field; Safe Mode / developer URL switches ask for confirmation; simultaneous OAuth connections by two admins no longer collide; readme updated; languages folder added.

= 1.0.0 =
* This is MetaWrench's first official release. Everything below this entry
  (2.00.x through 2.16.7) was the ground-up rebuild of the Backup/Migrate
  feature, done step by step and verified against real, live-site bugs
  rather than assumed fixed — that work is now considered complete and
  ready to ship, so the version number restarts at 1.0.0 to mark this as
  the real first public build rather than another internal iteration.
* Fixed (critical, live-confirmed): a cross-site Migrate could never
  actually finish. The database_restore phase legitimately replaces
  wp_users with the source site's users, which invalidates the admin
  session running the migration — every batch after that was rejected as
  "not logged in," permanently stranding the job before the site's URLs
  were ever rewritten, and leaving the destination site broken. A
  restore/migrate job can now continue on its own already-authorized job
  ID once it's underway, without needing a still-valid browser session on
  every single batch. Every other job type (backup, search & replace)
  still requires a fully valid session on every batch, unchanged.
* Fixed: a nonce action-name mismatch between the Migrate upload flow and
  its backend handlers caused "Permission denied" on every upload, and a
  dead code path left over from an earlier version of the upload flow
  caused uploads to silently stop progressing after the manifest step.
* Added: a completed Restore/Migrate now clears this plugin's own page
  cache and any detected host-level cache (LiteSpeed, WP Engine, Kinsta,
  SiteGround) automatically, so the destination site never needs a manual
  hard refresh to display its real styling and content right after a
  migration finishes.
* Verified: Backup's four remote storage destinations (Dropbox, Google
  Drive, Amazon S3, FTP/SFTP) all upload through one shared, tested
  interface, with the exact same code path used by both the manual
  "Back up now" button and the automatic scheduler. Each destination's
  Test Connection button performs a real write-and-verify round trip, not
  just a credentials-format check.

= 2.15.277 =
* Fixed: a restore or migration could silently downgrade the site's address from https:// to http:// everywhere it appears (the manifest written into every backup, and the address used for the restore/migrate URL rewrite) - traced directly to WordPress's home_url()/get_site_url() functions, which don't just return the stored address, but silently substitute the scheme to match whatever the CURRENT request looks like via is_ssl(). A single internal request that a reverse proxy failed to mark as HTTPS was enough to record the wrong address, which then got written permanently into the database and used as the "new" address in a site-wide search-and-replace. Both the backup-manifest capture and the restore/migrate address capture now read the site's real, permanent address directly - from the WP_HOME/WP_SITEURL constants when defined, or the raw stored database value otherwise - and never call any WordPress function that can substitute the scheme based on the current request.
* Note: this is a standalone fix, verified with a dedicated reproduction test (a request where is_ssl() incorrectly reports false while the site's real address is https://, confirming the address is still captured correctly). A broader rebuild of the entire Backup tab, which this fix will also be part of, is in progress separately.

= 2.15.276 =
* Fixed: raised the memory limit set for the migration/restore AJAX request from 512M to 1024M, and added a hard, clearly-reported cap on how large a single SQL statement is allowed to grow while streaming the database import. The batched database import (2.15.274) only ever holds the current in-flight statement in memory rather than the whole file - a real improvement over holding everything - but a real mysqldump's default "extended insert" option can still put thousands of rows into ONE statement on a big table (wp_postmeta especially), large enough on its own to run into a PHP memory ceiling. That kind of failure is a genuine PHP engine fatal, not a normal catchable error, so it can bypass this plugin's own error handling entirely and come back as a bare, unhelpful failure with nothing recorded. This now catches that condition on its own terms with a clear, specific message instead.
* Note: if a migration still fails at the "database" step with a raw "0" response and no message at all (rather than one of this plugin's own error messages), that specific signature means WordPress's own request handler never found a matching action for that request - which is not something happening inside this plugin's code, since every other step used the exact same request path successfully. The most common causes are a security/firewall rule or a caching layer between the browser and WordPress intercepting that one request. If this keeps happening, retrying the step once is worth trying first, since a caching-related version of this tends to be intermittent.

= 2.15.275 =
* Fixed: a backup's file-scanning phase already had a mitigation (added previously) for a known behavior of Local by Flywheel and similar Docker/virtualized dev environments, where a directory listing can silently come back missing entries on the first read with no error to say so - but that mitigation re-read each folder only twice, 10 milliseconds apart. That's enough to catch a listing that's merely inconsistent between calls, but far shorter than the sync lag these environments can actually show on a large folder (an uploads directory with thousands of files is exactly where this shows up), so two reads 10ms apart could both land in the same stale window and quietly agree on the same incomplete list - and when that mitigation did catch something, the only record of it was a debug log file on the server, never shown anywhere in the plugin itself.
* Added: when a folder's directory listing disagrees with itself across the quick check, that folder now gets several more retries with real backoff (up to about 1.5 seconds total) instead of trusting whatever the quick check saw, and it's tracked by name. If any folders needed this, the finished backup says so directly on screen - which folders, and a plain note that the backup may be missing files that exist on disk - instead of that only ever living in a log file nobody but a developer would think to open.
* Added: the "Backup complete" message now also states how much of wp-content was actually scanned (in MB, before zip compression), so a backup that looks smaller than expected can be sanity-checked right there instead of guessing.

= 2.15.274 =
* Fixed (a real architectural gap, found by directly comparing this plugin's database-import code against WPvivid's): the batched database-import step was re-reading the ENTIRE SQL dump from disk and re-splitting it into statements on every single batch call, then skipping ahead with an in-memory index - harmless on a small dump, but on a large one (400MB+) it meant every batch redid all the parsing work every batch before it had already done, and held the whole file (plus a full array of every statement in it) in memory at once. WPvivid avoids this by seeking a single open file handle to the exact byte offset the last batch stopped at and streaming forward from there; this now does the same. Verified against a direct reproduction: a synthetic dump with 400+ separate statements, multi-row inserts, escaped quotes, doubled quotes, and semicolons embedded inside string values was run through 17 simulated separate batches with artificial per-statement latency (so it couldn't finish in one request), and every statement executed, in the correct order, exactly matching what the previous all-at-once parser would have produced from the same file - with the byte offset always landing correctly at the start of the next unprocessed statement between batches.

= 2.15.273 =
* Added: the Backup tab now checks directly for content built with a page-builder plugin (Elementor, Beaver Builder, WPBakery, Divi Builder) that isn't actually installed on this site, and says so in plain terms right on the screen — instead of a restored site quietly rendering as bare, unstyled text with nothing anywhere to explain why. This runs on every page load, not only right after a restore, so it also surfaces on a site that was already restored before this check existed.

= 2.15.272 =
* Fixed (for real this time, verified against a direct reproduction of the exact failure): the last two attempts at this fix targeted unserialize() itself, but that call was never the problem - it succeeds fine and hands back a placeholder object for a class it doesn't recognize (e.g. an Elementor logger item on a site where Elementor isn't active). The actual crash happens one step later, when the address-rewriting code tries to WRITE a corrected value onto that placeholder object, which PHP refuses to allow. That placeholder is now left exactly as it came out of unserialize() instead of being written to, which a reproduction of the exact reported error confirms stops the crash while still correctly rewriting every normal stored value (including nested arrays).

= 2.15.271 =
* Fixed: the previous fix for the "incomplete object" unserialize() crash (2.15.270) didn't actually catch it. That specific PHP complaint isn't a thrown Exception or Error at all - it's PHP's older, classic style of error - so a normal try/catch never sees it, no matter how tightly it's wrapped. Actually intercepting it takes a temporary error handler instead, which is what this does; the offending stored value is skipped exactly as intended.

= 2.15.270 =
* Fixed: migrating a site whose database contained even one serialized PHP object from a plugin that isn't active on the destination (confirmed live with an Elementor internal log entry, "Elementor\Core\Logger\Items\JS") could fail the entire import. Rewriting stored addresses after a restore checks every text value for a serialized structure to update safely, but PHP's own unserialize() can throw a real error - not just a warning - when it can't find that value's class, which was enough to take down an otherwise-working migration over one unrelated, invisible piece of debug data. That one value is now left untouched instead of derailing the whole restore.

= 2.15.269 =
* Fixed: a genuine PHP error during a restore or migration could show WordPress's own "There has been a critical error on this website" page instead of anything MetaWrench could explain — and nothing was recorded about it afterward either. The reason: WordPress registers its own fatal-error handler before this plugin's code ever runs, so it stepped in first, printed its page, and exited — which stopped this plugin's own rollback and failure-recording from ever getting a turn to run. Restore and migration steps now catch that kind of error themselves before it can become an uncaught fatal, so the real underlying error message is shown directly, the site is still rolled back safely, and it's recorded the same way any other restore failure is.

= 2.15.268 =
* Fixed: a migration/restore failure that wasn't a normal, worded error (a bare "0" or "-1" from WordPress itself, or a PHP warning printed ahead of the real response) showed nothing but the word "Error", with no way to tell what actually happened. The progress screen now shows the server's actual response when this happens, including its HTTP status, instead of that one-word placeholder.

= 2.15.267 =
* Fixed: importing a database large enough to take a while ran as one single, unbounded request — on a host with a tight PHP execution-time limit (common on shared hosting), that request could get killed mid-import with no chance to report anything, which looked exactly like "a quick red error, then logged out, and nothing in the restore log." The database step now runs in short, resumable batches the same way file copying already does: it applies what fits in the time it's given, remembers exactly where it stopped, and the browser calls straight back for the next batch — so a slow host draws the import out over more requests instead of running out of time in the middle of one.

= 2.15.266 =
* Fixed: when Upload & Import (or Step-by-step Restore) hit more than a few database errors partway through, it correctly rolled the site back to keep it safe, but wrote down no record of what went wrong — every other restore failure path already left a note the Backup tab could show you after the fact; this one silently didn't. Since that failure also ends your admin session right as it happens (the partial import had already touched your login data before the rollback put it back), the on-screen error message was the only chance to read it, and it was gone before the page finished reloading. This path now records the same kind of note the others do, so the reason shows up on the Backup tab even after you've been logged out and back in.

= 2.15.265 =
* Fixed: restoring or migrating a backup onto a different site could bring the pages and content across but leave it completely unstyled. A database restore replaces every option in your site wholesale with whatever the backup's own site had — including which plugins were active and which address it answered to. File copying was already careful never to delete anything, but the database swap wasn't, so a plugin that was active on the destination site but wasn't part of the backup (for example, a page-builder plugin the theme depends on to render anything) went silently inactive with no notice, and any leftover domain from the source site could linger in stored data. Restoring now automatically reactivates any of your own plugins that got dropped this way (its files must still be present) and rewrites any stray source-site address back to this site's own — and says so in the completion message — no matter which of the three restore paths (Restore, Step-by-step Restore, or Upload & Import) you use.

= 2.15.264 =
* Fixed: the "Send Test To" field on Email > SMTP always showing your WordPress admin email and reverting to it after Save, no matter what address you typed or tested with. Its value was never actually saved anywhere — the field's HTML hard-coded your admin email on every page load instead of reading back what you'd entered. It's now stored and read back like every other field on that screen.

= 2.15.263 =
* Fixed: the "You have unsaved changes on this tab" prompt firing even after a setting was already saved, when switching tabs. Every settings screen saves in the background over AJAX rather than a normal form submit, but the prompt only ever cleared itself on a real submit — so touching any field armed it for the rest of the visit, save or not. Confirmed live on Performance > Fonts & Videos ("Host Google Fonts on my own server"). The one shared save-confirmation function every screen's AJAX save already calls now also clears this prompt, so it's fixed everywhere it could happen, not just the one reported screen.

= 2.15.262 =
* Removed: the "Test compression" button and its result text on the Text compression setting, now that the underlying check (2.15.261) is confirmed working correctly.

= 2.15.261 =
* Fix: "Enable text compression"'s check button could fail with a raw "cURL error 61: Unrecognized content encoding type" instead of an actual answer. It was asking the site for Brotli-compressed content (Accept-Encoding: br) as part of testing; if the server responded with Brotli and the PHP build making that check has no Brotli support in its own cURL library, cURL refused the reply outright. Now only asks for gzip/deflate, which every PHP-cURL build understands - a site that also serves Brotli to real browsers still compresses with gzip too (virtually universal), so the check still correctly reports compression as working.

= 2.15.260 =
* Change: removed the "Safe Mode: ON/OFF" text next to the Safe Mode toggle in the JavaScript section. Its state now shows the same way every other toggle on this page shows its state - the switch itself - rather than a second, separately-updated label next to it.

= 2.15.259 =
* Fix: the "Safe Mode: off" label now reads "Safe Mode: OFF", matching "ON" - both fully capitalized instead of just one.

= 2.15.258 =
* Change: the "Safe Mode: ON/off" label in the JavaScript section header no longer turns red when on - it's the same neutral text color as every other label on the page now.
* Fix: that label didn't update until the page was reloaded, whichever way Safe Mode was turned on or off - clicking its own toggle directly, or choosing "Turn this on with Safe Mode" in the confirmation dialog. It now updates immediately either way.

= 2.15.257 =
* Change: the Safe Mode confirmation dialog no longer appears when Safe Mode is already on - turning on a second (or third, or fourth) JavaScript setting while it's already protecting the site has nothing left for the dialog to offer, so the setting now just turns on immediately, the same as if the dialog didn't exist.
* Removed: Delay JavaScript's "Test on my site" button and its result text. It probed the site once from the server; the "Give your site a quick look" popup that already appears after saving Delay JS (or any other setting that can change how a page loads) sends the site owner to look at the real, live site instead, making the separate probe redundant.

= 2.15.256 =
* Fix: relocating Safe Mode into the JavaScript section's header (2.15.255) left one extra closing tag behind, which closed that section's card right after the header instead of around all four JavaScript toggles - visually splitting the card in two, with only the title (and, on a site with MW_DEBUG_SAVES defined, a QA-only debug notice) inside it and the actual toggles rendering outside it, unstyled. The card now correctly wraps the whole section again.
* Removed: the QA-only "Simulate constant ON" button and its explanation text, previously shown only on a site with the MW_DEBUG_SAVES developer constant defined (never on an ordinary live site). It was a temporary tool for verifying the wp-config.php MW_SAFE_MODE path without editing that file directly; the URL-based `?mw_safe_mode=1` method already covers that same need without a dedicated button.

= 2.15.255 =
* Fix (critical): the Safe Mode confirmation dialog's "with Safe Mode" choice turned on both the JavaScript setting and Safe Mode visually and reported "Settings saved," but the JS setting silently failed to persist - reverting to off after a reload while Safe Mode itself correctly stayed on. Root cause: this section autosaves each field independently the instant it changes, as one unqueued request per field; turning on two fields together fired two of those requests at nearly the same moment, and each was built from the settings as they stood before either write had landed, so whichever finished last silently overwrote the other's change. The dialog now saves both fields in a single request instead of two competing ones, which removes the race entirely.
* Change: the Safe Mode toggle moved out of its own block at the top of the Performance page and into the header of the JavaScript section - the one place it actually does anything - where it's always visible (on or off) and can be turned off directly without opening the confirmation dialog.
* Change: the confirmation dialog now offers a single action, "Turn this on with Safe Mode," plus a close button; closing without choosing still turns the setting on, just without Safe Mode, matching what the previous "without Safe Mode" button did.
* Fix: the dialog's button text was left-aligned instead of centered on a full-width button.

= 2.15.254 =
* New: turning on any of the four JavaScript performance settings that can occasionally break a front end (Minify, Defer, Combine, or Delay JavaScript) now shows a confirmation dialog first, explaining the risk and offering to turn Safe Mode on at the same time. Declining still turns the setting on - the dialog only asks about Safe Mode, it never blocks the setting itself. Turning any of these four back off is never gated. Safe Mode's own standalone toggle at the top of the Performance page is unchanged and still works exactly as before for turning it on or off directly, or recovering a site that's already broken.

= 2.15.253 =
* Fix: "Defer render-blocking JS" added the `defer` attribute to every eligible script tag, including ones with a wp_localize_script()/wp_add_inline_script() companion that WordPress always prints immediately after them, non-deferred (wp-i18n's own setLocaleData call is a stock example). Deferring the file while its companion still ran immediately meant the companion could execute before the file it depended on had loaded, throwing "wp is not defined" - confirmed live on every page when Defer JS and Combine JS were both enabled. Scripts carrying that kind of inline companion are now left undeferred, matching the same protection Combine JS already had.

= 2.15.252 =
* Fix (critical): Browser Cache's .htaccess rule matched every .js/.css file on the site by extension alone, with no exclusion for /wp-admin/ or /wp-includes/ - meaning it was also long-caching WordPress's own block-editor bundles and the lodash library they depend on. A core update could then leave a browser holding a stale copy of one of those files next to a fresh copy of another, crashing the editor with "_ is not a function". Admin and includes assets are now excluded from these rules entirely; there was never a page-speed reason to cache them; Lighthouse and real visitors never load them.
* Fix: Combine JavaScript Files re-enqueued the combined bundle with only jquery as its dependency, discarding whatever the original scripts actually declared (wp-i18n, wp-hooks, etc.). This could let a combined script that needed something like wp.i18n run before that dependency had loaded, throwing "wp is not defined". The bundle now carries forward every real dependency its combined scripts had.
* Fix: Combine JavaScript Files also silently dropped any wp_localize_script()/wp_add_inline_script() data tied to a combined script's handle (an ajaxurl, a nonce, a config object) - the data is registered against the handle, which no longer printed once dequeued. Scripts carrying that kind of inline data are now left to load individually instead of being folded into the bundle.
* Fix: the live character counters on the Tagline and Business Description fields (My Business -> Basics) never updated while typing - a quoting collision meant the count shown after saving and reloading counted bytes rather than characters, so an em dash or any accented character inflated the number; separately, the inline oninput handler itself had a stray double-quote that broke the JavaScript outright, so the counter never moved at all while typing or pasting. Both are fixed.
* Fix: clicking "No physical location" on the Locations tab didn't hide the address fields until the page was saved and reloaded - it now hides (and restores) them immediately.
* Change: "State" is now labeled "State / Territory" throughout the Locations tab.
* New: a Timezone field on the Hours tab, which also sets WordPress's own site timezone (Settings -> General) so Business Hours, scheduled posts, and backups all agree on what time it is.

= 2.15.246 =
* New: a QA-only button on the Performance page (visible only when the MW_DEBUG_SAVES developer constant is defined - the same gate already used by the malware scanner's "Seed test finding" tool) simulates define('MW_SAFE_MODE', true) in wp-config.php, so that path can be verified on a site where editing wp-config directly isn't practical, without adding any way to trigger it on a real, live site.

= 2.15.245 =
* Fix: turning Safe Mode on (or off) by visiting a URL with ?mw_safe_mode=1/0 correctly saved the toggle and told the Performance page it was active, but a page the static page cache had already generated kept being served exactly as before - the cache is handed to visitors directly by the web server's own rewrite rules, before WordPress or Safe Mode's own check ever run, so nothing had told it that anything changed. The URL toggle now clears the page cache in the same request that flips it, the same way saving the setting from the Performance page already did.

= 2.15.244 =
* Fix (feature restored): Safe Mode had quietly stopped existing - the option this readme has documented since 2.01.14 was never actually read or checked anywhere in the code, only cleaned up on uninstall. It's back: a toggle on the Performance page, visiting any page with ?mw_safe_mode=1 while logged in as an administrator (?mw_safe_mode=0 to turn it back off), and define( 'MW_SAFE_MODE', true ) in wp-config.php all switch off the static page cache, minify, combine, WebP serving, deferred CSS, and delayed JavaScript for visitors, without touching anything you've actually saved.
* Fix: the "Files to delay" box on the Delayed JavaScript setting showed grey example text ("googletagmanager", "fbevents") that looked like an already-active default. It was never a default - an empty box delays nothing, by design - it just wasn't obvious the box was empty. The example text now says plainly that it's an example, and an empty box shows a warning that nothing is being delayed until real file names are added.
* Fix: Minify HTML left most of a hand-formatted page's original whitespace in place - a single tab or a single leading space per line was one character, under the two-or-more the whitespace-collapsing rule looked for, so indentation survived by the hundreds of lines on a typical page. It also restored inline JSON-LD schema and <style> blocks completely untouched. Both are now compacted along with the rest of the page - JSON and CSS don't care about whitespace, so this is safe, and it's exactly the sort of inline block a site's own SEO plugin is likely to be adding to <head>.

= 2.15.243 =
* Fix (critical): restoring a backup from Available Backups could silently downgrade the live plugin to whatever MetaWrench version was packaged inside that backup's own wp-content/plugins/metawrench/ folder - with no warning, and no update notice afterward to get back to the version you were actually running. The equivalent file-copy step in Migrate was already protected against this exact problem since v2.01.09 ("Never overwrite the plugin currently doing the importing"), but the separate Restore code path never got the same fix until now. Restoring a backup should put your site's content back the way it was - it should never also roll back MetaWrench itself.
* Note: if a restore already downgraded your live site before installing this version, this fix prevents it from happening on the NEXT restore, but does not undo one that already happened - reinstall this current version's files over the downgraded copy to get back to it.

= 2.15.242 =
* Fix: the backup Download button still 503'd after the 2.15.241 fix, on files where an identical direct request to the same URL succeeded. The download handler had no idea what an HTTP Range request was and always sent the whole file back as a plain 200 - a mismatch with what a browser's own download manager commonly asks for (a Range probe, or a resume request) that a server in front of PHP can turn into a 503 instead of passing through cleanly. It now answers Range requests properly (206 Partial Content) the way a normal file-download endpoint does.
* Fix: adssettings.google.com was still being flagged as broken ("too many redirects") even after 2.15.241 added cookie support - because that URL requires a real signed-in Google session, which no amount of cookie/header handling from an anonymous checker can get past. A short, explicit list of known sign-in-gated Google account/ads hosts is now treated as unverifiable rather than broken specifically for a redirect-loop failure; any other kind of failure against those same hosts is still reported normally.
* Change: replaced the blocking native alert() on the Backup tab's Test Connection/Save buttons with a small self-dismissing on-page message, so a result no longer freezes the whole page (or automated tooling, or a screen reader) until someone clicks OK.

= 2.15.241 =
* Fix: login lockout let one extra wrong password through before engaging. WordPress's own credential check doesn't respect a lockout error from an earlier 'authenticate' filter once a username and password are both submitted, so the check now also runs on 'wp_authenticate_user', which WordPress does honor before it looks at the password.
* Fix: the lockout notice ("Your IP has been temporarily locked out... try again in X minutes") was rendering its own <strong> tags as literal text instead of bold, from being run through esc_html__() a second time.
* Fix: a restore or migration ends the current admin's session, and it now also clears any active login lockout on that admin's own IP - previously, restoring a site while testing login-lockout could leave the admin who ran it unable to log back in until the lockout expired.
* Fix: the Indexing tab's sitemap taxonomy checklist showed two identical "Category" chips and two identical "Tag" chips, from WordPress's own Category/Tag taxonomies and MetaWrench's own Announcement Categories/Tags taxonomies sharing the same label. Colliding labels are now disambiguated automatically.
* Fix: the broken-link checker reported some real, working pages (confirmed against Google's own support and account-settings pages) as broken. It now retries a 404 HEAD response with GET before giving up (some servers answer HEAD and GET differently), and carries cookies across redirects so a consent/locale redirect chain can actually complete instead of looping until the redirect limit is hit.
* Fix: the S3/FTP/Dropbox "Test Connection" and "Save" buttons could fail completely silently if the server response wasn't valid JSON (e.g. a PHP notice printed ahead of it) - there was no error handling on that path at all. All of them now show a real error message instead of doing nothing.
* Fix: the backup Download button could fail on larger backups where a direct request to the same URL succeeded. The download now clears every level of output buffering (not just one) before streaming the file, and raises the time limit for this one request.
* Fix: the license key field rejected some input with a misleading "cannot be empty" message when the real problem was invalid characters (anything sanitize_text_field() would strip as HTML) silently reducing it to an empty string. It now reports which case actually happened.
* Change: "Also remove everything I have set up" (the delete-data-on-uninstall option) is now also available on Security → General, not only on Performance, so it's somewhere a site owner looking for a data/privacy control would actually find it.
* Hardening: the QA-only "Seed test finding" tool (only ever reachable with the MW_DEBUG_SAVES developer constant defined) now names its test file with a random token instead of a timestamp, and writes a per-file .htaccess deny rule alongside it, on top of the server's own PHP-execution block in uploads/.

= 2.01.24 =
* Change: All legacy styling deleted for good — including the embedded style blocks inside every settings screen. One clean skin stylesheet now owns all presentation.


= 2.01.16 =
* Change: New admin look. Light header with a live site-health dial and notification bell, white canvas, icon-tile sub-tabs, unified status colours, and a slimmer safe-mode bar. No settings or behaviour changes.

= 2.01.15 =
* New: Licensing. Pro keys from Lemon Squeezy now activate, validate daily, and can be deactivated to move to another site. One key, one site.
* New: The free tier is real. Without an active key, the seven Performance tools, whole-site SEO scoring, the firewall, the email log, deliverability checks and migration are locked with an upgrade notice; everything else keeps working. Settings entered while locked are kept.
* Change: If the licence server is unreachable, the last good check is honoured for three days so a network problem never locks out a paying customer.

= 2.01.14 =
* New: Safe mode. Switches every Performance setting off for visitors without changing what is saved, so a site broken by minification, defer or delayed JavaScript comes back without FTP. Turn it on from the Performance page, by visiting /?mw_safe_mode=1 as an administrator, or with define( 'MW_SAFE_MODE', true ) in wp-config.php.
* Fix: Plugin and author links pointed at metawrench.com instead of metawrench.io.

= 2.01.13 =
* Removed the version line from the top of the Backup screen.

= 2.01.12 =
* Fixed a PHP warning printed inside the SEO card on the dashboard. The
  backlinks change figure was being read a few lines before it was created, so
  any site with debugging switched on had the warning text rendered into the
  card - which also broke the two-column layout on that screen.

= 2.01.11 =
* A completed migration no longer reports itself as an error. The final step
  imports the database, which ends the session the request was authenticated
  with - so the browser often loses the connection a moment after the server
  has finished the job successfully. That is now treated as completion, and
  only a drop on an earlier step counts as a failure.

= 2.01.10 =
* Reworked the order of a migration. Files are now copied before the database
  is imported, in resumable batches, with progress shown as it goes.
  Importing the database ends the session of whoever started the migration, so
  anything left to do afterwards had to survive without a login - which a copy
  of tens of thousands of files could not reliably do. The long job now happens
  while the session is still valid; the database swap is the short final step.

= 2.01.09 =
* Fixed migrations dying partway and leaving the site with a missing theme.
  The package contains the source site's copy of MetaWrench, and copying it
  over the running plugin replaced the code mid-import - which is also what
  produced "Plugin file does not exist" afterwards. The running plugin's own
  folder is now left alone.
* Themes, plugins and mu-plugins are copied before uploads, so an interrupted
  copy leaves a site that still renders rather than a white screen.

= 2.01.08 =
* Fixed themes, plugins and uploads not being copied during a migration on
  Windows servers and Local development sites. The file copy compared paths
  built with forward slashes against paths returned with backslashes, so the
  prefix never matched and nothing was written to the right place.
* The import now reports how many files it copied, so a silent zero is visible.

= 2.01.07 =
* Fixed migration leaving the destination site renamed to the source site.
  The import ran as a chain of separate browser requests, but importing the
  database replaces the users table and ends your login - so every request
  after it failed its permission check. The address rewrite, file copy and
  cleanup never ran.
* The import now completes inside a single request, while it still has the
  authority to finish the job.

= 2.01.06 =
* Fixed: migrating a backup onto a site also changed that site's address to the
  address of the site the backup came from. The URL step asked WordPress for
  "this site's address" after the database had already been replaced, so it was
  reading the imported site's address and concluding nothing needed changing.
* The destination's own address is now recorded on disk before the import
  starts, and written back into siteurl and home immediately afterwards.
* Import progress is no longer stored in wp_options, which the import itself
  overwrites - it now survives the database being replaced.

= 2.01.05 =
* Migration now warns before it runs. Choosing a file shows what the import
  actually does - it replaces the whole site, including every user account -
  and the import button stays disabled until you confirm you have the other
  site's login details.
* The warning lists the usernames found inside the backup, so you can see who
  will be able to sign in afterwards. Passwords cannot be read from a backup.

= 2.01.04 =
* Fixed "Duplicate entry" errors when migrating. The database import split the
  SQL file on any semicolon at the end of a line, which cut statements in half
  wherever an option held serialized data - widget_categories, widget_text and
  widget_rss being the usual casualties. The splitter now respects quotes.
* After a migration finishes, the screen explains that the site now uses the
  other site's logins and sends you to the login page, instead of leaving
  WordPress to show a bare "session expired" box.

= 2.01.03 =
* Fixed "Upload failed: Permission denied" when migrating a site. The page was
  sending the backup screen's security token, but the import handlers check for
  their own - so every upload was rejected before it started.

= 2.01.02 =
* Available Backups are sorted newest first again. The list had been sorted by
  filename, which only happened to work while every name began with the date.

= 2.01.01 =
* Backup files are now named after the site they came from, e.g.
  sampleplumbing-full-2026-08-20-1415.zip. Restoring the wrong site's backup was
  far too easy when every file was called mw-backup-full-<date>.
* Older backups keep their existing names and still restore normally.

= 2.01.00 =
* The main plugin file is now metawrench.php (was service-site.php).
  WordPress treats a renamed main file as a different plugin, so you will need
  to activate MetaWrench again after updating. Your settings are untouched.
* Storage folders renamed: wp-content/ss-backups is now wp-content/mw-backups,
  and ss-uploads is now mw-uploads. Existing backups are moved across
  automatically the first time you open the admin - nothing is lost.

= 2.00.39 =
* "Restore from a file" is now "Migrate a site", with the instructional text
  removed - the drop area is directly under the heading.

= 2.00.38 =
* Fixed a parse error introduced in 2.00.37 that made every MetaWrench admin
  screen fail with HTTP 500. Removing the Upload card had also removed the
  closing tag of the block above it.

= 2.00.37 =
* Backup, Restore and Migrate are now one screen instead of three tabs.
* Removed the "Upload a backup" card: it uploaded the file and then told you to
  reload, without ever restoring anything, and could not handle large files.
  "Restore from a file" replaces it and does the whole job.
* Removed the manual search and replace tool. The import updates the site
  address on its own.

= 2.00.36 =
* IMPORTANT: "Upload & Import" only unpacked the backup and then reported
  success. It never imported the database, swapped the site address or copied
  the files. It now runs the full sequence and shows which step it is on.
* The site address is swapped automatically during the import, so Step 3 is no
  longer a required step - it is there for extra replacements only.

= 2.00.35 =
* IMPORTANT: migration went straight to extracting and never took a rollback
  copy, so a migration that stopped partway left the site with no way back. It
  now takes one first, and runs the same pre-checks as a restore.
* If a migration stops partway, the explanation appears in the Site migration
  card itself, with the same dismiss button.
* Migration progress bar is green, and the page recovers without a refresh.

= 2.00.34 =
* Removed the "Restore a Backup" intro card. The Restore tab now goes straight
  to Available Backups and Upload a backup.

= 2.00.33 =
* The "restore stopped" message now appears inside the card you started the
  restore from - under Available Backups, or under Upload a backup - in the
  same place the progress bar was, instead of at the top of the page.
* It can be dismissed with an X, and stays dismissed.

= 2.00.32 =
* If a restore stops partway, the page now works out what happened on its own
  and shows the explanation - no need to refresh manually.
* Rollback copies left in the old location by earlier versions are moved out of
  Available Backups automatically.

= 2.00.31 =
* Fixed: the rollback copy taken before a restore appeared in Available Backups
  as though it were one of your own backups. Rollback copies now live in their
  own folder, are never listed, and are cleared out after 24 hours so a crashed
  restore cannot leave them piling up.

= 2.00.30 =
* The restore progress bar now appears inside the card you started the restore
  from, instead of in a separate panel at the bottom of the page where it was
  easy to miss. The card expands to make room, and the view scrolls to it.
* Progress bars are green rather than blue, and turn red if the restore fails.

= 2.00.29 =
* IMPORTANT: the Restore button in Available Backups had none of the safety
  work added in 2.00.27-28 - that only covered the upload/migrate path. It now
  runs the same pre-checks, takes a rollback copy of the database first, and
  puts it back if the restore stops partway.

= 2.00.28 =
* If a restore stops partway, the Backup screen now explains why - out of time,
  out of memory, out of disk space or a database error - instead of leaving the
  page spinning with no explanation.
* It also confirms whether the safety rollback put your database back, emails
  the admin, and keeps the technical detail behind a "Details for support" link.

= 2.00.27 =
* Restore now checks the server can finish the job before it starts. If there
  isn't enough disk space, or PHP can't unzip, or wp-content is read-only, it
  refuses up front and says why - rather than stopping halfway with the site
  already overwritten.
* Scheduled backups check for free disk space before writing. If there isn't
  room the backup is skipped, logged, and the admin is emailed (at most once a
  day) instead of leaving a truncated zip behind.


= 1.0.0 =
* Initial release
* Global settings page with business info, locations, service areas, hours, and social profiles
* Single and multi-location support with per-location hours
* Per-page meta title and description with live character counters
* Page/post title character counter injected on the edit screen
* Multi-schema support: WebPage, LocalBusiness, Service, FAQPage, Article, BlogPosting, Product, AboutPage, ContactPage
* Service schema with per-page service area checkboxes
* FAQ schema with dynamic question and answer builder
* Canonical URL, noindex, and nofollow controls per page
* Open Graph image picker using the WordPress media library
* Twitter/X card fields with fallback to meta fields
* Organization schema output on every singular page
* Clean JSON-LD output with no render-blocking

== Upgrade Notice ==

= 1.0.0 =
Initial release. No upgrade path needed.
