Latest Releases

These releases come straight from our repositories. Latest releases may not be published yet.

breezefront / breeze-content-builder

1.2.2 Sep

breezefront / breeze-blank

3.2.2 Sep

breezefront / breeze-enterprise

3.3.0 Aug
  • 3.3.0

    Report (rev.1)

    Package was generated and uploaded to GitHub on with the following dependencies:

    swissup/module-ajaxsearch:1.14.11

    swissup/module-breeze-ai:1.3.0

    swissup/module-breeze-content-builder:1.2.0

    swissup/module-breeze-content-builder-migration:1.0.1

    swissup/module-breeze-layout-builder:1.2.0

    swissup/module-easy-slide:1.8.18

    swissup/module-easycatalogimg:1.5.15

    swissup/module-navigationpro:1.19.3

    swissup/module-testimonials:1.4.2

    swissup/theme-frontend-breeze-enterprise-apollo:3.3.0

    swissup/module-core:1.13.1

    swissup/theme-frontend-breeze-enterprise-blank:3.3.0

    Changelog (autogenerated)

    swissup/module-ajaxsearch — 1.14.11 (was 1.14.10)
    • Version 1.14.11 0db900

    • Fixed layout shift when result list is long 8715cc

    • Fixed overflowed content in Firefox (Apollo theme) de660b

    • Merge pull request #57 from swissup/phpunit12-test-api-56

      Migrate Test/Unit off removed PHPUnit APIs (PHPUnit 12 / PHP 8.5) 095093

    • test: migrate removed test APIs for PHPUnit 12 / PHP 8.5

      The CI runner targets PHP 8.5, which requires PHPUnit 12.x. Migrate the
      unit tests off APIs removed in PHPUnit 10-12:

      - setMethods() (removed v10) -> onlyMethods(); all mocked methods verified
      to exist on the target class via reflection. Dropped dead duplicate mock
      builders (Magento CollectionFactory immediately overwritten by the
      Swissup one).
      - getMockForAbstractClass() (removed v12) -> createMock().
      - will($this->returnValue()/returnSelf()/onConsecutiveCalls()) (removed
      v12) -> willReturn()/willReturnSelf()/willReturnOnConsecutiveCalls().
      - Non-static data providers (hard error v12) -> public static function.

      Also fixed two removed APIs not listed in the issue but caught while
      running the suite:

      - $this->at() (removed v10) in DataTest and ProductTest -> willReturnMap()
      / atLeastOnce() / consecutive-call stubs.

      InitTest was stale independently of PHPUnit: it exercised a removed
      getClassNames() method and the old constructor/getSettings signature.
      Realigned it to the current Init block and dropped the redundant
      getClassNames test (already covered by Helper\DataTest).

      Verified: 41 tests, 99 assertions, 0 errors, 0 deprecations on
      PHPUnit 10.5 / PHP 8.3 (`bin/test`).

      Refs swissup/module-ajaxsearch#56

      Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> 20854c

    • test: add module-local PHPUnit runner

      Add bin/test and phpunit.xml.dist so the unit suite can be run in
      isolation via `bin/test` (runs vendor/bin/phpunit inside the container),
      mirroring the setup already present in module-askit. .gitignore keeps the
      PHPUnit result cache out of the repo.

      Refs swissup/module-ajaxsearch#56

      Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> a4e899

    swissup/module-breeze-ai — 1.3.0 (was 1.0.12)
    • Version 1.3.0 050f99

    • Merge pull request #83 from breezefront/fix/ai-buttons-overlap-name-82

      fix(admin): stop AI buttons overlapping long field values (#82) 3348b6

    • fix(admin): lay AI actions beside field, not over it (#82) 3eb55f

    • fix(admin): stop AI buttons overlapping long field values (#82) 1a4854

    • Merge pull request #81 from breezefront/feat/editor-chat-panel

      feat(content-builder): chat panel in the editor for building and refining a page (#64) 0932fb

    • fix(content-builder): give the message textarea an accessible name b48efb

    • fix(content-builder): ground every request in the live working copy

      pageContext() only supplies the page's title and id, so a first chat
      message had no representation of the page's actual content, and manual
      edits made between AI replies were silently overwritten by the last
      frozen AI document. Every request now seeds or refreshes its document
      turn from contentBuilder.getComponents() instead. Also drops historical
      image attachments from the request — maxBytes is sized for one
      screenshot under post_max_size, and replaying every prior attachment on
      each follow-up can push the aggregate body past that limit. 9ca7e3

    • fix(content-builder): drop a stale AI response after a page switch (#64) 371c91

    • refactor(content-builder): share the chat loop between the modal and the editor panel (#64)

      Extracts turn storage, chat-bubble rendering, and copy-to-clipboard into
      chat-core.js, used by both chat.js's field-targeted modal and
      bcb-chat-panel.js's docked panel. The request/response cycle stays local
      to each caller — they talk to different skills with different payload
      and response shapes, and forcing that through one function would trade
      duplication for a worse, branchier abstraction.

      Fixes two bugs the duplication had introduced in bcb-chat-panel.js: the
      Copy button was never bound (only chat.js's copy of the handler existed)
      and a stale "type a message" error could survive a valid follow-up
      submission. 1ab342

    • fix(content-builder): keep the generated document in follow-up history, guard double-submit a48469

    • feat(content-builder): chat panel in the editor for building and refining a page (#64)

      Docks a conversational panel into the Content Builder editor's tree panel
      via a mixin on bcb-panel.js. Every reply is applied straight into the
      working copy through content-builder.js's applyAiComponents(), and every
      request is sent persist:false so nothing reaches the draft until the
      admin uses the editor's own Save/Publish. 9e66ae

    • feat(content-builder): add non-persisting build endpoint and editor chat config (#64)

      Adds options.persist=false to Controller/Adminhtml/Page/Build so a request
      can return generated components without writing the Content Builder draft.
      Extracts vision-model selection out of BuildFromScreenshot into a shared
      VisionModelProvider, and adds Block/Adminhtml/Editor/AiChatConfig plus its
      layout/template to seed the editor route with the same config. 6eca5e

    • Merge pull request #80 from breezefront/claude/focused-payne-b60751

      fix(content-builder): reproduce every band the screenshot shows (#74) a84559

    • fix(content-builder): word the band warnings for the request that was sent (#74)

      Four defects found reviewing the review fixes.

      The count-mismatch warnings hardcoded "The reference design was read as ...",
      but the mismatch branch runs whether or not a screenshot was attached — the
      prompt asks for a band count on every build, not only on the image path. A
      text-only brief that came back over- or under-segmented therefore told the
      merchant about a reference design they never sent, on the build note and in
      the Content Builder editor. The flag that decides whether to warn now decides
      what the warning calls the thing the page was counted against.

      The band-wrapper warnings borrowed ComponentValidator's "root[N]" label while
      numbering a different list. The validator indexes the raw model output, where
      a dropped node still consumes its index; the audit sees the list after
      validation. One dropped band was enough to put two warnings saying "root[0]"
      in one list, pointing at two different bands. They are numbered as bands now.

      readScreenshot() read and base64-encoded the file before ImageValidator got to
      apply MAX_IMAGE_BYTES, so an oversized design export cost about 2.3x its size
      in memory to reach a verdict available from filesize(). The admin path is
      unaffected: the browser posts base64 and no file is read.

      FiveBandMock::drawProductRow() took $bottom and never used it, so that band
      alone would not follow a change to the band split in draw() — in the one file
      whose docblock claims the band boundaries are readable in source. 685502

    • fix(content-builder): keep the band count when the reply carries prose (#74)

      Three defects found reviewing the band-audit change.

      The prompt says "no prose" and models add it anyway, which is why
      jsonCandidates() exists at all. It tries the widest "[...]" run before
      "{...}", correct while a bare list was the asked-for shape. With an envelope
      whose "unsupported" is omitted and "components" last, that run is exactly the
      components array: it parses, wins, and the band count is thrown away. The
      audit then reported "the AI did not report how many bands it saw" for a reply
      that did report it. A list is now held as a fallback and the remaining
      candidates are still tried for an envelope, so a reply that really is a bare
      list still resolves to the first list.

      spreadStackedSlots() decided a band was stacked from the count of the first
      slot alone. "children" written as a flat list of nodes rather than a list of
      slots lands there with the node itself as the first slot, and its own keys are
      counted as sibling blocks — {"type": ..., "props": ...} counts 2 in a
      two-column band and was shredded across the slots, replacing the accurate
      short-slot warning with a repair that never happened. Every entry now has to
      look like a node.

      render.php ignored the file_put_contents result and printed "(0 bytes)" with
      status 0 on an unwritable path, sending whoever reproduces a measurement to
      look at the build command instead. ee671e

    • fix(content-builder): reproduce every band the screenshot shows (#74)

      Shown a five-band reference design, gpt-4o-mini returned a document whose
      columns bands were fakes: the container had the right type and the right
      column count, but every card sat in its first slot, so the band rendered as
      one column with the rest empty. Measured over ten runs of the committed mock,
      that happened in nine of them.

      Three changes, in order of how much they hold:

      - ComponentValidator spreads a container whose whole band was written into
      its first slot, when the arithmetic leaves no other reading. Prompting
      against this only half worked; the repair is unambiguous.
      - The prompt asks for {"bands", "unsupported", "components"} instead of a
      bare array, spells out the slot shape, and names the columns-versus-
      product_grid choice. A bare array is still accepted.
      - BandAudit compares the band count the model reports to what it emitted and
      reports the difference, plus any band the catalog cannot express. It
      reports and does not retry: a second round trip would double the cost of
      every build to re-roll an answer a small model is as likely to get wrong
      again, and the missing band cannot be synthesised locally.

      Measured with the mock and expected shape now under Test/Fixture, driven
      through the new --screenshot option. Ten runs each, gpt-4o-mini:

      before: 5 bands x5, 6 bands x4, 1 hard failure; 9/9 mis-slotted
      after: 5 bands x10, 0 mis-slotted, 8/10 with both columns containers

      gpt-4o gets the slots right with either prompt, so the mis-slotting was
      model capacity and the prompt closes the gap on the small model. 0e5f35

    • Merge pull request #79 from breezefront/fix/build-note-page-id-type

      fix(content-builder): match cms_page.page_id column type in the build note table (#72) 7faf89

    • fix(content-builder): match cms_page.page_id column type in the build note table (#72) 7f6aaa

    • Merge pull request #78 from breezefront/claude/priceless-turing-a344ac

      fix(config): offer model names the providers still serve (#73) 21eee6

    • Merge pull request #77 from breezefront/claude/jovial-chatelet-3447d7

      feat(content-builder): report what the validator repaired (#72) d23f45

    • feat(content-builder): report what the validator repaired (#72) 4b47e4

    • fix(content-builder): drop a corrupt build note instead of reporting a clean build (#72) e3994a

    • fix(gemini): redact api key from error, notice for retired models (#73) 922d51

    • fix(config): offer model names the providers still serve (#73) b1963c

    • Merge pull request #76 from breezefront/claude/lucid-kepler-a7ce73

      fix(content-builder): keep authoring placeholders out of generated copy (#71) 6a5b87

    • fix(content-builder): keep required sample copy on the malformed-value path (#71)

      fallbackValue() blanked any authoring placeholder, so a required copy prop the
      model sent as an array — quote.text, in the fixture — came out empty, which is
      the broken component the omitted-prop path deliberately avoids. The rule now
      lives in one predicate used by both paths.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 8a6461

    • fix(content-builder): keep authoring placeholders out of generated copy (#71)

      The catalog's "default" attribute does two jobs: the value a renderer needs
      when a prop is unset, and the sample text the editor drops into a fresh
      component. The validator filled both, so "Discover Something New" shipped as
      the heading of three FAQ answers. Copy-typed props whose default reads as
      prose are now stored blank — blank, not absent, because the templates read
      `$props['heading'] ?? '<sample>'` — and the prompt marks them VISIBLE-TEXT
      instead of quoting the sample text that taught the model to echo it.

      A list-valued prop is blanked as an empty list, so an unusable faq_items no
      longer falls back to the two sample questions. Tabs keeps its declared titles:
      the container counts its slots from them.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 00e837

    • Merge pull request #75 from breezefront/claude/amazing-lichterman-a86181

      fix(content-builder): keep the list props the model wrote (#70) 819b38

    • fix(content-builder): keep every tab title paired with its slot (#70)

      Dropping an unusable title compacted the list while normalizeChildren()
      truncates slots from the end, so ["Shipping", "", "Returns"] over three
      populated slots rendered "Returns" above the middle slot's content and threw
      the last one away.

      A list of strings is positional — its container pairs slot N with entry N —
      so an unusable entry is now replaced where it stands, taking the declared
      default's title for that position ("Tab 2"), which is what the container's
      renderer pads a short title list with anyway. Entries that carry their own
      content, like the FAQ items, have no slot to stay paired with and are still
      dropped.

      An empty list now says so rather than reporting that none of its zero
      entries was usable. efb774

    • fix(content-builder): read a keyed object as one list entry (#70)

      decodeList() accepted any array, so a keyed object counted as a list: an FAQ
      item the model sent unwrapped — {"q": ..., "a": ...} instead of [{...}] —
      was iterated field by field, turning the question and the answer into two
      bogus items, each with the other's text as its question.

      Only a real list is a list now. A keyed object carrying at least one declared
      field is wrapped as the single entry it is, so the question the model wrote
      survives; one carrying none falls back to the default with a warning naming
      the fields that were expected. 71d179

    • fix(content-builder): keep the list props the model wrote (#70)

      The catalog asks the model for the Tabs titles and the FAQ items as JSON
      arrays, then ComponentValidator coerced every non-scalar prop back to its
      default — so a generated FAQ arrived with its questions replaced by "Tab 1"
      / "Tab 2" / "Tab 3", and the Tabs slot count fell back to the default three,
      dropping any further answer with it.

      A prop whose declared default is a JSON array is now validated as a list
      instead of discarded: entries are cleaned one by one and written back in the
      JSON-string shape the editor stores and the renderers decode, so the slot
      count follows the titles the model actually wrote. The default is only
      reached when nothing usable is left, and the warning then names the reason.

      Recognised by the shape of the declared default rather than by prop name, so
      the FAQ accordion's question/answer items are covered by the same path. a4765b

    • Merge pull request #69 from breezefront/claude/stoic-yonath-58797a

      fix(content-builder): drop fake "spacing" prop from catalog prompt 7db871

    • fix(content-builder): drop fake "spacing" prop from catalog prompt

      The margin/padding summary line started with the group label instead of
      prop names, so the model read "spacing" as a prop and emitted it on every
      component that had one. Render one line per value range, leading with the
      real names, which also stops claiming -200 as the minimum for paddings.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> beb335

    • chore: drop stray .b64 scratch files from screenshots dir

      Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> e5e228

    • docs: add screenshots to test guide, correct scenarios after live testing

      Vision-model dropdown pre-filters server-side (10.6 unreachable via UI as
      written), Remove+re-attach confirmed clean (10.9). Found and filed a real
      draft-persistence bug in module-breeze-content-builder (#31) — re-run builds
      report success but don't survive a fresh page reload.

      Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> 8062ec

    • docs: add test scenarios and how-to-test guide for Content Builder screenshot import

      Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> 8e6e8d

    • Merge pull request #67 from breezefront/feat/value-backup

      feat(backup): record values before AI writes 70f7cc

    • fix(backup): skip ambiguous staging rows 27e298

    • fix(backup): guard capture reads, batch pruning 19f666

    • feat(backup): record values before AI writes 21d8f6

    • Merge pull request #66 from breezefront/test/bulk-selection-regression

      test(bulk): pin the scope a run resolves to b69352

    • test(bulk): seed the products the scope cases run against

      Copilot was right on both counts: the cases borrowed whatever the
      catalogue happened to hold, and the exclusion case skipped itself whenever
      no two SKUs shared a five-character prefix. A regression check that goes
      quiet when the surrounding data does not suit it is not a guard.

      Three products with a prefix unique to the run are created in setUp and
      removed in tearDown, so every case is deterministic and none can skip. The
      prefix also gets its own case, since the two filtered cases rest on it
      selecting this test's products and nothing else.

      Reverting BulkEntityIdResolver to the pre-33d5e97 behaviour now turns five
      of the six red, up from four. The unfiltered select-all stays green, which
      is correct - that path is the legitimate one and behaved the same before.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> e82c74

    • test(bulk): pin the scope a run resolves to

      The selection fault has landed three times. 15c1e7c introduced an
      all_selected flag driven by allSelected(), #31 closed it in June, and
      c13907e swapped in excludeMode() and brought it straight back - Magento
      raises either flag by itself once the checked rows cover the rows the grid
      is showing. Both attempts only ever argued about the client-side flag,
      while the resolver went on loading the collection with no filters at all.

      The E2E spec covers the request the wizard posts. Nothing covered what
      that request then resolves to, which is the number that decides how many
      products get rewritten. These cases do, against the live catalogue: an
      explicit selection, a genuine select-all, a select-all narrowed by a
      filter, exclusions inside a filtered set, and an empty selection.

      Reverting BulkEntityIdResolver to the pre-33d5e97 behaviour turns four of
      the five red, the exception being the unfiltered select-all - which is
      correct, since that case is the legitimate path and behaved the same
      before.

      Refs #61

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 8e8b71

    • Version 1.2.0 634a46

    • Merge pull request #63 from breezefront/feat/build-page-from-screenshot

      Build a Content Builder page from a screenshot 680ede

    • fix(vision): close the second round of review points on #63

      The one that mattered: `options['brief']` skipped the fix from the previous
      commit entirely. It leaves the history untouched by design, so an
      alternating conversation that ended on a user turn got our prompt appended
      after it — the exact pair Anthropic answers 400 to, reachable from REST
      where the caller supplies both a brief and a history. The prompt is now
      folded into a trailing user turn instead of following it, which keeps that
      turn's words rather than dropping them. The class docblock claimed "a
      history that already alternates keeps alternating"; it does now.

      `testSendsTheHistoryFollowedByTheGeneratedPrompt` was pinning the broken
      shape — two user turns, asserted as correct — so its history now ends on
      an assistant turn and the folding case has tests of its own.

      The block hid itself for every reason a build could fail except the two
      the controller actually enforces. A role holding `Magento_Cms::page` but
      not `Swissup_BreezeAi::index` or `Magento_Cms::save` saw a working-looking
      button whose every click came back as the login page, which the modal can
      only report as an expired session.

      Also from the review: the modal kept the previous brief when reopened, so
      it would have ridden the next screenshot unseen; an in-flight FileReader
      could re-arm a screenshot the admin had already removed, now invalidated
      by a token that `clearScreenshot()` moves on; the drop zone gained
      `role="button"` and the brief and model controls gained real labels, since
      a placeholder is not one and the select had no accessible name at all.

      A warning-free build no longer redirects on its own. The draft is saved by
      then but the CMS form behind the modal is not — this request never touched
      it — so a title or URL key typed before opening the modal was silently
      lost. The editor is offered as a link in both outcomes.

      635 unit, 18 integration, 30 e2e green.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 3072d9

    • fix(vision): answer the three points Copilot raised on #63

      The docs described a cap the code stopped using. `BuildFromScreenshot`
      reads `post_max_size`, and its own comment explains why `getMaxFileSize()`
      is the wrong ceiling for a base64 string in a text field — the doc still
      named `getMaxFileSize()`, which is exactly the sort of mismatch someone
      debugging a 413 would be misled by.

      The drop zone carries `tabindex="0"` but only answered to a click, so it
      could be tabbed to and not opened. Enter and Space now trigger the picker,
      with the same guard the click handler needs: the remove control sits
      inside the zone and answers to both keys, and swallowing them there would
      leave it dead to the keyboard.

      `getVisionModels()` is asked for four times while one CMS page form
      renders — `canShow`, `getConfigJson`, and `getSelectedModelId` from inside
      it — and each ask loads every model to read its metadata. Memoized for the
      life of the block, which is one request.

      631 tests green; 29 e2e, the new keyboard case among them.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 2e6454

    • test(e2e): assert the selection contract the wizard actually posts

      33d5e97 replaced the all_selected flag with Magento's own mass-action
      contract — one of selected or excluded, plus the grid namespace and the
      filters that were on screen — and updated SaveTest, but the Playwright spec
      kept asserting the flag that no longer exists. Red on master since then.

      The assertion now names what the fix was for: the filters travel with the
      selection, which is what stops an exclude-mode Select All from widening past
      the rows the grid was showing. f34845

    • feat(vision): build a Content Builder page from a screenshot

      PR #55 landed the whole image path — MessageInterface::getImages(),
      ImageBuilder, ImageValidator, VisionSupport, per-provider encoding — and
      nothing used it. Every caller of build_page passed an empty history, and no
      UI produced an images list. This gives the admin a way in.

      A button in the Content Builder fieldset of the CMS page form opens a modal:
      attach a screenshot, optionally type a brief, pick a vision-capable model.
      The file is read with FileReader and posted as a data URI inside
      history[0].images — nothing is written to disk. Controller/Adminhtml/Page/Build
      runs the skill, saves the result through DraftWriter, and answers with the URL
      of the editor the admin reviews it in.

      Synchronous rather than an option on the bulk wizard: a reference screenshot
      is a per-page input, and one screenshot over N pages produces N near-identical
      layouts. GenerateService, ImageBuilder, ImageValidator, AiRequest, Message and
      all three providers are used exactly as PR #55 built them, unchanged.

      BuildPage now lifts images off the user turns they arrived on and puts them on
      the prompt turn it builds, consuming the last user turn as the brief. That
      fixes an existing fault: Model\Provider\Claude passes the message list through
      unmerged and Anthropic refuses two user turns in a row, so the old
      array_merge($history, [prompt]) answered 400 whenever the lastUserMessage()
      fallback was the thing being used.

      PromptBuilder gains a conditional Reference design block. Two of its five
      rules carry the weight: without the band-per-row line the model answers with a
      flat component list and the row-wrapping rule loses to the picture, and
      without the ignore-brand-and-colours line it copies the screenshot's hex
      values into every colour prop and the draft clashes with the merchant's theme.

      Verified end to end against gpt-4o-mini on a real storefront screenshot:
      six row-wrapped bands, no validator repairs, draft readable in the editor.
      With a brief added, the structure holds and the copy follows the brief rather
      than transcribing the screenshot.

      540 -> 621 unit tests, all green. 9d8854

    • test(mcp): list build_page among the tools the server exposes

      McpTest asserts a hardcoded tool list written when the MCP endpoint shipped
      with four tools. build_page joined the skill pool in 1bf01f6, and every skill
      in the pool is exposed as a tool, so the integration suite has been red since
      then. Nothing about the server changed — only what the test expects. f8588c

    • test(e2e): cover the build-from-screenshot modal

      The one surface unit tests cannot reach. It exists because of a bug that got
      all the way to review: the file input is a child of the drop zone, so the
      synthetic click jQuery sends it bubbles back into the zone's own handler and
      recurses until the stack blows — the picker never opens and only drag and drop
      works. "clicking the drop zone opens the file picker" is the guard for that.

      Also covers the pre-flight refusal of a non-image, that the refusal sends no
      request, the preview and remove cycle, and that a reopened modal carries none
      of the previous run's state.

      Stops short of pressing Build draft: that would spend a real provider call.
      Skips itself where the control legitimately hides — no Content Builder, or no
      configured model that reads images. 10934b

    • test(mcp): list build_page among the tools the server exposes

      McpTest asserts a hardcoded tool list written when the MCP endpoint shipped
      with four tools. build_page joined the skill pool in 1bf01f6, and every skill
      in the pool is exposed as a tool, so the integration suite has been red since
      then. Nothing about the server changed — only what the test expects. 60d8c7

    • feat(vision): build a Content Builder page from a screenshot

      PR #55 landed the whole image path — MessageInterface::getImages(),
      ImageBuilder, ImageValidator, VisionSupport, per-provider encoding — and
      nothing used it. Every caller of build_page passed an empty history, and no
      UI produced an images list. This gives the admin a way in.

      A button in the Content Builder fieldset of the CMS page form opens a modal:
      attach a screenshot, optionally type a brief, pick a vision-capable model.
      The file is read with FileReader and posted as a data URI inside
      history[0].images — nothing is written to disk. Controller/Adminhtml/Page/Build
      runs the skill, saves the result through DraftWriter, and answers with the URL
      of the editor the admin reviews it in.

      Synchronous rather than an option on the bulk wizard: a reference screenshot
      is a per-page input, and one screenshot over N pages produces N near-identical
      layouts. GenerateService, ImageBuilder, ImageValidator, AiRequest, Message and
      all three providers are used exactly as PR #55 built them, unchanged.

      BuildPage now lifts images off the user turns they arrived on and puts them on
      the prompt turn it builds, consuming the last user turn as the brief. That
      fixes an existing fault: Model\Provider\Claude passes the message list through
      unmerged and Anthropic refuses two user turns in a row, so the old
      array_merge($history, [prompt]) answered 400 whenever the lastUserMessage()
      fallback was the thing being used.

      PromptBuilder gains a conditional Reference design block. Two of its five
      rules carry the weight: without the band-per-row line the model answers with a
      flat component list and the row-wrapping rule loses to the picture, and
      without the ignore-brand-and-colours line it copies the screenshot's hex
      values into every colour prop and the draft clashes with the merchant's theme.

      Verified end to end against gpt-4o-mini on a real storefront screenshot:
      six row-wrapped bands, no validator repairs, draft readable in the editor.
      With a brief added, the structure holds and the copy follows the brief rather
      than transcribing the screenshot.

      540 -> 621 unit tests, all green. c53c71

    • Version 1.1.2

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 3bc1cd

    • Merge pull request #60 from breezefront/fix/bulk-selection-scope

      fix(bulk): scope a run to what the grid was actually showing 49b5f5

    • fix(bulk): scope a run to what the grid was actually showing

      A client selected one product and the wizard queued a run over all 714.
      The submit path decided "everything is selected" from the grid
      excludeMode flag, and core turns that flag on by itself as soon as the
      checked rows cover the rows the grid currently holds - filtering down to
      a single product and ticking it is indistinguishable from pressing Select
      All. The server then made it worse: the all_selected branch loaded the
      entity collection with no filters at all, so even a deliberate select-all
      meant the whole catalogue rather than the filtered set. Either fault
      alone produces the wrong count.

      Both go away by speaking Magento's own mass-action contract. The wizard
      now posts selected or excluded alongside the grid namespace, filters and
      search, exactly as Magento_Ui/js/grid/massactions does, and
      BulkEntityIdResolver runs the collection through MassAction\Filter. An
      exclude-mode selection resolves against the filters that were on screen,
      so the accidental flag can no longer widen the scope.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 33d5e9

    • Version 1.1.1

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> f29385

    • Merge pull request #59 from breezefront/fix/model-form-provider-options

      fix(admin): serve the model form's options from the option sources 72aff2

    • fix(admin): serve the model form's options from the option sources

      The model select is repopulated in the browser whenever the provider
      changes, and the map it read from was a hand-maintained copy inside
      swissup_breezeai_model_form.xml. Adding GPT-5.6 updated OpenAiModel and
      the switcher rules but not that copy, so Sol, Terra and Luna could not be
      picked at all - and the run wizard's gpt-5.6-terra default could never
      match a saved row.

      ProviderModels builds the map from the same option sources the form
      already declares, injected through FormDataProvider::getMeta(). One list
      per provider, and registering a provider in di.xml is enough for its
      models to appear.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 936696

    • Merge pull request #58 from breezefront/fix/temperature-zero

      Stop dropping a temperature of 0 d79542

    • fix(providers): stop dropping a temperature of 0

      Claude and Gemini read the value with an assignment inside the condition:

      if ($temperature = $config->getExtra('temperature')) {

      which is a truthiness test, and `(bool) 0` is false. An admin setting 0 — the
      value you pick precisely to make output deterministic — had it silently discarded
      and got the provider's own default instead, 1.0 on Claude. No error, no warning,
      just different output than asked for.

      Compared against null and '' now, so 0 reaches the request and an unset field
      still sends nothing. OpenAI was already correct via `?? DEFAULT_TEMPERATURE`.

      Found by exercising the admin form rather than by reading: the field only became
      settable when the extra_config registry landed, and both existing provider tests
      covered temperature at 0.5 and 0.7, never at 0. Tests now cover 0 and unset for
      both providers, and fail on the old code.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> da9be6

    • Merge pull request #57 from breezefront/refactor/extra-config-registry

      One registry for the extra_config fields 40cddc

    • Merge pull request #55 from breezefront/feat/vision-image-input

      Vision foundation: let a message carry images, encode them per provider fd09a0

    • refactor(config): one registry for the extra_config fields

      Which fields live in the extra_config JSON was written down three times: in the
      save controller, in the form data provider, and in the form XML. Nothing failed
      when they disagreed — a field missing from the save list is silently not stored,
      one missing from the read list comes back blank on every edit. Adding `vision`
      meant three coordinated edits with no test that would catch a missed one.

      `temperature` is what that cost. All three providers read it; neither list
      carried it and the form had no field, so it could not be set from the admin at
      all. It has a field now, and the notice says what the ranges are and that GPT-5
      and the o-series ignore it.

      Model/ModelConfig/ExtraFields owns the keys and how each normalises. Types
      matter here: a blank submission clears the key, an int at zero clears it — no
      token cap means anything at zero — but a float at zero is kept, because zero is
      a meaningful temperature and dropping it would silently restore the provider
      default for someone asking for deterministic output.

      The form XML still declares its own fields: a field needs a label, a notice and a
      control that no list can supply. What moved is which keys travel in and out.

      Needs setup:di:compile — both consumers gained a constructor argument.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> d1dc4b

    • refactor(vision): cut ImageBuilder to the shape callers send

      Five accepted spellings of the same input for one call site: fromBinary had no
      caller at all, and fromArray took `data_uri` beside `url`, `mimeType` beside
      `mime_type`, and a data URI hidden in `data`. Each was a guess about a caller
      that does not exist — no skill attaches an image yet, and no JS sends one.

      What is left is what GenerateService passes: `{mime_type, data}`, or a data URI
      under `url` — the key OpenAI's own image_url part uses, so it is the one a caller
      already has a name for. fromDataUri and the base64 check became private; the
      public surface is fromArray, which is the only method anything calls.

      94 lines to 90, and 4 public methods to 1. Adding a shape back is cheap once a
      caller wants it; carrying four unused ones is not.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 66ac12

    • fix(vision): address the review findings on this PR

      VisionSupport keeps only the `vision` config key and reads ModelMetadata for
      what the name means, so its two prefix tables and its copy of matchesPrefix are
      gone — 116 lines to 70. The o-series can no longer be listed here as text-only
      while being listed there as reasoning.

      ImageValidator collects every image, checks the count, and only then decodes.
      The cap was the last rule tested, so an over-cap request paid for N base64
      decodes to be refused, and was refused for whichever image happened to be
      malformed first rather than for being over cap.

      ImageBuilder accepts and discards parameters between the mime type and
      `;base64`. A `charset` an exporter felt like adding says nothing about image
      bytes, and refusing the URI over it refused a readable image.

      GenerateService::toMessage() no longer claims Web API hands history over as
      arrays. It does not: /V1/breezeai/generate declares MessageInterface[] and
      ServiceInputProcessor dies with "Cannot instantiate interface" before the service
      runs, because the interface has no DI preference. The caller that really passes
      arrays is the admin controller, which decodes the body itself. History over REST
      predates image input and is not fixed here; docs/DEVELOPMENT.md says so instead
      of implying the route works.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> dd6371

    • feat(providers): let a message carry images, and encode them per provider

      A message gains getImages(): ImageInterface[] alongside its string content,
      and each provider encodes those images into its own wire format — an
      Anthropic base64 image block, an OpenAI image_url part holding a data URI,
      a Gemini inline_data part. Content stays a string, so a text-only turn
      produces the same request body it did before and every existing caller of
      `new Message($role, $content)` keeps working.

      Two things guard the send. ImageValidator enforces the limits the three
      providers share — JPEG/PNG/GIF/WebP, 5 MB decoded per image, 10 per
      request, user turns only — and VisionSupport answers whether the model a
      call is routed to reads images at all, from the model name plus a `vision`
      override on the model row (the new Image Input field). AiRequest runs both
      before reaching the provider, so an image sent to a text-only model fails
      with one sentence naming the model rather than a provider 400. Callers that
      want to know in advance ask AiRequest::supportsVision().

      ImageBuilder turns the shapes an image arrives in — raw bytes, a data URI,
      a decoded request payload — into an ImageInterface, and GenerateService
      uses it so history posted over the wire can carry images too.

      No skill attaches an image yet; this is the layer under that.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> ad9747

    • fix(data): document every Message constructor parameter

      Magento reads @param tags positionally — TypeProcessor::getParamDocBlockTag()
      indexes the tag list with the parameter's own offset — so a block documenting
      only $images typed $role from the images tag and had no tag to return for
      position 2. Reflecting the constructor to build a Message from request data
      died there with a TypeError before any of our own validation ran.

      The two missing tags are the whole fix. A test pins the invariant, since the
      next parameter added to this constructor would silently reintroduce it.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> b793a0

    • Merge pull request #56 from breezefront/refactor/model-metadata

      One table for what a model name means eb79b5

    • fix(model): correct the model facts the merge exposed

      Putting both axes in one table made three disagreements visible:

      o1 reads images. It was absent from the image-input list while present in the
      parameter-set one, which is the drift this table exists to stop. o1-mini and
      o1-preview genuinely do not, so they say so as longer prefixes.

      gpt-4-vision-preview — where vision on GPT-4 started — matched nothing and read
      as text-only, because the list carried gpt-4-turbo and gpt-4o but not the model
      named for the capability.

      claude-4 was dead weight: Claude 4 and later put the tier first, as in
      claude-opus-4-5, and those already match claude-opus.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> f113ee

    • refactor(model): read model facts from one table

      RequestBody carried three prefix lists and VisionSupport (in #55) carried two
      more, each with its own copy of matchesPrefix. Nothing tied them together, so
      they drifted: the parameter-set list knew about o1 and the image-input list did
      not, and no test could catch it because neither list knew the other existed.

      ModelMetadata holds the facts once, keyed by provider and name prefix. Every
      prefix a name starts with contributes, shortest first, so a longer prefix
      overrides a shorter one and states only what differs — o3-mini says it takes no
      image and inherits its parameter set from o3. That ordering is what removes the
      separate lists of exceptions each consumer used to keep beside its main list.

      A per-row sampling or vision value stays with the consumer that reads it: the
      row overriding the name is config, not a fact about the model.

      Behaviour is unchanged — every existing RequestBody test passes untouched, and
      the table reproduces the old lists name for name, o1's missing image support
      included. Correcting that is the next commit, kept separate so this one reads
      as the move it is.

      supportsVision() has no caller on master yet; #55 is the caller and rebases
      onto this.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 891642

    • feat(providers): let a message carry images, and encode them per provider

      A message gains getImages(): ImageInterface[] alongside its string content,
      and each provider encodes those images into its own wire format — an
      Anthropic base64 image block, an OpenAI image_url part holding a data URI,
      a Gemini inline_data part. Content stays a string, so a text-only turn
      produces the same request body it did before and every existing caller of
      `new Message($role, $content)` keeps working.

      Two things guard the send. ImageValidator enforces the limits the three
      providers share — JPEG/PNG/GIF/WebP, 5 MB decoded per image, 10 per
      request, user turns only — and VisionSupport answers whether the model a
      call is routed to reads images at all, from the model name plus a `vision`
      override on the model row (the new Image Input field). AiRequest runs both
      before reaching the provider, so an image sent to a text-only model fails
      with one sentence naming the model rather than a provider 400. Callers that
      want to know in advance ask AiRequest::supportsVision().

      ImageBuilder turns the shapes an image arrives in — raw bytes, a data URI,
      a decoded request payload — into an ImageInterface, and GenerateService
      uses it so history posted over the wire can carry images too.

      No skill attaches an image yet; this is the layer under that.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 659e74

    • Version 1.1.0 9aeb4e

    • Merge pull request #54 from breezefront/feat/bulk-run-model-and-system-prompt

      feat(bulk): pick the model and the framing per run, and pin them to it cab8ed

    • fix(bulk): keep the closing script tag out of a JS comment

      The comment warning about a prompt body holding a closing script tag
      contained one, which ended the block early and dumped the rest of the
      script onto the page as text.
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> d1f118

    • feat(bulk): pick the model and the framing per run, and pin them to it

      The bulk wizard ran on whatever the configuration said at the moment each
      operation was picked up, with no way to steer the model or tell it what
      kind of writing was wanted. The client rated the German output 4/10 and
      asked for both: a model per run, and a free-text instruction that reaches
      the model as framing rather than as part of the product text.

      Two things had to be true for that to mean anything.

      The framing has to arrive in the system channel, ahead of the content.
      It does: the skills put it in a system message, and the providers already
      agree that a caller's system message wins over their built-in default.
      Where a run brief is present the translate prompt drops its own "you are
      a professional e-commerce translator" opening — that line answers the one
      question the brief exists to answer — while keeping the mechanical rules
      about numbers, SKUs and HTML, which are output format rather than
      persona.

      And both have to hold still. A batch of a few thousand products drains
      over hours, so resolving configuration per operation means editing the
      default model halfway through silently splits the batch across two
      models with nothing in the result to say which got which. The model and
      the prompt are now decided when the run is scheduled and travel in the
      operation payload, which is already immutable once written — no queue
      schema change. Even leaving the dropdown alone pins a concrete row:
      "the default model" is a moving target.

      The model ID is a pointer and the row behind it stays editable, so the
      provider and model name ride along as a snapshot and the consumer stops
      rather than bill a model nobody chose. The API key is deliberately not
      snapshotted — rotating one has to reach the runs already queued.

      For Generate this reverses an existing precedence: a model passed in now
      beats the one bound to the prompt. Step 5 tells the operator which model
      the run will use, and a per-prompt model overriding that would make the
      screen lie. Nothing outside the wizard passes one, so the prompt's own
      model still decides everywhere else.

      Two small stores back the UI. Prompts gain a type, so a run brief can be
      saved in the existing library without turning up in the per-field
      dropdowns; rows written before the column are field prompts, which is all
      the module could store then. And one row per admin remembers what the
      wizard was last run with, so it opens on that instead of an empty form —
      a preference, not a reference, which is why it carries no foreign key to
      the model table: a model since deleted is simply not offered.

      Operations queued before any of this carry none of the new keys and keep
      running on the default model.

      setup:upgrade applies the schema; Claude, Gemini and the bulk consumer
      take new constructor arguments, so setup:di:compile is needed as well.

      ./bin/test unit — OK (454 tests, 901 assertions)
      phpcs --standard=Magento2 — 0 errors on every touched file
      Schema verified against a real database, not only declared.
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> fcb9d8

    • fix(providers): agree on what a caller's system message means (#53)

      * fix(providers): agree on what a caller's system message means

      The three providers disagreed. OpenAI lets a caller-supplied system message
      replace the built-in framing; Claude and Gemini filtered it out of the
      conversation and then sent their own constant anyway, so the instruction was
      read, dropped, and never reached the model. The same prompt therefore meant
      different things depending on which model it happened to be assigned to — and
      silently, because nothing errors when a system message is discarded.

      Nothing sends one today, so this fixes no live failure. It removes a trap: the
      first skill that reaches for its own framing would have got it on OpenAI and
      lost it on the other two, and the symptom is bad output rather than an error.

      `Model\Provider\SystemMessage` now resolves the framing for all three — the
      caller's, or the built-in default when the caller supplied none. Where it goes
      still differs per API: inline in `messages` for OpenAI, the top-level `system`
      field for Claude, `system_instruction` for Gemini. More than one system message
      is joined in order rather than reduced to the first, since Claude and Gemini
      have a single field for it and dropping the rest would lose instructions.

      The default string lived in three copies; `RequestBody::SYSTEM_MESSAGE` now
      points at the shared constant so it keeps working for anything referencing it.

      Claude and Gemini take a new constructor argument, so this needs
      `setup:di:compile`.

      ./bin/test unit — OK (409 tests, 810 assertions)
      phpcs --standard=Magento2 — 0 errors on all touched files

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

      * fix(openai): resolve the framing through the shared rule, not a copy of it

      The previous commit gave Claude and Gemini a shared SystemMessage but left
      OpenAI deciding for itself in buildMessages(). Only the default string was
      shared, so the two paths could still drift: several system messages were
      joined on Claude and Gemini and left scattered through the conversation on
      OpenAI, and any later change to resolve() would have reached two providers
      out of three.

      OpenAI now calls resolve() as well. The framing still travels as the first
      entry of `messages` because that is what the endpoint takes; the caller's
      system messages are lifted out of the conversation and folded into it, so
      the instruction is sent once rather than twice.

      resolve() also stops treating a blank system message as one supplied. An
      empty string is not framing, and passing it through meant Gemini receiving
      an empty `system_instruction` part — which it refuses — where before the
      constant always went out.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

      ---------

      Co-authored-by: Claude Opus 5 <noreply@anthropic.com> 2c716b

    • fix(openai): resolve the framing through the shared rule, not a copy of it

      The previous commit gave Claude and Gemini a shared SystemMessage but left
      OpenAI deciding for itself in buildMessages(). Only the default string was
      shared, so the two paths could still drift: several system messages were
      joined on Claude and Gemini and left scattered through the conversation on
      OpenAI, and any later change to resolve() would have reached two providers
      out of three.

      OpenAI now calls resolve() as well. The framing still travels as the first
      entry of `messages` because that is what the endpoint takes; the caller's
      system messages are lifted out of the conversation and folded into it, so
      the instruction is sent once rather than twice.

      resolve() also stops treating a blank system message as one supplied. An
      empty string is not framing, and passing it through meant Gemini receiving
      an empty `system_instruction` part — which it refuses — where before the
      constant always went out.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 7ddc5f

    • fix(providers): agree on what a caller's system message means

      The three providers disagreed. OpenAI lets a caller-supplied system message
      replace the built-in framing; Claude and Gemini filtered it out of the
      conversation and then sent their own constant anyway, so the instruction was
      read, dropped, and never reached the model. The same prompt therefore meant
      different things depending on which model it happened to be assigned to — and
      silently, because nothing errors when a system message is discarded.

      Nothing sends one today, so this fixes no live failure. It removes a trap: the
      first skill that reaches for its own framing would have got it on OpenAI and
      lost it on the other two, and the symptom is bad output rather than an error.

      `Model\Provider\SystemMessage` now resolves the framing for all three — the
      caller's, or the built-in default when the caller supplied none. Where it goes
      still differs per API: inline in `messages` for OpenAI, the top-level `system`
      field for Claude, `system_instruction` for Gemini. More than one system message
      is joined in order rather than reduced to the first, since Claude and Gemini
      have a single field for it and dropping the rest would lose instructions.

      The default string lived in three copies; `RequestBody::SYSTEM_MESSAGE` now
      points at the shared constant so it keeps working for anything referencing it.

      Claude and Gemini take a new constructor argument, so this needs
      `setup:di:compile`.

      ./bin/test unit — OK (409 tests, 810 assertions)
      phpcs --standard=Magento2 — 0 errors on all touched files

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 435eb3

    • fix(openai): send the parameter set the model family accepts (#52)

      * fix(openai): send the parameter set the model family accepts

      Every request carried temperature, frequency_penalty and presence_penalty,
      and max_tokens when configured. That is the GPT-4 parameter set. The GPT-5
      family and the o-series refuse all four on the same endpoint: the two
      penalties are rejected outright even at 0, temperature takes nothing but
      its default, and the token cap was renamed to max_completion_tokens. A
      request built the old way comes back 400, so those models did not degrade
      on this provider — they failed every call.

      Nobody hit it because the model dropdown stopped at gpt-4o-mini and
      reaching a newer one meant typing it into the custom field. The client on
      the translation project asked for gpt-5.6-terra, which is where this
      starts to matter.

      The body is now built by a small class of its own. The provider builds its
      HTTP client inline and cannot be exercised without a network, which left
      the part that has to be right per model family with no test at all; that
      part is now pure and covered.

      Family is decided by the model name, with a sampling extra-config key to
      override it: an Azure deployment or a proxy can serve GPT-5 under a name
      that says nothing, and prefix matching would send it what it refuses.
      reasoning_effort and verbosity are passed through where they are
      understood, so the effort knob is reachable without another release.

      A caller-supplied system message now replaces the built-in one instead of
      being appended after it — two system messages in one request left the
      model to pick. No caller sends one yet.

      The three GPT-5.6 tiers are in the model dropdown, so the client's model
      is a selection rather than a hand-typed string.

      phpcs --standard=Magento2 reports 0 errors on the touched files.
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

      * fix(openai): gate the reasoning knobs per family and expose them in admin

      The first pass treated GPT-5 and the o-series as one family. They are not:
      `verbosity` shipped with GPT-5 and the o-series rejects it, and `o1-mini`
      rejects `reasoning_effort` as well. Sending either to a model that refuses it
      reproduces the 400 this branch set out to remove, so the knobs are now offered
      per family while the token-cap rename and the dropped sampling parameters stay
      common to both.

      The knobs themselves had no way in. `sampling`, `reasoning_effort` and
      `verbosity` were readable only by hand-editing the extra_config JSON, which
      made the escape hatch for unnamed Azure and proxy deployments unreachable in
      practice. All three are now fields on the model form, saved into and read back
      out of extra_config alongside max_tokens.

      Also:

      - honour a configured `temperature` on the GPT-4 path instead of pinning 1.0,
      matching what the Claude and Gemini providers already do
      - read `sampling` without casting, so a non-string value in the JSON cannot
      raise an array-to-string conversion
      - note in the class docblock that an unrecognised model name falls back to the
      GPT-4 parameter set, so the next family will need its prefix added here

      ./bin/test unit — OK (401 tests, 802 assertions)
      phpcs --standard=Magento2 — 0 errors on all touched files

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

      ---------

      Co-authored-by: Claude Opus 5 <noreply@anthropic.com> 37217f

    • fix(openai): gate the reasoning knobs per family and expose them in admin

      The first pass treated GPT-5 and the o-series as one family. They are not:
      `verbosity` shipped with GPT-5 and the o-series rejects it, and `o1-mini`
      rejects `reasoning_effort` as well. Sending either to a model that refuses it
      reproduces the 400 this branch set out to remove, so the knobs are now offered
      per family while the token-cap rename and the dropped sampling parameters stay
      common to both.

      The knobs themselves had no way in. `sampling`, `reasoning_effort` and
      `verbosity` were readable only by hand-editing the extra_config JSON, which
      made the escape hatch for unnamed Azure and proxy deployments unreachable in
      practice. All three are now fields on the model form, saved into and read back
      out of extra_config alongside max_tokens.

      Also:

      - honour a configured `temperature` on the GPT-4 path instead of pinning 1.0,
      matching what the Claude and Gemini providers already do
      - read `sampling` without casting, so a non-string value in the JSON cannot
      raise an array-to-string conversion
      - note in the class docblock that an unrecognised model name falls back to the
      GPT-4 parameter set, so the next family will need its prefix added here

      ./bin/test unit — OK (401 tests, 802 assertions)
      phpcs --standard=Magento2 — 0 errors on all touched files

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 7d1b03

    • fix(openai): send the parameter set the model family accepts

      Every request carried temperature, frequency_penalty and presence_penalty,
      and max_tokens when configured. That is the GPT-4 parameter set. The GPT-5
      family and the o-series refuse all four on the same endpoint: the two
      penalties are rejected outright even at 0, temperature takes nothing but
      its default, and the token cap was renamed to max_completion_tokens. A
      request built the old way comes back 400, so those models did not degrade
      on this provider — they failed every call.

      Nobody hit it because the model dropdown stopped at gpt-4o-mini and
      reaching a newer one meant typing it into the custom field. The client on
      the translation project asked for gpt-5.6-terra, which is where this
      starts to matter.

      The body is now built by a small class of its own. The provider builds its
      HTTP client inline and cannot be exercised without a network, which left
      the part that has to be right per model family with no test at all; that
      part is now pure and covered.

      Family is decided by the model name, with a sampling extra-config key to
      override it: an Azure deployment or a proxy can serve GPT-5 under a name
      that says nothing, and prefix matching would send it what it refuses.
      reasoning_effort and verbosity are passed through where they are
      understood, so the effort knob is reachable without another release.

      A caller-supplied system message now replaces the built-in one instead of
      being appended after it — two system messages in one request left the
      model to pick. No caller sends one yet.

      The three GPT-5.6 tiers are in the model dropdown, so the client's model
      is a selection rather than a hand-typed string.

      phpcs --standard=Magento2 reports 0 errors on the touched files.
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 3b7370

    • Merge pull request #51 from breezefront/fix/category-translate-source-language

      fix(translate): name the source language when translating categories db336b

    • docs(translate): tighten the source-store comment

      The docblock read like a changelog entry for 1.0.11. Keep the reason the
      source store is handed to the skill, drop the history around it.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> f63c1b

    • fix(translate): name the source language when translating categories

      1.0.11 changed the translate prompt to name both languages ("translate
      from Danish to German") because a weak model, told only the target, left
      a good part of the text in the source language. The bulk consumer was
      only half-converted: translateProducts() passes source_store_id, but
      translateCategories() still calls the skill with the attribute and the
      target store alone.

      The source store was already used to load the category, so the values fed
      to the model were right — only the prompt was missing the "from" half.
      Category translations therefore kept running on the pre-1.0.11 prompt and
      came back partly untranslated, the exact symptom 1.0.11 was released to
      cure. Nothing surfaced it: the skill treats source_store_id as optional
      and silently omits the clause when it is absent.

      The unit test now asserts the whole options array rather than accepting
      any call, so the next field added to it cannot go missing the same way.

      phpcs --standard=Magento2 reports 0 errors on the touched files, the same
      as before.
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 6871f1

    • Merge pull request #50 from breezefront/fix/bulk-empty-field-panels-v2

      fix(bulk): do not offer a task whose field panel has nothing in it acd541

    • fix(bulk): do not offer a task whose field panel has nothing in it

      The wizard decides what step 2 may offer by asking whether a step 4 panel
      exists for the pair. A panel could exist and still be empty: Generate
      renders a row per attribute only when a prompt is bound to it, so with no
      prompts configured the panel is a bare fieldset. The check said yes, the
      operator picked Generate, and step 4 came up blank with a Run button that
      never enabled — the dead end the panel check was meant to close, reached
      from inside the panel instead of around it.

      Prompts are now resolved once up front and an attribute without any is
      dropped, which makes "the panel has fields" knowable before rendering.
      A panel with no fields is not rendered, so the existing check reports the
      truth for it. Translate gets the same treatment for symmetry, though its
      field list is only empty on entity types nobody can select today.

      That leaves step 2 able to come up with nothing at all in it, so it now
      says why instead of showing an empty box.

      phpcs --standard=Magento2 reports 0 errors on the touched file, the same
      as before.
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 392330

    • Merge pull request #48 from breezefront/fix/bulk-save-skill-entity-validation

      fix(bulk): refuse a skill the queue cannot run on the chosen entity type 24f2e1

    • Merge pull request #49 from breezefront/fix/bulk-empty-field-panels

      fix(bulk): do not offer a task whose field panel has nothing in it ecfa84

    • Merge pull request #46 from breezefront/fix/bulk-wizard-skill-filter

      fix(bulk): decide the offered tasks by the step 4 panel, not the skill's word 87738f

    • fix(bulk): refuse a skill the queue cannot run on the chosen entity type

      BulkConsumer::execute() routes on (skill, entity type) pairs and throws on
      anything it has no handler for, but the scheduling controller accepted
      whatever was posted. A request for a pair the consumer cannot route —
      generate on cms_page, say, which the wizard hides but the form will still
      carry — was answered with "has been scheduled", split into operations, and
      failed one by one where only the bulk log would show it.

      Save now checks the pair before it schedules anything, so the operator is
      told no while still looking at the screen. The check runs before the entity
      ids are resolved, which also spares a full-catalog id query for a request
      that was never going to run.

      QueueRoutes holds the pairs rather than inlining them, because the
      consumer's routing table is the real authority and the two must not drift:
      QueueRoutesTest reads the consumer's match arms and asserts they are
      exactly the declared list.

      It is deliberately not SkillSupport. That asks whether a skill can act on
      an entity type at all and answers from getSupportedEntityTypes(); Generate
      claims cms_page for the inline editor and is right to, but there is no
      generate/cms_page route in the consumer. What a skill can do and what the
      queue can carry out are separate facts, and only the second one decides
      what may be scheduled.

      370 unit tests pass. phpcs --standard=Magento2 reports 0 errors on the
      touched files, and Save.php carries the same 10 warnings as before.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 600476

    • Merge pull request #47 from breezefront/fix/cms-page-translate-scope

      fix(translate): stop writing CMS page translations into a category 219f60

    • fix(bulk): do not offer a task whose field panel has nothing in it

      The wizard decides what step 2 may offer by asking whether a step 4 panel
      exists for the pair. A panel could exist and still be empty: Generate
      renders a row per attribute only when a prompt is bound to it, so with no
      prompts configured the panel is a bare fieldset. The check said yes, the
      operator picked Generate, and step 4 came up blank with a Run button that
      never enabled — the dead end the panel check was meant to close, reached
      from inside the panel instead of around it.

      Prompts are now resolved once up front and an attribute without any is
      dropped, which makes "the panel has fields" knowable before rendering.
      A panel with no fields is not rendered, so the existing check reports the
      truth for it. Translate gets the same treatment for symmetry, though its
      field list is only empty on entity types nobody can select today.

      That leaves step 2 able to come up with nothing at all in it, so it now
      says why instead of showing an empty box.

      phpcs --standard=Magento2 reports 0 errors on the touched file, the same
      as before.
      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 1fe13b

    • fix(translate): refuse an unsupported entity type in the skill itself

      Narrowing getSupportedEntityTypes() to product and category told the truth
      but gated nothing. Three paths still reached a CMS page:

      - McpServer::executeSkill() passes entityType straight to GenerateService;
      the schema enum is a hint to the client, not a check. translate +
      cms_page + targetStore>0 went through Translate::execute(), which took
      the attribute list from the input field pool — and the pool still knows
      the cms_page fields — translated the text, saved nothing, and reported
      success. A silent no-op instead of an error.
      - The CMS page form kept its "AI Translate" buttons. Block\Adminhtml\Js
      hands translate.js every input field of the entity type, and the form
      has no store switcher, so currentStore is always 0: every click ran
      translateForAllStores() and died on the new save guard.
      - That death cost one AI request. translateForAllStores() calls the skill
      first and saves after, so the provider was paid before anything refused
      the entity.

      Translate::execute() now asserts the entity type against its own
      getSupportedEntityTypes() right after resolving it — before the provider
      call, so a refused entity costs nothing, and for every caller at once.
      saveAttributeForStore() keeps its throw as the last line of defence.

      SkillSupport answers "can this skill act on this entity type" for UI code;
      Js publishes skills.translate and translate.js renders no buttons when it
      is false. Generate still gets the full field list.

      Js gained a constructor argument — setup:di:compile.

      364 unit tests pass. phpcs --standard=Magento2 reports no errors on the
      touched files.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 551992

    • refactor(bulk): give the step 4 panel lookup one home

      The panel query behind the wizard's "can this task run here" check was
      written out three times over. wizardCanRun() and hasFieldSelection() ran
      byte-identical selectors, each with its own empty-argument guard, so a
      change to how a panel is addressed had to be made in more than one place
      to hold.

      findPanel() is now that one place, and both callers read as what they
      ask: does a panel exist, and does the panel that exists have a selection.

      The Build Landing Page panel also rendered unconditionally, though step 1
      offers CMS pages only when the Content Builder is installed. Without it
      the panel was unreachable markup in every admin; it now follows the same
      condition as the content type it serves.

      No behaviour change. 358 unit tests pass; phpcs --standard=Magento2
      reports the same 0 errors as before on the touched file.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 6d13ac

    • fix(translate): stop writing CMS page translations into a category

      translateForAllStores() routes every non-product entity through
      saveAttributeForStore()'s else branch, which loads a *category* by the entity's
      id. For a CMS page that id is a page_id, so translating page 8 for all store
      views wrote its translated meta_title onto category 8 — on this install, "New
      Luma Yoga Collection". A silent write into an unrelated record, no error, no
      trace.

      Extending the branch is not the fix either: cms_page has nowhere to put a
      per-store-view value. The table holds one row per page — title, meta and content
      are not store-scoped — and the store assignment lives in cms_page_store. A
      translated page in Magento is a separate page. So Translate stops claiming
      cms_page, which also narrows the MCP tool's entityType enum, and
      saveAttributeForStore() raises on anything it does not explicitly handle instead
      of guessing: entity ids collide freely across tables, and guessing corrupts.

      Generate keeps cms_page — that path is real. GenerateService::loadEntity()
      already resolves pages through PageRepository, so the AI button works on the SEO
      fields of the CMS page form. content_heading joins them: plain text, same
      mechanics as product-name, and the one obvious gap. title and identifier are
      left out on purpose (identifier drags url rewrites behind it), and content
      belongs to the Build Landing Page skill, which writes the whole document.

      Bulk is unaffected: BulkConsumer never handled generate/cms_page and still does
      not, so the wizard offers Build Landing Page alone for CMS pages.

      Verified against the compiled DI:

      cms_page fields content_heading, meta_title, meta_keywords, meta_description
      generate [product, category, cms_page]
      translate [product, category]
      build_page [cms_page]

      360 unit tests pass, including one that pins the corruption: translating a CMS
      page for all store views now raises and never touches the category repository.
      phpcs --standard=Magento2 reports no errors on the touched files. afe08f

    • fix(bulk): decide the offered tasks by the step 4 panel, not the skill's word

      Picking CMS Pages still listed Generate Content and Translate: both declare
      cms_page in getSupportedEntityTypes() for the inline editor, so the filter added
      in the previous commit let them through. Choosing either landed on an empty step
      4 with no step 5 and no Run button — the exact dead end this branch set out to
      remove, just reached by another route.

      A skill's own entity list says nothing about whether this screen can drive it.
      The wizard now asks the only thing that matters: does a step 4 panel exist for
      this (content type, task) pair. That is the condition for not dead-ending, and it
      stays true as panels are added or removed.

      getSkillEntityTypes() existed only to feed the old check and is dropped with it.

      Verified in the admin: CMS Pages now offers Build Landing Page alone, step 3 is
      skipped, step 4 reads "Describe the page", step 5 lists the 84 pages and Run
      schedules "AI Build Landing Page for 1 CMS page". 92afa6

    • fix(bulk): decide the offered tasks by the step 4 panel, not the skill's word

      Picking CMS Pages still listed Generate Content and Translate: both declare
      cms_page in getSupportedEntityTypes() for the inline editor, so the filter added
      in the previous commit let them through. Choosing either landed on an empty step
      4 with no step 5 and no Run button — the exact dead end this branch set out to
      remove, just reached by another route.

      A skill's own entity list says nothing about whether this screen can drive it.
      The wizard now asks the only thing that matters: does a step 4 panel exist for
      this (content type, task) pair. That is the condition for not dead-ending, and it
      stays true as panels are added or removed.

      getSkillEntityTypes() existed only to feed the old check and is dropped with it.

      Verified in the admin: CMS Pages now offers Build Landing Page alone, step 3 is
      skipped, step 4 reads "Describe the page", step 5 lists the 84 pages and Run
      schedules "AI Build Landing Page for 1 CMS page". 7413c9

    • Merge pull request #45 from breezefront/feat/bulk-cms-landing-page

      feat(bulk): build landing pages from the AI Bulk Action wizard eeabe7

    • fix(bulk): file the generated page draft under the scheduling admin

      BCB drafts are per user: Content/Load reads getDraft($pageId, 0, $currentUserId)
      and the collection filters on equality, so a draft written for anyone else is
      invisible in the editor. bcb_page_content.user_id is also a foreign key on
      admin_user, so the placeholder 0 the consumer was passing failed the insert
      outright — every build_page operation died with an integrity violation and no
      draft was ever stored. The unit tests missed it because DraftWriter is mocked.

      The scheduling admin's id now travels in the payload and DraftWriter requires it,
      rejecting 0 before touching the repository. Save refuses to schedule a page build
      when the current user cannot be identified, rather than queueing work that cannot
      land.

      Alongside that:

      - CMS Pages is only offered when the Content Builder is installed. Without it
      nothing can store the document, so the wizard used to run the whole batch
      through the AI only to fail on save. Step 1, the grids and the LISTING map are
      all derived from ViewData::getEntityTypes() now.
      - Step 4's heading follows the picked task: "Describe the page" for Build Landing
      Page instead of the field-list copy, which still said "Select attributes such
      as name, description, meta title".
      - Operator messages use real names: "AI Build Landing Page for 3 CMS pages"
      rather than "AI Build_page for 3 cms_page(s)".
      - Step 4 warns that the run replaces the current draft on every selected page.
      - Dropped the unused is_active column from the CMS page grid's select.

      Tests: DraftWriterTest covers both guards and the write, SaveTest covers the
      payload and the messages, BulkConsumerTest asserts the user id reaches the
      writer and that a payload without one fails the operation. 359 pass; phpcs
      --standard=Magento2 reports no errors. e8f294

    • feat(bulk): build landing pages from the AI Bulk Action wizard

      Step 2 listed every registered skill, so Build Landing Page was offered for
      products and categories. It declares cms_page and no attributes, leaving step 4
      empty and step 5 unreachable — the wizard dead-ended with no error.

      Tasks are now filtered by getSupportedEntityTypes() against the content type
      picked in step 1, and CMS Pages joins that step so the skill has a real path:
      a brief instead of the per-attribute field list, a CMS page grid, and no store
      view step (Content Builder documents are store-agnostic).

      The generated document is stored as the page's Content Builder draft. The BCB
      page flag is left alone: turning it on with nothing published renders a blank
      storefront page, so enabling the editor and publishing stay manual. The BCB
      dependency stays soft — the repository is resolved by name behind
      interface_exists(), like the component catalog reader.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> afe7f0

    swissup/module-breeze-content-builder — 1.2.0 (was 1.0.10)
    • Version 1.2.0 7f774f

    • Changes for support of the contact us page 812853

    • Added h1 for the Text component and switched to tags instead of CSS classes e054e6

    • Added new Map component c2d71b

    • Improve Columns vertical stretch 6b5da8

    • Version 1.1.0 d0ef07

    • Merge pull request #35 from breezefront/feat/ai-components-bridge

      feat(content-builder): apply AI-generated components to the working copy (#34) fa5587

    • Added new Video component 94bb8d

    • fix(content-builder): clone the incoming array before installing it as the working copy d02528

    • fix(content-builder): stay a no-op until sub-modules finish initializing 2c2289

    • fix(content-builder): commit a pending dirty edit before pushing the AI snapshot 3a1ce7

    • feat(content-builder): apply AI-generated components to the working copy (#34) 030d4c

    • Added 5 columns layout for the Columns component 4adc04

    • Version 1.0.12 ccc005

    • fix(banner): make the duplicate-H1 guard see the theme's own H1 (#32)

      Three review findings on the H1 work:

      1. The guard counted only Banner components, so it stayed silent on the most
      common duplicate: a CMS page with a Content Heading, where
      Magento\Cms\Block\Page::_prepareLayout() feeds page.main.title and
      module-theme's html/title.phtml emits <h1 class="page-title">. Load now
      reports pageHasH1 (Content Heading non-blank), setExternalContent takes the
      same flag from its opts so BLB can declare the product/category page title,
      and the guard adds that H1 to the count.

      2. The guard only ran on a headline_type change, so pasting an H1 Banner never
      warned. It now runs after paste and after content load as well, which also
      covers a tree that arrived from an AI build.

      3. getDraft() reloaded the draft by id without checking the load succeeded. If
      the row was deleted between the collection query and the reload, it returned
      an empty model and the caller saved a row with no page_id — rejected by
      MySQL, since page_id is NOT NULL with no default. It now falls back to the
      seeded-draft path, which is extracted into createSeededDraft().

      Load's constructor gained a dependency, so setup:di:compile is needed.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> abc266

    • feat(banner): allow H1 for the banner headline (#32)

      The Banner headline_type prop only offered h2-h6, so a page built entirely
      in Content Builder shipped without a primary heading — the document had no
      h1 at all, which is an on-page SEO defect.

      Add h1 to the option list and to the template's tag whitelist. The default
      stays h2, so existing pages and newly inserted Banners render exactly as
      before.

      Because a page should carry at most one h1, the editor now shows a soft
      notice when a headline is switched to h1 while another Banner already uses
      it. It warns rather than blocks — a legitimate layout may keep its h1
      outside the Content Builder bands.

      The Text component's heading_type is deliberately left alone: it renders a
      div with a heading class, not a real heading tag, so an "h1" there would be
      visual only and misleading.

      Fixes #32

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> ced844

    • fix(content): reload draft through singular load so updated_at advances

      getDraft() returned a collection-hydrated item with empty storedData,
      so save()'s update-diff wrote every column back unchanged, pinning
      updated_at and defeating ON UPDATE CURRENT_TIMESTAMP.

      Related to #31 (does not close it — content-loss symptom unconfirmed). b1759b

    • fix(banner): make the duplicate-H1 guard see the theme's own H1 (#32)

      Three review findings on the H1 work:

      1. The guard counted only Banner components, so it stayed silent on the most
      common duplicate: a CMS page with a Content Heading, where
      Magento\Cms\Block\Page::_prepareLayout() feeds page.main.title and
      module-theme's html/title.phtml emits <h1 class="page-title">. Load now
      reports pageHasH1 (Content Heading non-blank), setExternalContent takes the
      same flag from its opts so BLB can declare the product/category page title,
      and the guard adds that H1 to the count.

      2. The guard only ran on a headline_type change, so pasting an H1 Banner never
      warned. It now runs after paste and after content load as well, which also
      covers a tree that arrived from an AI build.

      3. getDraft() reloaded the draft by id without checking the load succeeded. If
      the row was deleted between the collection query and the reload, it returned
      an empty model and the caller saved a row with no page_id — rejected by
      MySQL, since page_id is NOT NULL with no default. It now falls back to the
      seeded-draft path, which is extracted into createSeededDraft().

      Load's constructor gained a dependency, so setup:di:compile is needed.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> a6d73f

    • feat(banner): allow H1 for the banner headline (#32)

      The Banner headline_type prop only offered h2-h6, so a page built entirely
      in Content Builder shipped without a primary heading — the document had no
      h1 at all, which is an on-page SEO defect.

      Add h1 to the option list and to the template's tag whitelist. The default
      stays h2, so existing pages and newly inserted Banners render exactly as
      before.

      Because a page should carry at most one h1, the editor now shows a soft
      notice when a headline is switched to h1 while another Banner already uses
      it. It warns rather than blocks — a legitimate layout may keep its h1
      outside the Content Builder bands.

      The Text component's heading_type is deliberately left alone: it renders a
      div with a heading class, not a real heading tag, so an "h1" there would be
      visual only and misleading.

      Fixes #32

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 620e7f

    • fix(content): reload draft through singular load so updated_at advances

      getDraft() returned a collection-hydrated item with empty storedData,
      so save()'s update-diff wrote every column back unchanged, pinning
      updated_at and defeating ON UPDATE CURRENT_TIMESTAMP.

      Related to #31 (does not close it — content-loss symptom unconfirmed). 5c797e

    • Remove custom accordion script d8cb73

    • Added new Accordion/FAQ component 3135bd

    • Version 1.0.11 17c28e

    • Use text color for Heading when set in the Text component 0335a1

    • Added font-size field and source button to WYSIWYG editor dfc235

    • Improve grid component styles on 640px-768px screen sizes fe4e91

    • Use banner text color for headings inside banner if set eb8b24

    • Added Feature Left/Right layouts to Grid component b1b27c

    swissup/module-breeze-layout-builder — 1.2.0 (was 1.1.1)
    • Version 1.2.0 24fff8

    • Added Contact Us page support d64e46

    • Config option for upcoming TaxVAT switcher in theme header 7f31bd

    swissup/module-navigationpro — 1.19.3 (was 1.19.2)
    • Version 1.19.3 421267

    • Do not use `is_active` from remote entity when creating new item
      Since we have new option `use_remote_include_in_menu`, use it instead
      of syncing `is_active` on item creation. 7c215a

    • Fixed not working ribbon for vertical menu 8fdb50

    • LESS 5.3.0 compatibility 922d05

    swissup/module-testimonials — 1.4.2 (was )
    • Version 1.4.2 4c12e4

    • Fixed wrong output when rating is not set 64ce08

    • Added show image config and more options for visible slides dropdown 283b05

    • Version 1.4.1 8ca632

    • Slider widget: added arrows/dots configuration 68ea17

    • Version 1.4.0 2ca286

    • Customizable frontend route, closes #12 e38c8c

    • Removed trailing slash in the canonical tag, see #13 109094

    • Fixed captcha for save controller after removing extend from deprecated class 2b4718

    • Fixed saving store relation bc50ab

    • Use escape in admin notification email b334aa

    • Small code fixes 29bd71

    • Rewrote front controllers without extending deprecated class 0d78e2

    • Updated code to use repository 16b611

    • Remove unnecessary setup_version ffbc53

    • Fixed wrong escaping 4ee69c

    • Fixed undefined $avgRating and division-by-zero 5e68bf

    • Removed deprecated call of $this->helper() in template 99e032

    • Replace deprecated code 94f1f1

    • Replace deprecated code db0ca7

    • Do not translate user-configured string 214f9a

    • Remove unused code 4a1b4f

    • Fixed wrong existing customer detecting 098a18

    • Fixed showing email regardless of config 09aacb

    • Fixed N+1 DB writes in mass status action 86330d

    • Added Testimonial Repository and updated controllers to use it 9a2a35

    • Fixed docblocks and const declarations a73587

    • Version 1.3.17 4e27d1

    • Fixed unescaped output detected error, #14 c764e3

    • Version 1.3.16 4f0b8b

    • Fixed wrong Swiper breakpoints config 7f13f2

    • Version 1.3.15 73865b

    • Fixed implicitly marking parameter as nullable error in PHP 8.4 061815

    • Version 1.3.14 95035c

    • Fixed accessibility error: Can't use h5 without all of the previous levels 127197

    • Version 1.3.13 37d4ce

    • Breeze: scroll reveal animations 9f8c52

    • Version 1.3.12 0f70e6

    • Merge branch 'breeze-dynamic-js' 3afab9

    • Lazy load for profile image in widget b5bb4a

    • Version 1.3.11 f70f2c

    • Breeze dynamic js. WIP d10c53

    • Breeze: remove unused file c5460e

    • Improve breeze intregration 63c3a6

    • Version 1.3.10 4eea58

    • Reduce css size 801626

    • Installer command added 026e30

    • Remove empty spaces around text when adding small quotes around (::before, ::after) fed341

    • Revert "Prevent layout shift when using swiper"

      This reverts commit 50793e261167585b5dae3151c03fb28acdb02994. edbf60

    • Prevent layout shift when using swiper 50793e

    • Version 1.3.9 27495e

    • PHP Stan fixes 0d973f

    • Fixed access to undefined constant 81023b

    • Do not use return in _construct ddfadb

    • Fixed not-working slides_to_show widget config, closes #11 b369c9

    • Version 1.3.8 436aa7

    • Added email validation in CreateTestimonial Resolver b355ee

    • Removed unused CreateTestimonial Resolver arguments 703253

    • Added email validation in admin form 0e254c

    • Version 1.3.7 cb93be

    • Fixed file upload in Magento 2.4.5+ 4b15a7

    • Version 1.3.6 b7f56e

    • Replaced Zend_Validate usage that was removed in Magento 2.4.6 5065f1

    • Version 1.3.5 fef03f

    • PHP 8.2: fixed dynamic properties creation e84a87

    • Version 1.3.4 594bef

    • Fixed creation of dynamic properties (php 8.2 compatibility) acc604

    • Version 1.3.3 a41d69

    • Use theme's font-family for author name aae183

    • Version 1.3.2 468b50

    • Prevent error when image input is removed from the submit form a7922d

    • Version 1.3.1 706c48

    • Fix: email is exposed regardless of "Show user email" (close #9) 794a57

    • Breeze compatibility for slide-list-widget d726e4

    • MCS fixes 96b6eb

    • Fixed invalid avatar alignment in testimonials list b5fa96

    • Breeze integration for list widget d7da46

    • JSCS fixes b1dcb3

    • Do not open new tab after double click on "load more" button c80881

    • No need to load content with post request. (Fixes sections reload after testimonials load) a4ce90

    • Version 1.3.0 4d01a6

    • Improve graphql StoreConfig extended config data e00c0a

    • Add graphql StoreConfig extended config data 5f05b5

    • add createTestimonial graphql mutation 4483cf

    • add single Resolver/DataProvider/Testimonial 10de83

    • Hide/show resovler fields depends config e64f9d

    • add date format resolver c1309d

    • add image url testimonial resolver b4020e

    • move company html to template d8d833

    • use dataprovider insteadof collection factory 641877

    • rm customerSession injection in resolver 31680d

    • Improve templates e559cc

    • add getTestimonials graphql request 49108a

    • Version 1.2.18 1b301f

    • Fixed incorrect date on non-US locale, closes #8 9310e3

    • Updated get identities code fb6870

    • Controllers code refactored 192ba7

    • Version 1.2.17 9dd35e

    • Fixed bad-looking testimonial link in customer dropdown c22bfb

    • Version 1.2.16 1e96b6

    • Automatically clean page cache when testimonial was changed 3d41b8

    • Added canonical url on testimonials page 833b4a

    • Use short testimonials page url 9ae78c

    • Version 1.2.15 b294a7

    • Added space between slides 10px in slider widget 17e88b

    • Disabled freeMode in slider widget d02ad1

    • Version 1.2.14 ea1293

    • Added image size parameters for slider widget 504de5

    • Fixed php error when uploading image in Magento 2.4.1 1098be

    • Version 1.2.13 9f141b

    • Use swiper instead on slick 93fc69

    • Version 1.2.12 ae5503

    • Fixed error filtering template in M2.4 6622bc

    • Version 1.2.11 2bbedd

    • Translations updated e965c4

    • Translations updated 5d1802

    • Version 1.2.10 ed5056

    • Removed unnecessary status change confirmations 2631fb

    • Version 1.2.9 8ab314

    • Fixed error when admin notification email is empty 262407

    • Improved slider widget bb7e7c

    • Version 1.2.8 df8489

    • Update uk_UA.csv (POEditor.com) 685cfe

    • Update sw_SE.csv (POEditor.com) c87b14

    • Update es_ES.csv (POEditor.com) 6f795a

    • Update ru_RU.csv (POEditor.com) 085215

    • Update pt_PT.csv (POEditor.com) 17c07b

    • Update pl_PL.csv (POEditor.com) 9dabf0

    • Update nb_NO.csv (POEditor.com) ae2a98

    • Update ko_KR.csv (POEditor.com) e25751

    • Update ja_JP.csv (POEditor.com) 6f92d3

    • Update it_IT.csv (POEditor.com) 51b62f

    • Update he_IL.csv (POEditor.com) 5f1aa8

    • Update de_DE.csv (POEditor.com) 827d5b

    • Update fr_FR.csv (POEditor.com) 72e587

    • Update nl_NL.csv (POEditor.com) 45037c

    • Update zh_CN.csv (POEditor.com) c70cd9

    • Update ar_SA.csv (POEditor.com) 572ad5

    • Add files via upload 05afc6

    • Version 1.2.7 c62689

    • Marketplace data added 1c280f

    • Version 1.2.6 b9fd6d

    • Installation instructions for clients added 684aad

    • Version 1.2.5 d1bd25

    • Resize images only by width when height is empty 09e85e

    • Version 1.2.4 a42213

    • Fixed file uploading in Magento 2.3 92c29e

    • Version 1.2.3 e0224a

    • Fixed wrong php doc aa15ac

    • Version 1.2.2 4cf186

    • Up required module-core version
      Since 1.8.0 module-core has Model/Media/FileInfo. Module uses it in `di.xml`. 1c4d4f

    • Use module-core\Model\Media\FileInfo to get file info f4877d

    • Version 1.2.1 d7e6dd

    • SlickCarousel update 50f3b8

    • Rename composer package to 'swissup/module-testimonials'; ver 1.2.0 cae583

    swissup/theme-frontend-breeze-enterprise-apollo — 3.3.0 (was 3.2.0)
    • Version 3.3.0 7f1f00

    • Added Contact Us page installer a791e7

    • Improve switcher label alignment inside mobile slideout a6415d

    • Fixed transparent menu icon for few seconds on slow web connection 7b8ab8

    • Sync gallery slider arrows with theme settings 6ce6f4

    • USP block content updated 96047c

    • Updated footer_top_content block content 946d00

    • Updated BCB homepage: moved to json; updated benefits block content 62bfe1

    • Fixed listing styles on Magento < 2.4.8
      LESS ^3.2.0 bugfix: This css block moved to the start of the styles. a38c1c

    • Firefox: Fixed horizontal scrollbar inside search slideout 613e59

    swissup/module-core — 1.13.1 (was 1.12.27)
    • Version 1.13.1 5b7b24

    • Fixed invisible update instructions when Mollie Payments is installed 72aab6

    • Version 1.13.0 f34c7b

    • Fill the links after ajax refresh b6a316

    • Show loading state on the "Check for Updates" button 1ab16e

    • Use optimized repo to fetch latest releases 1aa8fb

    • Check theme source files in Virtual Theme Check (#19)

      Making a virtual theme physical while its source files are unavailable is
      what breaks the storefront with "Required parameter 'theme_dir' was not
      passed", so the check now reports the state of the files and the fixer
      refuses to touch the themes it cannot read.

      - Virtual Theme Check gets Path and Status columns
      - Status names the files Magento can't read. Unreadable file is never
      reported as a removed one - a failed stat cannot tell them apart
      - Fix All opens up the permissions of unreadable source files and skips
      the themes that are still unreadable, listing them in the response
      - Readability check extracted into Swissup\Core\Model\Theme\SourceFiles,
      shared by the block and the controller
      - Config table styles shared between Modules and Virtual Theme Check

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 88915a

    • Check theme source files in Virtual Theme Check (#19)

      Making a virtual theme physical while its source files are unavailable is
      what breaks the storefront with "Required parameter 'theme_dir' was not
      passed", so the check now reports the state of the files and the fixer
      refuses to touch the themes it cannot read.

      - Virtual Theme Check gets Path and Status columns
      - Status names the files Magento can't read. Unreadable file is never
      reported as a removed one - a failed stat cannot tell them apart
      - Fix All opens up the permissions of unreadable source files and skips
      the themes that are still unreadable, listing them in the response
      - Readability check extracted into Swissup\Core\Model\Theme\SourceFiles,
      shared by the block and the controller
      - Config table styles shared between Modules and Virtual Theme Check

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 6e39e2

    • Mark core section with badge when outdated modules found 9a3764

    • Outdated modules badge and Modules config group (#25) 7df026

    • MCS fix 3b9121

    • Update instructions 0e686f

    • Read links info from marketplace meta 21e95b

    • Keep the last check time when a refresh fails

      Removing the stored time was what made the next load re-check the feed.
      An unreachable feed left no time at all: the config page had nothing to
      display, and every page load kept re-checking until one request made it
      through.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 3437f9

    • Post the modules refresh to an action of its own

      The refresh drops the version check throttle and re-reads the remote
      source, and a get carries no form key validation - Magento checks it for
      post requests only, and the url secret key is optional. Reading the list
      stays a get, so the two live in separate actions now.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 4a95c9

    • Keep the module links out of the response

      The links cell is rendered by the server and never touched by js.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 7556db

    • Send only the module metadata the browser renders

      The merged Loader record carries the local filesystem path and the
      download, license and purchase-code fields of the remote feed. None of
      them is rendered by the config page, so keep them on the server.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 2bd425

    • Too much red 2e307e

    • Use the same `outdated` class name everywhere

      The version span was already renamed, while the row and the js still said
      `_outdated` - a row that stopped being outdated kept its red version pill.

      Since the row now wears the very same class, the pill rule had to be
      scoped to the version cell, or the whole row would turn red.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> c21086

    • Report the check time with the modules, and keep outdated rows first

      Two things the clients were guessing at:

      The `Check for Updates` handler stamped the label with the current time,
      but the request may have re-used the stored packages after a failed feed
      request - `just now` was not always true. And an automatic load did check
      the remote source, yet nothing updated the label at all. The endpoint now
      returns `['items' => ..., 'last_check' => ...]`, and the label follows the
      time the server reports for every load.

      The table was re-rendered cell by cell, so a module that became outdated
      kept its old position while the counter above it grew. Mark the rows and
      move the outdated ones back to the top - both groups stay sorted by name.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 70f271

    • Keep the last check time once the check is due

      The timestamp was stored with a one hour lifetime and the throttle was
      `is the entry still there?`. Handy, but it also meant the config page
      could no longer tell how old the data was as soon as the check was due -
      exactly when that is worth showing next to `Check for Updates`.

      Store it without a lifetime and compare the age instead. Same hour
      boundary, and the label now reads `Last checked 2 h ago` instead of
      disappearing.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 9e4c2a

    • Don't update the modules on refresh. Wait for isVersionCheckRequired 129e66

    • Hovered color for outdated badge 0a0016

    • Show outdated modules count in config and add Modules group

      The admin had no hint that installed Swissup modules have updates
      available - one had to open Module Manager to find out.

      Add a counter badge to the Swissup tab of the config navigation, and a
      Modules group in Swissup > Core listing every installed module with its
      installed and latest version, outdated ones first.

      Neither of them may slow down the config page, so both render from the
      data stored in var/swissup/core (Loader::setOfflineMode()), and the
      versions, the counters and the badge itself are then refreshed by a
      single ajax request shared by all the consumers. `Check for Updates`
      re-runs that request with refresh=1, which drops the version check
      throttle and re-reads the remote source.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> bf1ad6

    • Move core config to the top as it will display outdated modules 115f37

    • Added ability to immidiately re-check remote source for updates `-r` 357d33

    • Keep the remote packages data in files instead of the cache (#24) cbeecc

    • Trim the comments down to what is not in the code

      The two lock docblocks carried the same paragraph, and save() explained
      the reasoning that belongs in the commit that introduced it.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 09d6b9

    • Serialize the writes with a lock taken before the entry is opened

      openFile() truncates whatever it opens, so a lock taken afterwards keeps
      the writes apart but not the truncations. Two saves could interleave into
      one file: the second one truncates, the first one writes a whole entry,
      and the second one then overwrites its beginning - leaving the head of
      one entry on the tail of another. The length line does not catch that
      when both entries encode to the same size, as fixed width values like a
      version hash always do.

      Take the lock before opening, on a file of its own - the entry cannot be
      opened before the lock is held, and the file lock() uses is likely held
      by the caller already, which flock would deadlock against.

      Failing to open a lock file is no longer reported as a lock held by
      somebody else. A lock left behind by another user cannot be opened at
      all, and reporting that as contention kept the caller away from the
      entry for good - silently serving a list that could never be refreshed.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 73cf28

    • Don't download the list that another process has just stored

      The version is fetched before the download lock is taken, so a process
      that was still asking for it when the lock was released would go on to
      download the very list the lock holder had just stored. Look at the
      storage once more before downloading.

      Also correct what the save() lock claims to do: it serializes the
      writes, but the truncation happens in openFile(), outside of it.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> d557a0

    • Reject an entry whose expiration time is not a number

      A corrupt header such as `expires:invalid` was cast to zero, which reads
      as "never expires" - so an entry that should have gone stale could be
      served indefinitely instead of being treated as missing. Validate the
      field the same way the length line is validated.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 12db16

    • Keep the remote packages data in files instead of the cache

      The packages list was kept in the Magento cache, so every cache flush
      threw it away and the next admin request had to download it again. It is
      now stored under var/swissup/core, where it outlives the flush.

      FileStorage writes the expiration time and the data length ahead of the
      contents, so an entry knows itself when it goes stale, and a reader can
      tell a half-written entry from a complete one - openFile() truncates the
      file before the write lock is taken, so a reader can catch the entry
      mid-save. An incomplete entry is reported as missing and refetched,
      rather than served as if it were whole.

      Remote revalidates against the version hash from packages.json, at most
      once an hour, and only downloads the full list when that hash moves. The
      download itself is guarded by a non-blocking lock, so several admins
      hitting the page at once produce one request instead of one each - the
      others serve the stored copy, or wait for the download when there is
      nothing stored yet. fetch() now reports connection errors and 4xx/5xx
      responses by returning an empty body, letting the caller fall back to
      the stored data instead of decoding a failure page.

      The admin notification feed keeps its last-update timestamp in the same
      storage, replacing its own copy of the file handling.

      Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 617dbd

    • Small fixes f7eef6

    • Single source of truth for is_outdated, is_installed 37ad01

    • Fixed `Undefined array key "type"` warning b2a9c1

    • Don't show non installed modules when asking for outdated bf5fb1

    swissup/theme-frontend-breeze-enterprise-blank — 3.3.0 (was 3.2.0)

breezefront / breeze

2.32.0 Aug

breezefront / breeze-ai

1.2.0 Aug