1.7.116 - 2026-05-20

Added

  • Languages.prefixless_primary_safe?/0 — boot- and mix-task-safe wrapper around default_language_no_prefix?/0. Returns false during mix-task context (via the same :phoenix_kit_config_status sentinel Routes.path/1 uses) and rescues any other lookup exception to false. Use from boot/middleware contexts where the Settings table may not be reachable; default_language_no_prefix?/0 remains the runtime entry point. Routes.path/1 and PhoenixKitWeb.Users.Auth now both delegate to this canonical implementation (PR #554).
  • PhoenixKit.Modules.Sitemap.LocalePath — shared emit_prefix?/2 decision rule for the three sitemap sources (publishing, static, posts). Each source still owns its segment formatting (display code with hreflang awareness for publishing, base code for static + posts); the module owns only the decision so the policy stays consistent across sources (PR #554).
  • DialectMapper.group_dialects_by_base/1 — counts sibling dialects per base language code. Used by the language switcher and admin/user nav dropdowns to decide whether to show a country qualifier (PR #555).
  • LanguageSwitcher.dedupe_names/1 + extract_base_language_name/1 — public helpers called from AdminNav and UserDashboardNav so all language menus share one country-qualifier dedup rule (PR #555).

Changed

  • Frontend language switcher (PhoenixKitWeb.Components.Core.LanguageSwitcher's dropdown + continent-grouped views) now drops the country qualifier from rendered labels when only one dialect of a given base language is enabled. English (United States), Estonian (Estonia), French (France) render as English, Estonian, French whenever no sibling dialect is configured; enabling a second dialect of the same base (e.g. en-US + en-GB) causes those entries to reacquire the country qualifier so they remain distinguishable. Same rule applies in the admin top-bar dropdown and user dashboard nav. Continent-grouped views compute sibling counts globally across all enabled languages, so a base split across continents keeps its qualifier in both groups. Restores the bare-label rendering that was lost when commit d1c2d577 rewrote the switcher to one-row-per-dialect (PR #555).
  • Sitemap sources publishing, static, and posts share one LocalePath.emit_prefix?/2 decision rule instead of three byte-identical copies; three near-identical private single_language_mode?/0 helpers collapse into one defensive lookup on LocalePath (PR #554).
  • redirect_invalid_locale/2 honors the site-wide default_language_no_prefix setting. With the setting OFF (default), an invalid locale segment is swapped for the primary base code so the redirect lands on the canonical prefixed shape; with the setting ON, the segment is stripped entirely. Previously the plug always emitted the prefixless shape, which was inconsistent with how the rest of the app emits primary-language URLs when the setting is OFF (PR #554).
  • Dependency bumps: etcher 0.3.0 → 0.4.0, fresco 0.5.2 → 0.5.3.

Fixed

  • Login (and any other primary-language POST) no longer fails with the default default_language_no_prefix setting. process_valid_locale/2 was unconditionally 301-redirecting /<default>/.../... for non-admin requests, discarding the POST body. The redirect is now gated on Languages.prefixless_primary_safe?/0 so the canonical primary shape matches whichever setting state the site is in (PR #554).
  • Sitemap sources static.ex and posts.ex honor the site-wide default_language_no_prefix setting for the primary language. Both had the same _is_default ignored bug that PR #552 fixed in the publishing source — they previously emitted /en/about and /en/blog/post in multilang mode regardless of the setting (PR #554).

1.7.115 - 2026-05-19

Added

  • Site-wide default_language_no_prefix setting on the Languages admin page (/admin/settings/languages) controls whether the primary language emits its locale segment in URLs. When on, /admin/users and /blog/post replace /en/admin/users and /en/blog/post across admin pages, public pages, sitemap, and redirects; other languages always keep their prefix. Default is off, matching the historical publishing default, so existing installs' indexed URLs stay stable on upgrade. Installs that previously toggled publishing_default_language_no_prefix get auto-migrated to the new key on the next boot via Languages.migrate_legacy/0 (PR #552).
  • @type t/0 on PhoenixKit.Settings.Setting schema. Lets consumers spec setting-returning functions ({:ok, Settings.Setting.t()} | {:error, …}) without tripping dialyzer's unknown_type warning.

Changed

  • Admin URL emission for the primary language now follows the new site-wide default_language_no_prefix setting (default off), restoring the /en/admin/users shape that 1.7.114 had emitted prefixless unconditionally. Both URL shapes still resolve at the router level via the dual-scope admin emission, so existing bookmarks and external links keep working (PR #552).
  • Dependency bumps: ecto 3.13.6 → 3.14.0, ecto_sql 3.13.5 → 3.14.0, fresco 0.5.0 → 0.5.2, hammer 7.3.0 → 7.4.0.

Fixed

  • Publishing sitemap honors default_language_no_prefix for the primary language. Previously the sitemap always emitted /en/blog/post in multilang mode regardless of the setting, drifting away from the URLs publishing actually served at request time (PR #552).
  • Languages.default_language_no_prefix?/0 docstring no longer references the nonexistent PhoenixKit.Migration.migrate_default_language_no_prefix/0 — points at the real entry point migrate_legacy/0.
  • Routes.admin_path/2 ## Examples no longer mixes setting-dependent cases with deterministic ones inside iex> doctest prompts, so adding doctest PhoenixKit.Utils.Routes later won't fail. Setting-dependent shapes are still shown, just outside the executable doctest block.

1.7.114 - 2026-05-19

Added

  • module_assigns attribute on LayoutWrapper.app_layout — a single map whose keys are merged into the assigns passed to a host's parent layout, letting feature modules thread arbitrary host-consumable data (e.g. phoenix_kit_publishing_translations) across the layout boundary without core having to declare each key (PR #551).

Changed

  • Locale resolution is now URL-authoritative. The phoenix_kit_locale_base session value and user.custom_fields["preferred_locale"] are no longer read for routing — the URL's locale segment (or its absence) is the only source of truth across both the LiveView mount and the HTTP plug. Fixes a sticky-locale bug where visiting one locale-prefixed URL pinned that locale onto every later prefixless URL (PR #551).
  • Admin URLs drop the locale segment for the primary language, matching non-admin behaviour. Both /<prefix>/admin/* and /<prefix>/:locale/admin/* remain routable, so legacy prefixed links keep working (PR #551).
  • Admin, public, and authenticated-dashboard LiveView routes are each served from a single unified live_session (:phoenix_kit_admin, :phoenix_kit_public, :phoenix_kit_authenticated) spanning both the primary-language (/<prefix>/...) and locale-prefixed (/<prefix>/:locale/...) URL shapes. Switching locale now stays on the WebSocket via push_navigate instead of forcing a full-page reload — previously each surface was split across two sessions and every locale change crossed a live_session boundary.
  • DialectMapper.resolve_dialect/2 collapsed to resolve_dialect/1: dialect resolution is URL-driven and no longer consults a user's custom_fields["preferred_locale"]. Removed the now-unused User.preferred_locale_changeset/2 and User.get_preferred_locale/1.

Fixed

  • Table RowMenu dropdown is portaled to <body> while open so its position: fixed coordinates escape any <dialog> or transform/contain containing block — previously the menu could render far off-screen inside modals (PR #551). It also no longer leaves a duplicate menu element behind when a server-side LiveView update re-renders the row while the menu is open.
  • Publishing route dispatch threads the workspace url_prefix, so it keeps working when PhoenixKit is mounted under a non-root path (PR #551).

1.7.113 - 2026-05-18

Added

  • line annotation kind — a two-endpoint line tool alongside dimension (same geometry, no arrowheads, no inline numeric label). V121 migration widens the phoenix_kit_annotations_kind_check constraint to accept it.
  • PhoenixKitWeb.Components.MediaCanvasViewer — shared LiveComponent owning the per-file canvas + Etcher annotation layer + composer popover + comments thread. Embedded by both MediaBrowser and MediaViewer, so files opened via MediaGalleryMediaViewer get the full pan/zoom + annotation experience (PR #550).
  • Storage.folder_subtree_uuids/1 is now public — walks a folder tree and returns every descendant uuid including the root (PR #549).

Changed

  • MediaBrowser migrated to Etcher 0.3 + Fresco 0.5: per-op annotation events collapse into a single bulk etcher:annotations-changed diff; fresco and etcher deps flipped from local path deps to hex pins (~> 0.5 / ~> 0.3) (PR #550).
  • Annotation composer now positions itself above its shape and drops the shape entirely when dismissed via Cancel (PR #550).
  • Deleting an annotation now hard-deletes its linked comments instead of leaving [removed] placeholders in the file's thread (PR #550).
  • Activity and Users/Sessions filter panels relocated into the <.table_default> toolbar row; tables render unconditionally with the empty state as a table-body row so filters/search stay visible on a zero-result filter (PR #549).

Fixed

  • Users/Sessions search regression — form-less <input>s no longer delivered phx-change; search inputs are wrapped in <form phx-change="search"> again (PR #549).
  • Folder-scoped media picker now includes images in nested subfolders, not just files directly in the scope folder (PR #549).
  • V120 migration tolerates a missing phoenix_kit_doc_template_presets table on hosts that never installed the Document Creator module — the index rebuild is guarded on table existence (PR #550).
  • V121 migration adds the kind-check constraint unconditionally after DROP CONSTRAINT IF EXISTS; the previous pg_constraint existence guard was not schema-scoped and would skip the add on multi-prefix installs.
  • Annotation updates no longer cast :uuid, closing a path where a stray payload uuid could rewrite an annotation's primary key.
  • MediaCanvasViewer annotation sync skips no-op UPDATEs — Etcher re-broadcasts the full annotation list on every mutation, so untouched rows are now diffed out and a zero-net-change re-broadcast does no DB work.
  • Users role-filter dropdown reflects the active filter again (<option selected> instead of an ignored <select value>); the empty-state "Clear Filters" button resets all filters via a dedicated handler.
  • Activity empty state distinguishes "No activities match the current filters" from "No activities recorded yet".

1.7.112 - 2026-05-18

Added

  • PhoenixKitWeb.Components.MediaGallery — reusable LiveComponent for selecting, ordering, previewing and removing a set of images.
  • PhoenixKitWeb.Components.MediaViewer — standalone image lightbox LiveComponent (prev/next, keyboard, download). Extracted from MediaGallery; usable independently.
  • Storage.get_files/1 — batch file fetch preserving input order.
  • FolderExplorer component — folder tree extracted from MediaBrowser as a reusable component (PR #544).
  • <.table_default> sort + drag-to-reorder primitives (PR #548)
    • New :sort_bar slot rendered above the toolbar, visible in both card and table views.
    • <.table_default_body> accepts :global attrs so consumers can wire the SortableGrid hook (phx-hook, data-sortable-*) directly onto <tbody>.
    • <.table_default_header_cell> :inner_block is now optional — empty <th> cells for drag-handle / selection columns no longer need a placeholder.
    • :card_body slot for fully-custom card content; :card_class accepts a string or (item) -> string function; :above_cards slot; 2xl:grid-cols-4 card grid.
  • <.sort_selector> core component — field-picker select + direction toggle, with a manual_field mode that swaps the toggle for a drag-handle hint (PR #548).
  • <.bulk_actions_bar> core component — selection counter + action-button slot + Clear button; wrapper_class covers the inline-card and sticky/blurred shapes (PR #548).
  • <.empty_state> core component — compact / card / featured "no rows" panels with optional icon, description, and CTA slot (PR #548).
  • <.form_section> and <.form_actions> core components — card-wrapped titled form sections and a Cancel + Submit footer bar (PR #548).
  • New PhoenixKit.Utils helpers (PR #548)
    • Reorder.reorder/4 — two-phase index-rewrite primitive for drag-to-reorder list views; schema-agnostic, UUID-filtered, payload-capped, returns {:ok, count}.
    • Values.blank_to_nil/1 and Values.presence/1 — canonical "" → nil helpers (the latter trims first); previously duplicated across 7+ modules.
    • Format.bytes/2 — single human-readable byte formatter with :decimals / :unknown / :base (1024 vs 1000) options; replaced 8 private copies.
  • V120 migration — document-creator category / type taxonomy (PR #545).

Changed

  • MediaSelectorModal accepts an optional notify: {module, id} to deliver the selection via send_update instead of a process message.
  • MediaGallery delegates its inline lightbox to MediaViewer — no behavior change for existing consumers.
  • <.draggable_list> gains an optional target attr (CSS selector). When set, the SortableGrid hook routes the reorder event via pushEventTo so it reaches a LiveComponent rather than the host LiveView.
  • <.sort_header_cell> polish — inactive-column up/down hint, in-flight loading spinner, and atom-or-string sort.dir tolerance (PR #548).
  • admin_page_headerback / back_click are now deprecated no-ops; the back arrow no longer renders. Retained so existing call sites compile (PR #548).
  • MediaBrowser folder management overhaul (PR #544)
    • Recursive folder trash + drag-to-trash; instant "untitled" folder creation.
    • Folders draggable across grid / list / sidebar; drop-into-current-folder via the main content area; whole-selection drag in select mode.
    • Per-file and per-folder kebab menus migrated to TableRowMenu (fixes clipping).
    • Folder rename made more apparent; click-away cancels.
  • etcher / fresco dependency requirements tightened to ~> 0.2.6.
  • Upgraded library dependencies.

Fixed

  • MediaGallery drag-to-reorder no longer pushes reorder_images to the host LiveView (where it had no handler and crashed the page). The grid now passes target to <.draggable_list> so the event reaches the component's own handle_event/3.
  • MediaBrowser breadcrumbs no longer duplicate the current folder or ignore scope; move-to-root now works under scope; update_folder/3 guarded against parent_uuid: nil; N+1 in session breadcrumb work eliminated (PR #544).
  • admin_page_header no longer carries an unused Icon import left by the back-button removal — restores a clean mix compile --warnings-as-errors.
  • Utils.Values.blank_to_nil/1 and presence/1 no longer raise on non-string input (e.g. a list from a key[]= query param) — they fall back to pass-through and nil respectively.
  • V120 migration review fixes — primary-key defaults, multi-prefix guards, exact legacy category mapping, uuid_generate_v7() per house convention (PR #545).
  • Avatar dropdown overflow, scroll, hover, and click-feedback fixes.

i18n

  • Ecommerce gettext manifest + ru / et translations; fixed 91 fuzzy ecommerce translations (PR #547).
  • Projects + comments gettext manifests with et / ru translations (PR #542).
  • Global Gettext locale synced alongside the backend-specific one.

1.7.111 - 2026-05-14

Added

  • V118 migration: callout + text annotation kinds + optional title column on phoenix_kit_annotations (PR #541)
    • Widens phoenix_kit_annotations_kind_check to include "callout" (leader-line annotation: anchor point + line to a labeled bbox) and "text" (freestanding click-drag text label). Both new tools shipped in Etcher 0.2; the CHECK update is folded into a single DROP + ADD so we don't take two trips over the same constraint
    • Adds nullable title varchar(200) column. Every kind can carry a short label — renders inline on the shape (above the bbox for rect/circle/polygon, at the leader endpoint for callout, inside the bbox for text). Length matches the schema-side validate_length(:title, max: 200). Lives in its own column so it stays queryable outside the JSONB blob
    • Annotation schema gains :title field + the two new kinds in @kinds; @cast_fields allows it through the changeset
  • PhoenixKit.Annotations.restore_linked_comments/3 (PR #541)
    • Undo-of-delete support: when an annotation deletion is reversed via Etcher's undo stack, the original uuid is gone but the soft-deleted comments still reference it through metadata.annotation_uuid. The function flips matching status: "deleted" comments back to "published" and rewrites their metadata.annotation_uuid to point at the recreated row. Returns count restored. No-ops cleanly when PhoenixKitComments isn't installed
  • AnnotationComposer title input (PR #541)
    • Optional title field above the comment textarea. Title-only annotations are now allowed (skips the comment-thread create entirely; the row gets only its title column set). update_draft accepts both comment and title keys, debounced 500ms each. Title persistence threaded through :annotation_composer_postedMediaBrowser.finalize_annotation_compose/3
  • MediaBrowser etcher 0.2 wiring (PR #541)
    • etcher:updated now accepts any combination of geometry / style / metadata / title in one payload (previously geometry-only). In-memory annotation list mirrors writes so the tooltip reflects the new title without waiting for a load_annotations_for/1 round-trip
    • Composer popover suppressed for kind == "text" (content arrives inline via etcher's foreignObject editor) and restore: true (recreated row already has its title/metadata; user wasn't trying to create a new annotation)
    • On restore: true + restore_from_uuid, walks soft-deleted comments via restore_linked_comments/3 and refreshes the comments sidebar
    • Tool list extended: [:rectangle, :circle, :polygon, :freehand, :callout, :text, :eraser]
    • Etcher overlay now attaches to the Fresco viewer even when Tessera has no sources (pre-PR: an empty tessera_sources(f) fell back to a plain <img> with no annotation overlay at all). Annotations now work on fresh uploads that haven't been through the variant generator
  • ViewerKeydown JS hook (PR #541)
    • Replaces phx-window-keydown="viewer_keydown" on the viewer modal. Two filters the stock binding couldn't express: (1) only Escape / ArrowLeft / ArrowRight reach the server (letter keys no longer spam LV logs), (2) navigation keys suppressed while focus is in <input> / <textarea> / contenteditable so arrow keys move the text caret instead of flipping the modal to the next image while typing
  • Post-merge review doc in dev_docs/pull_requests/2026/541-v118-callout-text-etcher-0.2/CLAUDE_REVIEW.md with finding disposition table

Changed

  • {:etcher, "~> 0.1"}{:etcher, "~> 0.2"} — adds callout / text / eraser tools, undo/redo, satellite titles, and a complete window.Etcher.layerFor(id) programmatic control surface (PR #541)
  • AnnotationComposer textarea phx-debounce 150 → 500ms — quieter LV logs at typical typing speed, no perceived input lag (PR #541)
  • priv/static/assets/phoenix_kit.js strips 584 lines of inlined fresco / tessera / etcher hooks. Parent apps now import each lib's own priv/static/ bundle ahead of phoenix_kit.js, and phoenix_kit adopts window.{Fresco,Tessera,Etcher}Hooks into window.PhoenixKitHooks. Eliminates drift between the inlined snapshot and the hex packages (PR #541)

Hygiene

  • Lockfile updates: fresco 0.1.1 → 0.1.2

1.7.110 - 2026-05-13

Added

  • V117 migration: document composition tables for phoenix_kit_document_creator (PR #539)
    • Adds nullable category :: varchar column + index to phoenix_kit_doc_templates so templates self-classify (financial / technical / etc.) and the template grid can filter by scope
    • Creates phoenix_kit_doc_document_sections — join table snapshotting (document_uuid, template_uuid, position, variable_values, image_params) for every section of every composed document. document_uuid → :delete_all cascades sections with their parent; template_uuid → :nilify_all lets sections outlive the template (regenerate-required state). Unique (document_uuid, position) + lookup index on (document_uuid)
    • Creates phoenix_kit_doc_template_presets — named reusable section recipes scoped via (scope_type, scope_id) and optionally categorized. sections is a JSONB array of [%{template_uuid, position, variable_values, image_params}]. Index on (scope_type, scope_id, category)
    • Legacy Document.template_uuid column retained: composed docs leave it NULL, legacy single-template docs continue to use it

Fixed

  • Fixed ungrouped handle_event/3 clauses in MediaBrowser by relocating creator_attrs/2 helper to private-helpers block
  • Restored sitemap dynamic <lastmod> for homepage and group listing pages (Sources.Static, Sources.Publishing)
    • PR #539's merge silently re-removed static_lastmod/1 and latest_post_date/2 (a zombie revert that came back via merge conflict and got cut again from a behind-the-base fork). Result: every static URL was reporting lastmod: <today> on every crawl (a known false-freshness signal Google de-prioritizes), and every group listing was shipping without <lastmod> at all
    • Homepage <lastmod> now uses a new lightweight Publishing.latest_post_date_global/0 helper — single pass over each group's posts to take max published_at. Replaces the prior shape that called Publishing.collect/1 and threw away everything except the :lastmod field (which triggered ~3× redundant list_posts/2 calls per group inside collect/1)

Hygiene

  • Routine lockfile updates (mix.lock)
  • Precommit: compile --force replaced with compile --warnings-as-errors --all-warnings, added deps.unlock --check-unused, switched from quality to quality.ci (format-check)
  • Dialyzer: removed 5 unused ignore filters (css_integration, process_scheduled_jobs_worker, duplicate conn_case/data_case, integrations guard_fail)
  • Removed stale :phoenix_kit self-entry from mix.lock

1.7.109 - 2026-05-12

Added

  • V114 migration: Integrations storage switched to uuid-only key column on phoenix_kit_settings (PR #536)
    • Collapses the per-row key from the composite integration:<provider>:<name> shape to just the row's UUIDv7. Lifts both name restrictions baked into the old shape: the regex [a-zA-Z0-9][a-zA-Z0-9\-_]* and per-provider uniqueness are gone. Any non-empty string (after trim) is now a valid connection name; duplicates within a provider coexist (uuids disambiguate). Names with spaces, punctuation, "My Company Drive (US)" — all allowed
    • add_connection/3: generates UUIDv7 up-front, embeds it in both the uuid and key columns; provider + name live purely in value_json. rename_connection/3 rewrites only the JSONB name field — storage key is the row uuid, untouched across renames, so consumer modules pinning to uuid keep working
    • Read sites (get_integration_by_uuid/1, list_connections/1, load_all_connections/1) source provider + name from JSONB; the list helpers expose :date_added so UI callers render "Created N ago" without a second lookup
    • provider:name string lookups now first-match by case-insensitive name sort (names aren't unique anymore). Read-shim contract preserved for legacy migrate_legacy/0 callsites
    • log_activity takes explicit (provider, name) so audit rows carry human-readable names — parsing the key would have stamped a uuid string into metadata.connection
    • Migration walks every integration:%-keyed row in a single UPDATE, backfills missing value_json -> 'name' / 'provider', ensures module = 'integrations', and rewrites key = uuid::text. Legacy V0-shape keys without :name fold to name = "default". down/1 rewrites back to composite shape with -<8-char> suffix from UUIDv7's random tail on duplicate (provider, name) pairs
  • V115 migration: phoenix_kit_annotations table for drawn-on-image shapes via the Etcher overlay (PR #537)
    • Stores rectangle / circle / polygon / freehand shapes tied to a phoenix_kit_files row in image-pixel coordinates. Geometry is JSONB; shape kinds enforced via DB-level CHECK constraint matching Etcher 0.1's four-tool set
    • file_uuid FK ON DELETE :delete_all — annotations vanish with their host image. creator_uuid nullable + ON DELETE :nilify_all so user deletion preserves their annotations as anonymous
    • Discussion threads attach via the existing comments convention: comments anchored to the file (resource_type = "file", resource_uuid = file_uuid) with metadata.annotation_uuid carrying the back-reference. Annotation-rooted comments appear in the file's main thread alongside non-annotated discussion
    • Indexes: (file_uuid) for per-file listing, partial (creator_uuid) WHERE creator_uuid IS NOT NULL for author lookups
  • V116 migration: nullable self-FK parent_uuid on phoenix_kit_entity_data (PR #538)
    • Each entity-data row can point at another row of the same entity as its parent. System field — always present, optional, never user-removable (does not appear in entities.fields_definition). Existing rows stay parent_uuid = NULL and become roots; no backfill
    • No ON DELETE cascade — parent/child linkage and same-entity scope are managed by the PhoenixKitEntities.EntityData context inside a transaction. A DB-level cascade would bypass the soft-delete machinery and the activity log
    • Same-entity enforcement is a context-layer responsibility. B-tree index on (parent_uuid) covers the "list children" query for the WordPress-style indented tree
  • PhoenixKit.Annotations context + PhoenixKit.Modules.Storage.EtcherAdapter (PR #537)
    • Context handles CRUD against phoenix_kit_annotations plus list_for_file_with_previews/1 that pulls every file comment in a single bulk query and groups by metadata.annotation_uuid for the tooltip preview
    • Annotations.delete/1 runs comment cascade + annotation row delete in a Repo.transaction/1 so a failure between the two doesn't leave the annotation alive with its discussion thread destroyed
    • Annotation.adapter_writable_fields/0 exposes the schema's @cast_fields (minus file_uuid, which the adapter sets server-side from target_uuid) as the source of truth for the adapter whitelist — the adapter's @schema_keys derives from it so a future schema field can't drift silently
    • EtcherAdapter implements the Etcher.Storage behaviour, dispatching to the context. Adapter explicitly whitelists payload keys before reaching String.to_existing_atom — guards against forward-compat with Etcher's payload shape growing new client-side keys
  • PhoenixKitWeb.Components.AnnotationComposer LiveComponent (PR #537)
    • Focused composer for attaching the first comment to a newly-drawn annotation. Explicit Post / Cancel control flow owns the annotation lifecycle: Post commits comment + solidifies annotation, Cancel rolls the annotation back. Communicates with the parent MediaBrowser via LC-to-LC send_update/2, no host-LV plumbing required
    • Scope: text + file uploads (image / video / audio / pdf / archive) + Giphy picker. Audio recording (which the full CommentsComponent supports) intentionally skipped for v1
  • MediaBrowser integration with the Etcher overlay (PR #537)
    • Etcher.layer mounted alongside Fresco.viewer in the modal. New etcher:created / :updated / :deleted / :selected handlers wire the JS overlay into the storage backend
    • Lifecycle: open_viewer/2 preloads annotations + rolls back any pending compose; finalize_annotation_compose/2 reloads annotations and pokes the file's CommentsComponent to refresh; refresh_file_comments/1 flips the component's loaded? to false to trigger a sidebar reload
    • creator_uuid is set server-side from the scope — client-supplied creator_uuid in the payload is overridden, preventing author spoofing
  • IntegrationPicker rewrite (PR #536)
    • Card subtitle: priority external_account_id → masked credential tail (first 8 + + last 4 for any of api_key / bot_token / access_key; ••• for keys under 14 chars)
    • Age line under subtitle using shared <.time_ago>
    • Status badge: distinct label + colour for each of the four canonical statuses (connected → green "Connected", error → red "Auth failed" with validation_status tooltip, configured → yellow "Not tested", disconnected → grey "Not connected")
    • Provider icon + display name auto-resolve via Integrations.Providers.get/1 — callers no longer pre-attach a :provider struct
    • Provider-name badge hidden when picker is filtered to a single provider (both real callsites do this)
    • Click feedback: phx-click-loading dims the clicked card + blocks rapid re-clicks during the LV round-trip; daisyUI loading-spinner swaps in for the status badge during the same window
    • provider_def memoized in a Map.new(connections, ...) shared between filter_by_search/3 and the render path — drops Providers.get/1 calls from 2N to N per render
    • 33 new component spec tests covering subtitle priority + masked credential + age + provider auto-resolve + status branches + filter-by-provider + search threshold + empty state + deleted-card warning + click-action dispatch
  • <.draggable_list> :sortable_handle attribute (PR #538)
    • Optional CSS selector (e.g. ".pk-drag-handle") that restricts drag initiation to elements matching the selector inside each item. When set, the item wrapper drops cursor-grab styling — the caller renders their own handle. Backward-compatible: default nil preserves whole-item drag. Mirrors <.table_default>'s :on_reorder + .pk-drag-handle convention. JS hook (SortableGrid) already supported data-sortable-handle; this PR wires the Elixir-side knob through
  • Etcher tooltip JS slot overrides (PR #537)
    • window.Etcher.tooltipSlots .header / .footer / .body translate metadata.comment_* keys into the rich tooltip (author header, date · count subheader, thumbnail + quoted text body). window.Etcher = window.Etcher || {} guards against load order
  • AnnotationComposerPosition JS hook keeping the MediaBrowser's floating annotation-composer popover inside the viewer bounds via re-clamping on mount + updates + window resize
  • Dep adds: :fresco ~> 0.1 (OpenSeadragon viewer wrapper, now a direct dep since Tessera 0.2 split it out), :etcher ~> 0.1 (annotation overlay)
  • Three post-merge review docs in dev_docs/pull_requests/2026/: 536-integrations-v114-uuid-keys-picker-ux/, 537-annotations-v115-etcher-overlay/, 538-v116-parent-uuid-draggable-handle/ — each with finding disposition tables tracking which items were addressed in follow-up commits and which were deferred to the original PR author

Changed

  • Integrations.validate_connection/2 rescue narrowed to [DBConnection.OwnershipError, Postgrex.Error, Req.TransportError] so genuine logic bugs (KeyError, ArgumentError, MatchError) bubble up to the supervisor instead of being swallowed under a generic "validation failed". validate_credentials/2 mirrored the narrowing post-merge for parity (PR #536)
  • Integrations.authenticated_request/4 docstring spells out the URL-trust contract: the integration's Bearer token is attached to every request, so callers must pin URLs to a domain allowlist before invoking. Internal callers (OpenRouterClient, OAuth refresh, userinfo) build URLs from the Providers registry which is hardcoded and safe; new callsites taking URLs from elsewhere need their own guard (PR #536)
  • phx-disable-with on Save / Test Connection / Disconnect / Delete buttons on integration_form.html.heex plus the OAuth Connect Account button. Pre-fix, a double-click + slow network could submit two save requests or spawn parallel HTTP probes (PR #536)
  • IntegrationForm create_connection + save_form_with_rename error branches preserve :new_name + :form_values on error so a failed :empty_name submit doesn't wipe the api_key the operator just typed. Dropped the dead :already_exists / :invalid_name error branches (those tuples no longer fire post-V114). Template: removed the now-incorrect "Letters, digits, hyphens, and underscores. Must be unique per provider." name-rules hint (PR #536)
  • PhoenixKit.Users.Permissions.module_label("db") / module_icon / module_description pin db in @core_* maps so the display is correct even when the external phoenix_kit_db module isn't loaded (PR #536)
  • MediaBrowser.format_date/1 strftime format string wrapped in gettext(...) so locales can reorder date components (%d %b %Y for en-GB / fr / de) without code changes
  • AnnotationComposer.first_error/1 routes through PhoenixKitWeb.Components.Core.Input.translate_error/1 — gettext-aware helper that interpolates %{count} and other opts properly
  • AGENTS.md CHANGELOG-ownership instruction corrected — entries are written by agents against the bumped @version heading, matching the project's actual workflow

Fixed

  • Post-merge fixes folded from review of PR #536:
    • V114 docstring drift after rebase rename: moduledoc references "V113" but the module is V114; "Stamp the table comment with '113'" while code stamps '114'; "post-V113 regression / invariant" in tests and picker comment. All swept to V114
    • Permissions.module_label("db") was falling through to String.capitalize("db") = "Db" when the external phoenix_kit_db module isn't loaded; test asserts "DB". Folded in to keep the post-rebase baseline green
    • Test fixture rows in storage/scope_test.exs, media_browser_scope_test.exs, media_browser_test.exs violated V113's phoenix_kit_files_user_or_parent_check CHECK constraint; each create_file!/1 now stamps user_uuid via a memoised ensure_user!/0 helper
    • IntegrationPicker.filter_by_search/3 had a shadowing name variable in the inner case pattern match; renamed to provider_name
    • String.slice negative-bound range pinned with explicit step (-4..-1//1) to silence Elixir 1.16+ range-step warning
    • Credo cleanup: get_integration/1 2-branch cond with true arm → if/else; inline PhoenixKit.Settings.Queries.get_setting_by_uuid/1 calls aliased as SettingsQueries in integrations_test.exs; three test files (storage/scope_test.exs, media_browser_scope_test.exs, media_browser_test.exs) alias PhoenixKit.Users.Auth for their ensure_user! helpers
  • Post-merge fixes folded from review of PR #537:
    • Annotations.delete/1 deleted linked comments outside a transaction — if the comment cascade succeeded and the annotation row delete then failed (FK violation, DB transient), comments were gone but the annotation remained with its discussion permanently destroyed. Wrapped in Repo.transaction/1 via an extracted delete_in_transaction/1 helper
    • resource_type = "annotation" claim in three moduledocs (annotation.ex, v115.ex, etcher_adapter.ex) contradicted the actual implementation, which anchors comments to the file (resource_type = "file") with metadata.annotation_uuid. All docs swept to match reality
    • Annotations.delete_linked_comments/1 bare rescue _ -> :ok swallowed every exception class including logic bugs. Narrowed to [DBConnection.OwnershipError, Postgrex.Error, ArgumentError] so logic bugs surface
    • AnnotationComposer.normalize/1 reinvented what Ecto.Changeset.cast/3 already does (accepts both atom- and string-keyed maps) AND silently passed the original map through when String.to_existing_atom failed, hiding typo'd field names from the user as "geometry: can't be blank" rather than "unknown field" — function deleted
    • In-repo Code.ensure_loaded?(PhoenixKit.Annotations) guard in MediaBrowser.load_annotations_for/1 was needless defensive code — Annotations is in the same compilation unit
    • @compile {:no_warn_undefined, [PhoenixKit.Modules.Storage, ...]} would have shadowed legitimate compile errors on a core module rename — Storage removed from the suppression list
    • AnnotationComposerPosition.destroyed cleanup conditional was a never-falls-through guard — simplified
    • Etcher slot-preservation JS comment misstated the mechanism — corrected to "PhoenixKit owns the tooltip layout; downstream consumers must load AFTER phoenix_kit.js"
    • Credo cleanup: Annotations.first_attachment_thumbnail/1 single-clause withcase; aliased Annotations, Storage, EtcherAdapter, Storage.File so six "nested modules could be aliased" findings clear
    • PhoenixKitComments dialyzer ignores added for annotations.ex + annotation_composer.ex (optional sibling package, guarded at runtime)
  • V114 down SQL collision-suffix source: substring(uuid::text from 1 for 8) extracted UUIDv7's timestamp prefix — same-millisecond rows produced identical suffixes ⇒ duplicate "uniquified" keys when two+ rows collided on (provider, name). Switched to substring(uuid::text from 25 for 8) (random tail, 32 bits of entropy). Mirrored in run_down! in the V114 test (PR #538 follow-up)
  • 3-row collision test added to V114 test suite covering N ≥ 3 case (exactly one plain key, N-1 distinct suffixed keys, all keys unique)
  • IntegrationPicker click feedback was missing: clicking a card showed no visual response during the 100-500ms LV round-trip — operators would click again and submit a second request. Added phx-click-loading:opacity-60 phx-click-loading:pointer-events-none + status-badge → spinner swap during the in-flight window (PR #536)

i18n

  • AnnotationComposer user-facing strings wrapped in gettext (flash messages + heex literals + ARIA labels — ~17 strings)
  • MediaBrowser format_date strftime pattern wrapped in gettext so locales reorder date components
  • IntegrationPicker status labels (Connected / Auth failed / Not tested / Not connected) and search placeholder + empty-state strings wrapped in gettext
  • IntegrationForm flash messages, button labels, name placeholder, danger-zone copy, OAuth step labels, redirect-uri instructions wrapped in gettext

1.7.108 - 2026-05-11

Added

  • V112 migration: phoenix_kit_projects* schema evolution (PR #533)
    • archived_at TIMESTAMP(0) on phoenix_kit_projects so the admin dashboard can soft-hide projects without flipping a status enum. Mirrors the workspace convention used by phoenix_kit_publishing's posts.trashed_at and phoenix_kit_files.trashed_at — null = visible, non-null = soft-hidden, with the timestamp doubling as audit metadata. Existing status = 'archived' rows backfilled into archived_at so the dashboard filters keep working transparently
    • phoenix_kit_projects_visible_idx partial index on (inserted_at DESC) WHERE archived_at IS NULL — one partial covers both project-list and template-list dashboard reads (neither view shows archived rows)
    • translations JSONB NOT NULL DEFAULT '{}' on phoenix_kit_projects, phoenix_kit_project_tasks, phoenix_kit_project_assignments for per-language overrides on user-input content (name / description / title). Primary stays in dedicated columns; JSONB only carries non-primary overrides
    • position INTEGER NOT NULL DEFAULT 0 on phoenix_kit_projects and phoenix_kit_project_tasks so the drag-and-drop reorder API can persist manual ordering. Existing rows fold into the 0 bucket and the schema's secondary order-by-inserted_at kicks in until a user actually drags
    • scheduled_start_date retyped from DATE to TIMESTAMP(0) so scheduled-overdue detection honors time-of-day (a project scheduled for today 09:00 flips to :overdue at 09:01, not at midnight). Column name kept — lying name + honest type beats the churn of renaming every call site
    • Drops the three remaining unique-name indexes — phoenix_kit_projects_name_template_index, phoenix_kit_projects_name_project_index, phoenix_kit_project_tasks_title_index. Name uniqueness is now policy, not schema — editing or duplicating names no longer trips a stale index, and future renames don't need migration coordination
    • Bumps migrator @current_version 111 → 112 — without this V112 was dead code
    • All steps idempotent (column-existence, index-existence, USING coercion clauses); down/1 reverses each change so a rollback restores the V111 shape. down/1 restores the V105/V101 unique indexes first, before dropping any V112 columns, so a duplicate-name conflict at rollback aborts cleanly rather than leaving a half-rolled schema
  • test/phoenix_kit/migrations/v112_test.exs — pins every V112 addition (archived_at column + type + nullability, visible-index existence AND predicate shape, translations JSONB on the three tables, scheduled_start_date retype, position columns) plus the four dropped indexes and the duplicate-name behavior. The predicate test refutes any is_template mention, closing the docs-drift loop
  • dev_docs/pull_requests/2026/533-v112-projects-schema-evolution/CLAUDE_REVIEW.md — post-merge review with finding dispositions
  • V113 migration: system-managed media flag for Tessera deep-zoom tiles + comments↔files junction (PR #534)
    • system_managed BOOLEAN NOT NULL DEFAULT false on phoenix_kit_files — marks internally-generated media (DZI tile pyramids + per-tile chunks) so the MediaBrowser excludes them from user listings and the variant generator skips them (tile chunks don't need small / medium / large — just an "original" FileInstance)
    • parent_file_uuid UUID nullable FK to phoenix_kit_files(uuid) ON DELETE CASCADE — system-managed tile rows cascade away when their source image is hard-deleted
    • user_uuid drops NOT NULL — system-managed rows belong to a parent File, not a user. The DB-level CHECK phoenix_kit_files_user_or_parent_check enforces "user_uuid IS NOT NULL OR parent_file_uuid IS NOT NULL" so raw inserts can't violate the invariant
    • phoenix_kit_files_system_dedup_index partial unique index on (parent_file_uuid, file_name) WHERE system_managed = true — concurrent lazy-generation requests for the same uncached tile dedupe at the DB level via the changeset's unique_constraint; Storage.store_system_file/3 recovers the winner's row on conflict
    • Two more partial indexes — phoenix_kit_files_parent_uuid_index (per-source lookup + cascade-cleanup) and phoenix_kit_files_system_managed_index (keeps the MediaBrowser's "user files only" sort cheap as the tile catalog grows)
    • phoenix_kit_comment_media junction table letting the comments module attach core File rows to comments with position + caption. Cascade on comment_uuid, RESTRICT on file_uuid (a file can't hard-delete while attached). Consumer code lands in a later PR
    • Bumps migrator @current_version 112 → 113; all DDL idempotent via IF NOT EXISTS / DO-blocks
  • Deep Zoom Image viewer in MediaBrowser via Tessera (OpenSeadragon wrapper, PR #534)
    • New Tessera.Viewer.viewer replaces the static <img> in the file modal; tessera_sources/1 builds the progressive layer list (medium → optional large → DZI manifest)
    • <Tessera.Storage> adapter (PhoenixKit.Modules.Storage.TesseraAdapter) lands tile writes in the storage pipeline (multi-bucket via Manager.store_file/2 + a system-managed File row via Storage.store_system_file/3)
    • Two new public endpoints — /tiles/:token/:dzi_filename and /tiles/:token/:files_segment/:level/:tile_filename — generate the DZI manifest and individual tiles lazily on first request, then serve from storage. Signed URLSigner token in the URL path (not query) so OpenSeadragon's tile-URL derivation preserves it across manifest → tile fetch. The "dzi" variant name is distinct from storage variants so a leaked file-serving token can't grant tile access. Unauthorized requests return 404 to prevent UUID enumeration
    • Per-file_uuid :global.set_lock mutex + double-checked locking around the generators serializes concurrent cold-path requests for the same image; different images stay parallel. Lock timeout surfaces as 503 + Retry-After header
    • Tempfile lifecycles wrapped in try/after so Tessera.generate_tile/4 or Manager.retrieve_file/2 exceptions don't leak files into System.tmp_dir!()
    • New storage setting storage_tile_generation_enabled (default "false") is the kill switch — when off, MediaBrowser emits no manifest URLs and the tile endpoints return 404
  • Storage.store_system_file/3 — context helper for system-managed media (tiles, manifests). Idempotent: check-then-insert with unique-violation recovery via the new dedup index, so two concurrent writers for the same key both end up with the same row
  • Storage query helper exclude_system_managed/1 — applied to every list_* / count_* / orphan / trash query so system-managed rows are invisible in the MediaBrowser regardless of how the query is composed
  • test/phoenix_kit/migrations/v113_test.exs — pins every V113 addition (system_managed column shape, parent_file_uuid FK with cascade verification, user_uuid nullability change, three indexes including the partial-unique dedup, CHECK constraint with a raw-SQL test that double-null inserts are rejected, comment_media table + its two indexes)
  • dev_docs/pull_requests/2026/534-media-browser-tessera-tiles/CLAUDE_REVIEW.md — review with finding dispositions; CRITICAL + HIGH items fixed in the same release

Changed

  • <.translatable_field> in PhoenixKitWeb.Components.MultilangForm — wrapper now flex flex-col gap-1 and base input/textarea classes carry w-full (commits 52856738, 0412beaf). daisyUI 5's .label is inline-flex and .input/.textarea are inline-block, so without forcing column direction here label and field sat on the same row. Aligns the multilang form's layout with the regular <.input> core component
  • test/phoenix_kit/migrations/v106_test.exs — dropped the "schema state (verified at boot)" describe block (its assertions pinned V112's drops, not V106's adds — moved to V112Test). File now scoped to V106's down/1 cross-mode duplicate pre-check, matching its filename

Fixed

  • V112 down/1 rollback ordering (post-merge review fix, commit 0cde04a8). Original down/1 dropped columns before restoring unique indexes; if post-V112 work introduced any duplicate names (V112's whole purpose), the CREATE UNIQUE INDEX would raise mid-rollback after columns were already gone. Reordered so index restoration runs first
  • V112 visible-index predicate docs/code alignment — migrator moduledoc claimed WHERE archived_at IS NULL AND is_template = false but actual index only filtered on archived_at IS NULL. Docs updated to match the emitted SQL with rationale (one partial covers both visible projects and visible templates)
  • MediaBrowser i18n — list-view dropdown duplicates of the folder menu (Color heading, delete data-confirm, Delete button) were missed in the original sweep; wrapped so list view matches grid view. Three flash/confirm messages embedded two independent counts in a single gettext call which made correct pluralization impossible in Russian (3 forms) and Estonian (2 forms); each composed from two ngettext calls injected into the surrounding gettext template (PR #532)

i18n

  • MediaBrowser UI strings wrapped in gettext + Russian and Estonian translations (PRs #532, plus earlier commits c717c9d0 / d44bf95c)
  • /admin/modules page strings wrapped in gettext + ru/et translations (PR #530)
  • Core sidebar tabs wired to PhoenixKitWeb.Gettext for ru/et (PR #529, commits 43e528ac / c5fd5bae)
  • Sitemap settings UI strings wrapped in gettext (commit afcc281a)
  • General Settings widened + /admin/modules/languages strings wrapped (commit 24ceec90)
  • Core badge.ex + time_display.ex status strings wrapped (commit 1fe0ad78)
  • 9 new tab-label msgids + complete Estonian translation in default.po (commit 373478f5)
  • Bare Active badge in users.html.heex:278 wrapped (commit 8a341e90)

Layout

  • Admin settings pages widened for wide screens (commit 3ea70ec1)
  • Module settings pages — sitemap, storage, referrals — widened for wide screens (commit d867cb57)

Hygiene

  • mix format reflow of long-line gettext / ngettext / put_flash calls in media_browser.ex and media_browser.html.heex (commit 9841ac31). Surfaced when mix precommit ran during V112 review-followup work; no semantic changes
  • New Hex dep: {:tessera, "~> 0.1"} — OpenSeadragon wrapper used by the Deep Zoom viewer

1.7.107 - 2026-05-10

Added

  • Two opt-in stateless helpers on PhoenixKitWeb.Components.Core.TableDefault (PR #528)
    • sort_header_cell/1 — clickable <th> with hero-chevron-up-mini/-down-mini icon when active, inert label-only <th> when sort attr is nil. Configurable event (default "toggle_sort"), target, align (:left/:right/:center). The align is applied to the <th class> (text-right / text-center) so non-sortable columns honour it consistently with the sortable ones
    • sort_header_cell/1 emits aria-sort="ascending"|"descending"|"none" on the <th> when sortable, omitted when inert. Pinned by 4 regression tests covering all three states + omitted
    • search_toolbar/1 — daisyUI input-sm with hero-magnifying-glass icon and phx-debounce (default 300ms). Optional <form> wrap when on_submit is set. Placeholder defaults to dgettext("default", "Search..."). phx-target propagates to both the <form> and <input> so submit-on-Enter retargets correctly when embedded in a LiveComponent
    • test/phoenix_kit_web/components/core/table_default_test.exs — new directory + 18 component tests; closes part of the standing core/ test-coverage TODO from CLAUDE.md
  • change_page event handler in PhoenixKitWeb.Live.Users.LiveSessions — pagination on /admin/users/live-sessions was bound to phx-click="goto_page" with no matching handle_event/3 clause, so clicking any page number raised FunctionClauseError and crashed the LV (PR #528 follow-up). Renamed the binding to change_page and added the handler mirroring the sibling users.ex:121 convention

Changed

  • PhoenixKitWeb.Live.Users.LiveSessions — collapsed :sort_by + :sort_order assigns into a single :sort = %{by, dir} map; renamed event "sort_by""toggle_sort" with "by" param. First click on a new column sorts ascending (was descending); subsequent clicks toggle. flip_dir/1 tightened from flip_dir(_) catch-all to explicit :desc clause so unintended values surface as a crash rather than silent coercion (PR #528)
  • lib/phoenix_kit_web/live/users/live_sessions.html.heex and lib/phoenix_kit_web/live/users/users.html.heex — both call sites of <.search_toolbar> dropped the redundant on_submit="search". The input's debounced phx-change="search" already covers the same event; keeping both made Enter fire "search" twice (immediate submit + 300ms-later debounced change) (PR #528 follow-up)

Fixed

  • lib/phoenix_kit_web/live/users/users.html.heex — replaced bare search form (every keystroke hit the server) with <.search_toolbar> carrying the 300ms phx-debounce (PR #528)
  • <.search_toolbar> form variant double-bound phx-change on both <form> and <input>, doubling work per keystroke. phx-change is now bound only on the <input>; the <form> carries phx-submit only. phx-target now propagates to both so LiveComponent embedding works end-to-end. Two regression tests pin both behaviours (PR #528, commit dfc91238)

i18n

  • mix gettext.extract --merge resync — adds "Search..." msgid + et / ru translations and surfaces accumulated drift from prior commits where extract wasn't run (PR #528, separate commit a7c1d35b)

Hygiene

  • .gitignore — adds /priv/static/assets/vendor/ so mix phoenix_kit.install runs against /app itself don't leave an outdated copy of the source JS in tree (PR #528)
  • mix.lockdb_connection 2.10.0 → 2.10.1, igniter 0.7.9 → 0.8.0 (pulls in ex_ast 0.11.0 as new transitive). Routine patch bumps

1.7.106 - 2026-05-08

Added

  • V111 migration: PDF library tables for the upcoming catalogue PDF subtab (PR #516)
    • phoenix_kit_cat_pdfs — thin per-upload row. file_uuid FK to phoenix_kit_files(uuid) ON DELETE RESTRICT (catalogue manages the file lifecycle; core prune can't remove files referenced by a live catalogue row). Soft-delete via status sentinel (active / trashed) + trashed_at. Two uploads of identical content (different filenames) → two rows sharing one phoenix_kit_files row + one extraction
    • phoenix_kit_cat_pdf_extractions — keyed by file_uuid PK. Worker state machine (pending → extracting → extracted | scanned_no_text | failed) + page_count + extracted_at + error_message. Cascades on file hard delete

    • phoenix_kit_cat_pdf_page_contents — content-addressed dedup cache. PK on content_hash (SHA-256 hex of normalized page text). Same page text across multiple PDFs is stored once. GIN trigram index lives here so the search index doesn't grow with cross-PDF duplication
    • phoenix_kit_cat_pdf_pages — composite PK (file_uuid, page_number); content_hash FK to the dedup cache (RESTRICT — orphaned content rows GC'd by a catalogue-side helper, not by FK cascade)
    • Enables pg_trgm extension; @current_version 110 → 111
  • PhoenixKit.KnownPackages — live catalog of known external PhoenixKit packages, replacing the previously hardcoded list in ModuleRegistry.known_external_packages/0 (PR #523)
    • Fetched on demand from https://hex.pm/api/packages?search=phoenix_kit_&sort=name and cached for 10 minutes in an ETS named table (:phoenix_kit_known_packages_cache)
    • Stale-while-revalidate with cap: on Hex failure, serves cached data up to :max_stale_age_ms (default 24h); beyond that, drops the cache and falls back to :extra_known_packages config entries only
    • :warning log on stale-served and empty-cache-extras-only; :error log when cache exceeds max stale age — operationally distinct alert levels
    • Link-header pagination with a 20-page cap (@max_pages) so a malformed Link header pointing back to the same page can't loop forever
    • extra_known_packages config knob — parent apps with private/forked packages declare them inline and they take precedence over Hex entries on the package dedup key (source: "config" baked in)
    • hex_docs_icon_name: hero-<name> convention — package authors append the marker to their Hex package description and the catalog UI picks it up; default is hero-puzzle-piece
  • Per-module gettext support on Dashboard sidebar labels and tooltips (PR #522)
    • PhoenixKit.Dashboard.Tab gains gettext_backend: module() | nil (default nil) and gettext_domain: String.t() (default "default") fields, plus localized_label/1 and localized_tooltip/1 resolvers that call Gettext.dgettext/3 when a backend is set and fall back to the raw label otherwise

    • PhoenixKit.Dashboard.Group gains the same two fields plus localized_label/1
    • Tab.divider/1 and Tab.group_header/1 accept the new opts; Tab.new/1 round-trips both via get_attr/2
    • 14 render sites in Sidebar, AdminSidebar, TabItem swap tab.labelTab.localized_label(tab) and equivalents — mechanically uniform, no shape changes
    • Hot-reload safety via Map.get/2 (not pattern matching) on the new fields — old-shape %Tab{} cached in ETS or :persistent_term from before the upgrade falls through as if gettext_backend were nil rather than raising FunctionClauseError. Pinned by an explicit Map.delete(:gettext_backend) regression test
    • guides/per-module-i18n.md — public guide for module developers (setup checklist, mix.exs / backend / .po flow, dynamic_children/2 locale handling, dividers and group headers, tooltips, greenfield template, retrofitting checklist, smoke test pattern, common pitfalls including the hot-reload safety contract)
    • dev_docs/instructions/2026-05-08-per-module-i18n-procedure.md — internal operational procedure capturing every gotcha hit during the Newsletters pilot (skip-worktree on mix.exs, path-dep workflow during local dev, conditional CI skip pattern for graceful degradation)
  • :per_translation_urls attr on the three LanguageSwitcher variants — language_switcher_dropdown/1, language_switcher_buttons/1, language_switcher_inline/1 (PR #525)
    • Each entry is %{code: <display_code>, url: <full_url>}. Both atom-keyed and string-keyed entries accepted (useful when the list comes from JSON/JSONB rather than Elixir code)
    • Resolves each language's base_code against the list via DialectMapper.extract_base/1 so "en-US" and "en" both resolve cleanly. Falls back to the locale-rewrite default when no entry matches OR the matched entry has a nil URL (e.g. an unpublished draft)
    • Useful when a feature module has computed canonical URLs that the simple locale-rewrite default can't reproduce — for example publishing's per-language URL slugs where /en/blog/my-post and /fr/blog/mon-article aren't related by segment swap. Pass assigns[:phoenix_kit_publishing_translations] from the layout
    • 7 new tests in test/phoenix_kit_web/components/core/language_switcher_test.exs pin the contract (atom-keyed, string-keyed, full-dialect normalization, per-language fallback, nil/empty/missing-attr pass-through)
  • Drag-handle scoping + sortable feedback infrastructure (PR #525)
    • <.table_default> emits data-sortable-handle=".pk-drag-handle" when @on_reorder is set; only the .pk-drag-handle element gets cursor-grab styling. Click-to-expand / button-press / text-selection on a card no longer fights with SortableJS drag detection
    • SortableGrid JS hook: new sortable:flash LV→client event handler. The host LV pushes {uuid: "...", status: "ok" | "error"} after each reorder_items attempt; the hook applies pk-sortable-flash-{ok,err} class for ~1.2s, idempotent via reflow trigger. Queries every [data-id] element so table-view + card-view both animate. Defensive status-validation guard — unknown values bail rather than falling into the err-class branch

    • <tr> cell-width preservation via onChoose / onUnchoose — SortableJS's forceFallback: true + fallbackOnBody: true clones the dragged <tr> to document.body, where it loses its <table> ancestor and <td>s collapse to content width. The hook now snapshots computed widths and pins them inline before the drag preview renders; onUnchoose restores them
    • data-sortable-handle attr threads to SortableJS's handle option for any caller; moved_id always included in the reorder_items payload (was only on cross-container moves) so the LV can push back a sortable:flash keyed to the just-moved row
  • MediaBrowser modal viewer becomes the default click target for non-admin / non-select_mode browsers, with read-only image / video / PDF / icon preview, metadata sidebar, Download button, prev/next chevrons (and ←/→ keyboard shortcuts), and Esc / backdrop close (PR #519)
    • Mobile-fullscreen layout via position: fixed; inset: 0 — bypasses daisyUI's grid + iOS Safari's 100vh/100dvh quirks. Desktop reverts to 95vw × 90vh centered modal with rounded corners. The !-prefix utility chain on .modal-box is required because daisyUI v5's defaults win the cascade over plain Tailwind utilities
    • MediaImageZoom JS hook lazy-loads Panzoom 4.6.0 from jsDelivr when the modal opens; image supports wheel/pinch/double-tap zoom and drag-pan. Listener attaches to the parent so the cursor doesn't have to land on the image; destroyed cleanup removes the wheel listener and destroys the Panzoom instance
    • Bulk-select still reachable — clicking the toolbar's Select button flips select_mode on, and from then on clicks toggle selection instead of opening the modal
  • LiveView login redirect now carries the original request path as ?return_to= (PR #519)
    • New login_path_with_return_to/1 private helper in PhoenixKitWeb.Users.Auth reads Phoenix.LiveView.get_connect_info(socket, :uri), encodes path?query via URI.encode_www_form/1, and threads it into the redirect target. Wired into the four redirect_require_login paths in on_mount hooks
    • Trailing-slash self-loop guard: String.trim_trailing(path, "/") on both sides of the equality check, so /users/log-in and /users/log-in/ are treated as the same path and no return-to round-trips back to itself
    • Pairs with the existing ?return_to= flow in login.ex (sanitize_return_to/1:user_return_to session → log_in_user/3)
  • PhoenixKit.ModuleRegistry.get_module_key_for_namespace/1 — symmetric with the existing get_by_key/1. Resolves a top-level Elixir namespace string (e.g. "PhoenixKitEntities") to the registered plugin's module_key/0 (PR #521)
    • Iterates all_modules/0, matches on Module.split(mod) == [top_namespace] (exact, single segment), returns the key string or nil for unmatched
    • Reads from :persistent_term so there's no GenServer roundtrip on the hot path
  • Microsoft 365 OAuth tenant override + generic interpolate_url/3 helper in PhoenixKit.Integrations.OAuth — providers can now substitute {key} placeholders in auth_url / token_url from per-row integration_data, falling back to a provider-level :url_defaults map (PR #516)
    • Closes the previously hardcoded /common/ Microsoft tenant — single-tenant operators got AADSTS50194 errors. New tenant_id setup field with common default; multi-tenant remains the default behavior. Three pinning tests in test/phoenix_kit/integrations/oauth_test.exs
    • Wired into authorization_url/5, exchange_code/4, refresh_access_token/2. URLs without { pass through unchanged (zero impact on Google / OpenRouter / Mistral / DeepSeek)
  • "Resolve a LiveView module to its permission key" block-comment on PhoenixKitWeb.Users.Auth.permission_key_for_admin_view/1 documenting the four-step resolution order (static map → custom-tabs → PhoenixKit.Modules.<X>.Web.* namespace → registered-plugin namespace) and the fail-closed nil default

Changed

  • DB module extracted from core into the standalone phoenix_kit_db Hex package (PR #518)
    • Removed lib/modules/db/ (db.ex, listener.ex, web/{activity,index,show}.{ex,html.heex}) — ~2010 lines across 8 files
    • module_registry.ex — dropped PhoenixKit.Modules.DB from internal_modules/0
    • integration.ex — dropped the three hand-registered live "/admin/db…" declarations (auto-discovery via admin_tabs/0 picks them up once the package is installed)
    • modules.html.heex — dropped the hardcoded DB module card; auto-render via <.module_card> based on admin_tabs/0 discovery
    • dev_docs/guides/2026-02-24-module-system-guide.md — moved lib/modules/db/db.ex from the Internal examples section to External as phoenix_kit_db/, between hello_world and document_creator
  • PhoenixKit.ModuleRegistry.not_installed_packages/0 switches from Code.ensure_loaded?(pkg.module) to OTP-app-name MapSet membership (PR #523) — the more correct semantics, since module-loading state and OTP-app-installed state aren't the same: an extracted-but-not-yet-installed module fragment could pass Code.ensure_loaded? but isn't actually a dep
  • PhoenixKit.Integrations.OAuth.verify_oauth_state/2 missing-state branch tightened from lenient :ok to {:error, :state_mismatch} (PR #516, closes a CSRF-relevant gap from PR #511's review NIT #10) — every connect_oauth event saves state via save_oauth_state/2 before redirect post-2026-05, so a missing state at callback time means either bypass or row-mutated-mid-flow; both are CSRF-relevant
  • IntegrationPicker drops the conn.name == "default" substitution that contradicted PR #511's own moduledoc ("Names are pure user-chosen labels with no system semantics") — always renders the user-chosen name + provider badge (PR #516, closes PR #511 NIT #6)
  • <.file_upload> full_upload/1 variant entry-progress label reads Uploading… {entry.progress}% instead of the bare percentage — entry.progress is always client→server upload progress per Phoenix LV convention, so the wording is universally accurate (PR #516)
  • LanguageSwitcher resolves the per-language URL once per iteration via inline <% url = ... %> and reuses it for href and phx-value-url — halves the per-render resolve_url/3 cost and pins both call sites to the same URL (post-merge triage)
  • KnownPackages moduledoc grew an "Operational signals" section enumerating the three log levels (:warning stale-served / no-cache / :error exceeded max stale age) and what each signals operationally (post-merge triage)

Fixed

  • Publishing routing-strategy collision: any host route shaped /:locale/<literal>/... declared after phoenix_kit_routes() was silently shadowed by publishing's /:language/:group/*path catch-all (PR #524)
    • phoenix_kit_routes/0 now emits a publishing-specific dispatch shim when PhoenixKitPublishing.RouterDispatch is loaded — internal-prefix scope at /<url_prefix>/__phoenix_kit_publishing_dispatch with /localized and /root discriminator sub-scopes, plus a def call/2 override that calls RouterDispatch.maybe_rewrite/1 on every request and only rewrites publishing-bound URLs onto the internal prefix. Host routes get a fair shot at every URL
    • restore_path/2 runs after route binding (via the new :phoenix_kit_publishing_internal pipeline) so canonical-URL generation reads the URL the client sent — without it, publishing's default_language_no_prefix redirect would spin on the internal prefix forever
    • Compile-time gated on Code.ensure_loaded?(PhoenixKitPublishing.RouterDispatch); installs without publishing in the dep tree get quote do end (no-op AST). The __mix_recompile__?/0 mechanism injected by phoenix_kit_routes/0 forces a host-router recompile when publishing is added or removed from deps — handles the dep-cache staleness case
    • Browser-smoke verified across 8 URL classes: localized + canonical publishing posts, host's /:locale/services/view/... routes (was 404 pre-fix), admin redirects, plain home, genuine 404s. HTML body sweep confirmed zero leakage of the internal prefix in canonical / og / links / JS / headers
  • Custom-role users with explicit plugin permissions (entities, billing, ai, …) were silently locked out of plugin admin pages because infer_permission_key_from_module/1 only resolved the core PhoenixKit.Modules.* namespace. External plugins (PhoenixKitEntities.*, PhoenixKitBilling.*, …) returned nil from all three resolution paths, collapsing onto the "no permission" branch in enforce_admin_view_permission/2 (PR #521)
    • New [top | _rest] -> ModuleRegistry.get_module_key_for_namespace(top) clause on infer_permission_key_from_module/1. Old _ -> nil fallback removed (unreachable post-Module.split/1). Owner / Admin behaviour and the fail-closed default for genuinely unknown views are preserved

    • Initial implementation used [^top_namespace | _] which matched any registered module whose Module.split starts with the segment; live repro on a parent app showed get_module_key_for_namespace("PhoenixKit") => "db" because PhoenixKit.Modules.DB happened to be the first registered module beginning with "PhoenixKit". Tightened to [^top_namespace] (exact, single segment) and pinned with a regression test

    • permission_key_for_admin_view/1 exposed as @doc false def (was defp) so 4 new unit tests in test/phoenix_kit_web/users/auth_test.exs can exercise the resolution layers without LiveView mounting machinery; 3 new tests in test/phoenix_kit/module_registry_test.exs pin get_module_key_for_namespace/1 (uses Module.create/3 with explicit top-level fixture names to avoid test-module auto-nesting)
  • PhoenixKitWeb.PagesHTML removed (PR #518) — the module had no controller, no routes, no callers. The embed_templates "pages_html/*" directive plus pages_html/show.html.heex plus the integration.ex docstring described a markdown-page-rendering feature that was never wired up. Publishing module covers actual CMS-page rendering. ast-grep --lang elixir --pattern 'PhoenixKitWeb.PagesHTML' confirms zero structural references remain
  • MediaBrowser chevron-button positioning: daisyUI's active-state CSS replaces transform with scale(0.97) on click, which would clobber a -translate-y-1/2 on the button itself and make it jump down 50% of its height. Chevron positioning now sits on a wrapper <div>, not the button (PR #519)
  • media_browser.html.heex modal-viewer leading comment referenced the now-removed viewer={true} attr; rewritten to describe the new default click behaviour (post-merge triage)
  • Dead defaults[String.to_atom(key)] fallback in OAuth.interpolate_url/3 — no provider in Providers.providers/0 ships an atom-keyed url_defaults, so the path was unreachable. Removed; comment documents that provider authors must use string keys (post-merge triage)
  • KnownPackages.fetch_hex_page/3 recursion grew an explicit @max_pages 20 cap so a malformed Link header pointing back to the same page can no longer loop forever; ensure_table/0 rescue now carries an explanatory comment about the :ets.whereis/1:ets.new/2 race window (post-merge triage)
  • KnownPackages test_helper.exs: the System.cmd("psql", ...) DB-existence check now try/rescue ErlangError so environments where psql isn't on PATH fall through to the connect-direct branch instead of crashing the test boot (PR #523)
  • Pre-existing credo Refactor.Apply opportunities on the three apply/3 calls in compile_publishing_routing/1 silenced with inline # credo:disable-for-next-line annotations and an empirically-verified comment explaining why the variable-indirection alternative (mod = ModuleName; mod.fun()) doesn't shield the compiler's static-resolution warning either. mix credo --strict now reports zero issues across the tree (post-merge triage)

Removed

  • PhoenixKit.Modules.DB and the entire lib/modules/db/ directory — extracted to the standalone phoenix_kit_db Hex package; companion repo TBA (PR #518)
  • PhoenixKitWeb.PagesHTML and its pages_html/show.html.heex template — dead code, never wired up to a controller or route (PR #518)
  • MediaBrowser's :viewer attr — the four-mode click handler collapsed to three modes (select_modeadmin → modal viewer); pickers reach select_mode via the toolbar's Select button. The default click action is now the modal viewer, so callers that previously passed viewer={true} see no behaviour change. Callers that depended on the old picker-by-default (no admin, no viewer → click toggles selection) need to instruct users to click the toolbar's Select button instead (PR #519)

1.7.105 - 2026-05-05

Added

  • PhoenixKit.Migration.ensure_current/2 — re-runnable analog of mix ecto.migrate for test helpers and any boot path running against a long-lived database (PR #515)
    • Passes a fresh wall-clock version (:os.system_time(:microsecond)) to Ecto.Migrator.up/4 on every call so Ecto sees a "new" migration each time and invokes the inner runner; PhoenixKit's own marker (the comment on the phoenix_kit table) short-circuits internally if there's nothing new to apply
    • Forwards :prefix from the Ecto.Migration runner context inside the new private PhoenixKit.Migration.Runner wrapper so callers passing prefix: "auth" aren't silently routed to "public"
    • Microsecond precision keeps the collision and clock-skew windows small enough that an NTP correction would have to rewind the clock by µs at exactly the wrong moment to hide a newly-shipped migration; bigint-safe (Postgres covers ~292 years)
    • The schema_migrations table accumulates one row per call — cosmetic noise acceptable for the test-DB use case; production migrations via mix ecto.migrate / mix phoenix_kit.update remain unchanged
  • V110 migration: nullable language VARCHAR(10) column on phoenix_kit_doc_templates so each Document Creator template can be tagged with a single locale (PR #515)
    • Full locale codes (en-US, et-EE, ja) — matches PhoenixKit.Module.Languages.get_enabled_languages/0 output; lossless, consumers that want bare base codes can derive them via DialectMapper.dialect_to_base/1
    • Existing rows survive without a backfill; the form (landing in phoenix_kit_document_creator separately) pre-selects the project's primary language when creating new templates
    • Documents intentionally do not get a language column — they inherit from template_uuid → templates.language
    • @current_version 109 → 110; ⚡ LATEST tag moved off V109 onto V110
  • PhoenixKit.Migration.Runner.runner_opts/1 — pure transform of the runner-context prefix into opts threaded to PhoenixKit.Migration.up/1 / down/1 (PR #515 review follow-up)
    • Split out of the previous closure-style runner_opts/0 so the prefix-forwarding behaviour can be regression-tested without spinning up a real Ecto.Migration.Runner process (which conflicts with the Ecto sandbox)
    • Three new unit assertions in test/phoenix_kit/migration_test.exs pin the contract: nil → [] (drop, so with_defaults/2's "public" default isn't clobbered), "auth" → [prefix: "auth"], arbitrary tenant prefix forwarded verbatim. If someone "simplifies" runner_opts to always return [], CI now fails
  • "Return contract" section in the ensure_current/2 moduledoc clarifying that failures (advisory-lock contention, migration crashes, connection errors) raise from Ecto.Migrator.up/4 rather than being wrapped in {:error, _} (PR #515 review follow-up)

Changed

  • test/test_helper.exs switched from path-form Ecto.Migrator.run(repo, migrations_path, :up, all: true) to PhoenixKit.Migration.ensure_current/2 (PR #515)
    • Deletes the now-redundant wrapper migration test/support/postgres/migrations/20260316000000_add_phoenix_kit.exs
  • AGENTS.md test-infra section updated: test_helper.exs is now the canonical migration application point, with a Do not warning against the stale tuple form Ecto.Migrator.run(repo, [{0, PhoenixKit.Migration}], :up, all: true) (PR #515)

Fixed

  • Documented test-helper migration patterns silently went stale after the first run (PR #515)
    • Both the tuple form (Ecto.Migrator.run(repo, [{0, PhoenixKit.Migration}], :up, all: true), documented in dev_docs/migration_cleanup.md) and the path form (used by core's own test_helper via the 20260316000000_add_phoenix_kit.exs wrapper) hit the same trap: Ecto.Migrator records the version in schema_migrations after the first call and filters that entry out of pending on every subsequent boot. PhoenixKit.Migration.up/1 was never re-invoked, so newly-shipped Vxxx migrations didn't apply on subsequent boots even though PhoenixKit's own marker was idempotent. Symptom: column ... does not exist after mix deps.update phoenix_kit brought in new migrations but the test DB stayed at the old marker
    • Verified empirically — core's own phoenix_kit_test was at marker 107 even though Hex 1.7.103 shipped V108 + V109; first boot after switching to ensure_current/2 advanced the marker through V108 / V109 / V110 correctly

1.7.104 - 2026-05-04

Changed

  • Customer Service module extracted from core into the standalone phoenix_kit_customer_support Hex package — companion repo: BeamLabEU/phoenix_kit_customer_support (PR #514)
    • Removed lib/modules/customer_service/ (~6 KLOC, 22 files) and lib/phoenix_kit_web/routes/customer_service.ex; the module is now an external optional dependency
    • module_registry.ex — dropped PhoenixKit.Modules.CustomerService from internal_modules/0, added the corresponding phoenix_kit_customer_support entry to known_external_packages/0
    • integration.ex — replaced inline /dashboard/customer-service/tickets route blocks with Code.ensure_loaded?(PhoenixKitCustomerSupport.Web.UserList) guards so absent-package = no routes
    • DB tables (phoenix_kit_tickets, phoenix_kit_ticket_*) stay in core under their existing names — they're domain-shaped, not module-shaped, and the prior migrations (V35/V51/V53/V58/V72/V74/V75/V77) remain in core's migration history
  • Renamed "Customer Service" → "Customer Support" across the public surface (PR #514)
    • Module: PhoenixKitCustomerServicePhoenixKitCustomerSupport
    • OTP app: :phoenix_kit_customer_service:phoenix_kit_customer_support
    • Hex package: phoenix_kit_customer_servicephoenix_kit_customer_support
    • Settings keys: customer_service_*customer_support_* (7 keys)
    • URL paths: /customer-service/*/customer-support/* (admin + user-facing, both base and locale-prefixed routes)
    • Permission key: customer_servicecustomer_support
    • Dashboard module card and admin nav target updated to match

Added

  • V109 migration: rename Customer Service module identifiers in-place so existing installs migrate cleanly (PR #514)
    • Renames 7 settings keys from customer_service_*customer_support_* in phoenix_kit_settings
    • Renames auto_granted_perm:customer_serviceauto_granted_perm:customer_support
    • Renames phoenix_kit_role_permissions.module_key from customer_servicecustomer_support
    • Idempotent (IF EXISTS guards on every rename); reversible down/1 for emergency rollback
    • @current_version 108 → 109; ⚡ LATEST tag moved off V107 onto V109

Fixed

  • PhoenixKit.Users.Auth.anonymize_user_tickets/1 was a no-op since the original Tickets → CustomerService rename — Module.concat([PhoenixKit, Modules, Tickets, Ticket]) resolved to a never-loaded module so the Code.ensure_loaded? guard always failed and ticket anonymization silently skipped on user deletion. Now points at PhoenixKitCustomerSupport.Ticket (PR #514)
  • V108 (drag-and-drop position columns, shipped in 1.7.103) was missing from the lib/phoenix_kit/migrations/postgres.ex per-version docstring catalog. Backfilled in this release alongside the V109 entry (PR #514 review)
  • lib/phoenix_kit/migrations/postgres/v109.ex rename_role_permission/4 carried an unused _prefix arg — the table name is already prefix-qualified at the call site. Trimmed to /3 (PR #514 review)

1.7.103 - 2026-05-02

Added

  • V107 migration: pin AI endpoints to a specific integration row via integration_uuid + add the missing unique index on lower(name) (PR #511)
    • Nullable integration_uuid uuid column on phoenix_kit_ai_endpoints with btree index
    • Backfill maps existing provider strings to integration rows: exact "provider:name" matches get the corresponding storage row; bare "provider" gets the most-recently-validated integration:provider:* row, tiebreaking on uuid ASC (UUIDv7 time-ordered). Unresolvable endpoints stay NULL
    • Unique index phoenix_kit_ai_endpoints_name_index ON (lower(name)) — the unique_constraint(:name) declaration in the changeset has been dead code since V34 created this table without the index
  • V108 migration: position integer DEFAULT 0 on three admin list surfaces — phoenix_kit_entities, phoenix_kit_cat_catalogues, phoenix_kit_cat_items — so drag-and-drop reordering can persist user-driven order (PR #512)
  • Strict-UUID Integrations public API (PR #511)
    • Write-side APIs now take only the integration row's uuid — no more deriving storage keys from JSONB fields
    • Integrations.resolve_to_uuid/1 — dual-input lookup primitive that accepts a uuid or a provider:name string (for migrate_legacy/0 callbacks)
    • migrate_legacy/0 optional callback on PhoenixKit.Module — each module owns its legacy data shape; core provides primitives. Orchestrated by PhoenixKit.ModuleRegistry.run_all_legacy_migrations/0
    • Mistral, DeepSeek, and Microsoft 365 added to the built-in providers registry
    • integration_picker updated: no auto-select on single-provider, toggle-to-deselect support
  • Drag-and-drop core infrastructure (PR #512)
    • <.draggable_list> new :draggable boolean attr (default true) — when false, renders without SortableJS hook and grab-cursor styling
    • <.table_default> new :on_reorder, :reorder_scope, :reorder_group, :item_id attrs — wire the card-view container as a SortableGrid hook target for cross-container drag
    • SortableGrid hook (JS): data-sortable-group for cross-container drag, readScope/1 helper for data-sortable-scope-* attrs, cross-container onEnd detection with from* scope prefix, try/catch wrapping
    • TableCardView hook (JS): updated() callback re-applies saved view mode after LV re-renders so card/table toggle survives SortableJS drops
  • Media viewer modal on MediaBrowser (PR #513)
    • New viewer={true} attr — clicking a file opens an in-place modal with image/video/PDF/icon preview, metadata sidebar (filename, type, MIME, size, uploaded date), and Download button. Closes via X / Esc / backdrop
    • Prev/next chevrons and ArrowLeft/ArrowRight keyboard shortcuts step through the current page's files; arrows hide at boundaries
    • PhoenixKitComments.Web.CommentsComponent embedded in the sidebar when the Comments module is installed and enabled (optional-dep wiring: @compile {:no_warn_undefined} + Code.ensure_loaded? + @dialyzer :nowarn_function)
  • Arity-2 dynamic_children_fn @typedoc + test-only delegate for the admin sidebar dispatcher (PR #506 follow-up in #512)

Changed

  • handle_event("click_file", …) in MediaBrowser refactored from two-mode if/else to four-clause cond: select_modeadminviewer → picker default (PR #513)
  • connected_at semantics clarified in AGENTS.md — rewritten on every successful re-test (not one-shot); last_validated_at rewritten unconditionally on every validation attempt, success or failure
  • Bumped leaf editor dependency ~> 0.2.10 → ~> 0.2.11 and the matching CDN URL (PR #513)

Fixed

  • AGENTS.md doc drift: PhoenixKit.Modules.run_all_legacy_migrations/0 corrected to PhoenixKit.ModuleRegistry.run_all_legacy_migrations/0; V107 moduledoc tiebreak clarified as uuid ASC not inserted_at ASC (PR #511 review)
  • V107 unique-name index verified with three new integration tests: index exists, duplicate names rejected, case-only differences collide (PR #511 review)
  • Media viewer modal: String.starts_with?/2 guarded with is_binary(f.mime_type) so nil mime_type falls through to the icon fallback instead of crashing (PR #513 review)
  • Media viewer modal: PDF iframe hardened with sandbox="allow-same-origin" to block embedded JavaScript in same-origin deployments (PR #513 review)
  • <.draggable_list> data-id now always emitted regardless of :draggable attr so click-to-select handlers and test selectors work in both modes (PR #512 review)
  • :reorder_scope attr doc on <.table_default> now documents the camelCase round-trip (:category_uuid"categoryUuid" in the LV handler payload) (PR #512 review)

1.7.102 - 2026-04-29

Added

  • V105 migration: CRM tables for the upcoming phoenix_kit_crm plugin (PR #507)
    • phoenix_kit_crm_role_settings — one row per role with enabled BOOLEAN NOT NULL DEFAULT false so existing roles stay opted out until explicitly enabled. PK on role_uuid; FK → phoenix_kit_user_roles(uuid) ON DELETE CASCADE
    • phoenix_kit_crm_user_role_view — per-user, per-scope view preferences (column selection, ordering, filters). UUIDv7 PK; unique (user_uuid, scope); index on (user_uuid); FK → phoenix_kit_users(uuid) ON DELETE CASCADE. scope is a string like "role:<uuid>" or "companies"
  • V106 migration: split phoenix_kit_projects.name uniqueness across templates and real projects (PR #510)
    • Replaces V101's single global unique index on lower(name) with two partial unique indexes: phoenix_kit_projects_name_template_index WHERE is_template = true and phoenix_kit_projects_name_project_index WHERE is_template = false
    • Lets a template "Onboarding" and a real project "Onboarding" coexist, unblocking Projects.create_project_from_template/2 for the common reuse-the-template-name path
    • down/1 recreates V101's single global index; lossy if a template and a real project share a name post-V106 — resolve duplicates before rolling back
  • Legal module i18n — translations across de/fr/it/pl plus refreshed ru/es. New de/fr/it/pl POs created via mix gettext.merge --locale with proper Plural-Forms headers (German n != 1, French n > 1, Italian n != 1, Polish 3-form rule). Pre-existing non-empty msgstr values preserved (PR #509)
  • lib/phoenix_kit_web/legal_gettext_manifest.ex — re-emits the 50 translatable strings used by phoenix_kit_legal so the gettext extractor (which doesn't walk into deps) records them into core's POT. Never called at runtime; pure extraction target with refresh procedure documented in the moduledoc (PR #509)
  • css_sources/0 accepts string entries — @callback css_sources() widened from [atom()] to [atom() | String.t()]. Strings flow through format_source/2source_for_path/1 (absolute paths emit @source "<abs>"; verbatim, relative get the standard ../../ prefix); atoms continue to resolve via parent app's mix.exs deps. Lets modules mix OTP-app atoms with literal path strings — first known consumer is phoenix_kit_legal, which ships a path-dep absolute fallback alongside its OTP-app entry so both Hex and path-dep installs work without parent-app toggles. Backwards compatible: existing def css_sources, do: [:phoenix_kit_my_module] keeps working unchanged (PR #509)

Changed

  • Bumped leaf editor dependency ~> 0.2.6 → ~> 0.2.10 and the matching CDN URL in priv/static/assets/phoenix_kit.js so the runtime loader pulls the same version. Includes min-width: 0 + toolbar-wrap fixes so the editor stops claiming an unbounded intrinsic width on mount (PR #508)
  • priv/gettext/default.pot cleanup — dropped ~900 phantom msgids left over from modules extracted to standalone packages (billing, publishing, entities, etc.) (PR #509)

Fixed

  • application/pdf uploads in MediaBrowser. determine_file_type/1 returned "pdf", but the File changeset validates file_type against ["image", "video", "audio", "document", "archive", "other"] — every PDF upload silently failed validation and never reached any bucket. Now maps application/pdf"document", matching how form-upload integrations already classify PDFs (PR #507)
  • V106 COMMENT ON TABLE version values were off by one (up wrote '105' instead of '106', down wrote '104' instead of '105'). The migration framework reads this comment as the source of truth for the migrated version, so on the incremental V105 → V106 upgrade path the comment never advanced past '105' — V106.up would replay on every deploy and the admin dashboard / mix phoenix_kit.status would report a stale version. Fresh installs masked the bug because handle_version_recording/4 stamps the final version on multi-step runs and overrode V106's bad write. Caught in review of PR #510 and amended in place since V106 had not yet shipped to Hex

1.7.101 - 2026-04-24

Added

  • Notifications module — per-user inbox driven by the activity log. When PhoenixKit.Activity.log/1 records an entry with target_uuid != actor_uuid, a row is inserted into phoenix_kit_notifications for the target user. Independent seen_at / dismissed_at per row, per-user PubSub topic ("phoenix_kit:notifications:<user_uuid>"), global kill-switch via notifications_enabled setting (default "true"). Admins still audit via /admin/activity and don't receive notifications (PR #505)
    • V104 migration: phoenix_kit_notifications with UUIDv7 PK, FKs to phoenix_kit_activities and phoenix_kit_users (both ON DELETE CASCADE), unique (activity_uuid, recipient_uuid) index, partial (recipient_uuid, inserted_at DESC) WHERE dismissed_at IS NULL index for the inbox read path
    • PhoenixKit.Notifications public API: maybe_create_from_activity/1, list_for_user/2, recent_for_user/2, count_unread/1, mark_seen/2, mark_all_seen/1, dismiss/2, dismiss_all/1, get_notification/2, enabled?/0, retention_days/0, prune/1
    • PhoenixKit.Notifications.Render.render/1 — maps action → %{icon, text, link, actor_uuid}; honors metadata overrides (notification_text, notification_icon, notification_link) before falling back to the action lookup
    • PhoenixKit.Notifications.Types registry — three core types (account, posts, comments) plus extension point for external modules via the new optional notification_types/0 callback on PhoenixKit.Module
    • PhoenixKit.Notifications.Prefs — per-user preferences persisted in custom_fields.notification_preferences (reuses V18 JSONB column; no migration). Fail-open on any ambiguity
    • PhoenixKit.Notifications.PruneWorker — daily Oban cron at "0 4 * * *"; retention via notifications_retention_days (falls back to activity_retention_days, default 90)
    • PhoenixKitWeb.Live.NotificationsBell — sticky nested LiveView for the bell + dropdown. Not mounted by default; parent apps render it where they have a user-facing header via Phoenix.Component.live_render(..., sticky: true, session: %{"user_uuid" => ...}). Badge + recent list refresh live via PubSub
    • Notification preferences section in PhoenixKitWeb.Live.Components.UserSettings — one toggle per registered type; unknown submitted keys dropped at the call site
    • notifications_enabled toggle on /admin/settings
  • Arity-2 dynamic_children_fn for admin sidebar tabs — callbacks can now be (scope, locale -> [tab]) in addition to the existing (scope -> [tab]). Backwards-compatible extension: the sidebar dispatches on arity, every existing 1-arity callback keeps working unchanged. Lets plugins render locale-aware child labels without reading Gettext.get_locale/1 at render time (PR #506)

1.7.100 - 2026-04-22

Added

  • V103 migration: nullable self-FK parent_uuid on phoenix_kit_cat_categories with b-tree index on (parent_uuid) for arbitrary-depth category trees. Existing rows stay NULL and become roots — no backfill. No DB-level ON DELETE cascade (subtree cascades are owned by the context layer so they go through soft-delete + activity log) (PR #503)
  • scope_folder_id attr on PhoenixKitWeb.Live.Components.MediaSelectorModal — filters the browse query to the given folder plus any files reached via FolderLink, and assigns newly-uploaded files into that folder (adopt as home if orphan, else add a FolderLink). Plugins scoping the picker to a single domain object (e.g. a catalogue item) pass this after lazy-creating their folder (PR #503)
  • PhoenixKit.Settings.Setting.optional_settings/0 accessor exposing @optional_settings for invariant tests
  • Invariant test (test/phoenix_kit/settings/setting_test.exs) asserting every empty-string default in PhoenixKit.Settings.get_defaults/0 is also in @optional_settings, to prevent the class of bug fixed in PR #502 from recurring

Changed

  • PhoenixKit.Modules.Storage.File changeset file_type allowlist widened from ["image", "video", "document", "archive"] to include "audio" and "other" so non-image/video uploads bucket cleanly (PR #503)
  • MediaSelectorModal.load_files/2 refactored into four composable scope_files_by_{user,folder,type,search} helpers — credo cyclomatic-complexity fix from adding the new scope branch (PR #503)

Fixed

  • Settings batch save no longer rolls back when site_icon_file_uuid or default_tab_title is left empty on the General Settings form. Both keys added to @optional_settings in PhoenixKit.Settings.Setting and seeded with empty-string defaults in PhoenixKit.Settings.get_defaults/0 (PR #502)
  • MediaSelectorModal.maybe_set_folder/2 errors (from the folder_uuid update or FolderLink insert) now log a warning via warn_on_folder_error/3 instead of being silently discarded by _ =. Previously, a failed scope assignment after a successful upload left no trace

1.7.99 - 2026-04-20

Added

  • V100 migration: staff tables — phoenix_kit_staff_departments, phoenix_kit_staff_teams, phoenix_kit_staff_people, phoenix_kit_staff_team_memberships (PR #498)
  • V101 migration: projects tables — phoenix_kit_project_tasks, phoenix_kit_project_task_dependencies, phoenix_kit_projects, phoenix_kit_project_assignments, phoenix_kit_project_dependencies; polymorphic assignee with CHECK (num_nonnulls(...) <= 1) (PR #498)
  • V102 migration: smart catalogues + per-catalogue/item discount (PR #500)
    • phoenix_kit_cat_catalogues.discount_percentage (NOT NULL DEFAULT 0) and kind ('standard' | 'smart') columns with CHECK constraints

    • phoenix_kit_cat_items.discount_percentage, default_value, default_unit override columns
    • new phoenix_kit_cat_item_catalogue_rules table with unique (item_uuid, referenced_catalogue_uuid) and ON DELETE CASCADE on both FKs
    • partial index on kind = 'smart'
  • PhoenixKitWeb.Components.MediaBrowser.Embed — one-line use macro that injects on_mount upload setup, the "validate" upload-channel stub, and the MediaBrowser handle_info delegator (PR #499)
  • MediaBrowser selection menu with bulk download (staggered <a download> dispatch via MediaDragDrop hook) (PR #499)
  • MediaBrowser admin attr to gate detail-page push_navigate — picker mode (default) vs admin mode (PR #499)
  • MediaBrowser drag-drop file-to-folder move (PR #499)
  • MediaBrowser toggleable search bar in the header (PR #499)
  • MediaBrowser drag-drop upload at any folder level (PR #499)
  • Site icon + default tab title settings, logo moved to main settings page (PR #499)
  • MultilangForm debounce flow: mount_multilang/1 attaches a hidden :handle_info hook via Phoenix.LiveView.attach_hook/4; handle_switch_language/2 schedules a 150 ms trailing debounce via Process.send_after (timer ref stored in socket.private to avoid render+diff cycles); switch_lang_js/2 toggles skeleton/fields hidden classes client-side at t=0 (PR #500)
  • <.input> gains a wrapper_class attr for the outer phx-feedback-for div (PR #500)
  • test_load_filters / test_ignore_filters in mix.exs for Elixir 1.19 mix test hygiene (PR #500)
  • AGENTS.md: Core Form Components section, Multilang Form Components section, and CHANGELOG-ownership rule (entries written by the maintainer, not agents)

Changed

  • MediaBrowser sidebar and content unified into a single card (PR #499)
  • Scope-root new-folder form aligned with sibling folder rows (PR #499)
  • Core form components (<.input>, <.select>, <.textarea>, <.checkbox>) now merge the class attr onto the styled element itself — matches the Phoenix 1.7 generator convention. No in-tree caller used the old wrapper-class behavior; external consumers should switch to wrapper_class on <.input> (PR #500)
  • compile.phoenix_kit_css_sources emits absolute dep paths verbatim instead of prefixing ../../ (PR #500)

Fixed

  • MediaBrowser list view broken by stale view-toggle CSS (PR #499)
  • Credo AliasUsage warning inside MediaBrowser.Embed's quoted block silenced (PR #499)

1.7.98 - 2026-04-16

Added

  • V99 migration: trashed_at column on phoenix_kit_files with partial index for soft-delete (PR #497)
  • Media trash bucket: soft-delete files with restore/empty/permanent-delete actions and sidebar count badge
  • PhoenixKit.Modules.Storage.Workers.PruneTrashJob — daily Oban cron (3 AM) that permanently deletes files older than trash_retention_days (default 30)
  • Drag-drop upload: drop device files directly onto the folder content area (FolderDropUpload JS hook)
  • URL-param hydration on first mount so reloads don't flash the root view

Fixed

  • Scope guard on restore_selected in MediaBrowser — a scoped embed could previously restore files outside its scope via a crafted toggle_select payload
  • Trash view, permanent-delete, and empty_trash now respect scope_folder_id via recursive CTE
  • list_files/1 excludes trashed files
  • Breadcrumb and search bar moved inside card body so padding matches grid/list content

1.7.97 - 2026-04-15

Added

  • V97 migration: per-item markup_percentage override on catalogue items (PR #493)
  • V98 migration: alternative_formats column on storage dimensions
  • PhoenixKit.Modules.Shared.Components.ImageSet — responsive <picture> component with AVIF/WebP/JPEG <source> entries
  • PhoenixKit.Modules.Storage.VariantNaming — format-suffix parsing utility
  • Multi-format variant generation (WebP/AVIF alongside primary format per dimension)
  • Variant dimensions and file sizes shown on media detail page
  • UUID search support on media page search bar

Changed

  • V95 migration made truly idempotent for folder_uuid column (raw SQL IF NOT EXISTS block)
  • Dimensions table format cell renders as JPEG + WEBP, AVIF (fixed stray " +" separator)

Fixed

  • Long text overflow in media detail sidebar
  • Missing original file size in variant download buttons

1.7.96 - 2026-04-13

Added

  • Sortable languages in admin (drag-and-drop reorder)
  • hide_source option on DraggableList component
  • Wiggle animation for reorder mode with prefers-reduced-motion support

Changed

  • Dedup language codes in reorder, use MapSet for lookup
  • Extract wiggle CSS to JS-injected styles with pk- prefix

1.7.95 - 2026-04-11

Added

  • V95 migration: media folders and folder links tables
  • V96 migration: catalogue_uuid FK on catalogue items for direct catalogue membership

1.7.94 - 2026-04-10

Added

  • Media folder system with sidebar, select mode, list/grid view, drag-drop file moving
  • Folder colors, inline rename, select folders, and context menus
  • Search bar and folder path column to media page
  • Media Health page for redundancy monitoring
  • Sync with progress tracking, pause/resume/stop controls, real-time sync log
  • Media sync moved to Oban worker for persistence and reliability
  • Multipart S3 uploads
  • Tigris storage provider support
  • Test Connection button to bucket form
  • Configurable max upload size setting
  • Reusable SearchableSelect LiveComponent
  • Provider-specific labels for B2, R2, and S3 bucket configuration
  • AWS regions dropdown via aws_regions hex package

Changed

  • Rename Storage to Media, use thumbnail variant for media grid
  • Redesign media sidebar with proper file explorer conventions
  • Migrate bucket and dimensions tables to table_default component
  • Restyle bucket/dimension forms with card sections and DaisyUI 5 fieldset/legend
  • Persist folder tree expand state and sidebar collapsed in localStorage
  • Remove LLMText module (preserved in feature/llmtext branch)
  • Remove CDN URL field from bucket configuration form

Fixed

  • Fix #478: register CSS sources compiler and create stub file on install
  • Fix integration activity logs always having nil actor_uuid
  • Fix S3 upload failures and false file location records
  • Fix folder card hover lag (use transition-colors instead of transition-all)
  • Fix folder name truncation, rename animation, and input autofocus
  • Fix select mode content jump and improve render performance
  • Fix mix tasks: Routes.path unavailable, ecto.migrate skips host repo
  • Fix OAuth users getting signed out after ~1-2 hours

1.7.93 - 2026-04-08

Fixed

  • Fix installer to auto-inject PhoenixKitHooks into app.js
  • Fix decrypt after legacy integration migration
  • Improve OAuth login with remember_me by default

Added

  • Activity logging for Integrations (setup, connect, disconnect, token refresh, validation)
  • Cookie max_age set to 60 days

Changed

  • Simplify integration status: configured removed, now connected or disconnected
  • Validate connection checks provider exists before credentials

1.7.92 - 2026-04-07

Fixed

  • Fix Google token refresh for named integration connections (e.g. google:work)
  • Add resolve_provider_lookup_key/2 and resolve_storage_key/2 helpers

Added

  • Add V94 migration for Document Creator sync (google_doc_id, status, path, folder_id columns)

1.7.91 - 2026-04-06

Added

  • Add centralized Integrations system for external service connections (OAuth, API keys, bot tokens)
  • Add AES-256-GCM encryption at rest for stored credentials
  • Add OAuth 2.0 CSRF state parameter protection
  • Add required_integrations/0 and integration_providers/0 callbacks to PhoenixKit.Module
  • Add IntegrationPicker reusable component
  • Add Integrations admin settings tab
  • Add provider registry with Google and OpenRouter built-in

Fixed

  • Fix password field overwrite bug when editing integrations
  • Fix duplicate line in maybe_set_userinfo/2
  • Consolidate validation logic into Integrations context
  • Make validation URL provider-configurable (no more hardcoded Telegram URL)

1.7.90 - 2026-04-04

Added

  • Add organization accounts support with person/organization user types
  • Add organization invitations system with token-based invite flow
  • Add V91 locations migration: location types, locations, and type assignments tables
  • Add V92 organization accounts migration (invitations, user type fields)
  • Add JS hooks integration for parent app install and update workflows
  • Add LLMText module for AI/LLM-friendly content generation
  • Add auth logo from settings to admin header
  • Add billing tabs component

Changed

  • Move tax rate and CountryData from Billing to core PhoenixKit
  • Remove hardcoded Billing and E-Commerce module cards in favor of auto-discovery
  • Update AGENTS.md with severity level definitions and JS hooks documentation

Fixed

  • Fix double sidebar for core modules and improve struct compatibility
  • Hide hamburger menu button when sidebar is permanently visible
  • Fix token security, gettext, and validation issues in organization invitations
  • Fix tax data loss, invitation status guard, and IbanData safety

1.7.88 - 2026-04-02

Changed

  • Migrate select elements to daisyUI 5 label wrapper pattern (#472)

Fixed

  • Fix negated condition in maintenance toggle flash message
  • Fix dialyzer warnings for CSS sources compiler, clean up 6 stale ignore entries

1.7.87 - 2026-03-31

Added

  • Add V89 migration: catalogue pricing with base_price and markup_percentage
  • Add status_badge component and wrapper_class attr to table_default
  • Add inline and auto display modes to table_row_menu
  • Add show_toggle attr to table_default and sync TableCardView instances
  • Add continent grouping to language switcher for many languages
  • Add language system tests, docs, error handling, and group_by_continent option

Changed

  • Unify admin and frontend language systems into single source of truth
  • Unify status badge components into single status_badge
  • Remove deprecated select-bordered class for daisyUI 5 compatibility
  • Disable automatic CI triggers, switch to manual-only

Fixed

  • Fix language switcher URL generation for prefixed admin paths
  • Fix dialyzer warnings in language switcher URL generation

1.7.86 - 2026-03-30

Changed

  • Update Shop module references from PhoenixKit.Modules.Shop to PhoenixKitEcommerce namespace
  • Update Billing module references from PhoenixKit.Modules.Billing to PhoenixKitBilling namespace
  • Restore LayoutWrapper in core module templates (not auto-applied to bundled modules)
  • Document extra_applications requirement for external module auto-discovery

Fixed

  • Fix missing url_path assign in referrals LiveViews causing runtime crash after LayoutWrapper restoration
  • Fix media selector modal mobile responsiveness (header overflow, button sizing, padding)

1.7.85 - 2026-03-30

Added

  • Add user-scoped media selector for avatar and fix custom fields position bug

Changed

  • Remove Pages module from core and clean up all references
  • Extract Connections module into external phoenix_kit_user_connections package
  • Remove unused Storage alias from user settings

1.7.84 - 2026-03-28

Added

  • Add missing cookie consent widget to dashboard layout
  • Add user dashboard routes for billing profiles

Changed

  • Remove Legal module from core (extracted to phoenix_kit_legal package)
  • Remove LayoutWrapper from remaining storage and maintenance templates
  • Remove duplicate LayoutWrapper from admin module templates

Fixed

  • Fix core modules misclassified as external plugin views causing double admin chrome

1.7.83 - 2026-03-27

Added

  • Add V88 migration: Publishing schema V2 restructure
  • Add user dashboard generator with LiveView templates and standardize layout
  • Add --index flag to user dashboard generator for overriding default dashboard
  • Add Estonian to backend languages, fix Chinese code zh-CN → zh
  • Add CountryData to core utils for billing extraction
  • Add sitemap scheduler startup recovery

Changed

  • Extract Comments module into external phoenix_kit_comments package
  • Remove Shop module from core (extracted to phoenix_kit_ecommerce package)
  • Remove Billing module from core (extracted to phoenix_kit_billing package)
  • Replace hardcoded external module stats with generic module_stats callback
  • Remove hardcoded module cards for extracted packages
  • Rename and simplify admin page generator
  • Update Leaf dependency to v0.2.6

Fixed

  • Fix V88 migration: index prefix and partial re-run safety
  • Fix orphan files query to use publishing_versions table
  • Fix post-review issues from PR #453: Shop.Cart guard, consent attrs, language naming
  • Fix shop modules: remove billing struct patterns and fix nil clause ordering
  • Fix double navbar on comments admin pages
  • Fix auth page background breaking footer and page layout

1.7.82 - 2026-03-24

Added

  • Add V86 migration: Document Creator tables (headers_footers, templates, documents)
  • Add V87 migration: Catalogue tables (manufacturers, suppliers, catalogues, categories, items)
  • Add system_prompt field to AI prompts and AI Playground page
  • Add database connection check to install and update tasks
  • Add AdminEditHelper for universal admin edit links in public views
  • Add email provider behaviour, refactor Mailer and UserNotifier
  • Add lastmod to sitemap group listings and homepage
  • Enrich external module cards with config stats, settings link, and module_card component

Changed

  • Extract Emails module from core to standalone phoenix_kit_emails package
  • Extract Publishing module to standalone phoenix_kit_publishing package
  • Extract Entities module to standalone phoenix_kit_entities package
  • Extract AI module to standalone phoenix_kit_ai package
  • Remove hardcoded Emails block from Modules page — now rendered as external package
  • Guard all Publishing references behind Code.ensure_loaded? for external module support
  • Guard EntityForm render call with Code.ensure_loaded? check in Pages renderer
  • Suppress warnings for optional external modules with @compile :no_warn_undefined
  • Exclude external module namespaces from Credo alias usage check
  • Make module registry and permissions tests count-independent after module extractions
  • Document ensure_compiled vs ensure_loaded? choice in integration route collection
  • Update Leaf dependency to v0.2.5

Fixed

  • Fix V86/V87 migrations to use uuid_generate_v7() instead of gen_random_uuid()
  • Fix post-merge issues from Emails extraction
  • Fix extract_admin_links: skip parent tabs, deduplicate paths
  • Fix external_plugin_view? to recognize PhoenixKit.Modules.*.Web as external packages
  • Fix DbConnectionCheck: correct spec, naming, and remove hard exit from status task
  • Fix media selector modal z-index to appear above all overlays
  • Fix module_card to render hero-* icons properly
  • Fix cookie consent: dynamic legal links, theme-aware backdrop, daisyUI toggle

1.7.81 - 2026-03-21

Changed

  • Extract Posts module to standalone phoenix_kit_posts package
  • Update Comments module to conditionally load Posts handler via Code.ensure_loaded?/1
  • Update scheduled jobs worker with extracted catch-up helpers for optional Posts dispatch

1.7.80 - 2026-03-20

Added

  • Add uuid type to custom fields system
  • Add auto-registration of custom field definitions on save with type inference

Changed

  • Extract Sync module to standalone phoenix_kit_sync package
  • Move custom fields domain logic to CustomFields module, deduplicate UUID regex, add error logging
  • Fix permissions table style — replace manual zebra striping with daisyUI table-zebra, use primary-colored header

Fixed

  • Fix avatar upload handling and custom_fields preservation in UserSettings
  • Fix admin page and user dashboard styles
  • Fix plugin reference name in module system guide

1.7.79 - 2026-03-20

Fixed

  • Fix UserSettings regressions from PR #436 redesign:
    • Restore timezone selector (timezone select, mismatch warning, browser detection)
    • Restore Apple OAuth provider icon (hero-device-phone-mobile)
    • Restore OAuth-only password warning for users without passwords
    • Restore provider email display in connected accounts list
    • Fix custom field select using index-based values instead of actual option values (data compatibility break)
    • Restore all custom field input types (textarea, number, email, url, date) — were collapsed to plain text
    • Restore required attribute on custom field inputs
    • Restore unique id attributes on password/email form hidden inputs
    • Restore profile/avatar success and error messages in template
    • Fix shadow-xlshadow-sm for card styling consistency
    • Fix divider placement — move out of username field, add "Additional Information" heading for custom fields
    • Extract extract_custom_fields/1 and merge_custom_fields/3 helpers to DRY duplicated logic

1.7.78 - 2026-03-18

Added

  • Add Tailwind/daisyUI class injection for markdown rendering — replaces inline <style> block with classes injected during Earmark post-processing (works without @tailwindcss/typography plugin)
  • Add blank line preservation in markdown content — intentional double blank lines render as visible spacing
  • Add translation worker retry resilience — on retry, already-translated languages are skipped by checking content timestamps against job inserted_at
  • Add dynamic timeout scaling for translation worker (~1.5 min per language, minimum 15 minutes)
  • Add structured logging with consistent prefixes ([Sync.Notifier], [Sync.API], [Sync.Connections]) throughout Sync connection flow
  • Add connection event logging on both sender and receiver sides for debugging

Changed

  • Rename Sync "Sender/Receiver" terminology to "Outgoing/Incoming" across UI
  • Allow editing incoming Sync connections (previously restricted to outgoing only)
  • Remove "with permanent connections" from Sync index page subtitle
  • Bump markdown render cache version to v2 to invalidate stale cached HTML

Fixed

  • Fix Sync sender URL resolving to localhost:4000 — now checks DB site_url setting before falling back to endpoint config
  • Fix auth_token_hash logged in full — truncate to first 8 characters in Sync connection logs
  • Fix double get_our_site_url() call per notification — pass resolved URL instead of recomputing
  • Fix Sync crash on non-UTF8 binary data — base64-encode raw binaries during serialization, decode on import
  • Fix Sync pull error responses silently ignored — add Logger.error to all failure paths (401, 404, HTTP errors, offline, invalid response)
  • Fix Sync completion UI not showing skipped/errored records — track and display per-table import counts with warning state

1.7.77 - 2026-03-17

Added

  • Add Open Graph and Twitter Card meta tags for public publishing pages (og:title, og:description, og:image, og:url, og:locale, canonical link, and Twitter Card tags)
  • Add og:site_name meta tag using project title
  • Add resolve_language_key/2 helper to LanguageHelpers for base code to dialect code matching in language maps
  • Add Tailwind Typography prose overrides using daisyUI theme variables (oklch(--bc), oklch(--p), oklch(--b2), oklch(--b3)) for theme-aware markdown styling
  • Add automated scheduled jobs cleanup to prevent table bloat (deletes completed jobs older than 7 days)
  • Add lastmod (last modified) to sitemap entries for SEO — router-discovered routes use beam file mtime, static entries use current date

Changed

  • Replace inline markdown CSS with centralized prose overrides in app.css using @layer base (removes 323 lines of duplication)
  • Update publishing preview template to show full public interface with working language switcher
  • Move MarkdownContent component to use Tailwind prose classes instead of custom inline styles
  • Extract duplicated resolve_language_key/2 from listing.ex and html.ex to shared LanguageHelpers module
  • Extract update_post_from_form/3 from publishing editor to reduce cyclomatic complexity (from >10 to <10)
  • Update Leaf content editor dependency from v0.1.0 to v0.2.0

Fixed

  • Fix mix phoenix_kit.status showing V01 instead of actual migration version — properly start Repo with parent app config when using --no-start
  • Fix language map lookup when canonical URL uses base code (e.g., "en" → "en-US" matching)
  • Fix absolute_url/2 to use stricter URL protocol checking ("http://" or "https://" instead of just "http")
  • Fix preview language links to conditionally include version parameter only when version is non-nil
  • Fix translation reload showing primary language content instead of translated content
  • Fix PubSub subscription mismatch for translation and version events on timestamp-mode posts (slug vs uuid topic mismatch)
  • Fix email template seeding failing on fresh install (wrap string fields in i18n maps)
  • Fix whitespace in slug format examples on publishing group pages

1.7.76 - 2026-03-16

Fixed

  • Fix mix phoenix_kit.status port conflict when app is already running (use --no-start to avoid booting the HTTP endpoint)
  • Add self-healing version comment detection — automatically corrects V83 comment bug where migrations ran but version stayed at V82

1.7.75 - 2026-03-16

Added

  • Add custom_fields support to registration_changeset/3 for atomic user creation with custom metadata
  • Add entity data view extension documentation and route override pattern

Fixed

  • Fix mobile overflow issues in email module UI (queue, blocklist, metrics, template editor)
  • Fix early validation in template editor — errors only shown after first user interaction
  • Fix Send Test Email modal overflowing on mobile (max-w-4xl → max-w-2xl)
  • Fix V83 migration missing down/1 version comment rollback
  • Fix V83 migration prefix_str inconsistency in version comment
  • Fix dialyzer guard_fail warnings from upstream publishing merge
  • Fix remaining doc warnings for delegated hidden functions

1.7.74 - 2026-03-16

Fixed

  • Remove dead should_regenerate_cache?/1 from Shared module (uncalled function returning true in every branch)
  • Remove obsolete bulk_operation_topic test referencing deleted PubSub function
  • Fix missing trailing newline in shared.ex

1.7.73 - 2026-03-13

Changed

  • Move module access guards from individual mount functions to centralized enforce_admin_view_permission hook
  • Disabled modules now block all roles (including Owner/Admin) at the on_mount level, covering all ~50 admin LiveViews automatically
  • Remove per-LiveView enabled?() mount guards from AI, Entities, Publishing, Sitemap, Billing, Customer Service, Emails, Email Tracking, Legal, Referrals, Shop settings

1.7.72 - 2026-03-13

Added

  • Add module access guards — disabled modules now hide action buttons and block mount on settings/endpoints
  • Add error flash auto-dismiss after 8 seconds
  • Add enabled?() mount guards to AI, Media, Entities, Publishing, Sitemap endpoints
  • Add error logging in Legal list_generated_pages instead of silent rescue

Fixed

  • Fix Legal module broken connection with DB-backed Publishing (post.pathpost.uuid, updated_atpublished_at)
  • Fix Legal module Configure button guard when module is disabled
  • Fix Sitemap RouterDiscovery including routes from disabled modules
  • Fix DB.Listener missing {:eventually, _ref} case for auto_reconnect

Changed

  • Remove duplicate enable/disable toggles from 7 module settings pages (Emails, Email Tracking, Legal, Referrals, Billing, Customer Service, Shop)
  • Simplify primary_language lookup in Publishing.DBStorage

1.7.71 - 2026-03-12

Fixed

  • Fix mixed atom/string key error in EntityData.maybe_add_position/1 when auto-assigning position to string-keyed params
  • Fix same mixed key error in EntityData.maybe_add_created_by/1
  • Fix FOR UPDATE with aggregate function error in EntityData.next_position/1 (PostgreSQL 0A000 feature_not_supported)

1.7.70 - 2026-03-12

Added

  • Add PhoenixKitGlobals component for JavaScript globals injection
  • Add metadata JSONB field to comments schema (V82 migration)
  • Add reply indicators to admin comments page
  • Add test comments seed script for visual verification
  • Add admin page generator category index pages with automatic route registration
  • Add duplicate validation (ID, URL, label) to admin page generator
  • Add compile-time warning for unresolved legacy admin LiveView modules
  • Add phoenix_kit_app_base/0 helper to Routes utility

Fixed

  • Fix dimension form inputs clearing each other on change
  • Fix MarkdownEditor toolbar not working on LiveView navigation
  • Fix CommentsComponent crash on post details page (resource_idresource_uuid)
  • Fix credo alias ordering in integration module
  • Fix WebP transparency loss in center-crop image processing
  • Fix 304 Not Modified support in FileController

Changed

  • Update admin page generator to use flat admin_dashboard_tabs config with live_view field
  • Deprecate legacy admin_dashboard_categories config format (warning on use)
  • Auto-infer LiveView modules from URL paths for legacy admin categories
  • Add attr :rest, :global to phoenix_kit_globals component

1.7.69 - 2026-03-10

  • Add responsive multi-column card grid to table_default component: 1 col on mobile, 2 cols on md, 3 cols on lg breakpoints
  • Style card view cards with bg-base-200 and shadow-sm to visually distinguish them from the page background

1.7.68 - 2026-03-10

  • Merge upstream changes: publishing editor rework, AI translation, bulk group actions, V77/V78 migration fixes, scheduled jobs queue, Settings.Queries module, plugin migration callbacks

1.7.67 - 2026-03-10

Breaking Changes (requires manual steps in parent app)

  • V79 migration rewritten in-place: drops phoenix_kit_mailing_* tables, creates phoenix_kit_newsletters_*
  • Oban queue renamed: mailing_deliverynewsletters_delivery (update config/config.exs)
  • Settings keys changed: mailing_enablednewsletters_enabled, mailing_default_templatenewsletters_default_template, mailing_rate_limitnewsletters_rate_limit
  • Email template category value changed: "mailing""newsletters" (existing templates need DB update)
  • URL paths changed: /admin/mailing/*/admin/newsletters/*, /mailing/unsubscribe/newsletters/unsubscribe

Changed

  • Rename PhoenixKit.Modules.MailingPhoenixKit.Modules.Newsletters and all submodules
  • Rename DB tables: phoenix_kit_mailing_lists/list_members/broadcasts/deliveriesphoenix_kit_newsletters_*
  • Rename Elixir modules: Mailing.List, Mailing.Broadcast, Mailing.Delivery, Mailing.ListMember, Mailing.Broadcaster, Mailing.Workers.DeliveryWorkerNewsletters.*
  • Rename web modules: Mailing.Web.*Newsletters.Web.*
  • Rename route module: PhoenixKitWeb.Routes.MailingRoutesNewslettersRoutes
  • Rename dashboard tabs: :admin_mailing:admin_newsletters

1.7.66 - 2026-03-09

  • Clean up publishing module: fix UUID routing bugs (slug vs UUID in version creation, PubSub broadcasts, translation status), remove dead code and filesystem path references
  • Optimize publishing DB queries: batch loading, ListingCache for dashboard, bulk UPDATE for translation statuses, debounced PubSub updates
  • Rework editor to two-column layout with content-first design (title + editor left, metadata right)
  • Rework AI translation: integrate AI prompt system, modal UI replacing slidedown, translation progress recovery across page refreshes
  • Replace primary language banner with compact tooltip on language switcher
  • Add skeleton loading UI for language switching in publishing editor
  • Fix collaborative editing: spectator initial sync, lock promotion JS updates, lock expiration timer
  • Fix admin sidebar highlighting for publishing group pages
  • Fix custom fields card hidden when no field definitions are registered
  • Add bulk "Add to Group" action on posts index with dynamic group filter dropdown

1.7.65 - 2026-03-08

  • Fix V77 migration crash: role_id column renamed to role_uuid in UUID migration

1.7.64 - 2026-03-08

  • Remove legacy filesystem paths from publishing module — strip all .phk virtual path references from mapper, editor, listing, and preview
  • Switch all event handlers and navigation from path-based to UUID-based routing
  • Fix timestamp-mode posts returning 404: normalize post_time to zero seconds, with hour:minute-only fallback query for legacy data
  • Add collision prevention for same-minute timestamp posts (auto-bump to next minute, max 60 attempts)
  • Add unique_constraint on (group_uuid, post_date, post_time) to schema
  • Render empty listing page instead of 404 when group exists but has no published posts
  • Show Primary Language banner during new post creation
  • Add missing PubSub handlers to publishing Index view (version_created, version_live_changed, version_deleted) with catch-all
  • Fix primary language migration using removed path field — now uses UUID/slug directly
  • Remove cache management UI from listing page (accessible via settings only)
  • Add safety guards to URL builders: raise ArgumentError on nil UUID instead of producing broken URLs

1.7.63 - 2026-03-06

  • Remove filesystem storage from publishing module — delete Storage, DualWrite, and all storage/* submodules (~7k lines removed)
  • Add LanguageHelpers and SlugHelpers as standalone modules, simplify to DB-only throughout
  • Fix slug conflict clearing bug: clear_url_slugs_for_conflicts passed wrong slug to DB cleanup
  • Fix ngettext interpolation in primary language migration modal (literal %{count} in UI)
  • Clean up stale filesystem references in comments, docs, and user-facing strings
  • Fix V77/V78 migration crashes when UUID columns are missing (tables created after V56 ran)
  • Simplify V77/V78 migrations — remove over-engineered column detection, rely on idempotent patterns
  • Fix email tracking bug: handle_delivery_result used get_log! (raises) in a nil-matching branch; add get_log/1 non-bang wrapper and remove unused public functions
  • Add migration_module/0 callback to plugin module system — mix phoenix_kit.update auto-discovers and runs plugin migrations
  • Add Settings.Queries module for database operations
  • Add dedicated queue and 1-day pruner for scheduled jobs cron worker
  • Fix user dashboard navigation links
  • Fix ueberauth providers configuration format in installer

1.7.62 - 2026-03-05

  • Fix UnicodeConversionError crash in integration plug when response body contains non-UTF8 binary data
  • Fix DB browser rendering of raw binary values (e.g. UUID bytes) in table and activity views
  • Add V78 migration: backfill missing AI module columns skipped by V41 conditional checks
    • Add reasoning_enabled, reasoning_effort, reasoning_max_tokens, reasoning_exclude to phoenix_kit_ai_endpoints
    • Add prompt_uuid, prompt_name to phoenix_kit_ai_requests with index and FK constraint

1.7.61 - 2026-03-04

  • Replace plug_cowboy with bandit ~> 1.0 as HTTP adapter (Phoenix 1.8 default)
  • Remove stale deps from lock: cowboy, cowlib, cowboy_telemetry, plug_cowboy, combine, dns_cluster, phoenix_live_dashboard, poolboy, timex, tzdata
  • Remove deprecated fetch_live_flash plug
  • Add audit_log query limits
  • Fix atom table exhaustion risk and remove duplicate function
  • Update excluded_apps list in route resolver to match current deps
  • Update HTML comments to EEx format in templates and components
  • Clean up stale dep spec and dead commented-out code

1.7.60 - 2026-03-03

  • Remove legacy FS→DB migration modules: DBImporter, MigrateToDatabaseWorker, ValidateMigrationWorker
  • Remove JsIntegration install/update module (JS setup is now manual)
  • Remove all "Import to DB" / "Migrate to Database" UI buttons from publishing pages
  • Remove DB import/migration PubSub broadcast functions and LiveView handlers
  • Simplify publishing listing: drop fs_post_count, needs_import, db_import_in_progress assigns
  • Move post title field into the editor content column with larger styling
  • Simplify editor save button logic (always clickable unless readonly/autosaving)
  • Add enrich_with_db_uuids/2 to ListingCache for UUID-based admin links in filesystem mode
  • Refine Sync module: migrate connection_id references to connection_uuid
  • Update publishing README to reflect DB-only storage model

1.7.59 - 2026-03-03

  • Fix V75: use CASCADE when dropping phoenix_kit_id_seq (meta table phoenix_kit.id DEFAULT depends on it)

1.7.58 - 2026-03-03

  • Add V75 migration: fix uuid column defaults and cleanup
    • Set DEFAULT uuid_generate_v7() on 27 tables missing it (Category A tables — V72 rename dropped old sequence DEFAULT)
    • Fix 4 tables using gen_random_uuid() (UUIDv4) → uuid_generate_v7() (UUIDv7)
    • Drop orphaned phoenix_kit_id_seq sequence

1.7.57 - 2026-03-03

  • Fix V74 migration: skip tables without bigint id (e.g. publishing tables created with UUID PKs)
  • Fix V74: use DROP COLUMN id CASCADE to handle dependent FK constraints in one statement

1.7.56 - 2026-03-03

  • Add V74 migration: drop integer id/_id columns, promote uuid to PK on all tables
    • Drop all FK constraints referencing integer id columns (dynamic discovery)
    • Drop ~95 integer FK columns across all tables (sourced from uuid_fk_columns.ex + extras)
    • Drop bigint id PK + promote uuid to PK on 47 Category B tables
    • After V74, every PhoenixKit table uses uuid as its primary key — no integer PKs remain
  • Remove source: :id from webhook_event.ex schema (DB column now matches field name)

1.7.55 - 2026-03-03

  • Fix scheduled_job.ex source: :id regression — PR #383 reintroduced mapping to dropped DB column
  • Add V73 migration: pre-drop prerequisites for Category B UUID migration
    • SET NOT NULL on 7 uuid columns (ai_endpoints, ai_prompts, consent_logs, payment_methods, role_permissions, subscription_types, sync_connections)
    • CREATE UNIQUE INDEX on 3 tables (consent_logs, payment_methods, subscription_types)
    • ALTER INDEX RENAME on 4 indexes to match renamed columns (post_tag_assignments, post_group_assignments, post_media, file_instances)
  • Add RepoHelper.get_pk_column/1 — queries pg_index for PK column name, falls back to "id"
  • Fix DB explorer to use dynamic PK column in fetch_row, table_preview, and notify trigger
  • Fix Sync API controller to use dynamic PK column in fetch_filtered_records and build_where_clause
  • Fix Sync connection notifier to use dynamic PK column in insert_record and build_update_clause
  • Update 4 schema constraint names to match V72 column renames (post_idpost_uuid, file_idfile_uuid)
  • Remove dead :user_id from OAuth replace_all_except list

1.7.54 - 2026-03-03

  • Add V72 migration: rename PK column iduuid on 30 Category A tables (metadata-only, instant)
  • Add 4 missing FK constraints: comments.user_uuid, comments_dislikes.user_uuid, comments_likes.user_uuid, scheduled_jobs.created_by_uuid
  • Remove source: :id mapping from 29 Category A Ecto schemas — DB column now matches field name directly

1.7.53 - 2026-03-02

  • Add mix phoenix_kit.doctor diagnostic command — detects migration version vs schema_migrations discrepancies, stale COMMENT tags, and common DB issues
  • Add update_mode to mix phoenix_kit.update — skips heavy DB components (Oban, cache warmers, settings queries) and caps Ecto pool at 2 during migrations to prevent DB saturation
  • Run ecto.migrate in-process instead of System.cmd for better error reporting and reliability
  • Fix three migration hang root causes: NULL UUIDs causing infinite backfill loop, orphaned FK references blocking constraint creation, varchar uuid columns crashing Ecto schema loader
  • Fix migration hang: disable DDL transaction in generated migration wrapper — prevents entire multi-version migration from running in a single transaction holding AccessExclusiveLock
  • Fix V50 migration hang: add lock_timeout for phoenix_kit_buckets ALTER TABLE and check column existence before ALTER
  • Fix settings cache race: warm synchronously in init/1 when sync_init: true; fix warm_critical_data inserting {key, value, nil} 3-tuples when TTL is nil
  • Fix cache sync_init blocking supervisor for 60s when DB is overloaded
  • Fix startup DB timeout: defer Dashboard.Registry init and reorder supervisor children
  • Silence cache warmer spam and auto-grant warning when role_permissions table doesn't exist yet
  • Fix gen.migration task: generate UUID primary keys and user_uuid FK instead of integer-based
  • Fix broken GDPR anonymization: remove leftover user_id: nil from update_all calls
  • Rename _id_uuid across all remaining application code: billing, shop, storage, entities, sync, emails, tickets, permissions, roles, connections, publishing, and user_notifier
  • Rename function names: find_role_by_idfind_role_by_uuid, parse_idparse_uuid, import_idimport_uuid
  • Fix Dialyzer warning: remove unreachable pattern match in cache warming
  • Fix V56/V63 migration crash: email_log_uuid backfill fails with datatype_mismatch when phoenix_kit_email_logs.uuid is character varying instead of native uuid type
  • Fix UUIDFKColumns: replace broken Elixir rescue with PostgreSQL EXCEPTION handler inside DO blocks — prevents outer transaction abort on backfill failure
  • Add ::uuid explicit cast in all UUIDFKColumns backfill SQL to handle varchar source columns gracefully
  • Fix V56: add pre-step to convert varchar uuid columns on all FK source tables to native uuid type before UUIDFKColumns.up runs
  • Fix V63: wrap matched_email_log_uuid backfill in DO block with EXCEPTION handler and ::uuid cast
  • Add V70 migration: re-backfills email_log_uuid and matched_email_log_uuid for installs where V56/V63 silently skipped the backfill; resets stale random UUIDs written by the V56 NULL-fill fallback
  • Add investigation doc: dev_docs/investigations/2026-03-01-varchar-uuid-migration-bug.md

1.7.52 - 2026-02-28

  • Add translatable title field to posts and fix timestamp-mode post handling
  • Add V69 migration: make role table integer FK columns nullable
  • Add mix precommit alias (compile → format → credo --strict) to mix.exs
  • Update AGENTS.md with pre-commit instructions, replacing old minimal checklist
  • Rename Scope.user_idScope.user_uuid for consistency
  • Rename user_iduser_uuid across event handlers, templates, and messages
  • Rename user_iduser_uuid in emails rate_limiter; log_idlog_uuid in emails interceptor, SQS processor, and sync task
  • Rename user_idaws_user_id in AWS credentials verifier
  • Rename resource_idresource_uuid in scheduled jobs
  • Rename _id_uuid in billing, shop, AI, entities, legal, posts, tickets, storage, scheduled jobs, and permissions
  • Rename _id_uuid across metadata, forms, helpers, and tests
  • Replace DateTime.utc_now() with UtilsDate.utc_now() across codebase
  • Remove redundant connection_id parameter from sync connection_notifier
  • Fix crash bugs: .user_id struct access → .user_uuid in billing events and order_form
  • Fix UUID field references for webhook_events and post images
  • Fix duplicate map keys left from UUID migration in hooks and rate_limiter
  • Fix alias ordering Credo violations across 18 files
  • Fix timestamp-mode post lookups, migration ordering, and admin UI (PR #376 review follow-up)
  • Fix tab_callback_context missing clause; demote double-wrap log to debug
  • Add dialyzer ignore for unused tab_callback_context clause
  • Update integration guide, making-pages-live guide, dashboard README, and usage-rules to use UUID terminology

1.7.51 - 2026-02-26

  • Add V64 migration: fix login crash by replacing user_id check constraint with user_uuid on user tokens table
  • Add V65 migration: rename SubscriptionPlan to SubscriptionType (table, columns, indexes, constraints)
  • Rename SubscriptionPlan schema, context functions, events, routes, LiveViews, and workers to SubscriptionType
  • Add orphaned media file cleanup system
    • mix phoenix_kit.cleanup_orphaned_files task with dry-run and --delete modes
    • DeleteOrphanedFileJob Oban worker with 60s delay and orphan re-check before deletion
    • Orphan filter toggle and "Delete all orphaned" button in Media admin UI
  • Add Delete File button with confirmation modal to Media Detail page
  • Add secondary language slug uniqueness validation via JSONB query in entity data
  • Rename seed_title_in_data to seed_translatable_fields in entity data form
  • Unify slug labels to "Slug (URL-friendly identifier)" across entity forms
  • Standardize dev_docs file naming convention ({date}-{kebab-case}-{type}.md)
  • Fix orphan detection crash: remove references to non-existent phoenix_kit_shop_variants table
  • Fix String.trim(nil) crash in SQS workers when AWS credentials not configured
  • Fix default preload :plan to :subscription_type in list_subscriptions/1 and list_user_subscriptions/2
  • Fix auth.ex storing integer file.id instead of file.uuid for avatar custom field
  • Fix create_subscription/2 dead code and key mismatch — now accepts :subscription_type_uuid as preferred key
  • Fix change_subscription_type/3 reading stale subscription_type_id instead of subscription_type_uuid
  • Fix AWS Config returning empty string instead of nil when credentials unconfigured
  • Fix email log Access behaviour error when called with EmailLogData struct
  • Fix Interceptor using deprecated log.id instead of log.uuid in header and event
  • Fix shop billing cascade: check disable_system() result and log on failure
  • Replace defp proxy wrappers with import in 4 shop LiveViews (cart, catalog, checkout)
  • Rename post_id to post_uuid in 5 private post functions
  • Remove legacy integer ID function clauses from posts and billing modules
  • Remove accidentally committed .beam files, add *.beam to .gitignore
  • Remove empty legacy subscription_plan_form.ex and subscription_plans.ex
  • Add doc notes about performance for all_admin_tabs/0 and get_config/0
  • Remove dead _plugin_session_name variable from integration routes

1.7.50 - 2026-02-25

  • Fix defp show_dev_notice? CLAUDE.md violation: replace private helper with <.dev_mailbox_notice> Phoenix Component
    • New component at lib/phoenix_kit_web/components/core/dev_notice.ex with message and class attrs
    • Removed from login.ex, registration.ex, magic_link.ex, forgot_password.ex, dashboard/settings.ex
    • Updated all corresponding HEEX templates to use <.dev_mailbox_notice>
  • Fix duplicate route alias compilation warnings in phoenix_kit_authenticated_routes/1
    • Split module-scope routes into authenticated_live_routes/0 and authenticated_live_locale_routes/0
    • Locale variants now use _locale suffix (e.g. :shop_user_orders_locale)
  • Fix undeclared sidebar_after_shop attr in shop_layout/1 component
  • Fix maybe_redirect_authenticated/1 hardcoded "/" redirect — use signed_in_path(socket) consistently
  • Fix double Map.from_struct in Emails.Interceptor.create_email_log/2 — redundant call removed

1.7.49 - 2026-02-24

  • Add V63 migration: UUID companion column safety net round 2
    • Add uuid identity column to phoenix_kit_ai_accounts (missed by V61 due to wrong table name)
    • Add account_uuid companion to phoenix_kit_ai_requests (backfilled from ai_accounts)
    • Add matched_email_log_uuid to phoenix_kit_email_orphaned_events (backfilled from email_logs)
    • Add subscription_uuid to phoenix_kit_invoices (backfilled from subscriptions)
    • Add variant_uuid to phoenix_kit_shop_cart_items (nullable, no variants table)
    • Update Invoice, AI Request, and CartItem schemas with new uuid companion fields

1.7.48 - 2026-02-24

  • Add V62 migration: rename 35 UUID-typed FK columns from _id suffix to _uuid suffix
    • Enforces naming convention: _id = integer (legacy/deprecated), _uuid = UUID
    • Groups: Posts module (15 renames), Comments (4), Tickets (6), Storage (3), Publishing (3), Shop (3), Scheduled Jobs (1)
    • No data migration — columns already held correct UUID values, pure rename
    • All DB operations idempotent (IF EXISTS guards) — safe on installs with optional modules disabled
    • Update all Ecto schemas, context files, web files, and tests to use new field names

1.7.47 - 2026-02-24

  • Fix V13 migration down/0 to use remove_if_exists instead of remove for idempotency
    • Fixes "column aws_message_id does not exist" error when rolling back V13

1.7.46 - 2026-02-24

  • Add plugin module system with PhoenixKit.Module behaviour, ModuleRegistry, and zero-config auto-discovery
    • 5 required + 8 optional callbacks with sensible defaults via use PhoenixKit.Module
    • Auto-discovers external modules by scanning .beam files for @phoenix_kit_module attribute
    • All 21 internal modules now implement the behaviour, removing 786 lines of hardcoded tab enumeration
    • External module admin routes auto-generated at compile time from admin_tabs with live_view field
  • Add live sidebar updates via PubSub when modules are enabled/disabled
  • Add server-side authorization on module toggle events (prevents crafted WebSocket bypass)
  • Add startup validation: duplicate module keys, permission key mismatches, duplicate tab IDs, missing permission fields
  • Add compile-time warnings for route module and LiveView compilation failures
  • Standardize AI, Billing, and Shop to use update_boolean_setting_with_module/3 (consistent with all other modules)
  • Fix billing→shop cascade: shop now disabled after billing toggle succeeds (prevents orphaned state)
  • Fix Tab.permission_granted?/2 to handle atom permission keys instead of silently bypassing checks
  • Fix static_children/0 to catch module children/0 failures instead of crashing the supervisor

1.7.45 - 2026-02-23

  • Fix auth forms mobile overflow on small screens (px-4 added to all form containers)
  • Fix daisyUI v5 compliance: remove deprecated input-bordered from <.input> component and all auth templates
  • Fix <.header> hardcoded text-zinc-* colors replaced with semantic text-base-content for dark theme support
  • Convert forgot_password, reset_password, confirmation, confirmation_instructions to unified card layout
  • Add missing LayoutWrapper.app_layout wrapper to confirmation form
  • Fix V40 migration silently skipping V32-V39 tables due to Ecto command buffering
    • Root cause: repo().query() (immediate) couldn't see buffered table creation commands
    • V31's flush() was the last flush before V40, creating a clean V31/V32 split
    • Add flush() to V40 and V56 to prevent recurrence on new installations
  • Add V61 migration: uuid column safety net for 6 tables missed by V40
    • Tables fixed: admin_notes, ai_requests, subscriptions, payment_provider_configs, webhook_events, sync_transfers
    • Also adds created_by_uuid FK column to phoenix_kit_scheduled_jobs

1.7.44 - 2026-02-23

  • Add Publishing module: DB storage, public post rendering, and i18n support
  • Add unified admin_page_header component, replace all per-page admin headers
  • Add try/rescue to all form save handlers to prevent silent data loss on validation errors
  • Add skeleton loading placeholders for entity language tab switching
  • Add "Update Entity" submit button at top of entity form for quicker saves
  • Add responsive card view to entities listing, remove stats/filters
  • Memoize IbanData.all_specs/0 with compile-time module attribute for performance
  • Auto-register built-in comment resource handlers
  • Make entity slug translatable and move it into Entity Information section
  • Move multilang info alert above language tabs with improved explanation
  • Tighten language tab spacing, replace daisyUI tab classes with compact utilities
  • Remove hardcoded category column, filter, and bulk action from data navigator
  • Fix CommentsComponent crash on post detail page
  • Fix Entity update crash from DateTime microseconds in :utc_datetime fields
  • Fix email_templates schema/migration mismatch breaking fresh installs
  • Fix locale disappearing from admin URLs on sidebar navigation
  • Fix badge component height on mobile devices
  • Fix cached plan error spam during migrations with column type changes
  • Fix CSS specificity debt and inline styles replaced with Tailwind classes
  • Fix mobile responsiveness across admin panel
  • Replace remaining DateTime.utc_now() with UtilsDate.utc_now() in all DB write contexts

1.7.43 - 2026-02-18

  • Standardize all schemas to :utc_datetime and DateTime.utc_now() across 73 files
    • Replace :utc_datetime_usec with :utc_datetime and NaiveDateTime with DateTime
    • Add V58 migration to convert all timestamp columns across 68 tables from timestamp to timestamptz
    • Fix UUID FK backfill to handle NULL UUIDs before applying NOT NULL constraints
  • Fix DateTime.utc_now() microsecond crashes in 19 files after :utc_datetime schema migration
    • Add DateTime.truncate(:second) to all DateTime.utc_now() calls in contexts
    • Affected: settings, billing, shop, emails, referrals, tickets, comments, auth, permissions, roles
  • Fix Language struct Access error on admin modules page and all bracket-access-on-struct bugs
  • Add 20 typed structs replacing plain maps across billing, entities, sync, emails, AI, and dashboard
    • Billing: CheckoutSession, SetupSession, WebhookEventData, PaymentMethodInfo, ChargeResult, RefundResult, ProviderInfo
    • Other: AIModel, FieldType, EmailLogData, LegalFramework, PageType, Group, TableSchema, ColumnInfo, SitemapFile, TimelineEvent, IbanData, SessionFingerprint
  • Fix register_groups to convert plain maps to Group structs, preventing sidebar crashes
  • Fix CastError in live sessions page by using UUID lookup instead of integer id
  • Fix guest checkout flow: relax NOT NULL on legacy integer FK columns, fix transaction error double-wrapping
  • Add return_to login redirect support for seamless post-login navigation (e.g., guest checkout)
  • Add cart merge on login for guest checkout sessions
  • Fix shop module .id to .uuid migration in Storage image lookups and import modules
  • Fix hardcoded "PhoenixKit" fallback in admin header project title
  • Fix admin sidebar submenu not opening on localized routes
  • Fix 2 dialyzer warnings in checkout session and UUID migration
  • Add multi-language support for Entities module
    • New Multilang module with pure-function helpers for multilang JSONB data
    • Language tabs in entity form, data form, and data view (adaptive compact mode for >5 languages)
    • Override-only storage for secondary languages with ghost-text placeholders
    • Lazy re-keying when global primary language changes (recomputes all secondary overrides)
    • Translation convenience API: Entities.set_entity_translation/3, EntityData.set_translation/3, EntityData.set_title_translation/3, and related get/remove functions
    • Multilang-aware category extraction in data navigator and entity data
    • Non-translatable fields (slug, status) separated into their own card
    • Required field indicators hidden on secondary language tabs
    • Title translations stored as _title in JSONB data column (unified with other field translations)
    • Slug generation disabled on secondary language tabs
    • Validation error messages wrapped in gettext for i18n
    • 124 pure function tests for Multilang, HtmlSanitizer, FieldTypes, FieldType
  • Fix entities multilang review issues
    • Unify title storage in JSONB data column, fix rekey logic for primary language changes
    • Add seed_title_in_data for lazy backwards-compat migration on mount
    • Replace String.to_existing_atom with compile-time @preserve_fields map
    • Fix 7 remaining issues from PR #341 permissions review
    • Add catch-all fallback clauses to Scope functions to prevent FunctionClauseError
    • Sort custom_keys/0 explicitly instead of relying on Erlang map ordering

1.7.42 - 2026-02-17

  • Use PostgreSQL IF NOT EXISTS / IF EXISTS for UUID column operations
    • Replace manual column_exists? checks with native DDL guards in V56 and UUIDFKColumns
    • Makes migrations more robust and idempotent

1.7.41 - 2026-02-16

  • Fix FK constraint creation crash when UUID target tables lack unique indexes
    • Ensure unique indexes on all FK-target uuid columns before adding FK constraints
    • Fixes invalid_foreign_key error on phoenix_kit_ai_endpoints and other tables

1.7.40 - 2026-02-16

  • Remove redundant mb-4 wrapper div around back buttons in 4 admin pages
  • Add V57 migration to repair missing UUID FK columns
  • Update language filter to use languages_official instead of languages_spoken

1.7.39 - 2026-02-16

  • Complete UUID migration (Pattern 2) across all remaining modules
    • Migrate posts, tickets, storage, comments, referrals, and connections schemas to UUID-based user references
    • Migrate posts like/dislike/mention functions to accept UUID user identifiers
    • Fix stale .id access across posts, storage, tickets, email, connections, and image downloader
    • Fix ProcessFileJob and media_detail to use user_uuid instead of deprecated user_id
    • Replace legacy .id access with .uuid across mix tasks and admin presence
    • Remove legacy integer fields from RoleAssignment schema
    • Fix 10 Dialyzer warnings across comments, connections, referrals, and shop modules
  • Harden permissions system with security and correctness fixes
    • Fix security and correctness issues in permissions system
    • Add permission edit protection for own role and higher-authority roles
    • Add Owner protection to can_edit_role_permissions/2 and standardize UUID usage
    • Fix edge cases, silent failures, and crash risks in permissions and roles
    • Fix dual-write in set_permissions/3 and cross-view PubSub refresh
    • Fix permissions summary to count only visible keys
    • Fix multiple bugs in custom permission keys and admin routing
    • Add auto-grant of custom permission keys to Admin role
    • Add defensive input validation to custom permission key registration
    • Fix unless/else to if/else for Credo compliance
  • Add gettext i18n to roles and permissions admin UI
  • Add Level 1 test suite for permissions, roles, and scope (156 tests)
  • Fix responsive header layout across all admin pages
    • Add responsive text classes (text-2xl sm:text-4xl / text-base sm:text-lg) to all page headers
    • Fix missed responsive text classes in storage, media selector, and publishing pages
  • Replace dropdown action menus with inline buttons in table rows
  • Fix require_module_access plug to check feature_enabled like LiveView on_mount
  • Fix admin sidebar wipe when enabling/disabling modules
  • Add get_role_by_uuid/1 API and update integration guide
  • Restore admin edit button in user dropdown and add product links in cart
  • Fix selected_ids to use MapSet for O(1) lookups
  • Fix Dialyzer CI failure for ExUnit.CaseTemplate test support files
  • Fix Credo nesting and Dialyzer MapSet opaque type warnings
  • Update Permissions Matrix page title and section labels

1.7.38 - 2026-02-15

  • Fix Ecto.ChangeError in entities by using DateTime instead of NaiveDateTime
  • Fix infinite recursion risk in category circular reference validation
  • Add DateTime inconsistency audit report with phased migration plan
  • Add custom permission key auto-registration for admin tabs
    • Custom admin tabs with non-built-in permission keys now auto-register with the permission system
    • Custom keys appear in the permission matrix and roles popup under "Custom" section
    • Owner role automatically gets access to custom permission keys
    • Custom LiveView permission enforcement via cached :persistent_term mapping
    • New API: Permissions.register_custom_key/2, unregister_custom_key/1, custom_keys/0, clear_custom_keys/0
    • Key validation: format check (~r/^[a-z][a-z0-9_]*$/), ArgumentError on built-in key collision

1.7.37 - 2026-02-15

  • Fix UUID PR review issues: aliases, dashboard_assigns, and naming issues
  • Fix V56 migration: add subscription_plans to uuid column setup lists
  • Add admin edit buttons and improve shop catalog UX
  • Add registry-driven admin navigation system
  • Fix localized field validation in Shop forms
  • And bunch of bugs and optimizations

1.7.36 - 2026-02-13

  • Add storefront sidebar filters, category grid, and dashboard shop integration
    • New CatalogSidebar component: reusable sidebar with collapsible filter sections and category tree navigation
    • New FilterHelpers module: filter data loading, URL query string building, price/vendor/metadata filtering
    • Storefront filter configuration in admin settings: enable/disable filters, edit labels, add metadata option filters
    • Auto-discovery of filterable product metadata options (e.g., Size, Color) with one-click filter creation
    • Price range filter with min/max inputs and range display
    • Vendor and metadata option filters with checkbox selection and active count badges
    • Filter state persisted in URL query params for shareable filtered views
    • "Show Categories in Shop" setting: displays category card grid above products on main shop page
    • Sidebar category navigation always visible in sidebar (decoupled from grid setting)
    • Dashboard layout integration: shop filters and categories rendered in dashboard sidebar for authenticated users
    • sidebar_after_shop slot in dashboard layout for injecting custom sidebar content
    • Product detail page updated to use shared sidebar and filter context for consistent navigation
    • Mobile filter drawer with toggle button and active filter count badge
    • Category page filters scoped to category products
    • Fix phx-value-value collision on filter checkboxes: renamed to phx-value-val to avoid HTML checkbox value="on" overwrite
    • Known issue: metadata option filters (e.g., Size) may not filter correctly in all cases; needs further investigation
  • Add file upload field type to Entities module
    • New file field type with configurable max entries, file size, and accepted formats
    • FormBuilder renders file upload UI with drag-and-drop zone (admin entity forms, placeholder)
    • New :advanced field category
  • Fix 3 remaining UUID migration bugs in billing forms
  • Fix 8 UUID migration bugs found in PR #330 post-merge review
  • Add UUIDv7 migration V56 with dual-write support

1.7.35 - 2026-02-12

  • Rewrite Sitemap module to sitemapindex architecture with per-module files
    • /sitemap.xml now returns a <sitemapindex> referencing per-module files at /sitemaps/sitemap-{source}.xml
    • Dual mode support: "Index mode" (per-module files, default) and "Flat mode" (single urlset when Router Discovery enabled)
    • New Source behaviour callbacks: sitemap_filename/0 and sub_sitemaps/1 for per-group file splitting
    • New Generator.generate_all/1 and generate_module/2 with auto-splitting at 50,000 URLs
    • FileStorage rewrite with save_module/2, load_module/1, delete_module/1, list_module_files/0
    • Cache rewrite supporting {:module_xml, filename} and {:module_entries, source} keys
    • Per-module stats stored as JSON in Settings with get_module_stats/0
    • Per-module regeneration via SchedulerWorker.regenerate_module_now/1 (Oban)
    • Settings UI overhaul: per-module sitemap cards with stats, regeneration buttons, mode indicators
    • Publishing source: per-blog sub-sitemaps via sitemap_publishing_split_by_group setting
    • Entities source: per-entity-type sub-sitemaps
    • Static source: login page excluded, registration conditionally included
    • Router Discovery default changed to false (index mode is new default)
    • Removed "cards" XSL style; added sitemap-index-minimal.xsl and sitemap-index-table.xsl
    • Sitemap routes no longer go through :browser pipeline (public XML endpoints)
  • Add PDF support for Storage module
    • New PdfProcessor module using poppler-utils (pdftoppm, pdfinfo)
    • First page rendered to JPEG thumbnail at configurable DPI
    • PDF metadata extraction (page count, title, author, creator, creation date)
    • VariantGenerator extended for document/PDF MIME types
    • Media UI: inline PDF viewer on detail page, PDF badges on thumbnails, metadata display
    • New system dependency checks for poppler in Dependencies module
  • Fix option price display for options with all-zero modifiers
    • New has_nonzero_modifiers?/1 filters out option groups where all price modifiers are zero
    • Price modifiers displayed as badges on option buttons (e.g., "+$5.00")
    • Cart saves all selected specs including non-price-affecting options (e.g., Color)
    • build_cart_display_name/3 includes all selected specs in display name
  • Fix category icons fallback to legacy product images
    • Category.get_image_url/2 falls back to featured_product.featured_image (legacy URL)
    • Product detail respects shop_category_icon_mode setting for category subtab icons
    • Guard clauses tightened for Storage vs legacy URL handling
  • Add ImportConfig filtering at CSV preview stage
    • Config filters applied during CSV analysis/preview, not just during import
    • Import wizard shows skipped product count with warning badge
    • Category creation uses language normalization for consistent JSONB slug keys
    • Imported option labels use _option_slots metadata for proper display names
  • Fix admin sidebar full-page reload after upstream merge
    • Comments and Sync routes merged into main admin live_session
  • Add runtime sitemaps directory to gitignore

1.7.34 - 2026-02-11

  • Extract Comments into standalone reusable module (V55 migration)
    • New PhoenixKit.Modules.Comments context with polymorphic resource_type + resource_id associations
    • New tables: phoenix_kit_comments, phoenix_kit_comments_likes, phoenix_kit_comments_dislikes
    • Reusable CommentsComponent LiveComponent that can be embedded in any resource detail page
    • Threaded comments with configurable max depth and content length
    • Like/dislike system with atomic counter cache
    • Moderation admin UI at {prefix}/admin/comments with filters, search, and bulk actions
    • Module settings page at {prefix}/admin/settings/comments
    • Resource handler callback system for notifying parent modules (e.g., Posts) of comment changes
    • "comments" permission key added (25 total permission keys, 20 feature modules)
    • Posts module refactored to consume Comments module API instead of inline implementation
    • Legacy phoenix_kit_post_comments tables preserved for backward compatibility
  • Add shop enhancements, sitemap sources, and admin navigation fix
    • Shop module improvements: product options toggle, import configs, drag-and-drop reordering, catalog language redirects
    • Sitemap module: shop source (categories, products, catalog), data source toggles in settings UI
    • Admin sidebar seamless navigation (consolidate live_sessions)
    • Migration fixes and V54 addition
  • Fix preview-to-editor round-trip state and data loss bugs
    • Fix 8 bugs in the preview_token handle_params path that had diverged from the other editor entry points as features were added over time
    • Merge disk metadata into preview post to prevent silent data loss when saving after a preview round-trip
    • Add error logging to enrich_from_disk for observability
  • Add module-level permission system for role-based admin access control
    • Custom roles can now be granted granular access to specific admin sections and feature modules. Permissions are managed through a new interactive matrix UI, enforced at both route and sidebar level, and update in real-time across all admin tabs via PubSub.

1.7.33 - 2026-02-04

  • Add module-level permission system (V53 migration)
    • phoenix_kit_role_permissions table with allowlist model (row present = granted)
    • 24 permission keys: 5 core sections + 19 feature modules
    • Owner bypasses all checks; Admin seeded with all 24 keys by default
    • Custom roles start with no permissions, assigned via matrix UI or API
    • PhoenixKit.Users.Permissions context for granting, revoking, and querying role permissions
    • Interactive permission matrix at {prefix}/admin/users/permissions
    • Inline permission editor in Roles page with grant/revoke all
    • Route-level enforcement via phoenix_kit_ensure_admin and phoenix_kit_ensure_module_access
    • Sidebar nav gated per-user based on granted permissions
    • Real-time PubSub updates: permission changes reflect across all admin tabs
    • Backward compatible: pre-existing Admins retain full access before V53 migration
  • Add PubSub events for real-time updates in Tickets and Shop modules
    • Tickets.Events module with broadcast for ticket lifecycle (created, updated, status changed, assigned, priority changed)
    • Comment and internal note events for ticket discussions
    • Shop.Events extension with product, category, inventory events
    • LiveViews subscribe to events for real-time UI updates
  • Add User Deletion API with GDPR-compliant data handling
    • delete_user/2 with cascade delete for related data (tokens, OAuth, billing profiles, carts)
    • Anonymization strategy for orders, posts, comments, tickets, email logs, files
    • Protection: cannot delete self, cannot delete last Owner
    • Admin UI with delete button, confirmation modal, and real-time list updates
    • Broadcast :user_deleted event for multi-admin synchronization
  • Fix compilation errors in auth.ex (pin operator with dynamic Ecto queries)
  • Update core PhoenixKit schemas and Referrals to new UUID standard
  • Update Shop module with localized slug support and unified image gallery
  • Add PubSub events for Tickets and Shop modules, User Deletion API
  • Added support for uuid to referral module
  • Add markdown rendering and bucket access types
  • Update Sync module to new UUID standard pattern
  • Update billing module to use DB-generated UUIDs
  • Update entities module to UUID standard matching AI module

1.7.32 - 2026-02-03

  • Storage Module: Smart file serving with bucket access types (V50 migration)

    • Add access_type field to buckets: "public", "private", "signed"
    • Local files are now served directly without temp file copying (performance improvement)
    • Public cloud buckets redirect to CDN URL (faster, reduces server load)
    • Private cloud buckets proxy files through server (for ACL-protected storage)
    • Add retry logic for bucket cache race conditions during file access

    ⚠️ BREAKING CHANGE: Cloud Bucket Access Type

    Cloud buckets (S3, B2, R2) now default to access_type = "public", which redirects users directly to the bucket's public URL instead of proxying through the server.

    If you have private/ACL-protected buckets:

    • Go to Storage → Buckets → Edit your bucket
    • Set "Access Type" to "Private"
    • Files will be proxied through the server using credentials (previous behavior)

    If you have public buckets (redirect mode):

    For redirect to work, your bucket must be publicly accessible:

    1. Enable Public Access in your cloud provider settings:

      • AWS S3: Disable "Block all public access" and set bucket policy
      • Backblaze B2: Set bucket to "Public"
      • Cloudflare R2: Configure public access or use Custom Domain
    2. Configure CORS if serving files cross-origin (required when your site domain differs from bucket domain):

      AWS S3 / R2 CORS configuration example:

      [
        {
          "AllowedHeaders": ["*"],
          "AllowedMethods": ["GET", "HEAD"],
          "AllowedOrigins": ["https://yourdomain.com"],
          "ExposeHeaders": ["ETag", "Content-Length"],
          "MaxAgeSeconds": 3600
        }
      ]

      Replace https://yourdomain.com with your actual domain, or use "*" for any origin (less secure but simpler for testing).

    See AWS documentation: https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html

1.7.31 - 2026-01-29

  • Refactor publishing module into submodules and improve URL slug handling
    • Storage module refactoring:
      • Split storage.ex into specialized submodules: Paths, Languages, Slugs, Versions, Deletion, and Helpers for better organization and maintainability
      • Move controller logic into submodules: Fallback, Language, Listing, PostFetching, PostRendering, Routing, SlugResolution, Translations
      • Move editor logic into submodules: Collaborative, Forms, Helpers, Persistence, Preview, Translation, Versions
    • Listing page improvements:
      • Show live version's translations and statuses instead of latest version
      • Fetch languages from filesystem when version_languages cache is empty
      • Fix paths to point to live version files when clicking language buttons
      • Add "showing vN" badge that combines with version count display
      • Fix public URL to always use post's primary language
    • URL slug priority system:
      • Directory slugs now have priority over custom url_slugs
      • Prevent setting url_slug that conflicts with another post's directory name
      • Auto-clear conflicting url_slugs instead of blocking saves
      • Show info notice when url_slugs are auto-cleared due to conflicts
      • Clear conflicting url_slugs from ALL translations, not just current one
      • Clear conflicting custom url_slugs when new post is created

1.7.30 - 2026-01-28

  • Posts Module
    • Add likes and dislikes system for post comments (V48 migration)
    • Post body field is no longer required
  • User Management
    • Add dropdown field type support for user custom fields
  • Shop Module (E-commerce)
    • Fix JSONB search queries and add defensive guards for robustness
    • Fix JSONB localized fields consistency across product/category operations
    • Add shop import enhancements with V49 migration
    • Fix image migration robustness and catalog display issues
    • Add language selection dropdown to CSV import for localized content
    • Add variant image mapping support for Shop products
    • Add legacy image support for backward-compatible variant mappings
  • Bug Fixes
    • Fix UUID column error for auth tables during upgrade - Users upgrading from PhoenixKit < 1.7.0 no longer get "column uuid does not exist" error when logging in. Added auth tables (users, tokens, roles, role_assignments) to UUIDRepair module.

1.7.29 - 2026-01-26

  • Add primary language improvements and AI translation progress tracking
    • Real-time translation progress - Added progress bars to editor and listing pages showing AI translation status
    • Primary language improvements - Posts now store their primary language for isolation from global setting changes
    • Language handling fixes - Fixed base code to dialect mapping (e.g., en → en-US) across public URLs and editor
    • UI polish - Updated language switcher colors, modal text, and added prominent primary language display in editor
    • Documentation - Added comprehensive README for the Languages module

1.7.28 - 2026-01-24

  • Major improvements to the Publishing module's multi-language workflow: renamed "master" to "primary" terminology, fixed URL routing with locales, added language migration tools, improved cache performance, and fixed several UI/UX issues in settings and admin pages.
    • Multi-Language System Improvements
      • Rename master to primary terminology - Updated all references from "master language" to "primary language" for consistency and clarity
      • Fix language in URL breaking navigation - Resolved issues where locale prefixes in URLs caused routing problems
      • Isolate posts from global primary_language changes - Posts now store their own primary language, preventing drift when global settings change
      • Add "Translate to This Language" button - Quick translation action for non-primary languages in the editor
      • Sort languages in dropdowns - Consistent alphabetical sorting across all language selectors
    • Migration Tools
      • Add version structure migration UI - Visual indicators and migration buttons throughout the publishing module
      • Fix legacy post migration - Resolved "post not found" errors when migrating from legacy to versioned structure
      • Handle dual directory structures - Fixed migration when both publishing/ and blogging/ directories exist
      • Add primary language migration system - Tools to migrate posts to use isolated primary language settings
    • Performance
      • Improve listing performance - Read from cache when possible, reducing database/filesystem hits
      • Language caching with WebSocket transport - Faster language resolution with proper cache invalidation
      • Add Create Group shortcut - Quick access button on publishing overview page
    • Settings & Admin UI Fixes
      • Fix General settings content language glitch - Resolved weird UI behavior when changing content language
      • Fix settings tab highlighting - General and Languages tabs now properly highlight on child pages
      • Fix admin header dropdowns - Theme and language dropdowns in admin header now work correctly
      • Update Entities module description - Clearer description on the Modules page
  • Updated the languages module added front and backend tabs for languages
  • Add localized routes for Shop module
    • Add locale-prefixed routes (/:locale/shop/...) for multi-language Shop module support
    • Add language validation to only allow enabled languages in URLs
    • Add language preview switcher for admin product detail page

1.7.27 - 2026-01-19

  • Changed / Added
    • Added prefix-aware navigation helpers and dynamic URL prefix support across dashboard, tabs, auth pages, and project home URLs, fixing issues when locale or prefix is nil.
    • Introduced comprehensive dashboard branding and theming:
      • Configurable branding, title suffix, and logo handling.
      • Shared theme controller with daisyUI integration, color scheme guide, and improved theme switcher placement.
    • Enhanced dashboard navigation:
      • Configurable subtab styling, redirects, highlights, and mobile subtab support.
      • Multiple context selectors with dependency support.
      • Reserved additional locale path segments for dashboard and users.
    • Added context-aware features:
      • Context-aware badges with update helpers, guards for nil contexts, and improved preservation during tab refresh.
      • Consistent context-aware merge behavior.
    • Improved authentication and user setup:
      • Added fetch_phoenix_kit_current_user to the auto-setup pipeline.
      • Fixed auth pages and titles to use centralized Settings/Config branding.
    • Performance and quality improvements:
      • Optimized Presence and Config modules to reduce repeated checks and lookups.
      • Added dashboard_assigns/1 helper to prevent unnecessary layout re-rendering.
      • Fixed hardcoded branding and paths to rely on configuration fallbacks.
    • Documentation updates:
      • Added guides for dashboard theming, tab path formats, subtab behavior, and context selectors.
      • Added prominent built-in features section and reduced overall documentation size.
  • Maintenance:
    • Fixed Credo/Dialyzer issues, formatting problems, and test failures.
    • Cleaned up unused Dialyzer ignores and added ignores for test support files.

1.7.26 - 2026-01-18

  • Language switcher fix

1.7.25 - 2026-01-16

  • Bug fix - Added check for nil on language_swithcer on log-in page

1.7.24 - 2026-01-15

  • Add Shop module with products, categories, cart, and checkout flow
  • Add user billing profiles for reusable billing information
  • Add payment options selection in checkout (bank transfer, card payment)
  • Add user order pages with UUID-based URLs
  • Add PubSub broadcasts to Billing module for real-time updates
  • Add automatic default currency for orders
  • Add Billing and Shop tabs to user dashboard tab system
  • Add automatic dashboard tabs refresh when modules are enabled/disabled
  • Fix user dashboard layout sidebar height calculation
  • Fix OAuth avatar display in admin navigation

1.7.23 - 2026-01-14

  • Added user functions, language switcher on login page (also support for Estonian and Russian on login)
  • Removed logs spamming about oban jobs

1.7.22 - 2026-01-13

  • Add AWS config module with centralized credential management
  • Add context selector for multi-tenant dashboard navigation
  • Add comprehensive user dashboard tab system with CLI generator
  • Consolidate Publishing module into self-contained structure
  • Publishing Module: Versioning, AI Translation, Per-Language URLs & Real-time Updates
  • Fixed referralcodes to referrals for more universal code

1.7.21 - 2026-01-10

  • Publishing Module: Versioning, AI Translation, Per-Language URLs & Real-time Updates
  • Fixed referralcodes to referrals for more universal code
  • Consolidate OAuth config through Config.UeberAuth abstraction

1.7.20 - 2026-01-09

  • Fix user avatar fallback when Gravatar is unavailable
  • Fixed issues with phx_kit install
  • Add scheduled job cancellation when disabling modules
  • Fix race condition in file controller for parallel requests

1.7.19 - 2026-01-07

We are doing code cleanup and refactoring to move forward with more new modules and more features:

  • Moved referral_codes module to correct location lib/modules and fixed issue with install not working
  • Standardize admin UI styling and add reusable components
  • Move Emails module to lib/modules/emails with PhoenixKit.Modules.Emails namespace
  • Migrate Entities, AI, and Blogging modules to lib/modules/ with PhoenixKit.Modules namespace
  • Updated the javascript usage to not create userspace javascript files
  • Move Sitemap and Billing modules to lib/modules/ with consolidated namespace
  • Move DB and Sync modules to lib/modules/ with PhoenixKit.Modules namespace
  • Moved posts module files to lib/modules folder
  • Add DB Explorer module

1.7.18 - 2026-01-03

  • Blog Versioning, Caching System, and Complete Programmatic API
  • Add Cookie Consent Widget (Legal Module Phase 2)
  • Add Legal module improvements and cookie consent enhancements