=== Australcode Uniform Images ===
Contributors: australcode
Tags: woocommerce, product images, image resize, avif, thumbnails
Requires at least: 6.5
Tested up to: 7.0
Requires PHP: 8.2
Stable tag: 0.34.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Make WooCommerce product images the same size without cropping. Smart padding plus AVIF/WebP/JPEG delivery that survives any page cache.

== Description ==

**Australcode Uniform Images** solves the classic WooCommerce catalog problem: product photos with different proportions that the theme crops brutally to align the grid. This plugin makes them uniform without cropping — it adds smart padding over a colored canvas (white by default), optionally trims the uniform background first to normalize inconsistent margins, and delivers each thumbnail in AVIF + WebP + JPEG using the `<picture>` element.

= Key differentiators =

* **No accidental crops** — the entire product fits inside the thumbnail, always. Compare with "smart crop" plugins that guess the subject and sometimes fail on small or asymmetric products.
* **Real `<picture>` markup** — multi-source with AVIF + WebP + JPEG fallback. **Survives any page cache** (LiteSpeed, WP Rocket, Cloudflare APO, W3 Total Cache) because it does not use `Vary: Accept`. The browser picks the optimal format; the cache serves HTML without negotiation.
* **4-tier quality preset** — Maximum / High / Standard / Economy with per-format values calibrated (e.g. Standard = AVIF Q60, WebP Q82, JPEG Q85 — visually indistinguishable from the original at half the size).
* **HPOS-ready from day one** — declares `custom_order_tables` and `cart_checkout_blocks` compatibility without any setup.
* **Cloudflare Image Transformations (CIT)** — optional integration to serve via Cloudflare edge (`/cdn-cgi/image/format=auto`) with automatic availability detection on your zone. If CIT is not enabled on your CF plan, the plugin keeps serving local derivatives without breaking anything.

= Free (no license required) =

* **Bulk regenerate with Action Scheduler** — async processing resumable after worker death, error classification (`oom`, `missing_original`, `engine_failed`, `fs_permission`), scope filters (`auto` = products only based on Settings; `all-used` = every image referenced in posts/products/Bricks templates/terms).
* **Embedded sample test** — processes 1-5 images on demand and shows the before/after grid with bytes and % savings. Available on the **Settings** page ("Quick preview") and on **Bulk regenerate** ("Generate sample").
* **Health page with CDN detection** — engine diagnostics, encoders, filesystem permissions, detected competing plugins (Smush/EWWW/ShortPixel/Imagify), Cloudflare Polish status (alerts if active — incompatible with the plugin's AVIF/WebP), Image Prioritizer (Performance Lab) status with automatic detection.
* **Basic WP-CLI** — 9 commands: `wp acimg health`, `wp acimg stats`, `wp acimg regenerate`, `wp acimg derivatives <id>`, `wp acimg purge-cache`, `wp acimg restore-originals`, `wp acimg migrate-from-sir`, `wp acimg reset`. `wp acimg doctor` requires a license.

= Pro (license required) =

* **Cloudflare Image Transformations (CIT) delivery** — emits `<img>` with CIT URLs (`/cdn-cgi/image/format=auto`) instead of local derivatives when CIT is enabled on your zone. Defensive automatic fallback to the normal path if CIT is unavailable.
* **Media Library audit + cleanup** — orphan image detector that understands **Bricks Builder** (PHP serialize), `wp_termmeta`, WooCommerce shortcodes, custom meta. Trash mode with typed confirmation, immutable audit log, and an `untrash` endpoint for rollback.
* **Image Health Monitor** — weekly cron that scans for missing derivatives, stale derivatives, Cloudflare Polish conflicts, and storage usage. Email digest to the admin only when there are actionable findings.
* **AI Alt Text BYOK** — alt text generation per attachment via OpenAI Vision (`gpt-4o-mini`) or Anthropic Vision (`claude-haiku-4-5`). Bring Your Own Key (no Merchant of Record over AI costs). Context enrichment with WooCommerce product title + category + brand.
* **Catalog Watch + Auto-Heal** — daily cron that detects ghost derivatives (DB rows without files), orphan files (files without DB rows), products with broken thumbnails. Auto-heal with dry-run preview + strict path validation. Persistent admin notice when there are actionable issues.
* **Multi-Site Manager** (Agency tier+) — centralized dashboard to monitor up to 25 client sites from a single wp-admin. BYO Application Password, aggregated metrics (derivatives + bytes + issues), automatic hourly sync + ad-hoc. No external services: direct wp-admin ↔ wp-admin communication via native REST.
* **CIT Cost Analyzer** — daily pull from Cloudflare GraphQL Analytics API. MTD cost + monthly projection + peak day to identify spikes. BYO API token (CF authenticates directly).

= Integration with other plugins =

* **Image Prioritizer / Performance Lab** — Australcode Uniform Images detects `fetchpriority="high"` and propagates correct loading/decoding to the final `<picture>`. Native LCP optimization.
* **Bricks Builder** — supports `wp_get_attachment_image` and optional output buffer rewriter for themes/page builders that bypass that filter.
* **WP Rocket / a3 Lazy Load** — compatible with `data-src`/`data-sizes` lazy loaders (reads both if present).

= v1.0 non-goals =

* **Smart crop** — would break the "no cropping" promise. If you need that, this plugin is not for you.
* **SaaS-only / phone home** — the plugin is 100% local. All optimization happens on your own server.
* **JPEG XL** — format still immature in browsers. We will reevaluate in 2027.

= How it compares =

| Concern | Smush / ShortPixel / Imagify | Australcode Uniform Images |
|---|---|---|
| Product photo cropping | Smart crop (algorithm guesses) | None — pads to a uniform canvas, full product always visible |
| Modern formats | WebP / AVIF (Pro/paid tiers) | AVIF + WebP + JPEG via `<picture>` element |
| Page cache | `Vary: Accept` (breaks LiteSpeed, WP Rocket, Cloudflare APO) | `<picture>` element (cache-safe by design) |
| Pricing model | $5-15/month SaaS subscription | One-time license, no SaaS dependency |
| WooCommerce-specific | Generic optimizer | Built for product grids — reads `wp_get_registered_image_subsizes()`, respects WC thumbnail_cropping setting |
| Processing location | SaaS server (your images leave) | 100% local on your server |

The other plugins optimize **any image**. Australcode Uniform Images does **one job extremely well**: uniform product thumbnails for WooCommerce grids, served cache-safe. If you already have one of the big plugins active, you can run Australcode side-by-side — it only touches images on registered WooCommerce sizes.

== Installation ==

= Via WordPress Admin (recommended) =

1. Go to **Plugins → Add new** in your WP admin, search for "Australcode Uniform Images" and click Install + Activate.
2. Navigate to the new top-level menu **Australcode Image → Settings** and choose your quality preset (default Standard works well for most cases).
3. Go to **Australcode Image → Bulk regenerate**, run a Sample Test with one product to validate visually, then click "Start bulk" to process the full catalog.

= Via WP-CLI =

```
wp plugin install australcode-uniform-images --activate
wp acimg health           # verify everything is OK
wp acimg regenerate       # bulk dry-run
wp acimg regenerate --start --yes   # starts bulk in the background
```

== Frequently Asked Questions ==

= Is it compatible with WooCommerce HPOS (High-Performance Order Storage)? =

Yes. Compatibility with `custom_order_tables` and `cart_checkout_blocks` is declared from day one via `before_woocommerce_init`. No manual configuration required.

= Does it work with Cloudflare? =

Yes, and well. The plugin emits `<picture>` with `<source>` per MIME type — **it survives Cloudflare APO** and any page cache perfectly because it does NOT use `Vary: Accept` (which would break caching).

If you have **Cloudflare Pro+**, you can enable **Image Transformations** from Settings → CDN edge. The plugin automatically detects whether CIT is available on your zone and emits `/cdn-cgi/image/` URLs that CF transforms on-demand with `format=auto`.

**Caution with Cloudflare Polish**: if active, it re-compresses the AVIF/WebP that the plugin already optimized — double compression equals visual artifacts. The plugin detects this on the Health page and alerts you with an admin notice. Recommendation: disable Polish, keep APO.

= Which image engine does it use? =

**Imagick** (preferred) with `libheif` for AVIF. **GD** as fallback. **libvips** is wired in the code but requires bundled binaries (not included in v0.x — future sprint). The active engine and its capabilities (read/write per format) appear on **Health → Image engines**.

= Do I need anything special for AVIF? =

Imagick compiled with `libheif` (common on modern hosts: Kinsta, WP Engine, SiteGround, recent Cloudways). If Imagick does not support AVIF, the plugin still emits WebP + JPEG and the Health page reports the limitation. **You do not get stuck without anything** — you just lose the most efficient format.

= How much storage does it use? =

Estimate: ~700 KB of derivatives on average per original image at Standard preset (varies a lot by content type — product photos with a uniform background compress better). For a catalog of 1,000 products with one image each, that's ~700 MB of derivatives. The plugin **NEVER touches the original** on disk, so you need space for original + derivatives.

= How do I regenerate after changing settings? =

Change settings on **Australcode Image → Settings**, save (the plugin shows a modal warning you that existing derivatives are invalidated), and then go to **Bulk regenerate** or run `wp acimg regenerate --start --yes`. The plugin keeps serving the old derivatives until regeneration completes — **zero visual downtime**.

= Conflicts with Smush / EWWW / ShortPixel / Imagify? =

Yes, **disable those plugins** before installing Australcode Uniform Images. The Health → Doctor page detects them and alerts you. The typical conflict is that those plugins also hook into `wp_get_attachment_image` and rewrite the HTML — the output ends up inconsistent.

= Does it have WP-CLI? =

Yes, 9 commands under `wp acimg <subcommand>`: health (CI smoke test), stats, regenerate, derivatives <id>, purge-cache, restore-originals, migrate-from-sir, reset (resets all persisted state without deactivating) — all available in the free version. `wp acimg doctor` (extended diagnostics) requires a Pro license. Each command supports `--format=json`. `wp help acimg` lists them all.

= How do I report a bug or request a feature? =

Open a support thread in the WordPress.org plugin support forum (linked from the plugin page sidebar). For Pro license-related issues, contact support@australcode.io.

== External services ==

This plugin connects to third-party services **only when you explicitly enable
the corresponding optional feature**. The free, core functionality (uniform
images, AVIF/WebP/JPEG `<picture>`, bulk regenerate, health) runs **100% on your
own server and contacts no external service**.

= Lemon Squeezy (license validation — Pro) =

When you activate, validate, or deactivate a Pro license, the plugin sends your
**license key** and your **site domain** to the Lemon Squeezy License API
(host `api.lemonsqueezy.com`, path `/v1/licenses/`). A background cron revalidates
the license roughly every 24 hours. This only happens if you enter a license
key. No data is sent in the free version.

* Terms of Service: https://www.lemonsqueezy.com/terms
* Privacy Policy: https://www.lemonsqueezy.com/privacy

= OpenAI / Anthropic (AI Alt Text — Pro, Bring Your Own Key) =

If you enable **AI Alt Text** and provide your own API key, the plugin sends the
**image** (as a base64 data URI) plus the related **WooCommerce product context**
(title, category, brand) to the provider you choose, to generate alt text:

* OpenAI Vision (`gpt-4o-mini`) — host `api.openai.com`, path `/v1/chat/completions`
  * Terms: https://openai.com/policies/terms-of-use
  * Privacy: https://openai.com/policies/privacy-policy
* Anthropic Vision (`claude-haiku-4-5`) — host `api.anthropic.com`, path `/v1/messages`
  * Terms: https://www.anthropic.com/legal/consumer-terms
  * Privacy: https://www.anthropic.com/legal/privacy

This runs only when you trigger alt text generation and only with the key you
supply. The plugin is not a Merchant of Record for these AI costs (BYOK).

= Cloudflare (Image Transformations + Cost Analyzer — Pro) =

If you enable **Cloudflare Image Transformations (CIT)**, image URLs of your own
site are served through Cloudflare's edge (`/cdn-cgi/image/...`) so Cloudflare can
transform them on-demand. If you enable the **CIT Cost Analyzer**, the plugin
queries the Cloudflare GraphQL Analytics API
(host `api.cloudflare.com`, path `/client/v4/graphql`) with the API token **you provide**
and your zone identifier, to report usage and cost. The plugin also performs a
`HEAD` request to one of **your own** derivative URLs to detect whether CIT and
Cloudflare Polish are active on your zone (no third-party data is sent in that
detection).

* Terms of Service: https://www.cloudflare.com/terms/
* Privacy Policy: https://www.cloudflare.com/privacypolicy/

== Upgrade Notice ==

= 0.33.1 =
WordPress.org compliance and listing fixes. No user-facing behavior changes. Safe in-place upgrade.

= 0.23.9 =
Uniform-by-default for all image sizes. After upgrading, run a Bulk Regenerate (Australcode Image → Bulk regenerate) to rebuild derivatives with the new uniform default.

== Screenshots ==

1. **Dashboard** — catalog metrics: processed images, generated derivatives, storage used, % savings when serving AVIF vs original, active quality preset, current bulk run.
2. **Settings** — 4-tier quality preset, per-size override, visual canvas color picker, sticky save bar that appears when changes are detected.
3. **Bulk regenerate** — scope dropdown with real-time count, dry-run with storage and time estimation, embedded sample test with before/after grid.
4. **Health** — full diagnostic: active engine + codec capabilities matrix, DB tables with row count, applied migrations, CDN edge (Cloudflare Polish/Mirage + Image Transformations detection).
5. **Clean library** (Pro) — orphan image audit with Bricks Builder + wp_termmeta + WooCommerce shortcodes detection. Trash mode with typed confirmation and audit log.
6. **Catalog Watch** (Pro) — daily cron that detects ghost derivatives, orphan files, and products with broken images. Auto-heal with dry-run preview before applying.
7. **Multi-Site Manager** (Agency / Network) — centralized dashboard to monitor up to 25 sites (Agency) or 1000 sites (Network) of clients. BYO Application Password, aggregated metrics + status badges.
8. **License management** (Pro) — activate, validate, and deactivate your license; per-tier activation counter; one-click upgrade path to a higher tier.

== Changelog ==

= 0.34.0 =
* Pro: nuevos overrides de modo por proporción de imagen. Por cada tamaño principal de WooCommerce puedes elegir cómo se procesa cada proporción (cuadrada 1:1, 4:3, 16:9, vertical, etc.): uniforme con lienzo, proporcional sin recorte, u omitir. Por ejemplo, mantener las imágenes cuadradas en "proporcional" mientras el resto del tamaño sigue su modo base. Disponible en todos los planes pagados.
* El override por proporción se integra al pipeline de generación (subida y regeneración en lote) y al cálculo de invalidación de derivados, por lo que cambiarlo regenera solo lo necesario.
* Visibilidad: el Dashboard ahora muestra cuántas imágenes se saltaron por el filtro de contexto (no asociadas a un post type procesable), con un acceso directo para ajustar el filtro. Evita la confusión de "subí una imagen y no pasó nada".

= 0.33.2 =
* Seguridad: los endpoints REST que operan sobre adjuntos ahora verifican la capability de edición/borrado por cada attachment (current_user_can edit_post/delete_post), además del gate general manage_woocommerce.

= 0.33.1 =

WP.org re-submission compliance + listing polish. No user-facing behavior changes — safe in-place upgrade.

* **External services URLs no longer pingable as 404** — the readme's "External services" section referenced API endpoints (`api.lemonsqueezy.com`, `api.openai.com`, `api.anthropic.com`, `api.cloudflare.com`) as full URLs. WP.org's review scanner pings every URL in the readme and flagged the Lemon Squeezy endpoint as a broken Terms/Privacy URL. Endpoints are now declared as host + path (not full clickable URLs); Terms/Privacy links remain and resolve.
* **Explicit output-buffer close** — `OutputBufferHook` (opt-in feature, off by default) now closes its `ob_start()` buffer explicitly on `shutdown`, guarded by `ob_get_level()`. Same rewritten output, paired open/close as WP.org guidelines expect. WP 6.5+ compatible.
* **Banners + branding** — re-generated WP.org banners with the current name (Australcode Uniform Images) and version; renamed the last legacy global variable in the bootstrap file.
* **Listing polish** — sharper short description and tags for the target niche, added Upgrade Notice, fixed screenshot #8 caption, aligned WP-CLI command count between Description and FAQ.

Internal: 296/296 unit tests passing.

= 0.33.0 =

Lote C — WP.org compliance fixes residuales para re-submission. Cierra los puntos del review WP.org del 20-May que no quedaron cubiertos por el rebrand inicial. Cero breaking changes a nivel de usuario.

* **`league/container` bumped 4.2.5 → 5.2.0** — la librería de DI bumpea major sin breaking changes en la API que el plugin usa (`add`, `addShared`, `delegate(new ReflectionContainer(true))`, `get`). 296/296 unit tests passing post-bump; runtime integración validada en wp-sandbox-staging.
* **Removed GitHub URLs from readme.txt** — la documentación pública del plugin (Installation, FAQ "How do I report a bug?", footer) apunta ahora exclusivamente a recursos accesibles públicamente: WP.org plugin page para download/install, support forum WP.org para reportes de issues free, support@australcode.io para Pro license issues. El repo de desarrollo es privado por diseño; los users no necesitan acceso al código fuente para usar el plugin (que viene con el ZIP del listing WP.org).
* **Internal repo rename** — el repo de desarrollo se renombró a `australcode-uniform-images` para coherencia con el slug del plugin (era `conecta-image-resize` del codename interno legacy). Cambio invisible al usuario final; solo afecta workflow del autor.

Internal: 296/296 unit tests passing. Plugin Check sobre v0.33.0 esperado 0 errors / 155 warnings (baseline S3 mantenido desde v0.23.14).

= 0.32.0 =

Lote B — fix de 4 hallazgos P1 de UX clarity de la prueba humana en drbrowns prod sobre v0.31.0. Cero breaking changes — solo correcciones de copy + tooltips. Safe in-place upgrade.

* **Dashboard header — botones con contexto** — los CTAs del header del Dashboard pasaron de etiquetas neutras ("Generar muestra" / "Configuración") a etiquetas con flecha de navegación + tooltip explicativo: "Probar con muestra →" (con tooltip "Probar el plugin con 1–5 imágenes antes de procesar el catálogo completo") y "Configuración →" (con tooltip "Calidad, formatos, tamaños y delivery del plugin"). La flecha indica que llevan a otra página; el tooltip da el porqué.
* **Settings — copy de heurística sizes legible** — el lede "Por cada size, elige cómo el plugin lo procesa. 'Auto' usa la heurística por defecto (sizes con crop=true → uniforme; proporcional → proporcional; legacy/plugins viejos → omitir)" ahora habla en idioma de usuario: "las imágenes pensadas para grilla (cuadradas) se uniforman; las pensadas para flujo (rectangulares) mantienen su proporción; las creadas por plugins antiguos que ya no usas se ignoran". Misma lógica, vocabulario no-técnico. Footer del table también re-escrito.
* **Bulk page — leds técnicos rewriteados estilo WP Rocket** — los tres helpers principales (header lede, "Generar muestra" lede, "Procesamiento masivo" lede + helper de cantidad) pasaron de mencionar "Action Scheduler", "background", "scope", "bypasea", "lotes de 5 cada 5 segundos" a explicaciones orientadas a beneficios + caveats sin jargon: "corre en segundo plano — puedes cerrar esta página, el trabajo continúa solo", "ignora tus filtros de Configuración a propósito", "Cada una toma unos 5–15 segundos en alta calidad".
* **Catalog Watch — flujo 3-step explícito + tooltips** — los 3 botones "Escanear ahora" / "Vista previa de reparación" / "Aplicar reparación" ganaron tooltips per-botón explicando qué hace cada uno, y debajo del row hay un nuevo helper "Flujo recomendado: 1) Escanear para detectar problemas. 2) Vista previa para ver qué se borraría (no modifica nada). 3) Aplicar para ejecutar la limpieza cuando estés conforme". El usuario ya no tiene que adivinar qué hace "vista previa" vs "aplicar".

Internal: 296/296 unit tests passing. Plugin Check sobre v0.32.0 esperado 0 errors / 155 warnings (baseline S3 mantenido desde v0.23.14).

= 0.31.0 =

Lote A — fix de 3 hallazgos P0 visuales de la prueba humana en drbrowns prod sobre v0.30.0. Cero breaking changes — solo correcciones de layout y copy. Safe in-place upgrade.

* **Notice positioning fix** — agrega el marker `<hr class="wp-header-end" />` después del header de cada admin page (10 pages: Dashboard, Configuración, Regenerar, Limpiar, Errores, Salud, Licencia, Multi-Site, CIT Cost, Catalog Watch). WordPress usa este marker para posicionar `admin_notices` correctamente; sin él, los notices del plugin (ej. `CatalogIssuesNotice` con 80892 issues) quedaban atravesados visualmente entre el título y los badges de versión/estado.
* **WP Customizer path correcto** — el copy del banner "Para que tus productos se vean uniformes en la grilla" ahora muestra la ruta completa `Apariencia → Personalizar → WooCommerce → Imágenes de producto → Cropping → 1:1 (cuadrado)` en vez de la versión truncada anterior. Refleja la navegación real de WP-Admin para que el usuario encuentre el setting al primer intento.
* **AI Alt Text section layout** — la sección "Alt text con IA" (Pro) ahora sale del grid `acimg-settings-layout` (TOC sticky + form 1fr) y se renderiza como sibling `<div class="acimg-settings-aux">` debajo, con ancho completo y background blanco. Antes quedaba en el slot `__full` del grid pero el aside sticky de TOC la tapaba visualmente; también usaba clases huérfanas (`acimg-section`) que no existen en el CSS — ahora usa `acimg-surface` como el resto de cards de Settings, manteniendo coherencia visual.

Internal: 296/296 unit tests passing. Plugin Check sobre v0.31.0 esperado 0 errors / 155 warnings (baseline S3 mantenido desde v0.23.14).

= 0.30.0 =

Final commercial-ready release — agrupa cierre del backlog perfeccionista post-audit 2026-05-25. Cero breaking changes acumulados entre v0.25 → v0.30, todos los bumps fueron polish + ADDs + transformations defensibles. Safe in-place upgrade.

* **Re-capturados los 8 screenshots WP.org** (1200×900) con todo el polish aplicado: icon SVG header branded + TOC lateral sticky en Settings + upgrade card en License + KPI "Ahorro estimado" + 9 commands WP-CLI + 4 Pro pages funcionales. Los screenshots muestran al usuario potencial el estado real del plugin con polish post-v0.25.
* **Skip jump v0.29.0** — version skip intencional para reflejar el major polish accumulated (v0.25 → v0.30 incluye 5 bumps: v0.26 visual identity + v0.27 Settings TOC + v0.28 License upgrade + el cierre v0.30 con los screenshots updated + readme final).

Internal: 296/296 unit tests passing. Plugin Check sobre v0.30.0 esperado 0 errors / 155 warnings (baseline S3 mantenido desde v0.23.14, 6 meses estable).

= 0.28.0 =

License page upgrade path (Sprint T4 direct, T-003 audit 2026-05-25). Cero breaking changes — additions limpios.

* **License page — upgrade path card** — converts the License page from "pure information" to "information + contextual actions". When the user has a tier below Network, a new card appears below the existing license info showing upgrade options: tier name + sites count + Pro badge + CTA "Upgrade a X →" with direct deep link to the Lemon Squeezy upgrade flow per tier. Mapping: Single → Studio + Agency / Studio → Agency + Network / Agency → Network. Help text post-card explains "Lemon Squeezy issues your new license key instantly; apply the upgrade from this same page by pasting the new key and activating".
* **Network tier max state** — when the user already has Network tier (top), the upgrade card is replaced by a "Tier máximo activo" card with `acimg-badge--pro` Network + thank-you message + "✉ Contactar soporte priority" CTA mailto link to support@australcode.io.
* **New utility CSS** `.acimg-upgrade-grid` + `.acimg-upgrade-card` — responsive grid layout (auto-fit, minmax 220px) collapsing to 1 column on mobile. Each card uses existing DS tokens (`--bg-surface`, `--border-default`, `--radius-md`, `--fs-15`, etc).

Internal: 296/296 unit tests passing. Plugin Check sobre v0.28.0 esperado 0 errors / 155 warnings (baseline S3 mantenido).

= 0.27.0 =

Major UX upgrade for the Settings page (Sprint T1 mockup-first). Cero breaking changes — el HTML interno de cada section se preserva exacto. Safe in-place upgrade.

* **Settings page — lateral sticky TOC + smooth scroll** — the Configuration page used to be a 3500px linear scroll on mobile (10 sections back-to-back). Now there's a sticky lateral navigation (left sidebar 200px desktop / `<select>` dropdown mobile) with smooth-scroll on click and IntersectionObserver-driven active state highlighting (the section currently centered in viewport gets accent bg in the TOC). Deep links to specific sections via URL hash also work (e.g. `#acimg-anchor-ai-alt-text` lands the user directly on AI Alt Text section). The Dashboard CTA card "Configurar ahora →" now lands precisely on the AI Alt Text section.
* **DS integration** — the new layout uses tokens existing en `admin.css`: `--bg-surface`, `--border-default`, `--bg-selected`, `--accent-600`, `--radius-md`, `--radius-xs`. Sticky position with `top: 32px` offset for WordPress admin bar. Max-height `calc(100vh - 64px)` con `overflow-y: auto` para sections muy largas. Section anchors (`.acimg-section-anchor`) usan `scroll-margin-top: 64px` para que el hash navigation no quede oculto bajo el admin bar.
* **Mobile responsive** — el TOC sticky collapsa a `<select>` dropdown con label "Ir a sección…" cuando viewport ≤ 900px. Smooth scroll funciona igual.

Internal: 296/296 unit tests passing. Plugin Check sobre v0.27.0 esperado 0 errors / 155 warnings (baseline S3 mantenido). 1 nuevo asset JS encolado en Settings page (`acimg-settings-toc`).

= 0.26.0 =

Visual identity boost (Sprint H1 cross-leverage Bsale patterns). Cero breaking changes, cero migrations — safe in-place upgrade.

* **Branded icon SVG in admin page header** — every admin page now displays the plugin's mark icon (Concept C — image inscribed in a canvas) on the left side of the title, with accent color and a subtle background. Applied via CSS pseudo-element so all 15+ page header occurrences benefit automatically without per-page changes. Cross-leverage pattern from `australcode-bsale` (audit familia 2026-05-26).
* **Highlighted CTA card for Pro features needing configuration** — when the user has Pro license active but AI Alt Text is not configured, the Dashboard now shows a left-accent CTA card guiding them to "Configurar ahora →" with a deep link to the Settings AI Alt Text section. Reduces silent friction where the user pays Pro and doesn't find how to activate the feature.
* **DashboardPage constructor** — 2 new deps autowired by `League\Container`: `LicenseGate` + `AiConfigRepository` (both already registered in the container, zero migration impact).

Internal: 296/296 unit tests passing. Plugin Check sobre v0.26.0 esperado 0 errors / 155 warnings (baseline S3 mantenido).

= 0.25.0 =

Transformations ADDs (no breaking changes, no migrations) — Sprint E del backlog post-v0.24.0 audit 2026-05-25.

* **New (Dashboard) — KPI "Ahorro estimado" en hero** — un 5to KPI card al lado de "Ahorro al servir AVIF (%)" ahora muestra el ahorro **en bytes humanos** (ej. "50 MB"). Heurística: `sum(original_bytes - avif_bytes)` over all processed attachments, asume ~99.5% visitors compatibles con AVIF (default 2026). Convierte el dashboard de "estado" a "valor demostrado" — el usuario ve cuánto le ahorró el plugin en cifras concretas, no solo en %.
* **Documented (Settings) — modal pre-save "Esto invalidará N derivados"** — el modal ya existía desde v0.23.x pero no estaba destacado en el listing. Ahora documentado: al click "Guardar configuración" con cambios que afectan el `pipeline_hash` (padMode, padColor, trim, quality preset, AVIF speed, formats enabled, etc.), aparece dialog con: count de derivados a invalidar, bytes en storage, lista exacta de campos que cambiaron, link a "Regenerar en lote" para reconstruir, opciones "Cancelar" / "Guardar de todos modos". Cero sorpresa para el usuario al cambiar settings.

Internal: 296/296 unit tests passing. Plugin Check sobre v0.25.0 esperado 0 errors / 155 warnings (baseline S3 mantenido). El método privado `estimateSavings()` en `DashboardPage` retorna un campo nuevo `bytes_saved` además de los existentes (sin breaking changes — campos viejos preservados).

= 0.24.1 =

Polish + DS coherence pass — no functional changes, no migrations, safe in-place upgrade.

* **Polish — status indicators** — 6 emoji occurrences in admin UI (white check, warning, red circle, lock) replaced with the canonical `.acimg-dot--*` color-coded indicators. Reasons: cross-OS rendering consistency, no impact on flex/grid alignment, screen reader friendlier (no "white heavy check mark" noise). Affected: Settings flash notice, Catalog Watch issues empty state, Multi-Site issues column + help list.
* **Polish — admin copy es-CL canonical** — 8 strings switched from hybrid EN/ES to neutral Chilean Spanish: "Vista previa heal" → "Vista previa de reparación", "Aplicar heal" → "Aplicar reparación", "Auto-Heal" → "Auto-reparación", "Sync all" → "Sincronizar todos", "Refresh ahora" → "Refrescar ahora", "Sample test" → "Generar muestra" (3 occurrences in Dashboard).
* **Polish — voseo cleanup** — `wp acimg purge-cache --all` confirmation prompt no longer uses Rioplatense "CONFIRMÁS". Now says "¿Confirmas borrar TODOS los derivados?".
* **DS — `.acimg-btn--loading` utility** added in `assets/css/admin.css`. Spinner reusable for any AJAX button (reuses existing `@keyframes acimg-spin`). Companion helper `acimgWithLoading(btn, asyncFn)` exposed globally via the new `assets/js/btn-loading.js` (always enqueued on plugin pages).
* **DS — `surface` vs `card` convention** documented inline in `admin.css` to prevent drift. `.acimg-surface` = main section with `<h2>` visible. `.acimg-card` = banner / sub-content / status panel without title.
* **CLI — `wp acimg purge-cache --dry-run` flag** added as explicit alias of the safe default behavior (mutually exclusive with `--yes`). Aligns with standard WP-CLI convention for destructive commands.
* **Docs — readme accurately lists 9 WP-CLI commands** (previously said 8, missing `wp acimg reset` which resets all persisted plugin state without deactivating). FAQ entry expanded with the same correction.

Internal: 296/296 unit tests passing. Plugin Check 0 errors, 155 warnings (S3 baseline maintained).

= 0.24.0 =

* **New (Pro) — AI Alt Text admin UI** — the BYOK alt text generation feature (OpenAI gpt-4o-mini / Anthropic claude-haiku-4-5) now has three dedicated UI surfaces:
  * **Settings → "Alt text con IA"** section: provider select, masked API key field, language picker, "Save" + "Generate test" buttons that hit `/alt-text/generate/<lastAttachmentId>` with `apply: false` for instant preview.
  * **Attachment editor → "Alt text con IA"** field: "Generate with AI" button next to the native Alt text field; preview before apply; one click fills the native field. Works in `post.php?action=edit` and the media modal.
  * **Media library bulk action**: "Generate alt with AI" entry in `upload.php?mode=list` bulk dropdown; processes selected attachments in batches of 5 with anti-rate-limit pacing (OpenAI 10rpm tier 1 / Anthropic 50rpm).
* All three surfaces are gated server-side (Pro license active + AI config present + `manage_woocommerce` capability) and gracefully hide for Free users.
* **Fix (P0 WP.org compliance)** — `uninstall.php` cleanup query was matching the legacy `pir_` prefix instead of the current `acimg_` after the rebrand, leaving up to 8 options (including encrypted license key and AI API key) residual in the DB after uninstall. Now matches `acimg_%` correctly.
* **Fix (P0 mobile polish)** — format chips (JPEG / WebP / AVIF) in the Dashboard "Configuración actual" card were breaking across two lines on mobile ("JPE G" / "WEB P" / "AVI F") because `.acimg-chip` had no `white-space: nowrap`. Now chips stay on one line and wrap to a new row only if there isn't enough horizontal space.
* **Fix (CLI copy)** — `wp acimg regenerate` dry-run success message used a Rioplatense Spanish conjugation ("pasá") instead of neutral Chilean Spanish ("pasa"). Corrected.
* **Fix (defensive)** — `Dashboard::sumOriginalBytesProcessed()` now uses `unserialize($x, ['allowed_classes' => false])` for `_wp_attachment_metadata` parsing, matching the defensive pattern already in `SirMigrator` and `MediaAuditController` (regression: 1 of 7 occurrences was missing the guard).

= 0.23.17 =

* **Fix (admin UI)** — info bars and notices no longer render with their text glued to the edges. The `.acimg-card` callout had no padding defined, and the `.acimg-notice` component carried a leftover `::before` accent with negative margins that didn't match the padding. Both are now consistent (proper padding + a single clean left accent).

= 0.23.16 =

* **Fix (Catalog Watch)** — the "Preview heal" and "Apply heal" buttons always showed "Error: desconocido" even when the server responded successfully: the JavaScript read the success/summary fields off the fetch wrapper object instead of the response body. Both handlers now unwrap the response correctly, so the preview/apply report real counts. Scale-independent fix (affected every site).

= 0.23.15 =

* **CLI/UI polish** — user-facing references to the CLI command corrected from the legacy `wp pir` to `wp acimg` across `wp help` output, admin hints, and inline docs (the command was already registered as `acimg`, so the old examples pointed at a non-existent command).
* **Fix** — the first-run onboarding wizard derived its nonce and "completed" flag out of scope, which caused a 403 on the onboarding REST calls and made the wizard re-appear on every Settings visit; both are now derived within the section that renders the wizard.
* **i18n** — translation catalog synced with the renamed CLI references.

= 0.23.14 =

* **WordPress.org compliance (Plugin Check)** — addressed the official Plugin Check errors: `Tested up to` bumped to 7.0; direct filesystem calls migrated to WordPress wrappers (`wp_delete_file`, `WP_Filesystem`, `wp_is_writable`); SQL queries hardened (`%i` for internal table identifiers, `LIKE` wildcards passed as prepared parameters with `esc_like`); `$_GET` input properly unslashed/sanitized.
* **External services disclosure** — added an "External services" section to the readme documenting Lemon Squeezy (license), OpenAI/Anthropic (AI Alt Text, BYOK), and Cloudflare (Image Transformations + Analytics): what data is sent, when, and links to each provider's terms and privacy policy.
* **Branding consistency** — user-facing references to the old menu name updated to "Australcode Image"; landing URLs unified to the canonical domain; `wp help acimg`.
* **Fix** — uninstall now removes the plugin's Action Scheduler actions using the current `acimg_` hook prefix (previously used the old prefix and left orphan rows).

= 0.23.13 =

* **WP.org compliance (enqueue)** — all 12 inline `<script>`/`<style>` blocks in admin pages and notices migrated to `wp_enqueue_script`/`wp_enqueue_style` with `wp_localize_script` for dynamic data. No behavior change; assets now load through the WordPress Scripts/Styles API. The HTML email `<style>` (HealthEmailFormatter) remains inline by necessity (email clients).

= 0.23.12 =

* **UX/UI deep audit follow-up** — 11 findings addressed (0 P0, 5 P1, 4 P2, 2 P3).
* **Human-readable durations**: scan durations show "12 min 7 s" instead of raw "726.896 s". New `DurationFormatter`.
* **Async scan reassurance**: Catalog Watch shows a banner during a running scan ("runs in background, you can close this window") with human status copy. Progress was always preserved server-side via Action Scheduler; now the UI says so.
* **Prominent cleanup CTA**: Dashboard "Storage recuperable" clean-up is now a button with the amount in the label ("Limpiar 3,19 GB →") instead of a tiny 11px link.
* **License badges fixed**: status badges referenced 3 non-existent CSS classes (fell back to grey); added semantic aliases (`--success`/`--danger`/`--neutral`).
* **Voseo cleanup**: removed the 10 remaining rioplatense conjugations the v0.23.7 pass missed.
* **Token migration**: Media Cleanup's 40 hardcoded hex → design-system tokens (dark-mode-ready); Health cron timestamps in site timezone instead of raw GMT; status emojis in tables → semantic dots; remaining English labels translated (CIT Cost, Catalog Watch headers).

= 0.23.11 =

* **Catalog Watch v2 + stale storage cleanup**: new `StaleDerivative` detector finds rows in `wp_pir_derivatives` whose `pipeline_hash` no longer matches the current settings hash. Storage reclaimable is reported in Dashboard, Catalog Watch and Bulk Regen pages. Auto-Healer supports stale removal (Pro feature) — orphans/ghosts remain Free as they are corruption repair, not storage optimization.
* **Dashboard "Storage recuperable" KPI**: permanent card showing reclaimable GB/MB + count of stale derivatives. If clean, shows "Catálogo limpio" with success indicator. Links to Catalog Watch for cleanup action.
* **Catalog Watch scan now async**: `/catalog-watch/scan` returns 202 + `job_id` and enqueues Action Scheduler. Frontend polls `/scan-status`. Fixes timeout on sites with >1000 attachments (Cloudflare 100s limit returned HTML 524 instead of JSON).
* **Health page Catalog Watch cron row**: new row showing the next scheduled run + last completed time + status indicator. Helps diagnose if the daily cron is properly scheduled (Pro), failed last run, or wasn't scheduled at all.
* **License activation hook**: when activating a license via REST `/license/activate`, the plugin now fires `do_action('pir_license_activated')` so other Pro features can initialize side-effects (like scheduling the Catalog Watch daily cron) without waiting for the next admin page load. Fixes the case where a fresh install with license activated later had the daily cron unscheduled.
* **WP-CLI gate refinement**: `wp acimg purge-cache --orphans` is now Free (corruption repair). `--stale` and `--all` remain Pro (storage optimization).

= 0.23.10 =

* **Onboarding wizard error handling**: when the `/onboarding/apply` REST endpoint returns an error (expired nonce, missing capability, security plugin interception), the wizard now shows an inline red error message in step 2 and restores the button instead of silently jumping to the "Configuration applied" screen. Fixes a UX bug where users could see a success message even though the configuration was not saved.
* **JS API wrapper hardened**: new `apiCheck()` helper validates HTTP status + parses JSON body + returns structured `{ ok, data, error }`. Replaces raw `fetch().then(r => r.json())` pattern that did not differentiate between success and error responses (fetch only throws on network errors, not on HTTP 4xx/5xx).
* **Test coverage**: new `OnboardingControllerTest` locks the WP_Error contract (code, message, data.status) that the wizard JS depends on. Future WP API changes that break the shape will be caught at test time.

= 0.23.9 =

* **Uniform-by-default for all image sizes**: Plugin now uniforms every registered size to its declared dimensions by default, regardless of the WP `crop` flag. Before 0.23.9, sizes with `crop=false` (`medium_large`, `woocommerce_single`, `large`) were left Proportional, producing rectangular derivatives that broke the "uniform images without crop" promise. Override per-size with `sizesConfig[<size>] = "proportional"`.
* **First-visit onboarding wizard**: detects WooCommerce `thumbnail_cropping` + registered sizes and applies the recommended config (all uniform) in one click.
* **Health check**: WooCommerce thumbnail cropping (1:1 / custom / uncropped) reported in Health page + dismissible notice when misaligned.
* **Content target default** changed 100 → 85 (15% breathing space, more premium). Existing installs keep their value.
* **Action required after upgrade**: run a Bulk Regen to regenerate derivatives with the new uniform default.

Full history of older versions (0.23.8 and earlier) is preserved in the project's archive — contact support@australcode.io if you need detail on a specific legacy version.
