Skip to content

Release 1.4.0 - #56

Merged
Drago37 merged 27 commits into
mainfrom
release/1.4.0
Sep 23, 2026
Merged

Drago37 merged 27 commits into
mainfrom
release/1.4.0

Conversation

@Drago37

@Drago37 Drago37 commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Release 1.4.0. Behaviour-preserving — no functional change for shoppers or the shop owner.

What's in it

  • Hexagonal / DDD refactor (issues Refactoriser le thème en architecture hexagonale et DDD #1, closed): bounded contexts under src/ (Shop core, Loyalty, Newsletter, OrderTracking) + a Shared kernel, three layers, pure inc/ business rules extracted into WordPress-free, unit-tested domain classes. An architecture test forbids WP/WC in any Domain/.
  • Dashboard test hardening (issue Sécuriser intégralement le tableau de pilotage par les tests #3, closed): schema-integrity e2e, systematic nonce/capability guard on admin actions, a deterministic performance e2e, and a Playwright navigation smoke — all in CI.
  • Security: CSV exports also neutralise leading tab/CR/LF (OWASP CSV-injection).
  • Tooling: make doctor for local role/cap repair.

⚠️ Deployment note — this is a large rename, not a targeted delta

The diff since 1.3.1 is 347 files (186 renames, 5 deletions) but no new Composer runtime dependency (vendor/ unchanged).

Do not use the usual targeted scripts/deploy-files.sh delta: it does not remove the old renamed paths (src/Pilotage/* would linger as orphans) and pushing ~280 files over FTPS risks an interrupted upload → a missing src/Shop class → fatal on every page (the 10 Sept incident).

Recommended: a full src/ mirror with --delete (removes the old src/Pilotage) + the changed inc//functions.php, then make verify-prod (SHA of every tracked theme file), OPcache reset, an uncached-URL smoke (/wp-login.php, /mon-compte/), and the make e2e-*-prod variants (isolated, no e-mail) as the final proof.

Do not merge until after a successful deployment (GitFlow: deploy from this open branch, then merge + tag on an explicit go-ahead).

Anthony Graule and others added 23 commits September 20, 2026 03:19
Introduce LuziApi\Support\Wp, a small stateless helper that converts the
mixed values returned by WordPress/WooCommerce (wpdb rows, get_meta(),
superglobals) into strict types without changing behaviour:

- int()/str() replace (int)/(string) casts on mixed scalars;
- row()/rows() normalise wpdb ARRAY_A results into array<string, mixed>;
- prepared() wraps wpdb::prepare(), centralising the single trusted
  internal-table-name literal-string exception and always returning a string.

This is the building block used to resorb the PHPStan baseline at level max.
Resolve the level-max errors in the Pilotage repositories, WooCommerce
adapters, service provider and history projector: wpdb prepare()/rows typed
through the Wp helper, WooCommerce getters guarded with instanceof, and honest
PHPDoc array shapes on the customer history projector. No behaviour change.
Resolve the level-max errors in the loyalty ledger and identity links, the
order-tracking access/session/history repositories and the tracking page
controller: wpdb prepare()/rows and superglobal reads typed through the Wp
helper, WP_Comment guarded with instanceof, honest PHPDoc on the loyalty view.
No behaviour change.
Resolve the level-max errors in the pilotage admin controllers and the shared
SurfacesActionErrors trait: superglobal ($_GET/$_POST) reads typed through the
Wp helper (absint kept where it clamped), the WooCommerce submenu access guarded
with is_array, wp_date() string|false results coerced, plus the mailer handler's
formatted record. No behaviour change.
Resolve the level-max errors in the inc/ procedural files and page.php: meta,
option and superglobal reads typed through the Wp helper, WooCommerce objects
(WC_Product, WC_Email, WC_Shipping_Rate, WC_DateTime) guarded with instanceof,
array<mixed> shipping/offer structures rebuilt via typed locals, and
wp_date()/wp_json_encode() string|false results coerced. No behaviour change.
The whole theme passes PHPStan level max, so the debt-freezing baseline is no
longer needed: delete phpstan-baseline.neon, remove its include from
phpstan.neon.dist and update CONTRIBUTING to state that max must stay green by
fixing the underlying type rather than adding a baseline or ignore.

Relates to: #13
Resorb the PHPStan baseline (theme clean at level max)
Purely structural rename, no behaviour change: the admin/order module
becomes the core "Shop" bounded context ahead of the hexagonal/DDD
migration of inc/ (issue #1).

- namespace LuziApi\Pilotage\ -> LuziApi\Shop\ across src/, inc/,
  functions.php and the 17 tools/e2e scripts
- src/Pilotage/ -> src/Shop/ and tests/Pilotage/ -> tests/Shop/
  (test namespace LuziApi\Tests\Pilotage -> LuziApi\Tests\Shop)
- composition-root class PilotageServiceProvider -> ShopServiceProvider

Admin-facing labels, Twig view paths (admin/pilotage/) and the
admin-pilotage.js asset are left unchanged. The internal layer reshape
(3 layers, Domain repositories/gateways, Shared kernel) and the
Loyalty->Shop cross-context leak fix are deferred to the issue #1 PR.

Gates green: PHP-CS-Fixer, PHPStan (max), PHPUnit (272 tests) and the
e2e suites (vente, vente+loyalty, loyalty, receipt, orders).

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
* docs(architecture): add ADR 0001 — hexagonal + CQRS conventions

Freezes the target architecture for the inc/ migration (issue #1),
aligned on the hellobees project: three bounded contexts (Shop core,
Loyalty, Newsletter) plus a Shared kernel; three layers only
(Domain/Application/Infrastructure, no UserInterface/Bootstrap folders,
composition root as a single ServiceProvider); CQRS handlers that return
their result (no presenter); typed aggregate ids; repository + Criteria +
Paginator in the domain; native exceptions via webmozart/assert; ACL
gateways for cross-context references.

* refactor(shared): unify the Clock port and adapter into a Shared kernel

Establishes src/Shared and removes real triplication: the three
per-context Clock ports (Shop, Loyalty, OrderTracking) and their three
WordPressClock adapters collapse into one Shared\Domain\Clock (now +
timezone, the superset) and one Shared\Infrastructure\WordPress\
WordPressClock. No behaviour change.

Loyalty/OrderTracking test doubles gain the timezone() method to satisfy
the superset interface. Gates green: CS-Fixer, PHPStan (max), PHPUnit
(272), e2e (receipt, loyalty, tracking).

* refactor(shared): promote the Money value object into the Shared kernel

Moves Money out of Shop/Domain/Shared (a "Shared" folder nested inside a
context) to the real kernel at Shared/Domain/ValueObject/Money, and its
test to tests/Shared. Pure move, no behaviour change.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (272), e2e (vente,
vente-volume).

* refactor(shared): absorb the Support helpers into the Shared kernel

Moves src/Support (the Wp WordPress/`$wpdb` typing helper used across
the infrastructure adapters, and WordPressMailerHandler) into
Shared/Infrastructure; the LuziApi\Support namespace disappears. Pure
move, no behaviour change.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (272), e2e
(customer-profile, receipt).

* refactor(shop): reshape to the three-layer layout

Aligns Shop with the ADR 0001 layout (Domain/Application/Infrastructure,
no UserInterface/Bootstrap folders):

- Application/Port/* (all system or cross-context gateways — the real
  aggregate repositories already live in Domain) -> Domain/Gateway/
- UserInterface/Admin/* -> Infrastructure/WordPress/Admin/
- Bootstrap/ShopServiceProvider -> Infrastructure/ShopServiceProvider

Domain keeps its per-subdomain module grouping (Sales, Customer,
Receipt, Inventory, Activity), a legitimate DDD organisation. Pure
structural move, no behaviour change.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (272), e2e (vente,
merge-admin, offered-pot, exclusion).

* refactor(shared): promote NormalizedPhone to the Shared kernel

Removes the last Loyalty -> Shop cross-context leak: LoyaltyIdentity
imported Shop\Domain\Customer\NormalizedPhone. The phone-normalisation
value object is generic identity material, so it moves to
Shared/Domain/ValueObject/NormalizedPhone; Shop and Loyalty now both
depend on the kernel (allowed) instead of Loyalty reaching into Shop.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (272), e2e
(customer-profile, identity-links, merge-admin).

* refactor(shop): extract the free-delivery zone rule into the domain

First inc/ business-rule migration (issue #1). The "free delivery =
France + 37150 + Bléré/Luzillé" rule moves out of inc/order-workflow.php
into Shop\Domain\Sales\DeliveryDestination, a WordPress-free value
object with self-contained city normalisation.

luziapi_is_local_delivery_destination becomes a thin WP adapter that
delegates to the value object; luziapi_normalize_city (used only
internally) is removed. No behaviour change.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (287, +15 new), e2e
(delivery-zone).

* refactor(shop): extract fulfillment-mode and order-source rules into the domain

Second inc/ business-rule migration (issue #1):
- FulfillmentMode: the shipping-method -> mode mapping (free_shipping =
  delivery, local_pickup = pickup) and the mode/intermediate-status
  consistency rule (out-for-delivery excludes pickup, ready-for-pickup
  excludes delivery).
- OrderSource: the sales-channel reference list and the source
  resolution (explicit source wins; legacy checkout/store-api orders
  read as "online").

Both are WordPress-free and keep returning the same strings, so every
caller (inc/, admin controllers, order repository, tracking gateway) is
untouched. The inc/order-workflow.php functions become thin WC adapters.
No behaviour change.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (308, +21 new), e2e
(orders workflow: delivery/pickup detection + source).

* refactor(shared): extract the French business-day calendar into the domain

Third inc/ business-rule migration (issue #1). The French public-holiday
computation (fixed days + Easter-based days), the business-day test and
the add-business-days arithmetic — used to compute the BACS payment
deadline and reminder — move from inc/payment-deadline.php into
Shared\Domain\BusinessCalendar, WordPress-free and reusable.

luziapi_add_business_days becomes a thin delegate; the internal
holiday/business-day helpers (no other callers) are removed. No
behaviour change.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (317, +9 new), e2e
(orders: BACS deadline scheduled on-hold, unscheduled once paid).

* refactor(shop): extract the order-status email content into the domain

Fourth inc/ business-rule migration (issue #1). The per-status customer
email content — message lines, step label and closing line — moves out
of the WC_Email subclass into Shop\Domain\Notification\
OrderStatusEmailContent, a WordPress-free value object built from
already-resolved context (order number, formatted deadline, cancellation
reason, pickup address).

The Luziapi_Order_Status_Email class keeps the WooCommerce wiring
(templates, hooks, sending, loyalty summary) and delegates the content
through a small email_content() helper. No behaviour change.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (324, +7 new), e2e
(orders workflow emails, vente + loyalty completed email).

* refactor(shop): extract legal-document versioning and withdrawal validation

Fifth inc/ business-rule migration (issue #1):
- LegalDocument: the versioned legal document (current version, label,
  immutable per-vintage PDF) and the PDF selection rule for the version
  a customer accepted (format check + fallback to the current version,
  so a dubious stored value never builds a file path). CGV now uses it.
- WithdrawalRequest: the withdrawal-form field validation and the scope
  label. Email-format validation stays with the WP host (is_email) and
  is passed in as a bool, keeping the value object WordPress-free.

The inc/commerce-legal.php functions become thin adapters. No behaviour
change.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (335, +11 new), e2e
(orders: CGV PDF attached to the first confirmation e-mail).

* refactor: guard the domain against WordPress and align CONTRIBUTING with ADR 0001

Closes the inc/ migration groundwork (issue #1):
- Architecture test: fails if any src/*/Domain/ file references WordPress
  or WooCommerce (wp_/wc_/$wpdb/get_option/hooks/WC_*/WP_*), locking in
  the framework-free domain established by this PR.
- CONTRIBUTING.md updated to the three-layer layout — no
  UserInterface/Bootstrap folders; inbound adapters and the
  composition-root ServiceProvider live in Infrastructure; ports live in
  Domain/Repository and Domain/Gateway; handlers return their result
  (no presenter) — and references ADR 0001. functions.php already boots
  each context's provider and stays minimal.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (336, +1 guard).

* refactor(shop): address check-pr findings (delivery parity, email guard, tests)

Following the automated review of the inc/ migration:
- DeliveryDestination now receives an already-normalised city; the
  WordPress normalisation (sanitize_text_field + remove_accents) moves
  back to the inc/ adapter, restoring exact parity with develop. The
  domain keeps only the FR + 37150 + Bléré/Luzillé rule, removing a
  theoretical normalisation divergence in a money-affecting rule.
- The order-status email drops the function_exists guard around the
  pickup address, restoring the original fail-loud behaviour.
- Tests: payment_reminder / processing / out_for_delivery email
  branches; a second Easter year (2027) asserting Ascension and
  Pentecost Monday; a negative add-business-days case; a positive
  control for the domain guard (minimum files scanned + the pattern
  matches known WordPress tokens); and the required-field error when the
  email is empty.

No behaviour change. Gates green: CS-Fixer, PHPStan (max), PHPUnit (337),
e2e (delivery-zone, orders, vente-loyalty).

---------

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
…yer layout (#44)

Mirrors the Shop reshape for the three remaining contexts so every
context follows ADR 0001 (Domain/Application/Infrastructure only):
- ports move from Application/Port to Domain/Gateway (and
  Domain/Repository for OrderTracking's TrackingAccessRepository);
- OrderTracking's UserInterface/Web controller and the Loyalty /
  OrderTracking Bootstrap ServiceProviders move into Infrastructure;
- functions.php boots the relocated providers.

Pure structural move, no behaviour change. The valuable Shared dedup
(Clock, NormalizedPhone) was already done in the base PR; the
typed-id / Criteria / event retrofit stays intentionally deferred as
disproportionate.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (337), e2e (tracking,
loyalty, subscribers, subscription-write).

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
#45)

Updates present-tense references to the current code layout after the
Pilotage -> Shop rename and the three-layer reshape (issue #1):
- src/Pilotage -> src/Shop, tests/Pilotage -> tests/Shop
- PilotageServiceProvider -> ShopServiceProvider
- "admin controllers in UserInterface / assembly in Bootstrap" ->
  Infrastructure

Across AGENTS.md, docs/fidelite.md, docs/processus-metier-commandes.md,
docs/tests-vente.md and docs/spec-tableau-pilotage.md. The dated
deployment-log entries in docs/prod-o2switch.md are left untouched as a
historical record.

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
…helper (#46)

First lot of issue #3 (test-securing the dashboard). The CSV
formula-injection guard was duplicated verbatim in ReceiptsController
and ActivityController (`csvCell()`) and had no test. It moves into
Shared\Domain\CsvFormulaGuard and gains unit coverage.

Behaviour-preserving: same rule (a cell starting with = + - @ is
prefixed with a quote to force it to text). Hardening the guard further
(leading tab/CR, etc.) would be a separate, explicit decision.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (345, +8 new).

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
…bility (#47)

Second lot of issue #3 (test-secure the dashboard). Adds a systematic
security guard: it enumerates every admin_post_/wp_ajax_ action
registered by the pilotage admin controllers and asserts each handler
verifies a nonce (check_admin_referer / check_ajax_referer) AND a
capability (assertPermission / current_user_can).

Reflection-based, no WordPress boot; a positive control (minimum number
of actions scanned) prevents a vacuous pass. A new write action added
without a guard now fails the suite.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (346, +1 covering all
current admin write actions).

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
…keys, restore) (#48)

Third lot of issue #3 (test-secure the dashboard). Adds an integration
e2e — `make e2e-pilotage-schema-local`, auto-discovered by
scripts/e2e-ci.sh so it also runs in CI — that verifies, against the
real PilotageSchemaManager:

- migration idempotence (re-run without SQL error, all six tables
  present);
- the sequence_number nullability fix (the documented dbDelta pitfall);
- that the unique constraints (receipts.sequence_number,
  harvest_lots.code, stock_movements.reference_key) reject duplicates
  (a concurrency proxy);
- a backup/restore round-trip.

Every check runs on `CREATE TEMPORARY TABLE … LIKE` copies, so no real
data is touched, and the temp tables are dropped at the end. AGENTS
documents it next to the dbDelta pitfall.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (346), new e2e (16
assertions).

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
…#49)

Fourth lot of issue #3 (test-secure the dashboard). Adds a deterministic
performance e2e — `make e2e-pilotage-perf-local`, auto-discovered by
scripts/e2e-ci.sh so it runs in CI — that fills a CREATE TEMPORARY TABLE
… LIKE copy of the receipts register with 20,000 rows spread over the
year, then replays the real register query on a selective window and
asserts:

- correctness at scale (exactly the window's rows, correct sum);
- the query plan uses the occurred_at index (EXPLAIN type != ALL, key =
  occurred_at) — a deterministic proof it scales, not a flaky wall-clock;
- a generous time guard (< 1s on 20k rows) against a catastrophic
  regression.

Temporary table only, no real data touched.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (346), new e2e (5
assertions).

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
…ashboard (#50)

Fifth and final lot of issue #3 (test-secure the dashboard). Adds a
headless-browser smoke — `make browser-local`, and a step in the CI e2e
job (Node 22 + `npx playwright install`) — that logs into wp-admin and
opens every pilotage view, asserting each renders without a PHP fatal or
an uncaught JS error and that the tab nav and the Vente form are present.
Deeper JS behaviour (the Vente anti-double-click) stays covered by the
jsdom unit tests in tests-js/.

Playwright is added as a theme devDependency. The browser test files
(tests-browser/) and playwright.config.js are excluded from deployment
in all three lists (Makefile DEPLOY_EXCLUDES, deploy-files.sh,
verify-prod-integrity.sh), and the run artifacts (test-results/,
playwright-report/, tests-browser/.auth/) are gitignored.

Validated locally: 11 browser checks green. jsdom and PHP suites
unaffected (no PHP changed).

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
Adds the missing entries so AGENTS covers all five #3 lots: the CSV
formula-injection guard test, the admin nonce/capability reflection
guard, and the receipt-register performance e2e — alongside the schema
e2e and the Playwright navigation smoke already documented.

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
#52)

Shared\Domain\CsvFormulaGuard now also neutralises cells that begin with
a tab, carriage return or newline (OWASP CSV-injection guidance), on top
of the existing = + - @ formula leads. A spreadsheet can otherwise glue
such a cell to its neighbour and reinterpret it. Both dashboard CSV
exports (Recettes, Journal d'activité) benefit automatically.

Behaviour change limited to those leading-whitespace inputs, which are
now prefixed with a quote to force plain text.

Gates green: CS-Fixer, PHPStan (max), PHPUnit.

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
Fixes a local dev environment whose WordPress roles drifted — a
copy-of-prod database where the `administrator` role lost its
capabilities, so every /wp-admin/ page returns 403 (which breaks
`make browser-local`). `make doctor` (tools/local-doctor.php) recreates
the standard roles, re-applies the WooCommerce caps and restores `admin`
to administrator.

Local-only: the tools/ folder is never deployed and the tool no-ops
outside WP-CLI. Run via `wp eval "require …"` (not `eval-file`), because
`declare(strict_types=1)` is illegal inside the eval() that `eval-file`
uses. AGENTS documents it next to the browser smoke.

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
Augments `make e2e-pilotage-perf-local`: beyond the 20k-row scale +
index-usage check on a temporary copy, it now also runs the real
WordPressReceiptRepository::occurredBetween() (the query AND the
row -> ReceiptEntry mapping) on a small tagged dataset inserted in the
real table under year 2099 (no real data), asserts the 5 entries are
read and mapped correctly, and deletes the test rows in the finally
block (robust even after a mid-run failure).

Gates green: CS-Fixer, PHPStan (max), and the e2e (6 assertions,
0 residual 2099 rows).

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
Aligns the class name with its bounded context: it manages the Shop
context's tables, and the "Pilotage" prefix predated the Pilotage -> Shop
context rename. Pure rename, no behaviour change.

The stored option key `luziapi_pilotage_receipts_schema_version` is
deliberately left unchanged (it is data, not code — renaming it would
trigger a one-time re-migration in production).

The dashboard-feature names (PilotageController, PilotageTabs, the
admin/pilotage/ views, admin-pilotage.js) keep "Pilotage" on purpose:
there it is the UI name of the dashboard (« tableau de pilotage »), not
the bounded context — renaming them to "Shop" would be less clear.

Gates green: CS-Fixer, PHPStan (max), PHPUnit (348), e2e (schema, perf,
receipt).

Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
Hexagonal/DDD refactor (behaviour-preserving), dashboard test suite
(schema, permissions/nonces, performance, browser smoke), make doctor,
and the CSV formula-guard hardening. Bumps LUZIAPI_VERSION and the theme
Version header; composes the 1.4.0 CHANGELOG section.
@Drago37 Drago37 added the enhancement New feature or request label Sep 22, 2026
@Drago37 Drago37 self-assigned this Sep 22, 2026
Anthony Graule added 4 commits September 22, 2026 12:11
…g rename

`scripts/deploy-files.sh` uploads new/renamed paths but does not remove
the old ones, so a large rename (Pilotage -> Shop in 1.4.0) leaves the
old src/ paths as orphans on prod. `scripts/prune-prod-orphans.sh`
(and `make deploy-prune-prod` / `deploy-prune-prod-apply`) lists the
src/ files present on prod but no longer tracked in git and removes
them.

Safe by design: same deploy gates, strictly scoped to src/, the prod
file list is read via a token-protected PHP script, each orphan path is
re-validated (charset, no "..") and deletion is dry-run by default
(--apply + confirmation to actually remove). Documented in AGENTS.
…esidue

The e2e loyalty suites reuse fixed test phones (0600000000/0600000001).
Loyalty aggregates by identity (email OR phone), and autoLink() ties a test
phone to the first run's email for good, so expand() re-aggregates residual
ledger data left by a run interrupted before its cleanup — inflating the
baseline of later runs. The tool purges only the identity cluster reachable
from those fake test numbers (identity links + residual ledger), dry-run by
default, with a size guard and a token-protected prod runner.
…ndler ctor)

The production-only loyalty e2e core still built GetCustomerLoyaltyHandler with a
WordPressClock as its second argument. That parameter became ?LoyaltyIdentityLinks
in 1.3.0 (identity linking, commit 74c4ea3), so the construction threw an uncaught
TypeError before the try block — an empty HTTP response (no JSON). The core is
prod-only (never run locally nor in CI), so the rot went unnoticed until the full
prod e2e sweep. Production wiring (LoyaltyServiceProvider) was always correct; this
is purely a stale test. Also drop the misleading '(expiration active)' assertion
label — pot expiry was removed in 1.3.0.
Record the 1.4.0 release deploy (245/245 SHA, base cded01e -> 9b2d980), the
Pilotage -> Shop orphan pruning (190 files removed, verify-prod 350/350), and
the full prod e2e sweep (13/13) with the two test-only fixes it surfaced: the
loyalty test-isolation residue purge and the stale prod-only loyalty e2e core.
@Drago37
Drago37 merged commit bed80fd into main Sep 23, 2026
6 checks passed
@Drago37
Drago37 deleted the release/1.4.0 branch September 23, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant