# Changelog

All notable changes to this project will be documented in this file.

## 0.4.0 - 2026-10-27

### Changed

- **Core pin widened to `>= 2.65.0 and < 4.0.0`:** works with core 3.x (core 3.0 stops creating the module tables; this module's own chain builds its tables). The floor is the first core with the `file_references/0` and `expected_schema/0` callbacks, which now carry `@impl PhoenixKit.Module`.

### Added

- **The module supplies the manifest of its 1 tables** (`expected_schema/0`, `PhoenixKitOG.ExpectedSchema`), so `mix phoenix_kit.repair` and `mix phoenix_kit.doctor` check and repair them again, now from the module's own chain instead of core's manifest. Generated from `PhoenixKitOG.Migrations` by core's `dev_docs/squash/generate_module_manifest.exs`, never edited by hand. Core uses it from the first release after 2.64.1; before that nothing reads it. Repair is additive (it creates what is missing and reports the rest) and checks the tables only while the database is at the chain's current version. No migration.

## 0.3.9 - 2026-10-13

### Fixed

- Deleting a template audits exactly the assignments it removed: they are
  deleted in the same transaction with `DELETE … RETURNING`, so an assignment
  a concurrent transaction moved to another template is no longer logged as
  deleted.
- `Assignments.update/4` returns an error (and the assignments page shows its
  not-found flash) when the row was removed since it was loaded, instead of
  raising `Ecto.StaleEntryError`.
- A shape with an image fill no longer shows OpenFresco's stand-in on a public
  card: an unresolved or remote fill drops the shape, and a static media UUID
  in the fill is inlined like any other image.

## 0.3.8 - 2026-10-13

### Added

- **Assignments** (#14): `Assignments.create/6` (refuses a scope that already
  has an assignment), `update/4` (saves the row it opened; scope is not an
  input), `get_by_uuid/1`, `list_for_template/1`, `count_by_template/0`.
  The admin Edit modal locks scope and group; New points at the existing
  assignment's Edit instead of replacing it.
- The templates list's delete confirmation states how many assignments go
  with the template.
- `[[global]]` entries in the editor's insert menu show their label and
  description.

### Changed

- **Honest public cards.** A wired slot with no value renders as nothing
  rather than `{{Description}}`; a remote http(s) image is left off the card
  (never a stand-in), and the assignments UI refuses a remote URL for an image
  slot. `[[site_url]]`, `[[site_host]]` and `[[page_url]]` come from the
  configured origin (`:canonical_host_resolver`, the `site_url` setting, the
  endpoint URL), never the request's `Host` header.
- `/new` creates the template on its connected mount and navigates to
  `/:uuid/edit`, so a reconnect no longer creates a second template; a taken
  "Untitled N" name moves on to the next number.
- `refine_og/4` also survives an exit, logs identifiers only, and resolves
  the post's group slug to the group uuid for the group tier.
- The render cache key hashes only the globals a scene references and no
  longer includes the module key, so every PNG cached before this release is
  re-rendered once.
- Deleting a template writes one `assignment.deleted` activity row per
  cascaded assignment, in the delete's transaction.
- A typed variable name is normalised to letters, digits and underscores.
- Every `phx-change` form also carries `phx-submit`; the template name
  commits on blur or Enter.
- The JS bundle now carries only the keyboard hook; pointer gestures belong
  to `open_fresco`.

### Fixed

- Backspace in a focused dropdown no longer deletes the selected element.
- `Templates.delete/2` returns `{:error, :not_found}` for a template another
  admin already deleted, instead of raising `Ecto.StaleEntryError`.

## 0.3.7 - 2026-09-25

### Changed

- **Actor and activity logging through core** (#13). The three admin
  LiveViews read the actor with core's `PhoenixKitWeb.Actor.opts/1` (scope
  first, bare user as fallback) instead of a private helper each, and
  `PhoenixKitOG.ActivityLog` calls `PhoenixKit.Activity.log/3`, which never
  raises, in place of its own guards.
- The core pin is now `>= 2.38.0 and < 3.0.0`, the first core carrying
  `PhoenixKitWeb.Actor` and `Activity.log/3`; the conformance test enforces
  the compound form.
- `js_sources/0` carries `@impl PhoenixKit.Module`.
- The admin header renders a section / crumb / title trail: "OpenGraph" as
  the section on every page, "Assignments" and "Edit" (under the template's
  name) or "New template" as titles.
- Dependency lock: `phoenix_kit` 2.40.1, plus routine bumps.

### Fixed

- Post-merge: opening `/admin/open-graph/new` logged an unattributed
  `template.created`; the insert now carries the signed-in actor.
- Post-merge: a failed write logs through core's `Activity.log_failed/3`, so
  the row carries core's `db_pending` marker and triggers no notification
  fan-out. The `failed` / `reason` metadata stays.
- Post-merge: "New template", now the editor's header title, is translated
  in all 7 locales.

## 0.3.6 - 2026-09-17

### Added

- **Module-owned migrations** (#12). `migration_module/0` now returns
  `PhoenixKitOG.Migrations`, so `mix phoenix_kit.update` picks the module
  up. V1 is pure adoption of the two tables core's V154 still creates:
  `CREATE TABLE IF NOT EXISTS` plus catalog-shape guards for the primary
  keys, the name UNIQUE constraint, the FK and the 3 indexes (safe on a
  host that renamed tables or objects), a DROP-first self-heal for an
  invalid canonically named index, and a `pkog_schema:1` marker comment
  on `phoenix_kit_og_templates`. Nothing observable changes; `down/1`
  never drops a table. README gains a "Removing this module" section.
- `Template.column_widths/0` and `Assignment.column_widths/0`, the single
  source for the varchar widths the migration DDL uses.

### Changed

- Dependency lock: `phoenix_kit` 2.13.6 → 2.28.1, plus routine bumps.

### Fixed

- Post-merge: the invalid-index migration tests fake a crashed build with
  `UPDATE pg_index`, which only a superuser may run, so they failed under
  an ordinary test role. They are now tagged `:requires_superuser` and
  excluded when the role lacks `rolsuper`.

## 0.3.5 - 2026-08-22

### Changed

- **The preview-platform switcher is core's `<.nav_tabs>`** instead of a
  hand-rolled `tabs-box` strip. Event tabs dispatch `phx-value-tab`, so
  the handler now matches `%{"tab" => _}` rather than `%{"platform" =>
  _}` (#10). The nine property-panel segmented controls stay radio
  inputs in `phx-change` forms — they are not tabs.

- Dependency lock: `phoenix_kit` 2.13.5 → 2.13.6. This call site uses
  the event-tab API that already shipped in 2.13.5 (`on_change` +
  `phx-value-tab`); it does not need `:border` or verbatim `:patch`.

### Fixed

- Post-merge: `class="tabs-sm p-0.5"` was appended to boxed's baked-in
  `p-1`. `class` can only add, so the two padding utilities fought
  (stylesheet order, not HTML order — `p-1` wins). Dropped `p-0.5` and
  kept `tabs-sm`. Tab ids are now maps at the source, matching every
  other event-tab strip.

### Added

- Regression tests for the payload-key contract: a source-level guard
  that the template is `<.nav_tabs>` matching `%{"tab" => _}` and that
  `platform_tabs/0` ids stay in sync with `@preview_platforms`, plus a
  LiveView click that wraps the stage in Facebook chrome and proves the
  old `%{"platform" => _}` payload is a silent no-op.

## 0.3.4 - 2026-08-21

### Fixed

- **Every editor `phx-change` form now has an `id`**, so LiveView form
  recovery actually works on the canvas — the same gap 0.3.3 closed for
  the three scope/group/template forms (#9).
- **daisyUI 4 `tabs-boxed` → v5 `tabs-box`** on the editor's segmented
  controls. These stay radio inputs in a form, not core's `<.nav_tabs>`
  (#9).
- Post-merge: two canvas form ids named for the wrong field
  (`og-canvas-size-form` was `bg_type`; `og-canvas-bg-type-form` was
  `bg_image_fit`) plus the button style strip (`preset`, not align).

## 0.3.3 - 2026-08-14

### Fixed

- **The assignment modal crashed with `KeyError :preview_loading` as soon as a
  template was picked**, which made the assignments screen unusable end to end:
  an assignment cannot be saved without choosing a template, and choosing one is
  what crashed. `edit_modal/1` is a function component, so `@preview_loading` in
  its body reads *that component's* assigns — and it was neither declared as an
  `attr` nor passed at the call site. Both are now in place (#8, #7).

  It survived four releases because the read sits behind
  `:if={@selected_template}`, and HEEx wraps the whole component invocation —
  attribute expressions included — in that conditional, so the modal rendered
  fine until a template was picked. Nothing warns at compile time.

- **The three scope/group/template forms had no `id`.** A `phx-change` form
  without one silently disables LiveView form recovery.

### Added

- **LiveView test plumbing** (`test/support/{live_case,test_endpoint,test_hooks,
  test_layouts,test_router}.ex`) — this package had none, so nothing could
  render a LiveView under test. Plus a regression guard pinning the invariant
  the bug broke: `edit_modal/1` reads no assign it neither declares nor assigns,
  and the call site passes `preview_loading` through (the attr defaults to
  `false`, so a dropped pass-through would otherwise kill the spinner silently).

### Changed

- Dependency updates: `phoenix_kit` 2.4.0. The `~> 2.0` pin is unchanged —
  nothing here uses core's new `Slug.put_slug/3`.

## 0.3.2 - 2026-08-11

### Changed

- Dependency updates: `phoenix_kit` 2.2.0 and the transitive set it pulls
  (`phoenix` 1.8.10, `hackney` 4.7.3). No source changes in this package.

## 0.3.1 - 2026-08-11

### Added

- **A conformance test for the `:phoenix_kit` requirement** (#6). The pin is a
  contract with consumers and nothing here exercised it: a host depending on
  both this module and a core version the requirement excludes gets an
  unsolvable dependency set and `mix deps.get` fails outright, while this
  repo's own suite stays green. The specific trap is the three-segment form —
  `~> 2.0.x` expands to `< 2.1.0`, so no core 2.1 satisfies it.

  The check reads the resolved dep first and falls back to the committed
  literal in `mix.exs`, so it stays meaningful under the `PHOENIX_KIT_PATH`
  override while still failing when a `path:` dep is genuinely committed.

## 0.3.0 - 2026-08-10

### Changed

- **⚠️ Requires `phoenix_kit ~> 2.0`.** The core pin moved to `~> 2.0`, so this
  release no longer resolves against core 1.7.

  Core 2.0.0 squashes the migration chain into a single `V135` baseline and makes
  V135 the chain's floor: `mix ecto.migrate` now *refuses* on a database below it
  rather than migrating. Check `mix phoenix_kit.status` **before** upgrading. A
  host below V135 must install `phoenix_kit 1.7.236` — the migration bridge, the
  last release carrying the full pre-squash chain — migrate until the reported
  version is at least V135, and only then move to 2.0.

  This package does not call migration internals, so the change is the pin
  itself.

### Added

- **Any module can render an OG image (PR #5).** `PhoenixKitOG.og_image_url/5`
  is now the generic entry point; the previous publishing-only coupling lived in
  the call shape, not in anything essential. Scene storage plus the OpenFresco
  editor and renderer sit behind it.

### Changed

- **`mix precommit` passes again.** It had been failing on `main` — 22 credo
  findings, unchanged since well before this release. Cleared all 22: missing
  aliases for nested modules, six too-deeply-nested function bodies extracted
  into named helpers, and four over-complex dispatch functions in `SceneEdit`
  (`update_element/4`, `update_canvas/3`, `set_anchor/4`, `insert/2`) re-shaped
  from wide `cond`/`case` blocks into function clauses over the same keys. All
  behaviour-preserving; `scene_edit_test.exs` passes throughout.
- Clearing credo let dialyzer run for the first time (`quality.ci` halts at the
  first failure). Six findings surfaced, none a defect — two are artifacts of
  the optional PNG rasterizer backend being absent in this package's own
  environment, four are defensive clauses dialyzer can prove dead from today's
  callers. Recorded in a new `.dialyzer_ignore.exs`, each with its reason.
- The optional calls into `phoenix_kit_publishing` use `apply/3` so dialyzer
  does not report `unknown_function` for a peer that is not a dependency. The
  runtime guards (`Code.ensure_loaded?` + `function_exported?` + `rescue`) are
  unchanged and remain the real contract.

## 0.2.1 - 2026-07-20

### Fixed
- The Hex package's `files:` list never included `priv/`, so the 0.2.0
  package shipped without `priv/static/assets/phoenix_kit_og.js` (the editor
  JS hooks bundle declared by `js_sources/0`) or `priv/gettext/` — any host
  installing from Hex (rather than a path/git dep) failed to compile with
  `js_sources/0 bundle not found`. `priv` is now included in the package.

## 0.2.0 - 2026-07-20

### Added
- Own `PhoenixKitOG.Gettext` backend + `priv/gettext` translations across 7
  locales for the common UI string set (editor long-tail strings ride as
  English fallback pending a translation pass)
- Editor JS hooks (drag/resize + keyboard) now ship via `js_sources/0` as a
  prebuilt bundle (`priv/static/assets/phoenix_kit_og.js`) instead of an
  inline `<script>` — the inline script only ran on a hard page load, so
  navigating into the editor from the Templates list left drag/resize
  unregistered
- On-disk render cache eviction — TTL + count cap, configurable via
  `:cache_ttl_seconds` / `:cache_max_files` / `:cache_prune_probability`
- `Variables.global_label/1` and `Variables.global_description/1` — the
  canonical translated label/description for each OG-owned global variable
- Failed create/update/delete actions now log a `failed: true` activity row
  (previously only successes were audited); a failed update/delete keeps its
  `resource_uuid` so the row still points at which record was targeted

### Changed
- Preview rendering (editor + assignments modal) now runs off the LiveView
  process via `start_async`/`cancel_async`, with a loading state — a
  synchronous rasterize could block the whole modal for up to the 5s backend
  timeout

### Fixed
- SVG injection — every interpolated geometry attribute and the glow-filter
  id are now escaped/sanitized; a crafted canvas can no longer break out of
  an attribute or inject markup
- Canvas `width`/`height` are clamped (`@max_dim` 4000) on both the SVG
  viewBox and the rasterizer's output buffer, preventing an oversized canvas
  from making the rasterizer allocate an unbounded pixel buffer
- Image hrefs resolving to `file://` are dropped instead of passed through —
  was a local-file-read primitive via a CLI rasterizer backend
- `Schemas.Assignment` now declares `unique_constraint/3` on its two partial
  unique indexes, so a concurrent double-save returns `{:error, changeset}`
  instead of raising `Ecto.ConstraintError` and crashing the LiveView
- Served OG images now send `X-Content-Type-Options: nosniff`
- A preview render that crashes or is superseded mid-flight no longer leaks
  an internal error tag into the flash message text

## 0.1.1 - 2026-07-04

### Fixed
- `resvg` is now an optional dependency instead of required. Every `resvg`
  release on Hex (up to 0.5.0, the latest) hard-pins `rustler_precompiled ~>
  0.8.1`, which could make `phoenix_kit_og` un-installable for a host app
  that already needs a newer `rustler_precompiled` for something else —
  version solving would fail with no way for the host app to work around it.
  `Render.Rasterizer` already falls back to the `resvg` CLI, `rsvg-convert`,
  or ImageMagick when the NIF isn't compiled in, so making it optional loses
  nothing for hosts that can't take the pin; add `{:resvg, "~> 0.5"}`
  directly in the host app to opt into the NIF fast path.

## 0.1.0 - 2026-07-04

### Added
- WYSIWYG SVG canvas editor for OpenGraph image templates — text, image, rect,
  and stamp elements, with `{{slot}}` (consumer-wired) and `[[global]]`
  (site_url, site_host, site_name, page_url, page_locale) variable syntax
- Hierarchical template assignment system (e.g. `post → group → default`);
  admin modal for CRUD plus live preview against a real published resource
- SVG → PNG rendering pipeline (`Render.render_url/2`): prefers the `:resvg`
  NIF, falls back to the `resvg` CLI, `rsvg-convert`, or ImageMagick; disk
  cache keyed by a SHA-256 of `(template, canvas, values, module_key)`
- `refine_og/4` integration seam for consumer modules — kill-switch via
  `enabled?/0`, pass-through on any resolution error or missing template so a
  public page render can never crash on OG rendering
- `preview_og_image_url/3` for consumer editors to show "what the plugin will
  produce" without swapping the live OG image
- `GET /phoenix_kit/og-image/:key` image controller — `image/png` without a
  charset suffix (Telegram drops previews on binary MIME with a text charset),
  30-day immutable cache headers, configurable cache directory
- Consumer opt-in via two callbacks on the consumer's `PhoenixKit.Module`
  implementation: `og_variables/0` (declares available variables) and
  `og_resolve/2` (fetches values at render time); first consumer wired up is
  `phoenix_kit_publishing`
- Activity logging for template and assignment CRUD
- Admin dashboard integration: OpenGraph overview tab plus Templates and
  Assignments subtabs
- `phoenix_kit_og_templates` and `phoenix_kit_og_assignments` schemas
  (migration V154), with a partial-unique-index pair so Postgres NULL
  `scope_uuid` (module-wide default) and per-scope assignments don't collide

### Fixed
- The template editor's `/new` route no longer leaks an orphaned template row
  on every fresh page load — creation is now gated on `connected?/1` since
  LiveView mounts twice (disconnected + connected) for a full page load
- `Render.Svg` no longer hardcodes `http://localhost:4000` for host-relative
  image sources (e.g. the signed local-storage fallback URL); it now degrades
  the same way any other unresolvable image href does
