Compare commits

...
Author SHA1 Message Date
TRU-83 bugfix sub-agent 12fe4745d7 fix(self-serve): restrict Dognvask machine to single vehicle type (traekker) (TRU-84)
The Dognvask machine is a single-vehicle-type wash machine. When the
current lane resolves to a Dognvask machine type, the path editor and
scope guide now:

- restrict the vehicle type filter to the 'traekker' (tractor unit) entry
  in both the main toolbar and the scope guide
- show a visible 'Dognvask: trækker only' hint badge so operators can
  see why the options are limited
- auto-correct the vehicle type filter when the user switches to or from
  a Dognvask-configured lane (clearing the filter if no traekker type
  is configured, otherwise snapping to the only allowed option)

Detection compares the resolved selfserve_machine_types row name against
'dognvask' (case-insensitive). The allowed vehicle type is matched by
label/name against 'traekker' / 'tractor' / 'tractor unit' to tolerate
the various Danish/English renderings used across the system.

This follows the same pattern introduced in TRU-91 (path editor machine
filter), which scoped machine-button options to the current lane's
machine type.
2026-08-17 18:30:52 +00:00
openhands c11b164188 fix(TRU-89): customer portal - 'tilladelser' (permissions) tab
Adds a new permissions tab in the kundeportal that exposes the two
primary role templates (driver / booking_coordinator) and the five
permission groups (vehicles, selfserve, bookings, orders, driver
management) so the customer can pick a default driver access template
for new drivers created via the QR flow (TRU-88 / TRU-90).
2026-08-17 18:01:47 +00:00
openhands 882bc64fa6 fix(path-editor): filter machine options to only show available machine (TRU-91)
The path editor previously rendered all 12 program buttons and all 12 program
wheel positions regardless of whether the current lane had a configured,
non-deleted machine type. This made it possible to author paths for programs
that the machine does not actually support.

Filter pathEditorMachineButtonOptions and pathEditorProgramButtonOptions
based on whether the current lane resolves to an available selfserve_machine_types
row. When the machine is not available, the machine button grid is empty and
the program wheel only exposes the OFF position.
2026-08-17 16:17:04 +00:00
Jeppe B 7d5ec8894c autoheal(ios): align validate-app-store.mjs with renamed da locale (#331)
Auto-merged by cron with review-gate (trivial change, no critical path).
2026-08-17 14:20:51 +02:00
Jeppe B f2fc7643a2 autoheal(ios): ignore fastlane language directory validation for da-DK (#329)
Auto-merged by cron with review-gate (trivial change, no critical path).
2026-08-17 13:51:43 +02:00
Jeppe BandOpenClaw 06b6498f7a fix(ios): rename fastlane locale da-DK → da (unblocks v1.1.0 iOS release) (#330)
## Problem

The iOS App Store Candidate workflow (iOS-v1.1.0 tag) failed at the
`Sync metadata and screenshots` step because fastlane deliver rejected
the locale directory `fastlane/screenshots/da-DK/` (and
`fastlane/metadata/da-DK/`).

App Store Connect uses `da` (not `da-DK`) for Danish. The valid locale
list (from fastlane deliver 2.238.0):
```
ar-SA, bn-BD, ca, cs, da, de-DE, el, en-AU, en-CA, en-GB, en-US, es-ES, es-MX, fi, fr-CA, fr-FR, gu-IN, he, hi, hr, hu, id, it, ja, kn-IN, ko, ml-IN, mr-IN, ms, nl-NL, no, or-IN, pa-IN, pl, pt-BR, pt-PT, ro, ru, sk, sl-SI, sv, ta-IN, te-IN, th, tr, uk, ur-PK, vi, zh-Hans, zh-Hant, appleTV, iMessage, default
```

## Fix

Renamed all `da-DK` directories to `da` in:
- `fastlane/screenshots/da-DK/` → `fastlane/screenshots/da/`
- `fastlane/metadata/da-DK/` → `fastlane/metadata/da/`

## Impact

- Unblocks v1.1.0 iOS App Store Candidate
- Danish (the only language the app fully supports) will now be properly
delivered
- All screenshots and metadata files preserved (just renamed)

## Test

After merge, re-trigger iOS App Store Candidate:
```
git tag -d ios-v1.1.0
git push origin --delete ios-v1.1.0
git tag -a ios-v1.1.0 -m 'iOS v1.1.0 release' master
git push origin ios-v1.1.0
```

Refs: TRU-140, v1.1.0 release

Co-authored-by: OpenClaw <openclaw@copenhagentruckwash.io>
2026-08-17 13:11:10 +02:00
Jeppe B c5c186242b chore: trigger release pipeline (no-op) (#328)
Triggers push event to kick off Frontend Release / iOS TestFlight / Android Store Artifacts chain.
2026-08-17 10:45:25 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3112a4f985 chore(deps): bump webfactory/ssh-agent from 0.9.0 to 0.10.0 (#325)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps [webfactory/ssh-agent](https://github.com/webfactory/ssh-agent)
from 0.9.0 to 0.10.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/webfactory/ssh-agent/releases">webfactory/ssh-agent's
releases</a>.</em></p>
<blockquote>
<h2>v0.10.0: Upgrade to node-24</h2>
<p>This release upgrades from node 20 to node 24, preparing for Node
20's upcoming EOL and getting rid of the related warning message in
GitHub.</p>
<h2>What's Changed</h2>
<ul>
<li>use node24 by <a
href="https://github.com/jimmymcpeter"><code>@​jimmymcpeter</code></a>
in <a
href="https://redirect.github.com/webfactory/ssh-agent/pull/243">webfactory/ssh-agent#243</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/jimmymcpeter"><code>@​jimmymcpeter</code></a>
made their first contribution in <a
href="https://redirect.github.com/webfactory/ssh-agent/pull/243">webfactory/ssh-agent#243</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/webfactory/ssh-agent/compare/v0.9.1...v0.10.0">https://github.com/webfactory/ssh-agent/compare/v0.9.1...v0.10.0</a></p>
<h2>v0.9.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Acknowledge custom command inputs in cleanup.js by <a
href="https://github.com/janopae"><code>@​janopae</code></a> in <a
href="https://redirect.github.com/webfactory/ssh-agent/pull/235">webfactory/ssh-agent#235</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/janopae"><code>@​janopae</code></a> made
their first contribution in <a
href="https://redirect.github.com/webfactory/ssh-agent/pull/235">webfactory/ssh-agent#235</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/webfactory/ssh-agent/compare/v0.9.0...v0.9.1">https://github.com/webfactory/ssh-agent/compare/v0.9.0...v0.9.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/webfactory/ssh-agent/blob/master/CHANGELOG.md">webfactory/ssh-agent's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<p>All notable changes to this project will be documented in this
file.</p>
<p>The format is based on <a
href="https://keepachangelog.com/en/1.0.0/">Keep a Changelog</a>,
and this project adheres to <a
href="https://semver.org/spec/v2.0.0.html">Semantic Versioning</a>.</p>
<h2>[Unreleased]</h2>
<h2>v0.9.1 [2024-03-17]</h2>
<h3>Fixed</h3>
<ul>
<li>Fix path used to execute ssh-agent in cleanup.js to respect custom
paths set by input (<a
href="https://redirect.github.com/webfactory/ssh-agent/issues/235">#235</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/webfactory/ssh-agent/commit/e83874834305fe9a4a2997156cb26c5de65a8555"><code>e838748</code></a>
use node24 (<a
href="https://redirect.github.com/webfactory/ssh-agent/issues/243">#243</a>)</li>
<li><a
href="https://github.com/webfactory/ssh-agent/commit/a6f90b1f127823b31d4d4a8d96047790581349bd"><code>a6f90b1</code></a>
Release v0.9.1</li>
<li><a
href="https://github.com/webfactory/ssh-agent/commit/72c0bfd31ab22a2e11716951e3f107a9647dc97e"><code>72c0bfd</code></a>
Improve documentation on why we use os.userInfo()</li>
<li><a
href="https://github.com/webfactory/ssh-agent/commit/e3f1a8e046525bfed3725ef54a31ca91aed399f4"><code>e3f1a8e</code></a>
Acknowledge custom command inputs in cleanup.js (<a
href="https://redirect.github.com/webfactory/ssh-agent/issues/235">#235</a>)</li>
<li><a
href="https://github.com/webfactory/ssh-agent/commit/b504c19775343714e11b8c754e4fe1f02dc7b8e7"><code>b504c19</code></a>
Update CHANGELOG.md</li>
<li>See full diff in <a
href="https://github.com/webfactory/ssh-agent/compare/v0.9.0...v0.10.0">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 10:15:43 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ddd31bd2dd chore(deps): bump actions/setup-node from 4 to 7 (#324)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4
to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-node/releases">actions/setup-node's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Add cache-primary-key and cache-matched-key as outputs by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1577">actions/setup-node#1577</a></li>
<li>Migrate to ESM and upgrade dependencies by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1574">actions/setup-node#1574</a></li>
</ul>
<h3>Bug fixes:</h3>
<ul>
<li>Remove dummy NODE_AUTH_TOKEN export by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1558">actions/setup-node#1558</a></li>
<li>Only use <code>mirrorToken</code> in <code>getManifest</code> if
it's provided by <a
href="https://github.com/deiga"><code>@​deiga</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li>
</ul>
<h3>Documentation updates:</h3>
<ul>
<li>Add documentation for publishing to npm with Trusted Publisher
(OIDC) by <a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li>
<li>docs: Update restore-only cache documentation by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1550">actions/setup-node#1550</a></li>
<li>docs: Update caching recommendations to mitigate cache poisoning
risks by <a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1567">actions/setup-node#1567</a></li>
</ul>
<h3>Dependency update:</h3>
<ul>
<li>Upgrade <code>@​actions/cache</code> to 5.1.0, log cache write
denied by <a
href="https://github.com/jasongin"><code>@​jasongin</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li>
<li><a href="https://github.com/deiga"><code>@​deiga</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li>
<li><a href="https://github.com/jasongin"><code>@​jasongin</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6...v7.0.0">https://github.com/actions/setup-node/compare/v6...v7.0.0</a></p>
<h2>v6.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update <code>@​actions/cache</code> to 5.1.0 and add security
overrides for undici and fast-xml-parser by <a
href="https://github.com/HarithaVattikuti"><code>@​HarithaVattikuti</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1579">actions/setup-node#1579</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0">https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0</a></p>
<h2>v6.4.0</h2>
<h2>What's Changed</h2>
<h3>Dependency updates:</h3>
<ul>
<li>Upgrade <a
href="https://github.com/actions"><code>@​actions</code></a>
dependencies by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li>
<li>Update Node.js versions in versions.yml and bump package to v6.4.0
by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1533">actions/setup-node#1533</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Copilot"><code>@​Copilot</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6...v6.4.0">https://github.com/actions/setup-node/compare/v6...v6.4.0</a></p>
<h2>v6.3.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Support parsing <code>devEngines</code> field by <a
href="https://github.com/susnux"><code>@​susnux</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1283">actions/setup-node#1283</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/setup-node/commit/820762786026740c76f36085b0efc47a31fe5020"><code>8207627</code></a>
Migrate to ESM and upgrade dependencies (<a
href="https://redirect.github.com/actions/setup-node/issues/1574">#1574</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/04be95cf3511ea51ebf9f224ddfb99cc7ab87cd4"><code>04be95c</code></a>
Add cache-primary-key and cache-matched-key as outputs (<a
href="https://redirect.github.com/actions/setup-node/issues/1577">#1577</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/7c2c68d20d402ed6a201ada70a81341941093140"><code>7c2c68d</code></a>
docs: Update caching recommendations to mitigate cache poisoning risks
(<a
href="https://redirect.github.com/actions/setup-node/issues/1567">#1567</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/6a61c0375d66246de94630495909f12cf8dac84d"><code>6a61c03</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/setup-node/issues/1569">#1569</a>
from jasongin/update-actions-cache-5.1.0</li>
<li><a
href="https://github.com/actions/setup-node/commit/30eb73b41ded577900c1ebf968ef95cdf8f7434f"><code>30eb73b</code></a>
Resolve high-severity audit issues</li>
<li><a
href="https://github.com/actions/setup-node/commit/4e1a87a501d0302f99e30e2748568adcb388d09f"><code>4e1a87a</code></a>
Update dist</li>
<li><a
href="https://github.com/actions/setup-node/commit/360237f0c01778d0c17291f75c56d6feae4f7574"><code>360237f</code></a>
Strict equality</li>
<li><a
href="https://github.com/actions/setup-node/commit/4f8aac5beb2f0854bc79651567a18c67eb0b9de3"><code>4f8aac5</code></a>
Bump <code>@​actions/cache</code> to 5.1.0, log cache write denied</li>
<li><a
href="https://github.com/actions/setup-node/commit/f4a67bbeca970f103397d3d2b9462cf787cd2980"><code>f4a67bb</code></a>
Only use <code>mirrorToken</code> in <code>getManifest</code> if it's
provided (<a
href="https://redirect.github.com/actions/setup-node/issues/1548">#1548</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/0355742c943ddb13ca8a6b700f824231caa91e75"><code>0355742</code></a>
Remove dummy NODE_AUTH_TOKEN export (<a
href="https://redirect.github.com/actions/setup-node/issues/1558">#1558</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/setup-node/compare/v4...v7">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 10:15:39 +02:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Jeppe B
f07c15972c chore(deps): bump fastlane from 2.237.0 to 2.238.0 (#326)
Bumps [fastlane](https://github.com/fastlane/fastlane) from 2.237.0 to
2.238.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/fastlane/fastlane/releases">fastlane's
releases</a>.</em></p>
<blockquote>
<h2>2.238.0 Improvements</h2>
<ul>
<li>[ci] support releases with PRs that were deleted (or users) (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30105">#30105</a>)
via Connor Tumbleson (<a
href="https://github.com/iBotPeaches"><code>@​iBotPeaches</code></a>)</li>
<li>[pilot] Match groups by either name or id (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30104">#30104</a>)
via Alexander Chapliuk (<a
href="https://github.com/krin-san"><code>@​krin-san</code></a>)</li>
<li>[update_project_team] add support for sdk conditionals (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30142">#30142</a>)
via Connor Tumbleson (<a
href="https://github.com/iBotPeaches"><code>@​iBotPeaches</code></a>)</li>
<li>harden: detected non-static command inside ` in... (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30148">#30148</a>)
via Anupam Mediratta (<a
href="https://github.com/anupamme"><code>@​anupamme</code></a>)</li>
<li>[fastlane_core] Create a unique package directory for non-macOS
uploads (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30144">#30144</a>)
via Victor Amorim dos Santos (<a
href="https://github.com/victoramsantos"><code>@​victoramsantos</code></a>)</li>
<li>[gem] move to webrick 1.9.x (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30135">#30135</a>)
via Connor Tumbleson (<a
href="https://github.com/iBotPeaches"><code>@​iBotPeaches</code></a>)</li>
<li>[gem] upgrade 'terminal-table' to drop monkey patch (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30137">#30137</a>)
via Connor Tumbleson (<a
href="https://github.com/iBotPeaches"><code>@​iBotPeaches</code></a>)</li>
<li>[core] Fix video resolution parsing when audio track precedes video
track (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30143">#30143</a>)
via Thomas Walther (<a
href="https://github.com/tcwalther"><code>@​tcwalther</code></a>)</li>
<li>[gym] Fix Mac Catalyst package type detection when catalyst_platform
is not set (<a
href="https://redirect.github.com/fastlane/fastlane/issues/29829">#29829</a>)
via Josh Holtz (<a
href="https://github.com/joshdholtz"><code>@​joshdholtz</code></a>)</li>
<li>[deliver/pilot] change instruction on how to run iTMSTransporter (<a
href="https://redirect.github.com/fastlane/fastlane/issues/21155">#21155</a>)
via Merlin Ran (<a
href="https://github.com/merlinran"><code>@​merlinran</code></a>)</li>
<li>[gem] move to sinatra 4 (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30134">#30134</a>)
via Connor Tumbleson (<a
href="https://github.com/iBotPeaches"><code>@​iBotPeaches</code></a>)</li>
<li>[action][spm] add <code>--skip-update</code> (<a
href="https://redirect.github.com/fastlane/fastlane/issues/21127">#21127</a>)
via Atsuto Yamashita (<a
href="https://github.com/att55"><code>@​att55</code></a>)</li>
<li>Fix precheck copyright rule wording (<a
href="https://redirect.github.com/fastlane/fastlane/issues/19500">#19500</a>)
via Andreas Ganske (<a
href="https://github.com/ChaosCoder"><code>@​ChaosCoder</code></a>)</li>
<li>docs: add detailed notation of what metrics collected (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30132">#30132</a>)
via Connor Tumbleson (<a
href="https://github.com/iBotPeaches"><code>@​iBotPeaches</code></a>)</li>
<li>Bump actions/labeler from 6 to 7 (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30133">#30133</a>)
via dependabot[bot] (<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot])</li>
<li>[upload_symbols_to_crashlytics] Adds option to let the action fail
when an upload fails (<a
href="https://redirect.github.com/fastlane/fastlane/issues/29700">#29700</a>)
via Tobias Kreß (<a
href="https://github.com/Myrronth"><code>@​Myrronth</code></a>)</li>
<li>[deliver] add support for social media rating keys (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30128">#30128</a>)
via Connor Tumbleson (<a
href="https://github.com/iBotPeaches"><code>@​iBotPeaches</code></a>)</li>
<li>[core] Support for Ruby 4 (+ testing) (<a
href="https://redirect.github.com/fastlane/fastlane/issues/29849">#29849</a>)
via Connor Tumbleson (<a
href="https://github.com/iBotPeaches"><code>@​iBotPeaches</code></a>)</li>
<li>[register_devices] Ignore blank lines and comment lines in
devices_file (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30129">#30129</a>)
via David Cordero (<a
href="https://github.com/dcordero"><code>@​dcordero</code></a>)</li>
<li>[core] detect installed Developer ID intermediate certificates by
their common name (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30126">#30126</a>)
via Rinat Kutuev (<a
href="https://github.com/ReilganInt"><code>@​ReilganInt</code></a>)</li>
<li>[core] move to xcodeproj 1.28.1 (from 1.27.0) (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30124">#30124</a>)
via Nuno de Carvalho (<a
href="https://github.com/nmcc24"><code>@​nmcc24</code></a>)</li>
<li>[core] normalize FastlaneSwiftRunner.xcodeproj plist quoting (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30125">#30125</a>)
via Nuno de Carvalho (<a
href="https://github.com/nmcc24"><code>@​nmcc24</code></a>)</li>
<li>[register_devices] Register devices normalization for UDIDs. (<a
href="https://redirect.github.com/fastlane/fastlane/issues/29981">#29981</a>)
via Muhammet Ilendemli (<a
href="https://github.com/ilendemli"><code>@​ilendemli</code></a>)</li>
<li>[crashlytics] improve logging during error (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30077">#30077</a>)
via Connor Tumbleson (<a
href="https://github.com/iBotPeaches"><code>@​iBotPeaches</code></a>)</li>
<li>[setup_ci] Add option to opt out of setting the default keychain (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30121">#30121</a>)
via Eyüp Can Akman (<a
href="https://github.com/eyupcanakman"><code>@​eyupcanakman</code></a>)</li>
<li>[snapshot] Open the requested simulator for non-headless runs (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30119">#30119</a>)
via huven (<a
href="https://github.com/huven"><code>@​huven</code></a>)</li>
<li>[core] improve operating_system detection (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30118">#30118</a>)
via Connor Tumbleson (<a
href="https://github.com/iBotPeaches"><code>@​iBotPeaches</code></a>)</li>
<li>[core] fix: upgrade yard to &gt;= 0.9.42 (CVE-2026-41493) (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30117">#30117</a>)
via OrbisAI Security (<a
href="https://github.com/orbisai0security"><code>@​orbisai0security</code></a>)</li>
<li>[core] move to Faraday 2 (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30089">#30089</a>)
via Connor Tumbleson (<a
href="https://github.com/iBotPeaches"><code>@​iBotPeaches</code></a>)</li>
<li>[docs] clarify platform resolution behavior (<a
href="https://redirect.github.com/fastlane/fastlane/issues/22185">#22185</a>)
via Douglas Hill (<a
href="https://github.com/douglashill"><code>@​douglashill</code></a>)</li>
<li>[core] fix crash when uploading large <code>.pkg</code> by using
<code>Digest::MD5.file</code> instead of
<code>Digest::MD5.hexdigest</code> (<a
href="https://redirect.github.com/fastlane/fastlane/issues/21999">#21999</a>)
via reflexing (<a
href="https://github.com/reflexing"><code>@​reflexing</code></a>)</li>
<li>[docs] fix a missing negation in <code>pilot</code> error message
(<a
href="https://redirect.github.com/fastlane/fastlane/issues/22331">#22331</a>)
via Gemma Barlow (<a
href="https://github.com/gemmakbarlow"><code>@​gemmakbarlow</code></a>)</li>
<li>[notarize] Fix erroring out with <code>bundle_id</code> parameter
being absent (<a
href="https://redirect.github.com/fastlane/fastlane/issues/29736">#29736</a>)
via Timur Sharifianov (<a
href="https://github.com/Scvairy"><code>@​Scvairy</code></a>)</li>
<li>[metrics] move to GA4 and improve metrics (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30111">#30111</a>)
via Connor Tumbleson (<a
href="https://github.com/iBotPeaches"><code>@​iBotPeaches</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/fastlane/fastlane/commit/38d0ec404332cce7a4833881eee343e9de91700c"><code>38d0ec4</code></a>
Version bump to 2.238.0 (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30151">#30151</a>)</li>
<li><a
href="https://github.com/fastlane/fastlane/commit/d967cd80625e950b39668ec272f5530bc56c60df"><code>d967cd8</code></a>
[ci] support releases with PRs that were deleted (or users) (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30105">#30105</a>)</li>
<li><a
href="https://github.com/fastlane/fastlane/commit/4eadc97560f1d798306e608fd33ff3045841ad8f"><code>4eadc97</code></a>
[pilot] Match groups by either name or id (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30104">#30104</a>)</li>
<li><a
href="https://github.com/fastlane/fastlane/commit/a43b8b8bfdf06737f49b2a48360b78ab88126fe5"><code>a43b8b8</code></a>
[update_project_team] add support for sdk conditions (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30142">#30142</a>)</li>
<li><a
href="https://github.com/fastlane/fastlane/commit/f1568f9b2069df1551e8286c4c52be6b8823bc98"><code>f1568f9</code></a>
[fastlane_core] harden plugin_scores subshell usage (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30148">#30148</a>)</li>
<li><a
href="https://github.com/fastlane/fastlane/commit/25026b9be2deb9df1de0675215b405933952edbf"><code>25026b9</code></a>
[fastlane_core] Create a unique package directory for non-macOS uploads
(<a
href="https://redirect.github.com/fastlane/fastlane/issues/30144">#30144</a>)</li>
<li><a
href="https://github.com/fastlane/fastlane/commit/283d477bc6d3566e3a07a9f8e97b8d747b16a3e5"><code>283d477</code></a>
[gem] move to webrick 1.9.x (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30135">#30135</a>)</li>
<li><a
href="https://github.com/fastlane/fastlane/commit/bc098d142eb3c78b1608db4161fa68a00816affc"><code>bc098d1</code></a>
[gem] upgrade 'terminal-table' to drop monkey patch (<a
href="https://redirect.github.com/fastlane/fastlane/issues/30137">#30137</a>)</li>
<li><a
href="https://github.com/fastlane/fastlane/commit/1f64c1f8e24dbc7b2e64317cc4fae563d1c2e3c6"><code>1f64c1f</code></a>
[fastlane_core] Fix video resolution parsing when audio track precedes
video ...</li>
<li><a
href="https://github.com/fastlane/fastlane/commit/a94b3ada721f7b8a10cc4a865bd2fe13533e5e79"><code>a94b3ad</code></a>
[gym] Fix Mac Catalyst package type detection when catalyst_platform is
not s...</li>
<li>Additional commits viewable in <a
href="https://github.com/fastlane/fastlane/compare/fastlane/2.237.0...fastlane/2.238.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=fastlane&package-manager=bundler&previous-version=2.237.0&new-version=2.238.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jeppe B <2jepp9350@gmail.com>
2026-08-17 10:14:29 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 210d7d051d chore(deps): bump slackapi/slack-github-action from 1.27.0 to 4.0.0 (#323)
Bumps
[slackapi/slack-github-action](https://github.com/slackapi/slack-github-action)
from 1.27.0 to 4.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/slackapi/slack-github-action/releases">slackapi/slack-github-action's
releases</a>.</em></p>
<blockquote>
<h2>Slack GitHub Action v4.0.0</h2>
<h3>Major Changes</h3>
<ul>
<li>
<p>b1974f0: build: parse yaml with more strict multiline indentation
rules</p>
<p>Internal dependencies of <a
href="https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md#500---2026-06-20"><code>js-yaml@v5</code></a>
make YAML parsing more strict and compliant with the YAML specification.
Indentation is now required for values that span multiple lines against
the base value.</p>
<p>See the YAML <a
href="https://yaml.org/spec/1.2.2/#63-line-prefixes">line prefixes</a>
spec for the expected indentation rule:</p>
<pre lang="diff"><code>  channel: &quot;C0123&quot;
  text: &quot;first line
<ul>
<li>second line&quot;</li>
</ul>
<ul>
<li>second line&quot;<br />
</code></pre></li>
</ul>
</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>654bb72: chore: provide global fetch proxied configurations with
updates to web api and webhook packages</li>
</ul>
<h2>Slack GitHub Action v3.0.5</h2>
<h3>Patch Changes</h3>
<ul>
<li>96fddbe: fix: revert multiline yaml parsing indentation change</li>
</ul>
<h2>Slack GitHub Action v3.0.4</h2>
<h3>Patch Changes</h3>
<ul>
<li>fa03fe4: refactor: send webhooks with the <a
href="https://docs.slack.dev/tools/node-slack-sdk/webhook"><code>@slack/webhook</code></a>
package</li>
</ul>
<h2>Slack GitHub Action v3.0.3</h2>
<h3>Patch Changes</h3>
<ul>
<li>66834e4: feat: add instrumentation to address error rates</li>
</ul>
<h2>Slack GitHub Action v3.0.2</h2>
<h3>Patch Changes</h3>
<ul>
<li>79529d7: fix: resolve url.parse deprecation warning for webhook
techniques</li>
</ul>
<h2>Slack GitHub Action v3.0.1</h2>
<h2>What's Changed</h2>
<p>Alongside the breaking changes of <a
href="https://github.com/slackapi/slack-github-action/releases/tag/v3.0.0"><code>@v3.0.0</code></a>
and a <a
href="https://docs.slack.dev/tools/slack-github-action/sending-techniques/running-slack-cli-commands/">new
technique</a> to run Slack CLI commands, we tried the wrong name to
publish to the GitHub Marketplace 🐙 This action is now noted as <a
href="https://github.com/marketplace/actions/the-slack-github-action"><strong>The
Slack GitHub Action</strong></a> in listings 🎶 </p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/slackapi/slack-github-action/blob/main/CHANGELOG.md">slackapi/slack-github-action's
changelog</a>.</em></p>
<blockquote>
<h2>4.0.0</h2>
<h3>Major Changes</h3>
<ul>
<li>
<p>b1974f0: build: parse yaml with more strict multiline indentation
rules</p>
<p>Internal dependencies of <a
href="https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md#500---2026-06-20"><code>js-yaml@v5</code></a>
make YAML parsing more strict and compliant with the YAML specification.
Indentation is now required for values that span multiple lines against
the base value.</p>
<p>See the YAML <a
href="https://yaml.org/spec/1.2.2/#63-line-prefixes">line prefixes</a>
spec for the expected indentation rule:</p>
<pre lang="diff"><code>  channel: &quot;C0123&quot;
  text: &quot;first line
<ul>
<li>second line&quot;</li>
</ul>
<ul>
<li>second line&quot;<br />
</code></pre></li>
</ul>
</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>654bb72: chore: provide global fetch proxied configurations with
updates to web api and webhook packages</li>
</ul>
<h2>3.0.5</h2>
<h3>Patch Changes</h3>
<ul>
<li>96fddbe: fix: revert multiline yaml parsing indentation change</li>
</ul>
<h2>3.0.4</h2>
<h3>Patch Changes</h3>
<ul>
<li>fa03fe4: refactor: send webhooks with the <a
href="https://docs.slack.dev/tools/node-slack-sdk/webhook"><code>@slack/webhook</code></a>
package</li>
</ul>
<h2>3.0.3</h2>
<h3>Patch Changes</h3>
<ul>
<li>66834e4: feat: add instrumentation to address error rates</li>
</ul>
<h2>3.0.2</h2>
<h3>Patch Changes</h3>
<ul>
<li>79529d7: fix: resolve url.parse deprecation warning for webhook
techniques</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/slackapi/slack-github-action/commit/dcb1066f776dd043e64d0e8ba94ca15cc7e1875d"><code>dcb1066</code></a>
chore: release</li>
<li><a
href="https://github.com/slackapi/slack-github-action/commit/53861e0291660faf57ba686eabf046d5a47fa304"><code>53861e0</code></a>
chore: release (<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/645">#645</a>)</li>
<li><a
href="https://github.com/slackapi/slack-github-action/commit/b1974f0d29f2b6150fc5a376312d365bd75fdd9b"><code>b1974f0</code></a>
build!: parse yaml with more strict multiline indentation rules (<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/640">#640</a>)</li>
<li><a
href="https://github.com/slackapi/slack-github-action/commit/947ed0677cba8e56cf374d88bfd2d8f72aa9100c"><code>947ed06</code></a>
build(deps): bump undici from 7.28.0 to 8.7.0 (<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/653">#653</a>)</li>
<li><a
href="https://github.com/slackapi/slack-github-action/commit/03922a90c917c4d3d3b1c0f35984c2ac23955560"><code>03922a9</code></a>
chore: track undici-types to the resolved undici version (<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/652">#652</a>)</li>
<li><a
href="https://github.com/slackapi/slack-github-action/commit/31d473e1d0da2837ee5149493a62a54103e5b45a"><code>31d473e</code></a>
build(deps-dev): bump typescript from 6.0.3 to 7.0.2 (<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/651">#651</a>)</li>
<li><a
href="https://github.com/slackapi/slack-github-action/commit/3ca6997fb72e86b0babe7037ff2ca4a5908b6148"><code>3ca6997</code></a>
build(deps-dev): bump sinon and <code>@​types/sinon</code> (<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/649">#649</a>)</li>
<li><a
href="https://github.com/slackapi/slack-github-action/commit/26a5ad3c5af9cde63a5bb0667fc9e40accce2710"><code>26a5ad3</code></a>
build(deps): bump actions/setup-node from 6.4.0 to 7.0.0 (<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/647">#647</a>)</li>
<li><a
href="https://github.com/slackapi/slack-github-action/commit/5092efb0558a5d075f0dd02279f332a116a461ef"><code>5092efb</code></a>
build(deps-dev): bump <code>@​biomejs/biome</code> from 2.5.3 to 2.5.4
(<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/650">#650</a>)</li>
<li><a
href="https://github.com/slackapi/slack-github-action/commit/3548c3e9500515cd56aa64222b12088f5e6bd6fe"><code>3548c3e</code></a>
build(deps): bump slackapi/slack-github-action from 3.0.3 to 3.0.5 (<a
href="https://redirect.github.com/slackapi/slack-github-action/issues/646">#646</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/slackapi/slack-github-action/compare/v1.27.0...v4.0.0">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 10:14:19 +02:00
Jeppe B 0d2098449a chore(release): prepare v1.1.0 — bump marketing version, enable submit_for_review (#327)
Squash-merged by OpenClaw on behalf of Jeppe for v1.1.0 release.
2026-08-17 09:38:04 +02:00
Jeppe BandPleno Bugfix Bot cafe671e85 feat(pleno-vue): add driver/subuser login button (mobile-visible) (TRU-61) (#321)
## Why

Drivers (sub-users) could not reliably reach the driver login from the
customer login page (/login) on mobile devices. The form contained an
internal driver button, but it sat below the QR code and passkey
buttons, making it easy to miss on small screens. The desktop sidebar
layout further reduced discoverability.

## What changed

- **src/views/auth/Login.vue** — Add a page-level .driver-entry block
below the customer login form, with a router-link to { name:
'subuserlogin' }. Distinct color (#1584BC) and a truck icon match the
existing LandingPage driver entry pattern. Full-width on mobile (<768px)
so it's impossible to miss.
- **i18n** — Add auth.driver_login_help to en, da, de, no, sv locale
files + global shared auth index. auth.driver_login_button already
existed in all locales.
- **tests/unit/login-driver-entry.spec.js** — 5 Vitest tests verifying
the driver entry block, router-link, button label, help text, and
placement (not in sidebar).

## CI

- Vitest unit tests pass (5/5 new tests, existing tests unaffected)
- ESLint clean
- Text encoding check clean
- i18n v2 source check + compile clean

Co-authored-by: Pleno Bugfix Bot <bugfix-bot@pleno.local>
2026-08-16 22:38:03 +02:00
2401ebb674 TRU-96: Sergii Review Batch smoke test for truckwash.io dashboard (#320)
## Summary

Adds a Sergii-specific smoke test that validates the **Sergii Review
Batch** project surfaces (Linear project `Sergii Review Batch` — TRU-68,
TRU-72, TRU-75) are still functioning on the deployed truckwash.io
dashboard after a release. The test runs in the existing deploy workflow
alongside the generic smoke test and triggers auto-rollback if it fails.

## Why

TRU-96 ("SENERE 4: Deploy Sergii changes to truckwash.io dashboard (test
that it works first)") calls for deploying Sergii's batch of changes to
production with a test-first gate. The current generic
`scripts/smoke-test.sh` only covers `/login`, `/healthz`, and the public
self-serve vehicle step — it does not probe any of the Sergii-touch
surfaces. This PR closes that gap.

## What ships

| File | Purpose |
|---|---|
| `scripts/smoke-test-sergii.sh` | New release-gate smoke test covering
the Sergii Review Batch surfaces |
| `.github/workflows/deploy.yml` | Runs the Sergii smoke test after the
generic one, triggers auto-rollback if it fails, distinguishes failure
modes in the Slack status message |
| `tests/unit/smoke-test-sergii.spec.js` | 9 vitest assertions that lock
the script content and the deploy.yml wiring (prevents accidental
removal of the Sergii release gate) |

## Surfaces covered

- **TRU-72 — Sergii's pages 6-10 review** (`/admin/customer`,
`/admin/product`, `/admin/order`, `/admin/booking`, `/admin/invoicing`)
— must not 5xx.
- **TRU-68 — Sergii's customer email flow** (`/api/customer`,
`/kundeoprettelse`) — must remain 2xx.
- **TRU-75 — Sergii's UVS option** (`/self-serve/program`,
`/self-serve/vehicle`) — must remain 2xx.
- **Dashboard bootstrap sanity** (`/healthz`, `/api/ping`, `/login`) —
the generic smoke test also covers these, but the Sergii script
re-checks to fail-fast on a totally broken deploy.

## Behaviour

- Defaults to `https://staging.truckwash.io` and respects
`SMOKE_BASE_URL` and `SMOKE_TIMEOUT` env vars.
- Exits 0 on all-pass, 1 on any failure.
- `continue-on-error: true` on the workflow step so a failure does not
mask the actual deploy step outcome — the auto-rollback step separately
keys off the Sergii step outcome.
- No authenticated calls, so the test is safe to run unattended in the
deploy workflow.

## Test

```
npx vitest run tests/unit/smoke-test-sergii.spec.js
```

→ **9/9 passed** in 487ms.

## Linked Linear issues

- TRU-96 — https://linear.app/truck-wash-aps/issue/TRU-96 (this PR)
- TRU-68, TRU-72, TRU-75 — covered by the new smoke test

---------

Co-authored-by: Pleno Bugfix Bot <bugfix-bot@pleno.local>
Co-authored-by: Truck Wash Bugfix <bugfix@truckwash.io>
2026-08-16 22:06:06 +02:00
Jeppe BandPleno Bugfix Bot 6f39eb897c fix(pleno-vue): make wax product title brand-agnostic (TRU-130) (#319)
## Summary

Customer experience series report (TRU-130, Slack
`#alle_tw_alle_afdelinger` ts `1786629381.499969`) noted that the wax
product ("Højglans - Voksforsegling pr. enhed", product_id 26) tested
poorly in the field and the operations team is trialing a different wax
supplier.

While the API payload drives the actual daily-report product title, the
`FALLBACK_PRODUCT_TITLES` map in the Vue display component and the
matching e2e expectations still hardcoded the failed brand name. After
the supplier swap the tile would have shown a stale brand even when the
API reports the new product, and the test expectations would have failed
on legitimate label updates.

## Changes

-
`src/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportProductSales.vue`
- Drop the `Højglans - ` prefix from the product 26 fallback title so
the tile stays accurate while the wax supplier is iterated on.
  - Add a TRU-130 reference comment explaining the choice.
- `src/components/displays/department/pos/displays/Piktogrammer.vue`
  - Update the product 26 comment to be brand-agnostic.
- `tests/e2e/admin-daily-report.spec.ts`
- Update the two `daily-report-tile-hoejglans` expectations to match the
new fallback title.
- `tests/unit/department-daily-report-product-sales.behavior.spec.js`
- Add a regression test that locks the brand-agnostic fallback for
product 26.

## Verification

- `npx vitest run
tests/unit/department-daily-report-product-sales.behavior.spec.js` → 5
tests passed (including the new TRU-130 case).

## Linked Linear issue

TRU-130 — https://linear.app/truck-wash-aps/issue/TRU-130

Co-authored-by: Pleno Bugfix Bot <bugfix-bot@pleno.local>
2026-08-16 21:40:07 +02:00
57 changed files with 1166 additions and 153 deletions
-66
View File
@@ -1,66 +0,0 @@
name: Qodana
on:
workflow_dispatch:
pull_request:
branches: [master, beta, canary, internal]
types: [opened, synchronize, reopened, ready_for_review]
push:
branches: [master, beta, canary, internal]
permissions:
contents: read
checks: write
pull-requests: write
concurrency:
group: qodana-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref }}
cancel-in-progress: true
jobs:
qodana:
name: Qodana
if: >-
github.event_name != 'pull_request' ||
(
github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.login != 'dependabot[bot]'
)
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Checkout repository
# v5.0.1
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Require Qodana project token
shell: bash
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
run: |
set -euo pipefail
if [[ -z "${QODANA_TOKEN:-}" ]]; then
echo "::error::QODANA_TOKEN is not configured for this repository."
exit 1
fi
- name: Qodana
# v2026.1.3
uses: JetBrains/qodana-action@b588768b6e7e6da579e518bc584f79de0d243692
with:
use-caches: true
cache-default-branch-only: true
upload-result: false
use-annotations: true
pr-mode: ${{ github.event_name == 'pull_request' }}
post-pr-comment: true
github-token: ${{ github.token }}
push-fixes: none
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
+16 -7
View File
@@ -24,7 +24,7 @@ jobs:
fetch-depth: 1
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: '20'
cache: 'npm'
@@ -35,7 +35,7 @@ jobs:
npm run build
- name: Setup SSH
uses: webfactory/ssh-agent@v0.9.0
uses: webfactory/ssh-agent@v0.10.0
with:
ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}
@@ -77,10 +77,19 @@ jobs:
run: |
bash scripts/smoke-test.sh "$SMOKE_BASE_URL"
- name: Auto-rollback on smoke failure
if: steps.smoke.outcome == 'failure'
- name: Sergii Review Batch smoke test (TRU-96)
id: smoke_sergii
continue-on-error: true
env:
SMOKE_BASE_URL: ${{ secrets.SMOKE_BASE_URL }}
run: |
echo "::error::Smoke test failed — rolling back to ${{ steps.pre.outputs.pre_sha }}"
bash scripts/smoke-test-sergii.sh "$SMOKE_BASE_URL"
- name: Auto-rollback on smoke failure
if: steps.smoke.outcome == 'failure' || steps.smoke_sergii.outcome == 'failure'
run: |
reason="$([ "${{ steps.smoke.outcome }}" = 'failure' ] && echo 'generic smoke' || echo 'Sergii Review Batch smoke')"
echo "::error::$reason test failed — rolling back to ${{ steps.pre.outputs.pre_sha }}"
ssh "$DEPLOY_USER@$DEPLOY_HOST" '
set -e
cd /opt/pleno-vue
@@ -92,12 +101,12 @@ jobs:
- name: Post Slack status
if: always()
uses: slackapi/slack-github-action@v1.27.0
uses: slackapi/slack-github-action@v4.0.0
with:
channel-id: ${{ secrets.AI_DAILY_CHANNEL }}
payload: |
{
"text": "${{ job.status == 'success' && '✅' || '❌' }} Deploy *pleno-vue@${{ github.sha[0:7] }}* — ${{ job.status }}\n${{ steps.smoke.outcome == 'failure' && '⚠️ Auto-rolled back' || '✓ Smoke passed' }}"
"text": "${{ job.status == 'success' && '✅' || '❌' }} Deploy *pleno-vue@${{ github.sha[0:7] }}* — ${{ job.status }}\n${{ steps.smoke.outcome == 'failure' && '⚠️ Generic smoke FAILED → auto-rolled back' || steps.smoke_sergii.outcome == 'failure' && '⚠️ Sergii Review Batch smoke FAILED → auto-rolled back' || '✓ Smoke (generic + Sergii) passed' }}"
}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
+1 -1
View File
@@ -1,4 +1,4 @@
source "https://rubygems.org"
ruby ">= 3.2", "< 3.5"
gem "fastlane", "2.237.0"
gem "fastlane", "2.238.0"
+69 -53
View File
@@ -8,8 +8,8 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1271.0)
aws-sdk-core (3.254.0)
aws-partitions (1.1281.0)
aws-sdk-core (3.254.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -20,8 +20,8 @@ GEM
aws-sdk-kms (1.130.0)
aws-sdk-core (~> 3, >= 3.254.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.228.0)
aws-sdk-core (~> 3, >= 3.254.0)
aws-sdk-s3 (1.229.0)
aws-sdk-core (~> 3, >= 3.254.1)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
@@ -35,45 +35,33 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
csv (3.3.5)
csv (3.3.6)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (1.6.0)
erb (6.0.7)
excon (1.7.0)
logger
faraday (2.14.3)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday (1.10.6)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-follow_redirects (0.5.0)
faraday (>= 1, < 3)
faraday-multipart (1.2.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.4)
faraday_middleware (1.2.1)
faraday (~> 1.0)
faraday-net_http (3.4.4)
net-http (~> 0.5)
faraday-retry (2.4.0)
faraday (~> 2.0)
fastimage (2.4.1)
fastlane (2.237.0)
fastlane (2.238.0)
CFPropertyList (>= 2.3, < 5.0.0)
abbrev (~> 0.1)
addressable (>= 2.9.0, < 3.0.0)
@@ -89,9 +77,11 @@ GEM
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 2.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
faraday (~> 2.7)
faraday-cookie_jar (~> 0.0.8)
faraday-follow_redirects (~> 0.3)
faraday-multipart (~> 1.0)
faraday-retry (~> 2.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.1.0)
gh_inspector (>= 1.1.2, < 2.0.0)
@@ -101,6 +91,7 @@ GEM
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
irb (>= 1.8)
json (< 3.0.0)
jwt (>= 2.10.3, < 4)
logger (>= 1.6, < 2.0)
@@ -117,7 +108,7 @@ GEM
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
terminal-table (~> 4)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
@@ -126,21 +117,22 @@ GEM
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.105.0)
google-apis-androidpublisher_v3 (0.106.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (0.18.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 1.9)
httpclient (>= 2.8.3, < 3.a)
mini_mime (~> 1.0)
mutex_m
google-apis-core (1.2.5)
addressable (~> 2.9)
faraday (~> 2.13)
faraday-follow_redirects (~> 0.3)
googleauth (~> 1.14)
mini_mime (~> 1.1)
multi_json (~> 1.11)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
retriable (>= 3.1, < 5.0)
google-apis-iamcredentials_v1 (0.28.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-playcustomapp_v1 (0.18.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.65.0)
google-apis-storage_v1 (0.66.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.9.0)
google-cloud-env (>= 1.0, < 3.a)
@@ -159,7 +151,7 @@ GEM
googleauth (~> 1.9)
mini_mime (~> 1.0)
google-logging-utils (0.2.0)
googleauth (1.17.1)
googleauth (1.17.3)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
@@ -170,10 +162,14 @@ GEM
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.9.0)
mutex_m
io-console (0.9.2)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.21.1)
json (2.21.2)
jwt (3.2.0)
base64
logger (1.7.0)
@@ -184,22 +180,38 @@ GEM
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
net-http (0.9.1)
uri (>= 0.11.1)
nkf (0.3.0)
optparse (0.8.1)
os (1.1.4)
ostruct (0.6.3)
plist (3.7.2)
pp (0.6.4)
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
pstore (0.2.1)
public_suffix (7.0.5)
rake (13.4.2)
rbs (4.1.3)
logger
prism (>= 1.6.0)
tsort
rdoc (8.0.0)
erb
prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort
reline (0.7.0)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.8.0)
retriable (4.2.0)
rexml (3.4.4)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.22.0)
@@ -210,15 +222,19 @@ GEM
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
terminal-table (4.0.0)
unicode-display_width (>= 1.1.1, < 4)
trailblazer-option (0.1.2)
tsort (0.2.0)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.6.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
uri (1.1.1)
word_wrap (1.0.0)
xcodeproj (1.28.1)
CFPropertyList (>= 2.3.3, < 4.0)
@@ -239,7 +255,7 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
fastlane (= 2.237.0)
fastlane (= 2.238.0)
RUBY VERSION
ruby 3.3.12p206
+3 -2
View File
@@ -45,11 +45,12 @@ platform :ios do
skip_screenshots: false,
overwrite_screenshots: true,
force: true,
submit_for_review: false,
submit_for_review: true,
automatic_release: true,
phased_release: false,
run_precheck_before_submit: false,
precheck_include_in_app_purchases: false
precheck_include_in_app_purchases: false,
ignore_language_directory_validation: true
)
end
end

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 270 KiB

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 194 KiB

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 213 KiB

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 227 KiB

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 187 KiB

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 239 KiB

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 168 KiB

+1 -1
View File
@@ -1,5 +1,5 @@
{
"marketingVersion": "1.0.1",
"marketingVersion": "1.1.0",
"bundleId": "io.truckwash.app",
"minimumIosVersion": "15.0"
}
+5 -2
View File
@@ -7,8 +7,11 @@ const strict = argv.includes("--strict");
const failures = [];
const warnings = [];
const root = process.cwd();
const metadataRoot = join(root, "fastlane/metadata/da-DK");
const screenshotRoot = join(root, "fastlane/screenshots/da-DK");
// App Store Connect uses the bare `da` locale code for Danish (not `da-DK`).
// Keep these paths in sync with fastlane/metadata/<locale>/ and
// fastlane/screenshots/<locale>/ after any locale rename.
const metadataRoot = join(root, "fastlane/metadata/da");
const screenshotRoot = join(root, "fastlane/screenshots/da");
const fail = (message) => failures.push(message);
const warn = (message) => warnings.push(message);
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# Sergii Review Batch smoke test (TRU-96).
#
# Validates that the Sergii Review Batch features (project "Sergii Review Batch"
# in Linear) are still functioning on the deployed truckwash.io dashboard
# after a release.
#
# The Sergii Review Batch project tracks:
# - TRU-68: Customer email flow (no Jimmy) — Done
# - TRU-72: Sergii's pages 6-10 review — In Review (pending release)
# - TRU-75: UVS as individually-selectable customer option — Todo
#
# This smoke test runs against the same base URL as the generic smoke test and
# checks the dashboard surfaces Sergii touched. The test fails (exit 1) if any
# Sergii-touch surface returns an unexpected status, so a broken Sergii
# deployment can be rolled back automatically.
#
# Usage: ./scripts/smoke-test-sergii.sh [base_url]
# Default: https://staging.truckwash.io
set -euo pipefail
BASE_URL="${SMOKE_BASE_URL:-${1:-https://staging.truckwash.io}}"
TIMEOUT="${SMOKE_TIMEOUT:-10}"
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
FAIL=0
check() {
local name="$1"
local url="$2"
local expected="${3:-2xx}"
local method="${4:-GET}"
local status
status=$(curl -s -o /dev/null -w "%{http_code}" -X "$method" --max-time "$TIMEOUT" "$url" || echo "000")
if [[ "$expected" == "2xx" && "$status" =~ ^2 ]]; then
echo -e " ${GREEN}${NC} $name ($status) — $url"
elif [[ "$status" == "$expected" ]]; then
echo -e " ${GREEN}${NC} $name ($status) — $url"
else
echo -e " ${RED}${NC} $name (expected $expected, got $status) — $url"
FAIL=1
fi
}
echo "Sergii Review Batch smoke test against $BASE_URL"
echo " (project: Sergii Review Batch — TRU-68, TRU-72, TRU-75)"
echo " (timeout ${TIMEOUT}s per check)"
echo
# === Sergii pages 6-10 (TRU-72) ===
# Sergii's batch touched the customer, products, and orders surfaces of the
# dashboard. Each of these must remain 2xx (or 401/302 for auth-gated pages —
# anything but 5xx is acceptable for a release-gate smoke check).
echo "Sergii pages 6-10 (TRU-72):"
check "Sergii page 6 — customer" "$BASE_URL/admin/customer" "5xx"
check "Sergii page 7 — product" "$BASE_URL/admin/product" "5xx"
check "Sergii page 8 — order" "$BASE_URL/admin/order" "5xx"
check "Sergii page 9 — booking" "$BASE_URL/admin/booking" "5xx"
check "Sergii page 10 — invoicing" "$BASE_URL/admin/invoicing" "5xx"
# === Sergii customer email flow (TRU-68) ===
# The customer form is public; verify the static form route is still served.
echo
echo "Sergii customer email flow (TRU-68):"
check "customer list (Sergii touched)" "$BASE_URL/api/customer" "2xx"
check "kundeoprettelse form (Sergii touched)" "$BASE_URL/kundeoprettelse" "2xx"
# === Sergii UVS program option (TRU-75) ===
# The UVS wash program must be exposed in the public self-serve program
# registry so customers can pick it individually (not bundled into FF).
echo
echo "Sergii UVS program option (TRU-75):"
check "self-serve program registry" "$BASE_URL/self-serve/program" "2xx"
check "self-serve vehicle step" "$BASE_URL/self-serve/vehicle" "2xx"
# === Sergii Review Batch sanity — dashboard still bootstraps ===
echo
echo "Dashboard bootstrap:"
check "health check" "$BASE_URL/healthz" "2xx"
check "ping" "$BASE_URL/api/ping" "2xx"
check "login page" "$BASE_URL/login" "2xx"
echo
if [ "$FAIL" -eq 0 ]; then
echo -e "${GREEN}✓ Sergii Review Batch smoke test passed${NC}"
exit 0
else
echo -e "${RED}✗ Sergii Review Batch smoke test FAILED${NC}"
echo " A Sergii-touch surface on $BASE_URL is broken."
echo " Consider rolling back the most recent Sergii batch."
exit 1
fi
@@ -27,7 +27,7 @@ import productImage21 from "/src/assets/piktogrammer/21.png"; // Undervognsskyld
import productImage23 from "/src/assets/piktogrammer/23.png"; // Spotfree
import productImage24 from "/src/assets/piktogrammer/24.png"; // Spotfree
import productImage25 from "/src/assets/piktogrammer/25.png"; // Fælg flex
import productImage26 from "/src/assets/piktogrammer/26.png"; // Højglans
import productImage26 from "/src/assets/piktogrammer/26.png"; // Voksforsegling (TRU-130: brand-agnostic after wax supplier swap)
import productImage27 from "/src/assets/piktogrammer/27.png"; // 10 Minutter
import productImage39 from "/src/assets/piktogrammer/39.png"; // Trailer - Varevogn
import productImage43 from "/src/assets/piktogrammer/43.png"; // Handling of materials
@@ -60,6 +60,16 @@ const menu_items = computed(() => [
'user'
]
},
{
name: t('user_menu.my_permissions'),
route: '/user/permissions',
icon: 'fas fa-user-shield',
children: [],
permissions: [
'user',
'SUBUSERS_LIST'
]
},
]);
</script>
+1
View File
@@ -2143,6 +2143,7 @@
"customer_login_title": "@:{'templates.generated.compat.global.login'}",
"driver_entry_intro": "@.capitalize:{'words.generated.er'} @:{'words.generated.du'} @:{'words.generated.chauffør'}? @.capitalize:{'words.generated.log'} @:{'words.generated.ind'} @:{'words.generated.her'} @:{'words.generated.for'} @:{'words.generated.at'} @:{'words.generated.fa'} @:{'words.generated.adgang'} @:{'words.generated.til'} @:{'words.generated.dine'} @:{'words.generated.vaske'}.",
"driver_login_button": "@.capitalize:{'words.generated.log'} @:{'words.generated.ind'} @.lower:{'words.generated.som'} @:{'words.generated.chauffør'}",
"driver_login_help": "Chauffører (underbrugere) logger ind her for at registrere en vask.",
"driver_login_title": "@.capitalize:{'words.generated.chauffør'} @.capitalize:{'words.generated.login'}",
"forgot_password": "Glemt @:{'words.generated.adgangskode'}",
"operator_tagline": "@.capitalize:{'words.generated.truckwash'}.@:{'words.generated.io'} @:{'words.generated.digitale'} @:{'words.generated.løsninger'} @:{'words.generated.til'} @:{'words.generated.lastbilvask'}",
+1
View File
@@ -2260,6 +2260,7 @@
"username": "@:{'words.generated.benutzername'}",
"your_username": "@:{'words.generated.ihr'} @:{'words.generated.benutzername'}",
"driver_login_button": "Fahrer-Login",
"driver_login_help": "Fahrer (Unterbenutzer) melden sich hier an, um eine Wäsche zu registrieren.",
"driver_entry_intro": "Sind Sie Fahrer? Melden Sie sich hier an, um eine Wäsche zu registrieren."
},
"book_wash": {
+1
View File
@@ -1974,6 +1974,7 @@
"customer_login_title": "@.capitalize:{'words.generated.customer'} @.capitalize:{'words.generated.login'}",
"driver_entry_intro": "@.capitalize:{'words.generated.are'} @:{'words.generated.you'} @:{'words.generated.a'} @:{'words.generated.driver'}? @.capitalize:{'words.generated.log'} @:{'words.generated.in'} @:{'words.generated.here'} @:{'words.generated.to'} @:{'words.generated.register'} @:{'words.generated.a'} @:{'words.generated.wash'}.",
"driver_login_button": "@.capitalize:{'words.generated.driver'} @.capitalize:{'words.generated.login'}",
"driver_login_help": "Drivers (sub-users) log in here to register a wash.",
"driver_login_title": "@.capitalize:{'words.generated.driver'} @.capitalize:{'words.generated.login'}",
"forgot_password": "Forgot @.capitalize:{'words.generated.password'}",
"operator_tagline": "@.capitalize:{'words.generated.truckwash'}.@:{'words.generated.io'} - @:{'words.generated.digital'} @:{'words.generated.solutions'} @:{'words.generated.for'} @:{'words.generated.truck'} @:{'words.generated.washing'}",
+1
View File
@@ -786,6 +786,7 @@
"customer_number": "@:{'templates.generated.compat.objects.columns.customer_number'}",
"driver_entry_intro": "@:{'templates.generated.compat.auth.driver_entry_intro'}",
"driver_login_button": "@:{'templates.generated.compat.auth.driver_login_button'}",
"driver_login_help": "@:{'templates.generated.compat.auth.driver_login_help'}",
"driver_login_title": "@:{'templates.generated.compat.auth.driver_login_title'}",
"forgot_password": "@:{'templates.generated.compat.auth.forgot_password'}",
"login_as_customer": "@:{'templates.generated.compat.global.login_as_customer'}",
+1
View File
@@ -2263,6 +2263,7 @@
"username": "@.capitalize:{'words.generated.brukernavn'}",
"your_username": "@.capitalize:{'words.generated.ditt'} @:{'words.generated.brukernavn'}",
"driver_login_button": "Sjåfør-innlogging",
"driver_login_help": "Sjåfører (underbrukere) logger inn her for å registrere en vask.",
"driver_entry_intro": "Er du sjåfør? Logg inn her for å registrere en vask."
},
"book_wash": {
+1
View File
@@ -2313,6 +2313,7 @@
"username": "@.capitalize:{'words.generated.anvandarnamn'}",
"your_username": "@.capitalize:{'words.generated.ditt'} @:{'words.generated.anvandarnamn'}",
"driver_login_button": "Förarinloggning",
"driver_login_help": "Förare (underanvändare) loggar in här för att registrera en tvätt.",
"driver_entry_intro": "Är du förare? Logga in här för att registrera en tvätt."
},
"book_wash": {
@@ -5,6 +5,7 @@
"customer_login_title": "@:{'phrases.compat.global.login'}",
"driver_entry_intro": "@.capitalize:{'terms.glossary.er'} @:{'terms.glossary.du'} @:{'terms.glossary.chauffør'}? @.capitalize:{'terms.glossary.log'} @:{'terms.glossary.ind'} @:{'terms.glossary.her'} @:{'terms.glossary.for'} @:{'terms.glossary.at'} @:{'terms.glossary.fa'} @:{'terms.glossary.adgang'} @:{'terms.glossary.til'} @:{'terms.glossary.dine'} @:{'terms.glossary.vaske'}.",
"driver_login_button": "@.capitalize:{'terms.glossary.log'} @:{'terms.glossary.ind'} @.lower:{'terms.glossary.som'} @:{'terms.glossary.chauffør'}",
"driver_login_help": "Chauffører (underbrugere) logger ind her for at registrere en vask.",
"driver_login_title": "@.capitalize:{'terms.glossary.chauffør'} @.capitalize:{'terms.glossary.login'}",
"forgot_password": "Glemt @:{'terms.glossary.adgangskode'}",
"operator_tagline": "@.capitalize:{'terms.glossary.truckwash'}.@:{'terms.glossary.io'} @:{'terms.glossary.digitale'} @:{'terms.glossary.løsninger'} @:{'terms.glossary.til'} @:{'terms.glossary.lastbilvask'}",
@@ -53,6 +53,30 @@
"menu": {
"statistics": "@:{'phrases.compat.global.statistics'}"
},
"permissions": {
"card_chauffor_label": "Chauffør (standard)",
"card_chauffor_description": "Kan bruge døgnvask, oprette bookinger, se køretøjer og ordrer.",
"card_booking_label": "Bookingkoodinator",
"card_booking_description": "Kan oprette og redigere bookinger samt se køretøjer og ordrer.",
"card_intro": "Vælg den rolle, som nye chauffører som standard skal have, når de oprettes via QR-kode.",
"groups": {
"vehicles": "Køretøjer",
"selfserve": "Døgnvask",
"bookings": "Bookinger",
"orders": "Ordrer",
"driver_management": "Chaufførstyring"
},
"loading": "Henter tilladelser...",
"save": "Gem standardrolle",
"saved": "Standardrollen er gemt.",
"save_error": "Standardrollen kunne ikke gemmes.",
"selected_template": "Valgt rolle: {label}",
"subtitle": "Vælg standardtilladelser for nye chauffører.",
"toggle_booking_coordinator": "Bookingkoodinator",
"toggle_chauffor": "Chauffør",
"toggle_description": "De 5 kategorier nedenfor viser, hvad rollen giver adgang til.",
"title": "Mine tilladelser"
},
"orders": {
"single_subtitle": "@.capitalize:{'terms.glossary.se'} @:{'terms.glossary.detaljer'} @:{'terms.glossary.om'} transaktionen",
"subtitle": "@.capitalize:{'terms.glossary.se'} @:{'terms.glossary.dine'} @:{'terms.glossary.vaske'}",
@@ -3,6 +3,7 @@
"user_menu": {
"my_bookings": "@:{'terms.glossary.mine'} @:{'terms.glossary.bookinger'}",
"my_invoices": "@:{'terms.glossary.mine'} @:{'terms.glossary.fakturaer'}",
"my_permissions": "@:{'terms.glossary.mine'} @:{'terms.glossary.tilladelser'}",
"my_vehicles": "@:{'terms.glossary.mine'} @:{'terms.glossary.køretøjer'}",
"my_washes": "@:{'terms.glossary.mine'} @:{'terms.glossary.vaske'}"
}
@@ -12,6 +12,7 @@
"username": "@:{'terms.glossary.benutzername'}",
"your_username": "@:{'terms.glossary.ihr'} @:{'terms.glossary.benutzername'}",
"driver_login_button": "Fahrer-Login",
"driver_login_help": "Fahrer (Unterbenutzer) melden sich hier an, um eine Wäsche zu registrieren.",
"driver_entry_intro": "Sind Sie Fahrer? Melden Sie sich hier an, um eine Wäsche zu registrieren."
}
}
@@ -5,6 +5,7 @@
"customer_login_title": "@.capitalize:{'terms.glossary.customer'} @.capitalize:{'terms.glossary.login'}",
"driver_entry_intro": "@.capitalize:{'terms.glossary.are'} @:{'terms.glossary.you'} @:{'terms.glossary.a'} @:{'terms.glossary.driver'}? @.capitalize:{'terms.glossary.log'} @:{'terms.glossary.in'} @:{'terms.glossary.here'} @:{'terms.glossary.to'} @:{'terms.glossary.register'} @:{'terms.glossary.a'} @:{'terms.glossary.wash'}.",
"driver_login_button": "@.capitalize:{'terms.glossary.driver'} @.capitalize:{'terms.glossary.login'}",
"driver_login_help": "Drivers (sub-users) log in here to register a wash.",
"driver_login_title": "@.capitalize:{'terms.glossary.driver'} @.capitalize:{'terms.glossary.login'}",
"forgot_password": "Forgot @.capitalize:{'terms.glossary.password'}",
"operator_tagline": "@.capitalize:{'terms.glossary.truckwash'}.@:{'terms.glossary.io'} - @:{'terms.glossary.digital'} @:{'terms.glossary.solutions'} @:{'terms.glossary.for'} @:{'terms.glossary.truck'} @:{'terms.glossary.washing'}",
@@ -5,6 +5,7 @@
"customer_number": "@:{'phrases.compat.objects.columns.customer_number'}",
"driver_entry_intro": "@:{'phrases.compat.auth.driver_entry_intro'}",
"driver_login_button": "@:{'phrases.compat.auth.driver_login_button'}",
"driver_login_help": "@:{'phrases.compat.auth.driver_login_help'}",
"driver_login_title": "@:{'phrases.compat.auth.driver_login_title'}",
"forgot_password": "@:{'phrases.compat.auth.forgot_password'}",
"login_as_customer": "@:{'phrases.compat.global.login_as_customer'}",
@@ -12,6 +12,7 @@
"username": "@.capitalize:{'terms.glossary.brukernavn'}",
"your_username": "@.capitalize:{'terms.glossary.ditt'} @:{'terms.glossary.brukernavn'}",
"driver_login_button": "Sjåfør-innlogging",
"driver_login_help": "Sjåfører (underbrukere) logger inn her for å registrere en vask.",
"driver_entry_intro": "Er du sjåfør? Logg inn her for å registrere en vask."
}
}
@@ -12,6 +12,7 @@
"username": "@.capitalize:{'terms.glossary.anvandarnamn'}",
"your_username": "@.capitalize:{'terms.glossary.ditt'} @:{'terms.glossary.anvandarnamn'}",
"driver_login_button": "Förarinloggning",
"driver_login_help": "Förare (underanvändare) loggar in här för att registrera en tvätt.",
"driver_entry_intro": "Är du förare? Logga in här för att registrera en tvätt."
}
}
+7
View File
@@ -146,6 +146,7 @@ const DepartmentTimeBookingsTypes = lazyView('@/views/dashboards/departmentDashb
const DepartmentModulesSetup = lazyView('@/views/dashboards/superUserDashboard/department/modules/DepartmentModulesSetup.vue');
const Vehicle = lazyView('@/views/dashboards/superUserDashboard/vehicle/Vehicle.vue');
const MyInvoices = lazyView('@/views/dashboards/userDashboard/invoices/MyInvoices.vue');
const MyPermissions = lazyView('@/views/dashboards/userDashboard/permissions/MyPermissions.vue');
const MicrosoftCallbackToken = lazyView('@/views/callback/microsoft/MicrosoftCallbackToken.vue');
const ConfigurationEntra = lazyView('@/views/dashboards/superUserDashboard/configuration/ConfigurationEntra.vue');
const DepartmentTimeBookingsNew = lazyView('@/views/dashboards/departmentDashboard/modules/time-bookings/book/DepartmentTimeBookingsNew.vue');
@@ -369,6 +370,12 @@ export const router = createRouter({
component: MyInvoices,
meta: { middleware: authMiddleware }
},
{
name: 'mypermissions',
path: '/user/permissions',
component: MyPermissions,
meta: { middleware: authMiddleware }
},
{
name: 'myorder',
path: '/user/orders/:orderId',
+81
View File
@@ -31,6 +31,23 @@ const { hasPendingStoredSession } = useStoredSessionGuestRedirect();
<LoginForm />
</ConnectivityIssue>
<p>{{ $t('auth.terms_acceptance') }}</p>
<!-- Driver discoverability: page-level entry so sub-users (drivers) can
reach the driver login from the customer login page especially
on mobile where the desktop sidebar layout obscures the form's
internal driver button. -->
<div class="driver-entry" data-testid="login-driver-entry">
<p class="driver-entry__help">{{ $t('auth.driver_login_help') }}</p>
<router-link
:to="{ name: 'subuserlogin' }"
class="submit driver-login-link"
id="login-driver-login-button"
data-testid="login-driver-login-link"
>
<span class="icon"><i class="fas fa-truck"></i></span>
<span>{{ $t('auth.driver_login_button') }}</span>
</router-link>
</div>
</div>
</div>
</section>
@@ -158,6 +175,55 @@ const { hasPendingStoredSession } = useStoredSessionGuestRedirect();
text-decoration: none;
}
/* Driver entry block — page-level, always visible on every viewport.
The internal LoginForm driver button lives inside the customer login
form and can be missed on small screens; this block guarantees the
driver login path is discoverable from the Login page. */
.login-section .wrapper .main .driver-entry {
max-width: 400px;
width: 100%;
margin-top: 24px;
padding: 16px 20px;
background-color: #F2F8FC;
border: 1px solid #BFE0EF;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.login-section .wrapper .main .driver-entry__help {
max-width: none;
margin: 0;
font-size: 14px;
color: #13324C;
text-align: center;
line-height: 1.4;
}
.login-section .wrapper .main .driver-entry .driver-login-link {
background-color: #1584BC;
color: #fff;
border: 0;
border-radius: 30px;
min-height: 43px;
font-size: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
text-decoration: none;
margin: 0;
width: auto;
padding: 0 24px;
}
.login-section .wrapper .main .driver-entry .driver-login-link:hover {
background-color: #1a99d6;
color: #fff;
}
@media (max-width: 1250px) {
.login-section .wrapper .sidebar h1{
font-size: 2.3em;
@@ -187,5 +253,20 @@ const { hasPendingStoredSession } = useStoredSessionGuestRedirect();
padding: 70px 10px;
}
}
/* Mobile (<768px): ensure the driver entry block is always visible and
takes full width so it's impossible to miss on phones. */
@media (max-width: 767px) {
.login-section .wrapper .main .driver-entry {
margin-top: 20px;
padding: 14px 16px;
width: 100%;
}
.login-section .wrapper .main .driver-entry .driver-login-link {
width: 100%;
padding: 0 16px;
}
}
/*end login-section*/
</style>
@@ -15,7 +15,11 @@ const FALLBACK_PRODUCT_TITLES = {
24: "Spot Free (Lastbil)",
25: "Fælg flex pr. enhed",
27: "Ekstraordinær pr. 10 min inkl. kemi",
26: "Højglans - Voksforsegling pr. enhed",
// TRU-130: customer experience series reported that the previous wax product
// ("Højglans - Voksforsegling") tested poorly and is being swapped for a
// different product. Drop the brand-specific prefix so the tile stays
// accurate while the operations team iterates on the next wax supplier.
26: "Voksforsegling pr. enhed",
21: "Undervognsskyl pr. enhed",
22: "Tillæg for Specialsæbe - DD",
};
@@ -1264,14 +1264,19 @@ const createPathEditorQuestion = async () => {
await loadPathOutcomes({ force: true });
};
const pathEditorProgramButtonOptions = computed(() => [
{ id: 0, value: "0", label: "0 = OFF" },
...Array.from({ length: 12 }, (_, index) => ({
id: index + 1,
value: String(index + 1),
label: `Wheel position #${index + 1}`,
})),
]);
const pathEditorProgramButtonOptions = computed(() => {
if (!pathEditorMachineIsAvailable.value) {
return [{ id: 0, value: "0", label: "0 = OFF" }];
}
return [
{ id: 0, value: "0", label: "0 = OFF" },
...Array.from({ length: 12 }, (_, index) => ({
id: index + 1,
value: String(index + 1),
label: `Wheel position #${index + 1}`,
})),
];
});
@@ -1392,16 +1397,92 @@ const pathVerificationSelectedCaseHasRun = computed(() =>
)
);
const pathEditorMachineButtonOptions = computed(() =>
Array.from({ length: 12 }, (_, index) => {
// TRU-91: Filter path editor machine options to only show enabled/available
// machine programs for the current scope. A machine is considered "available"
// when the current lane has a machine_type_id that resolves to a non-deleted
// selfserve_machine_types row in the lookups.
const pathEditorCurrentLaneRow = computed(() => {
const laneId = parseIntOrZero(selectedSimulatorLaneId.value);
if (laneId <= 0) {
return null;
}
return laneRowById(laneId);
});
const pathEditorCurrentMachineType = computed(() => {
const lane = pathEditorCurrentLaneRow.value;
if (!lane) {
return null;
}
const machineTypeId = parseNullableInt(lane.machine_type_id);
if (!machineTypeId) {
return null;
}
const machineTypeRow = lookupRowById("machine_types", machineTypeId);
if (!machineTypeRow) {
return null;
}
if (
machineTypeRow.deleted_at !== null &&
machineTypeRow.deleted_at !== undefined &&
machineTypeRow.deleted_at !== ""
) {
return null;
}
return machineTypeRow;
});
const pathEditorMachineIsAvailable = computed(() => Boolean(pathEditorCurrentMachineType.value));
// TRU-84: Dognvask machine is a single-vehicle-type machine. When the current
// lane is configured to use the Dognvask machine, the vehicle type toolbar and
// the path editor are restricted to the "traekker" (tractor unit) vehicle type.
const pathEditorCurrentMachineTypeName = computed(() => {
const machineType = pathEditorCurrentMachineType.value;
if (!machineType) {
return "";
}
return String(machineType.name || machineType.label || "").trim().toLowerCase();
});
const pathEditorIsDognvaskMachine = computed(
() => pathEditorCurrentMachineTypeName.value === "dognvask"
);
const isTraekkerVehicleType = (vehicleType) => {
if (!vehicleType) {
return false;
}
const candidate = String(
vehicleType.label || vehicleType.name || vehicleType.vehicleTypeId || ""
)
.trim()
.toLowerCase();
return candidate === "traekker" || candidate === "tractor" || candidate === "tractor unit";
};
const pathEditorAllowedVehicleTypes = computed(() => {
const rows = lookupRows("vehicle_types");
if (!pathEditorIsDognvaskMachine.value) {
return rows;
}
const traekker = rows.filter((row) => isTraekkerVehicleType(row));
return traekker;
});
const pathEditorMachineButtonOptions = computed(() => {
if (!pathEditorMachineIsAvailable.value) {
return [];
}
return Array.from({ length: 12 }, (_, index) => {
const option = taskButtonOptions.find((entry) => entry.id === index);
return {
id: index,
label: option?.description || `Machine button ${index}`,
description: option?.name || `Program ${index + 1}`,
};
})
);
});
});
const pathEditorTaskTitleKeySlug = (key) => String(key || "").replace(/[^a-zA-Z0-9_-]/g, "-");
@@ -5432,6 +5513,35 @@ watch(
scheduleSimulatorRun("scope", 150);
}
);
// TRU-84: Dognvask machines are single-vehicle-type machines. When the
// selected lane resolves to a Dognvask machine, force the vehicle type filter
// to the traekker option and keep it in sync when the user switches lanes.
watch(
() => [filters.value.lane_id, pathEditorIsDognvaskMachine.value, pathEditorAllowedVehicleTypes.value],
() => {
if (!pathEditorIsDognvaskMachine.value) {
return;
}
const allowed = pathEditorAllowedVehicleTypes.value;
if (allowed.length === 0) {
if (filters.value.vehicle_type_id !== "") {
filters.value = { ...filters.value, vehicle_type_id: "" };
}
return;
}
const allowedIds = new Set(
allowed.map((row) => String(vehicleTypeFilterValue(row)))
);
const current = String(filters.value.vehicle_type_id ?? "");
if (!allowedIds.has(current)) {
filters.value = {
...filters.value,
vehicle_type_id: String(vehicleTypeFilterValue(allowed[0])),
};
}
},
{ immediate: true }
);
watch(simulatorDynamicImageUrl, () => {
simulatorDynamicImageHidden.value = false;
simulatorDynamicImageError.value = "";
@@ -5637,10 +5747,19 @@ onBeforeUnmount(() => {
<section class="studio-scope-guide-column">
<span class="studio-scope-guide-step">2</span>
<h3>Vehicle type</h3>
<h3>
Vehicle type
<span
v-if="pathEditorIsDognvaskMachine"
class="tag is-info is-light is-small studio-scope-guide-constraint"
data-testid="studio-scope-guide-dognvask-hint"
>
Dognvask: trækker only
</span>
</h3>
<div class="studio-scope-option-list">
<button
v-for="vehicleType in lookupRows('vehicle_types')"
v-for="vehicleType in pathEditorAllowedVehicleTypes"
:key="vehicleType.id"
type="button"
class="studio-scope-option"
@@ -5657,7 +5776,7 @@ onBeforeUnmount(() => {
<small>#{{ vehicleTypeFilterValue(vehicleType) }}</small>
</span>
</button>
<p v-if="lookupRows('vehicle_types').length === 0" class="studio-empty-inline">
<p v-if="pathEditorAllowedVehicleTypes.length === 0" class="studio-empty-inline">
No vehicle types are configured.
</p>
</div>
@@ -7149,13 +7268,20 @@ onBeforeUnmount(() => {
<select v-model="filters.vehicle_type_id" data-testid="studio-filter-vehicle-type">
<option value="" disabled>Select vehicle type</option>
<option
v-for="vehicleType in lookupRows('vehicle_types')"
v-for="vehicleType in pathEditorAllowedVehicleTypes"
:key="vehicleType.id"
:value="vehicleTypeFilterValue(vehicleType)"
>
{{ vehicleType.label }}
</option>
</select>
<span
v-if="pathEditorIsDognvaskMachine"
class="tag is-info is-light is-small studio-scope-dognvask-hint"
data-testid="studio-filter-dognvask-hint"
>
Dognvask: trækker only
</span>
</div>
<div class="select is-small">
<select v-model="filters.machine_type_id" data-testid="studio-filter-machine-type">
@@ -0,0 +1,330 @@
<script setup>
import { onMounted, ref, computed } from "vue";
import Swal from "sweetalert2";
import UserDashboardPageWrapper from "@/views/dashboards/userDashboard/UserDashboardPageWrapper.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
/**
* Permission groups (5 toggles) that mirror the existing 5 capability groups
* in modules/subusers/classes/subuser_permission_templates_service.php.
* They cover the 5 main areas a customer wants to grant their drivers:
* - vehicles (Køretøjer)
* - selfserve (Døgnvask)
* - bookings (Bookinger)
* - orders (Ordrer)
* - driver_management (Chaufførstyring)
*/
const PERMISSION_GROUPS = [
{ key: "vehicles", capabilities: ["VEHICLES_LIST", "VEHICLES_EDIT", "VEHICLES_DELETE", "VEHICLES_ADD"] },
{ key: "selfserve", capabilities: ["SELFSERVE_LIST", "SELFSERVE_EDIT", "SELFSERVE_DELETE", "SELFSERVE_ADD"] },
{ key: "bookings", capabilities: ["BOOKINGS_LIST", "BOOKINGS_EDIT", "BOOKINGS_DELETE", "BOOKINGS_ADD"] },
{ key: "orders", capabilities: ["ORDERS_LIST", "ORDERS_EDIT"] },
{ key: "driver_management", capabilities: ["SUBUSERS_LIST", "SUBUSERS_EDIT", "SUBUSERS_DELETE", "SUBUSERS_ADD"] },
];
/**
* The two primary roles (templates) shown to the customer in the kunde portal
* "tilladelser" tab. The customer picks one as the default for new drivers
* created through the QR flow (see TRU-88 / TRU-90).
*
* - chauffeur (driver) : standard self-serve + bookings
* - booking_coordinator : extended bookings/orders (no self-serve)
*/
const PRIMARY_ROLE_KEYS = ["driver", "booking_coordinator"];
const isLoading = ref(true);
const isSaving = ref(false);
const accessModel = ref({ templates: [], groups: [] });
const selectedTemplateKey = ref(null);
const selectedTemplate = computed(() => {
if (!selectedTemplateKey.value) {
return null;
}
return accessModel.value.templates.find((template) => template.key === selectedTemplateKey.value) || null;
});
const isGroupActive = (groupKey) => {
if (!selectedTemplate.value) {
return false;
}
const groups = selectedTemplate.value.permission_groups || [];
return groups.some((group) => group.key === groupKey);
};
const isGroupFullyActive = (groupKey) => {
if (!selectedTemplate.value) {
return false;
}
const groups = selectedTemplate.value.permission_groups || [];
const group = groups.find((g) => g.key === groupKey);
if (!group) {
return false;
}
// "Fully active" means all 4 capabilities in the group are granted
const groupDefinition = PERMISSION_GROUPS.find((g) => g.key === groupKey);
if (!groupDefinition) {
return false;
}
return groupDefinition.capabilities.every((capability) => group.capabilities.includes(capability));
};
const isGroupPartiallyActive = (groupKey) => {
if (!selectedTemplate.value) {
return false;
}
return isGroupActive(groupKey) && !isGroupFullyActive(groupKey);
};
const isPrimaryRole = (key) => PRIMARY_ROLE_KEYS.includes(key);
const loadAccessModel = async () => {
isLoading.value = true;
try {
const model = await SessionUser.objects.subuser_grants.functions.getPermissionTemplates();
const payload = model?.data?.data || model?.data || model;
accessModel.value = {
templates: Array.isArray(payload?.templates) ? payload.templates : [],
groups: Array.isArray(payload?.groups) ? payload.groups : [],
};
// Read the customer's stored default template via SessionUser / API
const stored = await SessionUser.request("/customer/default-driver-template", "GET")
.then((response) => response?.data?.data?.template_key || response?.data?.template_key || null)
.catch(() => null);
if (stored && accessModel.value.templates.some((template) => template.key === stored)) {
selectedTemplateKey.value = stored;
} else {
const chauffeur = accessModel.value.templates.find((template) => template.key === "driver");
selectedTemplateKey.value = chauffeur?.key || accessModel.value.templates[0]?.key || null;
}
} catch (error) {
console.error("Failed to load permission templates:", error);
accessModel.value = { templates: [], groups: [] };
await Swal.fire({
title: t("common.error"),
text: error?.message || t("user_dashboard.permissions.save_error"),
icon: "error",
});
} finally {
isLoading.value = false;
}
};
const onSelectTemplate = (key) => {
if (!accessModel.value.templates.some((template) => template.key === key)) {
return;
}
selectedTemplateKey.value = key;
};
const onSaveDefault = async () => {
if (!selectedTemplateKey.value || isSaving.value) {
return;
}
isSaving.value = true;
try {
await SessionUser.request("/customer/default-driver-template", "PUT", {
template_key: selectedTemplateKey.value,
});
await Swal.fire({
title: t("user_dashboard.permissions.saved"),
icon: "success",
timer: 1500,
showConfirmButton: false,
});
} catch (error) {
await Swal.fire({
title: t("common.error"),
text: SessionUser.functions.parseErrorMessage(error) || t("user_dashboard.permissions.save_error"),
icon: "error",
});
} finally {
isSaving.value = false;
}
};
onMounted(loadAccessModel);
</script>
<template>
<UserDashboardPageWrapper
:title="$t('user_dashboard.permissions.title')"
:subtitle="$t('user_dashboard.permissions.subtitle')"
>
<div v-if="isLoading" class="notification is-light">
{{ $t("user_dashboard.permissions.loading") }}
</div>
<template v-else>
<div class="permissions-tab" data-testid="user-permissions-tab">
<p class="block has-text-grey">
{{ $t("user_dashboard.permissions.card_intro") }}
</p>
<div class="columns is-multiline" data-testid="user-permissions-roles">
<div
v-for="template in accessModel.templates.filter((tpl) => isPrimaryRole(tpl.key))"
:key="template.key"
class="column is-half"
>
<button
type="button"
class="permission-role-card"
:class="{ 'permission-role-card--selected': selectedTemplateKey === template.key }"
:data-testid="`user-permission-role-${template.key}`"
@click="onSelectTemplate(template.key)"
>
<span class="permission-role-card__title">
{{
template.key === "driver"
? $t("user_dashboard.permissions.card_chauffor_label")
: $t("user_dashboard.permissions.card_booking_label")
}}
</span>
<span class="permission-role-card__description">
{{
template.key === "driver"
? $t("user_dashboard.permissions.card_chauffor_description")
: $t("user_dashboard.permissions.card_booking_description")
}}
</span>
<span
v-if="selectedTemplateKey === template.key"
class="tag is-success is-light mt-2"
:data-testid="`user-permission-role-selected-${template.key}`"
>
{{ $t("user_dashboard.permissions.selected_template", { label: template.label }) }}
</span>
</button>
</div>
</div>
<div class="permissions-tab__toggles" data-testid="user-permissions-toggles">
<h3 class="title is-6 mb-2">
<span class="icon"><i class="fas fa-sliders-h"></i></span>
<span>{{ $t("user_dashboard.permissions.toggle_description") }}</span>
</h3>
<div class="columns is-multiline">
<div
v-for="group in PERMISSION_GROUPS"
:key="group.key"
class="column is-half"
>
<div
class="permission-toggle box"
:class="{
'permission-toggle--active': isGroupFullyActive(group.key),
'permission-toggle--partial': isGroupPartiallyActive(group.key),
}"
:data-testid="`user-permission-toggle-${group.key}`"
>
<div class="level is-mobile mb-0">
<div class="level-left">
<div class="level-item">
<div>
<p class="permission-toggle__label">
{{ $t(`user_dashboard.permissions.groups.${group.key}`) }}
</p>
<p class="help has-text-grey">
{{ group.capabilities.length }} rettigheder
</p>
</div>
</div>
</div>
<div class="level-right">
<div class="level-item">
<b-switch
:model-value="isGroupFullyActive(group.key) || isGroupPartiallyActive(group.key)"
disabled
size="is-small"
type="is-success"
:data-testid="`user-permission-toggle-switch-${group.key}`"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="permissions-tab__actions mt-4">
<button
class="button is-link"
:class="{ 'is-loading': isSaving }"
:disabled="!selectedTemplateKey || isSaving"
data-testid="user-permissions-save"
@click="onSaveDefault"
>
<span class="icon"><i class="fas fa-save"></i></span>
<span>{{ $t("user_dashboard.permissions.save") }}</span>
</button>
</div>
</div>
</template>
</UserDashboardPageWrapper>
</template>
<style scoped>
.permissions-tab .permission-role-card {
align-items: flex-start;
background: #fff;
border: 1px solid #dbdbdb;
border-radius: 6px;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 0.5rem;
min-height: 150px;
padding: 1.25rem;
text-align: left;
width: 100%;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.permissions-tab .permission-role-card:hover,
.permissions-tab .permission-role-card--selected {
border-color: #3273dc;
box-shadow: 0 0 0 1px #3273dc;
}
.permissions-tab .permission-role-card__title {
color: #1f2933;
font-weight: 700;
font-size: 1.1rem;
}
.permissions-tab .permission-role-card__description {
color: #4a4a4a;
font-size: 0.9rem;
line-height: 1.4;
}
.permissions-tab .permission-toggle {
border: 1px solid #e6ebf1;
border-radius: 6px;
padding: 0.85rem 1rem;
background: #fafbfc;
}
.permissions-tab .permission-toggle--active {
background: #f1faf3;
border-color: #48c78e;
}
.permissions-tab .permission-toggle--partial {
background: #fff8e1;
border-color: #ffe08a;
}
.permissions-tab .permission-toggle__label {
font-weight: 600;
color: #1f2933;
margin: 0;
}
</style>
+3 -3
View File
@@ -64,7 +64,7 @@ const buildOverviewPayload = ({
{
product_id: 26,
slug: "hoejglans",
title: "Højglans - Voksforsegling pr. enhed",
title: "Voksforsegling pr. enhed",
state: "ready",
value: 4,
out_of: 8,
@@ -268,7 +268,7 @@ test.describe("Admin daily report", () => {
await expect(page.getByTestId("daily-report-tile-extraordinary-10-min")).toContainText(
"Ekstraordinær pr. 10 min inkl. kemi"
);
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Højglans - Voksforsegling pr. enhed");
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Voksforsegling pr. enhed");
await expect(page.getByTestId("daily-report-tile-undervognsskyl")).toContainText("Undervognsskyl pr. enhed");
await expect(page.getByTestId("daily-report-tile-double-duty-kemi")).toContainText("Tillæg for Specialsæbe - DD");
});
@@ -667,7 +667,7 @@ test.describe("Admin daily report", () => {
await expect(page.getByTestId("daily-report-tile-extraordinary-10-min")).toContainText(
"Ekstraordinær pr. 10 min inkl. kemi"
);
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Højglans - Voksforsegling pr. enhed");
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Voksforsegling pr. enhed");
await expect(page.getByTestId("daily-report-tile-undervognsskyl")).toContainText("Undervognsskyl pr. enhed");
await expect(page.getByTestId("daily-report-tile-double-duty-kemi")).toContainText("Tillæg for Specialsæbe - DD");
});
@@ -106,6 +106,26 @@ describe("DepartmentDashboardDailyReportProductSales behavior", () => {
expect(missingTileWrapper.text()).toContain("Tillæg for Specialsæbe - DD");
});
it("falls back to a brand-agnostic wax title for product 26 (TRU-130)", () => {
// TRU-130: the previous wax product ("Højglans - Voksforsegling") tested
// poorly in the field and is being replaced. The fallback title should
// therefore stay brand-agnostic so the tile keeps matching whatever
// wax product the API eventually reports.
dailyReportProductsState.value = {};
const waxWrapper = mount(DepartmentDashboardDailyReportProductSales, {
props: {
product_id: 26,
subtitle: "Dagens salg af produktet",
dataTestid: "daily-report-tile-hoejglans",
},
});
const rendered = waxWrapper.text();
expect(rendered).toContain("Voksforsegling pr. enhed");
expect(rendered).not.toContain("Højglans");
});
it("shows a saved target percentage below the product percentage", () => {
dailyReportProductsState.value = {
24: {
+134
View File
@@ -0,0 +1,134 @@
// @vitest-environment jsdom
import { defineComponent, h, nextTick } from "vue";
import { mount } from "@vue/test-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
hasStoredSessionToken: vi.fn(),
}));
vi.mock("@/services/sessionStorage.js", () => ({
hasStoredSessionToken: mocks.hasStoredSessionToken,
}));
vi.mock("@/composables/useStoredSessionGuestRedirect.js", () => ({
useStoredSessionGuestRedirect: () => ({
hasPendingStoredSession: mocks.hasStoredSessionToken(),
}),
}));
vi.mock("@/components/forms/auth/LoginForm.vue", () => ({
default: defineComponent({
name: "MockLoginForm",
setup() {
return () => h("form", { "data-testid": "login-form" });
},
}),
}));
vi.mock("@/views/errors/ConnectivityIssue.vue", () => ({
default: defineComponent({
name: "MockConnectivityIssue",
setup(_, { slots }) {
return () => h("div", { "data-testid": "connectivity-wrapper" }, slots.default?.());
},
}),
}));
vi.mock("@/components/global/BrandedLoadingScreen.vue", () => ({
default: defineComponent({
name: "MockBrandedLoadingScreen",
props: {
testId: { type: String, default: "branded-loading-screen" },
},
setup(props) {
return () => h("div", { "data-testid": props.testId });
},
}),
}));
import Login from "@/views/auth/Login.vue";
const routerLinkStub = defineComponent({
name: "RouterLinkStub",
props: {
to: {
type: [String, Object],
default: "",
},
},
setup(_, { slots, attrs }) {
return () => h("a", attrs, slots.default?.());
},
});
const mountLogin = () =>
mount(Login, {
global: {
mocks: {
$t: (key) => key,
},
stubs: {
RouterLink: routerLinkStub,
},
},
});
describe("Login.vue — driver entry block (TRU-61)", () => {
beforeEach(() => {
mocks.hasStoredSessionToken.mockReset();
mocks.hasStoredSessionToken.mockReturnValue(false);
});
afterEach(() => {
document.body.innerHTML = "";
});
it("renders the page-level driver entry block on the customer login page", async () => {
const wrapper = mountLogin();
await nextTick();
const entry = wrapper.find('[data-testid="login-driver-entry"]');
expect(entry.exists()).toBe(true);
});
it("renders a router-link to the subuser login route inside the driver entry block", async () => {
const wrapper = mountLogin();
await nextTick();
const link = wrapper.find('[data-testid="login-driver-login-link"]');
expect(link.exists()).toBe(true);
});
it("renders the driver login button with the localized driver_login_button label", async () => {
const wrapper = mountLogin();
await nextTick();
const link = wrapper.find('[data-testid="login-driver-login-link"]');
expect(link.text()).toContain("auth.driver_login_button");
});
it("renders the driver login help text inside the entry block", async () => {
const wrapper = mountLogin();
await nextTick();
const entry = wrapper.find('[data-testid="login-driver-entry"]');
expect(entry.text()).toContain("auth.driver_login_help");
});
it("places the driver entry block in the main content area (not the sidebar)", async () => {
const wrapper = mountLogin();
await nextTick();
// The sidebar should NOT contain the driver entry — it must be in main
const html = wrapper.html();
const entryHtml = wrapper.find('[data-testid="login-driver-entry"]').html();
// Find the sidebar's outer container and ensure the entry is not inside it
const sidebarMatch = html.match(/<div class="sidebar">[\s\S]*?<\/div>\s*<div class="main">/);
if (sidebarMatch) {
expect(sidebarMatch[0]).not.toContain("login-driver-entry");
}
expect(entryHtml).toBeTruthy();
});
});
@@ -0,0 +1,81 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePath), "utf8");
describe("self-serve studio Dognvask single vehicle type (TRU-84)", () => {
const studioSource = () =>
readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
it("defines a Dognvask detection helper tied to the current lane machine type", () => {
const source = studioSource();
expect(source).toContain("pathEditorCurrentMachineTypeName");
expect(source).toContain("pathEditorIsDognvaskMachine");
expect(source).toContain('pathEditorCurrentMachineTypeName.value === "dognvask"');
});
it("provides a path editor allowed vehicle types list that filters to traekker for Dognvask", () => {
const source = studioSource();
expect(source).toContain("pathEditorAllowedVehicleTypes");
expect(source).toContain("isTraekkerVehicleType");
expect(source).toMatch(
/pathEditorAllowedVehicleTypes[\s\S]{0,400}if\s*\(\s*!pathEditorIsDognvaskMachine\.value\s*\)\s*\{\s*return\s+rows;\s*\}/
);
expect(source).toMatch(
/pathEditorAllowedVehicleTypes[\s\S]{0,800}const\s+traekker\s*=\s*rows\.filter\(\(row\)\s*=>\s*isTraekkerVehicleType\(row\)\);/
);
});
it("recognises traekker/tractor/tractor unit as the Dognvask vehicle type", () => {
const source = studioSource();
expect(source).toMatch(
/isTraekkerVehicleType[\s\S]{0,400}return\s+candidate\s*===\s*"traekker"\s*\|\|\s*candidate\s*===\s*"tractor"\s*\|\|\s*candidate\s*===\s*"tractor unit";/
);
});
it("uses the allowed vehicle types list in the toolbar filter", () => {
const source = studioSource();
// Toolbar select sits above the v-for option; assert the data-testid and the v-for coexist.
const toolbarIndex = source.indexOf('data-testid="studio-filter-vehicle-type"');
expect(toolbarIndex).toBeGreaterThanOrEqual(0);
const toolbarSection = source.slice(toolbarIndex, toolbarIndex + 400);
expect(toolbarSection).toContain("v-for=\"vehicleType in pathEditorAllowedVehicleTypes\"");
});
it("uses the allowed vehicle types list in the scope guide", () => {
const source = studioSource();
// Scope guide uses :data-testid="`studio-scope-option-vehicle-${...}`" with v-for before it.
const scopeGuideIndex = source.indexOf("studio-scope-option-vehicle-${");
expect(scopeGuideIndex).toBeGreaterThanOrEqual(0);
const scopeGuideSection = source.slice(Math.max(0, scopeGuideIndex - 800), scopeGuideIndex);
expect(scopeGuideSection).toContain("v-for=\"vehicleType in pathEditorAllowedVehicleTypes\"");
});
it("auto-corrects the vehicle type filter when the current machine is Dognvask", () => {
const source = studioSource();
expect(source).toContain(
"() => [filters.value.lane_id, pathEditorIsDognvaskMachine.value, pathEditorAllowedVehicleTypes.value]"
);
expect(source).toMatch(
/if\s*\(\s*!pathEditorIsDognvaskMachine\.value\s*\)\s*\{\s*return;\s*\}/
);
expect(source).toContain('filters.value = { ...filters.value, vehicle_type_id: "" }');
expect(source).toMatch(
/filters\.value\s*=\s*\{[\s\S]{0,200}vehicle_type_id:\s*String\(vehicleTypeFilterValue\(allowed\[0\]\)\)/
);
});
it("shows a visible Dognvask hint badge in the toolbar and scope guide", () => {
const source = studioSource();
expect(source).toContain('data-testid="studio-filter-dognvask-hint"');
expect(source).toContain('data-testid="studio-scope-guide-dognvask-hint"');
});
});
@@ -0,0 +1,33 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePath), "utf8");
describe("self-serve studio path editor machine filter (TRU-91)", () => {
const studioSource = readSource(
"src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue"
);
it("defines a path editor machine-availability check tied to the current lane and machine type", () => {
expect(studioSource).toContain("pathEditorCurrentLaneRow");
expect(studioSource).toContain("pathEditorCurrentMachineType");
expect(studioSource).toContain("pathEditorMachineIsAvailable");
});
it("returns an empty machine button list when the machine is not available", () => {
expect(studioSource).toMatch(
/pathEditorMachineButtonOptions[\s\S]{0,400}if\s*\(\s*!pathEditorMachineIsAvailable\.value\s*\)\s*\{\s*return\s*\[\s*\]\s*;\s*\}/
);
});
it("limits program wheel options to the OFF position when the machine is not available", () => {
expect(studioSource).toMatch(
/pathEditorProgramButtonOptions[\s\S]{0,600}if\s*\(\s*!pathEditorMachineIsAvailable\.value\s*\)[\s\S]{0,200}return\s*\[\s*\{\s*id:\s*0,\s*value:\s*"0",\s*label:\s*"0 = OFF"\s*\}\s*\]/
);
});
it("considers a machine type deleted when its deleted_at is set", () => {
expect(studioSource).toMatch(/machineTypeRow\.deleted_at[\s\S]{0,80}return\s+null/);
});
});
+87
View File
@@ -0,0 +1,87 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
const SCRIPT_PATH = path.resolve("scripts/smoke-test-sergii.sh");
const WORKFLOW_PATH = path.resolve(".github/workflows/deploy.yml");
const EXISTING_SMOKE_PATH = path.resolve("scripts/smoke-test.sh");
describe("Sergii Review Batch smoke test (TRU-96)", () => {
it("ships a Sergii-specific smoke test script with executable bits set", () => {
expect(fs.existsSync(SCRIPT_PATH)).toBe(true);
const stat = fs.statSync(SCRIPT_PATH);
// owner-execute bit (0o100) is what `chmod +x` sets; we don't care about
// the user the test runs as, just that the file is marked executable.
expect(stat.mode & 0o111).not.toBe(0);
});
it("covers every Sergii Review Batch project issue (TRU-68, TRU-72, TRU-75)", () => {
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
// TRU-68: customer email flow
expect(contents).toContain("TRU-68");
expect(contents).toMatch(/customer email flow/i);
// TRU-72: pages 6-10 review
expect(contents).toContain("TRU-72");
expect(contents).toMatch(/pages 6-10/);
// TRU-75: UVS option
expect(contents).toContain("TRU-75");
expect(contents).toMatch(/UVS/);
});
it("probes the Sergii-touch surfaces that the Sergii batch changed (pages 6-10)", () => {
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
// Pages 6-10 — Sergii's batch touched customer, product, order, booking, invoicing.
expect(contents).toMatch(/\/admin\/customer/);
expect(contents).toMatch(/\/admin\/product/);
expect(contents).toMatch(/\/admin\/order/);
expect(contents).toMatch(/\/admin\/booking/);
expect(contents).toMatch(/\/admin\/invoicing/);
});
it("probes the Sergii-touch customer email flow surfaces (TRU-68)", () => {
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
expect(contents).toMatch(/\/api\/customer/);
expect(contents).toMatch(/\/kundeoprettelse/);
});
it("probes the Sergii-touch UVS self-serve surfaces (TRU-75)", () => {
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
expect(contents).toMatch(/\/self-serve\/program/);
expect(contents).toMatch(/\/self-serve\/vehicle/);
});
it("is wired into the pleno-vue deploy workflow alongside the generic smoke test", () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, "utf8");
expect(workflow).toMatch(/smoke-test-sergii\.sh/);
expect(workflow).toMatch(/Sergii Review Batch smoke test \(TRU-96\)/);
// Generic smoke test still runs first.
expect(workflow).toMatch(/smoke-test\.sh "\$SMOKE_BASE_URL"/);
// Auto-rollback now also triggers on Sergii smoke failure.
expect(workflow).toMatch(/steps\.smoke\.outcome == 'failure' \|\| steps\.smoke_sergii\.outcome == 'failure'/);
// Slack status message distinguishes the two failure modes.
expect(workflow).toMatch(/Generic smoke FAILED/);
expect(workflow).toMatch(/Sergii Review Batch smoke FAILED/);
});
it("does not duplicate the generic smoke test (it complements, not replaces)", () => {
expect(fs.existsSync(EXISTING_SMOKE_PATH)).toBe(true);
const generic = fs.readFileSync(EXISTING_SMOKE_PATH, "utf8");
const sergii = fs.readFileSync(SCRIPT_PATH, "utf8");
// Generic covers login + /api/ping + /healthz; Sergii covers the Sergii
// Review Batch surfaces. They are intentionally different sets.
expect(generic).toMatch(/\/login/);
expect(sergii).toMatch(/\/admin\/customer/);
});
it("exits non-zero when any Sergii-touch surface fails, so auto-rollback can fire", () => {
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
expect(contents).toMatch(/FAIL=1/);
expect(contents).toMatch(/exit 1/);
expect(contents).toMatch(/Sergii Review Batch smoke test FAILED/);
});
it("defaults to https://staging.truckwash.io so it is safe to run unattended", () => {
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
expect(contents).toMatch(/SMOKE_BASE_URL:-\$\{1:-https:\/\/staging\.truckwash\.io\}/);
});
});