Recent Activities

This page shows what are we working on.

breezefront / theme-frontend-breeze-enterprise-eir

2 days ago success
  • head
    • Header slideout menu... d4f174

    • Listing styles for the list mode and old price update. cbff61

    • Update README.md to clarify usage of `with-bottom-divider` CSS class and provide detailed instructions 1b9ea5

    • Pagination and make with-bottom-divider general. 37ea17

    • Added list banner 5 config; updated installer, moved it to json a43985

    • Add new layout configuration and remove easybanner references; update footer content and styles 87165b

    • Refactor easybanner configuration and installer for product listing banners 8b1d0d

    • Installer. Listing grid banner added. 914c01

    • Add swatches variables and layout styles d59581

    • Update positioning for filter title and content in layered navigation 8cbd21

    • Add image dimensions for product listings and related widgets in view.xml 6985cd

    • layout and styles for category view, product toolbar, shop by button 08fd27

    • Add layered navigation styles and update imports 944ff1

    • Keep working on prodcut listing and category page. 411088

    • Keep working on product listing toolbar. ebcfac

    • Working on product listing and toolbar look. 1ab39d

    • Add Cooper* font 132906

    • Add listing item styles and variables e725b5

    • Product lisnting in progress - grey bg and border radius. 4e7c35

    • Add new variable files for colors, typography, header, footer, icons, navigation, popover, minisearch, and gradient ba122c

    • Refactor footer and newsletter styles for improved consistency and responsiveness d831e4

    • Footer top newsletter - some updates. 452ca5

    • Add footer top content block and styles for newsletter integration (WIP) 128131

    • Footer bottom. 8d58ea

    • Add footer content block and styles for enhanced footer layout 4d6b1b

    • Header. Hide menu on medium screens. Update search look on monile. e0146d

    • Installer. Add navigation slideout menu a89fd5

    • Add EasySlide functionality for header panel with custom slides and styles e55b0b

    • Add minisearch layout and styles to enhance search functionality 5c5aec

    • Update config.xml and LESS files for theme adjustments and styling improvements of search field 09f88d

    • Add Lexend font and related styles to the theme 0100e8

    • Create config.xml a78b04

    • Create installer.xml 073f18

    • Create view.xml baa3b6

    • Create theme.xml 440a18

    • Create registration.php 119b4f

    • Create README.md 2a5862

    • Create composer.json 16ec29

breezefront / module-breeze-ai

2 days ago success
  • head
    • 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

  • 1.0.13
    • Version 1.0.13 43bc3d

    • Merge pull request #44 from breezefront/chore/mcp-graphql-review-followups

      chore(mcp): document graphql guard params, look the tool up by name 4aade7

    • chore(mcp): document graphql guard params, look the tool up by name

      Follow-ups from the review of #43.

      `assertReadOnly()`, `getDefinitionKeywords()` and `skipStringValue()` were
      missing `@param` annotations, which `phpcs --standard=Magento2` reports on
      `Model/McpServer.php` (18 warnings before, 15 after — the rest are pre-existing).

      `testGraphqlToolIsDocumentedAsReadOnly` picked the graphql tool out of
      `tools/list` by its position in the array. The position shifts as soon as another
      skill is registered, so the test would start asserting against the wrong tool
      rather than failing for a reason anyone could read. It now looks the tool up by
      name and asserts the entry exists.

      No behaviour change. ./bin/test unit: OK (339 tests, 680 assertions).

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

    • Merge pull request #43 from breezefront/fix/mcp-graphql-read-only

      fix(mcp): reject mutations in the read-only graphql tool 84c5b5

    • fix(mcp): reject mutations in the read-only graphql tool

      The `graphql` MCP tool is documented as "read-only catalog access" in its own
      tool description, in the README and on breezefront.com, but `McpServer::graphql()`
      forwarded the `query` argument to the storefront GraphQL endpoint verbatim. No
      auth header is sent, so the document executes in guest scope — and guest scope is
      not read-only: Magento accepts `createEmptyCart`, `addProductsToCart`,
      `setGuestEmail` and the rest of the guest cart surface there. An agent connected
      over MCP could therefore write to the store through a tool advertised as read-only.

      `assertReadOnly()` now refuses any top-level definition other than `query` and
      `fragment` before the request goes out.

      The operation type is read from the definition position of the document rather
      than by searching for the word "mutation", which would be both bypassable and
      wrong. `getDefinitionKeywords()` walks the document tracking brace and paren
      depth, skips comments and string literals (including block strings), and records
      the keyword that opens each top-level definition — treating the anonymous
      shorthand `{ ... }` as `query`. So a leading newline, a `# comment`, an operation
      name, variable definitions with object default values or a directive cannot
      smuggle a keyword past it, and `mutation` used as a field name, an alias or
      inside a string argument is not mistaken for an operation.

      What the check covers: every definition in the document, so a mutation appended
      after a legitimate query is rejected regardless of `operationName`; mutations and
      subscriptions; type-system definitions (an allowlist, so unknown keywords are
      refused rather than passed through).

      What it does not cover: field-level read restrictions — any data guest scope can
      already read stays readable, which is what "read-only" claims and no more. It is
      not a GraphQL validator either; a malformed document (an unterminated string, say)
      may still be forwarded, and the endpoint rejects it before executing anything.

      `/llms.txt` is a separate case. Its "read-only" claim describes the store's own
      storefront `/graphql`, which Magento serves and this module cannot restrict, so
      the wording is corrected there instead of enforced: the endpoint is unauthenticated
      and does accept guest mutations, and the file documents read queries only.

      Also corrects the assumption in docs/plan/mcp-server.md that seeded this —
      "Catalog GraphQL is public/read-only, so no auth risk" conflated public with
      read-only.

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

breezefront / module-breeze

2 days ago success
  • head
    • Do not include requirejs-config if it's empty b4fbfe

    • Defer requirejs-config as all other scripts are deferred too 861179

    • Preload image from category-view block if main image is not found de4258

  • 2.31.4
    • Version 2.31.4 53d648

    • Improve cache key for js block f390bf

    • Update third-party JS libraries

      - underscore 1.13.6 -> 1.13.8
      - knockout 3.5.1 -> 3.5.3
      - js.cookie 3.0.1 -> 3.0.8
      - dayjs 1.11.7 -> 1.11.21
      - panzoom 4.5.1 -> 4.6.2
      - swiped-events 1.1.7 -> 1.2.0
      - tabbable 6.1.1 -> 6.5.0
      - focus-trap 7.3.1 -> 8.2.2
      - scrollyfills 1.0.3 -> 1.0.4
      - lite-yt-embed: 0.3.4 1d4f5d

    • Use view transition when prefers-reduced-motion is not set to `reduce` c4ed65

    • Remove useless comment 80489b

    • Fixed empty pages in Instagram Browser when view transition is enabled
      Closes https://github.com/breezefront/theme-frontend-breeze-evolution/issues/23 1c3432

    • fix(scroll-reveal): guard optional BreezeThemeEditor lookup with module-enabled check

      class_exists(BreezeThemeEditor::class) only reflects composer autoload
      availability, not whether Swissup_BreezeThemeEditor is actually
      enabled. When it's disabled, di.xml preferences aren't loaded, so
      ObjectManager::get(BreezeThemeEditor::class) fatals trying to
      instantiate the bare ValueRepositoryInterface it depends on
      (fixes #100).

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

breezefront / module-breeze-integrations

2 days ago success

swissup / module-pagespeed

2 days ago success
  • 1.18.4
    • Version 1.18.4

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

    • Merge pull request #102 from swissup/fix/requirejs-bundle-pool-order-27428

      fix(defer): keep the bundle pool and our RequireJS patches in place 7aeb97

    • test: repair the typo'd DOCTYPE in the offset fixture

      `<!` followed by a space is not a doctype, so libxml parsed it as a bogus
      comment. With LIBXML_HTML_NOIMPLIED the parser then had no document element to
      attach the real <html> to, emitting "misplaced <html> tag"; Magento's unit
      bootstrap promotes that warning to an exception, so the test errored on every
      run instead of asserting anything.

      Also close the space inside the require.js src in the same fixture. That one did
      not fail the test — the assertions only count //script[@src] and compare their
      offsets — but the path was meaningless as written.

      The three other fixtures in this file already spell the doctype correctly.

      Fixes #103

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

    • fix(defer): narrow the RequireJS patch guard to override.js

      The bootstrap guard matched the whole Swissup_Pagespeed/js/lib/requirejs/
      directory, which pinned preload-all-scripts.js in <head> as well. That file is
      not bootstrap: addPreloadAllScripts() add()s it at the end of the asset
      collection rather than inserting it after the min resolver, and its entire body
      runs inside a window load handler, so it cannot race require.config(). Guarding
      it only costs a render-blocking head script on stores with Force RequireJS
      Loading enabled.

      Match 'Swissup_Pagespeed/js/lib/requirejs/override' instead, and correct the
      comment: it described insert()-after-min-resolver placement that applies to
      override.js alone.

      testPreloadAllScriptsStaysOptimizable pins the reverse direction - the file must
      still move to body end and take async past the counter threshold. It fails with
      the directory-wide prefix and passes with the narrowed one.

      Also assert asset presence while collecting positions in
      testLiveHeadKeepsBootstrapChainInOrder: strpos returns false for a dropped
      asset, which asort() sorts first, reporting a bogus ordering violation instead
      of the missing script.

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

    • fix(defer): keep the bundle pool and our RequireJS patches in place

      The 1.18.2 bootstrap guard was meant to cover the bundled module pool, but
      'js/bundle/bundle' only matches Magento's own bundle0.js. Advanced JS Bundling
      names its pool after the layout handle -- default-bundle0.js,
      cms-page-view-bundle0.js -- so none of our bundles ever matched, and
      Swissup_Pagespeed/js/lib/requirejs/override.js was not listed at all.

      Combined with the 1.18.2 counter change, where skipped-but-executable scripts
      consume a slot, these scripts crossed the jsCounter > 5 threshold and started
      getting async -- something that did not happen before 1.18.2. Both call
      require.config(): the bundle pool registers the jsbuild sources, and override.js
      wraps require.config() and must be installed before requirejs-config calls it.
      So on a store running Advanced JS Bundling or Interactive Defer, 1.18.3
      introduced a fresh instance of the very failure class it fixed.

      Match the whole js/bundle/ directory instead of a filename prefix (anything
      living there is a module pool by construction) and add our own RequireJS patch
      directory.

      Regression coverage uses the head sequence dev2.dlastore.com actually serves,
      with both features on: six ignored third-party inline blocks, the global require
      config, then Magento's bootstrap interleaved with the bundle pool. The fixture
      asserts the whole chain stays ahead of the body content, carries no async/defer,
      and keeps its document order. It fails in seven places without the fix.

      Refs TTF-27428

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

  • 1.18.3
    • Version 1.18.3

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

    • Merge pull request #101 from swissup/fix/requirejs-bootstrap-coverage

      fix(defer): cover every RequireJS bootstrap asset and stop counter drift 5a4389

    • fix(defer): count only executable scripts and cover global require config

      Three follow-ups on the previous commit in this branch.

      Counting every skipped script restored bootstrap coverage but also counted
      inert blocks. text/x-magento-template, text/html and JSON payloads are never
      executed by the browser, and Magento emits a handful of them on most pages
      (minicart, checkout, swatches). Counting them inflated jsCounter and pushed
      later scripts past the async (> 5) and third-party defer (> 1) thresholds, so
      scripts that used to stay synchronous started getting async and losing their
      execution order. jsCounter now advances only for script tags the browser
      actually runs: an empty type, a JS MIME type, or the configured delayed type
      (those are executed by the delayed-script runtime, so they keep their slot).

      The inline bootstrap matcher only recognized the require.config() call. The
      other legitimate form, a global `var require = {...}` / `window.require =
      {...}` object emitted before require.js loads, carries the same
      baseUrl/paths/map data and is under the same ordering constraint, so it was
      still relocated behind the content and reproduced TTF-27428 on themes that
      use it. Added REQUIREJS_INLINE_GLOBAL_PATTERN.

      Inline matching is textual, not parsed, so a script mentioning the config
      statement inside a string or a comment is opted out too. That direction is
      deliberate - a missed optimization instead of a broken RequireJS bootstrap -
      but it was documented as if only src matching had that property. Corrected the
      docblock and pinned the behaviour with a test. Also gated both patterns behind
      a stripos() check, since the matcher runs on every inline body including the
      multi-hundred-kilobyte checkoutConfig payload.

      Tests: 35 -> 42. Verified against the pre-fix code: 6 failures, one per gap
      (inert-counter case plus five global-config spellings). With this change
      705/705 green, phpcs 0 errors.

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

    • fix(defer): cover every RequireJS bootstrap asset and stop counter drift

      PR #99 protected only requirejs/require.js, mage/requirejs/mixins.js and
      requirejs-config.js. Magento renders up to eight bootstrap assets in <head>
      (Magento\RequireJs\Block\Html\Head\Config::_prepareLayout), and the rest were
      still relocated to the end of <body>:

      - requirejs-min-resolver.js patches ctx.nameToUrl to append the .min sign and
      is emitted whenever JS minification is on, i.e. in production
      - mage/requirejs/baseUrlResolver.js + requirejs-map.js install the URL map
      - mage/requirejs/static.js and js/bundle/bundle*.js define the module pool

      Moving any of them behind the content reproduces the same failure class as
      TTF-27428: module ids resolve against the bare baseUrl and static URLs are wrong.

      Replace the five ignore signatures with isRequireJsBootstrap(), which matches
      external assets on src only (an unrelated script merely mentioning a bootstrap
      path in its code is no longer opted out of deferring) and inline blocks on the
      require.config()/requirejs.config() call itself, tolerant to any spacing.

      Also increment jsCounter for skipped scripts. Without it every ignore signature
      shifted the unpack/async/defer thresholds of later scripts: with PR #99 only
      three of six eligible inline scripts were still delayed on a page whose <head>
      carries the bootstrap.

      Extend RequireJsBootstrapOrderRegressionTest with a provider over all eight
      minified asset names, seven inline-config spellings, a guard that a script only
      mentioning a bootstrap path stays optimized, and a counter-stability assertion.
      Against the previous code the new cases produce 9 failures; with this change the
      suite is 698/698 green.

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

    • Merge pull request #100 from swissup/fix/category-image-swap-xss-14036

      fix(image): keep the processed marker out of the DOM (#xss-14036) 374f33

    • fix(dom): rank class last in attribute matching (#xss-14036)

      Follow-up on review of the processed-marker fix.

      AttributeBasedStrategy tried `class` before `src`, which is what let the
      production patch land on a foreign product image in the first place. The
      ambiguity guard alone does not cover a class that matches exactly one -
      but wrong - element, so order the attributes from most to least
      identifying and keep `class` as the last resort. Testing for a second
      match from the first match's offset replaces the full-document
      preg_match_all, and a skipped match is now logged instead of silently
      dropping the optimization.

      ProcessedNodeRegistryTrait no longer requires the consuming class to call
      an initializer from its constructor: the storage is created on first use,
      so a reader can never hit an uninitialized typed property. Added __clone
      so a cloned document gets its own registry, and documented that entries
      are keyed by PHP object identity and do not survive a node being rebuilt
      from HTML.

      PipelineCategoryImageSwapTest claimed to reproduce the image swap. It
      does not: in a fixture that small OffsetBasedStrategy still resolves the
      orphaned patch group, so AttributeBasedStrategy is never reached and no
      image is swapped. Its src assertions are non-regression guards; what is
      actually red before the fix is the dropped Lcp patch and the leaked
      marker. Docblock now says so, and the swap stays pinned at unit level.

      Tests: 673 tests, 2030 assertions, OK.
      phpcs --standard=Magento2 on the touched files: 0 errors.

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

    • fix(image): keep the processed marker out of the DOM (#xss-14036)

      On a category page the first two product images were rendered with each
      other's <img> tag. Reproduced on mcprod.boutinot.com/wines.html: item 1
      links to CHV722 but shows CHV726, item 2 the reverse. Only the two images
      that LCP marks with fetchpriority="high" were affected, and the wrong
      output was then stored in the full page cache.

      markImageAsProcessed() set data-pagespeed-processed through the raw
      \DOMElement::setAttribute(), so the mutation was never recorded as a patch.
      That made the oldHtml of every later patch of the same node stale, so
      PatchGrouper could not chain it and the patch fell through to the fuzzy
      replacement strategies. AttributeBasedStrategy then matched by
      class="product-image-photo" - shared by every product image - and rewrote
      the first product image with the second product's HTML.

      Keep the marker in an SplObjectStorage registry on TrackedDomDocument
      instead of in the DOM, and stop AttributeBasedStrategy from matching by a
      class that several elements share. A patch that fails to apply only loses
      an optimization; a patch applied to the wrong element corrupts the page.

      Side effect: data-pagespeed-processed no longer appears in page output.
      Nothing in JS, templates or tests reads it.

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

breezefront / theme-frontend-breeze-enterprise-apollo

2 days ago success
  • head
    • Firefox: Fixed horizontal scrollbar inside search slideout 613e59

  • 3.2.0
    • Version 3.2.0 4c4818

    • Per page limits according to design e54d9e

    • Fixed empty search slideout in Safari browser after closing keyboard cb1f77

    • Show testimonials block under category decsription, see #14 b30944

    • Theme Editor: Allow using gradients as bg color 3cb9ff

    • Improve recent products widget styles in sidebar 3d94fb

    • Revert "Move testimonials below category description"

      This reverts commit 046f5bb650b031b250b05a0009b5ec2edc27a619. 66ab0d

    • Move testimonials below category description 046f5b

    • Use `apollo-` prefix for custom quote icon 7f3938

    • Prevent zoom om mobile devices when focusing quantity field 0de256

    • Limit upsell and related products to 6 items on the product page 94dd46

    • Added BLB installation: Testimonials widget on category pages 60339b

    • Testimonials and BCB slider styles bb8fa5

    • Fixed qty overlap with actions in minicart on small screens, see #16 96cd74

    • Enabled mobile slider layout for columns and grid 77cb58

    • Created separate mobile slider 8880b4

    • Sync products widget styles with product list. Closes #13 241874

swissup / module-ajaxsearch

2 days ago success
  • head
    • 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

breezefront / module-breeze-content-builder

3 days ago success

breezefront / breeze-enterprise

3 days ago success