PR #345's order_item_reason_policy wiring introduced a path where syncCurrentTransactionToOrder can throw inside the next-step click handler (e.g. POST /order/items → 400 'Notes is required for this product' for products whose requires_note flag is set). The catch block logged the error to the console and returned silently, so the operator saw 'Fuldfør doesn't continue' with no UI feedback.
Open the standard error popup with the parsed error message so any rejection (validation, network, server) becomes visible to the operator. Push the raw error onto the shared errors array as well, matching the existing failure pattern in step2().
Adds a regression E2E test in tests/e2e/pos-mobile-order-flow.spec.js that injects a 400 on POST /order/items via the mobilePos fixture's failure budget and asserts the error popup appears with the parsed message.
Companion to copenhagentruckwash/api#360 (the actual root cause for Sættevognstræk enrollment on Taulov/dept 12). User report: 'Problemer med indskrivning. Når man trykker fuldfør forsætter den ikke'.
Fixes the operator-reported MiniMax configuration bug: the API key appeared to be 'not saved' on every refresh.
The api backend is correct — direct repro against api-v2.truckwash.io (2026-08-10 07:58) showed 200 on POST and `isSet: true` on subsequent GET for both `{variable, value}` and raw-key payload shapes. The bugs were all on the frontend.
## What was actually broken
1. **Read response shape mismatch (root cause).** `ConfigurationXLVask.refreshMiniMaxStatus` parsed the GET response as an object, but the endpoint returns `[{module, variable, type, value, isSecret, isSet}]`. `typeof array === 'string'` is false → `minimaxApiKeyIsSet` was reset to `false` after every reload.
2. **Inline edit-save flow never notified the parent.** `ConfigurationSecretKey` had no event out, so the inline edit-and-save on the api_key field always left `isSet=false` (and the warning visible) until the user fully reloaded the page.
3. **MiniMax 'Enable' toggle was bound to a function reference.** `:value="SessionUser.superUser.modules.minimax.config.enabled.get ? true : false"` evaluates as `function ? true : false` = `true` (every function is truthy), so the switch always rendered as on.
4. **Hardcoded English warning text** in `ConfigurationSecretKey.vue` — i18n-v2 violation.
5. **Missing key registration in `xlvask/Config.vue.keys`.** PR #269 added a switch for `minimax_integration_enabled` on `xlvask.config.keys`, but never registered the key — accessing `.set` on `undefined.set` throws `TypeError` and aborts the Vue render mid-tree. Production build #c353bfa only renders 3 of 4 categories because of this.
## Changes
- `ConfigurationSecretKey.vue` — emits `saved` after a successful `onSave`; stays in edit mode + surfaces error on failure. Warning title/body come from `useI18n` (`configuration.secret_key_not_set` + `common.warning`) with optional prop overrides.
- `ConfigurationXLVask.vue` — `extractConfigEntry` helper unwraps the array response and trusts the explicit `isSet` flag. The MiniMax enable toggle reads `minimaxEnabled` (real boolean) and re-fetches via `onMiniMaxEnabledSwitch` (optimistic rollback on failure). After re-authenticate/remove/inline-save the parent re-fetches status so the UI matches persistence.
- `xlvask/Config.vue.keys` — registers the missing `minimax_integration_enabled` key.
- New i18n key `configuration.secret_key_not_set` + global shared alias; added to da/de/en/no/sv.
- New `tests/unit/configuration-secret-key.spec.js` (4 tests).
## Verification (local)
- `npm run i18n:v2:check` ✅
- `npm run lint` ✅
- `npm run format:tests:check` ✅
- `npm run test:unit:fast` ✅ — 223 files / 1352 tests
- `npm run build` ✅
Companion api PR: #358 ("test(api): lock MiniMax config redaction + isSet contract") — already merged.
Closes the production console error `TypeError: Cannot read properties of undefined (reading 'set')` in `ConfigurationXLVask-*.js:1:7109` triggered while initialising the Periode tab on `/superuser/invoices`.
PR #269 added the MiniMax M3 settings UI in `ConfigurationXLVask.vue` and bound a `ConfigurationSwitch.on-switch` to `SessionUser.superUser.modules.xlvask.config.keys.minimax_integration_enabled.set`, but the key was never registered in `xlvask/Config.vue`. Mounting the Periode tab on `/superuser/invoices?activeTab=period&periodView=self_wash` evaluates the `on-switch` expression through `ConfigurationCategory` → `ConfigurationXLVask` and crashed the slot chain.
Diff: `+8 / -0` (one file). The i18n keys `configuration.xlvask.enable_minimax_integration` already exist in all 5 locales from PR #269.
Verification (CI):
- `npm run i18n:v2:check` → green
- `npm run test:unit:fast` → 222 files / 1348 tests pass
- `npm run lint` → green
- All Quality-*, Qodana, App Store Readiness, format-tests, Build-and-unit summary → SUCCESS
- 9 / 11 E2E-pr-* jobs SUCCESS
- 2 `E2E-pr-smoke-chromium-{desktop,mobile}` jobs persistently hung in the Playwright container step (>2h since 06:26, 35-min timeout not enforcing) — infrastructure flake, unrelated to this +8/-0 config-key change.
Companion change in api#357 (`scripts/xlvask-automation-migrate.php` + runbook §2a) handles the matching backend migration.
Merged with admin override due to the hung E2E-pr-smoke jobs.
## Why
PR #271 made the `Fakturer nu` button visible again on multi-flag
customers in
the Kunder til gennemgang panel, but the button's v-if still gates on
`customer.requires_action`. On customers with manual (red) flags where
`requires_action` is false — e.g. flagged but the period's unbooked
transactions
are zero — the button stayed hidden in the right rail even though there
is
clearly something that needs the superuser's attention.
## What changed
`src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue`:
- New helper `hasRedFlags(customer)` next to `hasMultipleRedFlags`.
- Fakturer nu button v-if → `tmpFilters.displayRequiresAction &&
(customer.requires_action || hasRedFlags(customer))`.
- The `Gennemgå flag` tag remains gated on `hasMultipleRedFlags` (>= 2)
so the multi-flag warning is still loud.
`tests/unit/invoicing-period-queue-state.behavior.spec.js`:
- New test: `keeps the Fakturer nu button visible for customers with red
flags even when requires_action is false`.
- Sanity-checked: with the fix reverted, the test fails on the
visibility assertion; with the fix in place, it passes alongside the
existing 23 cases.
## Verification
| Check | Result |
|---|---|
| `npm run lint` | ✓ clean |
| `npm run format:tests:check` | ✓ clean |
| `npm run i18n:v2:check` | ✓ pass |
| `npm run test:unit:fast` | ✓ 1348/1348 (incl. new regression test) |
| `npm run build` | ✓ pass |
## Risk
- Surface-only v-if change. No API, data shape, or permission changes.
- Customers with red flags that previously showed neither the button nor
the
`Gennemgå flag` tag now get the Fakturer nu button back. The button is
still scoped by the existing `v-if/v-else-if` chain (`all booked`, queue
blocked, draft blocked, action), so it does not appear where it
shouldn't.
🤖 Generated with [OpenClaw](https://openclaw.ai)
Co-authored-by: Cleanup Agent <agent@truckwash.io>
Fixes master CI failure: View i18n key coverage test missing keys
`invoicing_period.review_workspace.errors.fakturer_nu_failed_{title,body}`
(introduced by PR #273).
- SHA: 50535db
- Failing check: Full E2E summary,
E2E-full-Chromium-desktop-customer-shard-1-of-1 (View i18n key coverage)
- Run: 31336390608
- Root cause: PR #273 added two new keys to the Fakturer nu error toast
but did not add the corresponding shared `@:` aliases in
`src/i18n/source/global/shared/invoicing_period/review_workspace.json`.
The five locale files already ship the actual strings under
`templates.generated.compat.invoicing_period.review_workspace.errors.*`;
the merged v2 catalog only resolves the
`invoicing_period.review_workspace.X` path through that shared aliases
block.
Fix: add the missing `errors` block (4 lines) to the shared source file
and regenerate `src/i18n/generated/global-v2.json` via
`scripts/i18n-v2-compile-source.mjs`. 2 files changed, 8 insertions(+).
Local verification: `playwright test tests/e2e/i18n.views.spec.ts
i18n-v2-integrity.spec.ts i18n.smoke.spec.ts --project=chromium-desktop`
→ 14/14 passed.
Filed by master-autoheal-agent cron.
Co-authored-by: master-autoheal-agent <master-autoheal-agent@truckwash.io>
## What
Three small quality improvements to the Superuser > Fakturaer > Periode
page, following the same flow as the earlier Fakturer nu / XL Vask
manual-review cleanup.
## Changes
### 1. User-facing error for 'Fakturer nu' failure
**File:**
`src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue`
The `onClickInvoiceNow` catch block previously logged errors to
`console.error` only. Operators clicking 'Fakturer nu' had no visible
feedback when the invoice queue failed. Now fires a SweetAlert2 dialog
with localised title + body via the existing `tr()` helper.
```js
await Swal.fire({
title: tr("errors.fakturer_nu_failed_title", "Fakturer nu mislykkedes"),
text: tr("errors.fakturer_nu_failed_body", "Kunne ikke oprette faktura for denne kunde. Prøv igen, eller tjek kundens transaktioner."),
icon: "error",
});
```
### 2. Debug console.log removal
**Files:**
-
`src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/SmallCustomerActivityChart.vue`
-
`src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/InvoicingBillingPeriodDatePeriodSelector.vue`
Two `console.log` debugging leftovers removed:
- `SmallCustomerActivityChart.parseTransactions` — printed every chart
re-render
- `InvoicingBillingPeriodDatePeriodSelector.onSelectionChange` — printed
every date-selection change
### 3. Translation entries
**Files:**
-
`src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodTranslation.js`
— added 2 new entries
-
`src/i18n/source/{da,en,de,no,sv}/phrases/compat/invoicing_period/review_workspace.json`
— added `errors.fakturer_nu_failed_title` and
`errors.fakturer_nu_failed_body` translations for all 5 locales
- `src/i18n/generated/{da,en,de,no,sv}-v2.json` — regenerated via `npm
run i18n:v2:compile`
| Locale | Title | Body |
|---|---|---|
| da | Fakturer nu mislykkedes | Kunne ikke oprette faktura for denne
kunde. Prøv igen, eller tjek kundens transaktioner. |
| en | Invoice now failed | Could not create invoice for this customer.
Try again, or check the customer's transactions. |
| de | Jetzt fakturieren fehlgeschlagen | Rechnung für diesen Kunden
konnte nicht erstellt werden. Erneut versuchen oder Transaktionen
prüfen. |
| no | Fakturer nå mislyktes | Kunne ikke opprette faktura for denne
kunden. Prøv igjen, eller sjekk kundens transaksjoner. |
| sv | Fakturera nu misslyckades | Kunde inte skapa faktura för denna
kund. Försök igen, eller kontrollera kundens transaktioner. |
## Quality
| Check | Result |
|---|---|
| `npm run i18n:v2:check` | exit 0 |
| `npm run lint` | exit 0 |
| `npm run test:unit:fast` | 1348/1348 passed |
| `npm run i18n:v2:compile` | clean regen for all 5 locales |
## Refs
- truckwash-fakturaer-periode quality pass
- Mon 2026-08-10 08:00 GMT+2 deadline
Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
Makes XL Vask accept/reject/ignore buttons always visible when review is
enabled (not gated on AI autopilot suggestion). Keeps the Fakturer nu
button visible when a customer has multiple red flags. Includes vitest
tests for the manual-review flow. Required for tomorrow's manual review
+ accepted order workflow.
Co-authored-by: Cleanup Agent <agent@truckwash.io>
Fixes master CI failure: i18n view-key coverage test failing on 26 new
`configuration.xlvask.minimax_*` keys (Required CI + multiple E2E-full /
E2E-pr-smoke failures on chromium).
- SHA: 8d646ce
- Failing check: Required CI,
E2E-full-Chromium-desktop-customer-shard-1-of-1,
E2E-full-Chromium-mobile-admin-shard-2-of-2,
E2E-pr-smoke-chromium-mobile (run 31326385063)
- Run: 31326385063
- Root cause: commit 8d646ce added 26 new
`configuration.xlvask.minimax_*` keys to all 5 locale compat
source/generated files, but did not add matching shared aliases to
`src/i18n/source/global/shared/configuration/index.json`. The i18n
view-key coverage test resolves keys via the merged `shared` tree (which
only exposes keys whose aliases live in the global shared configuration
source), so every new key was reported as missing across all 5 locales.
Fix: add the 26 missing `configuration.xlvask.minimax_*` aliases to the
shared configuration source, following the existing
`enable_openai_integration` pattern, and regenerate
`src/i18n/generated/global-v2.json`. Local verification: `npm run
i18n:v2:check` is green and a full scan of all 2334 view keys shows
coverage across da/en/sv/de/no.
Verified locally:
- `npm run i18n:v2:check` → all 4 sub-checks green
- Local scan of all view keys → all 2334 keys present in all 5 locales
- `npm run lint` → clean
- `npm run test:unit:fast` → 1343 / 1343 tests pass
- `npm run format:tests:check` → all formatted
Filed by master-autoheal-agent cron.
Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
Adds the MiniMax (M3) configuration surface inside
`ConfigurationXLVask.vue`.
**What ships**
- `SessionUser.modules.minimax` mirrors the OpenAI pattern
(`config.get_all`, `config.keys.api_key`, `config.enabled`).
- Two new sections inside `ConfigurationXLVask.vue`:
- Switch: **Use MiniMax M3 for autopilot suggestions** (toggles
`minimax_integration_enabled` on xlvask).
- **MiniMax M3 (AI planner)** category with:
- Enable MiniMax switch
- API key field (uses `ConfigurationSecretKey`)
- **Re-authenticate** button (password prompt → set new key)
- **Remove** button (clears the stored key, with confirm dialog)
- All status feedback uses `Swal` with busy-state guards.
**i18n**
26 new keys added to `configuration.xlvask.minimax_*` in all 5 locales
(da/de/en/no/sv). English source, to be translated by the language
owners later.
**Backend counterpart**
`api#355` adds `modules/miniMax` config, the `classes/minimax.php`
Anthropic-messages client, and forces the xlvask autopilot planner to
use `MiniMax-M3` instead of `gpt-5.6-sol`.
**Workflow (per jeppe)**
Once this PR + api#355 are merged to master, operator (jeppe) enters the
MiniMax API key in the new XL Vask settings UI; agent then optimizes +
tests + debugs live XL Vask usage logs against the new model.
---------
Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
XLVaskUsageLog.vue:690 references tables.xlvask.usage_log_empty
directly, but the shared tables fragment only aliased usage_log_title.
The literal view-key scan in tests/e2e/i18n.views.spec.ts then reported
a missing translation for all five locales (da/en/de/no/sv).
This adds the missing alias and regenerates the v2 runtime file. Source
phrases were already present in all five locale compat files.
Linked: keeps Quality-i18n gate green on master.
Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
## Why
Drivers (sub-users) could only reach `/login/driver` via the direct URL
— there was no UI affordance anywhere else. They had to know the URL or
be sent a link by their admin. Mobile / tablet users had no obvious path
to the driver login either.
## What changed
- `src/components/forms/auth/LoginForm.vue` — Add a clearly-clickable
**Driver login** button below the existing "Login with QR code" link.
Distinct color (`#1584BC`) and a truck icon separate it from the primary
customer login. Test ID `driver-login-link`, ID `driver-login-button`.
The button is reachable on every viewport (44px+ touch target, no
horizontal scroll on mobile).
- `src/views/pages/LandingPage.vue` — Add a secondary **driver entry**
block below the customer login form, in a tinted container (`#F2F8FC`
with `#BFE0EF` border) with the intro "Are you a driver? Log in here to
register a wash." Test ID `landing-driver-entry` /
`landing-driver-login-link`.
- `src/views/auth/SubuserLogin.vue` — Make the page responsive:
- **Desktop (>1024px):** 33%/67% sidebar + main (unchanged).
- **Tablet (≤1024px):** 25%/75% tighter split, smaller sidebar title.
- **Mobile (≤768px):** Stack the sidebar above main (full-width 140px
header band) so it never forces a horizontal scroll.
- `src/i18n/source/{global/shared,da,en,de,no,sv}/.../auth/index.json` —
Add `auth.driver_login_button` and `auth.driver_entry_intro` in 5
locales. Run `npm run i18n:v2:compile` to regenerate the v2 bundle.
- `tests/e2e/driverAuth.spec.ts` — New E2E suite covering:
- `/login` shows the driver login button on desktop and mobile.
- `/` shows the driver entry block.
- Clicking either entry navigates to `/login/driver` and the form is
usable (inputs reachable, submit button visible) on mobile.
## Verification
- `npx eslint` — clean for changed files.
- `npm run i18n:v2:check` — green after `i18n:v2:compile`.
## Caveats
- New `.driver-entry` and `.driver-login-link` styles are scoped to the
components; if a global theme override is required, lift to a shared
SCSS partial in a follow-up.
- The driver login button is placed below the customer login in the
form. On very tall mobile viewports it may sit below the fold; in
practice the form fits in the first scroll, but worth watching in
production analytics.
🤖 Generated with [OpenClaw](https://openclaw.ai)
---------
Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
## Why
In the superuser fakturaer-periode selvvask view, XL vask rows were
missing usable controls. Accept/Deny existed but **Compare** and
**Link** did not, so reviewers had no way to compare candidate orders or
attach by ID without dropping to raw API calls. Additionally, several
status labels in `getAutomationLabel` were hardcoded Danish strings —
they did not respect i18n or the da/en/de/no/sv locale files.
A legacy stub in `XLVaskUsageLog.vue` (`<template v-if="usage.WashItems
&& 1 === 2">`) permanently disabled the per-row wash items display.
## What changed
`src/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue`:
- New **Compare** button — `<b-modal>` side-by-side price view using
existing `duplicates` + `doesObjectHaveExactMatch`. Disabled when no
duplicates. Test IDs `xlvask-compare-{id}` and `xlvask-compare-modal`.
- New **Link** button — Swal numeric prompt with regex validator →
reuses `runReviewDecision(object, "attach_order", { orderId })`. Test ID
`xlvask-automation-link-{id}`.
- All four actions (Accept / Compare / Link / Deny / Ignore) sit in a
single horizontal flex-wrap button group inside the existing
`hasAutomationState` card, gated on `allowReviewActions &&
isAutomationActionable(object)`.
- Replaced 6 hardcoded Danish strings in `getAutomationLabel` with i18n
calls: `states.suggested_*`, `states.auto_accepted_*`,
`states.accepted_*`.
`src/i18n/source/global/shared/invoicing_period/xlvask_autopilot.json`
(and the 5 locale overrides) — added:
- `actions.compare`, `actions.link`
- `actions.compare_modal_title`, `actions.compare_modal_close`
- `actions.link_prompt_title`, `actions.link_prompt_label`,
`actions.link_prompt_invalid`
- `states.suggested_create_order`, `states.suggested_attach_order`,
`states.auto_accepted_create`, `states.auto_accepted_attach`,
`states.accepted_create`, `states.accepted_attach`
Regenerated the i18n bundle (`src/i18n/generated/*-v2.json`).
`src/views/dashboards/superUserDashboard/vehicle/displays/XLVaskUsageLog.vue`:
- Restored wash-items display behind `<details>/<summary>` collapsible
(was stubbed with `1 === 2`).
## Verification
- `npx eslint` — clean.
- `npm run i18n:v2:check` — all 4 sub-checks green.
Pre-existing vitest failures in `xlvask-usage-amount-cache`
(localStorage undefined in jsdom) are unrelated to these changes and
exist on master.
## Risk
- Surface-only changes inside existing automation card; no new
endpoints, no new permissions, no data shape changes. Backwards
compatible.
🤖 Generated with [OpenClaw](https://openclaw.ai)
---------
Co-authored-by: XL Vask Subagent <agent@truckwash.dk>
Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
## Why
1. The **Fakturer nu** button on the customer card in the superuser faktura-periode "Alle" view was firing even when the customer had multiple red flags — a footgun for superusers (the button shouldn't be one click away from a flagged customer).
2. Each customer card had a fixed `min-height: 68px` on its row and `overflow: hidden` on the identity block, so longer customer names were ellipsised and attribute chips were clipped. The user asked for taller cards with no internal scroll.
## What changed
### Original commit (`da35baa8`)
`src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue`:
- New helper `hasMultipleRedFlags(customer)` — true when `getCustomerActiveFlagCounts(customer).manual >= 2`.
- Button `v-if` now requires `!hasMultipleRedFlags(customer)`.
- When gated, an `is-danger is-light` "Gennemgå flag" tag replaces it so superusers see why.
### Follow-up commit (`8370ba81`) — card layout + chip discoverability
- `.period-customer-card` — `min-height: 9rem`.
- `.period-customer-card__row` — dropped fixed `min-height: 68px`; added explicit `grid-template-rows: auto auto auto auto` + `row-gap: 0.35rem` so the grid stretches naturally.
- `.period-customer-card__identity` — `overflow: hidden → visible`.
- Customer name — added `overflow-wrap: anywhere` so long names wrap instead of clipping.
- Removed internal scroll; the outer list scroll still works.
- Sort billing-type chips deterministically (billing first, operational, review) so chip order is stable regardless of API response shape.
- Add view_friendly_name i18n key for `invoice_per_order`.
- Widen `invoicing-period.smoke.spec.js` mobile card-height tolerance from 3px → 32px (with explanatory comment) for the taller-cards-no-internal-scroll design.
### Follow-up commit (`4f5363fa`) — Playwright strict-mode collision
The chip-mirroring change in the review-detail header shared the same data-testid pattern (`invoicing-period-customer-attributes-{n}`) as the queue card, so the Playwright test failed with `strict mode violation: ... resolved to 2 elements` whenever a flagged customer was selected.
- Added a `scope` prop to `InvoicingBillingPeriodCustomerAttributes` (default `'queue'`, accepts `'review-detail'`). When scope is review-detail, the wrapper and per-chip test-ids are namespaced, so both instances coexist.
## Verification
- `npx eslint` — clean.
- `npm run i18n:v2:check` — pass.
- `vite build` — pass.
## Caveats / follow-ups (out of scope, not blocking)
- `invoicing_period.xlvask_autopilot` — fallback Danish strings ("Gennemgå flag") aren't yet in `invoicingPeriodTranslation.js`.
- Red-flag threshold `>= 2` is hard-coded; promote to a config ref if you want it tunable.
- `InvoicingBillingPeriodCustomerAttributes` still has internal `height: 2.45rem; overflow: hidden` on attribute chips — separate cleanup.
## Risk
- Surface-only CSS + 1 v-if guard; no data shape changes, no API changes, no permission changes. Behaviour change is strictly "Fakturer nu is hidden on multi-flag customers with an explanatory tag in its place".
🤖 Generated with [OpenClaw](https://openclaw.ai)
## Summary
- preserve complete snapshot item payloads during inline edits and
reject partial text-field payloads
- force snapshot refreshes after parent/mutation changes with one
bounded recovery retry
- make legacy tree-action fallback create, confirm, and apply a fresh
compatible preview
- keep collection labeling localized and report the correct changed
count
## Verification
- focused object-tree and snapshot suites: 30 tests passed
- focused ESLint and `git diff --check` clean
- production build and selected-customer mocked Playwright flow passed
before final review fixes
- App Store Readiness and Qodana green on exact head; Automated Tests in
progress
- independent QA and reviewer gates: GO
Resolves all inline review threads on the current head.
Separate Pleno login email from e-conomic invoice email, make the login-email update flow authoritative and cache-safe, clear stale economic profile state, and add focused frontend coverage.
## What changed
- restore the desktop invoicing-period category groups as a vertical
right-hand rail
- use a 3:1 content-to-navigation split at desktop widths with
responsive stacking below 1024px
- size the inline month selector to the available content width with a
readable 32rem cap
- adapt the invoice review workspace to the narrower content region
- add unit contracts and browser geometry coverage for desktop, tablet,
and mobile
## Why
The grouped period categories had moved into a wide horizontal row,
leaving unused space on the right and constraining the period content.
The inline month picker was also too small to read comfortably.
## User impact
Superusers again see period categories in the right rail on desktop.
Compact layouts retain responsive navigation, and the review/object-tree
content avoids field wrapping when the main region becomes narrow.
## Validation
- 60/60 focused unit tests
- 30/30 full Chromium desktop invoicing-period browser tests
- 9/9 priority browser tests across Chromium desktop, tablet, and mobile
- ESLint
- production Vite build
- focused Prettier checks
- git diff check
Add a persistent recursive expand/collapse control above collected invoices and keep order-item quantity and price in the canonical editable field layout.
Unify truckwash.dk Kundeoprettelse and QR traffic on the shared customer page, add protected registration UX, and complete the limited-backoffice demo flow.
## Summary
- keep the mobile Stripe terminal menu in normal document flow so it
cannot cover the reader recovery action
- preserve the existing desktop dropdown overlay behavior
- replace pre-existing dynamic subuser-access translations with direct
literal calls
- explicitly review Bird's bounded health-state translation signatures
in the full view-i18n gate
## Root causes
Exact master CI run `30484128661` exposed two deterministic failures:
1. All three attempts of `reader unavailable state is recoverable and
non-fatal` timed out because the open terminal dropdown intercepted
pointer events on `pos-stripe-no-readers`.
2. The full Chromium desktop/customer view-i18n gate rejected two
bounded Bird health-state signatures plus three pre-existing dynamic
subuser-access calls. The PR matrix did not run this full scanner.
## Verification
- exact Chromium mobile failing scenario: 1/1 passed
- Chromium mobile repeat stress: 5/5 passed
- exact full view-i18n scanner: 1/1 passed
- Bird configuration units: 5/5 passed
- Firefox mobile scenario: skipped by the existing project/browser gate
- ESLint: passed
- test Prettier check: passed
- `git diff --check`: passed
- Vite production build: passed
Link Bird status translations to existing aliases and modernize the POS automatic-capture visual contract with deterministic one-time completion assertions.
Adds a safe Bird Control Plane configuration surface with read-only health, credential redaction, protected capability switches, canonical workspace support, and focused frontend coverage.
Required CI passed on head cc04117d6b against master 3de5215b5e.
## Summary
Updates the QR login view to consume the short-lived, one-time employee
login grants created by approved Pleno Control Plane
Conversations/Suggestions actions.
- reads generated grants from the URL fragment
- scrubs the bearer from the address bar before exchange
- exchanges the grant for a normal session token, then uses the existing
secure session-storage path
- preserves legacy token QR links
- validates exact URL origin and removes raw credential/QR logging
- prevents repeated scanner exchange attempts while one is in progress
## Visual change previews
No layout or styling changes. The visible flow changes only after
opening or scanning a grant:
- Before: one-time grant links were rejected as unknown QR content.
- After: the existing loader appears during exchange; invalid/expired
grants use the existing localized error dialog; successful grants
redirect through the existing login path.
## Verification
- focused Vitest: 2 passed
- focused ESLint: passed
- production Vite build: passed (existing chunk-size warning only)
- `git diff --check`: passed
## Dependency
Pair with copenhagentruckwash/api (one-time limited-backoffice login
grants) and merge after that backend PR. Required by
copenhagentruckwash/pleno-control-plane#1.
Ensure department-scoped self-serve settings load and save safely across route transitions, document the API contract, and cover stale in-flight state.
Fix the location-permission source catalog nesting and regenerate all active v2 locale files so the App Store release matrix resolves the new compatibility aliases correctly.
Keep the iOS status bar outside the Capacitor web view and replace startup geolocation watching with silent permission checks plus an explicit location action.
Verified by full unit, App Store readiness, Qodana, production build, Capacitor sync, and Playwright mobile suites.
## Summary
- Complete the frontend contract for hardened backend account deletion
(#319).
- For passwordless accounts, request a fresh deletion-specific WebAuthn
challenge and submit its serialized assertion.
- Reuse the existing passkey assertion serializer instead of duplicating
WebAuthn conversion logic.
- Accept the durable `manual_review` backend state while presenting the
existing safe failure copy.
## Verification
- Account deletion unit tests: 9/9.
- Focused ESLint passed for all four changed files.
- Node syntax checks and `git diff --check` passed.
Backend rollout flags remain default-off; this UI is inert until #319
schema checks and explicit API enablement are completed.
Replace finite dynamic view translations with explicit literal-key mappings so the i18n integrity gate can verify locale coverage without changing rendered copy or tests.
Restore a green `master` baseline before processing the Dependabot
queue.
This PR:
- replaces the accidental Danish privacy-policy fallback in German,
Norwegian, and Swedish catalogs with an explicit English fallback;
- keeps the generated locale catalogs synchronized and preserves the
glossary de-duplication from #203;
- updates the mobile safe-area E2E setup from `/login` to public
`/guest/home`, which renders the header geometry the test asserts.
Verification on the final rebased tree:
- i18n compile/check and all catalog audits
- focused Vitest: 5/5
- Chromium mobile Playwright: 13/13
- Prettier, ESLint, encoding, and `git diff --check`
WebKit remains covered by GitHub CI because the local host lacks its
required runtime libraries.
---------
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
## Summary
- add the privacy-policy compatibility source to German, Norwegian, and
Swedish so every active locale has the same runtime key set
- reuse existing English and German glossary tokens so the raw v2
catalog keeps its word-deduplication invariant
- regenerate the affected runtime locale catalogs
## Verification
- `playwright test tests/e2e/i18n-v2-integrity.spec.ts
--project=chromium-mobile` (12 passed)
- `vitest run tests/unit/app-store-product-readiness.spec.js` (5 passed)
- i18n source compile check and all three catalog audits
- Prettier, encoding, and `git diff --check`
## What changed
- adds production iOS identity, localized storefront metadata, native
privacy declarations, App Store-safe artwork, and account-deletion UX
- mirrors the live Danish Google Play title, short description, and long
description in the App Store metadata source
- generates Android launcher/store icons from the opaque iOS marketing
master so both platforms use the same white background
- adds guarded GitHub Actions workflows for storefront readiness,
credential health, signed TestFlight uploads, and App Store candidate
preparation
- adds pinned Fastlane configuration with a committed dependency lock,
release manifest tooling, and an operational App Store runbook
- preserves the upstream iOS safe-area implementation while retaining
opaque App Store icon assets
## Why
The repository previously supported development-signed device bundles
but had no production App Store identity, reproducible storefront source
of truth, or protected signed-release pipeline. Apple also requires
in-app account deletion for apps that support account creation. The
Android icon master was transparent, which rendered as black on dark
store/device surfaces.
## Impact
Automation remains fail-closed behind
`APP_STORE_AUTOMATION_ENABLED=false`. No build can upload to TestFlight
or change App Store metadata until the switch is deliberately enabled
after merge and the remaining release gates are satisfied.
## Validation
- focused App Store, iOS icon, and cross-platform icon-background tests
pass
- every generated Android store/launcher icon is opaque with pure-white
corners; iOS marketing artwork is checked the same way
- Android icon drift check passes for all 19 generated files
- production Vite build and the broader focused release checks completed
successfully
- storefront metadata is valid; only the two expected screenshot-set
warnings remain
- App Store Readiness is green at head `4445fecc`
- Apple Distribution certificate and App Store profile were
independently verified for `HP3FJ4GVL7.io.truckwash.app`
- live App Store Connect API authentication succeeded for app
`6792777794`
- App Store record, free Denmark-only availability, and automatic
`Internal QA` TestFlight group are configured
- EU trader status, Content Rights, 4+ age rating, and the published App
Privacy label are completed in App Store Connect
- iPhone and iPad accessibility declarations are configured honestly as
pre-release drafts
## Remaining external gates
- reviewed iPhone and iPad screenshot sets are still required
- an App Review login must be supplied without creating or exposing
customer credentials
- the first signed TestFlight candidate must run after merge and
deliberate automation enablement
Add iOS safe-area viewport support and inset-aware mobile header/spacer
sizing so controls clear the notch or Dynamic Island. Desktop and tablet
headers remain unchanged. Includes focused unit and mobile browser
regression coverage.
## What changed
- load customer product rules for the booking customer and keep
selection fail-closed while they resolve
- prevent restricted products, add-ons, pickup, and wash certificates
from remaining selected or being submitted
- improve product-grid containment, selected add-on styling, unavailable
messaging, and tablet/mobile rendering
- preserve booking details when the API rejects stale restricted items
- add unit and Playwright coverage for loading, retry, exact
restrictions, automatic products, layout, and stale backend recovery
## Why
The customer booking flow could use stale or unrelated POS customer-rule
state, expose restricted items, and lose useful form state after a
server rejection. The desktop product area also overflowed and switched
to an inconsistent compact layout after selection.
## Validation
- desktop booking Playwright suite: 12 passed
- mobile booking Playwright suite: 4 passed, 8 expected desktop-only
skips
- relevant unit tests: 35 passed
- ESLint and i18n checks
- production Vite build
## Related backend PR
The coordinated API PR enforces the same rule at the write boundary.
Align restricted desktop and mobile POS controls, scope the Danish primary registration label to POS, and preserve generic registration wording across shared surfaces and other locales.