Recent Activities
This page shows what are we working on.
-
head
-
Product page title font-size. d0987c
-
Footer. Minor fix for ring reval animation. Restore missing divider in footer. (#3) 54cdc4
-
Footer ring reveal animation added (#3) 4a8be8
-
BCB homepage: added animated badges to top banners grid 197e37
-
BCB homepage: added rotation animation for Full Width Banner 1420f9
-
BCB homepage: top banner updated a53bd5
-
Update favicon color 5ea666
-
Update favicon 18d8dc
-
Slightly update footer background. 5fcbfc
-
Footer content update. Add contact block (#3) dea42e
-
Fixed missing minicart overlay on first open (#2) 54feaa
-
BCB homepage: updated Browse categories block dbfa89
-
Breadcrumbs 196469
-
Done with search (#2) d8e0f3
-
Enhance minisearch functionality with slideout behavior and styling adjustments without Ajaxsearch (#2) 8c4a9d
-
Minisearch styles when ajaxsearch disabled... in progress (#2) 259fac
-
Remove unused styles from viewcart action and update empty subtitle styles in minicart (#2) dba7a5
-
Minicart complete (#2) 56efd2
-
Search on desktop (#2) 9c587e
-
BCB homepage: added videos section 7ee864
-
Minicart inprogress (only buttons to adjust)... (#2) c1be91
-
Minicart update in progress.... c3d18b
-
Minicart update in progress... (#2) 55e7b9
-
INstaller. Create dummy CMS block header_panel_info to allow user add custom content (#2) f70d58
-
Adjust minicart counter (#2) 03e843
-
BCB homepage: Added brand logos section b905f1
-
BCB homepage: Added testimonials section 2ab2cf
-
Update newsletter styles: adjust input border and add consent positioning (#2) 02d37d
-
Header. Update search behaviou and look of the result dropown. 993432
-
BCB homepage: Added FAQ a0bfde
-
Add focused search styles. d80f88
-
BCB homepage: added new sections 875068
-
BCB homepage: added ECI and full-width banner d40cfa
-
Top navigation added. 8fa9a3
-
Remove color from menu links dec081
-
Clone currency and store switchers in header slideout menu for improved functionality de36e3
-
Enhance header and newsletter styles for improved layout and responsiveness in header slideout menu. b63dd0
-
Installer. Update header slideout menu styles. 825795
-
Update footer top content HTML to enhance newsletter section styling.
Don't set font familiy for header in Page Builder ba0b81 -
BCB homepage WIP b228af
-
Basic navpro slideout styles 75ac0e
-
Refactor header slideout menu and newsletter styles for improved layout and consistency 3b42ff
-
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
-
-
1.0.15
-
Version 1.0.15 712f0f
-
Version 1.0.15 393497
-
feat(auth): make the GraphQL Bearer token header configurable (#31)
On sites behind HTTP Basic Auth the Theme Editor was unusable: the browser asked
for the Basic Auth password over and over, and no correct password stopped it.
The admin UI authenticates its GraphQL calls with `Authorization: Bearer
<token>`. The web server consumes that header first, tries to read it as Basic
credentials, fails, and answers `401 WWW-Authenticate: Basic` before Magento
runs. Every XHR then triggers a native password prompt. A single Authorization
header cannot carry both Basic credentials and a Bearer token, so this cannot be
fixed on the client alone.
Adds `breeze_theme_editor/general/auth_header`, default `Authorization` — no
behaviour change for existing installs. When set to a custom name (e.g.
`X-Bte-Authorization`), the admin JS sends the token there and drops
`Authorization`, and a graphql-area plugin puts the value back into
`Authorization` right before TokenUserContext reads it.
Two details the implementation depends on:
- TokenUserContext receives the shared `App\Request\Http` instance despite its
`Webapi\Request` type hint. `Webapi\Request` is not shared and gets no
interceptor generated at all, and `$_SERVER` is snapshotted at bootstrap, so
the shared request object is the only thing worth mutating.
- Such servers forward their own `Authorization: Basic ...` to PHP after
authenticating the browser, so a non-Bearer value is replaced rather than
skipped. Skipping left the admin unauthenticated.
Header names that cannot carry the token — the ones this client sets itself
(Content-Type, X-Requested-With, Store) and the fetch spec's forbidden request
headers — fall back to `Authorization` instead of silently breaking auth.
Authentication is unchanged: same JWT, same core validator, same ACL plugin on
every resolver. Basic Auth is not bypassed — requests without Basic credentials
still get 401.
Documents both fixes in the README, including a server-side alternative scoped
and anchored to the exact GraphQL endpoint (an unanchored rule would let any
request with an arbitrary Bearer value bypass Basic Auth site-wide). The nginx
recipe is verified against nginx 1.24 with Basic Auth enabled; the Apache
variants are documented as untested.
Verified end to end against a local nginx with auth_basic enabled, and through
the real admin UI in Chrome. Tests: PHPUnit 898, Jest 992.
Closes #31 fbd6bd -
fix(auth): anchor the Basic Auth exception to the exact endpoint (#31)
Addresses the third review on #32.
The documented rules were prefix matches, so they widened the bypass beyond the
endpoint they were meant to scope. Apache <Location> matches by prefix, and both
the <If> condition and the nginx map used unanchored patterns, so a route such
as /graphql-admin could opt out of Basic Auth with an arbitrary Bearer value.
The vhost form now uses <LocationMatch "^/graphql$">, the .htaccess <If> and the
nginx map are anchored, and the nginx map is case-sensitive with an explicit
branch for the query string ($request_uri carries it).
Re-verified against nginx 1.24 with Basic Auth enabled:
/graphql, /graphql?x=1 with a valid token reach Magento, authenticated
/graphql-admin, /GraphQL, /, and /graphql
without an auth header Basic challenge, not bypassed
The client's 401 snippet had the same problem plus an unescaped path: a base
path containing regex metacharacters (/shop.v2/graphql) widened the generated
rule further. The path is now escaped and the pattern anchored. The message also
hardcoded X-Bte-Authorization, which misreports the header when a different name
is configured; it interpolates the configured one.
Not changed: the review also suggested reserving Permissions-Policy in the
header denylist. That is a response header — it is not in the fetch spec's
forbidden request headers, so scripts can set it and it would work. Adding it
would reject a usable value.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 3ced18 -
fix(auth): correct the nginx recipe and stop assuming /graphql (#31)
Addresses the second review on #32.
The nginx workaround did not work. Magento's routing internally redirects
/graphql to index.php, the redirected request re-enters the PHP location, and
that location inherits the server-level auth_basic — so scoping the exception to
`location = /graphql` changed nothing and the request still got a Basic
challenge. Verified locally: the previous recipe returns 401.
The realm is now decided at server level from $request_uri, which survives the
internal redirect, via three small maps. Verified against nginx 1.24: /graphql
with a Bearer header reaches Magento (200 with a valid token), while the site
root with the same header and /graphql without it both still get the Basic
challenge, so the site stays protected.
The endpoint path is also no longer assumed to be /graphql. It is built from the
store base URL, so a subdirectory install answers on /shop/graphql and both the
documented rules and the snippet printed on a 401 silently did nothing there.
The client now derives the real path from the configured endpoint and prints it,
and the README says to substitute it.
Finally, the browser test runner asserted headers['Authorization'] directly,
which fails on an editor page configured with a custom header even though the
client behaves correctly. It now asks ConfigManager which header to check.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 136c2d -
fix(auth): scope the Basic Auth exception and reject unusable headers (#31)
Addresses the review on #32.
The documented server-side alternative was unscoped. Apache and nginx can only
check that the header starts with "Bearer", not that the token is valid, so the
rule as written let anyone bypass Basic Auth on every route by sending an
arbitrary Bearer value. Both recipes are now scoped to /graphql, with a warning
that explains why and notes that /graphql is a public API in Magento. The same
scoping is applied to the snippet the client prints on a 401.
Header validation accepted names that cannot carry the token: the client sets
Content-Type, X-Requested-With and Store itself and would overwrite them, and
browsers refuse to let scripts set forbidden request headers such as Cookie,
Host and Origin. Both groups now fall back to Authorization instead of silently
breaking authentication, as do the Proxy- and Sec- prefixes.
The 401 message also misdiagnosed the failure once a custom header was already
configured: a Basic challenge then means the browser sent no valid Basic
credentials, not that the token was intercepted. It now branches on the
configured header and says so.
Verified against the running site: with the header set to Content-Type or
Cookie, the custom header is ignored and Authorization keeps working.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 71d926 -
feat(auth): make the GraphQL Bearer token header configurable (#31)
On sites behind HTTP Basic Auth the Theme Editor was unusable: the browser
asked for the Basic Auth password over and over, and no correct password
stopped it.
The admin UI authenticates its GraphQL calls with `Authorization: Bearer
<token>`. The web server consumes that header first, tries to read it as Basic
credentials, fails, and answers `401 WWW-Authenticate: Basic` before Magento
runs. Every XHR then triggers a native password prompt. A single Authorization
header cannot carry both Basic credentials and a Bearer token, so this cannot
be fixed on the client alone.
Add `breeze_theme_editor/general/auth_header`, default `Authorization` — no
behaviour change for existing installs. When set to a custom name (e.g.
`X-Bte-Authorization`), the admin JS sends the token there and drops
`Authorization`, and a graphql-area plugin puts the value back into
`Authorization` right before TokenUserContext reads it.
Two details the implementation depends on:
- TokenUserContext receives the shared `App\Request\Http` instance despite its
`Webapi\Request` type hint. `Webapi\Request` is not shared and cannot be
plugged into at all, and `$_SERVER` is snapshotted at bootstrap, so the
shared request object is the only thing worth mutating.
- Such servers forward their own `Authorization: Basic ...` to PHP after
authenticating the browser. Skipping when the header is present would leave
those credentials for TokenUserContext to choke on, so a non-Bearer value is
replaced.
Authentication is unchanged: same JWT, same core validator, same ACL plugin on
every resolver. Basic Auth is not bypassed — requests without Basic credentials
still get 401.
Verified against a local nginx with auth_basic enabled:
config=Authorization
basic creds + Bearer in Authorization 401 nginx Basic prompt (the bug)
basic creds + Bearer in X-Bte-Authorization 403 Magento denied
config=X-Bte-Authorization
basic creds + Bearer in X-Bte-Authorization 200 authenticated
no basic creds 401 nginx Basic prompt
Also documents both fixes in the README and in the 401 error the client shows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 47bbfb
-
-
1.0.14
-
head
-
Do not add dom duplicates to $.registry 84beb6
-
-
2.32.0
-
Version 2.32.0 62d26e
-
Remove unused `lazyAsync` function 98e9d8
-
Gallery: Fixes slider fallback for mobile devices for `expanded` option eb5ff6
-
Center dots when using expanded layout (on mobile) 88f78f
-
Gallery: Restore proper tabindex in destructor 19bb81
-
Gallery: Use slider fallback for mobile devices for `expanded` option 7847d1
-
Improved slider destructor to cleanup listeners and markup dd8400
-
Remove unused code 6292c3
-
Do not close accordion when multipleCollapsible is used b1b4a7
-
LazyAsync 9e0a37
-
Removed redundant 'contentUpdated' dead1b
-
Faster DOM traversal after contentUpdated event 9c2d61
-
Simplify quotedScope from prev commit 37a7b7
-
Improve scope binding match logic
minicart_content should not match x_minicart_content;
minicart.content should not match minicart_content 5e9bca -
Speculation rules: fixed not working exclude rules c78263
-
Added missing destructor to pagebuilderCarousel 81d228
-
Do not close dropdownDialog when dragging the slider inside eed793
-
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
-
-
head
-
Added Additional HTML prop for Banner component to store animations 7941f0
-
Hover zoom only image/background, keep text content static 27547f
-
Declare the modules BCB actually needs a4923d
-
Fixed composer.json 975c9b
-
Added magento/module-ui dependency
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> 154d8a -
fix(composer): declare the modules BCB actually needs (breezefront/breeze-content-builder#1)
The metapackage does not need `swissup/module-core` — it arrives
transitively through this module, exactly as in breezefront/breeze-ai#1.
What the metapackage cannot fix is a module that under-declares its own
dependencies, which is what breaks an install on a store that does not
happen to ship every Magento module. Seven of the modules whose classes
this one injects or instantiates were missing from `require`:
- magento/module-catalog — `Block/Adminhtml/Product/Widget/Chooser`
extends `Magento\Catalog\Block\Adminhtml\Product\Widget\Chooser`, and
`FeaturedProductRenderer` injects `ProductRepositoryInterface`,
`Helper\Image` and `Helper\Output`.
- magento/module-widget — `Widget\Helper\Conditions`
(`ProductGridRenderer`), `Widget\Model\Widget` and
`Widget\Model\Widget\Config` (`AdminToolbarPlugin`).
- magento/module-swatches — `FeaturedProductRenderer` promotes
`Swatches\ViewModel\Product\Renderer\Configurable` as a constructor
property, so DI compilation needs it present.
- magento/module-catalog-inventory — `FallbackProductProvider` injects
`CatalogInventory\Helper\Stock`.
- magento/module-variable — `AdminToolbarPlugin` injects
`Variable\Model\Variable\Config`.
- magento/module-customer — `BcbContentRenderer` reads
`Customer\Model\Context::CONTEXT_GROUP` for its cache key.
- magento/module-configurable-product — `FeaturedProductRenderer` reads
`Configurable::TYPE_CODE` with no guard.
- magento/module-newsletter — `NewsletterRenderer` creates
`Newsletter\Block\Subscribe` with no guard; nothing catches the failure.
- magento/module-theme — already sequenced in `etc/module.xml`; it was
the only entry there absent from `require`.
Also tightened:
- `php: "^8.1"` added. Promoted `readonly` properties are used throughout
(`BcbContentRenderer`, every controller, the renderers), so 8.1 is the
real floor. Matches `swissup/module-breeze` and
`swissup/module-breeze-theme-editor`.
- `magento/framework: "*"` → `"^103.0"`. `"*"` guarded nothing; 103.0 is
the 2.4 line, and it is what `swissup/module-breeze-theme-editor` —
already required here — asks for.
Left as is, on purpose:
- `swissup/module-core: "^1.13.1"` — already the tightest constraint in
the family (`module-breeze` and `module-breeze-theme-editor` ask for
`^1.12.27`) and 1.13.1 exists, so it resolves. Not widened.
- `magento/module-review` and `swissup/module-marketplace` are soft:
`FeaturedProductRenderer` guards the review block with `class_exists()`
and `Installer/Command/BcbCmsPage` only names
`Swissup\Marketplace\Installer\Request` in a docblock. Moved to
`suggest` rather than `require`.
- `swissup/module-breeze` stays out. The only coupling is
`view/frontend/layout/breeze_default.xml`, a handle Magento loads only
when Breeze is installed; `cms_page_view.xml` carries the rendering on
any theme.
- `magento/module-page-builder` and `magento/module-ui` appear in
comments only.
- The remaining `magento/module-*: "*"` constraints are untouched — no
version floor is known to be wrong, and inventing one would be a
narrower claim than the code supports.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 2dca6d -
Merge pull request #36 from breezefront/fix/widget-description-for-ai
docs(components): stop telling the AI a known widget directive is invented 45a7e6 -
docs(components): stop telling the AI a known widget directive is invented
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 3d1aa4
-
-
1.2.0
-
1.6.0
-
chore(release): 1.6.0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> aa4f21
-
-
1.5.1
-
chore(release): 1.5.1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 58c7d9 -
fix(content-builder): refuse an ambiguous widget class, keep repairing past a scalar (#91)
(cherry picked from commit 0780424f1b73d597798af9a4fc49874420870240) d1465f -
fix(content-builder): key the widget catalog by id (#91)
(cherry picked from commit daf86df827fbabebaeafb0f502bdfa2245b132d8) 8dba93 -
fix(content-builder): repair a stray backslash in the reply (#91)
(cherry picked from commit c3b34c496bf6c163120f74142696a55a96704687) 51abb3 -
fix(content-builder): name a widget by its id, not its block class (#91)
(cherry picked from commit 25d60e8d621ec2ca691c9501668f68d59a207f6a) 4aaba4 -
fix(content-builder): refuse an ambiguous widget class, keep repairing past a scalar (#91) 078042
-
fix(content-builder): key the widget catalog by id (#91) daf86d
-
fix(content-builder): repair a stray backslash in the reply (#91) c3b34c
-
fix(content-builder): name a widget by its id, not its block class (#91) 25d60e
-
fix(content-builder): refuse an ambiguous widget class, keep repairing past a scalar (#91) 692be1
-
fix(content-builder): key the widget catalog by id (#91) 097887
-
fix(content-builder): repair a stray backslash in the reply (#91) c17744
-
fix(content-builder): name a widget by its id, not its block class (#91) 9a0ea1
-
fix(content-builder): match the type spelling the parser accepts (#91) b4085c
-
fix(content-builder): normalize widget types before parsing (#91) 027158
-
fix(image): call OpenAI's image endpoint directly, once (#93) (#102)
* fix(image): call OpenAI's image endpoint directly, once (#93)
The SDK could not be used here, and it was costing money rather than elegance.
Composer allows `^0.10 || ^0.19`; PHP 8.1 — which this module supports — can only
resolve 0.10, and 0.10 types this response for the pre-`gpt-image-1` API, which
always carried a URL. So on a supported platform the call reached the paid
endpoint and then threw while hydrating what came back. The HTTP fallback added
behind that throw made it worse: two paid calls for one picture, and only one of
them in the ledger.
One endpoint and one JSON field is all this ever needed. Plain Guzzle through the
ClientFactory that is already injected: one code path on every supported PHP
version, one call per attempt, and the timeout still comes off the request.
The error mapping is read off the response status rather than out of the message
now that there is a response to read — 401/403 as the key, 429 as the rate limit,
400 as a rejected request naming the model and quality, and the body's
`error.message` for anything else, which beats Guzzle's truncated summary. A
refusal stays a plain LocalizedException so the caller does not count it against
the spend cap; only a 200 that carried no usable image is an ImageResponseException,
which is the charge that did happen.
Verified against the real endpoint with an invalid key: one HTTP call, and
"OpenAI: invalid image API key. Check configuration."
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(image): count a response timeout as a charge that may have happened (#93)
ConnectException does not mean the request never went out. cURL reports a
connect timeout and a response timeout as the same errno 28, and Guzzle files
both under the same class as a resolution failure — but a response timeout means
the request was delivered, and OpenAI finishes the picture and bills for it
whether or not we are still listening. An image takes 10-30 seconds against a
default budget of 60, so this is an ordinary outcome rather than a corner.
Told apart by cURL's own handler context: connect_time stays zero until a
connection exists, so a timeout with a connect time behind it had its request on
the wire. A resolution, connection or TLS error is never a charge whatever the
timings say, and a timeout with no context at all — a handler other than cURL —
is an open question.
Open questions are answered towards "billed": over-reporting a rare timeout
costs a cap slot and a ledger line that may not match a charge, while
under-reporting hides real spend, which is the failure this class was rewritten
to stop. The message says which of the two happened, so nobody reads the line as
a confirmed picture.
Reported by Copilot on PR #102.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(image): decide a failed image call by bytes sent, not by timings (#93)
Two ways the previous classification still said "unbilled" after the request had
gone out.
libcurl reports `connect_time` as zero when it reuses a keep-alive connection,
and a page generating three images reuses the connection for the second and
third — so a response timeout on exactly the calls most likely to be billed was
read as a connect timeout and dropped from the ledger and the cap.
`CURLE_GOT_NOTHING` is a server that accepted the request and closed without
answering. Guzzle files it as a connection error like any other, and it is
post-send by definition, so it was being dropped too.
Decided on `request_size` now, which is the only direct evidence: anything above
zero means the request left this machine. Resolution, connection and TLS errors
never wrote a byte; GOT_NOTHING always did; a timeout is post-send unless the
byte counts are present and read zero; and an absent context stays an open
question, answered the conservative way. Timings are not consulted at all.
Reported by Copilot on PR #102, second pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com> 4c3b03 -
fix(image): decide a failed image call by bytes sent, not by timings (#93)
Two ways the previous classification still said "unbilled" after the request had
gone out.
libcurl reports `connect_time` as zero when it reuses a keep-alive connection,
and a page generating three images reuses the connection for the second and
third — so a response timeout on exactly the calls most likely to be billed was
read as a connect timeout and dropped from the ledger and the cap.
`CURLE_GOT_NOTHING` is a server that accepted the request and closed without
answering. Guzzle files it as a connection error like any other, and it is
post-send by definition, so it was being dropped too.
Decided on `request_size` now, which is the only direct evidence: anything above
zero means the request left this machine. Resolution, connection and TLS errors
never wrote a byte; GOT_NOTHING always did; a timeout is post-send unless the
byte counts are present and read zero; and an absent context stays an open
question, answered the conservative way. Timings are not consulted at all.
Reported by Copilot on PR #102, second pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 7190d2 -
fix(content-builder): read a directive backslash as a separator (#91) da1323
-
fix(image): count a response timeout as a charge that may have happened (#93)
ConnectException does not mean the request never went out. cURL reports a
connect timeout and a response timeout as the same errno 28, and Guzzle files
both under the same class as a resolution failure — but a response timeout means
the request was delivered, and OpenAI finishes the picture and bills for it
whether or not we are still listening. An image takes 10-30 seconds against a
default budget of 60, so this is an ordinary outcome rather than a corner.
Told apart by cURL's own handler context: connect_time stays zero until a
connection exists, so a timeout with a connect time behind it had its request on
the wire. A resolution, connection or TLS error is never a charge whatever the
timings say, and a timeout with no context at all — a handler other than cURL —
is an open question.
Open questions are answered towards "billed": over-reporting a rare timeout
costs a cap slot and a ledger line that may not match a charge, while
under-reporting hides real spend, which is the failure this class was rewritten
to stop. The message says which of the two happened, so nobody reads the line as
a confirmed picture.
Reported by Copilot on PR #102.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 87ab21 -
fix(image): call OpenAI's image endpoint directly, once (#93)
The SDK could not be used here, and it was costing money rather than elegance.
Composer allows `^0.10 || ^0.19`; PHP 8.1 — which this module supports — can only
resolve 0.10, and 0.10 types this response for the pre-`gpt-image-1` API, which
always carried a URL. So on a supported platform the call reached the paid
endpoint and then threw while hydrating what came back. The HTTP fallback added
behind that throw made it worse: two paid calls for one picture, and only one of
them in the ledger.
One endpoint and one JSON field is all this ever needed. Plain Guzzle through the
ClientFactory that is already injected: one code path on every supported PHP
version, one call per attempt, and the timeout still comes off the request.
The error mapping is read off the response status rather than out of the message
now that there is a response to read — 401/403 as the key, 429 as the rate limit,
400 as a rejected request naming the model and quality, and the body's
`error.message` for anything else, which beats Guzzle's truncated summary. A
refusal stays a plain LocalizedException so the caller does not count it against
the spend cap; only a 200 that carried no usable image is an ImageResponseException,
which is the charge that did happen.
Verified against the real endpoint with an invalid key: one HTTP call, and
"OpenAI: invalid image API key. Check configuration."
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 621e5b -
test(content-builder): cover the escape repair edges (#91) 5e8089
-
test(content-builder): pin the escape error the repair keeps (#91) ed702d
-
feat(content-builder): generate an image where the design has only a placeholder (#93) (#100)
* feat(image): tell a wireframe stand-in from a flat design (#93)
PlaceholderDetector cannot be the trigger for generating a picture, and the
reason is what its answer costs. There, a false positive keeps the
dummyimage.com URL the build already used; in front of generation, a false
positive paints an invention over a band a designer meant to be flat — a
brand-coloured hero, a dark call-to-action strip — and bills for it. So the
question has to be a different one, asked of positive evidence: not "is this
crop empty" but "is this a wireframe at all".
Answered of the whole screenshot, once per build, because a grey box in a mock
and a grey box in a design are indistinguishable at the size of one crop. Two
signals have to agree: chroma, which a design's photography or brand colour
pushes up, and luminance entropy, which is what stops a black-and-white
photographic design being read as a mock. A crop's own drawn marks — a cross,
an outlined frame — are the second way in, for the design that is finished
apart from the one band still holding a box.
Measured against real files: this module's five-band mock lands at chroma 0.014
and entropy 1.9, a storefront screenshot with photography at chroma 0.32, a
photograph at entropy 5.1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(content-builder): draw the picture a wireframe left a box for (#93)
The other half of issue #85. Extraction answers a screenshot of a real design
and answers nothing at all for a wireframe, where the region genuinely is a grey
box and the merchant replaces the placeholder by hand.
Second, not first, and the order is the point: extraction is free and exact,
while one generated image costs $0.01 to $0.25 against about $0.03 for the whole
text build that placed it. So generation runs only where extraction has already
declined, and only where a prop passes every gate — the setting is on, the prop
is a band's main image rather than an icon, the build call itself described a
missing picture there, and WireframeDetector agrees the reference is a stand-in.
Off by default, with a hard per-build cap that is a spend limit rather than a
tidiness one, hence no "unlimited" at zero.
The descriptions ride back on the build call like the regions do, so this costs
no request of its own. An image model is configured on its own — Claude
generates no images, so the text model says nothing about who can draw one — and
lives in its own pool rather than as a row that could become the default a chat
call routes to.
Nothing here can cost the build: a refusal, a timeout, a missing key, a full
disk or the cap each leave the dummyimage.com placeholder in place and say so in
the build note, one note per prop rather than two contradicting each other.
Every call that answered is written to the usage ledger as kind=image, priced
through the same table the admin's pre-build estimate is priced through, so a
page's cost is complete exactly where it is largest.
The crop is deliberately not sent as a reference image. Under these gates it is
the wireframe's own grey box: nothing to imitate, and billed as input. Its shape
is used instead, to pick the size.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(admin): say what a build may spend on images before it starts (#93)
A ceiling, not a forecast, and it cannot be otherwise: how many pictures a page
needs is only known once the build call has answered, since the descriptions
ride back on it. So the modal and the editor's chat panel show the most the run
can cost — the cap times the rate — which is the number the cap exists to make
true. Priced through the same ImagePrice the ledger writes, so the estimate and
the invoice cannot disagree.
One sentence, composed once and shared by both controls, which compose their own
config JSON and would otherwise drift apart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(content-builder): a free image rate is not an unknown one (#93)
The page total read "cost is not known" off the numeric total rather than off
whether a rate was found, so a store on a zero per-image rate — an internal
proxy, a plan with images included, the override set to 0, all of which
Helper\Config deliberately accepts — was told its pictures cost an unknown
amount instead of nothing. Gate the wording on costKnown, which is the thing
that actually tracks it.
While there: an amount under a cent is written with four places rather than two
in both the build note and the pre-build estimate. Two writes $0.0040 as $0.00,
which in this line reads as "the picture was free" — the same claim Usage\Cost
keeps its own precision to avoid.
Reported by Copilot on PR #100.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(image): recognise OpenAI's key and rate-limit errors by their wording (#93)
A live run against the real endpoint with a deliberately bad key came back as
"OpenAI image error: Incorrect API key provided: sk-…", not as the intended
"invalid image API key. Check configuration." The SDK raises its own exception
carrying the API's message body and nothing else, so a check for '401' never
matches — the one failure an admin can act on was reported as a generic error.
Match the wording as well as the status, and the same for the rate limit.
Model\Provider\OpenAi::handleException() has the identical blind spot for chat
calls. Left alone here: it is not this issue's code, and the fix belongs with a
run that observes it the same way this one was observed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(content-builder): stop telling the model a stand-in is already handled (#93)
A live run explained the silence. With generation on, every gate open and the
description rules in the prompt, a small model returned no image_prompts at all
on a wireframe — because the region rules, which sit closer to the coordinates
it was writing, ended with "leave those props out of image_regions and let the
placeholder rule above fill them". A model that believes a prop is taken care of
has no reason to describe it.
So that clause now has two versions and the build picks by whether anything will
draw the picture: point at the placeholder when nothing will, and at
image_prompts when something will, naming the consequence of saying nothing. The
ask itself is stated as a requirement rather than as an offer for the same
reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(image): address 5 Copilot review issues on PR #100 (#93)
- Gemini: catch ConnectException before it bypasses handleException and
leaks ?key= credential in server log (ConnectException is not a
RequestException subclass)
- WireframeDetector: convert palette/indexed images to truecolor before
per-pixel sampling; imagecolorat() returns a palette index on GIF and
indexed PNG, not packed RGB, so a coloured palette image was read as
achromatic and misclassified as a wireframe
- PromptBuilder: REGION_STAND_IN_RULE_GENERATING now asks the model to
still report image_regions for stand-in props (for shape/mark
detection) and describe them in image_prompts — the previous wording
told the model to omit the region, making the crop-marks gate
unreachable for isolated placeholders in finished designs
- ImageExtractor: take a crop in memory when extraction is off but
generation is on and the page is not already a wireframe, so the
crop-marks gate works in generation-only mode; skip the crop when the
wireframe verdict already covers the whole page
- ImageExtractor/ImageGenerator: track billable API attempts separately
from successful saves; a storage failure after the provider answered
no longer reopens a generation slot above the admin-configured cap
* fix(test): replace assert() with assertNotFalse() — assert() forbidden by Magento CS (#93)
* fix(image): address 4 more Copilot review issues on PR #100 (#93)
- ImageResponseException: new exception subclass for 'provider answered
but no image' (HTTP 200, refusal, bad base64) — distinct from
LocalizedException so ImageGenerator can count it as billed without
treating every auth rejection and rate-limit error as billable too
- ImageGenerator: catch ImageResponseException as billed (writeImage +
billed=true); catch everything else as not-billed (provider never
reached); mb_substr/mb_strlen for multibyte prompt truncation so a
multilingual description is not split mid-codepoint
- PromptBuilder: IMAGE_PROMPT_RULES last item rewritten — stand-ins get
both image_regions (shape/mark detection) AND image_prompts
(description); the 'never both' rule was directly contradicting the
REGION_STAND_IN_RULE_GENERATING that this PR introduced
- ImageExtractor: add 'placeholder' flag to attempt struct so
reportUnfilled() can distinguish 'PlaceholderDetector rejected this
crop' from 'cropBytes() took a crop for stand-in detection only';
the old attempt['crop'] !== null check was true for both and falsely
told the admin the reference showed a placeholder on any non-wireframe
page where generation declined for a different reason
* fix(openai): HTTP fallback for PHP 8.1 + openai-php 0.10 (#93)
openai-php 0.10 (the only version PHP 8.1 can install) throws a
TypeError when hydrating gpt-image-1's base64 response. The paid
API call was wasted. Bypass the SDK and call the REST endpoint
directly via Guzzle so the call still produces an image.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com> c6926a -
fix(openai): HTTP fallback for PHP 8.1 + openai-php 0.10 (#93)
openai-php 0.10 (the only version PHP 8.1 can install) throws a
TypeError when hydrating gpt-image-1's base64 response. The paid
API call was wasted. Bypass the SDK and call the REST endpoint
directly via Guzzle so the call still produces an image. d675d0 -
fix(image): address 4 more Copilot review issues on PR #100 (#93)
- ImageResponseException: new exception subclass for 'provider answered
but no image' (HTTP 200, refusal, bad base64) — distinct from
LocalizedException so ImageGenerator can count it as billed without
treating every auth rejection and rate-limit error as billable too
- ImageGenerator: catch ImageResponseException as billed (writeImage +
billed=true); catch everything else as not-billed (provider never
reached); mb_substr/mb_strlen for multibyte prompt truncation so a
multilingual description is not split mid-codepoint
- PromptBuilder: IMAGE_PROMPT_RULES last item rewritten — stand-ins get
both image_regions (shape/mark detection) AND image_prompts
(description); the 'never both' rule was directly contradicting the
REGION_STAND_IN_RULE_GENERATING that this PR introduced
- ImageExtractor: add 'placeholder' flag to attempt struct so
reportUnfilled() can distinguish 'PlaceholderDetector rejected this
crop' from 'cropBytes() took a crop for stand-in detection only';
the old attempt['crop'] !== null check was true for both and falsely
told the admin the reference showed a placeholder on any non-wireframe
page where generation declined for a different reason 362f3d -
fix(test): replace assert() with assertNotFalse() — assert() forbidden by Magento CS (#93) 4ad84b
-
fix(image): address 5 Copilot review issues on PR #100 (#93)
- Gemini: catch ConnectException before it bypasses handleException and
leaks ?key= credential in server log (ConnectException is not a
RequestException subclass)
- WireframeDetector: convert palette/indexed images to truecolor before
per-pixel sampling; imagecolorat() returns a palette index on GIF and
indexed PNG, not packed RGB, so a coloured palette image was read as
achromatic and misclassified as a wireframe
- PromptBuilder: REGION_STAND_IN_RULE_GENERATING now asks the model to
still report image_regions for stand-in props (for shape/mark
detection) and describe them in image_prompts — the previous wording
told the model to omit the region, making the crop-marks gate
unreachable for isolated placeholders in finished designs
- ImageExtractor: take a crop in memory when extraction is off but
generation is on and the page is not already a wireframe, so the
crop-marks gate works in generation-only mode; skip the crop when the
wireframe verdict already covers the whole page
- ImageExtractor/ImageGenerator: track billable API attempts separately
from successful saves; a storage failure after the provider answered
no longer reopens a generation slot above the admin-configured cap 403bca -
fix(content-builder): survive an unescaped widget type (#91) 2df025
-
fix(content-builder): stop telling the model a stand-in is already handled (#93)
A live run explained the silence. With generation on, every gate open and the
description rules in the prompt, a small model returned no image_prompts at all
on a wireframe — because the region rules, which sit closer to the coordinates
it was writing, ended with "leave those props out of image_regions and let the
placeholder rule above fill them". A model that believes a prop is taken care of
has no reason to describe it.
So that clause now has two versions and the build picks by whether anything will
draw the picture: point at the placeholder when nothing will, and at
image_prompts when something will, naming the consequence of saying nothing. The
ask itself is stated as a requirement rather than as an offer for the same
reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> cc06d7 -
fix(image): recognise OpenAI's key and rate-limit errors by their wording (#93)
A live run against the real endpoint with a deliberately bad key came back as
"OpenAI image error: Incorrect API key provided: sk-…", not as the intended
"invalid image API key. Check configuration." The SDK raises its own exception
carrying the API's message body and nothing else, so a check for '401' never
matches — the one failure an admin can act on was reported as a generic error.
Match the wording as well as the status, and the same for the rate limit.
Model\Provider\OpenAi::handleException() has the identical blind spot for chat
calls. Left alone here: it is not this issue's code, and the fix belongs with a
run that observes it the same way this one was observed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> be718d -
fix(content-builder): a free image rate is not an unknown one (#93)
The page total read "cost is not known" off the numeric total rather than off
whether a rate was found, so a store on a zero per-image rate — an internal
proxy, a plan with images included, the override set to 0, all of which
Helper\Config deliberately accepts — was told its pictures cost an unknown
amount instead of nothing. Gate the wording on costKnown, which is the thing
that actually tracks it.
While there: an amount under a cent is written with four places rather than two
in both the build note and the pre-build estimate. Two writes $0.0040 as $0.00,
which in this line reads as "the picture was free" — the same claim Usage\Cost
keeps its own precision to avoid.
Reported by Copilot on PR #100.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 334c50 -
feat(admin): say what a build may spend on images before it starts (#93)
A ceiling, not a forecast, and it cannot be otherwise: how many pictures a page
needs is only known once the build call has answered, since the descriptions
ride back on it. So the modal and the editor's chat panel show the most the run
can cost — the cap times the rate — which is the number the cap exists to make
true. Priced through the same ImagePrice the ledger writes, so the estimate and
the invoice cannot disagree.
One sentence, composed once and shared by both controls, which compose their own
config JSON and would otherwise drift apart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> c676d0 -
feat(content-builder): draw the picture a wireframe left a box for (#93)
The other half of issue #85. Extraction answers a screenshot of a real design
and answers nothing at all for a wireframe, where the region genuinely is a grey
box and the merchant replaces the placeholder by hand.
Second, not first, and the order is the point: extraction is free and exact,
while one generated image costs $0.01 to $0.25 against about $0.03 for the whole
text build that placed it. So generation runs only where extraction has already
declined, and only where a prop passes every gate — the setting is on, the prop
is a band's main image rather than an icon, the build call itself described a
missing picture there, and WireframeDetector agrees the reference is a stand-in.
Off by default, with a hard per-build cap that is a spend limit rather than a
tidiness one, hence no "unlimited" at zero.
The descriptions ride back on the build call like the regions do, so this costs
no request of its own. An image model is configured on its own — Claude
generates no images, so the text model says nothing about who can draw one — and
lives in its own pool rather than as a row that could become the default a chat
call routes to.
Nothing here can cost the build: a refusal, a timeout, a missing key, a full
disk or the cap each leave the dummyimage.com placeholder in place and say so in
the build note, one note per prop rather than two contradicting each other.
Every call that answered is written to the usage ledger as kind=image, priced
through the same table the admin's pre-build estimate is priced through, so a
page's cost is complete exactly where it is largest.
The crop is deliberately not sent as a reference image. Under these gates it is
the wireframe's own grey box: nothing to imitate, and billed as input. Its shape
is used instead, to pick the size.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 83f19f -
feat(image): tell a wireframe stand-in from a flat design (#93)
PlaceholderDetector cannot be the trigger for generating a picture, and the
reason is what its answer costs. There, a false positive keeps the
dummyimage.com URL the build already used; in front of generation, a false
positive paints an invention over a band a designer meant to be flat — a
brand-coloured hero, a dark call-to-action strip — and bills for it. So the
question has to be a different one, asked of positive evidence: not "is this
crop empty" but "is this a wireframe at all".
Answered of the whole screenshot, once per build, because a grey box in a mock
and a grey box in a design are indistinguishable at the size of one crop. Two
signals have to agree: chroma, which a design's photography or brand colour
pushes up, and luminance entropy, which is what stops a black-and-white
photographic design being read as a mock. A crop's own drawn marks — a cross,
an outlined frame — are the second way in, for the design that is finished
apart from the one band still holding a box.
Measured against real files: this module's five-band mock lands at chroma 0.014
and entropy 1.9, a storefront screenshot with photography at chroma 0.32, a
photograph at entropy 5.1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> ac3cba
-
-
head
-
Fixed js error when `header-panel` is removed 036903
-
-
3.3.1
-
head
-
Typo fix 50a7b6
-
Cleanup CSS ea255a
-
Fix grid styles 4d48e9
-
Stop running composer from the backend (#17)
Running composer through the web server is not reliable, so the backend no
longer dispatches composer operations. Install, Update, Remove, Enable and
Disable now open a modal with the command to copy and run in a terminal, both
for a single row and for a massaction. Run Installer stays a real backend
operation - the one-click installer does not use composer.
Enable and Disable had no console equivalent, so marketplace:package:enable and
marketplace:package:disable were added (aliases marketplace:enable and
marketplace:disable).
Channel configuration and Tasks History are removed from the backend as well.
Channels are managed with the marketplace:channel:* and marketplace:auth:*
commands. With the settings form gone, ChannelsSave was the last job producer,
so the whole queue is removed: JobDispatcher, QueueDispatcher, Validator, the
Job entity, the activity widget, the queue crons and the job table.
The SettingsDataProvider modifiers are kept as empty stubs so that the channel
modules registering them in the pool keep compiling.
BREAKING: the swissup_marketplace_job table is dropped on setup:upgrade, and
HandlerInterface::validateBeforeDispatch() is removed - it had no callers left.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> cccd99 -
Ignore the packages with an invalid name
The channel metadata is not validated, and the package name ends up in
the command we suggest to copy and run. A name containing ;, $() or a
whitespace turns that command into something else.
Filtered where the channel data is read, so that such a package never
reaches the grid, the suggested command, or the installer request. The
pattern is the one composer validates the names with, so nothing that
composer is able to install is dropped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 956e42 -
Improve package disable/enable operations cc8491
-
Address review comments
- Massaction: require the row to expose the action before offering a
command. Local.php marks every downloaded package as enabled and only
overrides it for the modules, so a downloaded theme passed the shared
disable check even though Links.php never adds Enable/Disable to a
non-module row - marketplace:disable would then write a derived, bogus
module name to config.php.
- Copy button: writeText() is asynchronous and rejects when the clipboard
is blocked. Copy now returns a deferred, falls back to execCommand on
rejection, and the Copied label is shown only once a copy succeeded.
- README grammar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> f2fdaf -
Stop running composer from the backend
Running composer through the web server was never reliable, so the backend no
longer dispatches composer operations. Install, Update, Remove, Enable and
Disable now open a modal with the command to copy and run in a terminal, both
for a single row and for a massaction. Run Installer stays a real backend
operation - the one-click installer does not use composer.
Enable and Disable had no console equivalent, so marketplace:package:enable and
marketplace:package:disable were added (aliases marketplace:enable and
marketplace:disable).
Channel configuration and Tasks History are removed from the backend as well.
Channels are managed with the marketplace:channel:* and marketplace:auth:*
commands. With the settings form gone, ChannelsSave was the last job producer,
so the whole queue is removed: JobDispatcher, QueueDispatcher, Validator, the
Job entity, the activity widget, the queue crons and the job table.
The SettingsDataProvider modifiers are kept as empty stubs so that the channel
modules registering them in the pool keep compiling.
BREAKING: the swissup_marketplace_job table is dropped on setup:upgrade, and
HandlerInterface::validateBeforeDispatch() is removed - it had no callers left.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> d70a96 -
Forward command output strings to terminal c17318
-
Allow using dry-run option 2d329d
-
Reuse composer_cache from user's directory if possible 9cc10a
-
Command shortcuts 2cfa4c
-
-
1.11.0
-
head
-
1.0.3
-
3.3.2
-
3.3.1