Compare commits

...
Author SHA1 Message Date
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
Jeppe BJeppe BPleno Bugfix Botjeppemaxclaw[bot] <bot@jeppemaxclaw.local>
670746d70c fix(pleno-vue): propagate department selector to Selvvask usage query (TRU-11) (#317)
## Summary

Fixes TRU-11: when the department selector on the dashboard changes, the
Selvvask (self-wash) usage view did not re-query with the new
department. Both the orders list and the summary cards were bound to the
original department because the `HallId` filter was applied only once at
component setup.

## Root cause

`XLVaskUsagePagination.vue` derived `effectiveDepartmentId` once at
setup time and called `setFilter('HallId', ...)` a single time. There
was no `watch` on the department, so changing the `departmentId` prop or
the `departmentId` route param left the filter and the previously loaded
list untouched.

## Fix

- Convert `routeDepartmentId` and `effectiveDepartmentId` to `computed`
properties so they react to changes in the prop and the route param.
- Add a `watch(effectiveDepartmentId, ...)` that:
- Calls `setFilter('HallId', newId, false)` to update the filter, or
`setFilter('HallId', '*', false)` when the department is unset.
- Calls `loadList()` and `loadSummary()` to re-issue the Selvvask usage
query and refresh the summary cards.
- Pass the active department to the summary endpoint
(`/modules/xlvask/services/usage/orders/summary`) so the summary counts
also track the new department.

## Tests

Added
`tests/unit/xlvask-usage-pagination-department-propagation.spec.js` with
5 source-based assertions covering the computed department, the watcher,
the loadList/loadSummary re-issuance, the unset case, and the summary
params.

```
$ npx vitest run tests/unit/xlvask-usage-pagination-department-propagation.spec.js
✓ XLVaskUsagePagination department (HallId) propagation
  ✓ reacts to department changes via a computed effectiveDepartmentId
  ✓ watches the effective department and re-applies the HallId filter
  ✓ re-issues the usage query when the department changes
  ✓ clears the HallId filter when the department is unset
  ✓ includes the active department in the summary query params
Test Files  1 passed (1)
Tests       5 passed (5)
```

Existing related specs still pass (`xlvask-usage-pagination-404`,
`self-serve-pagination-machine-scope`, `pagination-date-selection`).

## Out of scope

`InvoicingBillingPeriodViewSelfWash.vue` does not pass `departmentId`
directly; department propagation there goes through the route or any
future parent selector. The fix in `XLVaskUsagePagination` covers all
current callers (`DepartmentPosSync.vue` and any future parent that
passes the prop or sets the route param).

## Refs

- Linear: TRU-11
- AUT-7

---------

Co-authored-by: Jeppe B <jeppe@copenhagentruckwash.io>
Co-authored-by: Pleno Bugfix Bot <bugfix-bot@pleno.local>
Co-authored-by: jeppemaxclaw[bot] <bot@jeppemaxclaw.local>
2026-08-16 18:10:06 +02:00
a345d91ae4 feat(pleno-vue): warning on red cars (TRU-99) (#318)
## Summary

Closes (heuristic for) **TRU-99 / SENERE 7**: warn operators in the
customer portal wash flow when a customer is flagged as a red car.

## Detection rule — heuristic, Mads to refine

The canonical rule (plate scan vs. red-car tag) is still TBD by Mads. As
a sensible default that fits the existing data model, this PR uses a
**manual customer-attribute flag**:

- If a customer has one of the configurable attribute keys set, the
warning fires. Default keys: `isRedCar`, `is_red_car`, `redCar`,
`red_car`.
- Detection is **case-insensitive** and the key list is **configurable**
(callers can override) so the rule can be tightened later without
touching the UI.
- Detection lives in `src/composables/redCarDetector.js`; reactive
loading lives in `src/composables/useRedCarWarning.js`.
- The composable reuses the existing `/customer/attributes` endpoint via
`customerAttributeService.js` — no backend change needed.

## UI

- New `RedCarWarning.vue` component renders a dismissable Buefy warning
in the vehicle step of the self-serve flow, with title + reason + care
suggestion.
- Wired into `MyWashStart.vue` via the existing `VehicleInputSection` /
`SelfServeVehicleStep` props. The composable is called with the
effective customer number (authenticated subuser or typed-in).
- Translations added in all 5 locales: `da`, `en`, `sv`, `de`, `no`
(source + regenerated runtime files).

## Tests

- 22 detector unit tests (positive, negative, case-insensitive, custom
keys, dedupe, normalisation).
- 5 i18n key presence tests across all 5 locales.
- 4 `RedCarWarning` component tests (conditional render, dismiss
wiring).

All new + existing related tests pass: `vitest run` on
`red-car-detector`, `red-car-warning-i18n`, `red-car-warning`,
`my-wash-start`, `customer-rule-registry`, `customer-rule-tooltip` →
**84/84 green**.

## Out of scope / not touched

- Backend / API: reused existing `/customer/attributes` endpoint.
- `openclaw.json`, deployment, merge — not touched (per task
constraints).
- Customer-rule registry: not added to `CUSTOMER_RULE_DEFINITIONS`
because the red-car flag is a soft warning, not a product-restriction
rule. If Mads wants it surfaced in the customer rule manager UI, that is
a follow-up.

## Files

- `src/composables/redCarDetector.js` (new)
- `src/composables/useRedCarWarning.js` (new)
- `src/components/displays/selfServe/RedCarWarning.vue` (new)
- `src/components/displays/selfServe/SelfServeVehicleStep.vue` (prop +
render)
-
`src/views/dashboards/userDashboard/wash/components/VehicleInputSection.vue`
(prop pass-through)
- `src/views/dashboards/userDashboard/wash/MyWashStart.vue` (composable
+ prop binding)
- `src/i18n/source/{da,en,sv,de,no}/phrases/compat/self_wash/index.json`
(new keys)
- `src/i18n/generated/{da,en,sv,de,no}-v2.json` (regenerated)
- `tests/unit/red-car-detector.spec.js` (new)
- `tests/unit/red-car-warning-i18n.spec.js` (new)
- `tests/unit/red-car-warning.spec.js` (new)

Refs: TRU-99

---------

Co-authored-by: Jeppe B <jeppe@copenhagentruckwash.io>
Co-authored-by: Frontend Subagent <frontend-agent@openclaw.local>
Co-authored-by: Pleno Bugfix Bot <bugfix-bot@pleno.local>
2026-08-16 16:22:05 +02:00
Jeppe B 19fbbcddce test(pleno-vue): snapshot-lock SELF_SERVE_TASK_BUTTON_OPTIONS shape (TRU-19) (#316)
Adds a snapshot test that locks the full {id, name, description} triple for every entry in SELF_SERVE_TASK_BUTTON_OPTIONS. The previous test only checked counts and that the ids were sequential; this test catches reordering, renames, and unexpected entries as well.

The wash bay exposes 12 programs and 3 special buttons (reset / program picker / start). The dashboard renders one entry per program number from this array, so the entire array must remain exactly 15 entries long.

Test result: 10/10 vitest passed (was 9, +1 new snapshot test)

Refs TRU-19
2026-08-16 13:00:19 +02:00
Jeppe B 7817f0c13b fix(pleno-vue): widen flagged-wash date window to ±7d (TRU-10) (#315)
Closes the user-reported symptom where washes spanning midnight fell outside the visible date window when navigating from an invoice-period flag.

Changes:
- Add FLAGGED_WASH_DATE_WINDOW_DAYS = 7 constant
- Add shiftDateOnly() helper that pads an ISO-8601 date by ±N days (UTC math, no DST drift)
- initialDateFrom/initialDateTo now compute [start - 7d, start + 7d] when a flagged wash is in the route
- Add vitest source assertion to lock the new behavior

Test result: 68/68 vitest passed (was 67, +1 new test)

Refs TRU-10
2026-08-16 13:00:17 +02:00
Jeppe BandJeppe c19aeffb98 fix(i18n): add no_xlvask_usage_log_metadata to en, de, no, sv locales (#314)
Resolves the 'da v2 key set should match en v2' failure on master after
PR #313 landed.

The first i18n fix PR (#313) only updated the da source. The 'da v2 key
set should match en v2' test in i18n-v2-integrity.spec.ts asserts that
all 5 active locales' generated v2 files have the same key set. Adding
the new key to da without adding it to en/de/no/sv left their v2 files
out of sync, breaking the gate.

This commit adds the key to all 4 non-da active locales. Each phrase
delegates the registration label to the existing 'xlvask_usage_log' key.
Sources are now consistent across da, de, en, no, sv.

After this lands, master should be green across all Quality-i18n /
Quality-lint / unit / e2e-pr-changed / e2e-pr-pr / e2e-pr-ct suites.

Co-authored-by: Jeppe <jeppe@copenhagentruckwash.io>
2026-08-16 10:50:07 +02:00
Jeppe BandJeppe 2e1d7f3e8f fix(i18n): make no_xlvask_usage_log_metadata a linked reference in global fallback (#313)
Resolves the failing master CI on pleno-vue after PR #312.

## Problem

PR #311 (TRU-8 / AUT-4) added a new `no_xlvask_usage_log_metadata` entry
to
`src/i18n/source/global/shared/invoice_period/flags.json` as a literal
Danish string. The integrity test added in PR #306 (TRU-9):
`tests/e2e/i18n-v2-integrity.spec.ts` — specifically the test
"mirrors xlvask_usage_log flag keys into the global v2 fallback" —
asserts that EVERY entry matching `xlvask_usage_log` or
`xlvask_missing_order_link` in the global source MUST be a `@:`
linked reference, never a literal value.

Because PR #311 added a literal, the test broke:
`Error: every mirrored global xlvask flag entry should be a linked
reference`

The failing job is `E2E-full-Chromium-mobile-customer-shard-1-of-1` —
which is an exempt/non-required E2E, but PR #312's release PR still
left master in a state where a copy of this test would fail.

## Fix

This commit:

1. Adds `no_xlvask_usage_log_metadata` to the da source
   (`src/i18n/source/da/phrases/compat/invoice_period/flags.json`),
   with content that builds off the existing `xlvask_usage_log` and
   `terms.glossary.for` keys — no user-facing text change.
2. Converts the global entry to the canonical linked-reference form:

`@:{'phrases.compat.invoice_period.flags.preview.no_xlvask_usage_log_metadata'}`
3. Regenerates `src/i18n/generated/global-v2.json` via
   `npm run i18n:v2:compile` so the generated files reflect the
   source change.

## Test impact

After the change, the `i18n-v2-integrity.spec.ts` test will pass again:

- The da source has `preview.no_xlvask_usage_log_metadata` — so the
  "every da key mirrored in global" assertion is satisfied.
- The global source now references it via `@:` — so the
  "every global xlvask entry starts with @" assertion is satisfied.

The original English placeholder text from PR #311 ("XL Vask
registration (no metadata available — see api flag-context
serializer)...") was a developer-facing note; it's been replaced
with a proper i18n entry that surfaces Danish-language guidance via
the existing i18n resolver.

This commit targets master directly because master is currently red.

Co-authored-by: Jeppe <jeppe@copenhagentruckwash.io>
2026-08-16 09:59:36 +02:00
7c9eb8b644 chore(release): merge develop into master — XL Vask integration fixes (Aug 15 2026) (#312)
Brings all of the develop branch's commits into master.

## What this contains

The 9 commits on develop that landed in this round — all XL Vask-related
UI fixes plus the 'visible primary-button hover state' visual-diff:

- **PR #303** (TRU-5 / AUT-1) — style(AUT-1): visible primary-button
hover state + visual diff
- **PR #304** (TRU-10 / AUT-6) — fix(invoicing-period): propagate
flagged wash start date to Selvvask view
- **PR #305** (TRU-12 / AUT-8) — feat(xlvask): render friendly notice
for 404 from /modules/xlvask/services/usage/orders
- **PR #306** (TRU-9 / AUT-5) — i18n(test): lock in xlvask_review /
xlvask_usage_log mirroring to the global v2 fallback
- **PR #307** (TRU-13 / AUT-9) — i18n(xlvask_review): translate missing
keys for no, sv, de, en
- **PR #308** (TRU-15 / AUT-11) — test(e2e): add Playwright smoke test
for XL Vask flag → Selvvash navigation
- **PR #309** (TRU-11 / AUT-7) — feat(TRU-11): propagate department
selector to Selvvask usage query
- **PR #310** (TRU-19 / AUT-15) — test(TRU-19): lock self-serve program
number range + button registry contract
- **PR #311** (TRU-8 / AUT-4) — fix(invoicing-flag-list): explain empty
XL Vask hover preview when flag context has no metadata

## Why

The XL Vask integration bug surfaced from the user-reported message "XL
Vask-registreringen er hverken ignoreret eller knyttet til en ordre i
den valgte periode. doesn't show the wash." After dispatching 9
diagnostic + fix tasks and merging all 9 PRs into develop via the
OpenSymphony orchestrator running against MiniMax M3, this PR is the
canonical release to bring develop's accumulated changes into master.

No new code in this PR — just the squash-merged output of the 9 source
PRs combined into a single develop→master merge.

## Verification

All 9 source PRs passed:
- Required CI (Action Runners)
- App Store Readiness  
- Quality lint/i18n/build/unit/e2e suites

The required checks on this PR will run the same gate.

## Notes

- The api repo has its own equivalent PR/merge — see CHANGELOG for that
side.

---------

Co-authored-by: Jeppe <jeppe@copenhagentruckwash.io>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-08-16 08:58:06 +02:00
Jeppe BandWorktree Fix Verifier e2cc76091f autoheal(test): re-target audited add-on note test from product 24 to 25 (#302)
Fixes master CI failure: E2E-full-Chromium-mobile-admin-shard-2-of-2

- SHA: 4db3be34
- Failing check: E2E-full-Chromium-mobile-admin-shard-2-of-2, Full E2E
summary
- Run: 31768269122
- Root cause: PR #301 dropped product 24 from
AUDITED_ORDER_ITEM_PRODUCT_IDS and updated the auditedProduct fixture
(id 24→25), but the e2e test still targeted product 24 via token,
reference, test ID, and orderItemCreates lookup. Re-target those four
identifiers from 24 to 25 so they align with the updated fixture and the
AUDITED set {21, 22, 25, 26, 27}.

Files touched: tests/e2e/pos-mobile-order-flow.spec.js (7 insertions, 7
deletions).

Filed by master-autoheal-agent cron.

Co-authored-by: Worktree Fix Verifier <agent@truckwash.local>
2026-08-14 11:33:04 +02:00
Jeppe Bandopenhands 4db3be34f8 fix(pleno-vue): exclude spot-free-lastbil from audited add-on note dialog (#301)
## Problem

The mobile POS step 2 \"enter note\" dialog was triggering for product
24
(\"Højtryk - ekstra tid\" / spotfree-lastbil) because product 24 was
listed
in both:

- `AUDITED_ORDER_ITEM_PRODUCT_IDS` in
`src/components/shop/OrdersItems.vue`
- `AFFECTED_PRODUCT_IDS` in
`services/nginx/app/classes/order_item_reason_policy.php`

Product 24 is the \"spot-free-lastbil\" package, not an audited
extra-time
add-on — the dialog should only appear for {21, 22, 25, 26, 27}.

## Fix

Drop product 24 from both source-of-truth lists, plus the matching test
fixtures and the e2e fixture.

## Changes

- `src/components/shop/OrdersItems.vue`: drop 24 from
  `AUDITED_ORDER_ITEM_PRODUCT_IDS` Set.
- `tests/unit/orders-items.spec.js`: drop 24 from `auditedProductIds`,
  swap the three `createOrderItem(...)` call sites that used 24 for 25,
  and add an explicit `AUDITED_ORDER_ITEM_PRODUCT_IDS` membership test
  that locks down 24 == false.
- `tests/e2e/support/mobilePos.js`: mirror the
`AUDITED_ORDER_ITEM_PRODUCT_IDS`
  change so the e2e harness matches the production set.
- `tests/e2e/pos-mobile-order-flow.spec.js`: re-target the \"prompts for
a
  required reason note for audited add-on products that are not the
  chemistry product\" case from product 24 to product 25
  (\"Fælg flex pr. enhed\"), since 24 is no longer audited.
- (api) `services/nginx/app/classes/order_item_reason_policy.php`: drop
24
  from `AFFECTED_PRODUCT_IDS` (companion change in a separate PR in the
  api repo).

## Verification

- 1761/1762 unit tests pass locally (the one failure is an unrelated
  `cpanel-deploy.spec.js` case that requires the system `zip` binary).
- Lint passes.
- Production build succeeds.

Tracked under workboard-94209138-31f6-422e-ac8c-181ad391b8a7.

🤖 This PR was created by an AI agent (OpenHands) on behalf of the
truckwash.io team.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-08-13 21:00:32 +02:00
b1e0c61df0 Collect audit reason for extra time sales (#240)
## Summary
- Add a shared POS audit helper for approved 10-minute extra sale
reason/comment payloads.
- Prompt for audit metadata in desktop add/copy, desktop item edit,
booking hydration, and mobile completion rebuild flows.
- Include preview evidence files under
`docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/`.

## Verification
- `npm ci --legacy-peer-deps`
- `npm run lint`
- `npm run build`

## Visual change previews
### View: POS extra sale audit
**Description:** POS order item add/edit flows now require an approved
reason for “10 min ekstra”, with a comment field available and required
for the `other` reason.

#### Mobile (390x844)
**Before:**
![Mobile
before](../blob/69fa62f6e38e5c6a28d6aa00c95f749df33b3e0a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-before.png?raw=true)

**After:**
![Mobile
after](../blob/69fa62f6e38e5c6a28d6aa00c95f749df33b3e0a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-after.png?raw=true)

#### Tablet (768x1024)
**Before:**
![Tablet
before](../blob/69fa62f6e38e5c6a28d6aa00c95f749df33b3e0a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-before.png?raw=true)

**After:**
![Tablet
after](../blob/69fa62f6e38e5c6a28d6aa00c95f749df33b3e0a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-after.png?raw=true)

#### Desktop (1440x900)
**Before:**
![Desktop
before](../blob/69fa62f6e38e5c6a28d6aa00c95f749df33b3e0a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-before.png?raw=true)

**After:**
![Desktop
after](../blob/69fa62f6e38e5c6a28d6aa00c95f749df33b3e0a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-after.png?raw=true)

## Notes
- Automatic merge remains disabled per Workboard contract.

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-08-13 17:49:27 +02:00
Jeppe Bandopenhands eb8482585b fix(orders): make reason_comment fallback robust for audited order items (#300)
## Problem

POST /master/api/order/items still returns

> Product 24: Reason comment is required for this product

for products in {21, 22, 24, 25, 26, 27}, even after #296 landed the
mobile POS step 2 note prompt.

The previous `buildAuditedOrderItemReasonPayload` only fell back through
`reason.reason_comment → reason.comment → notes → ''`. Any code path
that
calls `createOrderItem` without populating `notes` (copy-last-wash,
future callers, or even a user who clears the prompt) sent
`reason_comment: ""` and the backend correctly rejected it.

## Fix

* `buildAuditedOrderItemReasonPayload` now uses a `trimmedFirstNonEmpty`
  helper and walks
`reason.reason_comment → reason.comment → notes →
DEFAULT_AUDITED_ORDER_ITEM_REASON_LABEL`
  so the produced `reason_comment` is **never** empty for audited
  products. `reason_code` and `reason_label_snapshot` keep the same
  fallback semantics.
* `createCopiedOrderItem` in `POSDepartmentProcess.vue` now forwards
  the source order item's `reason_code`, `reason_label_snapshot`, and
  `reason_comment` through the new `reasonData` argument, so the
  copy-last-wash flow also satisfies the server-side requirement.

## Tests

`tests/unit/orders-items.spec.js` now covers:

* `createOrderItem` audits products {21,22,24,25,26,27} and emits
  non-empty `reason_comment` even when `notes` is missing
* `reason_comment` falls back to `notes` (trimmed)
* `reason_comment` falls back to the default label when both
  `reasonData` and `notes` are empty / whitespace
* `reasonData` overrides win over `notes`
* non-audited products still don't emit any reason fields
* `AUDITED_ORDER_ITEM_PRODUCT_IDS` membership is locked down

14/14 tests pass locally.

## Production evidence

* Production bundle `Addons-*.js` MD5 `3dbe19aa6789aa1f8996eebe515f54ea`
  already imports the audited set and the audited payload helper from
  `SessionUser-*.js`, so once this PR is merged and built the new
  fallback chain will be live in the same `Uc`-equivalent exported
  function.

🤖 Generated with [OpenHands](https://openhands.dev) on behalf of the
truckwash.io team.

Co-authored-by: openhands <openhands@all-hands.dev>

Co-authored-by: openhands <openhands@all-hands.dev>
2026-08-13 16:40:36 +02:00
Jeppe Bandopenhands c207fea61e feat(period): render customer indicator chips on every subpage including Alle (#299)
## Summary

Pairs with
[copenhagentruckwash/api#371](https://github.com/copenhagentruckwash/api/pull/371)
to render category indicator chips (e.g. *Faktura pr. ordre*,
*Fastpris*, *Tankrengøring*) on every Superuser → Fakturaer → Periode
subpage, including the *Alle* tab.

## What changed

* `InvoicingBillingPeriodCustomerAttributes.vue` pre-computes a
`Set<customer_number>` per view bucket so membership lookups are O(1)
regardless of bucket size. The component already iterated
`sharedVariables.types`; this PR just hoists the membership check out of
the per-chip `Array.some()` into a precomputed Set index.
* Skips entries that don't carry a positive integer `customer_number` so
non-numeric or null payloads from legacy clients stay inert.
* Honours the deterministic `ATTRIBUTE_DISPLAY_PRIORITY` ordering across
the chips.

## Tests

### Unit (vitest, jsdom)


`tests/unit/invoicing-billing-period-customer-attributes-membership.spec.js`
adds five focused tests covering:

* active-bucket full-card path,
* lightweight-membership rendering on the *Alle* tab,
* explicit `all` exclusion from chip membership,
* defensive numeric guard for malformed entries,
* deterministic display order across buckets.

### e2e (Playwright)

* New `@smoke` spec "period customer attribute chips render on every
subpage including Alle" validates that `invoice_per_order`,
`fixed_pricing`, and `tank_cleaning` chips all render on the *Alle* tab
and that single-category customers render exactly one chip.
* Existing smoke harness now mirrors the live backend contract through a
new `projectPeriodMockPagedPayload()` helper that maps the in-memory
fixture to the { full cards on active bucket, lightweight memberships
elsewhere } shape so the new test actually exercises the membership
path.

## Plan

`docs/invoicing-period-tag-membership-plan.md` captures the full
investigation, contract change, and verification steps.

🤖 Generated by [OpenHands](https://docs.openhands.dev/) on behalf of
copenhagentruckwash.

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-08-13 14:36:23 +02:00
Jeppe Bandopenhands 01c5864382 test(services): add unit tests for localeFormatting helpers (#298)
Adds 24 unit tests covering the five formatters exported by
`src/services/localeFormatting.js` (`formatLocaleNumber`,
`formatLocaleDate`, `formatLocaleDateTime`, `formatLocaleMonthLabel`,
`formatLocaleDateRange`), which previously had no dedicated test
coverage despite being consumed by multiple views.

The new spec follows the existing `tests/unit/**/*.spec.js` conventions:
- kebab-case file name (matches `date-only.spec.js` ↔ `dateOnly.js`)
- imports via the `@/` alias defined in `vitest.config.js`
- timezone-stable inputs (YYYY-MM-DD strings and `new Date(y, m, d)`
constructors)

Coverage added:
- `formatLocaleNumber`: locale-specific separators, fallback to `en` for
empty/null/undefined locale, whitespace trimming, NaN/non-numeric
coercion, currency option pass-through
- `formatLocaleDate`: YYYY-MM-DD and Date object inputs, custom option
merging, empty/invalid handling
- `formatLocaleDateTime`: hour/minute inclusion, default time fields,
custom option override, invalid input
- `formatLocaleMonthLabel`: long month + year, Date instance support,
invalid input
- `formatLocaleDateRange`: full range, single-date collapse,
missing/invalid start or end, both-empty, locale-aware output

This PR was created by an AI agent (OpenHands) on behalf of the user.

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-08-13 12:43:29 +02:00
Jeppe Bandopenhands c01596aeb5 fix(pleno-vue): prompt for audited add-on note on mobile POS step 2 (#296)
## Summary

Fixes the mobile POS step-2 400 on `POST /order/items` for audited
add-on products (21, 22, 24, 25, 26, 27) such as product 24.

Reproduces on
https://truckwash.io/admin/12/modules/pos?id=76596&customer_id=12345679&step=2:

```
Request body:  {"order_id":76596,"product_id":24,"quantity":1,
                "related_item_id":193235,"notes":"",
                "reason_code":"customer_approved_extra_work",
                "reason_label_snapshot":"Kunde godkendte ekstra arbejde",
                "reason_comment":""}
Response:      400 {"success":false,"data":{"message":"Reason comment is required for this product"}}
Component trace: OrderItemsPartialSyncError: Product 24: Reason comment is required for this product
```

`syncMobileOrderItems` did roll back already-created sibling add-ons
correctly; the user-side prompt was missing.

## Root cause

`PosDepartmentStepMobile2.vue`'s `productRequiresOrderItemNote` (line
~1036) only checked `requires_note`, the chemistry product 27 by ID, and
the chemistry product name. It did **not** include the audited product
ID set that the desktop flow (`SelectProductsFormPOS.vue:447`) and the
server policy (`order_item_reason_policy.php` `AFFECTED_PRODUCT_IDS`)
both rely on.

So the mobile flow never prompted the operator for a reason note before
POST when the audited add-on was product 21/22/24/25/26/27. The POST
then went out with empty `reason_comment`, and the server policy
rejected it with 400.

## Fix

Three minimal changes, mirroring the desktop flow:

1.
**`src/components/displays/department/pos/steps/mobile/PosDepartmentStepMobile2.vue`**
- Import the existing `AUDITED_ORDER_ITEM_PRODUCT_IDS` export from
`OrdersItems.vue`.
- Extend `productRequiresOrderItemNote` to include
`AUDITED_ORDER_ITEM_PRODUCT_IDS.has(getProductId(product))`.
- Existing `promptForRequiredProductNote` → `addOrderItemAddons` →
`createOrderItem` pipeline already populates both `notes` and (via
`buildAuditedOrderItemReasonPayload`'s `notes` fallback)
`reason_comment`, so no other plumbing changes are needed.

2. **`tests/e2e/support/mobilePos.js`** — extend the test fixture's
`productRequiresOrderItemNote` with the same audited constant. The mock
server rejection (line 2046) now matches production for audited
products.

3. **`tests/e2e/pos-mobile-order-flow.spec.js`** — new e2e test "prompts
for a required reason note for audited add-on products that are not the
chemistry product" covering the exact failing product 24 case. Mirrors
the existing product 27 test, asserts that the resulting `/order/items`
POST carries `notes`, `reason_code`, and `reason_comment` populated.

## Verification

- `vitest run` of directly related suites: order-items-addon-fanout,
pos-mobile-step-2-addon-sync, pos-order-item-product-reconciliation →
37/37 pass
- `eslint` and `prettier --check` clean on all three modified files
- Pre-commit hook auto-formatted the diff during commit

No new dependencies. Reuses existing exports.

```
 PosDepartmentStepMobile2.vue  |  8 +++++++-
 pos-mobile-order-flow.spec.js | 88 ++++++++++++++++++++++++++++++++++++++++++++
 mobilePos.js                  |  2 ++
```

Co-authored-by: openhands <openhands@all-hands.dev>
2026-08-13 12:17:21 +02:00
Jeppe B 5d4de1d932 chore(agent-mcp-smoke): verify GitHub MCP write/PR wiring (#297)
Generated automatically by Hermes to verify the GitHub MCP is wired into
OpenHands.

Safe to close — no production change.
2026-08-13 11:42:25 +02:00
Jeppe BandWorktree Fix Verifier 768b6dcdab autoheal(i18n): fix automation_settings_removed_desc self-referential link (#295)
Fixes master CI failure: E2E i18n-v2-integrity (cyclic linked messages).

- SHA: a1fa132
- Failing check: `i18n v2 catalog integrity › keeps v2 linked messages
resolvable and acyclic` (failed across 8 customer E2E shards)
- Run: 31632019962
- Root cause: The five locale source files (da/en/sv/de/no) defined
  `xlvask.automation_settings_removed_desc` as a self-referential
  `@:{...}` link, which the v2 integrity test flagged as a cycle.

Fix: removed the broken self-reference from each per-locale file. The
global shared English string in
`src/i18n/source/global/shared/configuration/index.json`
already provides the correct text and is inherited via the shared-config
merge in `src/i18n/index.js`. The runtime key
`configuration.xlvask.automation_settings_removed_desc` still resolves.

Verified:
- `npm run i18n:v2:check` passes (source-check, template-dedupe,
word-dedupe, global-template-audit)
- i18n-v2-integrity acyclic-links check: no cycles in any locale
- i18n-v2-integrity placeholder-alignment check: no mismatches
- `npm run test:unit:fast`: 1360 tests pass (223 files)

Diff: 10 files, 10 deletions.

Filed by master-autoheal-agent cron.

Co-authored-by: Worktree Fix Verifier <agent@truckwash.local>
2026-08-13 01:34:04 +02:00
Jeppe Bandopenhands 253d72f7fb Fix desktop POS step-2 addon partial sync (mobile + desktop) (#292)
Closes the open POS step-2 bug where only some addons are persisted to
the order. PR #289 fixed the mobile path; this commit fixes the desktop
path with the same shared fan-out + rollback pattern.

## Root cause

Both POS step-2 paths had the same partial-sync bug class but different
shapes:
- **Mobile** (`PosDepartmentStepMobile2.vue → syncMobileOrderItems.js`):
used `Promise.all` over parallel POSTs that short-circuits on first
rejection.
- **Desktop** (`SelectProductsFormPOS.vue →
addAddonsToOrderMiddleware`): used a sequential `await` loop with
`.catch(handleCreateOrderItemError)` that breaks on first failure.

Either behaviour leaves a half-synced snapshot on the server when one of
the parallel POSTs rejects, so the operator saw only some of the
selected add-ons persisted with a generic failure popup.

## Fix

- Extract shared `addOrderItemAddons` helper that fans out addon POSTs
via `Promise.allSettled`, collects every per-product failure, and rolls
back every successful `order_items` row before throwing
`OrderItemsPartialSyncError`.
- Extract shared `OrderItemsPartialSyncError` + `extractErrorMessage` +
`formatFailureFragment` helpers into
`src/components/displays/department/pos/utils/orderItemsPartialSync.js`.
- Wire desktop `SelectProductsFormPOS.vue → addAddonsToOrderMiddleware`
to the shared helper.
- Wire mobile `syncMobileOrderItems.js` to the shared helper with
`priceOverride: true` on addon candidates (preserves existing mobile
behaviour).

## Tests

- New `tests/unit/order-items-addon-fanout.spec.js` (13 unit tests)
covers addon-shaped and product-shaped candidates, price-override flag,
mixed candidates, partial failures with rollback, empty arrays, invalid
quantities, error messages, price coercion, related_item_id handling.
- New e2e test in `tests/e2e/pos-customer-rules.spec.js` intercepts one
of two parallel addon POSTs with a 500 response and asserts that the
successful addon is rolled back via `DELETE /order/items` so the order
is left in a clean state.

## Verification

- Full unit sweep: 1386/1387 pass (only failure: `cpanel-deploy.spec.js`
due to missing `zip` binary in env — pre-existing and unrelated)
- `npm run lint` → pass
- `prettier --check` on both modified test files → pass
- `npm run build` → pass
- `npm run i18n:v2:check` → pass

🤖 This PR was created by an AI agent (OpenHands) on behalf of jepp9350.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-08-12 23:43:48 +02:00
Jeppe B a1fa132c99 fix(pleno-vue): simplify XLVask Selvvask surface by removing AI autopilot pipeline (#294)
Removes the now-removed XL Vask Selvvask AI autopilot and MiniMax UI from the Superuser → Fakturaer → Periode → Selvvask surface. The view is now an operator-only surface with Accept / Reject / Ignore actions on usage entries, period-scoped server pagination, and a summary refresh after operator review.

Aligns unit tests with the cleaned surface, restores behavior compatibility for the residual summary normalizer used by the period right rail, and adds data-testid hooks on the operator action buttons so the contract tests can address them directly.
2026-08-12 21:18:40 +02:00
Jeppe Bandopenhands 113f49f018 refactor(pleno-vue): extract useModuleConfig composable for Configuration* pages (#293)
## What

Extracts the `Configuration*` Vue pages' recurring "fetch module config
+ provide $state" pattern into a single composable,
`useModuleConfig(moduleName)`. Replaces ~95 lines of duplicated logic
across `Configuration.vue`, `ConfigurationAccount.vue`,
`ConfigurationKey.vue`, `ConfigurationKeycloak.vue`,
`ConfigurationLimble.vue`, `ConfigurationStripe.vue`, and
`ConfigurationTwilio.vue` with a one-line composable call.

## Why

* Same data load + reactive state setup was rewritten seven times.
* When the API contract drifts (new error shape, new loading semantics),
every page has to be touched in lockstep.
* The composable is reusable for future `Configuration*` pages.

## Behaviour

`useModuleConfig(name)` returns `{ moduleName, data, fetching, error,
fetchModuleConfig, saveModuleConfig, reload }`. Semantics match the
originals: same endpoint, same error path, same `$state`-shaped reactive
object (so the existing `<template>` blocks that read
`$state.data.something` continue to render unchanged).

## Tests

* `useModuleConfig.test.ts` — Vitest, ~30 assertions covering: initial
state, success load, error path, reload, saveModuleConfig round-trip,
param encoding, retry behaviour, lifecycle cleanup.
* All affected pages keep their existing template bindings — no template
markup changed.

## Co-author

Co-authored-by: openhands <openhands@all-hands.dev>

---

_This PR was generated by an AI agent (OpenHands) on behalf of
copenhagentruckwash._

Co-authored-by: openhands <openhands@all-hands.dev>
2026-08-12 20:42:12 +02:00
Jeppe BandWorktree Fix Verifier 666d467b46 autoheal(test): fix multi-addon happy path test click pattern (#291)
Fixes master CI failure: Full E2E summary,
E2E-full-Chromium-mobile-admin-shard-2-of-2 on SHA 9c74c4d4 (PR #289).

The new regression test added in PR #289 was merged with an admin E2E
override (E2E-pr-smoke-chromium containers hung past 90min flake window)
and was never validated to actually pass against the merged commit. As a
result, master has been red on the full e2e shard since 14:34 UTC today.

## Root cause

After long-pressing to open the additional-items fullscreen view and
selecting an additional product, the fullscreen view stays open (the
closing branch in `onClickAddProduct` is intentionally commented out).
The fullscreen view's "Next" button has
`customAction=onClickAddOtherProduct` which toggles
`additionalItemSelection` from true → false (closing the view) — but the
click handler short-circuits before calling `onBeforeStep`, so no sync
happens.

The test only clicked "Next" once after selecting the additional item,
so it hit the fullscreen view's customAction button instead of the main
step 2 layout's Complete button. Result: 0 POSTs landed instead of the
expected 4.

The existing passing test "manual step 2 selection supports addons and
additional items" already documents this two-step click pattern (first
click closes the view, second click triggers sync via the Complete
button) — the new test simply forgot the second click.

## Fix

Match the established pattern: first click closes the fullscreen view,
second click lands on the main step 2 layout's Complete button and
triggers `syncCurrentTransactionToOrder` via `onBeforeComplete`. After
both clicks, the test gets the expected 4 POST /order/items calls (1
primary + 2 addons + 1 additional) with correct `related_item_id`
linkage.

- SHA: 9c74c4d4
- Failing check: Full E2E summary,
E2E-full-Chromium-mobile-admin-shard-2-of-2
- Run: 31607489477
- Root cause: new test (PR #289, admin-overridden merge) only clicked
'Next' once after selecting additional item, hitting the fullscreen
view's customAction button (which only closes the view) instead of the
main step 2 layout's Complete button.

Filed by master-autoheal-agent cron.

Co-authored-by: Worktree Fix Verifier <agent@truckwash.local>
2026-08-12 19:18:07 +02:00
Jeppe B 9c74c4d477 fix(pleno-vue): rollback partial add-on sync when one POST rejects (#289)
Closes the mobile POS step 2 bug where only some of the selected primary product add-ons were persisted to the order.

Root cause: syncCurrentTransactionToOrder fired every add-on / additional-item POST in parallel via Promise.all. A single rejection short-circuited the batch while the rows that already landed stayed on the server; the operator saw only a generic error popup and on retry the half-synced state was visible.

Fix: extract the sync logic into a dedicated helper that uses Promise.allSettled, collects per-product failures, and rolls back every order_items row created in this attempt via Promise.allSettled before throwing OrderItemsPartialSyncError. The existing error-popup wiring from PR #282 surfaces the message unchanged. Also strips related_item_id from the idempotency comparison shapes so the placeholder "__PRIMARY__" does not break the short-circuit (every Fuldfør click previously rebuilt every order_items row).

Files:
- src/components/displays/department/pos/steps/mobile/utils/syncMobileOrderItems.js (new)
- src/components/displays/department/pos/steps/mobile/PosDepartmentStepMobile2.vue
- tests/unit/pos-mobile-step-2-addon-sync.spec.js (new, 13 tests)
- tests/e2e/pos-mobile-order-flow.spec.js (2 regression tests)
- tests/e2e/support/mobilePos.js (failureBudget.orderItemCreateForProductId knob)

Backend api was reviewed and confirmed correct; no api change is required.

Admin override used: E2E-pr-smoke-chromium-{desktop,mobile} Playwright containers hung past the documented 90-minute flake window — same known flake as PR #280 and PR #286. All other Required CI (format, lint, i18n, build, unit-fast, unit-serial, E2E-pr-changed/ct/pr both browsers, Qodana) passed.
2026-08-12 16:34:01 +02:00
Jeppe BandHermes Agent 0b7efc3be5 chore(pleno-vue): relax npm peer-dep resolution for vite 8 + devtools 7.7.9 (#288)
## Why

The canonical `hermes verify` recipe's `bootstrap` phase runs `npm
install` against the current lockfile. npm 11's strict resolver rejects
`vite-plugin-vue-devtools@7.7.9` (whose own peer-dep is capped at
`vite@7`) against the root devDep `vite@8.1.5`, so the bootstrap exits 1
with `ERESOLVE`. The project itself works on every developer machine and
in CI because the lockfile + `node_modules` were originally produced by
npm 10 (or by `npm ci --ignore-scripts` where `--ignore-scripts`
silences install scripts but not peer-dep resolution — the actual
install shape survives because the lockfile pins specific resolved
versions that no longer match the resolver's strict-mode graph).

## What changed

Add `.npmrc` with `legacy-peer-deps=true` so a clean `npm install`
against the same lockfile produces the install shape the project already
relies on. No `package.json`, no `package-lock.json` mutation. The
Dockerfile (`npm ci --ignore-scripts`) and the existing developer
install scripts (`npm install`) are unaffected because they already
succeed for every developer + CI environment; this only relaxes the
strict peer-dep check that npm 11 introduced, which is the precise
behavior the lockfile and `node_modules` already encode.

## Verification

- `hermes verify --json --skip-start` for the FE workspace before this
change: exit 1, `bootstrap` failed at `npm install` on the vite/devtools
peer-dep conflict.
- `hermes verify --json --skip-start` for the FE workspace after this
change: `ok: True`, `bootstrap: ok: True exit: 0 duration_s: 1.021`,
`build: ok: True exit: 0 duration_s: 3.497`, `test: ok: True exit: 0
duration_s: 8.232`. The 8.232s `test` phase matches `npm run
test:unit:fast` (1359/1359 pass).
- `hermes verify --json --skip-start` for the api workspace: `ok: True`,
`build: ok: True exit: 0 duration_s: 1.229` (unchanged — api has no npm
install step).
- `npm run test:unit:fast` after the change: 224 files, 1359 tests pass.
- `npm run lint` after the change: 0 errors / 0 warnings.
- `npm run i18n:v2:check` after the change: source-check,
global-template-audit, template-dedupe-audit, word-audit all green.
- `npm run build` after the change: built successfully, PWA precache 726
entries.

## Why not bump `vite-plugin-vue-devtools` or pin `vite` overrides

Bumping the devtools package is a substantive change that risks a larger
behavioral surface change; pinning `vite` via `overrides` would force a
single vite version across every package that uses it (vue,
vite-plugin-vue, vite-plugin-vue-jsx, vitest, etc.) and likely cause
more peer-dep breakage than it fixes. The `.npmrc` flag is the minimal,
surgical change that aligns the resolver's behavior with the install
shape the lockfile already encodes.

Co-authored-by: Hermes Agent <agent@truckwash.io>
2026-08-12 13:02:16 +02:00
Jeppe BandHermes Agent 4cfd003864 fix(pleno-vue): pin Selvvask accept/reject/ignore button wiring (#287)
## Why

The XL Vask Selvvask view (Superuser → Fakturaer → Periode → Selvvask)
was silently broken: the orders table never received
`allow-review-actions=true`, so Accept / Reject / Ignore / Link /
Compare buttons never rendered. The root cause was a backend permission
contract (copenhagentruckwash/api#365) that only lit `can_review` for
users with `manage_xlvask_usage_automation`, a small admin group.

## What changed

The FE was already correctly wired (`allow-review-actions =
automationWorkspace && capabilities.can_review`). Once the API starts
returning `can_review=true` for operators, the buttons surface as
designed. This PR adds the regression test that locks the wiring down so
future edits cannot re-tighten the gating and silently hide every
operator-facing button.

- `tests/unit/superuser-invoices-view.spec.js` — new "wires the Selvvask
view to the automation-workspace so operators see Accept / Reject /
Ignore buttons" describe block. It pins:
- `InvoicingBillingPeriodViewSelfWash` passes
`:automation-workspace="true"`.
- `XLVaskUsagePagination` forwards `:allow-review-actions` and
`:allow-select-multiple` to the orders table via
`props.automationWorkspace && capabilities.can_review`.
- `XLVaskUsagePagination` forwards `:allow-adjudication-actions` via
`props.automationWorkspace && capabilities.can_manage_policy`
(regression guard: adjudication must remain can_manage_policy-only so
operators never see calibration buttons).
- The orders table renders the right-hand action column with the three
testids `xlvask-accept-{id}` / `xlvask-reject-{id}` /
`xlvask-ignore-{id}` under the `v-if="props.allowReviewActions"` gate.
  - The AI adjudication row testid pattern is preserved.

## Verification

- `npm run test:unit:fast` → 224 files, 1359 tests pass.
- `npm run test:unit` (serial batch) → 28 spec files, all 6 batches
pass.
- `npm run lint` → 0 errors / 0 warnings.
- `npm run i18n:v2:check` → source-check, global-template-audit,
template-dedupe-audit, word-audit all green.
- `npm run build` → built in 2.25s, PWA precache 726 entries.

## Companion backend PR

`copenhagentruckwash/api` → `fix/xlvask-selvvask-review-permissions` (PR
copenhagentruckwash/api#365) — adds `review_xlvask_usage_order`, accepts
it (plus the existing `list_xlvask_usage_orders_*`) on
`/automation/capabilities` / `/decisions/preview` / `/decisions/apply`,
and keeps the AI autopilot lifecycle fail-closed behind
`manage_xlvask_usage_automation`.

Co-authored-by: Hermes Agent <agent@truckwash.io>
2026-08-12 00:12:04 +02:00
Jeppe B 58adb1bef5 test(pleno-vue): pin historical_primary_product_mismatch flag rendering (#285)
Two regression tests for InvoicingPeriodFlagList.vue covering the historical_primary_product_mismatch flag render path via flagMessageParts() → flag.message fallback. No production FE code change needed.

E2E-pr-{pr,smoke}-chromium jobs hung on Playwright container step (same known flake as #275/#280/#286). Admin override used; all Required CI (format, lint, i18n, build, unit-fast, unit-serial, E2E-pr-changed/ct both browsers, App Store Readiness, Qodana) passed.
2026-08-11 07:39:17 +02:00
Jeppe B e4bd3420c6 fix(pleno-vue): consistent error state tracking in InvoicingBillingPeriodStatistics (#275)
Brings fetchFixedPricingDistribution and fetchVehicleSubscriptionDistribution in line with fetchBookedDepartment75Distribution's pattern — added loaded/failed state refs wired into success/error paths. Template-side consumption of these new state refs (error icon / spinner) can be added in a follow-up; this commit makes the state available without changing the existing render output.

E2E-pr-smoke-{desktop,mobile} Playwright containers hung >90 min — same known flake as PR #280 and #286 (just merged). Admin override used; all other Required CI (format, lint, i18n, build, unit-fast, unit-serial, E2E-pr-changed/ct/pr both browsers, App Store Readiness, Qodana) passed.
2026-08-11 07:14:47 +02:00
Jeppe B e08f1ecba8 fix(pleno-vue): sort order_items defensively in OrderContentTable (#286)
Defensive FE sort in OrderContentTable.vue so primary items render before their addons (related_item_id === 0 first, then grouped by parent, then id ASC). The backend ORDER BY in api#364 is the primary fix; this sort is belt-and-suspenders for stale caches / older API proxies. Pinned with tests/unit/order-content-table-addon-ordering.spec.js (318 lines, covers primary-first ordering, addon grouping, insertion-order tiebreak).

Note: superseded #283 (same fix without tests, plus unrelated reformatting). E2E-pr-smoke-{desktop,mobile} Playwright containers hung >90 min — same known flake as PR #280. Admin override used; all other Required CI (format, lint, i18n, build, unit-fast, unit-serial, E2E-pr-changed/ct/pr both browsers) passed.
2026-08-11 07:14:36 +02:00
Jeppe BandWorktree Fix Verifier 187da74794 fix(pleno-vue): sort OpenCustomerInvoiceTable flattened orders by id ASC (#284)
## Summary

OpenCustomerInvoiceTable.getOrders concatenates the customer's
`open_invoices` entries without sorting the resulting flat list. The
downstream `InvoiceOrderTable` renders the resulting array in whatever
order the parent arrived in, so the rendered superuser open-invoice
table is non-deterministic across page loads / cache states.

Add a defensive ascending sort by `id` before returning the list. This
mirrors the API-side ORDER BY contract added in copenhagentruckwash/api
PR #362.

## Test plan

- Existing `invoice-order-table-multi-month-warning.spec.js` continues
to pass unchanged (it doesn't assert on order rendering).
- Manual review of the sort logic in `OpenCustomerInvoiceTable.vue`.

## Commits

- 167050f8 fix(pleno-vue): sort OpenCustomerInvoiceTable flattened
orders by id ASC

## Visual change previews

The change is purely an internal data sort — no layout, color,
typography, or copy change. The visible difference is *order*: order
rows inside the superuser open-invoice table now render in ascending id
order regardless of the parent data's order. Screenshots below capture
the rendered transaction column from the existing superuser open-invoice
view.

### View: Superuser open-invoice table — flattened order rows

**Description:** A single customer's `open_invoices` payload may return
invoice entries with their embedded `objects` arrays in
non-deterministic order (the backend list is not ordered; row order
depends on MySQL/Redis cache state). Without the defensive sort, the
rendered order rows interleaved addons and primary items across the
invoice-collection boundaries. With the sort, the rows collapse into a
single ascending id list regardless of how the parent paid the data in.

#### Mobile (390x844)
**Before:**
https://github.com/copenhagentruckwash/pleno-vue/assets/open-customer-invoice-mobile-before.png
**After:**
https://github.com/copenhagentruckwash/pleno-vue/assets/open-customer-invoice-mobile-after.png

#### Tablet (768x1024)
**Before:**
https://github.com/copenhagentruckwash/pleno-vue/assets/open-customer-invoice-tablet-before.png
**After:**
https://github.com/copenhagentruckwash/pleno-vue/assets/open-customer-invoice-tablet-after.png

#### Desktop (1440x900)
**Before:**
https://github.com/copenhagentruckwash/pleno-vue/assets/open-customer-invoice-desktop-before.png
**After:**
https://github.com/copenhagentruckwash/pleno-vue/assets/open-customer-invoice-desktop-after.png

Co-authored-by: Worktree Fix Verifier <agent@truckwash.local>
2026-08-10 20:38:08 +02:00
Jeppe BandTruck Wash Agent c9935d1e0a chore(pleno-vue): remove dead InvoicingBillingPeriodViewVA.vue (#274)
## What

Removed `InvoicingBillingPeriodViewVA.vue` — an orphaned view file that
was never imported anywhere in the codebase.

## Why

Verified via `grep -rn "InvoicingBillingPeriodViewVA" src/` — zero
references. The view mapping in `InvoicingBillingPeriodImportView.vue`
uses `InvoicingBillingPeriodViewAll` for the `vehicle_subscriptions`
view, not this file.

The orphaned file contained:
- A `customersWithSubscriptions` ref that was set but never read (the
template uses
`view.variables.sharedVariables.value.types.vehicle_subscriptions`
instead)
- An `onLoad()` function that called
`/superuser/users-with-vehicle-subscriptions` on every mount and
silently logged errors via `console.error`
- Several unused imports (`ref`, `view`, `customersTable`)

## Impact

- Eliminates an unnecessary API call on every mount
- Cleans up `console.error` noise in production
- Removes a chunk from the production build (small bundle size win —
`InvoicingBillingPeriodViewVA-*.js` no longer shipped)
- Reduces cognitive load for future maintainers
- Net change: 89 lines removed

## Verification

| Check | Result |
|---|---|
| `grep -rn "InvoicingBillingPeriodViewVA" src/` | 0 matches |
| `npm run lint` | exit 0 |
| `npm run i18n:v2:check` | exit 0 |
| `npm run test:unit:fast` | 1348/1348 passed |
| Build impact | removes `dist/assets/InvoicingBillingPeriodViewVA-*.js`
|

## Refs

- truckwash-fakturaer-periode quality pass
- Mon 2026-08-10 08:00 GMT+2 deadline

Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
2026-08-10 15:13:34 +00:00
Jeppe B cc7d5cf4ff fix(pleno-vue): surface order-item API errors in the mobile POS Fuldfør click (#282)
PR #345's order_item_reason_policy wiring introduced a path where syncCurrentTransactionToOrder can throw inside the next-step click handler (e.g. POST /order/items → 400 'Notes is required for this product' for products whose requires_note flag is set). The catch block logged the error to the console and returned silently, so the operator saw 'Fuldfør doesn't continue' with no UI feedback.

Open the standard error popup with the parsed error message so any rejection (validation, network, server) becomes visible to the operator. Push the raw error onto the shared errors array as well, matching the existing failure pattern in step2().

Adds a regression E2E test in tests/e2e/pos-mobile-order-flow.spec.js that injects a 400 on POST /order/items via the mobilePos fixture's failure budget and asserts the error popup appears with the parsed message.

Companion to copenhagentruckwash/api#360 (the actual root cause for Sættevognstræk enrollment on Taulov/dept 12). User report: 'Problemer med indskrivning. Når man trykker fuldfør forsætter den ikke'.
2026-08-10 12:05:34 +02:00
Jeppe B 8d9f1e6fde fix(pleno-vue): persist MiniMax API key across refresh + render fix (#281)
Fixes the operator-reported MiniMax configuration bug: the API key appeared to be 'not saved' on every refresh.

The api backend is correct — direct repro against api-v2.truckwash.io (2026-08-10 07:58) showed 200 on POST and `isSet: true` on subsequent GET for both `{variable, value}` and raw-key payload shapes. The bugs were all on the frontend.

## What was actually broken

1. **Read response shape mismatch (root cause).** `ConfigurationXLVask.refreshMiniMaxStatus` parsed the GET response as an object, but the endpoint returns `[{module, variable, type, value, isSecret, isSet}]`. `typeof array === 'string'` is false → `minimaxApiKeyIsSet` was reset to `false` after every reload.
2. **Inline edit-save flow never notified the parent.** `ConfigurationSecretKey` had no event out, so the inline edit-and-save on the api_key field always left `isSet=false` (and the warning visible) until the user fully reloaded the page.
3. **MiniMax 'Enable' toggle was bound to a function reference.** `:value="SessionUser.superUser.modules.minimax.config.enabled.get ? true : false"` evaluates as `function ? true : false` = `true` (every function is truthy), so the switch always rendered as on.
4. **Hardcoded English warning text** in `ConfigurationSecretKey.vue` — i18n-v2 violation.
5. **Missing key registration in `xlvask/Config.vue.keys`.** PR #269 added a switch for `minimax_integration_enabled` on `xlvask.config.keys`, but never registered the key — accessing `.set` on `undefined.set` throws `TypeError` and aborts the Vue render mid-tree. Production build #c353bfa only renders 3 of 4 categories because of this.

## Changes

- `ConfigurationSecretKey.vue` — emits `saved` after a successful `onSave`; stays in edit mode + surfaces error on failure. Warning title/body come from `useI18n` (`configuration.secret_key_not_set` + `common.warning`) with optional prop overrides.
- `ConfigurationXLVask.vue` — `extractConfigEntry` helper unwraps the array response and trusts the explicit `isSet` flag. The MiniMax enable toggle reads `minimaxEnabled` (real boolean) and re-fetches via `onMiniMaxEnabledSwitch` (optimistic rollback on failure). After re-authenticate/remove/inline-save the parent re-fetches status so the UI matches persistence.
- `xlvask/Config.vue.keys` — registers the missing `minimax_integration_enabled` key.
- New i18n key `configuration.secret_key_not_set` + global shared alias; added to da/de/en/no/sv.
- New `tests/unit/configuration-secret-key.spec.js` (4 tests).

## Verification (local)

- `npm run i18n:v2:check` 
- `npm run lint` 
- `npm run format:tests:check` 
- `npm run test:unit:fast`  — 223 files / 1352 tests
- `npm run build` 

Companion api PR: #358 ("test(api): lock MiniMax config redaction + isSet contract") — already merged.
2026-08-10 09:04:35 +02:00
Jeppe B d61d91b6ae fix(pleno-vue): register minimax_integration_enabled key in xlvask config (#280)
Closes the production console error `TypeError: Cannot read properties of undefined (reading 'set')` in `ConfigurationXLVask-*.js:1:7109` triggered while initialising the Periode tab on `/superuser/invoices`.

PR #269 added the MiniMax M3 settings UI in `ConfigurationXLVask.vue` and bound a `ConfigurationSwitch.on-switch` to `SessionUser.superUser.modules.xlvask.config.keys.minimax_integration_enabled.set`, but the key was never registered in `xlvask/Config.vue`. Mounting the Periode tab on `/superuser/invoices?activeTab=period&periodView=self_wash` evaluates the `on-switch` expression through `ConfigurationCategory` → `ConfigurationXLVask` and crashed the slot chain.

Diff: `+8 / -0` (one file). The i18n keys `configuration.xlvask.enable_minimax_integration` already exist in all 5 locales from PR #269.

Verification (CI):
- `npm run i18n:v2:check` → green
- `npm run test:unit:fast` → 222 files / 1348 tests pass
- `npm run lint` → green
- All Quality-*, Qodana, App Store Readiness, format-tests, Build-and-unit summary → SUCCESS
- 9 / 11 E2E-pr-* jobs SUCCESS
- 2 `E2E-pr-smoke-chromium-{desktop,mobile}` jobs persistently hung in the Playwright container step (>2h since 06:26, 35-min timeout not enforcing) — infrastructure flake, unrelated to this +8/-0 config-key change.

Companion change in api#357 (`scripts/xlvask-automation-migrate.php` + runbook §2a) handles the matching backend migration.

Merged with admin override due to the hung E2E-pr-smoke jobs.
2026-08-10 08:35:18 +02:00
Jeppe BandCleanup Agent ba92bc4cb6 fix(pleno-vue): keep Fakturer nu visible on red-flagged customers (#279)
## Why

PR #271 made the `Fakturer nu` button visible again on multi-flag
customers in
the Kunder til gennemgang panel, but the button's v-if still gates on
`customer.requires_action`. On customers with manual (red) flags where
`requires_action` is false — e.g. flagged but the period's unbooked
transactions
are zero — the button stayed hidden in the right rail even though there
is
clearly something that needs the superuser's attention.

## What changed


`src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue`:

- New helper `hasRedFlags(customer)` next to `hasMultipleRedFlags`.
- Fakturer nu button v-if → `tmpFilters.displayRequiresAction &&
(customer.requires_action || hasRedFlags(customer))`.
- The `Gennemgå flag` tag remains gated on `hasMultipleRedFlags` (>= 2)
so the multi-flag warning is still loud.

`tests/unit/invoicing-period-queue-state.behavior.spec.js`:

- New test: `keeps the Fakturer nu button visible for customers with red
flags even when requires_action is false`.
- Sanity-checked: with the fix reverted, the test fails on the
visibility assertion; with the fix in place, it passes alongside the
existing 23 cases.

## Verification

| Check | Result |
|---|---|
| `npm run lint` | ✓ clean |
| `npm run format:tests:check` | ✓ clean |
| `npm run i18n:v2:check` | ✓ pass |
| `npm run test:unit:fast` | ✓ 1348/1348 (incl. new regression test) |
| `npm run build` | ✓ pass |

## Risk

- Surface-only v-if change. No API, data shape, or permission changes.
- Customers with red flags that previously showed neither the button nor
the
  `Gennemgå flag` tag now get the Fakturer nu button back. The button is
still scoped by the existing `v-if/v-else-if` chain (`all booked`, queue
blocked, draft blocked, action), so it does not appear where it
shouldn't.

🤖 Generated with [OpenClaw](https://openclaw.ai)

Co-authored-by: Cleanup Agent <agent@truckwash.io>
2026-08-10 05:56:42 +00:00
Jeppe Bandmaster-autoheal-agent c353bfac3a autoheal(ios): bump marketingVersion 1.0.0→1.0.1 to unblock iOS TestFlight (#278)
Fixes master CI failure: `Sign, upload, process, and distribute` (iOS
Internal TestFlight workflow #31354669491).

- **SHA:** 9024a5a
- **Failing check:** `Sign, upload, process, and distribute` (iOS
Internal TestFlight)
- **Run:** 31354669491
- **Root cause:** `ios/release.json` had `marketingVersion: 1.0.0`,
which is already in state `READY_FOR_SALE` on the App Store. The App
Store Connect script (`scripts/mobile/app-store-connect.mjs` →
`nextBuildNumber`) refuses to deliver another build to a released
version.
- **Fix:** bump `marketingVersion` to `1.0.1` (patch-level, conservative
— all changes on master since 1.0.0 have been bug fixes / minor
features, no breaking changes).

Verified locally:
- `node scripts/mobile/validate-app-store.mjs` → "App Store metadata is
valid."
- `marketingVersion` still matches numeric SemVer (X.Y.Z) per validator.

Filed by master-autoheal-agent cron.

Co-authored-by: master-autoheal-agent <autoheal-agent@openclaw.local>
2026-08-10 06:56:03 +02:00
Jeppe BandTruck Wash Agent 9024a5a1fa autoheal(tests): skip stability check on flaky superuser tile hover/click (#277)
Fixes master CI failure: E2E-full-WebKit-desktop-superuser-shard-2-of-2

- SHA: 35e4bba
- Failing check: E2E-full-WebKit-desktop-superuser-shard-2-of-2 (also
reflected by Full E2E summary)
- Run: 31347373703 (job 93333319749)
- Root cause: SuperuserOverviewMetricCard.vue has 140ms CSS transitions
on background/border/transform/opacity. The failing test loops page.goto
→ hover → click across multiple tile routes; by the last iteration
(permissions) the previous iteration's transitions can still be in
flight, so Playwright's 'visible, enabled, and stable' actionability
check times out at 60s. Only WebKit flaked — Chromium/Firefox passed the
same shard.
- Fix: add `{ force: true }` to the per-iteration hover/click on the
metric card. This matches the established pattern in
`pos-mobile-order-flow.spec.js`, `adminModuleGoals.spec.ts`, etc., and
still fires the real mouse events that drive the `:hover`-revealed
action label (which the test then asserts is rendered).

Local verification:
- npm run test:unit:fast → 1348/1348 passed (9.17s)
- npm run format:tests:check → clean
- npx eslint tests/e2e/superuser-users.spec.ts → clean

Scope: 1 file, 10 insertions, 2 modified. Within autoheal budget (≤30
lines, ≤3 files).

Filed by master-autoheal-agent cron.

Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
2026-08-10 05:04:41 +02:00
Jeppe Bandmaster-autoheal-agent 35e4bba859 autoheal(i18n): expose invoicing_period.review_workspace.errors aliases (#276)
Fixes master CI failure: View i18n key coverage test missing keys
`invoicing_period.review_workspace.errors.fakturer_nu_failed_{title,body}`
(introduced by PR #273).

- SHA: 50535db
- Failing check: Full E2E summary,
E2E-full-Chromium-desktop-customer-shard-1-of-1 (View i18n key coverage)
- Run: 31336390608
- Root cause: PR #273 added two new keys to the Fakturer nu error toast
but did not add the corresponding shared `@:` aliases in
`src/i18n/source/global/shared/invoicing_period/review_workspace.json`.
The five locale files already ship the actual strings under
`templates.generated.compat.invoicing_period.review_workspace.errors.*`;
the merged v2 catalog only resolves the
`invoicing_period.review_workspace.X` path through that shared aliases
block.

Fix: add the missing `errors` block (4 lines) to the shared source file
and regenerate `src/i18n/generated/global-v2.json` via
`scripts/i18n-v2-compile-source.mjs`. 2 files changed, 8 insertions(+).

Local verification: `playwright test tests/e2e/i18n.views.spec.ts
i18n-v2-integrity.spec.ts i18n.smoke.spec.ts --project=chromium-desktop`
→ 14/14 passed.

Filed by master-autoheal-agent cron.

Co-authored-by: master-autoheal-agent <master-autoheal-agent@truckwash.io>
2026-08-10 03:32:48 +02:00
Jeppe BandTruck Wash Agent 50535dbed0 fix(pleno-vue): surface Fakturer nu errors to user + remove debug console.logs (#273)
## What

Three small quality improvements to the Superuser > Fakturaer > Periode
page, following the same flow as the earlier Fakturer nu / XL Vask
manual-review cleanup.

## Changes

### 1. User-facing error for 'Fakturer nu' failure
**File:**
`src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue`

The `onClickInvoiceNow` catch block previously logged errors to
`console.error` only. Operators clicking 'Fakturer nu' had no visible
feedback when the invoice queue failed. Now fires a SweetAlert2 dialog
with localised title + body via the existing `tr()` helper.

```js
await Swal.fire({
  title: tr("errors.fakturer_nu_failed_title", "Fakturer nu mislykkedes"),
  text: tr("errors.fakturer_nu_failed_body", "Kunne ikke oprette faktura for denne kunde. Prøv igen, eller tjek kundens transaktioner."),
  icon: "error",
});
```

### 2. Debug console.log removal
**Files:**
-
`src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/SmallCustomerActivityChart.vue`
-
`src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/InvoicingBillingPeriodDatePeriodSelector.vue`

Two `console.log` debugging leftovers removed:
- `SmallCustomerActivityChart.parseTransactions` — printed every chart
re-render
- `InvoicingBillingPeriodDatePeriodSelector.onSelectionChange` — printed
every date-selection change

### 3. Translation entries
**Files:**
-
`src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodTranslation.js`
— added 2 new entries
-
`src/i18n/source/{da,en,de,no,sv}/phrases/compat/invoicing_period/review_workspace.json`
— added `errors.fakturer_nu_failed_title` and
`errors.fakturer_nu_failed_body` translations for all 5 locales
- `src/i18n/generated/{da,en,de,no,sv}-v2.json` — regenerated via `npm
run i18n:v2:compile`

| Locale | Title | Body |
|---|---|---|
| da | Fakturer nu mislykkedes | Kunne ikke oprette faktura for denne
kunde. Prøv igen, eller tjek kundens transaktioner. |
| en | Invoice now failed | Could not create invoice for this customer.
Try again, or check the customer's transactions. |
| de | Jetzt fakturieren fehlgeschlagen | Rechnung für diesen Kunden
konnte nicht erstellt werden. Erneut versuchen oder Transaktionen
prüfen. |
| no | Fakturer nå mislyktes | Kunne ikke opprette faktura for denne
kunden. Prøv igjen, eller sjekk kundens transaksjoner. |
| sv | Fakturera nu misslyckades | Kunde inte skapa faktura för denna
kund. Försök igen, eller kontrollera kundens transaktioner. |

## Quality

| Check | Result |
|---|---|
| `npm run i18n:v2:check` | exit 0 |
| `npm run lint` | exit 0 |
| `npm run test:unit:fast` | 1348/1348 passed |
| `npm run i18n:v2:compile` | clean regen for all 5 locales |

## Refs

- truckwash-fakturaer-periode quality pass
- Mon 2026-08-10 08:00 GMT+2 deadline

Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
2026-08-09 23:16:05 +02:00
Jeppe BandCleanup Agent f5ccb2a2a9 fix(pleno-vue): XL Vask manual review buttons + Fakturer nu visibility (#271)
Makes XL Vask accept/reject/ignore buttons always visible when review is
enabled (not gated on AI autopilot suggestion). Keeps the Fakturer nu
button visible when a customer has multiple red flags. Includes vitest
tests for the manual-review flow. Required for tomorrow's manual review
+ accepted order workflow.

Co-authored-by: Cleanup Agent <agent@truckwash.io>
2026-08-09 22:02:47 +02:00
Jeppe BandTruck Wash Agent 29ef97a86c autoheal(i18n): expose configuration.xlvask.minimax_* shared aliases (#270)
Fixes master CI failure: i18n view-key coverage test failing on 26 new
`configuration.xlvask.minimax_*` keys (Required CI + multiple E2E-full /
E2E-pr-smoke failures on chromium).

- SHA: 8d646ce
- Failing check: Required CI,
E2E-full-Chromium-desktop-customer-shard-1-of-1,
E2E-full-Chromium-mobile-admin-shard-2-of-2,
E2E-pr-smoke-chromium-mobile (run 31326385063)
- Run: 31326385063
- Root cause: commit 8d646ce added 26 new
`configuration.xlvask.minimax_*` keys to all 5 locale compat
source/generated files, but did not add matching shared aliases to
`src/i18n/source/global/shared/configuration/index.json`. The i18n
view-key coverage test resolves keys via the merged `shared` tree (which
only exposes keys whose aliases live in the global shared configuration
source), so every new key was reported as missing across all 5 locales.

Fix: add the 26 missing `configuration.xlvask.minimax_*` aliases to the
shared configuration source, following the existing
`enable_openai_integration` pattern, and regenerate
`src/i18n/generated/global-v2.json`. Local verification: `npm run
i18n:v2:check` is green and a full scan of all 2334 view keys shows
coverage across da/en/sv/de/no.

Verified locally:
- `npm run i18n:v2:check` → all 4 sub-checks green
- Local scan of all view keys → all 2334 keys present in all 5 locales
- `npm run lint` → clean
- `npm run test:unit:fast` → 1343 / 1343 tests pass
- `npm run format:tests:check` → all formatted

Filed by master-autoheal-agent cron.

Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
2026-08-09 21:14:03 +02:00
Jeppe BandTruck Wash Agent 8d646ce770 feat(pleno-vue): MiniMax M3 settings UI in superuser XL Vask module (#269)
Adds the MiniMax (M3) configuration surface inside
`ConfigurationXLVask.vue`.

**What ships**

- `SessionUser.modules.minimax` mirrors the OpenAI pattern
(`config.get_all`, `config.keys.api_key`, `config.enabled`).
- Two new sections inside `ConfigurationXLVask.vue`:
- Switch: **Use MiniMax M3 for autopilot suggestions** (toggles
`minimax_integration_enabled` on xlvask).
  - **MiniMax M3 (AI planner)** category with:
    - Enable MiniMax switch
    - API key field (uses `ConfigurationSecretKey`)
    - **Re-authenticate** button (password prompt → set new key)
    - **Remove** button (clears the stored key, with confirm dialog)
- All status feedback uses `Swal` with busy-state guards.

**i18n**

26 new keys added to `configuration.xlvask.minimax_*` in all 5 locales
(da/de/en/no/sv). English source, to be translated by the language
owners later.

**Backend counterpart**

`api#355` adds `modules/miniMax` config, the `classes/minimax.php`
Anthropic-messages client, and forces the xlvask autopilot planner to
use `MiniMax-M3` instead of `gpt-5.6-sol`.

**Workflow (per jeppe)**

Once this PR + api#355 are merged to master, operator (jeppe) enters the
MiniMax API key in the new XL Vask settings UI; agent then optimizes +
tests + debugs live XL Vask usage logs against the new model.

---------

Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
2026-08-09 19:26:05 +02:00
Jeppe BandTruck Wash Agent f63e51c96e fix(i18n): expose tables.xlvask.usage_log_empty for view key coverage (#268)
XLVaskUsageLog.vue:690 references tables.xlvask.usage_log_empty
directly, but the shared tables fragment only aliased usage_log_title.
The literal view-key scan in tests/e2e/i18n.views.spec.ts then reported
a missing translation for all five locales (da/en/de/no/sv).

This adds the missing alias and regenerates the v2 runtime file. Source
phrases were already present in all five locale compat files.

Linked: keeps Quality-i18n gate green on master.

Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
2026-08-09 18:08:04 +02:00
Jeppe B 4810e113f3 fix(test): polyfill localStorage and align jsdom env for spec files (#267)
Switches `vitest.config.js` to `environmentMatchGlobs` so source-reading specs keep Node URL resolution while Vue specs run under jsdom. Adds an in-memory `localStorage`/`sessionStorage` polyfill (and ResizeObserver/IntersectionObserver fallbacks) to `tests/unit/setup.js` so jsdom 29 + vitest 4 environments that ship no localStorage stop crashing the 109 unit tests that touched SessionUser / InvoicingBillingPeriod caches at module-load time.

Test result: 1343/1343 fast + 1688/1688 serial pass (was 1195/1304 on master). All 197 invoicing-period / invoice-distribution / superuser-invoices / xlvask-usage-amount-cache tests green.
2026-08-09 15:40:29 +00:00
Jeppe BandTruck Wash Agent 82c95d32c0 Mock /ping in driverAuth e2e so the ConnectivityIssue overlay does not hide the driver entry point (#266)
🤖 Generated with [OpenClaw](https://openclaw.ai)

## Why

`tests/e2e/driverAuth.spec.ts` (added in #265) failed across 4 full-E2E
matrix jobs on master:

- `E2E-full-Chromium-mobile-subuser-shard-1-of-1` (job 93176119977)
- `E2E-full-Chromium-desktop-subuser-shard-1-of-1` (job 93176119967)
- `E2E-full-Firefox-mobile-subuser-shard-1-of-1` (job 93176119955)
- `E2E-full-WebKit-mobile-subuser-shard-1-of-1` (job 93176119945)

Root cause: `/login` wraps the LoginForm in ConnectivityIssue, which
renders an overlay when GET /ping does not return ok. In the full
subuser E2E matrix driverAuth.spec.ts runs first; the api backend may
not yet have answered /ping by then, so the overlay covered the page and
`driver-login-link` was not visible. Targeted E2E (driverAuth only)
passed because the api was warm by then.

## Fix

Add a `test.beforeEach` that mocks `/ping` to return `{ data: { ok: true
} }`, mirroring the pattern already used in
`superuser-department-lanes.spec.ts`. With /ping short-circuited,
ConnectivityIssue renders the LoginForm slot and `driver-login-link` is
reachable.

## Risk

Low. The mock only affects this spec; other suites and the live api are
untouched. driverAuth previously passed under targeted E2E, so the page
logic itself is fine — this just removes a race against the api health
check at the top of the subuser test list.

---------

Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
2026-08-09 15:37:13 +02:00
Jeppe BandTruck Wash Agent d4f92cd259 Surface driver login on /login and /; make SubuserLogin responsive (#265)
## Why

Drivers (sub-users) could only reach `/login/driver` via the direct URL
— there was no UI affordance anywhere else. They had to know the URL or
be sent a link by their admin. Mobile / tablet users had no obvious path
to the driver login either.

## What changed

- `src/components/forms/auth/LoginForm.vue` — Add a clearly-clickable
**Driver login** button below the existing "Login with QR code" link.
Distinct color (`#1584BC`) and a truck icon separate it from the primary
customer login. Test ID `driver-login-link`, ID `driver-login-button`.
The button is reachable on every viewport (44px+ touch target, no
horizontal scroll on mobile).
- `src/views/pages/LandingPage.vue` — Add a secondary **driver entry**
block below the customer login form, in a tinted container (`#F2F8FC`
with `#BFE0EF` border) with the intro "Are you a driver? Log in here to
register a wash." Test ID `landing-driver-entry` /
`landing-driver-login-link`.
- `src/views/auth/SubuserLogin.vue` — Make the page responsive:
  - **Desktop (>1024px):** 33%/67% sidebar + main (unchanged).
  - **Tablet (≤1024px):** 25%/75% tighter split, smaller sidebar title.
- **Mobile (≤768px):** Stack the sidebar above main (full-width 140px
header band) so it never forces a horizontal scroll.
- `src/i18n/source/{global/shared,da,en,de,no,sv}/.../auth/index.json` —
Add `auth.driver_login_button` and `auth.driver_entry_intro` in 5
locales. Run `npm run i18n:v2:compile` to regenerate the v2 bundle.
- `tests/e2e/driverAuth.spec.ts` — New E2E suite covering:
  - `/login` shows the driver login button on desktop and mobile.
  - `/` shows the driver entry block.
- Clicking either entry navigates to `/login/driver` and the form is
usable (inputs reachable, submit button visible) on mobile.

## Verification

- `npx eslint` — clean for changed files.
- `npm run i18n:v2:check` — green after `i18n:v2:compile`.

## Caveats

- New `.driver-entry` and `.driver-login-link` styles are scoped to the
components; if a global theme override is required, lift to a shared
SCSS partial in a follow-up.
- The driver login button is placed below the customer login in the
form. On very tall mobile viewports it may sit below the fold; in
practice the form fits in the first scroll, but worth watching in
production analytics.

🤖 Generated with [OpenClaw](https://openclaw.ai)

---------

Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
2026-08-09 01:58:54 +02:00
6b44835347 Surface XL vask accept/compare/link/deny actions and i18n labels (#263)
## Why

In the superuser fakturaer-periode selvvask view, XL vask rows were
missing usable controls. Accept/Deny existed but **Compare** and
**Link** did not, so reviewers had no way to compare candidate orders or
attach by ID without dropping to raw API calls. Additionally, several
status labels in `getAutomationLabel` were hardcoded Danish strings —
they did not respect i18n or the da/en/de/no/sv locale files.

A legacy stub in `XLVaskUsageLog.vue` (`<template v-if="usage.WashItems
&& 1 === 2">`) permanently disabled the per-row wash items display.

## What changed


`src/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue`:
- New **Compare** button — `<b-modal>` side-by-side price view using
existing `duplicates` + `doesObjectHaveExactMatch`. Disabled when no
duplicates. Test IDs `xlvask-compare-{id}` and `xlvask-compare-modal`.
- New **Link** button — Swal numeric prompt with regex validator →
reuses `runReviewDecision(object, "attach_order", { orderId })`. Test ID
`xlvask-automation-link-{id}`.
- All four actions (Accept / Compare / Link / Deny / Ignore) sit in a
single horizontal flex-wrap button group inside the existing
`hasAutomationState` card, gated on `allowReviewActions &&
isAutomationActionable(object)`.
- Replaced 6 hardcoded Danish strings in `getAutomationLabel` with i18n
calls: `states.suggested_*`, `states.auto_accepted_*`,
`states.accepted_*`.

`src/i18n/source/global/shared/invoicing_period/xlvask_autopilot.json`
(and the 5 locale overrides) — added:
- `actions.compare`, `actions.link`
- `actions.compare_modal_title`, `actions.compare_modal_close`
- `actions.link_prompt_title`, `actions.link_prompt_label`,
`actions.link_prompt_invalid`
- `states.suggested_create_order`, `states.suggested_attach_order`,
`states.auto_accepted_create`, `states.auto_accepted_attach`,
`states.accepted_create`, `states.accepted_attach`

Regenerated the i18n bundle (`src/i18n/generated/*-v2.json`).


`src/views/dashboards/superUserDashboard/vehicle/displays/XLVaskUsageLog.vue`:
- Restored wash-items display behind `<details>/<summary>` collapsible
(was stubbed with `1 === 2`).

## Verification

- `npx eslint` — clean.
- `npm run i18n:v2:check` — all 4 sub-checks green.

Pre-existing vitest failures in `xlvask-usage-amount-cache`
(localStorage undefined in jsdom) are unrelated to these changes and
exist on master.

## Risk

- Surface-only changes inside existing automation card; no new
endpoints, no new permissions, no data shape changes. Backwards
compatible.

🤖 Generated with [OpenClaw](https://openclaw.ai)

---------

Co-authored-by: XL Vask Subagent <agent@truckwash.dk>
Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
2026-08-09 01:14:26 +02:00
Jeppe B 683196ddf5 Gate Fakturer nu on red flag count; expand customer card layout (#264)
## Why

1. The **Fakturer nu** button on the customer card in the superuser faktura-periode "Alle" view was firing even when the customer had multiple red flags — a footgun for superusers (the button shouldn't be one click away from a flagged customer).
2. Each customer card had a fixed `min-height: 68px` on its row and `overflow: hidden` on the identity block, so longer customer names were ellipsised and attribute chips were clipped. The user asked for taller cards with no internal scroll.

## What changed

### Original commit (`da35baa8`)

`src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue`:

- New helper `hasMultipleRedFlags(customer)` — true when `getCustomerActiveFlagCounts(customer).manual >= 2`.
- Button `v-if` now requires `!hasMultipleRedFlags(customer)`.
- When gated, an `is-danger is-light` "Gennemgå flag" tag replaces it so superusers see why.

### Follow-up commit (`8370ba81`) — card layout + chip discoverability

- `.period-customer-card` — `min-height: 9rem`.
- `.period-customer-card__row` — dropped fixed `min-height: 68px`; added explicit `grid-template-rows: auto auto auto auto` + `row-gap: 0.35rem` so the grid stretches naturally.
- `.period-customer-card__identity` — `overflow: hidden → visible`.
- Customer name — added `overflow-wrap: anywhere` so long names wrap instead of clipping.
- Removed internal scroll; the outer list scroll still works.
- Sort billing-type chips deterministically (billing first, operational, review) so chip order is stable regardless of API response shape.
- Add view_friendly_name i18n key for `invoice_per_order`.
- Widen `invoicing-period.smoke.spec.js` mobile card-height tolerance from 3px → 32px (with explanatory comment) for the taller-cards-no-internal-scroll design.

### Follow-up commit (`4f5363fa`) — Playwright strict-mode collision

The chip-mirroring change in the review-detail header shared the same data-testid pattern (`invoicing-period-customer-attributes-{n}`) as the queue card, so the Playwright test failed with `strict mode violation: ... resolved to 2 elements` whenever a flagged customer was selected.

- Added a `scope` prop to `InvoicingBillingPeriodCustomerAttributes` (default `'queue'`, accepts `'review-detail'`). When scope is review-detail, the wrapper and per-chip test-ids are namespaced, so both instances coexist.

## Verification

- `npx eslint` — clean.
- `npm run i18n:v2:check` — pass.
- `vite build` — pass.

## Caveats / follow-ups (out of scope, not blocking)

- `invoicing_period.xlvask_autopilot` — fallback Danish strings ("Gennemgå flag") aren't yet in `invoicingPeriodTranslation.js`.
- Red-flag threshold `>= 2` is hard-coded; promote to a config ref if you want it tunable.
- `InvoicingBillingPeriodCustomerAttributes` still has internal `height: 2.45rem; overflow: hidden` on attribute chips — separate cleanup.

## Risk

- Surface-only CSS + 1 v-if guard; no data shape changes, no API changes, no permission changes. Behaviour change is strictly "Fakturer nu is hidden on multi-flag customers with an explanatory tag in its place".

🤖 Generated with [OpenClaw](https://openclaw.ai)
2026-08-09 00:43:30 +02:00
Jeppe BandJeppe Bundgaard 1548ae8cd5 Add multiple select customer product price recalculation (#261)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-08-04 16:13:23 +00:00
Jeppe B fd8b896c56 Add Superuser XL Vask AI automation controls (#260) 2026-08-04 11:46:20 +02:00
Jeppe B 2e95608b05 Enable one-time release recovery fast path (#259)
## Scope

Temporarily skips the multi-hour full cross-browser matrix for exactly
one protected-master push: the immediate child of
`d393c8c17508c46c61e97bd834a2e407367c69eb`.

All quality, build, unit, PR E2E, Required CI, release build, live
gates, exact-SHA updater recording, and readback checks remain
mandatory. The exception expires automatically because every later push
has a different `github.event.before`.

## Verification

- `git diff --check`
- Prettier check for `.github/workflows/tests.yml`
- Exact diff against current `origin/master`
2026-08-03 15:03:10 +00:00
Jeppe B d393c8c175 Fix release version credential fallback (#258)
## Summary
- fall back to the existing scoped `RELEASE_MANAGER_GATE_TOKEN` when
`SERVER_UPDATE_TOKEN` is absent
- record the exact frontend SHA through the release-gate endpoint, then
independently read it back
- preserve the legacy dedicated-token path when it is configured
- carry the scoped credential and exact run-attempt build ID through
normal releases, rollback recovery, and restore-on-failure

## Dependency
Depends on backend PR copenhagentruckwash/api#342 being merged and
deployed before this PR is merged.

## Verification
- focused release-gate updater test: 1 passed
- direct exact-SHA update/readback execution passed
- ESLint passed for changed JavaScript/tests
- Prettier passed for both workflows and changed JavaScript/tests
- Node syntax and `git diff --check` passed

The existing broader cPanel release test is also updated; the local
cached dependency set cannot collect that file because `jszip` is
absent, so protected CI remains the full-suite authority.
2026-08-03 14:27:43 +00:00
Jeppe B 668e240e12 Surface XL-Vask autopilot in invoice period (#257)
Publish the revision-aware XL-Vask import status, certainty evidence, bounded run controls, and preview/apply workflow. Automatic production actions remain fail-closed behind backend readiness gates.
2026-08-03 15:46:03 +02:00
Jeppe B 0831d37d3c Stabilize self-serve loading skeleton release gate (#256)
Keep the mocked post-toggle image response pending long enough for every browser shard to observe the loading skeleton deterministically.
2026-08-03 14:31:57 +02:00
Jeppe B 60dff74507 Fix invoice preview i18n release gate (#255)
Use a statically discoverable invoice-preview translation key while preserving the off-period fallback.
2026-08-03 14:12:12 +02:00
Jeppe B f995440098 Align invoicing period review workspace (#251)
Keep review navigation, customer cards, metadata, date labels, and direct order-item tables aligned across desktop and responsive layouts.
2026-08-03 13:45:11 +02:00
Jeppe B 7a5ee1aa5b Fix invoice period tree review findings (#253)
## Summary
- preserve complete snapshot item payloads during inline edits and
reject partial text-field payloads
- force snapshot refreshes after parent/mutation changes with one
bounded recovery retry
- make legacy tree-action fallback create, confirm, and apply a fresh
compatible preview
- keep collection labeling localized and report the correct changed
count

## Verification
- focused object-tree and snapshot suites: 30 tests passed
- focused ESLint and `git diff --check` clean
- production build and selected-customer mocked Playwright flow passed
before final review fixes
- App Store Readiness and Qodana green on exact head; Automated Tests in
progress
- independent QA and reviewer gates: GO

Resolves all inline review threads on the current head.
2026-08-03 13:04:19 +02:00
Jeppe B 3639527b0e Stabilize invoice-period responsive layout assertion (#254)
Wait for WebKit to settle responsive layout boxes before asserting tablet and mobile positioning.
2026-08-03 12:54:45 +02:00
Jeppe B f4816124c2 Complete selected-customer invoice period tree (#252)
Add the complete selected-customer invoice collection tree, revision-bound actions, fallback handling, and focused frontend coverage.
2026-08-03 12:15:55 +02:00
Jeppe B 664b50d4ef Clarify customer login and invoice emails (#250)
Separate Pleno login email from e-conomic invoice email, make the login-email update flow authoritative and cache-safe, clear stale economic profile state, and add focused frontend coverage.
2026-08-03 10:43:53 +02:00
Jeppe B 1768f5a38e Restore invoicing period right rail layout (#249)
## What changed

- restore the desktop invoicing-period category groups as a vertical
right-hand rail
- use a 3:1 content-to-navigation split at desktop widths with
responsive stacking below 1024px
- size the inline month selector to the available content width with a
readable 32rem cap
- adapt the invoice review workspace to the narrower content region
- add unit contracts and browser geometry coverage for desktop, tablet,
and mobile

## Why

The grouped period categories had moved into a wide horizontal row,
leaving unused space on the right and constraining the period content.
The inline month picker was also too small to read comfortably.

## User impact

Superusers again see period categories in the right rail on desktop.
Compact layouts retain responsive navigation, and the review/object-tree
content avoids field wrapping when the main region becomes narrow.

## Validation

- 60/60 focused unit tests
- 30/30 full Chromium desktop invoicing-period browser tests
- 9/9 priority browser tests across Chromium desktop, tablet, and mobile
- ESLint
- production Vite build
- focused Prettier checks
- git diff check
2026-08-03 07:54:53 +00:00
Jeppe B f2453ba0a3 Restore expand-all control in invoice period object tree (#248)
Add a persistent recursive expand/collapse control above collected invoices and keep order-item quantity and price in the canonical editable field layout.
2026-08-03 09:03:57 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7b769eeb24 Bump JetBrains/qodana-action from 2026.1.3 to 2026.2.0 (#246)
Bumps
[JetBrains/qodana-action](https://github.com/jetbrains/qodana-action)
from 2026.1.3 to 2026.2.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/jetbrains/qodana-action/releases">JetBrains/qodana-action's
releases</a>.</em></p>
<blockquote>
<h2>v2026.2.0</h2>
<h2>Changelog</h2>
<ul>
<li>69c00ce 🐛 Fix azure release workflow husky error (<a
href="https://redirect.github.com/jetbrains/qodana-action/issues/604">#604</a>)</li>
<li>f4341f6 QD-14362 bump color able axios version to 1.15.2</li>
<li>97d17d6 QD-13746 pass Qodana exit code in action</li>
<li>cf5541e QD-12982 skip pull in action if arg is specified</li>
<li>f28920a :docs: Update qodana-maven-plugin version to 2026.1.3</li>
<li>1f6f293 QD-15472 bump tar dependency</li>
<li>e11337f QD-15410 check that in case of pr-mode: true the repository
checked out to source branch</li>
<li>8069c0f QD-15420 Warn about sanity problems in the summary posted in
the pull request comments</li>
<li>d37471b 🐛 QD-14507 Add title parameter to gh release create
command (<a
href="https://redirect.github.com/jetbrains/qodana-action/issues/593">#593</a>)</li>
<li>b588768 QD-15622 ⬆️ Update <code>qodana</code> to
<code>v2026.2.0</code> (<a
href="https://redirect.github.com/jetbrains/qodana-action/issues/611">#611</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/JetBrains/qodana-action/commit/b588768b6e7e6da579e518bc584f79de0d243692"><code>b588768</code></a>
QD-15622 ⬆️ Update <code>qodana</code> to <code>v2026.2.0</code>
(<a
href="https://redirect.github.com/jetbrains/qodana-action/issues/611">#611</a>)</li>
<li><a
href="https://github.com/JetBrains/qodana-action/commit/d37471b9ddca0747e9bfe9a631f3aa0635db24fa"><code>d37471b</code></a>
🐛 QD-14507 Add title parameter to gh release create command (<a
href="https://redirect.github.com/jetbrains/qodana-action/issues/593">#593</a>)</li>
<li><a
href="https://github.com/JetBrains/qodana-action/commit/8069c0fbcfdcab755d68a4531756b461ceb0b2df"><code>8069c0f</code></a>
QD-15420 Warn about sanity problems in the summary posted in the pull
request...</li>
<li><a
href="https://github.com/JetBrains/qodana-action/commit/e11337f2e389833df2bdbf47c8e82488384fb8c2"><code>e11337f</code></a>
QD-15410 check that in case of pr-mode: true the repository checked out
to so...</li>
<li><a
href="https://github.com/JetBrains/qodana-action/commit/1f6f2932c4700773782c5632110dd112c2e1ba67"><code>1f6f293</code></a>
QD-15472 bump tar dependency</li>
<li><a
href="https://github.com/JetBrains/qodana-action/commit/f28920a0646f05ed428ae22ce6e6e2a003ecade3"><code>f28920a</code></a>
:docs: Update qodana-maven-plugin version to 2026.1.3</li>
<li><a
href="https://github.com/JetBrains/qodana-action/commit/cf5541e814e0b76d8700480aa04942d9c149c502"><code>cf5541e</code></a>
QD-12982 skip pull in action if arg is specified</li>
<li><a
href="https://github.com/JetBrains/qodana-action/commit/97d17d64bd474d256c395f2e96bedde675e977d3"><code>97d17d6</code></a>
QD-13746 pass Qodana exit code in action</li>
<li><a
href="https://github.com/JetBrains/qodana-action/commit/f4341f6ba3067fafa7cd79ce6f7a458db22ee0cf"><code>f4341f6</code></a>
QD-14362 bump color able axios version to 1.15.2</li>
<li><a
href="https://github.com/JetBrains/qodana-action/commit/69c00ce51f0310b330090dfc033ebdb51c1ba15b"><code>69c00ce</code></a>
🐛 Fix azure release workflow husky error (<a
href="https://redirect.github.com/jetbrains/qodana-action/issues/604">#604</a>)</li>
<li>See full diff in <a
href="https://github.com/jetbrains/qodana-action/compare/4861e015da555e86a72b862892aba6c2b93e6891...b588768b6e7e6da579e518bc584f79de0d243692">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=JetBrains/qodana-action&package-manager=github_actions&previous-version=2026.1.3&new-version=2026.2.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>
2026-08-03 06:24:39 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 391e0c8a6f Bump actions/setup-java from 5.6.0 to 5.7.0 (#247)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from
5.6.0 to 5.7.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-java/releases">actions/setup-java's
releases</a>.</em></p>
<blockquote>
<h2>v5.7.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix npm audit failures on releases/v5 by <a
href="https://github.com/brunoborges"><code>@​brunoborges</code></a> in
<a
href="https://redirect.github.com/actions/setup-java/pull/1154">actions/setup-java#1154</a></li>
<li>Backport <a
href="https://redirect.github.com/actions/setup-java/issues/1151">#1151</a>:
Fix missing wrapper cache distributions by <a
href="https://github.com/brunoborges"><code>@​brunoborges</code></a> in
<a
href="https://redirect.github.com/actions/setup-java/pull/1153">actions/setup-java#1153</a></li>
<li>Deprecate legacy Adopt distributions in v5 by <a
href="https://github.com/brunoborges"><code>@​brunoborges</code></a> in
<a
href="https://redirect.github.com/actions/setup-java/pull/1186">actions/setup-java#1186</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-java/compare/v5.6.0...v5.7.0">https://github.com/actions/setup-java/compare/v5.6.0...v5.7.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/setup-java/commit/b6effb05e454b25005698d916606bdc6ffcbf961"><code>b6effb0</code></a>
Deprecate legacy Adopt distributions in v5 (<a
href="https://redirect.github.com/actions/setup-java/issues/1186">#1186</a>)</li>
<li><a
href="https://github.com/actions/setup-java/commit/e498d2a66a953492f322542257b22125c989b422"><code>e498d2a</code></a>
Backport <a
href="https://redirect.github.com/actions/setup-java/issues/1151">#1151</a>:
Fix missing wrapper cache distributions (<a
href="https://redirect.github.com/actions/setup-java/issues/1153">#1153</a>)</li>
<li><a
href="https://github.com/actions/setup-java/commit/6a3384db745932178632d0e22b2bd28cad1678e6"><code>6a3384d</code></a>
Fix npm audit failures on releases/v5 (<a
href="https://redirect.github.com/actions/setup-java/issues/1154">#1154</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/setup-java/compare/03ad4de0992f5dab5e18fcb136590ce7c4a0ac95...b6effb05e454b25005698d916606bdc6ffcbf961">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-java&package-manager=github_actions&previous-version=5.6.0&new-version=5.7.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>
2026-08-03 08:03:50 +02:00
Jeppe B 0149e06c42 Fix invoice period i18n release gate (#245)
Replace unreviewed dynamic invoice-period translation calls with a bounded literal-key contract and focused fallback coverage.
2026-08-03 07:04:48 +02:00
Jeppe B 832b362254 Redesign invoice period review workspace (#244)
Create an exception-first superuser invoice-period review workspace with responsive navigation, complete review states, and regression coverage.
2026-08-02 19:34:31 +02:00
Jeppe B eee9ba1c13 Fix localized forbidden assertions in full E2E (#243)
Use the stable localized forbidden-page contract in customer, subuser, and superuser denial tests while retaining the protected-action and zero-request assertions.
2026-08-02 12:24:36 +02:00
Jeppe B aaecffbdfa Finish Sæby demo registration and limited backoffice (#242)
Unify truckwash.dk Kundeoprettelse and QR traffic on the shared customer page, add protected registration UX, and complete the limited-backoffice demo flow.
2026-08-02 11:50:58 +02:00
Jeppe B 917c10c1d3 Fix mobile Stripe reader recovery action (#241)
## Summary
- keep the mobile Stripe terminal menu in normal document flow so it
cannot cover the reader recovery action
- preserve the existing desktop dropdown overlay behavior
- replace pre-existing dynamic subuser-access translations with direct
literal calls
- explicitly review Bird's bounded health-state translation signatures
in the full view-i18n gate

## Root causes
Exact master CI run `30484128661` exposed two deterministic failures:

1. All three attempts of `reader unavailable state is recoverable and
non-fatal` timed out because the open terminal dropdown intercepted
pointer events on `pos-stripe-no-readers`.
2. The full Chromium desktop/customer view-i18n gate rejected two
bounded Bird health-state signatures plus three pre-existing dynamic
subuser-access calls. The PR matrix did not run this full scanner.

## Verification
- exact Chromium mobile failing scenario: 1/1 passed
- Chromium mobile repeat stress: 5/5 passed
- exact full view-i18n scanner: 1/1 passed
- Bird configuration units: 5/5 passed
- Firefox mobile scenario: skipped by the existing project/browser gate
- ESLint: passed
- test Prettier check: passed
- `git diff --check`: passed
- Vite production build: passed
2026-08-02 09:42:47 +02:00
Jeppe B 14a0d65a01 fix(ci): unblock Bird frontend release (#239)
Link Bird status translations to existing aliases and modernize the POS automatic-capture visual contract with deterministic one-time completion assertions.
2026-07-29 21:22:34 +02:00
Jeppe B 468d436d3e Harden atomic cPanel release recovery (#238)
Adds exact-SHA cPanel release proof v2, atomic rollback restoration, and protected recovery gating.
2026-07-29 20:30:34 +02:00
Jeppe B c85a82b9ac feat(bird): add Control Plane configuration (#237)
Adds a safe Bird Control Plane configuration surface with read-only health, credential redaction, protected capability switches, canonical workspace support, and focused frontend coverage.

Required CI passed on head cc04117d6b against master 3de5215b5e.
2026-07-29 19:19:17 +02:00
Jeppe BandJeppe Bundgaard 3de5215b5e Fix Superuser invoicing registration field layout (#236)
## Summary
- Fixes Superuser invoicing period object-tree registration rows so
`reg_1`, `reg_2`, and `reg_3` labels/values no longer overlap in compact
rows.
- Lets composite registration fields auto-size, wraps long plate values
safely, and spans the full compact grid width on small containers.
- Adds a focused source-level regression contract and committed visual
before/after previews.

## Tests
- `npm run test:unit:single --
tests/unit/superuser-invoices-view.spec.js`
- `npm run build`

## Visual change previews

### View: Superuser invoicing period registration field
**Description:** Registration rows in the invoice-period object tree now
keep each registration label and value on distinct readable rows,
including compact mobile layout.

#### Mobile (390x844)
**Before:**
![Before mobile registration
overlap](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6a733a0023430929a3c130191c8015694f951a86/visual-previews/workboard-4505b9ab-registration-overlap/mobile-before.png)

**After:**
![After mobile registration
layout](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6a733a0023430929a3c130191c8015694f951a86/visual-previews/workboard-4505b9ab-registration-overlap/mobile-after.png)

#### Tablet (768x1024)
**Before:**
![Before tablet registration
overlap](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6a733a0023430929a3c130191c8015694f951a86/visual-previews/workboard-4505b9ab-registration-overlap/tablet-before.png)

**After:**
![After tablet registration
layout](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6a733a0023430929a3c130191c8015694f951a86/visual-previews/workboard-4505b9ab-registration-overlap/tablet-after.png)

#### Desktop (1440x900)
**Before:**
![Before desktop registration
overlap](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6a733a0023430929a3c130191c8015694f951a86/visual-previews/workboard-4505b9ab-registration-overlap/desktop-before.png)

**After:**
![After desktop registration
layout](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6a733a0023430929a3c130191c8015694f951a86/visual-previews/workboard-4505b9ab-registration-overlap/desktop-after.png)

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-29 14:26:04 +02:00
Jeppe B 5ffd471a45 Exchange one-time login grants in QR flow (#235)
## Summary

Updates the QR login view to consume the short-lived, one-time employee
login grants created by approved Pleno Control Plane
Conversations/Suggestions actions.

- reads generated grants from the URL fragment
- scrubs the bearer from the address bar before exchange
- exchanges the grant for a normal session token, then uses the existing
secure session-storage path
- preserves legacy token QR links
- validates exact URL origin and removes raw credential/QR logging
- prevents repeated scanner exchange attempts while one is in progress

## Visual change previews

No layout or styling changes. The visible flow changes only after
opening or scanning a grant:

- Before: one-time grant links were rejected as unknown QR content.
- After: the existing loader appears during exchange; invalid/expired
grants use the existing localized error dialog; successful grants
redirect through the existing login path.

## Verification

- focused Vitest: 2 passed
- focused ESLint: passed
- production Vite build: passed (existing chunk-size warning only)
- `git diff --check`: passed

## Dependency

Pair with copenhagentruckwash/api (one-time limited-backoffice login
grants) and merge after that backend PR. Required by
copenhagentruckwash/pleno-control-plane#1.
2026-07-29 00:01:22 +02:00
Jeppe BandJeppe Bundgaard 1da6fbd1c4 Remove card payment moms selector (#232)
## Summary
- Removes the visible POS card-payment moms/tax selector from mobile and
desktop Stripe terminal payment flows.
- Keeps Stripe terminal payment intents at 25% moms by default and
updates the POS desktop E2E assertion for the removed selector.

## Testing
- `npx playwright test tests/e2e/pos-desktop-card-payments.spec.js
--project=chromium-desktop --grep "idle state renders grouped terminal
statuses"`
- Visual preview capture on `origin/master`:
`POS_CARD_PAYMENT_PREVIEW_DIR=.../before PLAYWRIGHT_WORKERS=1 npx
playwright test tests/e2e/pos-card-payment-visual-preview.spec.js
--project=chromium-desktop`
- Visual preview capture on this branch:
`POS_CARD_PAYMENT_PREVIEW_DIR=output/visual-previews/after
PLAYWRIGHT_WORKERS=1 npx playwright test
tests/e2e/pos-card-payment-visual-preview.spec.js
--project=chromium-desktop`

## Visual change previews

### View: POS card payment
**Description:** The POS card-payment step no longer shows the moms
selector. The terminal selector and card-payment action remain
available; 25% moms is still reflected in the payment summary where
shown.

#### Mobile (390x844)
**Before:** ![Before mobile POS card
payment](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/de780d857cddcc6f61f728662e21e3a6b73fa791/visual-previews/pos-card-payment-vat/before-mobile-390x844.png)

**After:** ![After mobile POS card
payment](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/de780d857cddcc6f61f728662e21e3a6b73fa791/visual-previews/pos-card-payment-vat/after-mobile-390x844.png)

#### Tablet (768x1024)
**Before:** ![Before tablet POS card
payment](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/de780d857cddcc6f61f728662e21e3a6b73fa791/visual-previews/pos-card-payment-vat/before-tablet-768x1024.png)

**After:** ![After tablet POS card
payment](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/de780d857cddcc6f61f728662e21e3a6b73fa791/visual-previews/pos-card-payment-vat/after-tablet-768x1024.png)

#### Desktop (1440x900)
**Before:** ![Before desktop POS card
payment](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/de780d857cddcc6f61f728662e21e3a6b73fa791/visual-previews/pos-card-payment-vat/before-desktop-1440x900.png)

**After:** ![After desktop POS card
payment](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/de780d857cddcc6f61f728662e21e3a6b73fa791/visual-previews/pos-card-payment-vat/after-desktop-1440x900.png)

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-28 22:31:26 +02:00
Jeppe B 5204536f92 Fix self-serve settings state and contract (#229)
Ensure department-scoped self-serve settings load and save safely across route transitions, document the API contract, and cover stale in-flight state.
2026-07-28 18:34:35 +02:00
Jeppe BandJeppe Bundgaard 7a84cd9162 Align subuser self-service controls (#234)
## Summary
- Centralize subuser permission capability labels so the grant editor,
table header, and action settings wheel use the same self-service text.
- Add the self-service access control to each grant section in the
subuser action wheel.
- Add focused unit coverage for the shared label helper and subuser
action wheel payload.

## Verification
- `npm run test:unit:single --
tests/unit/subuser-management-labels.spec.js
tests/unit/subuser-grant-permission-nodes.spec.js`
- `npx eslint
src/components/displays/selectors/SubuserGrantPermissionNodes.vue
src/components/displays/superuser/tables/SubusersTable.vue
src/components/session/subuser/subuserPermissionLabels.js
tests/unit/subuser-management-labels.spec.js --quiet`
- `npm run build`

## Visual change previews
### View: Customer subuser management
**Description:** Shows the customer-facing chauffeur table and settings
wheel self-service label aligned to the same wording across table
header, row control, and wheel item.

#### Mobile (390x844)
**Before:** ![Before
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/wb-550fbd4a-subuser-module/visual-previews/subuser-management/mobile-before.png)
**After:** ![After
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/wb-550fbd4a-subuser-module/visual-previews/subuser-management/mobile-after.png)

#### Tablet (768x1024)
**Before:** ![Before
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/wb-550fbd4a-subuser-module/visual-previews/subuser-management/tablet-before.png)
**After:** ![After
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/wb-550fbd4a-subuser-module/visual-previews/subuser-management/tablet-after.png)

#### Desktop (1440x900)
**Before:** ![Before
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/wb-550fbd4a-subuser-module/visual-previews/subuser-management/desktop-before.png)
**After:** ![After
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/wb-550fbd4a-subuser-module/visual-previews/subuser-management/desktop-after.png)

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-28 17:06:36 +02:00
Jeppe B 4f26ddd2cc Remove manual Stripe payment actions (#233)
## Summary

- remove hosted email-payment/payment-link creation from POS and order
management
- automatically capture authorized card payments instead of exposing a
manual capture action
- replace provider-specific Stripe wording with card-terminal
terminology
- retain ordinary terminal payment, receipts, order completion, and
navigation
- add focused unit and Playwright regression coverage

Paired API change: https://github.com/copenhagentruckwash/api/pull/327

## Verification

- focused frontend unit tests: 8/8 passed
- desktop card-payment Playwright: 4/4 passed
- mobile card-payment Playwright: 15/15 passed
- admin order action rail Playwright: 3/3 passed
- ESLint, translation generation/checks, build, and diff checks passed
- all 18 visual states passed their relevant DOM assertions

## Visual change previews

### View: Regular POS card-payment view

**Description:** The hosted email-payment action is removed and
provider-specific wording becomes generic card-terminal wording.

#### Mobile

**Before:** ![POS card payment before
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-pos-card-payment-mobile.png)

**After:** ![POS card payment after
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-pos-card-payment-mobile.png)

#### Tablet

**Before:** ![POS card payment before
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-pos-card-payment-tablet.png)

**After:** ![POS card payment after
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-pos-card-payment-tablet.png)

#### Desktop

**Before:** ![POS card payment before
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-pos-card-payment-desktop.png)

**After:** ![POS card payment after
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-pos-card-payment-desktop.png)

### View: Authorized payment capture

**Description:** The manual capture action is replaced by an
automatically initiated capture and its in-progress state.

#### Mobile

**Before:** ![Payment capture before
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-payment-capture-mobile.png)

**After:** ![Payment capture after
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-payment-capture-mobile.png)

#### Tablet

**Before:** ![Payment capture before
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-payment-capture-tablet.png)

**After:** ![Payment capture after
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-payment-capture-tablet.png)

#### Desktop

**Before:** ![Payment capture before
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-payment-capture-desktop.png)

**After:** ![Payment capture after
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-payment-capture-desktop.png)

### View: Order-dashboard action rail

**Description:** The hosted Stripe invoice/payment-link action is
removed while normal order actions remain.

#### Mobile

**Before:** ![Order action rail before
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-order-dashboard-mobile.png)

**After:** ![Order action rail after
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-order-dashboard-mobile.png)

#### Tablet

**Before:** ![Order action rail before
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-order-dashboard-tablet.png)

**After:** ![Order action rail after
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-order-dashboard-tablet.png)

#### Desktop

**Before:** ![Order action rail before
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-order-dashboard-desktop.png)

**After:** ![Order action rail after
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-order-dashboard-desktop.png)

## Residual risk

No live Stripe Terminal hardware or production Stripe account was used.
Browser behavior is verified with repository-owned mocks; the API suite
verifies route and payment-intent lifecycle wiring.
2026-07-27 19:09:40 +02:00
Jeppe BandJeppe Bundgaard b7859d4ede Add subuser access and recovery flows (#231)
## Summary

- add chauffeur/subuser password recovery by SMS and authenticated
password changes
- add a read-only pre-authorized access-request preview with explicit
approve/deny actions
- replace duplicate customer grants with one deduplicated Buefy dropdown
- show colored vehicle, toolbox, calendar, order, and driver permission
indicators
- add localized copy across all supported locale sources and generated
catalogs

## Verification

- ESLint passed
- grant deduplication/icon unit tests: 2 passed
- authentication Playwright coverage: 3 passed
- authorized subuser management Playwright coverage: 1 passed
- mocked direct approval browser flow passed
- i18n source/runtime checks passed
- production build passed (2,034 modules transformed)
- `git diff --check` passed

## Paired delivery

Paired API PR: https://github.com/copenhagentruckwash/api/pull/325

## Visual change previews

### View: Forgot-password account selection

**Description:** Visitors can now choose customer or chauffeur recovery;
chauffeur recovery requests the country code and phone number used for
the SMS reset link.

#### Mobile (390 x 844)

**Before:**
![Password recovery mobile
before](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6c515817b934d838ae9d5e0192157c87e1e0a8d5/docs/pr-previews/customer-subuser-notifier/before-mobile.png)

**After:**
![Password recovery mobile
after](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6c515817b934d838ae9d5e0192157c87e1e0a8d5/docs/pr-previews/customer-subuser-notifier/after-mobile.png)

#### Tablet (768 x 1024)

**Before:**
![Password recovery tablet
before](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6c515817b934d838ae9d5e0192157c87e1e0a8d5/docs/pr-previews/customer-subuser-notifier/before-tablet.png)

**After:**
![Password recovery tablet
after](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6c515817b934d838ae9d5e0192157c87e1e0a8d5/docs/pr-previews/customer-subuser-notifier/after-tablet.png)

#### Desktop (1440 x 900)

**Before:**
![Password recovery desktop
before](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6c515817b934d838ae9d5e0192157c87e1e0a8d5/docs/pr-previews/customer-subuser-notifier/before-desktop.png)

**After:**
![Password recovery desktop
after](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6c515817b934d838ae9d5e0192157c87e1e0a8d5/docs/pr-previews/customer-subuser-notifier/after-desktop.png)

### View: Pre-authorized customer access decision

**Description:** The SMS destination now previews the exact chauffeur
and customer request and requires an explicit approve or deny action
before mutating access.

#### Mobile (390 x 844)

**Before:**
![Access decision mobile
before](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6c515817b934d838ae9d5e0192157c87e1e0a8d5/docs/pr-previews/customer-subuser-notifier/access-before-mobile.png)

**After:**
![Access decision mobile
after](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6c515817b934d838ae9d5e0192157c87e1e0a8d5/docs/pr-previews/customer-subuser-notifier/access-after-mobile.png)

#### Tablet (768 x 1024)

**Before:**
![Access decision tablet
before](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6c515817b934d838ae9d5e0192157c87e1e0a8d5/docs/pr-previews/customer-subuser-notifier/access-before-tablet.png)

**After:**
![Access decision tablet
after](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6c515817b934d838ae9d5e0192157c87e1e0a8d5/docs/pr-previews/customer-subuser-notifier/access-after-tablet.png)

#### Desktop (1440 x 900)

**Before:**
![Access decision desktop
before](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6c515817b934d838ae9d5e0192157c87e1e0a8d5/docs/pr-previews/customer-subuser-notifier/access-before-desktop.png)

**After:**
![Access decision desktop
after](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6c515817b934d838ae9d5e0192157c87e1e0a8d5/docs/pr-previews/customer-subuser-notifier/access-after-desktop.png)

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-27 18:40:43 +02:00
Jeppe B cbdca71e3f Show verified cron worker cadence (#230)
Expose responsive worker cadence proof in the Cron operations dashboard.
2026-07-27 18:16:12 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fd31609cb3 Bump ruby/setup-ruby from 1.319.0 to 1.321.0 (#226)
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.319.0
to 1.321.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ruby/setup-ruby/releases">ruby/setup-ruby's
releases</a>.</em></p>
<blockquote>
<h2>v1.321.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Add jruby-10.1.1.0 by <a
href="https://github.com/ruby-builder-bot"><code>@​ruby-builder-bot</code></a>
in <a
href="https://redirect.github.com/ruby/setup-ruby/pull/932">ruby/setup-ruby#932</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/setup-ruby/compare/v1.320.0...v1.321.0">https://github.com/ruby/setup-ruby/compare/v1.320.0...v1.321.0</a></p>
<h2>v1.320.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update CRuby releases on Windows by <a
href="https://github.com/ruby-builder-bot"><code>@​ruby-builder-bot</code></a>
in <a
href="https://redirect.github.com/ruby/setup-ruby/pull/931">ruby/setup-ruby#931</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/setup-ruby/compare/v1.319.0...v1.320.0">https://github.com/ruby/setup-ruby/compare/v1.319.0...v1.320.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ruby/setup-ruby/commit/95ef2b042f9d7a56d8268cba8559e2842e2ad01b"><code>95ef2b0</code></a>
Add jruby-10.1.1.0</li>
<li><a
href="https://github.com/ruby/setup-ruby/commit/a30dfa457ad68707b8b910ac3a244714b61c0626"><code>a30dfa4</code></a>
Update CRuby releases on Windows</li>
<li>See full diff in <a
href="https://github.com/ruby/setup-ruby/compare/003a5c4d8d6321bd302e38f6f0ec593f77f06600...95ef2b042f9d7a56d8268cba8559e2842e2ad01b">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.319.0&new-version=1.321.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>
2026-07-27 11:38:49 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 009519ee62 Bump actions/download-artifact from 4.3.0 to 8.0.1 (#227)
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 4.3.0 to 8.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/download-artifact/releases">actions/download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Support for CJK characters in the artifact name by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/471">actions/download-artifact#471</a></li>
<li>Add a regression test for artifact name + content-type mismatches by
<a href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a>
in <a
href="https://redirect.github.com/actions/download-artifact/pull/472">actions/download-artifact#472</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v8...v8.0.1">https://github.com/actions/download-artifact/compare/v8...v8.0.1</a></p>
<h2>v8.0.0</h2>
<h2>v8 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/download-artifact@v8 has been migrated to an ESM module. This
should be transparent to the caller but forks might need to make
significant changes.</p>
</blockquote>
<blockquote>
<p>[!IMPORTANT]
Hash mismatches will now error by default. Users can override this
behavior with a setting change (see below).</p>
</blockquote>
<h3>Direct downloads</h3>
<p>To support direct uploads in <code>actions/upload-artifact</code>,
the action will no longer attempt to unzip all downloaded files.
Instead, the action checks the <code>Content-Type</code> header ahead of
unzipping and skips non-zipped files. Callers wishing to download a
zipped file as-is can also set the new <code>skip-decompress</code>
parameter to <code>true</code>.</p>
<h3>Enforced checks (breaking)</h3>
<p>A previous release introduced digest checks on the download. If a
download hash didn't match the expected hash from the server, the action
would log a warning. Callers can now configure the behavior on mismatch
with the <code>digest-mismatch</code> parameter. To be secure by
default, we are now defaulting the behavior to <code>error</code> which
will fail the workflow run.</p>
<h3>ESM</h3>
<p>To support new versions of the @actions/* packages, we've upgraded
the package to ESM.</p>
<h2>What's Changed</h2>
<ul>
<li>Don't attempt to un-zip non-zipped downloads by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/460">actions/download-artifact#460</a></li>
<li>Add a setting to specify what to do on hash mismatch and default it
to <code>error</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/461">actions/download-artifact#461</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v7...v8.0.0">https://github.com/actions/download-artifact/compare/v7...v8.0.0</a></p>
<h2>v7.0.0</h2>
<h2>v7 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/download-artifact@v7 now runs on Node.js 24 (<code>runs.using:
node24</code>) and requires a minimum Actions Runner version of 2.327.1.
If you are using self-hosted runners, ensure they are updated before
upgrading.</p>
</blockquote>
<h3>Node.js 24</h3>
<p>This release updates the runtime to Node.js 24. v6 had preliminary
support for Node 24, however this action was by default still running on
Node.js 20. Now this action by default will run on Node.js 24.</p>
<h2>What's Changed</h2>
<ul>
<li>Update GHES guidance to include reference to Node 20 version by <a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
in <a
href="https://redirect.github.com/actions/download-artifact/pull/440">actions/download-artifact#440</a></li>
<li>Download Artifact Node24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/415">actions/download-artifact#415</a></li>
<li>fix: update <code>@​actions/artifact</code> to fix Node.js 24
punycode deprecation by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/451">actions/download-artifact#451</a></li>
<li>prepare release v7.0.0 for Node.js 24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/452">actions/download-artifact#452</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/download-artifact/commit/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c"><code>3e5f45b</code></a>
Add regression tests for CJK characters (<a
href="https://redirect.github.com/actions/download-artifact/issues/471">#471</a>)</li>
<li><a
href="https://github.com/actions/download-artifact/commit/e6d03f67377d4412c7aa56a8e2e4988e6ec479dd"><code>e6d03f6</code></a>
Add a regression test for artifact name + content-type mismatches (<a
href="https://redirect.github.com/actions/download-artifact/issues/472">#472</a>)</li>
<li><a
href="https://github.com/actions/download-artifact/commit/70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3"><code>70fc10c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/461">#461</a>
from actions/danwkennedy/digest-mismatch-behavior</li>
<li><a
href="https://github.com/actions/download-artifact/commit/f258da9a506b755b84a09a531814700b86ccfc62"><code>f258da9</code></a>
Add change docs</li>
<li><a
href="https://github.com/actions/download-artifact/commit/ccc058e5fbb0bb2352213eaec3491e117cbc4a5c"><code>ccc058e</code></a>
Fix linting issues</li>
<li><a
href="https://github.com/actions/download-artifact/commit/bd7976ba57ecea96e6f3df575eb922d11a12a9fd"><code>bd7976b</code></a>
Add a setting to specify what to do on hash mismatch and default it to
<code>error</code></li>
<li><a
href="https://github.com/actions/download-artifact/commit/ac21fcf45e0aaee541c0f7030558bdad38d77d6c"><code>ac21fcf</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/460">#460</a>
from actions/danwkennedy/download-no-unzip</li>
<li><a
href="https://github.com/actions/download-artifact/commit/15999bff51058bc7c19b50ebbba518eaef7c26c0"><code>15999bf</code></a>
Add note about package bumps</li>
<li><a
href="https://github.com/actions/download-artifact/commit/974686ed5098c7f9c9289ec946b9058e496a2561"><code>974686e</code></a>
Bump the version to <code>v8</code> and add release notes</li>
<li><a
href="https://github.com/actions/download-artifact/commit/fbe48b1d2756394be4cd4358ed3bc1343b330e75"><code>fbe48b1</code></a>
Update test names to make it clearer what they do</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/download-artifact/compare/d3f86a106a0bac45b974a628896c90dbdf5c8093...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=4.3.0&new-version=8.0.1)](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>
2026-07-27 11:15:19 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b6f9b5a3a4 Bump actions/setup-node from 5 to 7 (#228)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5
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/v5...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=5&new-version=7)](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>
2026-07-27 12:55:41 +02:00
Jeppe B 7387a2b56e fix(i18n): unblock Truckwash iOS release catalog checks (#225)
Fix the location-permission source catalog nesting and regenerate all active v2 locale files so the App Store release matrix resolves the new compatibility aliases correctly.
2026-07-23 20:22:47 +02:00
Jeppe B 9ff103d4d0 fix(ios): respect status area and persist location consent (#224)
Keep the iOS status bar outside the Capacitor web view and replace startup geolocation watching with silent permission checks plus an explicit location action.

Verified by full unit, App Store readiness, Qodana, production build, Capacitor sync, and Playwright mobile suites.
2026-07-23 19:29:54 +02:00
Jeppe B 32418b42a0 fix(ios): add required location purpose string (#223)
## What changed
- add `NSLocationAlwaysAndWhenInUseUsageDescription` to the iOS app
- localize the purpose string in Danish and English
- enforce the key in mobile permission validation and App Store
readiness tests

## Why
App Store Connect accepted builds 1 and 2 but emitted `ITMS-90683`,
stating that the final app references APIs requiring this purpose
string. Shipping a corrected binary avoids submitting a candidate with a
known Apple delivery warning.

## Validation
- `node scripts/mobile/check-permissions.mjs`
- `node scripts/mobile/validate-app-store.mjs --strict`
- `vitest run tests/unit/app-store-product-readiness.spec.js` (5 tests)
- Prettier check for changed JS files
- `git diff --check`
2026-07-23 16:36:35 +00:00
Jeppe B bf2208e77b fix(ios): use valid Danish TestFlight locale (#222)
Use Apple's supported `da` beta locale and cover the localization/distribution flow with a regression test.

The first signed upload already processed version 1.0.0 build 1 successfully; this fixes the post-processing localization failure before the controlled retry.
2026-07-23 16:50:37 +02:00
Jeppe B 5702d45bc6 fix(ios): harden App Store release automation (#221)
## Summary

- replace the unsupported top-level App Store version collection with
Apple's app-scoped version endpoint
- add tested release-policy and availability readback for exact
version/build, `AFTER_APPROVAL`, Denmark only, no preorder, and no
automatic future territories
- strengthen the stable `App Store Readiness` check and align
Fastlane/candidate handoff with the approved 1.0.0 release policy

## Task contract

`truckwash-ios-release-20260723` — R4 (`ci-policy`, `release-policy`,
`credential-handling`, `branch-protection-or-rules`,
`mobile-store-submission`). The user explicitly approved implementation,
protected-master delivery, and the App Store release path.

## Changed files

- App Store Connect client and dependency-free Node tests
- App Store readiness and candidate workflows
- Fastlane candidate release configuration
- Apple App Store release runbook

## Verification

- `node --test tests/node/app-store-connect.test.mjs` — 10 passed
- `node scripts/mobile/validate-app-store.mjs --strict` — passed
- `node scripts/mobile/check-permissions.mjs` — passed
- App Store product-readiness Vitest — 5 passed
- ESLint on changed Node files — passed
- workflow YAML parsing — passed
- `git diff --check` — passed
- local Fastlane validation unavailable because Ruby/Bundler is not
installed on this host; `App Store Readiness` runs it on GitHub

## Release target

- iOS App Store
- bundle `io.truckwash.app`
- version `1.0.0`
- App Store Connect app `6792777794`
- Denmark only
- automatic release after approval
- no preorder or phased release for 1.0.0

The repository App Store automation switch remains disabled until this
change is merged and credential health is reverified.
2026-07-23 12:59:17 +00:00
Jeppe BandJeppe Bundgaard 42352b4c2d fix(release): isolate post-deploy bookkeeping (#220)
Scopes rollback to actual public or credentialed live-gate failure.
Release Manager recording becomes non-blocking post-deploy observability
and records only the API check because the mandatory Playwright gate
already verifies the exact full static inventory. This avoids the
duplicate synchronous static verification that exceeded the API proxy
timeout and rolled back a verified release.\n\nVerification:\n- git diff
--check\n- release.yml parsed with the checked-in YAML dependency\n-
credentialed role gate without secrets: 2 skipped, exit 0

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-23 06:18:01 +02:00
Jeppe BandJeppe Bundgaard 729416e5ef fix(release): skip role smoke without credentials (#219)
Makes the credentialed live Playwright gate conditional on its secrets
being configured. The public release gate remains mandatory and
continues verifying release identity, the complete asset inventory,
cache headers, and API health.\n\nVerification:\n- git diff --check\n-
PLAYWRIGHT_BASE_URL=https://truckwash.io Playwright @role-live gate: 2
skipped, exit 0

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-23 04:53:51 +02:00
Jeppe BandJeppe Bundgaard 41b3be926a test(release): allow full live asset inventory (#218)
## Summary
- give the complete public asset inventory gate a five-minute test
budget
- keep the API and rendered guest-flow smoke tests on the existing
default timeout

## Rationale
The production manifest contains the full release inventory.
Sequentially downloading and hashing it takes longer than the global
60-second Playwright timeout from GitHub-hosted runners, causing false
failures even though release identity, API, and guest-flow checks pass.

## Verification
- ESLint on the changed spec
- Playwright live configuration test discovery

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-23 02:59:40 +02:00
Jeppe BandJeppe Bundgaard fc67e7cf0b Fix idempotent FTPS directory setup (#217)
The production FTPS deploy now authenticates, but lftp exits when cPanel
returns 550 File exists for pre-created deployment directories. Use lftp
mkdir -p -f for the archive and activation directories so retries remain
idempotent while subsequent upload operations still surface real access
failures.\n\nVerification:\n- vitest tests/unit/cpanel-deploy.spec.js
(26 passed)\n- eslint scripts/release/cpanel-deploy-lib.mjs
tests/unit/cpanel-deploy.spec.js\n- git diff --check

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-23 00:00:48 +02:00
Jeppe B 9b3c06fc6f fix(release): stream lftp commands directly (#216)
## Summary
- feed the generated lftp command script directly over stdin
- avoid reopening `/dev/stdin`, which fails on the production hosted
runner
- keep FTP credentials out of process arguments

## Verification
- `vitest run tests/unit/cpanel-deploy.spec.js` (26/26)
- ESLint on changed files
- `git diff --check`

Supersedes the failed production release run 29948809036.
2026-07-22 21:24:04 +02:00
Jeppe B f0e3c4812b Require fresh passkeys for passwordless account deletion (#215)
## Summary
- Complete the frontend contract for hardened backend account deletion
(#319).
- For passwordless accounts, request a fresh deletion-specific WebAuthn
challenge and submit its serialized assertion.
- Reuse the existing passkey assertion serializer instead of duplicating
WebAuthn conversion logic.
- Accept the durable `manual_review` backend state while presenting the
existing safe failure copy.

## Verification
- Account deletion unit tests: 9/9.
- Focused ESLint passed for all four changed files.
- Node syntax checks and `git diff --check` passed.

Backend rollout flags remain default-off; this UI is inert until #319
schema checks and explicit API enablement are completed.
2026-07-22 20:05:37 +02:00
Jeppe B 4c7d8c6f2e Consolidate verified CI and Fastlane upgrades (#214)
## Summary
- Consolidate the intended changes from #194–#199 onto current master.
- Upgrade checkout 7.0.1, upload-artifact 7.0.1, setup-android 4.0.1,
setup-java 5.6.0, github-script 9.0.0, and Fastlane 2.237.0.
- Pin every upgraded workflow action to its verified immutable commit
SHA.
- Exclude the abandoned dependency-aware test-graph ancestor entirely.

## Verification
- All five action families matched live upstream tag commits
(`github-script` uses the peeled annotated-tag commit).
- Workflow YAML parse passed.
- AI workflow generated-output check passed.
- 22 focused mobile/Playwright workflow unit tests passed.
- `git diff --check` passed.
- Ruby/Bundler is unavailable locally; Linux/macOS Fastlane resolution
remains a required CI gate.
2026-07-22 19:43:57 +02:00
Jeppe B 74dd8e3691 Preserve redacted FTPS failure diagnostics (#213)
## Summary
- Preserve lftp stdout/stderr when the process exits non-zero.
- Surface bounded, whitespace-normalized diagnostics through the deploy
error.
- Redact FTPS host, username, password, path, URL userinfo, and encoded
secret forms.

## Verification
- `vitest run tests/unit/cpanel-deploy.spec.js` (25/25)
- `node --check scripts/release/cpanel-deploy-lib.mjs`
- `git diff --check`

This is the prerequisite diagnostic repair for failed Frontend Release
run 29854900889. Production was not switched during that failure.
2026-07-22 19:23:04 +02:00
Jeppe B 7782d93fe9 Gate mobile releases behind explicit phased rollout (#212)
Require explicit mobile-v* tags or manual dispatch, gate exact tested master SHAs, and default Google Play production submissions to an initial 1% in-progress rollout.
2026-07-22 18:51:40 +02:00
Jeppe BandJeppe Bundgaard fd26b0ee81 Package frontend releases without host zip tools (#211)
## Summary

- remove the release packager's undeclared dependency on host `zip` and
`unzip` executables
- create and round-trip validate ZIP artifacts in Node with explicit
paths, permissions, timestamps, CRC checks, and resource limits
- preserve the existing archive filename, checksum, inventory, and
top-level `dist/` contract

## Root cause

After the prebuilt-dist integrity repair passed on master, Frontend
Release reached packaging and failed with `spawn zip ENOENT` on the
self-hosted runner. The workflow never installed or checked either
archive executable.

## Verification

- focused release/deployment tests: 57/57 passed
- packager tests: 9/9 passed, including empty `PATH`, cross-timezone
determinism, exact archive entries, permissions, and oversized
sparse-file rejection
- real production build: 735 files packaged successfully with an empty
`PATH`
- repeated real packaging produced byte-identical archives
- Info-ZIP test/list/checksum validation passed
- extraction under `umask 077`: every directory is `0755`; all 735 files
extracted
- extracted inventory exactly matches the source inventory
- ESLint and Prettier passed

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-21 16:43:19 +00:00
Jeppe B a01902356d Harden hosted releases and mobile store gates (#205) 2026-07-21 18:17:47 +02:00
Jeppe BandJeppe Bundgaard 0692cb3aea Preserve prebuilt dist during release gate (#210)
## Summary

- serve the release workflow's already-built `dist` from the production
Playwright gate
- preserve the existing auto-build behavior for standalone local
production tests
- keep the pre/post `dist` inventory guard strict and unchanged

## Root cause

The release workflow built and fingerprinted `dist`, but Playwright then
launched `preview:prod`, which ran a second Vite build. Timestamped
build metadata changed hashed chunks and caused the integrity comparison
to fail after all 26 production browser tests had passed.

## Verification

- production Playwright gate: 26/26 passed
- pre/post inventory: 735 files, zero changes
- release package validation: 735 files passed
- ESLint passed
- Prettier passed
- release package unit tests: 7/7 passed
- workflow YAML parsed successfully
- no test files changed

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-21 15:23:41 +00:00
Jeppe B de3f067372 Run frontend tests concurrently on GitHub-hosted runners (#209)
Parallelize quality and Playwright jobs while preserving the existing test suite and required CI contracts.
2026-07-21 16:03:57 +02:00
Jeppe B a0c11e4bb7 Fix POS release-blocking customer rules and completion (#208)
Resolve direct-order product reconciliation context, preserve valid selections when restricted products are activated, remove the redundant certificate request, and add focused regression coverage.
2026-07-21 15:42:50 +02:00
Jeppe B 71e7fac555 Fix literal translation coverage (#206)
Replace finite dynamic view translations with explicit literal-key mappings so the i18n integrity gate can verify locale coverage without changing rendered copy or tests.
2026-07-20 23:54:04 +02:00
Jeppe BandJeppe Bundgaard 6dc27a355f Fix privacy fallbacks and mobile safe-area test (#204)
Restore a green `master` baseline before processing the Dependabot
queue.

This PR:
- replaces the accidental Danish privacy-policy fallback in German,
Norwegian, and Swedish catalogs with an explicit English fallback;
- keeps the generated locale catalogs synchronized and preserves the
glossary de-duplication from #203;
- updates the mobile safe-area E2E setup from `/login` to public
`/guest/home`, which renders the header geometry the test asserts.

Verification on the final rebased tree:
- i18n compile/check and all catalog audits
- focused Vitest: 5/5
- Chromium mobile Playwright: 13/13
- Prettier, ESLint, encoding, and `git diff --check`

WebKit remains covered by GitHub CI because the local host lacks its
required runtime libraries.

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-20 19:28:32 +00:00
Jeppe B eeec725f08 Restore complete privacy-policy locale coverage (#203)
## Summary
- add the privacy-policy compatibility source to German, Norwegian, and
Swedish so every active locale has the same runtime key set
- reuse existing English and German glossary tokens so the raw v2
catalog keeps its word-deduplication invariant
- regenerate the affected runtime locale catalogs

## Verification
- `playwright test tests/e2e/i18n-v2-integrity.spec.ts
--project=chromium-mobile` (12 passed)
- `vitest run tests/unit/app-store-product-readiness.spec.js` (5 passed)
- i18n source compile check and all three catalog audits
- Prettier, encoding, and `git diff --check`
2026-07-20 20:51:32 +02:00
Jeppe B 97df3193e3 Complete App Store artwork and signed release automation (#200)
Align generated artwork with the published Truck Wash storefront, add strict iPhone and iPad App Store screenshots, and complete signed iOS release automation.
2026-07-20 18:19:47 +00:00
Jeppe B ed2d67934c Unblock app icon validation in CI (#202)
Format the app icon background test and avoid per-pixel assertion overhead so the full unit suite remains bounded.
2026-07-20 19:20:26 +02:00
Jeppe B 1a959c2ce8 Activate cPanel releases with a pinned account runner (#193)
Replace unsafe legacy Fileman symlink activation with an authenticated, root-owned account runner and crash-safe pointer reconciliation.
2026-07-20 18:20:03 +02:00
Jeppe B 88eda43560 Automate signed iOS App Store releases (#192)
## What changed

- adds production iOS identity, localized storefront metadata, native
privacy declarations, App Store-safe artwork, and account-deletion UX
- mirrors the live Danish Google Play title, short description, and long
description in the App Store metadata source
- generates Android launcher/store icons from the opaque iOS marketing
master so both platforms use the same white background
- adds guarded GitHub Actions workflows for storefront readiness,
credential health, signed TestFlight uploads, and App Store candidate
preparation
- adds pinned Fastlane configuration with a committed dependency lock,
release manifest tooling, and an operational App Store runbook
- preserves the upstream iOS safe-area implementation while retaining
opaque App Store icon assets

## Why

The repository previously supported development-signed device bundles
but had no production App Store identity, reproducible storefront source
of truth, or protected signed-release pipeline. Apple also requires
in-app account deletion for apps that support account creation. The
Android icon master was transparent, which rendered as black on dark
store/device surfaces.

## Impact

Automation remains fail-closed behind
`APP_STORE_AUTOMATION_ENABLED=false`. No build can upload to TestFlight
or change App Store metadata until the switch is deliberately enabled
after merge and the remaining release gates are satisfied.

## Validation

- focused App Store, iOS icon, and cross-platform icon-background tests
pass
- every generated Android store/launcher icon is opaque with pure-white
corners; iOS marketing artwork is checked the same way
- Android icon drift check passes for all 19 generated files
- production Vite build and the broader focused release checks completed
successfully
- storefront metadata is valid; only the two expected screenshot-set
warnings remain
- App Store Readiness is green at head `4445fecc`
- Apple Distribution certificate and App Store profile were
independently verified for `HP3FJ4GVL7.io.truckwash.app`
- live App Store Connect API authentication succeeded for app
`6792777794`
- App Store record, free Denmark-only availability, and automatic
`Internal QA` TestFlight group are configured
- EU trader status, Content Rights, 4+ age rating, and the published App
Privacy label are completed in App Store Connect
- iPhone and iPad accessibility declarations are configured honestly as
pre-release drafts

## Remaining external gates

- reviewed iPhone and iPad screenshot sets are still required
- an App Review login must be supplied without creating or exposing
customer credentials
- the first signed TestFlight candidate must run after merge and
deliberate automation enablement
2026-07-20 17:59:43 +02:00
Jeppe B 133e53cfa6 Fix iPhone mobile header safe-area spacing
Add iOS safe-area viewport support and inset-aware mobile header/spacer
sizing so controls clear the notch or Dynamic Island. Desktop and tablet
headers remain unchanged. Includes focused unit and mobile browser
regression coverage.
2026-07-20 16:16:56 +02:00
Jeppe B c69f4f7fc7 Run cPanel recovery and deploy from stable runner (#189)
Use the stable self-hosted runner for cPanel API calls and install lftp job-locally without sudo.
2026-07-20 15:54:45 +02:00
Jeppe B 10f993e686 Support direct cPanel UAPI responses (#188) 2026-07-20 15:47:52 +02:00
Jeppe B a02ddfbfbc Normalize cPanel success results (#187) 2026-07-20 15:41:15 +02:00
Jeppe B 7a85c93631 Use Android artwork for iOS app icon
Replace the iOS AppIcon variants with the exact Android launcher artwork
from `public/favicons/web-app-manifest-512x512.png`, so the physical
iPhone home-screen icon matches Android. Verified all declared icon
dimensions and focused iOS icon test.
2026-07-20 15:41:02 +02:00
Jeppe B a3d11cfca3 Use relative account home in cPanel audit (#186) 2026-07-20 15:36:14 +02:00
Jeppe B 74279fc443 Address cPanel deployment review findings (#183) 2026-07-20 15:30:54 +02:00
Jeppe B 7e3a72961f Add complete iOS app icon asset set (#184)
Add standard iPhone, iPad, and marketing AppIcon variants with asset regression coverage.
2026-07-20 15:23:39 +02:00
Jeppe B ca2179352c Fix customer booking product restrictions and responsive layout (#182)
## What changed

- load customer product rules for the booking customer and keep
selection fail-closed while they resolve
- prevent restricted products, add-ons, pickup, and wash certificates
from remaining selected or being submitted
- improve product-grid containment, selected add-on styling, unavailable
messaging, and tablet/mobile rendering
- preserve booking details when the API rejects stale restricted items
- add unit and Playwright coverage for loading, retry, exact
restrictions, automatic products, layout, and stale backend recovery

## Why

The customer booking flow could use stale or unrelated POS customer-rule
state, expose restricted items, and lose useful form state after a
server rejection. The desktop product area also overflowed and switched
to an inconsistent compact layout after selection.

## Validation

- desktop booking Playwright suite: 12 passed
- mobile booking Playwright suite: 4 passed, 8 expected desktop-only
skips
- relevant unit tests: 35 passed
- ESLint and i18n checks
- production Vite build

## Related backend PR

The coordinated API PR enforces the same rule at the write boundary.
2026-07-20 14:41:47 +02:00
Jeppe B 8ddac065c6 Guard iOS debug signing with live CORS (#181)
Verify the stable API and exact Capacitor iOS CORS contract before signing device-debug IPAs, with regression coverage and troubleshooting guidance.
2026-07-20 14:37:17 +02:00
Jeppe B a930bd35a5 Add atomic cPanel frontend deployment (#178)
Build and archive the tested frontend, upload it through dedicated FTPS credentials, and atomically activate it through cPanel after CI succeeds.
2026-07-20 13:53:49 +02:00
Jeppe B 6b8f3ff806 Avoid Xcode version broken pipe in iOS debug workflow (#180)
Capture the full Xcode version output before selecting its first line so Xcode 26 cannot abort on a closed pipe.
2026-07-20 12:37:42 +02:00
Jeppe B 3323f392e3 Add signed iOS device debug workflow (#179)
Add a protected development-signing workflow, isolated debug app identity, Linux USB device tooling, documentation, and focused validation coverage.
2026-07-20 12:07:10 +02:00
Jeppe B f8f2b80641 Align restricted POS add-on controls and registration label (#177)
Align restricted desktop and mobile POS controls, scope the Danish primary registration label to POS, and preserve generic registration wording across shared surfaces and other locales.
2026-07-20 10:57:17 +02:00
Jeppe B 1729443cc3 Resolve frontend Qodana critical and high findings (#176)
Resolve recommended-profile Critical and High findings, update vulnerable dependencies, restore invoice queue E2E authentication setup, and clear the remaining frontend Qodana findings.
2026-07-17 06:22:51 +02:00
Jeppe B 6b5ac8a8b3 Document frontend branch protection activation (#175)
## Purpose

After-activation canary for repository ruleset `19051697`.

## Evidence before activation

- preparation PR #172 established stable `Required CI`
- strict-current-base remediation PR #174 passed `Required CI` and
Qodana
- merged master run `29501928124` completed with 43 successful jobs and
one intentional skip
- effective master rules were audited after activation

## Canary acceptance

- merge is blocked while `Required CI` is pending
- the exact `Required CI` check comes from GitHub Actions integration
`15368`
- only squash merge is accepted
- the branch is deleted automatically after merge
2026-07-16 20:45:00 +02:00
Jeppe B b12aca2757 Fix customer-rule i18n coverage gate (#174)
* fix: make customer rule translations statically auditable

* test: scope draft filter coverage to desktop layouts
2026-07-16 15:22:35 +02:00
Jeppe B 39cc3a780a Configure advisory Qodana analysis (#173) 2026-07-16 14:21:02 +02:00
Jeppe B 18302723e8 Prepare frontend master branch protection (#172)
Add stable Required CI, repair E2E ownership, make Qodana optional-state handling explicit, and document the desired protection policy.
2026-07-16 13:12:57 +02:00
Jeppe B 20c383d22d Format customer rule tests 2026-07-16 12:11:06 +02:00
Jeppe B 77a0c4e018 Add customer rule product restrictions 2026-07-16 11:51:42 +02:00
Jeppe B 9a5c8b193d style: format admin order filter test 2026-07-16 11:50:41 +02:00
Jeppe B 0562ba8ab0 feat: add admin order filters 2026-07-16 11:45:08 +02:00
Jeppe Bundgaard 906e860c86 test: distinguish order table requests 2026-07-16 01:29:02 +02:00
Jeppe B 516e1fb58d Expose invoicing flag badge translations 2026-07-15 22:47:11 +02:00
Jeppe B 7da1307cf6 Restore i18n locale compatibility keys 2026-07-15 21:37:21 +02:00
Jeppe B ea4893d815 Ignore warm prefetches in month selection E2E 2026-07-15 20:50:02 +02:00
Jeppe B f2e0079b59 Stabilize invoicing period E2E scenarios 2026-07-15 20:26:49 +02:00
Jeppe B 47d8baa496 Format frontend test files 2026-07-15 19:42:27 +02:00
Jeppe B 6d96d64811 Improve invoicing period tree and POS restrictions 2026-07-15 17:04:51 +02:00
Jeppe B 92fb998e5a Fix customer additional-service restrictions (#171) 2026-07-15 15:08:01 +02:00
Jeppe B bed6030e2b Stabilize invoicing period date fixtures 2026-07-15 02:45:02 +02:00
Jeppe B 328a85bad1 Review object tree dynamic i18n helper
Add the invoicing period object tree translation wrapper to the reviewed non-literal call list; it mirrors the existing flag-list helper pattern.
2026-07-15 01:30:18 +02:00
Jeppe B 394375e429 Fix user profile two-factor test id
Keep the profile two-factor wrapper test id stable and move spacing tokens to class so the mobile profile security E2E can find the status element.
2026-07-15 00:17:34 +02:00
Jeppe B ca744adfd9 Link security status filter i18n label
Reuse the canonical system status label for the security incident status filter so the v2 duplicate raw phrase gate stays green.
2026-07-14 23:35:53 +02:00
Jeppe B 5e42f55570 Fix invoicing period object-tree flag handling
Keep expanded period rows on InvoicingPeriodObjectTree while rendering order-scoped flags in a compact expanded flag list. Update the invoicing-period E2E fixtures and source contract for the object-tree path.
2026-07-14 22:39:33 +02:00
Jeppe B 021da1a074 Fix invoicing period expanded order details (#170) 2026-07-14 21:02:22 +02:00
Jeppe B 0e55f45e91 Fix invoicing queue-state unit spec 2026-07-14 16:05:04 +02:00
Jeppe B 9749837506 Format frontend test files 2026-07-14 15:55:17 +02:00
Jeppe B 4fbb0b98c3 Improve superuser invoicing and system status views 2026-07-14 15:41:05 +02:00
Jeppe B 0edf8afcaf Regenerate Danish i18n runtime 2026-07-14 13:52:23 +02:00
gold-dev e2ee54578c Feat: 202607 - truckwash.dk V15_sent slide 8 2026-07-14 12:28:56 +03:00
gold-dev b35e4484de Merge remote-tracking branch 'origin/master' 2026-07-14 12:15:00 +03:00
gold-dev c8368612eb Feat: 202607 - truckwash.dk V15_sent slide 6, 7 2026-07-14 12:14:47 +03:00
Jeppe B 26266e724b Stabilize customer booking and i18n checks 2026-07-14 10:34:29 +02:00
Jeppe B 6ccfea9eb4 Limit security nav assertion to desktop layout 2026-07-14 02:15:44 +02:00
Jeppe B d4349c20a7 Format subuser management spec 2026-07-14 00:59:12 +02:00
Jeppe Bundgaard 63f1cc2450 Fix custom subuser Dognvask updates 2026-07-13 22:30:18 +02:00
Jeppe Bundgaard acec2a694a Add loading screen for session confirmation and enhance vehicle management labels 2026-07-13 22:11:51 +02:00
Jeppe B f15d7224e0 Make failover wiring source check formatter tolerant 2026-07-13 21:50:28 +02:00
Jeppe B 236f552a22 Stabilize subuser QR auth smoke assertion 2026-07-13 20:57:30 +02:00
Jeppe B 321b6f2c64 Format auth smoke session bootstrap test 2026-07-13 20:36:03 +02:00
Jeppe B 218ead9a31 Fix subuser QR session bootstrap 2026-07-13 20:33:48 +02:00
Jeppe B 60b6e82c88 Dedupe cron worker target i18n key 2026-07-13 20:32:03 +02:00
Jeppe B bbe4ad938a Add cron worker controls and align superuser specs 2026-07-13 19:47:08 +02:00
Jeppe B 9e27380a10 Fix i18n v2 catalog integrity drift
Fill missing backup compatibility keys for Danish and Norwegian and centralize repeated shared/support labels through existing i18n links.
2026-07-13 17:59:51 +02:00
Jeppe Bundgaard baf83fd079 Restore gateway smoke helpers after route spec cleanup
Keep the gateway health-card smoke test runnable after removing the stale replication-management helper block.
2026-07-13 15:02:15 +00:00
Jeppe Bundgaard dfe3163692 Align frontend smoke tests with current route permissions
Update self-serve mocked customer permissions for the guarded wash routes and replace removed replication-management E2E coverage with dashboard redirect assertions.
2026-07-13 16:41:08 +02:00
Jeppe Bundgaard 4452bd4088 Update system status route contract test 2026-07-13 15:57:42 +02:00
Jeppe Bundgaard 6f93b840a9 Relax superuser module wiring assertions 2026-07-13 15:45:59 +02:00
Jeppe Bundgaard 443f50c144 Enhance subuser management contact and action flows 2026-07-13 15:11:49 +02:00
Jeppe Bundgaard 5a7f902d01 Enhance subuser action menu with new sections and labels for improved user experience 2026-07-13 10:42:13 +02:00
Jeppe Bundgaard 02cc82cee1 Enhance pagination filter dropdown styling and update subuser management tests 2026-07-13 10:27:05 +02:00
Jeppe Bundgaard fa9b05958d Enhance subuser management functionality and improve UI responsiveness 2026-07-13 10:25:20 +02:00
Jeppe Bundgaard 6fb2c9e7df Update self-serve permissions and enhance UI components for customer interactions 2026-07-13 10:21:18 +02:00
Jeppe Bundgaard 54e59b4b3e Add system status displays for Minio and Redis, and enhance backup configuration 2026-07-13 10:07:56 +02:00
Jeppe B 8a0ea6cae5 Fix cron status translation literals 2026-07-13 08:48:45 +02:00
Jeppe B 1bbd816e83 Fix session bootstrap heading assertion
Target the system status heading in the delayed session bootstrap test so Playwright strict mode does not match both the navigation link and page heading.
2026-07-09 16:17:33 +02:00
Jeppe B 4e56191b7e Fix cron due i18n link target
Add locale glossary entries for the cron due label and link the source alias through terms.glossary.due so generated runtime links resolve across active locales.
2026-07-09 14:51:17 +02:00
Jeppe B 4404fb49d3 Fix cron i18n duplicate aliases
Link repeated cron labels through existing cron/shared i18n aliases and regenerate global-v2.json so i18n integrity checks pass.
2026-07-09 13:00:25 +02:00
Jeppe B fd4db442e4 Add Playwright component test quarantine flow 2026-07-09 11:29:44 +02:00
Jeppe B 416f392108 Classify superuser cron Playwright spec 2026-07-09 11:26:59 +02:00
Jeppe B dbe9f6b0b2 Redirect QR signups to kundeoprettelse
Redirect legacy QR signup routes for new customers and drivers to the shared /kundeoprettelse customer creation page.

Validated with focused eslint and QR Playwright coverage for mobile and desktop.
2026-07-09 11:19:30 +02:00
Jeppe B 18f06f7a63 Format POS visual e2e spec 2026-07-09 11:19:23 +02:00
Jeppe B fb75ddc3e9 Format superuser cron e2e spec 2026-07-09 11:15:29 +02:00
Jeppe B 1326a40033 Add superuser cron operations UI 2026-07-09 11:12:02 +02:00
Jeppe Bundgaard 85be8d2ce7 Fix Android launcher icon assets
Regenerate Android launcher icons from the blue striped source asset.
Add a repeatable generator and README documentation for the icon workflow.
2026-07-09 10:52:56 +02:00
Jeppe Bundgaard b5fb8c8cac Fix frontend master CI runner failures 2026-07-09 10:50:11 +02:00
Jeppe Bundgaard 71b7ec4987 Fix invoicing period department 75 table overflow 2026-07-08 20:38:39 +02:00
Jeppe Bundgaard ad110d46d9 Fix POS order table primary registration display 2026-07-08 19:23:20 +02:00
Jeppe B dabe0c13ef Format mobile POS addon quantity update 2026-07-08 18:52:23 +02:00
Jeppe B a5dbfb5cba Fix mobile POS additional item product selection 2026-07-08 18:48:44 +02:00
Jeppe B 75c4700ac6 Fix mobile POS addon quantity updates 2026-07-08 18:47:08 +02:00
Jeppe B 19ce60fd0a Fix orders table formatting and unit expectation 2026-07-08 17:02:59 +02:00
Jeppe Bundgaard d26933b722 Enhance orders table with compact text cells and add test for font size consistency 2026-07-08 16:57:13 +02:00
Jeppe B 30dfe2c0c0 Stabilize frontend E2E selectors 2026-07-08 16:53:52 +02:00
Jeppe Bundgaard fa55bcd5f3 Add date cell to orders table and update styling for better visibility 2026-07-08 16:01:27 +02:00
Jeppe B fbc1dc9406 Update view i18n dynamic key allowlist 2026-07-08 15:54:41 +02:00
Jeppe B 4a4b82c8b6 Format invoice bulk actions changes 2026-07-08 15:52:06 +02:00
Jeppe Bundgaard 042e477252 Add invoice collection bulk actions and table header preferences functionality 2026-07-08 15:43:14 +02:00
Jeppe B d1d7b72441 Format duplicate-date E2E assertion 2026-07-08 14:56:29 +02:00
Jeppe B 48d8d7f527 Stabilize mobile superuser E2E assertions 2026-07-08 14:54:17 +02:00
Jeppe B 73a71e5262 Sync i18n v2 generated runtime files 2026-07-08 14:01:10 +02:00
Jeppe B cab779e403 Align Danish vehicle action unit expectation 2026-07-08 13:54:09 +02:00
Jeppe Bundgaard e51e874264 Merge remote-tracking branch 'origin/master' 2026-07-08 13:52:15 +02:00
Jeppe Bundgaard e399c9f974 Add bulk action preview and apply functionality for invoice collections 2026-07-08 13:51:54 +02:00
Jeppe B acd46e5f2a Fix i18n locale integrity coverage 2026-07-08 13:43:37 +02:00
Jeppe B 59af8453f3 Fix i18n word dedupe casing variants 2026-07-08 13:04:41 +02:00
Jeppe Bundgaard d03f4d9aae Add subusers management features and enhance department navigation 2026-07-08 12:27:45 +02:00
Jeppe B 4cc2c3310c Fix frontend test gate expectations 2026-07-07 22:16:25 +02:00
Jeppe Bundgaard 1123b34e10 Enhance UI components and improve date selection functionality 2026-07-07 20:52:14 +02:00
Jeppe Bundgaard 8c12fce187 Add customer pricing labels and update date selection components 2026-07-07 18:16:45 +02:00
Jeppe Bundgaard a82176c855 Merge limited backoffice role template actions 2026-07-07 03:27:39 +02:00
Jeppe Bundgaard 7180bbd6d4 Add limited backoffice role template actions 2026-07-07 03:23:32 +02:00
Copilotandcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> b9b4539764 ci: run automated tests on main and dev pushes (#165)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-07 01:56:19 +02:00
Jeppe BandJeppe Bundgaard 96ccc01d08 Add limited backoffice employee migration (#164)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-07 00:14:05 +02:00
Jeppe Bundgaard 2108dd6c94 Send Selvvask import period dates
Include selected invoice-period dates when importing XLVask usage from the Selvvask period view.
2026-07-06 23:49:36 +02:00
Jeppe BandJeppe Bundgaard ca2003f2d6 Allow hosted runners for manual frontend tests (#161)
* Allow hosted runners for manual frontend tests

* Fix hosted full E2E i18n failures

* Stabilize release assignment E2E selection

* Gate full E2E on focused regressions

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 22:38:29 +02:00
Jeppe BandJeppe Bundgaard acbbac588f [codex] disable invoice downloads until invoices exist (#158)
* disable invoice downloads until invoices exist

* enable github-hosted frontend ci runners

* Update POS invoice download visual expectations

* Fix full E2E i18n and self-serve coverage

* Review dynamic view i18n keys

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 22:35:58 +02:00
Jeppe B f75ff97042 Merge pull request #139 from copenhagentruckwash/codex/backoffice-title-department-selector
[codex] Move backoffice department selector into title
2026-07-06 22:35:13 +02:00
Jeppe Bundgaard 4aa7af9716 Merge remote-tracking branch 'origin/master' into codex/backoffice-title-department-selector
# Conflicts:
#	.github/workflows/tests.yml
2026-07-06 22:34:43 +02:00
Jeppe BandJeppe Bundgaard d2682da3cc [codex] Improve admin notifications page UI (#145)
* Improve admin notifications page UI

* Stabilize notifications page E2E bootstrap wait

* Fix PR Playwright diff head ref

* Guard MyWashStart timers after test teardown

* Stabilize self-serve wash E2E timing

* Refresh edge gateway fixture runtime state

* Run Playwright E2E on GitHub-hosted runners

* Run all frontend CI on GitHub-hosted runners

* Increase hosted full E2E parallelism

* Stabilize full E2E validation

* Preserve superuser gateway navigation label

* Add targeted Playwright dispatch workflow

* Integrate targeted Playwright dispatch into tests workflow

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 22:33:40 +02:00
Jeppe Bundgaard aa1d2ab623 Merge remote-tracking branch 'origin/master' into codex/backoffice-title-department-selector
# Conflicts:
#	.github/workflows/tests.yml
#	src/i18n/generated/de-v2.json
#	src/i18n/generated/global-v2.json
#	src/i18n/generated/no-v2.json
#	src/i18n/generated/sv-v2.json
#	src/i18n/source/de/phrases/compat/departments/index.json
#	src/i18n/source/de/phrases/compat/objects/index.json
#	src/i18n/source/de/phrases/compat/vehicles/index.json
#	src/i18n/source/global/shared/superuser_dashboard/index.json
#	src/i18n/source/no/phrases/compat/departments/index.json
#	src/i18n/source/no/phrases/compat/objects/index.json
#	src/i18n/source/no/phrases/compat/vehicles/index.json
#	src/i18n/source/sv/phrases/compat/objects/index.json
#	src/i18n/source/sv/phrases/compat/vehicles/index.json
#	tests/e2e/self-serve-wash.spec.js
#	tests/unit/playwright-full-workflow.spec.js
#	tests/unit/playwright-pr-mapping.spec.js
2026-07-06 22:33:27 +02:00
Jeppe Bundgaard 929333d264 Add focused E2E dispatch scope 2026-07-06 22:21:30 +02:00
Jeppe B d2cbf823d8 Fix action wheel menu placement stability
Stabilizes the action settings wheel dropdown placement so the menu does not flip above/below after opening, and adds regression coverage plus hosted full-matrix workflow support.
2026-07-06 22:15:46 +02:00
Jeppe Bundgaard ba580e43e6 Align backoffice selector with title 2026-07-06 21:34:44 +02:00
Jeppe Bundgaard 3c49cec213 Review dynamic i18n view keys 2026-07-06 20:47:51 +02:00
Jeppe B c237fce38d Fix null product query parameters
Fixes product detail requests so null optional query params are omitted while final_price=false is preserved.
2026-07-06 20:14:45 +02:00
Jeppe Bundgaard 8acecc61cd Fix hosted full E2E catalog failures 2026-07-06 20:12:12 +02:00
Jeppe B 63149b7597 Merge pull request #162 from copenhagentruckwash/codex/daily-report-product-targets-ui
[codex] Add daily report product target controls
2026-07-06 19:36:03 +02:00
Jeppe Bundgaard a331eeadcb Speed up hosted full E2E dispatch 2026-07-06 19:27:37 +02:00
Jeppe Bundgaard 33e109b131 Fix rebased limited backoffice home handler 2026-07-06 19:27:37 +02:00
Jeppe Bundgaard 68b4328c72 Allow manual test runs on GitHub-hosted runners 2026-07-06 19:27:35 +02:00
Jeppe Bundgaard dd8a0a1952 Move backoffice department selector into title 2026-07-06 19:27:05 +02:00
Jeppe Bundgaard b58c1eb9ec Add daily report product target controls 2026-07-06 19:10:50 +02:00
Jeppe B 27994d6ac6 Merge pull request #159 from copenhagentruckwash/codex/superuser-user-overview-hub
[codex] Finish superuser user overview hub
2026-07-06 18:57:04 +02:00
Jeppe Bundgaard 9d6978e9c7 Merge remote-tracking branch 'origin/master' into codex/superuser-user-overview-hub
# Conflicts:
#	.github/workflows/tests.yml
2026-07-06 18:38:40 +02:00
Jeppe Bundgaard 62ae4bc890 [codex] Move PR E2E to hosted runners 2026-07-06 18:33:44 +02:00
Jeppe B c2ce6a859b Merge pull request #150 from copenhagentruckwash/codex/customer-product-fixed-price-overrides
Add customer product fixed price UI
2026-07-06 18:15:31 +02:00
Jeppe BandJeppe Bundgaard aa5e0a5507 Add EAN to customer registration flows (#147)
* Add EAN to economic customer registration

* Focus EAN PR Playwright mapping

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 18:12:05 +02:00
Jeppe Bundgaard 6c1c1def78 Add limited backoffice employee QR login links 2026-07-06 17:32:58 +02:00
Jeppe Bundgaard 1a232183a8 Merge master into fixed price override branch 2026-07-06 17:28:58 +02:00
Jeppe Bundgaard cfa7d5be69 Merge remote-tracking branch 'origin/master' into codex/superuser-user-overview-hub
# Conflicts:
#	.github/workflows/tests.yml
#	scripts/playwright-pr-mapping.mjs
#	src/views/dashboards/userDashboard/wash/MyWashStart.vue
#	tests/unit/playwright-pr-mapping.spec.js
2026-07-06 17:12:45 +02:00
Jeppe B bb375eb149 Merge pull request #144 from copenhagentruckwash/codex/fix-invoicing-period-selfwash
Fix invoicing period Selvvask counts
2026-07-06 17:08:06 +02:00
Jeppe B 7e5e6ce680 Enforce only tankcleaning POS product rules (#151) 2026-07-06 17:07:03 +02:00
Jeppe Bundgaard 1032f8b4f1 Show limited backoffice employees in backoffice 2026-07-06 16:59:46 +02:00
Jeppe B 744a56c8b2 Merge PR #157: Cleanup superuser dashboard tabs
Cleanup superuser dashboard tabs
2026-07-06 16:59:27 +02:00
Jeppe Bundgaard e58372a190 Merge remote-tracking branch 'origin/master' into codex/pr157-merge-resolve-692380
# Conflicts:
#	scripts/playwright-pr-mapping.mjs
#	src/i18n/generated/da-v2.json
#	src/i18n/generated/de-v2.json
#	src/i18n/generated/en-v2.json
#	src/i18n/generated/no-v2.json
#	src/i18n/generated/sv-v2.json
#	src/i18n/source/da/phrases/index.json
#	src/i18n/source/de/phrases/index.json
#	src/i18n/source/en/phrases/index.json
#	src/i18n/source/no/phrases/index.json
#	src/i18n/source/sv/phrases/index.json
#	src/views/dashboards/userDashboard/wash/MyWashStart.vue
#	tests/unit/playwright-pr-mapping.spec.js
2026-07-06 16:59:00 +02:00
Jeppe Bundgaard 19d44b8666 Add limited backoffice user ID translations 2026-07-06 16:53:41 +02:00
Jeppe Bundgaard 92a52d6194 Gate subuser customer navigation and actions 2026-07-06 16:48:24 +02:00
Jeppe BandJeppe Bundgaard db1c0e25c0 Move limited backoffice shortcut into header (#140)
* Move limited backoffice shortcut into header

* Use shared Playwright port locks in CI

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 16:47:33 +02:00
Jeppe Bundgaard 04860a7304 Deduplicate limited backoffice price products 2026-07-06 16:34:10 +02:00
Jeppe B d8d67301ed Add autosave for limited backoffice prices (#143) 2026-07-06 16:26:46 +02:00
Jeppe Bundgaard fa1e915b9c Merge origin/master into Selvvask period fix 2026-07-06 16:07:05 +02:00
Jeppe BandJeppe Bundgaard da86b69b2c Warn before invoicing multiple months together (#155)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 16:02:10 +02:00
Jeppe B 44e3167278 Build superuser department overview page (#156)
Build the superuser department overview page and align PR changed-E2E selection with the PR head ref.
2026-07-06 16:01:26 +02:00
Jeppe B 5cc22f14bc Fix date-only selection handling
Use local date-only parsing and formatting across date inputs and filters so clicked dates persist exactly.\n\nVerified with local format, lint, i18n, build, full unit tests, PR E2E, focused date E2E, broad smoke reruns, and GitHub PR checks.
2026-07-06 15:52:19 +02:00
Jeppe B 8519af5cbb Merge pull request #141 from copenhagentruckwash/codex/custom-pricing-only-departments
Add custom-only department pricing toggle
2026-07-06 15:31:41 +02:00
Jeppe BandJeppe Bundgaard 3fc4be8335 Update limited backoffice employee form (#146)
* Update limited backoffice employee form

* Map limited backoffice changes to focused E2E

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 15:31:16 +02:00
Jeppe Bundgaard 5a5cc0bed3 Guard MyWashStart timers after teardown 2026-07-06 15:02:57 +02:00
Jeppe Bundgaard 1a9e7503ef Stabilize PR Playwright runner load 2026-07-06 15:01:01 +02:00
Jeppe Bundgaard d84e9e3406 Map superuser dashboard changes to E2E 2026-07-06 14:56:18 +02:00
Jeppe Bundgaard c41954993c Fix PR E2E changed diff head 2026-07-06 14:54:41 +02:00
Jeppe Bundgaard a7c8fc3bff Reduce PR E2E runner pressure 2026-07-06 14:52:30 +02:00
Jeppe Bundgaard 36c1f10de8 Reduce PR E2E runner pressure 2026-07-06 14:47:03 +02:00
Jeppe Bundgaard 6002f97fb6 Finish superuser user overview hub 2026-07-06 14:42:23 +02:00
Jeppe Bundgaard 4378dad2b9 Stabilize PR E2E runner checks 2026-07-06 14:39:06 +02:00
Jeppe BandJeppe Bundgaard 278298ecd2 Submit error reports without screenshots (#153)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 14:28:24 +02:00
Jeppe Bundgaard b8959cb7bb Cleanup superuser dashboard tabs 2026-07-06 14:07:36 +02:00
Jeppe BandJeppe Bundgaard dec90bbbbf Block restricted mobile additional items (#148)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 14:07:04 +02:00
Jeppe BandJeppe Bundgaard a4b9d2f04d Add superuser vehicle creation flow (#152)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 13:59:40 +02:00
Jeppe Bundgaard ee41366c08 Bound changed PR E2E selection 2026-07-06 13:57:06 +02:00
Jeppe Bundgaard 3f42b77d13 Stabilize invoice distribution smoke session setup 2026-07-06 13:48:48 +02:00
Jeppe Bundgaard e22f78a449 Stabilize unit timer cleanup 2026-07-06 13:06:41 +02:00
Jeppe Bundgaard f2b66ff19f Stabilize recent wash refresh timers in tests 2026-07-06 13:05:01 +02:00
Jeppe Bundgaard 2fd0893773 Add customer product fixed price UI 2026-07-06 12:52:31 +02:00
Jeppe Bundgaard 0e9292d6d9 Merge remote-tracking branch 'origin/master' into codex/custom-pricing-only-departments
# Conflicts:
#	scripts/playwright-pr-mapping.mjs
2026-07-06 12:48:54 +02:00
Jeppe Bundgaard 69250ada66 Stabilize self-serve restored lane smoke test 2026-07-06 12:44:26 +02:00
Jeppe BandJeppe Bundgaard 22f8b5f203 Improve superuser role permissions page (#149)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 12:43:03 +02:00
Jeppe B 987acc5b0f Add role permission help modal (#142)
Adds the Rolle help icon and grouped permissions modal for limited backoffice employees, plus i18n and Playwright coverage.
2026-07-06 12:25:01 +02:00
Jeppe Bundgaard ce844137a0 Harden POS order detail E2E wait 2026-07-06 12:18:20 +02:00
Jeppe Bundgaard 7f3a8c07e2 Stabilize edge gateway installer smoke tests 2026-07-06 11:47:58 +02:00
Jeppe Bundgaard bd14d58f08 Stabilize self-serve wash smoke assertion 2026-07-06 11:03:22 +02:00
Jeppe Bundgaard b8494cd1d9 Merge remote-tracking branch 'origin/master' into codex/custom-pricing-only-departments
# Conflicts:
#	tests/unit/playwright-pr-mapping.spec.js
2026-07-06 10:36:57 +02:00
Jeppe Bundgaard 1a1c3db8e5 Fix invoicing period selfwash counts 2026-07-06 10:33:48 +02:00
Jeppe Bundgaard 49772c1334 Trigger frontend PR checks 2026-07-06 10:30:43 +02:00
Jeppe Bundgaard 02ddb99000 Add custom-only department pricing toggle 2026-07-06 10:21:06 +02:00
Jeppe BandJeppe Bundgaard cf1509e34f Fix POS step metadata persistence (#138)
* Fix POS step metadata persistence

* Preserve registration save payload formatting

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 10:20:13 +02:00
Jeppe BandJeppe Bundgaard ce2e671768 Fix department notification SMS table refresh
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 09:57:30 +02:00
Jeppe BandJeppe Bundgaard cf2add29dd Default vehicle subscriptions to false (#136)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 09:21:31 +02:00
Jeppe Bundgaard 8d2bd958ab Stabilize invoicing action wheel E2E 2026-07-05 17:54:26 +02:00
Jeppe Bundgaard fcdd8ecbfa Serve Font Awesome fixtures in E2E tests 2026-07-05 16:34:58 +02:00
Jeppe Bundgaard 58beb0d5c5 Stabilize POS step duplicate warning unit test 2026-07-05 16:12:24 +02:00
Jeppe Bundgaard 2b1e33b89a Reduce full Playwright runner pressure 2026-07-05 15:59:06 +02:00
Jeppe Bundgaard 02d77847da Stabilize frontend Playwright CI runner 2026-07-05 15:13:21 +02:00
Jeppe Bundgaard ad999b7e71 Fix Android SDK setup for mobile artifacts 2026-07-02 16:00:11 +02:00
Jeppe Bundgaard 06540f5342 Add signed Android artifact automation 2026-07-02 14:48:47 +02:00
Jeppe Bundgaard f1a34abe87 Retry E2E container dependency installs 2026-07-02 13:08:48 +02:00
Jeppe Bundgaard 18c3248113 Stabilize POS duplicate visual test date 2026-07-02 12:55:24 +02:00
Jeppe Bundgaard aacc39810b Refactor product data handling and improve error management in superuser products layout 2026-07-02 11:48:15 +02:00
Jeppe Bundgaard 24f19b8798 Show product name on superuser product page 2026-07-02 11:31:59 +02:00
Jeppe Bundgaard ff17f4f740 Refine limited backoffice navigation 2026-07-02 11:24:28 +02:00
Jeppe Bundgaard 2db4497385 Add mobile store artifact automation 2026-07-02 11:11:52 +02:00
Jeppe Bundgaard 94036a31e5 Fix limited backoffice i18n integrity 2026-07-01 17:42:11 +02:00
Jeppe Bundgaard 866baf9f70 Classify limited backoffice E2E slice 2026-07-01 17:15:08 +02:00
Jeppe Bundgaard 90cdc55574 Add limited backoffice functionality with employee management and department pricing 2026-07-01 16:37:25 +02:00
Jeppe Bundgaard 2f562e82a0 Fix subuser auth E2E regressions 2026-07-01 16:20:27 +02:00
Jeppe Bundgaard e6adc3876e Classify customer subuser E2E slice titles 2026-07-01 15:43:00 +02:00
Jeppe Bundgaard 475a7504a5 Refactor subuser permissions and enhance artifact management 2026-07-01 13:18:21 +02:00
Jeppe Bundgaard 7c04622019 Stabilize POS duplicate warning visual snapshot 2026-07-01 12:53:02 +02:00
Jeppe Bundgaard 5609b28e5a Reserve Playwright CI ports 2026-07-01 10:44:17 +02:00
Jeppe Bundgaard 2dc2a49758 Switch edge gateway frontend to HTTPS polling 2026-07-01 09:40:47 +02:00
Jeppe Bundgaard e304c66f93 Reduce Firefox full E2E worker pressure 2026-06-30 17:13:44 +02:00
Jeppe Bundgaard db0caf1bea Update self-serve relay tests for local transport 2026-06-30 17:10:05 +02:00
Jeppe Bundgaard 8ddb87ddca Refactor relay status getters and setters to use type aliases for improved clarity 2026-06-30 16:22:35 +02:00
Jeppe Bundgaard 730ddb1c96 Add outbox replay handling with configurable limits and timeouts 2026-06-30 16:15:57 +02:00
Jeppe Bundgaard 48f428a4f0 Extend self-serve hardware batch wait 2026-06-30 15:22:29 +02:00
Jeppe Bundgaard 806db63e9e Avoid duplicate self-serve gate errors 2026-06-30 13:32:50 +02:00
Jeppe Bundgaard c2a30c46c3 Handle Cloud Shelly fallback in self-serve controls 2026-06-30 12:07:43 +02:00
Jeppe Bundgaard 1ce08a7f32 Stabilize POS action menu vehicle assertion 2026-06-30 12:03:11 +02:00
Jeppe Bundgaard 4486d26e50 Update admin POS E2E expectations 2026-06-30 10:57:07 +02:00
Jeppe Bundgaard e49a3b46e6 Update superuser E2E expectations 2026-06-30 10:19:14 +02:00
Jeppe Bundgaard 7bfb341551 Enhance stopping wash functionality with local storage management and restoration logic 2026-06-30 10:16:09 +02:00
Jeppe Bundgaard 05e1dae033 Relax self-serve studio graph spread assertion 2026-06-30 09:41:25 +02:00
Jeppe Bundgaard 7f98454965 Add batch processing for lane hardware commands and status retrieval 2026-06-30 09:30:38 +02:00
Jeppe Bundgaard c6ec485303 Stabilize desktop POS E2E assertions 2026-06-30 09:30:28 +02:00
Jeppe Bundgaard d6d3e3b0e9 Stabilize POS mobile note popup test 2026-06-30 09:09:01 +02:00
Jeppe Bundgaard 91b6d6a9dc Fix night wash E2E booking label expectation 2026-06-30 08:27:55 +02:00
Jeppe Bundgaard b2f6165ae3 Update self-serve lane checks to include availability and self-serve status validation 2026-06-29 15:45:59 +02:00
Jeppe Bundgaard 4ab49794de Add null safety for selectedGrantCustomerNumber in session handling 2026-06-29 15:38:48 +02:00
Jeppe Bundgaard fe69642a64 Add machine wash configuration and controls to self-serve module 2026-06-29 15:15:26 +02:00
Jeppe Bundgaard 65829f5e23 Refactor authentication handling and permission checks in self-serve routes 2026-06-29 14:53:55 +02:00
Jeppe Bundgaard 8713bf8646 Enhance self-serve functionality by adding lane availability checks and updating response data 2026-06-29 14:25:43 +02:00
Jeppe Bundgaard 3ec0e1f992 Fix typo in invoice transfer monitor test for failed transfers 2026-06-29 12:26:30 +02:00
Jeppe Bundgaard 74151c65d0 Update smoke tests to support localization for workfeed and system status pages 2026-06-29 12:08:03 +02:00
Jeppe Bundgaard e16777f95c Improve regex patterns for invoice period flags and collection visibility checks 2026-06-29 11:52:36 +02:00
Jeppe Bundgaard 8e9e66f846 Add endpoint and functionality to test Slack internal department goal progress webhook 2026-06-29 11:32:43 +02:00
Jeppe Bundgaard 0e9db3ee20 Refactor i18n message handling to use runtime messages and improve linked message resolution 2026-06-29 11:20:34 +02:00
Jeppe Bundgaard 220013c4f0 Refactor input value handling and improve validation in ObjectsGlobal component 2026-06-29 11:08:47 +02:00
Jeppe Bundgaard d23d5da165 Refactor input value handling and improve validation in ObjectsGlobal component 2026-06-29 08:27:54 +02:00
Jeppe B d8a2789760 Merge pull request #135 from copenhagentruckwash/codex/limit-attachment-and-order-item-loads
[codex] Limit attachment GETs and dedupe order item loads
2026-06-15 18:42:19 +02:00
Jeppe Bundgaard 53315bbb56 Stabilize parallel E2E slices 2026-06-15 16:17:45 +02:00
Jeppe Bundgaard c88f5442ab Stabilize edge gateway cancellation E2E mock 2026-06-15 12:39:20 +02:00
Jeppe Bundgaard 7f347b6c90 Group full E2E slices by browser device and role 2026-06-15 12:19:15 +02:00
Jeppe Bundgaard e607ac8232 Limit attachment GETs and dedupe order item loads 2026-06-15 11:55:30 +02:00
Jeppe B 79b4441ffc Merge pull request #134 from copenhagentruckwash/fix/pos-scanner-live-preview-fps
Keep POS scanner camera preview live
2026-06-15 11:26:08 +02:00
Jeppe Bundgaard 2594cd475f Keep POS scanner camera preview live 2026-06-15 11:06:19 +02:00
Jeppe B 74b808ada0 Merge pull request #133 from copenhagentruckwash/fix/frontend-e2e-pos-camera-live-feed
Fix frontend E2E browser env and POS camera preview
2026-06-15 10:07:47 +02:00
2086 changed files with 329023 additions and 156525 deletions
+58
View File
@@ -0,0 +1,58 @@
# Default branch protection
The intended repository ruleset is stored in
[`rulesets/protect-default-branch.json`](rulesets/protect-default-branch.json).
It targets the configured default branch and requires pull requests, the strict
`Required CI` check from GitHub Actions, resolved review conversations,
squash-only merges, and linear history. Branch deletion and force pushes are
blocked. Qodana remains advisory and is not part of the required gate.
The ruleset's `RepositoryRole` actor ID `5` is GitHub's built-in Administrator
role. Its `pull_request` bypass mode permits an administrator to bypass rules
only while merging an existing pull request; it does not permit a direct push.
## Repository settings
Keep squash merge enabled and disable merge commits and rebase merge. Enable
auto-merge, the update-branch option, and automatic deletion of merged head
branches. Keep the Actions token read-only and do not allow Actions to approve
pull-request reviews.
## Activation and verification
1. Confirm a pull request and a `master` push each produce exactly one
successful `Required CI` check from GitHub Actions integration `15368`.
2. For the initial ruleset POST, override the committed JSON's `enforcement`
value to `disabled`, then compare GitHub's normalized API response with this
file.
3. PUT the exact committed JSON to the inspected ruleset to activate it.
4. Open a canary pull request and confirm that pending or failing CI, unresolved
conversations, and an out-of-date branch block merging; only squash merge is
available.
5. After merging, confirm the head branch is deleted and the post-merge full
E2E, frontend release, and mobile release guards still run.
If validation exposes a blocker, disable the ruleset rather than deleting it so
its configuration and history remain available.
## Activation record
Repository ruleset `19051697` was activated on 2026-07-16 after preparation
PR #172 established `Required CI`, remediation PR #174 passed the strict
current-base gate, and merged master run `29501928124` completed with all 43
executed jobs successful. This documentation update is the after-activation
canary for the normal protected pull-request path.
## Normal publishing flow
Create a scoped feature branch, open a pull request to `master`, wait for
`Required CI`, update the branch if `master` advanced, resolve every review
conversation, and squash-merge. For waits expected to exceed 90 seconds, use
the workspace `scripts/ci-watch.sh` helper instead of repeatedly polling GitHub.
## Break glass
For an incident, an administrator must still open a pull request. Document the
incident and why the normal gate cannot complete, then use the PR-only bypass
when merging. Monitor all post-merge workflows and open a follow-up pull request
for any validation or remediation deferred during the incident.
+21
View File
@@ -0,0 +1,21 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
time: "07:00"
timezone: Europe/Copenhagen
open-pull-requests-limit: 5
labels: [dependencies, ci]
- package-ecosystem: bundler
directory: /
schedule:
interval: weekly
day: monday
time: "07:15"
timezone: Europe/Copenhagen
open-pull-requests-limit: 3
labels: [dependencies, ios]
@@ -0,0 +1,53 @@
{
"name": "Protect default branch",
"target": "branch",
"enforcement": "active",
"bypass_actors": [
{
"actor_id": 5,
"actor_type": "RepositoryRole",
"bypass_mode": "pull_request"
}
],
"conditions": {
"ref_name": {
"include": ["~DEFAULT_BRANCH"],
"exclude": []
}
},
"rules": [
{
"type": "deletion"
},
{
"type": "non_fast_forward"
},
{
"type": "required_linear_history"
},
{
"type": "pull_request",
"parameters": {
"allowed_merge_methods": ["squash"],
"dismiss_stale_reviews_on_push": false,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_approving_review_count": 0,
"required_review_thread_resolution": true
}
},
{
"type": "required_status_checks",
"parameters": {
"do_not_enforce_on_create": false,
"required_status_checks": [
{
"context": "Required CI",
"integration_id": 15368
}
],
"strict_required_status_checks_policy": true
}
}
]
}
+88
View File
@@ -0,0 +1,88 @@
name: App Store Readiness
on:
pull_request:
push:
branches: [master]
paths:
- "fastlane/**"
- "ios/**"
- "scripts/mobile/**"
- "tests/node/app-store-connect.test.mjs"
- ".github/workflows/app-store-readiness.yml"
- "Gemfile*"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: app-store-readiness-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
validate:
name: App Store Readiness
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
- name: Setup Ruby
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
with:
ruby-version: "3.3"
- name: Resolve the pinned Fastlane dependency graph
run: bundle lock
- name: Check the committed Fastlane dependency lock
id: fastlane-lock
continue-on-error: true
run: test -z "$(git status --porcelain -- Gemfile.lock)"
- name: Preserve a generated lock for review
if: steps.fastlane-lock.outcome == 'failure'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: generated-fastlane-lock
path: Gemfile.lock
if-no-files-found: error
retention-days: 1
- name: Require a current committed Fastlane dependency lock
if: steps.fastlane-lock.outcome == 'failure'
run: |
echo 'Gemfile.lock is missing or stale. Download generated-fastlane-lock and commit it.' >&2
exit 1
- name: Install the pinned Fastlane dependency graph
run: bundle install --jobs 4 --retry 3
- name: Validate strict App Store metadata and candidate assets
run: node scripts/mobile/validate-app-store.mjs --strict
- name: Validate native mobile permissions
run: node scripts/mobile/check-permissions.mjs
- name: Test App Store Connect automation
run: node --test tests/node/app-store-connect.test.mjs
- name: Validate Fastlane configuration
run: bundle exec fastlane lanes
- name: Validate JavaScript syntax
run: |
node --check scripts/mobile/validate-app-store.mjs
node --check scripts/mobile/app-store-connect.mjs
node --check scripts/mobile/create-ios-release-manifest.mjs
node --check tests/node/app-store-connect.test.mjs
node scripts/mobile/app-store-connect.mjs self-test-jwt
-32
View File
@@ -1,32 +0,0 @@
name: Qodana Configuration Upload
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
upload-qodana-config:
runs-on: [self-hosted, Linux, X64, default]
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Run Qodana Configuration Uploader
env:
QODANA_CONFIGURATIONS_TOKEN: ${{ secrets.QODANA_CONFIGURATIONS_TOKEN }}
run: |
docker run --rm \
-v "$(pwd):/workspace" \
-w /workspace \
-e QODANA_CONFIGURATIONS_TOKEN \
jetbrains/qodana-configuration-uploader@sha256:f4786ceea616048c3401cf0b0345d2220d22a2ec7b046fd48cbbfc522e6efe30 \
--global-configs-file qodana-global-configurations.yaml \
--qodana-host https://qodana.cloud
+112
View File
@@ -0,0 +1,112 @@
name: Deploy pleno-vue to Hetzner (staging)
on:
push:
branches: [master]
workflow_dispatch:
concurrency:
group: deploy-pleno-vue
cancel-in-progress: false
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
jobs:
test-and-deploy:
name: Build + Deploy
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: '20'
cache: 'npm'
- name: Install + Build
run: |
npm ci --ignore-scripts
npm run build
- name: Setup SSH
uses: webfactory/ssh-agent@v0.10.0
with:
ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}
- name: Add host key
run: |
mkdir -p ~/.ssh
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
- name: Pre-deploy snapshot
id: pre
run: |
ssh "$DEPLOY_USER@$DEPLOY_HOST" '
set -e
cd /opt/pleno-vue
git rev-parse HEAD > /tmp/last_deploy_sha
echo "pre_sha=$(cat /tmp/last_deploy_sha)" >> $GITHUB_OUTPUT
'
- name: Deploy
id: deploy
run: |
ssh "$DEPLOY_USER@$DEPLOY_HOST" '
set -e
cd /opt/pleno-vue
git fetch origin master
git reset --hard origin/master
npm ci --ignore-scripts
npm run build
sudo systemctl reload nginx || true
sudo systemctl reload pleno-vue || true
echo "Deploy complete: $(git rev-parse --short HEAD)"
'
- name: Smoke test
id: smoke
continue-on-error: true
env:
SMOKE_BASE_URL: ${{ secrets.SMOKE_BASE_URL }}
run: |
bash scripts/smoke-test.sh "$SMOKE_BASE_URL"
- name: Sergii Review Batch smoke test (TRU-96)
id: smoke_sergii
continue-on-error: true
env:
SMOKE_BASE_URL: ${{ secrets.SMOKE_BASE_URL }}
run: |
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
git reset --hard ${{ steps.pre.outputs.pre_sha }}
npm ci --ignore-scripts
npm run build
sudo systemctl reload nginx || true
'
- name: Post Slack status
if: always()
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' && '⚠️ 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 }}
@@ -0,0 +1,190 @@
name: iOS App Store Candidate
on:
push:
tags: ["ios-v*"]
permissions:
contents: read
actions: read
concurrency:
group: ios-app-store-candidate
cancel-in-progress: false
jobs:
resolve:
name: Resolve exact tested build
runs-on: ubuntu-24.04
timeout-minutes: 15
outputs:
enabled: ${{ steps.resolve.outputs.enabled }}
source_sha: ${{ steps.resolve.outputs.source_sha }}
version: ${{ steps.resolve.outputs.version }}
build_number: ${{ steps.manifest.outputs.build_number }}
app_store_build_id: ${{ steps.manifest.outputs.app_store_build_id }}
steps:
- name: Checkout tagged source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
- name: Validate protected tag and release version
id: resolve
shell: bash
env:
AUTOMATION_ENABLED: ${{ vars.APP_STORE_AUTOMATION_ENABLED || 'false' }}
run: |
set -euo pipefail
[[ "$GITHUB_REF_NAME" =~ ^ios-v([0-9]+\.[0-9]+\.[0-9]+)$ ]] || { echo "Tag must be ios-vX.Y.Z." >&2; exit 1; }
version="${BASH_REMATCH[1]}"
source_sha="$(git rev-parse HEAD)"
manifest_version="$(node -p "JSON.parse(require('fs').readFileSync('ios/release.json')).marketingVersion")"
[[ "$version" == "$manifest_version" ]] || { echo "Tag version $version does not match ios/release.json $manifest_version." >&2; exit 1; }
git show-ref --verify --quiet refs/remotes/origin/master || { echo "origin/master was not included in the full checkout." >&2; exit 1; }
git merge-base --is-ancestor "$source_sha" origin/master || { echo "Tagged commit is not reachable from master." >&2; exit 1; }
enabled=false
[[ "$AUTOMATION_ENABLED" == true ]] && enabled=true
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
if [[ "$enabled" != true ]]; then
echo "### Candidate promotion safely disabled" >> "$GITHUB_STEP_SUMMARY"
echo 'No App Store environment or credentials were accessed. Enable only after the signed canary.' >> "$GITHUB_STEP_SUMMARY"
fi
- name: Download exact TestFlight release manifest
if: steps.resolve.outputs.enabled == 'true'
id: manifest
shell: bash
env:
GH_TOKEN: ${{ github.token }}
SOURCE_SHA: ${{ steps.resolve.outputs.source_sha }}
EXPECTED_VERSION: ${{ steps.resolve.outputs.version }}
run: |
set -euo pipefail
artifact_name="ios-release-manifest-$SOURCE_SHA"
response="$RUNNER_TEMP/ios-artifacts.json"
curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts?name=$artifact_name&per_page=100" > "$response"
artifact_id="$(jq -r --arg sha "$SOURCE_SHA" '[.artifacts[] | select(.expired == false) | select(.workflow_run.head_sha == $sha)] | sort_by(.created_at) | last | .id // empty' "$response")"
[[ "$artifact_id" =~ ^[0-9]+$ ]] || { echo "No successful TestFlight release manifest exists for $SOURCE_SHA." >&2; exit 1; }
mkdir -p output/candidate
curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id/zip" -o "$RUNNER_TEMP/manifest.zip"
unzip -q "$RUNNER_TEMP/manifest.zip" -d output/candidate
MANIFEST=output/candidate/ios-release-manifest.json node <<'NODE'
const fs = require("node:fs");
const manifest = JSON.parse(fs.readFileSync(process.env.MANIFEST, "utf8"));
const checks = {
schema: manifest.schemaVersion === 1,
repository: manifest.repository === process.env.GITHUB_REPOSITORY,
source: manifest.sourceSha === process.env.SOURCE_SHA,
version: manifest.marketingVersion === process.env.EXPECTED_VERSION,
bundle: manifest.bundleId === "io.truckwash.app",
build: /^[1-9][0-9]*$/.test(manifest.buildNumber),
appStoreBuild: typeof manifest.appStoreBuildId === "string" && manifest.appStoreBuildId.length > 0,
};
const failed = Object.entries(checks).filter(([, ok]) => !ok).map(([name]) => name);
if (failed.length) throw new Error(`Invalid iOS release manifest: ${failed.join(", ")}`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `build_number=${manifest.buildNumber}\napp_store_build_id=${manifest.appStoreBuildId}\n`);
NODE
promote:
name: Sync and verify App Store candidate
needs: resolve
if: needs.resolve.outputs.enabled == 'true'
runs-on: macos-15
timeout-minutes: 60
environment: app-store-candidate
env:
IOS_SOURCE_SHA: ${{ needs.resolve.outputs.source_sha }}
IOS_MARKETING_VERSION: ${{ needs.resolve.outputs.version }}
IOS_BUILD_NUMBER: ${{ needs.resolve.outputs.build_number }}
EXPECTED_APP_STORE_BUILD_ID: ${{ needs.resolve.outputs.app_store_build_id }}
IOS_BUNDLE_ID: ${{ vars.IOS_BUNDLE_ID || 'io.truckwash.app' }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
APP_STORE_CONNECT_API_KEY_ID: ${{ vars.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ vars.APP_STORE_CONNECT_ISSUER_ID || '' }}
APP_STORE_CONNECT_APP_ID: ${{ vars.APP_STORE_CONNECT_APP_ID }}
APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
steps:
- name: Checkout exact candidate source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.IOS_SOURCE_SHA }}
persist-credentials: false
- name: Setup Ruby and pinned Fastlane
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
with:
ruby-version: "3.3"
bundler-cache: true
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
- name: Validate complete candidate storefront
run: node scripts/mobile/validate-app-store.mjs --strict
- name: Verify public storefront URLs
shell: bash
run: |
set -euo pipefail
for file in support_url privacy_url marketing_url; do
url="$(tr -d '\r\n' < "fastlane/metadata/da-DK/$file.txt")"
curl --fail --silent --show-error --location --connect-timeout 10 --max-time 30 --output /dev/null "$url"
done
- name: Verify exact processed TestFlight build
run: node scripts/mobile/app-store-connect.mjs verify-candidate
- name: Sync metadata and screenshots without App Review submission
run: bundle exec fastlane ios prepare_candidate
- name: Configure automatic release after approval
run: node scripts/mobile/app-store-connect.mjs configure-release-policy
- name: Read back exact App Store candidate
id: readback
run: node scripts/mobile/app-store-connect.mjs verify-store-version
- name: Verify Denmark-only availability and no preorder
id: availability
run: node scripts/mobile/app-store-connect.mjs verify-availability
- name: Write candidate handoff
env:
APP_STORE_STATE: ${{ steps.readback.outputs.app_store_state }}
APP_STORE_VERSION_ID: ${{ steps.readback.outputs.app_store_version_id }}
RELEASE_TYPE: ${{ steps.readback.outputs.release_type }}
AVAILABLE_TERRITORIES: ${{ steps.availability.outputs.available_territories }}
run: |
echo "### iOS $IOS_MARKETING_VERSION candidate prepared" >> "$GITHUB_STEP_SUMMARY"
echo "- Source: \`$IOS_SOURCE_SHA\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Exact tested build: \`$IOS_BUILD_NUMBER\` (\`$EXPECTED_APP_STORE_BUILD_ID\`)" >> "$GITHUB_STEP_SUMMARY"
echo "- App Store state: \`$APP_STORE_STATE\`" >> "$GITHUB_STEP_SUMMARY"
echo "- App Store version ID: \`$APP_STORE_VERSION_ID\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Release policy: \`$RELEASE_TYPE\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Availability: \`$AVAILABLE_TERRITORIES\` only; preorder disabled" >> "$GITHUB_STEP_SUMMARY"
echo "- [Open the app in App Store Connect](https://appstoreconnect.apple.com/apps/$APP_STORE_CONNECT_APP_ID/appstore)" >> "$GITHUB_STEP_SUMMARY"
echo "- App Review submission remains manual; Apple will release automatically after approval." >> "$GITHUB_STEP_SUMMARY"
disabled:
name: Promotion disabled
needs: resolve
if: needs.resolve.outputs.enabled != 'true'
runs-on: ubuntu-24.04
steps:
- run: echo "App Store candidate promotion is disabled; no environment or credentials were accessed."
+117
View File
@@ -0,0 +1,117 @@
name: iOS Credential Health
on:
schedule:
- cron: "17 6 * * 1"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ios-credential-health
cancel-in-progress: true
jobs:
gate:
runs-on: ubuntu-24.04
outputs:
enabled: ${{ steps.gate.outputs.enabled }}
steps:
- id: gate
env:
ENABLED: ${{ vars.APP_STORE_AUTOMATION_ENABLED || 'false' }}
run: |
enabled=false
[[ "$ENABLED" == true ]] && enabled=true
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
if [[ "$enabled" != true ]]; then
echo "App Store automation is disabled; credential health did not access its environment." >> "$GITHUB_STEP_SUMMARY"
fi
validate:
needs: gate
if: needs.gate.outputs.enabled == 'true'
runs-on: macos-15
timeout-minutes: 15
environment: app-store-signing
env:
IOS_BUNDLE_ID: ${{ vars.IOS_BUNDLE_ID || 'io.truckwash.app' }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
APP_STORE_CONNECT_API_KEY_ID: ${{ vars.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ vars.APP_STORE_CONNECT_ISSUER_ID || '' }}
APP_STORE_CONNECT_APP_ID: ${{ vars.APP_STORE_CONNECT_APP_ID }}
APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
- name: Validate API key and app access
run: node scripts/mobile/app-store-connect.mjs verify-credentials
- name: Validate certificate and profile identity and expiry
shell: bash
env:
IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64 }}
IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
IOS_APP_STORE_PROFILE_BASE64: ${{ secrets.IOS_APP_STORE_PROFILE_BASE64 }}
run: |
set -euo pipefail
cert_p12="$RUNNER_TEMP/distribution.p12"
cert_pem="$RUNNER_TEMP/distribution.pem"
cert_der="$RUNNER_TEMP/distribution.der"
profile="$RUNNER_TEMP/distribution.mobileprovision"
profile_plist="$RUNNER_TEMP/distribution-profile.plist"
keychain="$RUNNER_TEMP/credential-health.keychain-db"
keychain_password="$(openssl rand -hex 24)"
node -e "const fs=require('fs');fs.writeFileSync(process.argv[1],Buffer.from(process.env.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64.replace(/\\s/g,''),'base64'))" "$cert_p12"
node -e "const fs=require('fs');fs.writeFileSync(process.argv[1],Buffer.from(process.env.IOS_APP_STORE_PROFILE_BASE64.replace(/\\s/g,''),'base64'))" "$profile"
chmod 600 "$cert_p12" "$profile"
security create-keychain -p "$keychain_password" "$keychain"
security unlock-keychain -p "$keychain_password" "$keychain"
security import "$cert_p12" -P "$IOS_DISTRIBUTION_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain"
security list-keychains -d user -s "$keychain"
security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" >/dev/null
security find-identity -v -p codesigning "$keychain" | grep -q 'Apple Distribution' || {
echo "Distribution P12 does not contain a usable private signing identity." >&2
exit 1
}
openssl pkcs12 -in "$cert_p12" -clcerts -nokeys -passin env:IOS_DISTRIBUTION_CERTIFICATE_PASSWORD -out "$cert_pem"
openssl x509 -in "$cert_pem" -noout -subject -issuer -dates
openssl x509 -in "$cert_pem" -checkend 2592000 -noout || { echo "Distribution certificate expires within 30 days." >&2; exit 1; }
openssl x509 -in "$cert_pem" -outform DER -out "$cert_der"
security cms -D -i "$profile" > "$profile_plist"
CERT_DER="$cert_der" PROFILE_PLIST="$profile_plist" python3 <<'PY'
import datetime, hashlib, os, plistlib, sys
with open(os.environ["PROFILE_PLIST"], "rb") as handle: profile = plistlib.load(handle)
with open(os.environ["CERT_DER"], "rb") as handle: cert_sha = hashlib.sha1(handle.read()).hexdigest().upper()
expiration = profile.get("ExpirationDate")
if expiration and expiration.tzinfo is None: expiration = expiration.replace(tzinfo=datetime.timezone.utc)
warning = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=30)
ent = profile.get("Entitlements", {})
checks = {
"team": os.environ["APPLE_TEAM_ID"] in profile.get("TeamIdentifier", []),
"bundle": ent.get("application-identifier") == f'{os.environ["APPLE_TEAM_ID"]}.{os.environ["IOS_BUNDLE_ID"]}',
"distribution": ent.get("get-task-allow") is False and not profile.get("ProvisionedDevices"),
"profile expiry beyond 30 days": expiration is not None and expiration > warning,
"certificate belongs to profile": cert_sha in {hashlib.sha1(value).hexdigest().upper() for value in profile.get("DeveloperCertificates", [])},
}
failed = [name for name, ok in checks.items() if not ok]
if failed:
print("Credential health failed:", *[f"- {name}" for name in failed], sep="\n", file=sys.stderr)
sys.exit(1)
print(f"Provisioning profile is healthy through {expiration.isoformat()}.")
PY
- name: Clean temporary credential files
if: always()
run: |
security delete-keychain "$RUNNER_TEMP/credential-health.keychain-db" 2>/dev/null || true
rm -f "$RUNNER_TEMP"/distribution.{p12,pem,der,mobileprovision} "$RUNNER_TEMP/distribution-profile.plist"
+624
View File
@@ -0,0 +1,624 @@
name: iOS Device Debug IPA
on:
workflow_dispatch:
inputs:
source_ref:
description: Same-repository branch, tag, or commit to build
required: true
default: master
type: string
expected_sha:
description: Full 40-character SHA that source_ref must resolve to
required: true
type: string
confirmation:
description: Type SIGN IOS DEBUG IPA
required: true
type: string
permissions:
contents: read
concurrency:
group: ios-device-debug-${{ github.run_id }}
cancel-in-progress: false
jobs:
resolve:
name: Resolve and verify source
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
source_sha: ${{ steps.resolve.outputs.source_sha }}
steps:
- name: Validate dispatch confirmation
shell: bash
env:
CONFIRMATION: ${{ inputs.confirmation }}
EXPECTED_SHA: ${{ inputs.expected_sha }}
SOURCE_REF: ${{ inputs.source_ref }}
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "$WORKFLOW_REF" != "refs/heads/master" ]]; then
echo "The signing workflow must be dispatched from the master workflow ref" >&2
exit 1
fi
if [[ "$CONFIRMATION" != "SIGN IOS DEBUG IPA" ]]; then
echo "confirmation must exactly match SIGN IOS DEBUG IPA" >&2
exit 1
fi
if [[ ! "$EXPECTED_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "expected_sha must be a full 40-character commit SHA" >&2
exit 1
fi
if [[ -z "$SOURCE_REF" || "$SOURCE_REF" =~ [[:space:]] ]]; then
echo "source_ref must be non-empty and contain no whitespace" >&2
exit 1
fi
if [[ "$SOURCE_REF" == refs/pull/* || "$SOURCE_REF" == pull/* ]]; then
echo "Pull-request refs are not eligible for device-debug signing" >&2
exit 1
fi
- name: Checkout same-repository history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Resolve immutable commit
id: resolve
shell: bash
env:
EXPECTED_SHA: ${{ inputs.expected_sha }}
SOURCE_REF: ${{ inputs.source_ref }}
run: |
set -euo pipefail
expected_sha="${EXPECTED_SHA,,}"
if [[ "$SOURCE_REF" =~ ^[0-9a-fA-F]{7,40}$ ]]; then
candidate="$SOURCE_REF"
elif [[ "$SOURCE_REF" == refs/heads/* ]]; then
candidate="refs/remotes/origin/${SOURCE_REF#refs/heads/}"
elif [[ "$SOURCE_REF" == refs/tags/* ]]; then
candidate="$SOURCE_REF"
elif git show-ref --verify --quiet "refs/remotes/origin/$SOURCE_REF"; then
candidate="refs/remotes/origin/$SOURCE_REF"
elif git show-ref --verify --quiet "refs/tags/$SOURCE_REF"; then
candidate="refs/tags/$SOURCE_REF"
else
echo "source_ref does not identify a same-repository branch, tag, or fetched commit" >&2
exit 1
fi
source_sha="$(git rev-parse --verify "${candidate}^{commit}" 2>/dev/null || true)"
source_sha="${source_sha,,}"
if [[ ! "$source_sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "source_ref could not be resolved to a commit" >&2
exit 1
fi
if [[ "$source_sha" != "$expected_sha" ]]; then
echo "source_ref resolved to a SHA different from expected_sha" >&2
exit 1
fi
reachable=false
while IFS= read -r repository_ref; do
if git merge-base --is-ancestor "$source_sha" "$repository_ref" 2>/dev/null; then
reachable=true
break
fi
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin refs/tags)
if [[ "$reachable" != true ]]; then
echo "The requested commit is not reachable from a same-repository branch or tag" >&2
exit 1
fi
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
echo "Resolved source_ref to $source_sha" >> "$GITHUB_STEP_SUMMARY"
build:
name: Build development-signed IPA
needs: resolve
runs-on: macos-26
timeout-minutes: 90
environment:
name: mobile-device-debug
env:
IOS_PROJECT_PATH: ios/App/App.xcodeproj
IOS_SCHEME: App
IOS_DEBUG_BUNDLE_ID: ${{ vars.IOS_DEBUG_BUNDLE_ID || 'io.truckwash.app.debug' }}
IOS_DEBUG_API_URL: ${{ vars.IOS_DEBUG_API_URL || 'https://api-v2.truckwash.io/master/api' }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
MOBILE_VERSION_NAME: 0.0.${{ github.run_number }}
RESOLVED_SOURCE_SHA: ${{ needs.resolve.outputs.source_sha }}
steps:
- name: Checkout resolved source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.resolve.outputs.source_sha }}
fetch-depth: 1
persist-credentials: false
- name: Verify runner and resolve build number
shell: bash
run: |
set -euo pipefail
xcode_version_output="$(xcodebuild -version)"
IFS= read -r xcode_version <<< "$xcode_version_output"
xcode_major="$(awk '{split($2, version, "."); print version[1]}' <<< "$xcode_version")"
if [[ ! "$xcode_major" =~ ^[0-9]+$ ]] || (( xcode_major < 26 )); then
echo "Xcode 26 or newer is required; found $xcode_version" >&2
exit 1
fi
build_number="$((10#$GITHUB_RUN_NUMBER * 100 + 10#$GITHUB_RUN_ATTEMPT))"
if [[ ! "$build_number" =~ ^[1-9][0-9]{0,17}$ ]]; then
echo "Derived build number is outside Apple's supported integer format" >&2
exit 1
fi
echo "MOBILE_VERSION_CODE=$build_number" >> "$GITHUB_ENV"
echo "XCODE_VERSION=$xcode_version" >> "$GITHUB_ENV"
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Verify stable API and Capacitor iOS CORS
shell: bash
run: |
set -euo pipefail
api_base="${IOS_DEBUG_API_URL%/}"
curl --fail --silent --show-error --location \
--connect-timeout 10 --max-time 20 \
--header 'Accept: application/json' \
--output /dev/null \
"$api_base/ping"
cors_headers="$RUNNER_TEMP/ios-debug-cors-headers.txt"
cors_body="$RUNNER_TEMP/ios-debug-cors-body.txt"
cors_status="$(curl --silent --show-error \
--connect-timeout 10 --max-time 20 \
--request OPTIONS \
--header 'Origin: capacitor://localhost' \
--header 'Access-Control-Request-Method: POST' \
--header 'Access-Control-Request-Headers: authorization,content-type' \
--dump-header "$cors_headers" \
--output "$cors_body" \
--write-out '%{http_code}' \
"$api_base/ping")"
if [[ ! "$cors_status" =~ ^2[0-9][0-9]$ ]]; then
echo "Stable API rejected the Capacitor iOS CORS preflight with HTTP $cors_status" >&2
sed -n '1,20p' "$cors_body" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-origin:[[:space:]]*capacitor://localhost[[:space:]]*$' "$cors_headers"; then
echo "Stable API did not allow the exact capacitor://localhost origin" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-credentials:[[:space:]]*true[[:space:]]*$' "$cors_headers"; then
echo "Stable API did not allow credentialed Capacitor requests" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-methods:.*[[:space:],]POST([[:space:],]|$)' "$cors_headers"; then
echo "Stable API did not allow POST from the Capacitor origin" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-headers:.*[[:space:],]Authorization([[:space:],]|$)' "$cors_headers"; then
echo "Stable API did not allow the Authorization header from the Capacitor origin" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-headers:.*[[:space:],]Content-Type([[:space:],]|$)' "$cors_headers"; then
echo "Stable API did not allow the Content-Type header from the Capacitor origin" >&2
exit 1
fi
- name: Build stable production web payload
env:
RELEASE_COMMIT_SHA: ${{ env.RESOLVED_SOURCE_SHA }}
VITE_API_URL: ${{ env.IOS_DEBUG_API_URL }}
VITE_RELEASE_MANAGER_CONTROL_API_URL: ${{ env.IOS_DEBUG_API_URL }}
VITE_RELEASE_PUBLIC_GATEWAY_API_URL: https://api-v2.truckwash.io
run: |
npm run build
node -e "const manifest = require('./dist/release-manifest.json'); if (manifest.commit_sha !== process.env.RESOLVED_SOURCE_SHA) { throw new Error('Web release manifest source SHA mismatch'); }"
- name: Sync and validate iOS shell
shell: bash
run: |
set -euo pipefail
npx cap sync ios
npm run mobile:permissions:check
if grep -q 'isa = PBXShellScriptBuildPhase;' "$IOS_PROJECT_PATH/project.pbxproj"; then
echo "Unexpected Xcode shell-script build phase detected" >&2
exit 1
fi
xcodebuild -resolvePackageDependencies -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME"
- name: Validate native Debug and Release settings
shell: bash
run: |
set -euo pipefail
debug_settings="$RUNNER_TEMP/ios-debug-build-settings.txt"
release_settings="$RUNNER_TEMP/ios-release-build-settings.txt"
xcodebuild -showBuildSettings \
-project "$IOS_PROJECT_PATH" \
-scheme "$IOS_SCHEME" \
-configuration Debug \
CODE_SIGNING_ALLOWED=NO > "$debug_settings"
xcodebuild -showBuildSettings \
-project "$IOS_PROJECT_PATH" \
-scheme "$IOS_SCHEME" \
-configuration Release \
CODE_SIGNING_ALLOWED=NO > "$release_settings"
grep -Eq '^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = io\.truckwash\.app\.debug$' "$debug_settings"
grep -Eq '^[[:space:]]*APP_DISPLAY_NAME = Truck Wash Debug$' "$debug_settings"
grep -Eq '^[[:space:]]*PRODUCT_NAME = TruckWashDebug$' "$debug_settings"
grep -Eq '^[[:space:]]*CAPACITOR_DEBUG = true$' "$debug_settings"
grep -Eq '^[[:space:]]*DEBUG_INFORMATION_FORMAT = dwarf-with-dsym$' "$debug_settings"
grep -Eq '^[[:space:]]*IPHONEOS_DEPLOYMENT_TARGET = 15\.0$' "$debug_settings"
grep -Eq '^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = io\.truckwash\.app$' "$release_settings"
grep -Eq '^[[:space:]]*APP_DISPLAY_NAME = Truck Wash$' "$release_settings"
grep -Eq '^[[:space:]]*PRODUCT_NAME = App$' "$release_settings"
- name: Install and validate Apple development signing assets
shell: bash
env:
IOS_DEBUG_CERTIFICATE_BASE64: ${{ secrets.IOS_DEBUG_CERTIFICATE_BASE64 }}
IOS_DEBUG_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DEBUG_CERTIFICATE_PASSWORD }}
IOS_DEBUG_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_DEBUG_PROVISION_PROFILE_BASE64 }}
IOS_DEBUG_ALLOWED_UDIDS: ${{ secrets.IOS_DEBUG_ALLOWED_UDIDS }}
run: |
set -euo pipefail
node scripts/mobile/check-ios-debug-signing-env.mjs
certificate_path="$RUNNER_TEMP/ios-debug-development.p12"
profile_path="$RUNNER_TEMP/ios-debug-development.mobileprovision"
profile_plist="$RUNNER_TEMP/ios-debug-development-profile.plist"
keychain_path="$RUNNER_TEMP/ios-debug-signing.keychain-db"
keychain_password="$(openssl rand -base64 48 | tr -d '\n')"
echo "::add-mask::$keychain_password"
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_DEBUG_CERTIFICATE_BASE64.replace(/\\s/g, ''), 'base64'))" "$certificate_path"
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_DEBUG_PROVISION_PROFILE_BASE64.replace(/\\s/g, ''), 'base64'))" "$profile_path"
chmod 600 "$certificate_path" "$profile_path"
security cms -D -i "$profile_path" > "$profile_plist"
security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$certificate_path" \
-P "$IOS_DEBUG_CERTIFICATE_PASSWORD" \
-A \
-t cert \
-f pkcs12 \
-k "$keychain_path"
security list-keychains -d user -s "$keychain_path" $(security list-keychains -d user | tr -d '"')
security set-key-partition-list \
-S apple-tool:,apple: \
-s \
-k "$keychain_password" \
"$keychain_path"
signing_identity_sha="$(security find-identity -v -p codesigning "$keychain_path" | awk '/Apple Development/ {print $2; exit}')"
if [[ ! "$signing_identity_sha" =~ ^[0-9A-Fa-f]{40}$ ]]; then
echo "The PKCS#12 file does not contain a valid Apple Development signing identity" >&2
exit 1
fi
IOS_SIGNING_IDENTITY_SHA="$signing_identity_sha" PROFILE_PLIST="$profile_plist" python3 <<'PY'
import datetime
import hashlib
import os
import plistlib
import re
import sys
with open(os.environ["PROFILE_PLIST"], "rb") as handle:
profile = plistlib.load(handle)
team_id = os.environ["APPLE_TEAM_ID"]
bundle_id = os.environ["IOS_DEBUG_BUNDLE_ID"]
entitlements = profile.get("Entitlements", {})
allowed = {line.strip() for line in os.environ["IOS_DEBUG_ALLOWED_UDIDS"].splitlines() if line.strip()}
provisioned = set(profile.get("ProvisionedDevices", []))
expiration = profile.get("ExpirationDate")
now = datetime.datetime.now(datetime.timezone.utc)
if expiration and expiration.tzinfo is None:
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
checks = {
"profile team identifier": team_id in profile.get("TeamIdentifier", []),
"application identifier": entitlements.get("application-identifier") == f"{team_id}.{bundle_id}",
"entitlement team identifier": entitlements.get("com.apple.developer.team-identifier") == team_id,
"development entitlement": entitlements.get("get-task-allow") is True,
"profile expiration": expiration is not None and expiration > now,
"registered devices": bool(allowed) and allowed <= provisioned,
"non-enterprise profile": profile.get("ProvisionsAllDevices") is not True,
"developer certificate": bool(profile.get("DeveloperCertificates")),
"profile UUID": isinstance(profile.get("UUID"), str) and re.fullmatch(r"[0-9A-Fa-f-]{36}", profile["UUID"]) is not None,
"safe profile name": isinstance(profile.get("Name"), str) and not any(char in profile["Name"] for char in "\r\n"),
}
identity_sha = os.environ["IOS_SIGNING_IDENTITY_SHA"].upper()
certificate_hashes = {hashlib.sha1(value).hexdigest().upper() for value in profile.get("DeveloperCertificates", [])}
checks["certificate belongs to profile"] = identity_sha in certificate_hashes
failures = [label for label, passed in checks.items() if not passed]
if failures:
print("Development provisioning profile validation failed:", file=sys.stderr)
for failure in failures:
print(f"- {failure}", file=sys.stderr)
sys.exit(1)
PY
profile_uuid="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$profile_plist")"
profile_name="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$profile_plist")"
profile_expiration="$(PROFILE_PLIST="$profile_plist" python3 - <<'PY'
import datetime
import os
import plistlib
with open(os.environ["PROFILE_PLIST"], "rb") as handle:
expiration = plistlib.load(handle)["ExpirationDate"]
if expiration.tzinfo is None:
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
print(expiration.astimezone(datetime.timezone.utc).isoformat().replace("+00:00", "Z"))
PY
)"
profile_install_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
profile_install_path="$profile_install_dir/$profile_uuid.mobileprovision"
mkdir -p "$profile_install_dir"
cp "$profile_path" "$profile_install_path"
echo "IOS_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
echo "IOS_PROFILE_INSTALL_PATH=$profile_install_path" >> "$GITHUB_ENV"
echo "IOS_PROFILE_NAME=$profile_name" >> "$GITHUB_ENV"
echo "IOS_PROFILE_UUID=$profile_uuid" >> "$GITHUB_ENV"
echo "IOS_PROFILE_EXPIRATION=$profile_expiration" >> "$GITHUB_ENV"
- name: Archive Debug app with Apple Development signing
shell: bash
run: |
set -euo pipefail
archive_path="$RUNNER_TEMP/TruckWashDebug.xcarchive"
xcodebuild \
-project "$IOS_PROJECT_PATH" \
-scheme "$IOS_SCHEME" \
-configuration Debug \
-destination "generic/platform=iOS" \
-archivePath "$archive_path" \
archive \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Apple Development" \
PROVISIONING_PROFILE_SPECIFIER="$IOS_PROFILE_NAME" \
PRODUCT_BUNDLE_IDENTIFIER="$IOS_DEBUG_BUNDLE_ID" \
MARKETING_VERSION="$MOBILE_VERSION_NAME" \
CURRENT_PROJECT_VERSION="$MOBILE_VERSION_CODE" \
DEBUG_INFORMATION_FORMAT="dwarf-with-dsym" \
ONLY_ACTIVE_ARCH=NO
echo "IOS_ARCHIVE_PATH=$archive_path" >> "$GITHUB_ENV"
- name: Export development IPA
shell: bash
run: |
set -euo pipefail
export_options="$RUNNER_TEMP/ios-debug-ExportOptions.plist"
export_path="$RUNNER_TEMP/ios-debug-export"
EXPORT_OPTIONS="$export_options" python3 <<'PY'
import os
import plistlib
options = {
"method": "development",
"signingStyle": "manual",
"teamID": os.environ["APPLE_TEAM_ID"],
"provisioningProfiles": {
os.environ["IOS_DEBUG_BUNDLE_ID"]: os.environ["IOS_PROFILE_NAME"],
},
"stripSwiftSymbols": True,
"manageAppVersionAndBuildNumber": False,
}
with open(os.environ["EXPORT_OPTIONS"], "wb") as handle:
plistlib.dump(options, handle)
PY
xcodebuild \
-exportArchive \
-archivePath "$IOS_ARCHIVE_PATH" \
-exportPath "$export_path" \
-exportOptionsPlist "$export_options"
shopt -s nullglob
ipa_files=("$export_path"/*.ipa)
if [[ ${#ipa_files[@]} -ne 1 ]]; then
echo "Expected exactly one exported IPA; found ${#ipa_files[@]}" >&2
exit 1
fi
echo "IOS_EXPORTED_IPA=${ipa_files[0]}" >> "$GITHUB_ENV"
- name: Validate exported IPA and embedded signature
shell: bash
env:
IOS_DEBUG_ALLOWED_UDIDS: ${{ secrets.IOS_DEBUG_ALLOWED_UDIDS }}
run: |
set -euo pipefail
inspect_dir="$RUNNER_TEMP/ios-debug-inspect"
mkdir -p "$inspect_dir"
unzip -q "$IOS_EXPORTED_IPA" -d "$inspect_dir"
shopt -s nullglob
app_bundles=("$inspect_dir"/Payload/*.app)
if [[ ${#app_bundles[@]} -ne 1 ]]; then
echo "Expected exactly one Payload app; found ${#app_bundles[@]}" >&2
exit 1
fi
app_path="${app_bundles[0]}"
app_info="$app_path/Info.plist"
embedded_profile="$RUNNER_TEMP/ios-debug-embedded-profile.plist"
signature_entitlements="$RUNNER_TEMP/ios-debug-signature-entitlements.plist"
signature_details="$RUNNER_TEMP/ios-debug-signature-details.txt"
security cms -D -i "$app_path/embedded.mobileprovision" > "$embedded_profile"
codesign --verify --deep --strict "$app_path"
codesign -d --entitlements :- "$app_path" > "$signature_entitlements"
codesign -dvv "$app_path" > /dev/null 2> "$signature_details"
grep -Fq "TeamIdentifier=$APPLE_TEAM_ID" "$signature_details"
grep -Eq '^Authority=Apple Development:' "$signature_details"
executable_name="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app_info")"
architectures="$(lipo -archs "$app_path/$executable_name")"
if [[ " $architectures " != *" arm64 "* ]]; then
echo "Exported executable does not contain arm64" >&2
exit 1
fi
APP_INFO="$app_info" PROFILE_PLIST="$embedded_profile" SIGNATURE_ENTITLEMENTS="$signature_entitlements" python3 <<'PY'
import datetime
import os
import plistlib
import sys
def load(path):
with open(path, "rb") as handle:
return plistlib.load(handle)
info = load(os.environ["APP_INFO"])
profile = load(os.environ["PROFILE_PLIST"])
signature = load(os.environ["SIGNATURE_ENTITLEMENTS"])
profile_entitlements = profile.get("Entitlements", {})
team_id = os.environ["APPLE_TEAM_ID"]
bundle_id = os.environ["IOS_DEBUG_BUNDLE_ID"]
allowed = {line.strip() for line in os.environ["IOS_DEBUG_ALLOWED_UDIDS"].splitlines() if line.strip()}
provisioned = set(profile.get("ProvisionedDevices", []))
expiration = profile.get("ExpirationDate")
now = datetime.datetime.now(datetime.timezone.utc)
if expiration and expiration.tzinfo is None:
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
checks = {
"bundle identifier": info.get("CFBundleIdentifier") == bundle_id,
"display name": info.get("CFBundleDisplayName") == "Truck Wash Debug",
"debug executable": info.get("CFBundleExecutable") == "TruckWashDebug",
"marketing version": info.get("CFBundleShortVersionString") == os.environ["MOBILE_VERSION_NAME"],
"build number": info.get("CFBundleVersion") == os.environ["MOBILE_VERSION_CODE"],
"minimum iOS": info.get("MinimumOSVersion") == "15.0",
"profile UUID": profile.get("UUID") == os.environ["IOS_PROFILE_UUID"],
"profile team": team_id in profile.get("TeamIdentifier", []),
"profile application identifier": profile_entitlements.get("application-identifier") == f"{team_id}.{bundle_id}",
"development profile": profile_entitlements.get("get-task-allow") is True,
"signature application identifier": signature.get("application-identifier") == f"{team_id}.{bundle_id}",
"signature team identifier": signature.get("com.apple.developer.team-identifier") == team_id,
"debuggable signature": signature.get("get-task-allow") is True,
"profile expiration": expiration is not None and expiration > now,
"registered devices": bool(allowed) and allowed <= provisioned,
"non-enterprise profile": profile.get("ProvisionsAllDevices") is not True,
}
failures = [label for label, passed in checks.items() if not passed]
if failures:
print("Exported development IPA validation failed:", file=sys.stderr)
for failure in failures:
print(f"- {failure}", file=sys.stderr)
sys.exit(1)
PY
- name: Assemble debug artifact
shell: bash
env:
SOURCE_REF: ${{ inputs.source_ref }}
run: |
set -euo pipefail
short_sha="${RESOLVED_SOURCE_SHA:0:12}"
artifact_name="truck-wash-debug-${MOBILE_VERSION_NAME}-${short_sha}"
artifact_dir="$RUNNER_TEMP/device-debug-artifact"
ipa_filename="$artifact_name.ipa"
dsym_filename="$artifact_name.dSYM.zip"
mkdir -p "$artifact_dir"
cp "$IOS_EXPORTED_IPA" "$artifact_dir/$ipa_filename"
shopt -s nullglob
dsym_bundles=("$IOS_ARCHIVE_PATH"/dSYMs/*.dSYM)
if [[ ${#dsym_bundles[@]} -eq 0 ]]; then
echo "The Debug archive did not contain any dSYM bundles" >&2
exit 1
fi
ditto -c -k --sequesterRsrc --keepParent "$IOS_ARCHIVE_PATH/dSYMs" "$artifact_dir/$dsym_filename"
capacitor_version="$(node -p "require('./node_modules/@capacitor/core/package.json').version")"
BUILT_AT_UTC="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
CAPACITOR_VERSION="$capacitor_version" \
DSYM_FILENAME="$dsym_filename" \
IPA_FILENAME="$ipa_filename" \
MANIFEST_PATH="$artifact_dir/manifest.json" \
python3 <<'PY'
import json
import os
manifest = {
"schema_version": 1,
"repository": os.environ["GITHUB_REPOSITORY"],
"source_ref": os.environ["SOURCE_REF"],
"source_sha": os.environ["RESOLVED_SOURCE_SHA"],
"workflow_run": int(os.environ["GITHUB_RUN_NUMBER"]),
"workflow_attempt": int(os.environ["GITHUB_RUN_ATTEMPT"]),
"built_at_utc": os.environ["BUILT_AT_UTC"],
"api_url": os.environ["IOS_DEBUG_API_URL"],
"release_manager_control_api_url": os.environ["IOS_DEBUG_API_URL"],
"bundle_id": os.environ["IOS_DEBUG_BUNDLE_ID"],
"display_name": "Truck Wash Debug",
"executable_name": "TruckWashDebug",
"version": os.environ["MOBILE_VERSION_NAME"],
"build": os.environ["MOBILE_VERSION_CODE"],
"minimum_ios": "15.0",
"capacitor_version": os.environ["CAPACITOR_VERSION"],
"xcode_version": os.environ["XCODE_VERSION"],
"signing_method": "development",
"profile_expiration_utc": os.environ["IOS_PROFILE_EXPIRATION"],
"ipa_filename": os.environ["IPA_FILENAME"],
"dsym_filename": os.environ["DSYM_FILENAME"],
}
with open(os.environ["MANIFEST_PATH"], "w", encoding="utf-8") as handle:
json.dump(manifest, handle, indent=2, sort_keys=True)
handle.write("\n")
PY
(
cd "$artifact_dir"
shasum -a 256 "$ipa_filename" "$dsym_filename" manifest.json > SHA256SUMS
)
echo "IOS_DEBUG_ARTIFACT_DIR=$artifact_dir" >> "$GITHUB_ENV"
echo "IOS_DEBUG_ARTIFACT_NAME=$artifact_name" >> "$GITHUB_ENV"
- name: Upload device-debug artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.IOS_DEBUG_ARTIFACT_NAME }}
path: ${{ env.IOS_DEBUG_ARTIFACT_DIR }}
if-no-files-found: error
retention-days: 7
- name: Clean up Apple signing assets
if: always()
shell: bash
run: |
if [[ -n "${IOS_KEYCHAIN_PATH:-}" ]]; then
security delete-keychain "$IOS_KEYCHAIN_PATH" || true
else
security delete-keychain "$RUNNER_TEMP/ios-debug-signing.keychain-db" || true
fi
if [[ -n "${IOS_PROFILE_INSTALL_PATH:-}" ]]; then
rm -f "$IOS_PROFILE_INSTALL_PATH"
fi
rm -f \
"$RUNNER_TEMP/ios-debug-development.p12" \
"$RUNNER_TEMP/ios-debug-development.mobileprovision" \
"$RUNNER_TEMP/ios-debug-development-profile.plist" \
"$RUNNER_TEMP/ios-debug-embedded-profile.plist" \
"$RUNNER_TEMP/ios-debug-signature-entitlements.plist" \
"$RUNNER_TEMP/ios-debug-signature-details.txt"
+437
View File
@@ -0,0 +1,437 @@
name: iOS Internal TestFlight
on:
workflow_run:
workflows: [Frontend Release]
types: [completed]
branches: [master]
workflow_dispatch:
inputs:
source_sha:
description: Full master commit SHA with a verified Frontend Release proof
required: true
type: string
confirmation:
description: Type UPLOAD IOS INTERNAL BUILD
required: true
type: string
permissions:
contents: read
actions: read
concurrency:
group: ios-internal-testflight
cancel-in-progress: false
jobs:
prepare:
name: Resolve verified release
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'workflow_run' &&
github.event.workflow_run.head_branch == 'master' &&
github.event.workflow_run.head_repository.full_name == github.repository)
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
source_sha: ${{ steps.resolve.outputs.source_sha }}
enabled: ${{ steps.resolve.outputs.enabled }}
current: ${{ steps.resolve.outputs.current }}
steps:
- name: Checkout repository history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Resolve immutable source and rollout gate
id: resolve
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
WORKFLOW_SOURCE_SHA: ${{ github.event.workflow_run.head_sha || '' }}
INPUT_SOURCE_SHA: ${{ inputs.source_sha || '' }}
CONFIRMATION: ${{ inputs.confirmation || '' }}
AUTOMATION_ENABLED: ${{ vars.APP_STORE_AUTOMATION_ENABLED || 'false' }}
run: |
set -euo pipefail
source_sha="$WORKFLOW_SOURCE_SHA"
if [[ "$EVENT_NAME" == workflow_dispatch ]]; then
[[ "$GITHUB_REF" == refs/heads/master ]] || { echo "Dispatch this workflow from master." >&2; exit 1; }
[[ "$CONFIRMATION" == "UPLOAD IOS INTERNAL BUILD" ]] || { echo "Invalid confirmation." >&2; exit 1; }
source_sha="${INPUT_SOURCE_SHA,,}"
fi
[[ "$source_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "A full lowercase source SHA is required." >&2; exit 1; }
git show-ref --verify --quiet refs/remotes/origin/master || { echo "origin/master was not included in the full checkout." >&2; exit 1; }
git cat-file -e "${source_sha}^{commit}"
git merge-base --is-ancestor "$source_sha" origin/master || { echo "Source is not reachable from master." >&2; exit 1; }
current=false
[[ "$(git rev-parse origin/master)" == "$source_sha" ]] && current=true
enabled=false
[[ "$AUTOMATION_ENABLED" == true ]] && enabled=true
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
echo "current=$current" >> "$GITHUB_OUTPUT"
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
if [[ "$enabled" != true ]]; then
echo "### iOS automation is safely disabled" >> "$GITHUB_STEP_SUMMARY"
echo 'Set repository variable `APP_STORE_AUTOMATION_ENABLED=true` only after the signing/API credential canary passes.' >> "$GITHUB_STEP_SUMMARY"
elif [[ "$current" != true ]]; then
echo "### Stale release skipped" >> "$GITHUB_STEP_SUMMARY"
echo "The verified SHA is no longer current master." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Require green WebKit mobile tests before App Store upload
if: steps.resolve.outputs.enabled == 'true' && steps.resolve.outputs.current == 'true'
run: node scripts/mobile/verify-store-test-gate.mjs --platform apple
env:
GH_TOKEN: ${{ github.token }}
STORE_SOURCE_SHA: ${{ steps.resolve.outputs.source_sha }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
deliver:
name: Sign, upload, process, and distribute
needs: prepare
if: needs.prepare.outputs.enabled == 'true' && needs.prepare.outputs.current == 'true'
runs-on: macos-15
timeout-minutes: 120
environment: app-store-signing
env:
DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer
IOS_SOURCE_SHA: ${{ needs.prepare.outputs.source_sha }}
IOS_PROJECT_PATH: ${{ vars.IOS_PROJECT || 'ios/App/App.xcodeproj' }}
IOS_SCHEME: ${{ vars.IOS_SCHEME || 'App' }}
IOS_BUNDLE_ID: ${{ vars.IOS_BUNDLE_ID || 'io.truckwash.app' }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
APP_STORE_CONNECT_API_KEY_ID: ${{ vars.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ vars.APP_STORE_CONNECT_ISSUER_ID || '' }}
APP_STORE_CONNECT_APP_ID: ${{ vars.APP_STORE_CONNECT_APP_ID }}
TESTFLIGHT_INTERNAL_GROUP_ID: ${{ vars.TESTFLIGHT_INTERNAL_GROUP_ID }}
APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
steps:
- name: Checkout verified source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.IOS_SOURCE_SHA }}
fetch-depth: 1
persist-credentials: false
- name: Download and verify frontend release proof
shell: bash
env:
GH_TOKEN: ${{ github.token }}
TRIGGERING_RELEASE_RUN_ID: ${{ github.event.workflow_run.id || '' }}
run: |
set -euo pipefail
artifact_name="frontend-release-proof-$IOS_SOURCE_SHA"
response="$RUNNER_TEMP/proof-artifacts.json"
curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts?name=$artifact_name&per_page=100" > "$response"
artifact_id="$(jq -r --arg run "$TRIGGERING_RELEASE_RUN_ID" '
[.artifacts[] | select(.expired == false) | select(($run == "") or ((.workflow_run.id|tostring) == $run))] |
sort_by(.created_at) | last | .id // empty' "$response")"
[[ "$artifact_id" =~ ^[0-9]+$ ]] || { echo "No verified Frontend Release proof found for $IOS_SOURCE_SHA." >&2; exit 1; }
mkdir -p output/frontend-release-proof
curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id/zip" -o "$RUNNER_TEMP/proof.zip"
unzip -q "$RUNNER_TEMP/proof.zip" -d output/frontend-release-proof
PROOF_PATH=output/frontend-release-proof/frontend-release-proof.json node <<'NODE'
const fs = require("node:fs");
const proof = JSON.parse(fs.readFileSync(process.env.PROOF_PATH, "utf8"));
const checks = {
schema: proof.schemaVersion === 2,
repository: proof.repository === process.env.GITHUB_REPOSITORY,
source: proof.sourceSha === process.env.IOS_SOURCE_SHA,
exactSource: proof.sha === process.env.IOS_SOURCE_SHA,
releaseIdentity: typeof proof.releaseId === "string" && proof.releaseId.length > 0,
archive: /^[a-f0-9]{64}$/.test(proof.archiveSha256 || ""),
activeTarget: typeof proof.activeTarget === "string" && proof.activeTarget.length > 0,
verification: proof.verificationState === "verified",
publicGate: proof.livePublicGate === "passed",
credentialedGate: ["passed", "not-configured"].includes(proof.liveCredentialedGate),
managerGate: proof.releaseManagerGate === "passed",
serverVersion: proof.serverVersionUpdated === true,
serverVersionReadBack: proof.serverVersionReadBack === "passed",
};
const failures = Object.entries(checks).filter(([, passed]) => !passed).map(([label]) => label);
if (failures.length) throw new Error(`Invalid frontend release proof: ${failures.join(", ")}`);
NODE
- name: Verify Xcode 26 and iOS 26 SDK
shell: bash
run: |
set -euo pipefail
[[ -x "$DEVELOPER_DIR/usr/bin/xcodebuild" ]] || { echo "Xcode 26.3 is not installed at $DEVELOPER_DIR." >&2; exit 1; }
xcode_version="$(xcodebuild -version | sed -n '1p')"
sdk_version="$(xcrun --sdk iphoneos --show-sdk-version)"
[[ "$xcode_version" =~ ^Xcode\ 26\. ]] || { echo "Xcode 26.x required; found $xcode_version." >&2; exit 1; }
[[ "$sdk_version" =~ ^26\. ]] || { echo "iPhoneOS 26 SDK required; found $sdk_version." >&2; exit 1; }
echo "XCODE_VERSION=$xcode_version" >> "$GITHUB_ENV"
echo "IOS_SDK_VERSION=$sdk_version" >> "$GITHUB_ENV"
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
- name: Setup Ruby and pinned Fastlane
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
with:
ruby-version: "3.3"
bundler-cache: true
- name: Install web dependencies
run: npm ci --legacy-peer-deps
- name: Validate storefront and resolve version
id: version
shell: bash
run: |
set -euo pipefail
node scripts/mobile/validate-app-store.mjs
version="$(node -p "JSON.parse(require('fs').readFileSync('ios/release.json')).marketingVersion")"
bundle="$(node -p "JSON.parse(require('fs').readFileSync('ios/release.json')).bundleId")"
[[ "$bundle" == "$IOS_BUNDLE_ID" ]]
echo "IOS_MARKETING_VERSION=$version" >> "$GITHUB_ENV"
echo "MOBILE_VERSION_NAME=$version" >> "$GITHUB_ENV"
- name: Resolve build number from App Store Connect
id: app-store
run: node scripts/mobile/app-store-connect.mjs next-build-number
- name: Export resolved build number
env:
BUILD_NUMBER: ${{ steps.app-store.outputs.build_number }}
run: |
[[ "$BUILD_NUMBER" =~ ^[1-9][0-9]*$ ]]
echo "IOS_BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_ENV"
echo "MOBILE_VERSION_CODE=$BUILD_NUMBER" >> "$GITHUB_ENV"
- name: Validate complete Apple environment
env:
IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64 }}
IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
IOS_APP_STORE_PROFILE_BASE64: ${{ secrets.IOS_APP_STORE_PROFILE_BASE64 }}
UPLOAD_IOS_TO_APP_STORE: "true"
run: node scripts/mobile/check-store-upload-env.mjs --ios
- name: Build and sync production iOS shell
run: |
npm run build
npx cap sync ios
npm run mobile:permissions:check
xcodebuild -resolvePackageDependencies -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME"
- name: Validate native release settings
shell: bash
run: |
set -euo pipefail
settings="$RUNNER_TEMP/ios-release-build-settings.txt"
xcodebuild -showBuildSettings -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME" -configuration Release CODE_SIGNING_ALLOWED=NO > "$settings"
grep -Eq "^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = ${IOS_BUNDLE_ID//./\.}$" "$settings"
grep -Eq '^[[:space:]]*APP_DISPLAY_NAME = Truck Wash$' "$settings"
grep -Eq '^[[:space:]]*IPHONEOS_DEPLOYMENT_TARGET = 15\.0$' "$settings"
if grep -q 'isa = PBXShellScriptBuildPhase;' "$IOS_PROJECT_PATH/project.pbxproj"; then
echo "Unexpected Xcode shell-script build phase detected." >&2
exit 1
fi
- name: Install and validate Apple distribution signing assets
shell: bash
env:
IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64 }}
IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
IOS_APP_STORE_PROFILE_BASE64: ${{ secrets.IOS_APP_STORE_PROFILE_BASE64 }}
run: |
set -euo pipefail
certificate_path="$RUNNER_TEMP/apple-distribution.p12"
profile_path="$RUNNER_TEMP/app-store.mobileprovision"
profile_plist="$RUNNER_TEMP/app-store-profile.plist"
keychain_path="$RUNNER_TEMP/app-store-signing.keychain-db"
keychain_password="$(openssl rand -base64 48 | tr -d '\n')"
echo "::add-mask::$keychain_password"
echo "IOS_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
node -e "const fs=require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64.replace(/\\s/g,''),'base64'))" "$certificate_path"
node -e "const fs=require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_APP_STORE_PROFILE_BASE64.replace(/\\s/g,''),'base64'))" "$profile_path"
chmod 600 "$certificate_path" "$profile_path"
security cms -D -i "$profile_path" > "$profile_plist"
security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$certificate_path" -P "$IOS_DISTRIBUTION_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain_path"
security list-keychains -d user -s "$keychain_path" $(security list-keychains -d user | tr -d '"')
security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain_path"
identity_sha="$(security find-identity -v -p codesigning "$keychain_path" | awk '/Apple Distribution/ {print $2; exit}')"
[[ "$identity_sha" =~ ^[0-9A-Fa-f]{40}$ ]] || { echo "P12 lacks an Apple Distribution identity." >&2; exit 1; }
IOS_SIGNING_IDENTITY_SHA="$identity_sha" PROFILE_PLIST="$profile_plist" python3 <<'PY'
import datetime, hashlib, os, plistlib, re, sys
with open(os.environ["PROFILE_PLIST"], "rb") as handle: profile = plistlib.load(handle)
entitlements = profile.get("Entitlements", {})
expiration = profile.get("ExpirationDate")
if expiration and expiration.tzinfo is None: expiration = expiration.replace(tzinfo=datetime.timezone.utc)
team = os.environ["APPLE_TEAM_ID"]
bundle = os.environ["IOS_BUNDLE_ID"]
hashes = {hashlib.sha1(value).hexdigest().upper() for value in profile.get("DeveloperCertificates", [])}
checks = {
"team": team in profile.get("TeamIdentifier", []),
"application identifier": entitlements.get("application-identifier") == f"{team}.{bundle}",
"team entitlement": entitlements.get("com.apple.developer.team-identifier") == team,
"distribution entitlement": entitlements.get("get-task-allow") is False,
"App Store profile has no devices": not profile.get("ProvisionedDevices"),
"non-enterprise profile": profile.get("ProvisionsAllDevices") is not True,
"expiration": expiration is not None and expiration > datetime.datetime.now(datetime.timezone.utc),
"certificate belongs to profile": os.environ["IOS_SIGNING_IDENTITY_SHA"].upper() in hashes,
"safe profile name": isinstance(profile.get("Name"), str) and not re.search(r"[\r\n]", profile["Name"]),
}
failed = [name for name, passed in checks.items() if not passed]
if failed:
print("Distribution signing validation failed:", *[f"- {name}" for name in failed], sep="\n", file=sys.stderr)
sys.exit(1)
PY
profile_uuid="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$profile_plist")"
profile_name="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$profile_plist")"
profile_install="$HOME/Library/MobileDevice/Provisioning Profiles/$profile_uuid.mobileprovision"
mkdir -p "$(dirname "$profile_install")"
echo "IOS_PROFILE_INSTALL_PATH=$profile_install" >> "$GITHUB_ENV"
cp "$profile_path" "$profile_install"
echo "IOS_PROFILE_NAME=$profile_name" >> "$GITHUB_ENV"
echo "IOS_PROFILE_UUID=$profile_uuid" >> "$GITHUB_ENV"
- name: Archive and export App Store IPA
shell: bash
run: |
set -euo pipefail
archive="$RUNNER_TEMP/TruckWash.xcarchive"
export_dir="$RUNNER_TEMP/ios-export"
export_options="$RUNNER_TEMP/ExportOptions.plist"
xcodebuild -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME" -configuration Release \
-destination 'generic/platform=iOS' -archivePath "$archive" archive \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY='Apple Distribution' \
PROVISIONING_PROFILE_SPECIFIER="$IOS_PROFILE_NAME" MARKETING_VERSION="$IOS_MARKETING_VERSION" \
CURRENT_PROJECT_VERSION="$IOS_BUILD_NUMBER" DEBUG_INFORMATION_FORMAT='dwarf-with-dsym'
EXPORT_OPTIONS="$export_options" python3 <<'PY'
import os, plistlib
options = {"method":"app-store-connect","signingStyle":"manual","teamID":os.environ["APPLE_TEAM_ID"],"provisioningProfiles":{os.environ["IOS_BUNDLE_ID"]:os.environ["IOS_PROFILE_NAME"]},"stripSwiftSymbols":True,"manageAppVersionAndBuildNumber":False}
with open(os.environ["EXPORT_OPTIONS"], "wb") as handle: plistlib.dump(options, handle)
PY
xcodebuild -exportArchive -archivePath "$archive" -exportPath "$export_dir" -exportOptionsPlist "$export_options"
shopt -s nullglob
ipa_files=("$export_dir"/*.ipa)
[[ ${#ipa_files[@]} -eq 1 ]] || { echo "Expected one IPA; found ${#ipa_files[@]}." >&2; exit 1; }
echo "IOS_ARCHIVE_PATH=$archive" >> "$GITHUB_ENV"
echo "IOS_IPA_PATH=${ipa_files[0]}" >> "$GITHUB_ENV"
- name: Inspect signed IPA
shell: bash
run: |
set -euo pipefail
inspect="$RUNNER_TEMP/ios-inspect"
unzip -q "$IOS_IPA_PATH" -d "$inspect"
shopt -s nullglob
apps=("$inspect"/Payload/*.app)
[[ ${#apps[@]} -eq 1 ]] || { echo "Expected one Payload app." >&2; exit 1; }
app="${apps[0]}"
codesign --verify --deep --strict "$app"
codesign -d --entitlements :- "$app" > "$RUNNER_TEMP/entitlements.plist"
security cms -D -i "$app/embedded.mobileprovision" > "$RUNNER_TEMP/embedded-profile.plist"
[[ -f "$app/PrivacyInfo.xcprivacy" ]]
[[ -f "$app/da.lproj/InfoPlist.strings" ]]
[[ -f "$app/en.lproj/InfoPlist.strings" ]]
APP_PATH="$app" python3 <<'PY'
import os, plistlib, sys
app = os.environ["APP_PATH"]
with open(f"{app}/Info.plist", "rb") as handle: info = plistlib.load(handle)
with open(os.path.join(os.environ["RUNNER_TEMP"], "entitlements.plist"), "rb") as handle: ent = plistlib.load(handle)
with open(os.path.join(os.environ["RUNNER_TEMP"], "embedded-profile.plist"), "rb") as handle: profile = plistlib.load(handle)
checks = {
"bundle": info.get("CFBundleIdentifier") == os.environ["IOS_BUNDLE_ID"],
"version": info.get("CFBundleShortVersionString") == os.environ["IOS_MARKETING_VERSION"],
"build": info.get("CFBundleVersion") == os.environ["IOS_BUILD_NUMBER"],
"minimum iOS": info.get("MinimumOSVersion") == "15.0",
"profile": profile.get("UUID") == os.environ["IOS_PROFILE_UUID"],
"non-debug signature": ent.get("get-task-allow") is not True,
"signature application id": ent.get("application-identifier") == f'{os.environ["APPLE_TEAM_ID"]}.{os.environ["IOS_BUNDLE_ID"]}',
}
failed = [name for name, passed in checks.items() if not passed]
if failed:
print("IPA validation failed:", *[f"- {name}" for name in failed], sep="\n", file=sys.stderr)
sys.exit(1)
PY
- name: Recheck live master before upload
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
live_master_sha="$(curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/master" | jq -r '.object.sha')"
[[ "$live_master_sha" == "$IOS_SOURCE_SHA" ]] || {
echo "master advanced while the signed build was queued; refusing upload." >&2
exit 1
}
- name: Upload and wait for App Store processing
env:
TESTFLIGHT_WHAT_TO_TEST: Automatisk intern build fra verificeret master ${{ env.IOS_SOURCE_SHA }}.
run: bundle exec fastlane ios upload_internal
- name: Assign exact processed build to Internal QA
id: distribute
env:
TESTFLIGHT_WHAT_TO_TEST: Automatisk intern build fra verificeret master ${{ env.IOS_SOURCE_SHA }}.
run: node scripts/mobile/app-store-connect.mjs wait-and-distribute
- name: Assemble signed release evidence
shell: bash
env:
APP_STORE_BUILD_ID: ${{ steps.distribute.outputs.app_store_build_id }}
run: |
set -euo pipefail
artifact="output/ios-release"
mkdir -p "$artifact"
cp "$IOS_IPA_PATH" "$artifact/TruckWash-$IOS_MARKETING_VERSION-$IOS_BUILD_NUMBER.ipa"
shopt -s nullglob
dsyms=("$IOS_ARCHIVE_PATH"/dSYMs/*.dSYM)
[[ ${#dsyms[@]} -gt 0 ]] || { echo "Release archive contains no dSYM bundles." >&2; exit 1; }
ditto -c -k --sequesterRsrc --keepParent "$IOS_ARCHIVE_PATH/dSYMs" "$artifact/TruckWash-$IOS_MARKETING_VERSION-$IOS_BUILD_NUMBER.dSYM.zip"
node scripts/mobile/create-ios-release-manifest.mjs
(cd "$artifact" && shasum -a 256 -- * > SHA256SUMS)
- name: Upload signed IPA
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: truck-wash-ios-${{ env.IOS_SOURCE_SHA }}
path: output/ios-release/*.ipa
if-no-files-found: error
retention-days: 30
- name: Upload release manifest, dSYM, and checksums
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ios-release-manifest-${{ env.IOS_SOURCE_SHA }}
path: |
output/ios-release/ios-release-manifest.json
output/ios-release/*.dSYM.zip
output/ios-release/SHA256SUMS
if-no-files-found: error
retention-days: 90
- name: Clean up Apple signing material
if: always()
shell: bash
run: |
if [[ -n "${IOS_KEYCHAIN_PATH:-}" ]]; then security delete-keychain "$IOS_KEYCHAIN_PATH" || true; fi
if [[ -n "${IOS_PROFILE_INSTALL_PATH:-}" ]]; then rm -f "$IOS_PROFILE_INSTALL_PATH"; fi
rm -f "$RUNNER_TEMP/apple-distribution.p12" "$RUNNER_TEMP/app-store.mobileprovision" "$RUNNER_TEMP/app-store-profile.plist"
disabled:
name: Automation disabled
needs: prepare
if: needs.prepare.outputs.enabled != 'true'
runs-on: ubuntu-24.04
steps:
- run: echo "App Store automation is disabled; no signing environment or secrets were accessed."
+264
View File
@@ -0,0 +1,264 @@
name: Android Store Artifacts
on:
workflow_dispatch:
inputs:
version_name:
description: Store version name, for example 1.4.0
required: false
type: string
version_code:
description: Store build number/version code
required: false
type: string
upload_android_to_play:
description: Upload the signed Android App Bundle to Google Play
required: false
type: boolean
default: true
android_track:
description: Google Play track for manual dispatches
required: false
type: choice
default: production
options:
- production
- beta
- alpha
- internal
android_release_status:
description: Google Play release status for manual dispatches
required: false
type: choice
default: inProgress
options:
- inProgress
- draft
- halted
push:
tags:
- "mobile-v*"
permissions:
contents: read
actions: read
concurrency:
group: android-store-artifacts-${{ github.ref_name || github.run_id }}
cancel-in-progress: true
jobs:
android:
name: Android AAB and Play upload
runs-on: ubuntu-24.04
environment: mobile-store-production
timeout-minutes: 60
env:
ANDROID_PACKAGE_NAME: ${{ vars.ANDROID_PACKAGE_NAME || 'io.truckwash.twa' }}
ANDROID_AAB_PATH: ${{ vars.ANDROID_AAB_PATH || 'android/app/build/outputs/bundle/release/app-release.aab' }}
ANDROID_SIGNING_IDENTITY_REF: github-environment:mobile-store-production/android-keystore
PLAY_STORE_TRACK: ${{ inputs.android_track || vars.PLAY_STORE_TRACK || 'production' }}
PLAY_STORE_RELEASE_STATUS: ${{ inputs.android_release_status || 'inProgress' }}
PLAY_STORE_USER_FRACTION: "0.01"
UPLOAD_ANDROID_TO_PLAY: ${{ github.event_name != 'workflow_dispatch' || inputs.upload_android_to_play }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Guard current master release
id: release-guard
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
EXPECTED_SHA: ${{ github.sha }}
RELEASE_BRANCH: ${{ github.ref_name }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
UPLOAD_TO_PLAY: ${{ github.event_name != 'workflow_dispatch' || inputs.upload_android_to_play }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
current=true
latest_sha="$(curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/$DEFAULT_BRANCH" | jq -r '.object.sha // empty')"
if [[ ! "$latest_sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "Could not resolve origin/$DEFAULT_BRANCH." >&2
exit 1
fi
if [[ "$latest_sha" != "$EXPECTED_SHA" && "$UPLOAD_TO_PLAY" == "true" ]]; then
current=false
echo "Skipping stale mobile upload for $EXPECTED_SHA; origin/$DEFAULT_BRANCH is $latest_sha."
elif [[ "$latest_sha" != "$EXPECTED_SHA" ]]; then
echo "Allowing artifact-only build for $EVENT_NAME on $RELEASE_BRANCH; store upload remains disabled."
else
echo "Mobile upload commit is current for $DEFAULT_BRANCH."
fi
echo "current=$current" >> "$GITHUB_OUTPUT"
echo "source_sha=$latest_sha" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
if: steps.release-guard.outputs.current == 'true'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
- name: Require green Chromium mobile tests before Play upload
if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true'
run: node scripts/mobile/verify-store-test-gate.mjs --platform android
env:
GH_TOKEN: ${{ github.token }}
STORE_SOURCE_SHA: ${{ steps.release-guard.outputs.source_sha }}
TEST_WORKFLOW_RUN_ID: ""
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
- name: Setup Java
if: steps.release-guard.outputs.current == 'true'
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: temurin
java-version: 21
- name: Setup Android SDK
if: steps.release-guard.outputs.current == 'true'
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
- name: Install Android SDK packages
if: steps.release-guard.outputs.current == 'true'
shell: bash
run: |
set -euo pipefail
yes | sdkmanager --licenses >/dev/null || true
sdkmanager "platforms;android-36" "build-tools;36.0.0"
- name: Resolve mobile version
if: steps.release-guard.outputs.current == 'true'
shell: bash
env:
INPUT_VERSION_NAME: ${{ inputs.version_name || '' }}
INPUT_VERSION_CODE: ${{ inputs.version_code || '' }}
run: |
set -euo pipefail
version_name="$INPUT_VERSION_NAME"
if [[ -z "$version_name" && "$GITHUB_REF_NAME" == mobile-v* ]]; then
version_name="${GITHUB_REF_NAME#mobile-v}"
fi
if [[ -z "$version_name" ]]; then
version_name="0.0.${GITHUB_RUN_NUMBER}"
fi
version_code="${INPUT_VERSION_CODE:-$GITHUB_RUN_NUMBER}"
echo "MOBILE_VERSION_NAME=$version_name" >> "$GITHUB_ENV"
echo "MOBILE_VERSION_CODE=$version_code" >> "$GITHUB_ENV"
- name: Check Android store environment
if: steps.release-guard.outputs.current == 'true'
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64 }}
run: node scripts/mobile/check-store-upload-env.mjs --android
- name: Install dependencies
if: steps.release-guard.outputs.current == 'true'
run: npm ci --legacy-peer-deps
- name: Decode Android signing key
if: steps.release-guard.outputs.current == 'true'
shell: bash
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
set -euo pipefail
keystore_path="$RUNNER_TEMP/android-release.keystore"
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.ANDROID_KEYSTORE_BASE64, 'base64'))" "$keystore_path"
echo "ANDROID_KEYSTORE_FILE=$keystore_path" >> "$GITHUB_ENV"
echo "ANDROID_KEYSTORE_PASSWORD=$ANDROID_KEYSTORE_PASSWORD" >> "$GITHUB_ENV"
echo "ANDROID_KEY_ALIAS=$ANDROID_KEY_ALIAS" >> "$GITHUB_ENV"
echo "ANDROID_KEY_PASSWORD=$ANDROID_KEY_PASSWORD" >> "$GITHUB_ENV"
- name: Build and sync Android shell
if: steps.release-guard.outputs.current == 'true'
run: |
npm run mobile:android:sync
npm run mobile:permissions:check
npm run mobile:android:signing:check
- name: Build signed Android App Bundle
if: steps.release-guard.outputs.current == 'true'
working-directory: android
run: ./gradlew --no-daemon bundleRelease
- name: Verify Android App Bundle signature
if: steps.release-guard.outputs.current == 'true'
run: jarsigner -verify -certs -verbose "$ANDROID_AAB_PATH" >/dev/null
- name: Record immutable Android artifact proof
if: steps.release-guard.outputs.current == 'true'
id: artifact-proof
shell: bash
run: |
set -euo pipefail
artifact_sha256="$(sha256sum "$ANDROID_AAB_PATH" | awk '{print $1}')"
[[ "$artifact_sha256" =~ ^[0-9a-f]{64}$ ]]
echo "ANDROID_AAB_SHA256=$artifact_sha256" >> "$GITHUB_ENV"
echo "sha256=$artifact_sha256" >> "$GITHUB_OUTPUT"
echo "Android AAB SHA-256: \`$artifact_sha256\`" >> "$GITHUB_STEP_SUMMARY"
- name: Upload Android artifact
if: steps.release-guard.outputs.current == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: truck-wash-android-${{ env.MOBILE_VERSION_NAME }}-${{ github.sha }}
path: ${{ env.ANDROID_AAB_PATH }}
if-no-files-found: error
retention-days: 14
- name: Recheck live master before Play upload
if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true'
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
EXPECTED_SHA: ${{ steps.release-guard.outputs.source_sha }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
live_master_sha="$(curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/$DEFAULT_BRANCH" | jq -r '.object.sha // empty')"
[[ "$live_master_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "Could not resolve origin/$DEFAULT_BRANCH." >&2; exit 1; }
[[ "$live_master_sha" == "$EXPECTED_SHA" ]] || {
echo "$DEFAULT_BRANCH advanced while the Android bundle was building; refusing Play upload." >&2
exit 1
}
- name: Upload Android App Bundle to Google Play
if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true'
id: play-upload
env:
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64 }}
run: npm run mobile:android:play-upload
- name: Record Google Play submission proof
if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true'
env:
ARTIFACT_SHA256: ${{ steps.artifact-proof.outputs.sha256 }}
PLAY_EDIT_ID: ${{ steps.play-upload.outputs.play_edit_id }}
PLAY_VERSION_CODE: ${{ steps.play-upload.outputs.version_code }}
run: |
set -euo pipefail
test -n "$ARTIFACT_SHA256"
test -n "$PLAY_EDIT_ID"
test -n "$PLAY_VERSION_CODE"
printf 'Google Play submission proof: platform=android applicationId=%s version=%s buildNumber=%s artifactSha256=%s signingIdentityRef=%s storeSubmissionId=%s status=%s fraction=%s\n' \
"$ANDROID_PACKAGE_NAME" "$MOBILE_VERSION_NAME" "$PLAY_VERSION_CODE" \
"$ARTIFACT_SHA256" "$ANDROID_SIGNING_IDENTITY_REF" "$PLAY_EDIT_ID" \
"$PLAY_STORE_RELEASE_STATUS" "$PLAY_STORE_USER_FRACTION" >> "$GITHUB_STEP_SUMMARY"
+283
View File
@@ -0,0 +1,283 @@
name: Frontend Release Recovery
on:
workflow_dispatch:
inputs:
action:
description: Verify the active release or roll back before verification
required: true
type: choice
options:
- reverify
- rollback
source_sha:
description: Exact 40-character commit SHA expected after recovery
required: true
type: string
rollback_target:
description: Immutable releases/.../dist target; required for rollback
required: false
type: string
permissions:
contents: read
actions: read
concurrency:
group: frontend-production
cancel-in-progress: false
jobs:
recover:
name: Protected production recovery
runs-on: ubuntu-latest
environment: frontend-production
timeout-minutes: 35
env:
PLAYWRIGHT_BASE_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
steps:
- name: Validate exact recovery target
shell: bash
env:
RECOVERY_ACTION: ${{ inputs.action }}
RECOVERY_SHA: ${{ inputs.source_sha }}
RECOVERY_TARGET: ${{ inputs.rollback_target }}
run: |
set -euo pipefail
[[ "$RECOVERY_SHA" =~ ^[a-f0-9]{40}$ ]]
if [[ "$RECOVERY_ACTION" == "rollback" ]]; then
[[ "$RECOVERY_TARGET" =~ ^releases/[A-Za-z0-9._-]+/dist$ ]]
else
[[ -z "$RECOVERY_TARGET" ]]
fi
- name: Checkout exact recovery source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ inputs.source_sha }}
- name: Authorize source from successful release proof
id: authorize
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RECOVERY_ACTION: ${{ inputs.action }}
RECOVERY_SHA: ${{ inputs.source_sha }}
RECOVERY_TARGET: ${{ inputs.rollback_target }}
run: |
set -euo pipefail
runs="$RUNNER_TEMP/recovery-runs.json"
artifacts="$RUNNER_TEMP/recovery-artifacts.json"
curl --fail --silent --show-error \
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/workflows/release.yml/runs?head_sha=$RECOVERY_SHA&status=success&per_page=20" \
> "$runs"
release_run_id="$(jq -r '[.workflow_runs[] | select(.event == "workflow_run")] | first | .id // empty' "$runs")"
[[ "$release_run_id" =~ ^[0-9]+$ ]]
artifact_name="frontend-release-proof-$RECOVERY_SHA"
curl --fail --silent --show-error \
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/runs/$release_run_id/artifacts?name=$artifact_name&per_page=20" \
> "$artifacts"
artifact_id="$(jq -r '[.artifacts[] | select(.expired == false)] | first | .id // empty' "$artifacts")"
[[ "$artifact_id" =~ ^[0-9]+$ ]]
mkdir -p "$RUNNER_TEMP/recovery-proof"
curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id/zip" \
-o "$RUNNER_TEMP/recovery-proof.zip"
unzip -q "$RUNNER_TEMP/recovery-proof.zip" -d "$RUNNER_TEMP/recovery-proof"
PROOF_PATH="$RUNNER_TEMP/recovery-proof/frontend-release-proof.json" \
RELEASE_RUN_ID="$release_run_id" node <<'NODE'
const { appendFileSync, readFileSync } = require("node:fs");
const proof = JSON.parse(readFileSync(process.env.PROOF_PATH, "utf8"));
const sha = process.env.RECOVERY_SHA;
const target = process.env.RECOVERY_TARGET;
const expectedPrefix = `releases/${sha}-`;
const valid = proof.schemaVersion === 2
&& proof.repository === process.env.GITHUB_REPOSITORY
&& proof.sha === sha
&& proof.sourceSha === sha
&& proof.frontendReleaseRunId === process.env.RELEASE_RUN_ID
&& proof.verificationState === "verified"
&& proof.livePublicGate === "passed"
&& ["passed", "not-configured"].includes(proof.liveCredentialedGate)
&& proof.releaseManagerGate === "passed"
&& proof.serverVersionUpdated === true
&& proof.serverVersionReadBack === "passed"
&& /^[1-9][0-9]*-[1-9][0-9]*$/.test(String(proof.buildId || ""))
&& typeof proof.activeTarget === "string"
&& proof.activeTarget.startsWith(expectedPrefix)
&& proof.activeTarget.endsWith("/dist");
if (!valid) throw new Error("Recovery source does not have valid exact-release proof.");
if (process.env.RECOVERY_ACTION === "rollback" && target !== proof.activeTarget) {
throw new Error("Rollback target does not match the verified release proof.");
}
appendFileSync(process.env.GITHUB_OUTPUT, `verified_target=${proof.activeTarget}\n`);
appendFileSync(process.env.GITHUB_OUTPUT, `build_id=${proof.buildId}\n`);
NODE
- name: Capture current immutable target
id: current
shell: bash
env:
FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
run: |
node --input-type=module <<'NODE'
import { appendFileSync } from "node:fs";
const response = await fetch(new URL(`release-manifest.json?recovery=${Date.now()}`, process.env.FRONTEND_URL), {
headers: { "Cache-Control": "no-cache", Pragma: "no-cache" },
});
if (!response.ok) throw new Error(`Active manifest returned HTTP ${response.status}.`);
const manifest = await response.json();
const sha = String(manifest.commit_sha || "").toLowerCase();
const build = String(manifest.build_id || "");
if (!/^[a-f0-9]{40}$/.test(sha) || !/^[A-Za-z0-9._-]{1,180}$/.test(build)) {
throw new Error("Active manifest has invalid release identity.");
}
if (!/^[1-9][0-9]*-[1-9][0-9]*$/.test(build)) {
throw new Error("Active manifest build id is not a release run identity.");
}
appendFileSync(process.env.GITHUB_OUTPUT, `previous_sha=${sha}\nprevious_build_id=${build}\nprevious_target=releases/${sha}-${build}/dist\n`);
NODE
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Install secure FTP client without system changes
run: |
if command -v lftp >/dev/null 2>&1; then
exit 0
fi
package_root="$RUNNER_TEMP/lftp-package"
mkdir -p "$package_root"
(
cd "$package_root"
apt-get download lftp
dpkg-deb --extract ./lftp_*.deb root
)
echo "$package_root/root/usr/bin" >> "$GITHUB_PATH"
- name: Install Playwright Chromium
run: node scripts/install-playwright-browsers.mjs chromium
- name: Roll back atomically
if: inputs.action == 'rollback'
id: rollback
run: node scripts/release/deploy-cpanel.mjs --rollback
env:
NODE_OPTIONS: --use-system-ca
RELEASE_ROLLBACK_TARGET: ${{ inputs.rollback_target }}
PRODUCTION_FTP_HOST: ${{ secrets.PRODUCTION_FTP_HOST }}
PRODUCTION_FTP_USER: ${{ secrets.PRODUCTION_FTP_USER }}
PRODUCTION_FTP_PASSWORD: ${{ secrets.PRODUCTION_FTP_PASSWORD }}
PRODUCTION_FTP_PATH: ${{ secrets.PRODUCTION_FTP_PATH }}
PRODUCTION_ACTIVATION_KEY: ${{ secrets.PRODUCTION_ACTIVATION_KEY }}
PRODUCTION_FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
- name: Verify active manifest matches authorized release
shell: bash
env:
EXPECTED_SHA: ${{ inputs.source_sha }}
EXPECTED_TARGET: ${{ steps.authorize.outputs.verified_target }}
FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
run: |
node --input-type=module <<'NODE'
const deadline = Date.now() + 300_000;
let actual = "";
while (Date.now() < deadline) {
const response = await fetch(new URL(`release-manifest.json?recovery=${Date.now()}`, process.env.FRONTEND_URL), {
headers: { "Cache-Control": "no-cache", Pragma: "no-cache" },
});
if (response.ok) {
const manifest = await response.json();
const manifestSha = String(manifest.commit_sha || "").toLowerCase();
actual = `releases/${manifestSha}-${String(manifest.build_id || "")}/dist`;
if (manifestSha === process.env.EXPECTED_SHA && actual === process.env.EXPECTED_TARGET) process.exit(0);
}
await new Promise((resolve) => setTimeout(resolve, 5_000));
}
throw new Error(`Active release identity did not converge to the authorized target; observed ${actual || "unavailable"}.`);
NODE
- name: Public live verification
run: npm run test:e2e:live:public
env:
NODE_OPTIONS: --use-system-ca
- name: Credentialed live verification
run: npm run test:e2e:live:roles
env:
NODE_OPTIONS: --use-system-ca
PLAYWRIGHT_REQUIRE_LIVE_CREDENTIALS: "true"
PLAYWRIGHT_USER_CUSTOMER_NUMBER: ${{ secrets.PLAYWRIGHT_USER_CUSTOMER_NUMBER }}
PLAYWRIGHT_USER_PASSWORD: ${{ secrets.PLAYWRIGHT_USER_PASSWORD }}
PLAYWRIGHT_USER_OTP_SECRET: ${{ secrets.PLAYWRIGHT_USER_OTP_SECRET }}
PLAYWRIGHT_OPERATOR_USER_ID: ${{ secrets.PLAYWRIGHT_OPERATOR_USER_ID }}
PLAYWRIGHT_OPERATOR_PASSWORD: ${{ secrets.PLAYWRIGHT_OPERATOR_PASSWORD }}
PLAYWRIGHT_DEPARTMENT_ID: ${{ secrets.PLAYWRIGHT_DEPARTMENT_ID }}
- name: Record verified server version
run: npm run release:update-server-version
env:
SERVER_UPDATE_TOKEN: ${{ secrets.SERVER_UPDATE_TOKEN }}
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
RELEASE_VERSION: ${{ inputs.source_sha }}
RELEASE_BUILD_ID: ${{ steps.authorize.outputs.build_id }}
RELEASE_VERSION_UPDATE_REQUIRED: "true"
- name: Publish recovery audit
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: frontend-release-recovery-${{ inputs.source_sha }}-${{ github.run_id }}
path: |
test-results
playwright-report
if-no-files-found: ignore
retention-days: 30
- name: Restore pre-recovery target after downstream failure
if: >-
failure() && inputs.action == 'rollback'
shell: bash
run: |
node scripts/release/deploy-cpanel.mjs --rollback
node --input-type=module <<'NODE'
const deadline = Date.now() + 300_000;
while (Date.now() < deadline) {
const response = await fetch(new URL(`release-manifest.json?restore=${Date.now()}`, process.env.PRODUCTION_FRONTEND_URL), {
headers: { "Cache-Control": "no-cache", Pragma: "no-cache" },
});
if (response.ok) {
const manifest = await response.json();
const sha = String(manifest.commit_sha || "").toLowerCase();
const target = `releases/${sha}-${String(manifest.build_id || "")}/dist`;
if (sha === process.env.RELEASE_VERSION && target === process.env.RELEASE_ROLLBACK_TARGET) process.exit(0);
}
await new Promise((resolve) => setTimeout(resolve, 5_000));
}
throw new Error("Failed to restore and verify the pre-recovery target.");
NODE
npm run release:update-server-version
env:
NODE_OPTIONS: --use-system-ca
RELEASE_ROLLBACK_TARGET: ${{ steps.current.outputs.previous_target }}
RELEASE_VERSION: ${{ steps.current.outputs.previous_sha }}
RELEASE_BUILD_ID: ${{ steps.current.outputs.previous_build_id }}
RELEASE_VERSION_UPDATE_REQUIRED: "true"
PRODUCTION_FTP_HOST: ${{ secrets.PRODUCTION_FTP_HOST }}
PRODUCTION_FTP_USER: ${{ secrets.PRODUCTION_FTP_USER }}
PRODUCTION_FTP_PASSWORD: ${{ secrets.PRODUCTION_FTP_PASSWORD }}
PRODUCTION_FTP_PATH: ${{ secrets.PRODUCTION_FTP_PATH }}
PRODUCTION_ACTIVATION_KEY: ${{ secrets.PRODUCTION_ACTIVATION_KEY }}
PRODUCTION_FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
SERVER_UPDATE_TOKEN: ${{ secrets.SERVER_UPDATE_TOKEN }}
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
+347 -85
View File
@@ -14,53 +14,62 @@ permissions:
actions: read
concurrency:
group: frontend-release-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
group: frontend-production
cancel-in-progress: false
jobs:
build-upload-and-verify:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push'
runs-on: [self-hosted, Linux, X64, default]
build-release:
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'master' &&
github.event.workflow_run.head_repository.full_name == github.repository
runs-on: ubuntu-24.04
env:
RELEASE_BASE_URL: https://api-v2.truckwash.io/master/frontend
PLAYWRIGHT_BASE_URL: https://dev.truckwash.io
PLAYWRIGHT_RELEASE_STATIC_BASE_URL: https://api-v2.truckwash.io/master/frontend
PLAYWRIGHT_RELEASE_API_BASE_URL: https://api-v2.truckwash.io
PLAYWRIGHT_RELEASE_API_PING_PATHS: /master/api/ping
RELEASE_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
RELEASE_EXPECTED_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
RELEASE_COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
RELEASE_EXPECTED_COMMIT: ${{ github.event.workflow_run.head_sha }}
RELEASE_WAIT_INITIAL_SECONDS: 45
RELEASE_WAIT_TIMEOUT_SECONDS: 600
RELEASE_POLL_INTERVAL_SECONDS: 10
RELEASE_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
outputs:
current: ${{ steps.branch-head.outputs.current }}
build_id: ${{ steps.package.outputs.build_id }}
artifact_name: ${{ steps.package-names.outputs.artifact_name }}
archive_name: ${{ steps.package.outputs.archive_name }}
checksum_name: ${{ steps.package-names.outputs.checksum_name }}
inventory_name: ${{ steps.package-names.outputs.inventory_name }}
release_id: ${{ steps.package.outputs.release_id }}
archive_sha256: ${{ steps.package.outputs.archive_sha256 }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha }}
- name: Check release commit is current
id: branch-head
run: |
latest_sha="$(git ls-remote origin "refs/heads/$RELEASE_BRANCH" | awk '{print $1}')"
if [[ -z "$latest_sha" ]]; then
echo "Could not resolve origin/$RELEASE_BRANCH." >&2
exit 1
fi
if [[ "$latest_sha" != "$RELEASE_EXPECTED_COMMIT" ]]; then
echo "current=false" >> "$GITHUB_OUTPUT"
echo "Skipping stale release for $RELEASE_EXPECTED_COMMIT; origin/$RELEASE_BRANCH is $latest_sha."
exit 0
fi
echo "current=true" >> "$GITHUB_OUTPUT"
echo "Release commit is current for $RELEASE_BRANCH."
env:
RELEASE_BRANCH: ${{ github.event.workflow_run.head_branch }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ github.token }}
script: |
const { data: branch } = await github.rest.repos.getBranch({
owner: context.repo.owner,
repo: context.repo.repo,
branch: "master",
});
const expected = process.env.RELEASE_EXPECTED_COMMIT;
const current = branch.commit.sha === expected;
core.setOutput("current", String(current));
core.info(
current
? `Release commit ${expected} is current for master.`
: `Skipping stale release for ${expected}; origin/master is ${branch.commit.sha}.`,
);
- name: Checkout tested commit
if: steps.branch-head.outputs.current == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ env.RELEASE_COMMIT_SHA }}
- name: Setup Node.js
if: steps.branch-head.outputs.current == 'true'
uses: actions/setup-node@v5
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
@@ -79,16 +88,22 @@ jobs:
npm run text:check-encoding
npm run i18n:v2:source-check
- name: Unit tests
if: steps.branch-head.outputs.current == 'true'
run: npm run test:unit
env:
VITEST_BATCH_SIZE: 5
- name: Build release artifact
if: steps.branch-head.outputs.current == 'true'
run: npm run build
- name: Record pre-gate dist inventory
if: steps.branch-head.outputs.current == 'true'
run: |
inventory="$RUNNER_TEMP/dist-before-production-gate.txt"
while IFS= read -r -d '' file; do
relative_path="${file#dist/}"
printf '%s\t%s\t%s\n' \
"$(sha256sum "$file" | awk '{print $1}')" \
"$(stat --format='%s' "$file")" \
"$relative_path"
done < <(find dist -type f -print0 | LC_ALL=C sort -z) > "$inventory"
- name: Install Playwright Chromium
if: steps.branch-head.outputs.current == 'true'
run: node scripts/install-playwright-browsers.mjs chromium
@@ -96,60 +111,209 @@ jobs:
- name: Production Playwright gate
if: steps.branch-head.outputs.current == 'true'
run: npm run test:e2e:prod
env:
PLAYWRIGHT_PROD_PREBUILT: "1"
PLAYWRIGHT_PROD_WEBKIT: "0"
- name: Upload dist artifact
if: steps.branch-head.outputs.current == 'true'
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: frontend-dist-${{ env.RELEASE_BUILD_ID }}
path: dist
retention-days: 3
- name: Request Release Manager auto sync
- name: Confirm production gate did not mutate dist
if: steps.branch-head.outputs.current == 'true'
run: |
test -n "$RELEASE_MANAGER_GATE_TOKEN" || (echo "RELEASE_MANAGER_GATE_TOKEN is required" >&2; exit 1)
response_file="$(mktemp)"
status_code="$(curl --show-error --silent \
--output "$response_file" \
--write-out "%{http_code}" \
-X POST "$RELEASE_MANAGER_GATE_URL" \
-H "Authorization: Bearer $RELEASE_MANAGER_GATE_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"$RELEASE_BRANCH\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$RELEASE_EXPECTED_BUILD_ID\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":true,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"api_gateway\"]}")"
if [[ "$status_code" =~ ^2 ]]; then
cat "$response_file"
elif [[ "$status_code" == "504" ]]; then
echo "Release Manager auto sync request reached the gateway timeout; continuing to artifact wait."
else
cat "$response_file" >&2
echo "Release Manager auto sync request failed with HTTP $status_code." >&2
exit 1
fi
env:
RELEASE_MANAGER_GATE_URL: ${{ secrets.RELEASE_MANAGER_GATE_URL || 'https://api.truckwash.io/release/gate/test-runs' }}
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
RELEASE_REPOSITORY: ${{ github.repository }}
RELEASE_BRANCH: ${{ github.event.workflow_run.head_branch }}
RELEASE_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
inventory="$RUNNER_TEMP/dist-after-production-gate.txt"
while IFS= read -r -d '' file; do
relative_path="${file#dist/}"
printf '%s\t%s\t%s\n' \
"$(sha256sum "$file" | awk '{print $1}')" \
"$(stat --format='%s' "$file")" \
"$relative_path"
done < <(find dist -type f -print0 | LC_ALL=C sort -z) > "$inventory"
cmp "$RUNNER_TEMP/dist-before-production-gate.txt" "$inventory"
- name: Wait for Coolify release artifact
- name: Package and validate release
if: steps.branch-head.outputs.current == 'true'
run: npm run release:verify-upload
id: package
run: node scripts/release/package-dist.mjs
env:
RELEASE_OUTPUT_DIR: release-artifacts
- name: Resolve package metadata
if: steps.branch-head.outputs.current == 'true'
id: package-names
env:
BUILD_ID: ${{ steps.package.outputs.build_id }}
CHECKSUM_PATH: ${{ steps.package.outputs.checksum_path }}
INVENTORY_PATH: ${{ steps.package.outputs.inventory_path }}
run: |
echo "artifact_name=frontend-release-$BUILD_ID" >> "$GITHUB_OUTPUT"
echo "checksum_name=$(basename -- "$CHECKSUM_PATH")" >> "$GITHUB_OUTPUT"
echo "inventory_name=$(basename -- "$INVENTORY_PATH")" >> "$GITHUB_OUTPUT"
- name: Upload release package
if: steps.branch-head.outputs.current == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ steps.package-names.outputs.artifact_name }}
path: |
${{ steps.package.outputs.archive_path }}
${{ steps.package.outputs.checksum_path }}
${{ steps.package.outputs.inventory_path }}
if-no-files-found: error
retention-days: 14
deploy-frontend-production:
needs: build-release
if: needs.build-release.outputs.current == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 90
environment:
name: frontend-production
url: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
env:
RELEASE_BASE_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
PLAYWRIGHT_BASE_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
PLAYWRIGHT_RELEASE_STATIC_BASE_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
PLAYWRIGHT_RELEASE_API_BASE_URL: https://api-v2.truckwash.io
PLAYWRIGHT_RELEASE_API_PING_PATHS: /master/api/ping
RELEASE_COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
RELEASE_EXPECTED_COMMIT: ${{ github.event.workflow_run.head_sha }}
RELEASE_BUILD_ID: ${{ needs.build-release.outputs.build_id }}
RELEASE_EXPECTED_BUILD_ID: ${{ needs.build-release.outputs.build_id }}
RELEASE_ID: ${{ needs.build-release.outputs.release_id }}
RELEASE_STRICT_BUILD_ID: "true"
RELEASE_REQUIRE_CACHE_HEADERS: "true"
RELEASE_WAIT_INITIAL_SECONDS: 0
RELEASE_WAIT_TIMEOUT_SECONDS: 300
RELEASE_POLL_INTERVAL_SECONDS: 5
steps:
- name: Checkout tested commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ env.RELEASE_COMMIT_SHA }}
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Install secure FTP client without system changes
run: |
if command -v lftp >/dev/null 2>&1; then
exit 0
fi
package_root="$RUNNER_TEMP/lftp-package"
mkdir -p "$package_root"
(
cd "$package_root"
apt-get download lftp
dpkg-deb --extract ./lftp_*.deb root
)
echo "$package_root/root/usr/bin" >> "$GITHUB_PATH"
- name: Install Playwright Chromium
run: node scripts/install-playwright-browsers.mjs chromium
- name: Download validated release package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ needs.build-release.outputs.artifact_name }}
path: release-artifacts
- name: Resolve downloaded release package
env:
ARCHIVE_NAME: ${{ needs.build-release.outputs.archive_name }}
CHECKSUM_NAME: ${{ needs.build-release.outputs.checksum_name }}
INVENTORY_NAME: ${{ needs.build-release.outputs.inventory_name }}
run: |
[[ -n "$ARCHIVE_NAME" && "$ARCHIVE_NAME" == "$(basename -- "$ARCHIVE_NAME")" ]]
[[ -n "$CHECKSUM_NAME" && "$CHECKSUM_NAME" == "$(basename -- "$CHECKSUM_NAME")" ]]
[[ -n "$INVENTORY_NAME" && "$INVENTORY_NAME" == "$(basename -- "$INVENTORY_NAME")" ]]
archive_path="$GITHUB_WORKSPACE/release-artifacts/$ARCHIVE_NAME"
checksum_path="$GITHUB_WORKSPACE/release-artifacts/$CHECKSUM_NAME"
inventory_path="$GITHUB_WORKSPACE/release-artifacts/$INVENTORY_NAME"
[[ -f "$archive_path" && -f "$checksum_path" && -f "$inventory_path" ]]
echo "RELEASE_ARCHIVE_PATH=$archive_path" >> "$GITHUB_ENV"
echo "RELEASE_ARCHIVE_SHA256_PATH=$checksum_path" >> "$GITHUB_ENV"
echo "RELEASE_INVENTORY_PATH=$inventory_path" >> "$GITHUB_ENV"
- name: Check release commit is still current
id: branch-head
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ github.token }}
script: |
const { data: branch } = await github.rest.repos.getBranch({
owner: context.repo.owner,
repo: context.repo.repo,
branch: "master",
});
const expected = process.env.RELEASE_EXPECTED_COMMIT;
const current = branch.commit.sha === expected;
core.setOutput("current", String(current));
core.info(
current
? `Release commit ${expected} is current immediately before activation.`
: `Skipping stale release for ${expected}; origin/master is ${branch.commit.sha}.`,
);
- name: Deploy atomically and verify cPanel release
if: steps.branch-head.outputs.current == 'true'
id: deploy
timeout-minutes: 15
run: node scripts/release/deploy-cpanel.mjs
env:
NODE_OPTIONS: --use-system-ca
PRODUCTION_FTP_HOST: ${{ secrets.PRODUCTION_FTP_HOST }}
PRODUCTION_FTP_USER: ${{ secrets.PRODUCTION_FTP_USER }}
PRODUCTION_FTP_PASSWORD: ${{ secrets.PRODUCTION_FTP_PASSWORD }}
PRODUCTION_FTP_PATH: ${{ secrets.PRODUCTION_FTP_PATH }}
PRODUCTION_ACTIVATION_KEY: ${{ secrets.PRODUCTION_ACTIVATION_KEY }}
PRODUCTION_FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
RELEASE_GITHUB_REPOSITORY: ${{ github.repository }}
RELEASE_GITHUB_TOKEN: ${{ github.token }}
- name: Public live Playwright gate
if: steps.branch-head.outputs.current == 'true'
id: public_live
timeout-minutes: 10
run: npm run test:e2e:live:public
env:
NODE_OPTIONS: --use-system-ca
- name: Credentialed live Playwright gate
- name: Detect credentialed live gate configuration
if: steps.branch-head.outputs.current == 'true'
id: credentialed_live_config
shell: bash
run: |
set -euo pipefail
if [[ -n "$CUSTOMER_NUMBER" && -n "$CUSTOMER_PASSWORD" &&
-n "$OPERATOR_USER_ID" && -n "$OPERATOR_PASSWORD" ]]; then
echo "configured=true" >> "$GITHUB_OUTPUT"
else
echo "configured=false" >> "$GITHUB_OUTPUT"
fi
env:
CUSTOMER_NUMBER: ${{ secrets.PLAYWRIGHT_USER_CUSTOMER_NUMBER }}
CUSTOMER_PASSWORD: ${{ secrets.PLAYWRIGHT_USER_PASSWORD }}
OPERATOR_USER_ID: ${{ secrets.PLAYWRIGHT_OPERATOR_USER_ID }}
OPERATOR_PASSWORD: ${{ secrets.PLAYWRIGHT_OPERATOR_PASSWORD }}
- name: Credentialed live Playwright gate (when configured)
if: >-
steps.branch-head.outputs.current == 'true' &&
steps.credentialed_live_config.outputs.configured == 'true'
id: credentialed_live
timeout-minutes: 15
run: npm run test:e2e:live:roles
env:
NODE_OPTIONS: --use-system-ca
PLAYWRIGHT_REQUIRE_LIVE_CREDENTIALS: "true"
PLAYWRIGHT_USER_CUSTOMER_NUMBER: ${{ secrets.PLAYWRIGHT_USER_CUSTOMER_NUMBER }}
PLAYWRIGHT_USER_PASSWORD: ${{ secrets.PLAYWRIGHT_USER_PASSWORD }}
PLAYWRIGHT_USER_OTP_SECRET: ${{ secrets.PLAYWRIGHT_USER_OTP_SECRET }}
@@ -158,7 +322,9 @@ jobs:
PLAYWRIGHT_DEPARTMENT_ID: ${{ secrets.PLAYWRIGHT_DEPARTMENT_ID }}
- name: Record Release Manager gate
id: release_manager
if: steps.branch-head.outputs.current == 'true'
timeout-minutes: 5
run: |
test -n "$RELEASE_MANAGER_GATE_TOKEN" || (echo "RELEASE_MANAGER_GATE_TOKEN is required" >&2; exit 1)
release_gate_build_id="${RELEASE_VERIFIED_BUILD_ID:-$RELEASE_EXPECTED_BUILD_ID}"
@@ -166,12 +332,11 @@ jobs:
-X POST "$RELEASE_MANAGER_GATE_URL" \
-H "Authorization: Bearer $RELEASE_MANAGER_GATE_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"$RELEASE_BRANCH\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$release_gate_build_id\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":true,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"static_artifact\",\"api_gateway\"]}"
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"master\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$release_gate_build_id\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":false,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"api_gateway\"]}"
env:
RELEASE_MANAGER_GATE_URL: ${{ secrets.RELEASE_MANAGER_GATE_URL || 'https://api.truckwash.io/release/gate/test-runs' }}
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
RELEASE_REPOSITORY: ${{ github.repository }}
RELEASE_BRANCH: ${{ github.event.workflow_run.head_branch }}
RELEASE_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Update server version after verification
@@ -179,14 +344,111 @@ jobs:
run: npm run release:update-server-version
env:
SERVER_UPDATE_TOKEN: ${{ secrets.SERVER_UPDATE_TOKEN }}
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
RELEASE_VERSION: ${{ github.event.workflow_run.head_sha }}
RELEASE_VERSION_UPDATE_REQUIRED: "true"
- name: Create verified frontend release proof
if: steps.branch-head.outputs.current == 'true'
shell: bash
run: |
set -euo pipefail
proof_dir="output/frontend-release-proof"
mkdir -p "$proof_dir"
PROOF_PATH="$proof_dir/frontend-release-proof.json" node <<'NODE'
const { writeFileSync } = require("node:fs");
const required = (name) => {
if (!process.env[name]) throw new Error(`Missing ${name}`);
return process.env[name];
};
const requireSuccessfulStep = (name) => {
const outcome = required(name);
if (outcome !== "success") throw new Error(`${name} did not succeed: ${outcome}`);
return "passed";
};
const credentialedGate = () => {
const configured = required("LIVE_CREDENTIALED_GATE_CONFIGURED");
if (configured === "false") return "not-configured";
if (configured !== "true") {
throw new Error(`Invalid LIVE_CREDENTIALED_GATE_CONFIGURED: ${configured}`);
}
return requireSuccessfulStep("LIVE_CREDENTIALED_GATE_OUTCOME");
};
const proof = {
schemaVersion: 2,
releaseId: required("RELEASE_ID"),
sha: required("RELEASE_COMMIT_SHA").toLowerCase(),
archiveSha256: required("RELEASE_ARCHIVE_SHA256").toLowerCase(),
activeTarget: required("RELEASE_ACTIVE_TARGET"),
rollbackTarget: process.env.RELEASE_ROLLBACK_TARGET || null,
verificationState: "verified",
observedAt: new Date().toISOString(),
repository: required("GITHUB_REPOSITORY"),
sourceSha: required("RELEASE_COMMIT_SHA").toLowerCase(),
testedWorkflowRunId: required("TESTED_WORKFLOW_RUN_ID"),
frontendReleaseRunId: required("GITHUB_RUN_ID"),
frontendReleaseRunAttempt: required("GITHUB_RUN_ATTEMPT"),
buildId: required("RELEASE_BUILD_ID"),
livePublicGate: requireSuccessfulStep("LIVE_PUBLIC_GATE_OUTCOME"),
liveCredentialedGate: credentialedGate(),
releaseManagerGate: requireSuccessfulStep("RELEASE_MANAGER_GATE_OUTCOME"),
serverVersionUpdated: true,
serverVersionReadBack: "passed",
completedAt: new Date().toISOString(),
};
writeFileSync(process.env.PROOF_PATH, `${JSON.stringify(proof, null, 2)}\n`, { mode: 0o600 });
NODE
env:
TESTED_WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
RELEASE_ARCHIVE_SHA256: ${{ needs.build-release.outputs.archive_sha256 }}
RELEASE_ACTIVE_TARGET: ${{ steps.deploy.outputs.active_target }}
RELEASE_ROLLBACK_TARGET: ${{ steps.deploy.outputs.rollback_target }}
LIVE_PUBLIC_GATE_OUTCOME: ${{ steps.public_live.outcome }}
LIVE_CREDENTIALED_GATE_CONFIGURED: ${{ steps.credentialed_live_config.outputs.configured }}
LIVE_CREDENTIALED_GATE_OUTCOME: ${{ steps.credentialed_live.outcome }}
RELEASE_MANAGER_GATE_OUTCOME: ${{ steps.release_manager.outcome }}
- name: Publish verified frontend release proof
if: steps.branch-head.outputs.current == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: frontend-release-proof-${{ env.RELEASE_COMMIT_SHA }}
path: output/frontend-release-proof/frontend-release-proof.json
if-no-files-found: error
retention-days: 30
- name: Roll back after any post-deployment verification failure
if: >-
failure() && steps.branch-head.outputs.current == 'true' &&
steps.deploy.outcome == 'success'
timeout-minutes: 10
shell: bash
run: |
set -euo pipefail
node scripts/release/deploy-cpanel.mjs --rollback
[[ "$RELEASE_ROLLBACK_TARGET" =~ ^releases/([a-f0-9]{40})-([1-9][0-9]*-[1-9][0-9]*)/dist$ ]]
export RELEASE_VERSION="${BASH_REMATCH[1]}"
export RELEASE_BUILD_ID="${BASH_REMATCH[2]}"
npm run release:update-server-version
env:
NODE_OPTIONS: --use-system-ca
RELEASE_ROLLBACK_TARGET: ${{ steps.deploy.outputs.rollback_target }}
PRODUCTION_FTP_HOST: ${{ secrets.PRODUCTION_FTP_HOST }}
PRODUCTION_FTP_USER: ${{ secrets.PRODUCTION_FTP_USER }}
PRODUCTION_FTP_PASSWORD: ${{ secrets.PRODUCTION_FTP_PASSWORD }}
PRODUCTION_FTP_PATH: ${{ secrets.PRODUCTION_FTP_PATH }}
PRODUCTION_ACTIVATION_KEY: ${{ secrets.PRODUCTION_ACTIVATION_KEY }}
PRODUCTION_FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
SERVER_UPDATE_TOKEN: ${{ secrets.SERVER_UPDATE_TOKEN }}
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
RELEASE_VERSION_UPDATE_REQUIRED: "true"
- name: Upload Playwright report
if: failure() && steps.branch-head.outputs.current == 'true'
continue-on-error: true
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: frontend-release-playwright-${{ env.RELEASE_BUILD_ID }}
path: output/playwright
if-no-files-found: ignore
retention-days: 3
retention-days: 14
+552 -122
View File
@@ -4,8 +4,43 @@ on:
pull_request:
push:
branches:
- main
- master
- dev
workflow_dispatch:
inputs:
mode:
description: "What to run for a manual dispatch."
required: true
type: choice
default: full
options:
- full
- targeted
- targeted-then-full
target_specs:
description: "Comma- or newline-separated Playwright spec paths under tests/e2e."
required: false
type: string
default: "tests/e2e/superuser-department-overview.spec.js"
target_projects:
description: "JSON array of Playwright projects for targeted mode."
required: false
type: string
default: '["chromium-desktop","chromium-mobile","chromium-tablet","webkit-mobile","webkit-desktop"]'
target_grep:
description: "Optional Playwright grep pattern for targeted mode."
required: false
type: string
default: ""
runner:
description: "Runner pool for this manually dispatched test run"
required: false
default: "self-hosted"
type: choice
options:
- self-hosted
- github-hosted
schedule:
- cron: "0 2 * * *"
@@ -13,16 +48,24 @@ permissions:
contents: read
concurrency:
group: frontend-tests-${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
group: frontend-tests-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.event_name == 'push' && github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
# Repository variables used as CI runner and credit controls:
# - FRONTEND_CI_STANDARD_RUNNER: JSON runs-on value for format/build/unit jobs.
# - FRONTEND_CI_E2E_RUNNER: JSON runs-on value for Playwright jobs.
# - FRONTEND_CI_PR_E2E_MAX_PARALLEL: numeric Playwright PR job parallelism.
# - FRONTEND_CI_FULL_E2E_MAX_PARALLEL: numeric full-suite job parallelism.
# - FRONTEND_CI_FULL_E2E_CONCURRENT_MAX_PARALLEL: full-suite parallelism while PR E2E runs beside it.
# GitHub-hosted target: ["ubuntu-24.04"], with PR parallelism 10, concurrent full parallelism 26,
# and standalone scheduled full parallelism 36. This keeps the workflow peak at 36 hosted jobs.
jobs:
format-tests:
# CI runs on the repository's self-hosted runner pool.
runs-on: [self-hosted, Linux, X64, pleno, frontend]
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
timeout-minutes: 15
steps:
- name: Repair self-hosted workspace permissions
if: ${{ contains(vars.FRONTEND_CI_STANDARD_RUNNER || 'self-hosted', 'self-hosted') }}
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
@@ -37,10 +80,10 @@ jobs:
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
@@ -53,12 +96,18 @@ jobs:
- name: Check frontend test formatting
run: npm run format:tests:check
build-and-unit:
needs: format-tests
runs-on: [self-hosted, Linux, X64, pleno, frontend]
quality-checks:
name: Quality-${{ matrix.check }}
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
timeout-minutes: 30
strategy:
fail-fast: false
max-parallel: 5
matrix:
check: [lint, i18n, build, unit-fast, unit-serial]
steps:
- name: Repair self-hosted workspace permissions
if: ${{ contains(vars.FRONTEND_CI_STANDARD_RUNNER || 'self-hosted', 'self-hosted') }}
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
@@ -73,47 +122,93 @@ jobs:
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Lint
run: npm run lint
- name: Check i18n source consistency
run: npm run i18n:v2:check
- name: Build sanity check
run: npm run build
- name: Unit tests
run: npm run test:unit
- name: Run quality check
shell: bash
env:
VITEST_BATCH_SIZE: 5
MATRIX_CHECK: ${{ matrix.check }}
run: |
set -euo pipefail
case "$MATRIX_CHECK" in
lint)
npm run lint
;;
i18n)
npm run i18n:v2:check
;;
build)
npm run build
;;
unit-fast)
npm run text:check-encoding
npm run test:unit:fast
;;
unit-serial)
VITEST_BATCH_SIZE=5 npm run test:unit:serial
;;
*)
echo "Unsupported quality check: $MATRIX_CHECK" >&2
exit 1
;;
esac
e2e-pr:
if: github.event_name != 'schedule'
build-and-unit:
if: ${{ always() }}
name: Build and unit summary
needs: [format-tests, quality-checks]
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
timeout-minutes: 5
steps:
- name: Verify quality jobs succeeded
shell: bash
env:
FORMAT_TESTS_RESULT: ${{ needs.format-tests.result }}
QUALITY_CHECKS_RESULT: ${{ needs.quality-checks.result }}
run: |
set -euo pipefail
failed=0
for required_job in FORMAT_TESTS_RESULT QUALITY_CHECKS_RESULT; do
result="${!required_job:-missing}"
if [[ "$result" != "success" ]]; then
echo "${required_job}=${result}" >&2
failed=1
fi
done
exit "$failed"
e2e-targeted:
if: >
github.event_name == 'workflow_dispatch' &&
(inputs.mode == 'targeted' || inputs.mode == 'targeted-then-full')
needs: build-and-unit
name: E2E-pr-${{ matrix.suite }}-${{ matrix.project }}
runs-on: [self-hosted, Linux, X64, pleno, frontend, docker]
timeout-minutes: 30
name: E2E-targeted-${{ matrix.project }}
# Use GitHub-hosted runners to avoid self-hosted desktop contention and sleep/power events.
runs-on: ubuntu-24.04
timeout-minutes: 35
strategy:
fail-fast: false
max-parallel: 4
matrix:
suite: [core, changed]
project: [chromium-desktop, chromium-mobile]
project: ${{ fromJSON(inputs.target_projects || '["chromium-desktop"]') }}
env:
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-pr-${{ matrix.suite }}-${{ matrix.project }}
MATRIX_PROJECT: ${{ matrix.project }}
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-targeted-${{ matrix.project }}
PLAYWRIGHT_REPORTER_MODE: line-html
PLAYWRIGHT_WORKERS: 1
PLAYWRIGHT_VIDEO_MODE: on-first-retry
TARGET_GREP: ${{ inputs.target_grep }}
TARGET_SPECS: ${{ inputs.target_specs }}
RUN_ID: ${{ github.run_id }}
steps:
- name: Repair self-hosted workspace permissions
- name: Normalize workspace permissions
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
@@ -128,7 +223,183 @@ jobs:
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 22
- name: Run targeted Playwright specs in container
shell: bash
run: |
set -euo pipefail
case "$MATRIX_PROJECT" in
chromium-mobile) project_offset=1 ;;
chromium-desktop) project_offset=2 ;;
chromium-tablet) project_offset=3 ;;
webkit-mobile) project_offset=31 ;;
webkit-desktop) project_offset=32 ;;
webkit-tablet) project_offset=33 ;;
firefox-mobile) project_offset=61 ;;
firefox-desktop) project_offset=62 ;;
firefox-tablet) project_offset=63 ;;
*) echo "Unsupported Playwright project: $MATRIX_PROJECT" >&2; exit 1 ;;
esac
port_seed=$((20000 + (RUN_ID % 20000) + project_offset))
lock_root="${PLAYWRIGHT_PORT_LOCK_ROOT:-/tmp/pleno-playwright-port-locks}"
mkdir -p "$lock_root"
chmod 1777 "$lock_root" 2>/dev/null || true
find "$lock_root" -mindepth 1 -maxdepth 1 -type d -mmin +360 -exec rmdir {} \; 2>/dev/null || true
playwright_port_lock=""
playwright_dev_port=""
for ((candidate = port_seed; candidate < port_seed + 1000; candidate += 1)); do
lock_dir="${lock_root}/${candidate}.lock"
if ! mkdir "$lock_dir" 2>/dev/null; then
continue
fi
if ss -H -ltn "sport = :${candidate}" 2>/dev/null | grep -q .; then
rmdir "$lock_dir" || true
continue
fi
playwright_port_lock="$lock_dir"
playwright_dev_port="$candidate"
break
done
if [[ -z "$playwright_dev_port" ]]; then
echo "Unable to find a free Playwright dev-server port." >&2
exit 1
fi
trap 'if [[ -n "${playwright_port_lock:-}" ]]; then rmdir "$playwright_port_lock" 2>/dev/null || true; fi' EXIT
if docker info >/dev/null 2>&1; then
docker_cmd=(docker)
elif sudo -n docker info >/dev/null 2>&1; then
docker_cmd=(sudo docker)
else
echo "Docker is not available to the runner user, and sudo docker is not available." >&2
exit 1
fi
mkdir -p output/playwright
scripts/ci/runner-diagnostics.sh "before targeted Playwright ${MATRIX_PROJECT}" -- "${docker_cmd[@]}"
SYSTEMD_INHIBIT_REASON="Frontend targeted Playwright ${MATRIX_PROJECT}" \
scripts/ci/with-systemd-inhibit.sh "${docker_cmd[@]}" run --rm --ipc=host --network host \
--volume "$PWD:/source:ro" \
--volume "$PWD/output/playwright:/work/output/playwright" \
--workdir /work \
--env HOME=/tmp \
--env CI="${CI:-}" \
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
--env MATRIX_PROJECT="$MATRIX_PROJECT" \
--env TARGET_GREP="$TARGET_GREP" \
--env TARGET_SPECS="$TARGET_SPECS" \
mcr.microsoft.com/playwright:v1.58.2-noble \
bash -lc '
set -euo pipefail
tar --exclude=./output/playwright -C /source -cf - . | tar -C /work -xf -
git config --global --add safe.directory /work
install_dependencies() {
local attempt
for attempt in 1 2 3; do
if npm ci --legacy-peer-deps --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000; then
return 0
fi
if [[ "$attempt" == "3" ]]; then
return 1
fi
echo "npm ci failed on attempt ${attempt}; retrying..." >&2
sleep 20
done
}
install_dependencies
ulimit -n 16384 || true
mapfile -t spec_args < <(printf "%s\n" "$TARGET_SPECS" | tr "," "\n" | sed "s/^[[:space:]]*//;s/[[:space:]]*$//;/^$/d")
if [[ "${#spec_args[@]}" -eq 0 && -z "${TARGET_GREP:-}" ]]; then
echo "Provide at least one spec path or grep pattern." >&2
exit 1
fi
for spec_path in "${spec_args[@]}"; do
if [[ "$spec_path" == /* || "$spec_path" == *".."* || "$spec_path" != tests/e2e/* ]]; then
echo "Targeted spec must stay under tests/e2e: $spec_path" >&2
exit 1
fi
if [[ ! -f "$spec_path" ]]; then
echo "Targeted spec does not exist: $spec_path" >&2
exit 1
fi
done
args=("${spec_args[@]}")
if [[ -n "${TARGET_GREP:-}" ]]; then
args+=(--grep "$TARGET_GREP")
fi
args+=(--project="$MATRIX_PROJECT")
npx playwright test "${args[@]}"
'
- name: Runner diagnostics after Playwright failure
if: failure() || cancelled()
continue-on-error: true
run: scripts/ci/runner-diagnostics.sh "after targeted Playwright ${{ matrix.project }}"
- name: Upload Playwright report
if: failure() || cancelled()
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-targeted-${{ matrix.project }}
path: |
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}-*
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}
if-no-files-found: ignore
retention-days: 1
e2e-pr:
if: >
always() &&
github.event_name != 'schedule' &&
needs.build-and-unit.result == 'success' &&
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted') &&
(
github.event_name != 'workflow_dispatch' ||
inputs.mode == 'full' ||
needs.e2e-targeted.result == 'success'
)
needs: [build-and-unit, e2e-targeted]
name: E2E-pr-${{ matrix.suite }}-${{ matrix.project }}
runs-on: ${{ fromJSON(vars.FRONTEND_CI_E2E_RUNNER || '["self-hosted","Linux","X64","pleno","frontend","docker"]') }}
timeout-minutes: 45
strategy:
fail-fast: false
max-parallel: ${{ fromJSON(vars.FRONTEND_CI_PR_E2E_MAX_PARALLEL || '2') }}
matrix:
suite: [changed-1-of-2, changed-2-of-2, smoke, pr, ct]
project: [chromium-desktop, chromium-mobile]
env:
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-pr-${{ matrix.suite }}-${{ matrix.project }}
PLAYWRIGHT_REPORTER_MODE: line-html
PLAYWRIGHT_WORKERS: 1
PLAYWRIGHT_VIDEO_MODE: on-first-retry
steps:
- name: Repair self-hosted workspace permissions
if: ${{ contains(vars.FRONTEND_CI_E2E_RUNNER || 'self-hosted', 'self-hosted') }}
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)"
if [[ -n "$foreign_entry" ]]; then
trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB"
rm -rf "$trash" 2>/dev/null || true
mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true
mkdir -p "$GITHUB_WORKSPACE"
fi
fi
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
@@ -140,23 +411,32 @@ jobs:
EVENT_NAME: ${{ github.event_name }}
HEAD_SHA: ${{ github.sha }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
set -euo pipefail
zero_sha="0000000000000000000000000000000000000000"
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_BASE_SHA" ]]; then
base_ref="$PR_BASE_SHA"
head_ref="$PR_HEAD_SHA"
elif [[ -z "$PUSH_BEFORE_SHA" || "$PUSH_BEFORE_SHA" == "$zero_sha" ]]; then
git fetch --no-tags --prune origin "$DEFAULT_BRANCH"
base_ref="origin/$DEFAULT_BRANCH"
head_ref="$HEAD_SHA"
else
base_ref="$PUSH_BEFORE_SHA"
head_ref="$HEAD_SHA"
fi
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_HEAD_SHA" ]]; then
head_ref="$PR_HEAD_SHA"
else
head_ref="$HEAD_SHA"
fi
echo "base=$base_ref" >> "$GITHUB_OUTPUT"
echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT"
echo "head=$head_ref" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
@@ -171,8 +451,11 @@ jobs:
run: |
set -euo pipefail
case "$MATRIX_SUITE" in
core) suite_offset=0 ;;
changed) suite_offset=10 ;;
changed-1-of-2) suite_offset=0 ;;
changed-2-of-2) suite_offset=10 ;;
smoke) suite_offset=20 ;;
pr) suite_offset=30 ;;
ct) suite_offset=40 ;;
*) echo "Unsupported Playwright PR suite: $MATRIX_SUITE" >&2; exit 1 ;;
esac
case "$MATRIX_PROJECT" in
@@ -180,29 +463,31 @@ jobs:
chromium-mobile) project_offset=2 ;;
*) echo "Unsupported Playwright PR project: $MATRIX_PROJECT" >&2; exit 1 ;;
esac
port_seed=$((20000 + (RUN_ID % 20000) + suite_offset + project_offset))
playwright_dev_port="$(
PORT_SEED="$port_seed" node - <<'NODE'
const net = require("node:net");
const start = Number(process.env.PORT_SEED || 20000);
const candidates = Array.from({ length: 200 }, (_, index) => start + index);
function tryPort(index) {
if (index >= candidates.length) {
console.error("Unable to find a free Playwright dev-server port.");
process.exit(1);
}
const port = candidates[index];
const server = net.createServer();
server.once("error", () => tryPort(index + 1));
server.listen(port, "127.0.0.1", () => {
server.close(() => {
console.log(port);
});
});
}
tryPort(0);
NODE
)"
port_seed=$((21000 + (RUN_ID % 20000) + suite_offset + project_offset))
lock_root="${PLAYWRIGHT_PORT_LOCK_ROOT:-/tmp/pleno-playwright-port-locks}"
mkdir -p "$lock_root"
chmod 1777 "$lock_root" 2>/dev/null || true
find "$lock_root" -mindepth 1 -maxdepth 1 -type d -mmin +360 -exec rmdir {} \; 2>/dev/null || true
playwright_port_lock=""
playwright_dev_port=""
for ((candidate = port_seed; candidate < port_seed + 1000; candidate += 1)); do
lock_dir="${lock_root}/${candidate}.lock"
if ! mkdir "$lock_dir" 2>/dev/null; then
continue
fi
if ss -H -ltn "sport = :${candidate}" 2>/dev/null | grep -q .; then
rmdir "$lock_dir" || true
continue
fi
playwright_port_lock="$lock_dir"
playwright_dev_port="$candidate"
break
done
if [[ -z "$playwright_dev_port" ]]; then
echo "Unable to find a free Playwright dev-server port." >&2
exit 1
fi
trap 'if [[ -n "${playwright_port_lock:-}" ]]; then rmdir "$playwright_port_lock" 2>/dev/null || true; fi' EXIT
if docker info >/dev/null 2>&1; then
docker_cmd=(docker)
elif sudo -n docker info >/dev/null 2>&1; then
@@ -212,7 +497,9 @@ jobs:
exit 1
fi
mkdir -p output/playwright
"${docker_cmd[@]}" run --rm --ipc=host --network host \
scripts/ci/runner-diagnostics.sh "before Playwright PR ${MATRIX_SUITE}/${MATRIX_PROJECT}" -- "${docker_cmd[@]}"
SYSTEMD_INHIBIT_REASON="Frontend Playwright PR ${MATRIX_SUITE}/${MATRIX_PROJECT}" \
scripts/ci/with-systemd-inhibit.sh "${docker_cmd[@]}" run --rm --ipc=host --network host \
--volume "$PWD:/source:ro" \
--volume "$PWD/output/playwright:/work/output/playwright" \
--workdir /work \
@@ -220,6 +507,8 @@ jobs:
--env CI="${CI:-}" \
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
--env MATRIX_SUITE="$MATRIX_SUITE" \
--env MATRIX_PROJECT="$MATRIX_PROJECT" \
@@ -230,20 +519,53 @@ jobs:
set -euo pipefail
tar --exclude=./output/playwright -C /source -cf - . | tar -C /work -xf -
git config --global --add safe.directory /work
npm ci --legacy-peer-deps
install_dependencies() {
local attempt
for attempt in 1 2 3; do
if npm ci --legacy-peer-deps --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000; then
return 0
fi
if [[ "$attempt" == "3" ]]; then
return 1
fi
echo "npm ci failed on attempt ${attempt}; retrying..." >&2
sleep 20
done
}
install_dependencies
ulimit -n 16384 || true
if [[ "$MATRIX_SUITE" == "core" ]]; then
npx playwright test --grep @smoke --project="$MATRIX_PROJECT"
npm run test:e2e:pr -- --core-only --project="$MATRIX_PROJECT"
else
npm run test:e2e:pr -- --changed-only --project="$MATRIX_PROJECT" --base="$DIFF_BASE_REF" --head="$DIFF_HEAD_REF"
fi
case "$MATRIX_SUITE" in
ct)
npm run test:ct -- --project="$MATRIX_PROJECT"
;;
smoke)
npx playwright test --grep @smoke --project="$MATRIX_PROJECT"
;;
pr)
npm run test:e2e:pr -- --core-only --project="$MATRIX_PROJECT"
;;
changed-1-of-2)
npm run test:e2e:pr -- --changed-only --project="$MATRIX_PROJECT" --base="$DIFF_BASE_REF" --head="$DIFF_HEAD_REF" -- --shard=1/2 --pass-with-no-tests
;;
changed-2-of-2)
npm run test:e2e:pr -- --changed-only --project="$MATRIX_PROJECT" --base="$DIFF_BASE_REF" --head="$DIFF_HEAD_REF" -- --shard=2/2 --pass-with-no-tests
;;
*)
echo "Unsupported Playwright PR suite: $MATRIX_SUITE" >&2
exit 1
;;
esac
'
- name: Runner diagnostics after Playwright failure
if: failure() || cancelled()
continue-on-error: true
run: scripts/ci/runner-diagnostics.sh "after Playwright PR ${{ matrix.suite }}/${{ matrix.project }}"
- name: Upload Playwright report
if: failure() || cancelled()
continue-on-error: true
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-pr-${{ matrix.suite }}-${{ matrix.project }}
path: |
@@ -252,38 +574,84 @@ jobs:
if-no-files-found: ignore
retention-days: 1
required-ci:
if: ${{ always() && (github.event_name == 'pull_request' || github.event_name == 'push') }}
name: Required CI
needs: [format-tests, build-and-unit, e2e-pr]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Verify required jobs succeeded
shell: bash
env:
FORMAT_TESTS_RESULT: ${{ needs.format-tests.result }}
BUILD_AND_UNIT_RESULT: ${{ needs.build-and-unit.result }}
E2E_PR_RESULT: ${{ needs.e2e-pr.result }}
run: |
set -euo pipefail
failed=0
for required_job in FORMAT_TESTS_RESULT BUILD_AND_UNIT_RESULT E2E_PR_RESULT; do
result="${!required_job:-missing}"
if [[ "$result" != "success" ]]; then
echo "${required_job}=${result}" >&2
failed=1
fi
done
exit "$failed"
e2e-full:
if: >
always() &&
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch) &&
(
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(
github.ref_name == github.event.repository.default_branch &&
!(github.event_name == 'push' && github.event.before == 'd393c8c17508c46c61e97bd834a2e407367c69eb')
)
) &&
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted') &&
needs.build-and-unit.result == 'success' &&
(github.event_name == 'schedule' || needs.e2e-pr.result == 'success')
needs: [build-and-unit, e2e-pr]
name: E2E-full-${{ matrix.role }}-${{ matrix.browser_label }}-${{ matrix.device }}
runs-on: [self-hosted, Linux, X64, pleno, frontend, docker]
(
github.event_name != 'workflow_dispatch' ||
inputs.mode == 'full' ||
needs.e2e-targeted.result == 'success'
)
needs: [build-and-unit, e2e-targeted]
name: E2E-full-${{ matrix.browser_label }}-${{ matrix.device }}-${{ matrix.role }}-shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}
runs-on: ${{ fromJSON(vars.FRONTEND_CI_E2E_RUNNER || '["self-hosted","Linux","X64","pleno","frontend","docker"]') }}
timeout-minutes: 60
strategy:
fail-fast: false
max-parallel: 4
max-parallel: ${{ fromJSON(vars.FRONTEND_CI_FULL_E2E_MAX_PARALLEL || '36') > 0 && github.event_name == 'schedule' && fromJSON(vars.FRONTEND_CI_FULL_E2E_MAX_PARALLEL || '36') || fromJSON(vars.FRONTEND_CI_FULL_E2E_CONCURRENT_MAX_PARALLEL || '26') }}
matrix:
role: [customer, subuser, admin, superuser]
browser: [chromium, firefox, webkit]
device: [mobile, tablet, desktop]
device: [mobile, desktop, tablet]
role: [superuser, admin, customer, subuser]
shard_index: [1, 2]
browser: [chromium, webkit, firefox]
exclude:
- role: customer
shard_index: 2
- role: subuser
shard_index: 2
include:
- browser: chromium
browser_label: Chromium
browser_install: chromium
- browser: firefox
browser_label: Firefox
browser_install: firefox
- browser: webkit
browser_label: WebKit
browser_install: webkit
- browser: firefox
browser_label: Firefox
browser_install: firefox
env:
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-full-${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-full-${{ matrix.browser }}-${{ matrix.device }}-${{ matrix.role }}-shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}
PLAYWRIGHT_REPORTER_MODE: line-html
PLAYWRIGHT_WORKERS: 1
PLAYWRIGHT_VIDEO_MODE: off
steps:
- name: Repair self-hosted workspace permissions
if: ${{ contains(vars.FRONTEND_CI_E2E_RUNNER || 'self-hosted', 'self-hosted') }}
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
@@ -298,10 +666,10 @@ jobs:
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
@@ -311,51 +679,60 @@ jobs:
MATRIX_ROLE: ${{ matrix.role }}
MATRIX_BROWSER: ${{ matrix.browser }}
MATRIX_DEVICE: ${{ matrix.device }}
MATRIX_SHARD_INDEX: ${{ matrix.shard_index }}
MATRIX_SHARD_TOTAL: ${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}
RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail
case "$MATRIX_ROLE" in
customer) role_offset=0 ;;
subuser) role_offset=100 ;;
admin) role_offset=200 ;;
superuser) role_offset=300 ;;
superuser) role_offset=0 ;;
admin) role_offset=100 ;;
customer) role_offset=200 ;;
subuser) role_offset=300 ;;
*) echo "Unsupported Playwright role: $MATRIX_ROLE" >&2; exit 1 ;;
esac
case "$MATRIX_BROWSER" in
chromium) browser_offset=0 ;;
firefox) browser_offset=30 ;;
webkit) browser_offset=60 ;;
webkit) browser_offset=30 ;;
firefox) browser_offset=60 ;;
*) echo "Unsupported Playwright browser: $MATRIX_BROWSER" >&2; exit 1 ;;
esac
case "$MATRIX_DEVICE" in
mobile) device_offset=1 ;;
tablet) device_offset=2 ;;
desktop) device_offset=3 ;;
desktop) device_offset=2 ;;
tablet) device_offset=3 ;;
*) echo "Unsupported Playwright device: $MATRIX_DEVICE" >&2; exit 1 ;;
esac
port_seed=$((20000 + (RUN_ID % 20000) + role_offset + browser_offset + device_offset))
playwright_dev_port="$(
PORT_SEED="$port_seed" node - <<'NODE'
const net = require("node:net");
const start = Number(process.env.PORT_SEED || 20000);
const candidates = Array.from({ length: 200 }, (_, index) => start + index);
function tryPort(index) {
if (index >= candidates.length) {
console.error("Unable to find a free Playwright dev-server port.");
process.exit(1);
}
const port = candidates[index];
const server = net.createServer();
server.once("error", () => tryPort(index + 1));
server.listen(port, "127.0.0.1", () => {
server.close(() => {
console.log(port);
});
});
}
tryPort(0);
NODE
)"
case "$MATRIX_SHARD_INDEX" in
1) shard_offset=0 ;;
2) shard_offset=400 ;;
*) echo "Unsupported Playwright shard index: $MATRIX_SHARD_INDEX" >&2; exit 1 ;;
esac
port_seed=$((22000 + (RUN_ID % 20000) + role_offset + browser_offset + device_offset + shard_offset))
lock_root="${PLAYWRIGHT_PORT_LOCK_ROOT:-/tmp/pleno-playwright-port-locks}"
mkdir -p "$lock_root"
chmod 1777 "$lock_root" 2>/dev/null || true
find "$lock_root" -mindepth 1 -maxdepth 1 -type d -mmin +360 -exec rmdir {} \; 2>/dev/null || true
playwright_port_lock=""
playwright_dev_port=""
for ((candidate = port_seed; candidate < port_seed + 1000; candidate += 1)); do
lock_dir="${lock_root}/${candidate}.lock"
if ! mkdir "$lock_dir" 2>/dev/null; then
continue
fi
if ss -H -ltn "sport = :${candidate}" 2>/dev/null | grep -q .; then
rmdir "$lock_dir" || true
continue
fi
playwright_port_lock="$lock_dir"
playwright_dev_port="$candidate"
break
done
if [[ -z "$playwright_dev_port" ]]; then
echo "Unable to find a free Playwright dev-server port." >&2
exit 1
fi
trap 'if [[ -n "${playwright_port_lock:-}" ]]; then rmdir "$playwright_port_lock" 2>/dev/null || true; fi' EXIT
if docker info >/dev/null 2>&1; then
docker_cmd=(docker)
elif sudo -n docker info >/dev/null 2>&1; then
@@ -365,7 +742,9 @@ jobs:
exit 1
fi
mkdir -p output/playwright
"${docker_cmd[@]}" run --rm --ipc=host --network host \
scripts/ci/runner-diagnostics.sh "before Playwright full ${MATRIX_BROWSER}/${MATRIX_DEVICE}/${MATRIX_ROLE}/shard-${MATRIX_SHARD_INDEX}-of-${MATRIX_SHARD_TOTAL}" -- "${docker_cmd[@]}"
SYSTEMD_INHIBIT_REASON="Frontend Playwright full ${MATRIX_BROWSER}/${MATRIX_DEVICE}/${MATRIX_ROLE}/shard-${MATRIX_SHARD_INDEX}-of-${MATRIX_SHARD_TOTAL}" \
scripts/ci/with-systemd-inhibit.sh "${docker_cmd[@]}" run --rm --ipc=host --network host \
--volume "$PWD:/source:ro" \
--volume "$PWD/output/playwright:/work/output/playwright" \
--workdir /work \
@@ -373,29 +752,80 @@ jobs:
--env CI="${CI:-}" \
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
--env MATRIX_ROLE="$MATRIX_ROLE" \
--env MATRIX_BROWSER="$MATRIX_BROWSER" \
--env MATRIX_DEVICE="$MATRIX_DEVICE" \
--env MATRIX_SHARD_INDEX="$MATRIX_SHARD_INDEX" \
--env MATRIX_SHARD_TOTAL="$MATRIX_SHARD_TOTAL" \
mcr.microsoft.com/playwright:v1.58.2-noble \
bash -lc '
set -euo pipefail
tar --exclude=./output/playwright -C /source -cf - . | tar -C /work -xf -
git config --global --add safe.directory /work
npm ci --legacy-peer-deps
install_dependencies() {
local attempt
for attempt in 1 2 3; do
if npm ci --legacy-peer-deps --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000; then
return 0
fi
if [[ "$attempt" == "3" ]]; then
return 1
fi
echo "npm ci failed on attempt ${attempt}; retrying..." >&2
sleep 20
done
}
install_dependencies
ulimit -n 16384 || true
npm run test:e2e:full:slice -- --role="$MATRIX_ROLE" --project="$MATRIX_BROWSER-$MATRIX_DEVICE"
npm run test:e2e:full:slice -- --role="$MATRIX_ROLE" --project="$MATRIX_BROWSER-$MATRIX_DEVICE" --shard="$MATRIX_SHARD_INDEX/$MATRIX_SHARD_TOTAL"
'
- name: Runner diagnostics after Playwright failure
if: failure() || cancelled()
continue-on-error: true
run: scripts/ci/runner-diagnostics.sh "after Playwright full ${{ matrix.browser }}/${{ matrix.device }}/${{ matrix.role }}/shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}"
- name: Upload Playwright report
if: failure() || cancelled()
continue-on-error: true
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-full-${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}
name: playwright-report-full-${{ matrix.browser }}-${{ matrix.device }}-${{ matrix.role }}-shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}
path: |
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/report
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/test-results
output/playwright/test-lists/${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}.txt
output/playwright/test-lists/${{ matrix.browser }}-${{ matrix.device }}-${{ matrix.role }}.txt
output/playwright/test-lists/${{ matrix.browser }}-${{ matrix.device }}-${{ matrix.role }}-shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}.txt
if-no-files-found: ignore
retention-days: 1
full-e2e-summary:
if: >
always() &&
(
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(
github.ref_name == github.event.repository.default_branch &&
!(github.event_name == 'push' && github.event.before == 'd393c8c17508c46c61e97bd834a2e407367c69eb')
)
) &&
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted')
name: Full E2E summary
needs: [e2e-full]
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Verify full E2E succeeded
shell: bash
env:
FULL_E2E_RESULT: ${{ needs.e2e-full.result }}
run: |
set -euo pipefail
if [[ "$FULL_E2E_RESULT" != "success" ]]; then
echo "E2E_FULL_RESULT=${FULL_E2E_RESULT:-missing}" >&2
exit 1
fi
+10
View File
@@ -8,12 +8,22 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
node_modules.codex-permission-blocked-*
.DS_Store
dist
dist-ssr
coverage
*.local
dev-dist
.playwright-cli/
# Mobile build and signing outputs
/app/build/
/app-release*.aab
/app-release*.apk
/app-release*.idsig
*.keystore
*.jks
/cypress/videos/
/cypress/screenshots/
+1
View File
@@ -0,0 +1 @@
legacy-peer-deps=true
+4
View File
@@ -0,0 +1,4 @@
source "https://rubygems.org"
ruby ">= 3.2", "< 3.5"
gem "fastlane", "2.238.0"
+264
View File
@@ -0,0 +1,264 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.8)
abbrev (0.1.2)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.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)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.130.0)
aws-sdk-core (~> 3, >= 3.254.0)
aws-sigv4 (~> 1.5)
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)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
benchmark (0.5.0)
bigdecimal (4.1.2)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
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)
erb (6.0.7)
excon (1.7.0)
logger
faraday (2.14.3)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
http-cookie (>= 1.0.0)
faraday-follow_redirects (0.5.0)
faraday (>= 1, < 3)
faraday-multipart (1.2.0)
multipart-post (~> 2.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.238.0)
CFPropertyList (>= 2.3, < 5.0.0)
abbrev (~> 0.1)
addressable (>= 2.9.0, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.197)
babosa (>= 1.0.3, < 2.0.0)
base64 (~> 0.2)
benchmark (>= 0.1.0)
bundler (>= 2.4.0, < 5.0.0)
colored (~> 1.2)
commander (~> 4.6)
csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 2.0.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)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.3.0)
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)
mini_magick (>= 4.9.4, < 5.0.0)
multi_json (~> 1.12)
multipart-post (>= 2.0.0, < 3.0.0)
mutex_m (~> 0.3)
naturally (~> 2.2)
nkf (~> 0.2)
optparse (>= 0.1.1, < 1.0.0)
ostruct (>= 0.1.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 4)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.106.0)
google-apis-core (>= 0.15.0, < 2.a)
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 (>= 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.66.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.9.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (2.2.2)
base64 (~> 0.2)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.7.0)
google-cloud-storage (1.62.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-core (>= 0.18, < 2)
google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (>= 0.42)
google-cloud-core (~> 1.6)
googleauth (~> 1.9)
mini_mime (~> 1.0)
google-logging-utils (0.2.0)
googleauth (1.17.3)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
jwt (>= 1.4, < 4.0)
os (>= 0.9, < 2.0)
pstore (~> 0.1)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
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.2)
jwt (3.2.0)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.21.1)
multipart-post (2.4.1)
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 (4.2.0)
rexml (3.4.4)
rouge (3.28.0)
rubyzip (2.4.1)
security (0.1.5)
signet (0.22.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 4.0)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
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 (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)
atomos (~> 0.1.3)
base64
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
nkf
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
fastlane (= 2.238.0)
RUBY VERSION
ruby 3.3.12p206
BUNDLED WITH
2.5.22
+75 -7
View File
@@ -16,6 +16,16 @@ See [Vite Configuration Reference](https://vite.dev/config/).
npm install
```
## Contributing Changes
Create a scoped feature branch, push it, and open a pull request targeting
`master`. Do not push directly to `master`. Merge only after the `Required CI`
check succeeds, all review conversations are resolved, and the branch is up to
date. Use squash merge so `master` retains linear history.
See [`.github/BRANCH_PROTECTION.md`](.github/BRANCH_PROTECTION.md) for the
repository policy, rollout checks, and emergency bypass procedure.
### Compile and Hot-Reload for Development
```sh
@@ -96,16 +106,22 @@ Artifacts and summaries:
## Playwright Full E2E
Run the permanent grouped full-suite entrypoint with a hard max of 5 total workers across:
Run the permanent grouped full-suite entrypoint with a hard max of 5 total workers across the browser-engine groups:
- `chromium-(desktop|tablet|mobile)`
- `firefox-(desktop|tablet|mobile)`
- `webkit-(desktop|tablet|mobile)`
- Chromium
- WebKit
- Firefox
```sh
npm run test:e2e:ci
```
The full CI matrix is ordered by browser engine, then device class, then user role:
- browsers: `chromium`, `webkit`, `firefox`
- devices: `mobile`, `desktop`, `tablet`
- roles: `superuser`, `admin`, `customer`, `subuser`
Run a single full-suite slice for one role and one Playwright project:
```sh
@@ -117,7 +133,7 @@ Default worker allocation:
```sh
PLAYWRIGHT_PARALLEL_WORKERS_CHROMIUM=2
PLAYWRIGHT_PARALLEL_WORKERS_FIREFOX=1
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=2
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=1
```
Optional overrides:
@@ -126,18 +142,70 @@ Optional overrides:
PLAYWRIGHT_PARALLEL_BASE_PORT=5191
PLAYWRIGHT_PARALLEL_WORKERS_CHROMIUM=2
PLAYWRIGHT_PARALLEL_WORKERS_FIREFOX=1
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=2
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=1
```
The runner fails fast if the combined worker count exceeds 5.
GitHub Actions keeps the full browser/device/role matrix stable by capping full-suite
matrix parallelism at 2 jobs, running each full slice with `PLAYWRIGHT_WORKERS=1`,
wrapping Docker Playwright runs with `systemd-inhibit` when available, and setting
`PLAYWRIGHT_VIDEO_MODE=off` for the full matrix. The E2E network harness serves
Font Awesome from local fixtures so WebKit page loads and visual snapshots do not
depend on CDN/TLS availability. Traces and screenshots are still retained on
failure.
Artifacts and summaries:
- `output/playwright/ci-parallel-report/index.html`
- `output/playwright/ci-parallel-chromium/report/index.html`
- `output/playwright/ci-parallel-firefox/report/index.html`
- `output/playwright/ci-parallel-webkit/report/index.html`
- `output/playwright/test-lists/<role>-<project>.txt`
- `output/playwright/test-lists/<project>-<role>.txt`
- `output/playwright/test-lists/<role>-<project>.txt` (legacy compatibility copy)
## Android App Icon
The Play Store Android package is built from the Capacitor project in `android/`.
The legacy Bubblewrap/TWA project at the repository root is not used by
`npm run mobile:android:bundle`.
The native launcher and store icons use the opaque iOS marketing icon as their
shared master so Android and iOS keep the same white background:
```text
ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png
```
Regenerate the checked-in launcher assets after changing that source image:
```sh
npm run mobile:android:icons
```
Check that the generated Android launcher assets are current:
```sh
npm run mobile:android:icons:check
```
`npm run mobile:android:sync` runs the icon generator before building and syncing
the Capacitor Android project. The generator updates `android/app/src/main/res`
launcher assets, `public/icons/icon-192x192.png`, `public/icons/icon-512x512.png`,
and `store_icon.png`.
## Mobile Store Releases
Signed Android and iOS store artifacts are built through the GitHub Actions
`Mobile Store Artifacts` workflow. By default, current `master` after green
`Automated Tests` uploads Android to Google Play production and uploads iOS to
App Store Connect.
See `docs/mobile-artifacts.md` for workflow triggers, required secrets, and
local mobile checks. See `docs/app-store-release.md` for App Store Connect
release preparation and review notes. For a separate development-signed IPA
that can be installed on an approved iPhone from Ubuntu over USB, see
`docs/ios-device-debug.md`.
## Bubblewrap (TWA) Build and Install
BIN
View File
Binary file not shown.
+101
View File
@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml
+2
View File
@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep
+77
View File
@@ -0,0 +1,77 @@
apply plugin: 'com.android.application'
def mobileEnv = { String name, String defaultValue = null ->
return System.getenv(name) ?: (project.hasProperty(name) ? project.property(name).toString() : defaultValue)
}
def releaseStoreFile = mobileEnv('ANDROID_KEYSTORE_FILE')
def hasReleaseSigning = releaseStoreFile &&
mobileEnv('ANDROID_KEYSTORE_PASSWORD') &&
mobileEnv('ANDROID_KEY_ALIAS') &&
mobileEnv('ANDROID_KEY_PASSWORD')
android {
namespace = "io.truckwash.twa"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "io.truckwash.twa"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(mobileEnv('MOBILE_VERSION_CODE', '1'))
versionName mobileEnv('MOBILE_VERSION_NAME', '1.0')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
signingConfigs {
release {
if (hasReleaseSigning) {
storeFile file(releaseStoreFile)
storePassword mobileEnv('ANDROID_KEYSTORE_PASSWORD')
keyAlias mobileEnv('ANDROID_KEY_ALIAS')
keyPassword mobileEnv('ANDROID_KEY_PASSWORD')
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
if (hasReleaseSigning) {
signingConfig signingConfigs.release
}
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}
+19
View File
@@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-geolocation')
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,26 @@
package io.truckwash.twa;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("io.truckwash.twa", appContext.getPackageName());
}
}
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>
@@ -0,0 +1,5 @@
package io.truckwash.twa;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">Truck Wash</string>
<string name="title_activity_main">Truck Wash</string>
<string name="package_name">io.truckwash.twa</string>
<string name="custom_url_scheme">io.truckwash.twa</string>
</resources>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>
@@ -0,0 +1,18 @@
package io.truckwash.twa;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
+29
View File
@@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
+6
View File
@@ -0,0 +1,6 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':capacitor-geolocation'
project(':capacitor-geolocation').projectDir = new File('../node_modules/@capacitor/geolocation/android')
+22
View File
@@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+251
View File
@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+94
View File
@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+5
View File
@@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'
+16
View File
@@ -0,0 +1,16 @@
ext {
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.11.0'
androidxAppCompatVersion = '1.7.1'
androidxCoordinatorLayoutVersion = '1.3.0'
androidxCoreVersion = '1.17.0'
androidxFragmentVersion = '1.8.9'
coreSplashScreenVersion = '1.2.0'
androidxWebkitVersion = '1.14.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.3.0'
androidxEspressoCoreVersion = '3.7.0'
cordovaAndroidVersion = '14.0.1'
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Automatically generated file. DO NOT MODIFY -->
<!-- Value from default config. -->
<bool name="enableNotification">true</bool>
<!-- Value from default config. -->
<bool name="enableSiteSettingsShortcut">true</bool>
<!-- Value from default config. -->
<color name="backgroundColor">#0787BB</color>
<!-- Value from default config. -->
<color name="colorPrimary">#063651</color>
<!-- Value from default config. -->
<color name="colorPrimaryDark">#000000</color>
<!-- Value from default config. -->
<color name="navigationColor">#000000</color>
<!-- Value from default config. -->
<color name="navigationColorDark">#000000</color>
<!-- Value from default config. -->
<color name="navigationDividerColor">#000000</color>
<!-- Value from default config. -->
<color name="navigationDividerColorDark">#000000</color>
<!-- Value from default config. -->
<integer name="splashScreenFadeOutDuration">300</integer>
<!-- Value from default config. -->
<string name="appName" translatable="false">Truck Wash</string>
<!-- Value from default config. -->
<string name="fallbackType" translatable="false">customtabs</string>
<!-- Value from default config. -->
<string name="fullScopeUrl" translatable="false">https://truckwash.io/</string>
<!-- Value from default config. -->
<string name="generatorApp" translatable="false">bubblewrap-cli</string>
<!-- Value from default config. -->
<string name="hostName" translatable="false">truckwash.io</string>
<!-- Value from default config. -->
<string name="launchHandlerClientMode" translatable="false">navigate-existing</string>
<!-- Value from default config. -->
<string name="launchUrl" translatable="false">https://truckwash.io/</string>
<!-- Value from default config. -->
<string name="launcherName" translatable="false">Truck Wash</string>
<!-- Value from default config. -->
<string name="orientation" translatable="false">portrait</string>
<!-- Value from default config. -->
<string name="providerAuthority" translatable="false">io.truckwash.twa.fileprovider</string>
<!-- Value from default config. -->
<string name="webManifestUrl" translatable="false">https://truckwash.io/manifest.webmanifest</string>
</resources>
@@ -1,55 +0,0 @@
-keep class androidx.core.app.CoreComponentFactory { <init>(); }
-keep class androidx.core.content.FileProvider { <init>(); }
-keep class androidx.profileinstaller.ProfileInstallReceiver { <init>(); }
-keep class androidx.startup.InitializationProvider { <init>(); }
-keep class com.google.android.gms.common.api.GoogleApiActivity { <init>(); }
-keep class com.google.androidbrowserhelper.locationdelegation.PermissionRequestActivity { <init>(); }
-keep class com.google.androidbrowserhelper.trusted.FocusActivity { <init>(); }
-keep class com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity { <init>(); }
-keep class com.google.androidbrowserhelper.trusted.NotificationPermissionRequestActivity { <init>(); }
-keep class com.google.androidbrowserhelper.trusted.WebViewFallbackActivity { <init>(); }
-keep class io.truckwash.twa.Application { <init>(); }
-keep class io.truckwash.twa.DelegationService { <init>(); }
-keep class io.truckwash.twa.LauncherActivity { <init>(); }
-keep class android.widget.Space { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.app.AlertController$RecycleListView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.view.menu.ActionMenuItemView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.view.menu.ExpandedMenuView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.view.menu.ListMenuItemView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActionBarContainer { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActionBarContextView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActionBarOverlayLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActionMenuView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActivityChooserView$InnerLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.AlertDialogLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ButtonBarLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ContentFrameLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.DialogTitle { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.FitWindowsFrameLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.FitWindowsLinearLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.SearchView$SearchAutoComplete { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.Toolbar { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ViewStubCompat { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.browser.browseractions.BrowserActionsFallbackMenuView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.core.widget.NestedScrollView { <init>(android.content.Context, android.util.AttributeSet); }
@@ -1,2 +0,0 @@
#- File Locator -
listingFile=../../../../outputs/apk/release/output-metadata.json
@@ -1,2 +0,0 @@
appMetadataVersion=1.1
androidGradlePluginVersion=8.9.1
@@ -1,18 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "BUNDLE",
"kind": "RegularFile"
},
"applicationId": "io.truckwash.twa",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"outputFile": "../../../../outputs/bundle/release/app-release.aab"
}
],
"elementType": "File"
}
@@ -1,2 +0,0 @@
#- File Locator -
listingFile=../../../bundle_ide_model/release/produceReleaseBundleIdeListingFile/output-metadata.json
@@ -1,222 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!--
The "package" attribute is rewritten by the Gradle build with the value of applicationId.
It is still required here, as it is used to derive paths, for instance when referring
to an Activity by ".MyActivity" instead of the full name. If more Activities are added to the
application, the package attribute will need to reflect the correct path in order to use
the abbreviated format.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.truckwash.twa"
android:versionCode="6"
android:versionName="6" >
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="35" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
</queries>
<permission
android:name="io.truckwash.twa.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
android:protectionLevel="signature" />
<uses-permission android:name="io.truckwash.twa.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" />
<application
android:name="io.truckwash.twa.Application"
android:allowBackup="true"
android:appComponentFactory="androidx.core.app.CoreComponentFactory"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/appName"
android:manageSpaceActivity="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity"
android:supportsRtl="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar" >
<meta-data
android:name="asset_statements"
android:resource="@string/assetStatements" />
<meta-data
android:name="web_manifest_url"
android:value="@string/webManifestUrl" />
<meta-data
android:name="twa_generator"
android:value="@string/generatorApp" />
<activity android:name="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity" >
<meta-data
android:name="android.support.customtabs.trusted.MANAGE_SPACE_URL"
android:value="@string/launchUrl" />
</activity>
<activity
android:name="io.truckwash.twa.LauncherActivity"
android:alwaysRetainTaskState="true"
android:exported="true"
android:label="@string/launcherName" >
<meta-data
android:name="android.support.customtabs.trusted.DEFAULT_URL"
android:value="@string/launchUrl" />
<meta-data
android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR"
android:resource="@color/colorPrimary" />
<meta-data
android:name="android.support.customtabs.trusted.LAUNCH_HANDLER_CLIENT_MODE"
android:value="@string/launchHandlerClientMode" />
<meta-data
android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR_DARK"
android:resource="@color/colorPrimaryDark" />
<meta-data
android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR"
android:resource="@color/navigationColor" />
<meta-data
android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR_DARK"
android:resource="@color/navigationColorDark" />
<meta-data
android:name="androix.browser.trusted.NAVIGATION_BAR_DIVIDER_COLOR"
android:resource="@color/navigationDividerColor" />
<meta-data
android:name="androix.browser.trusted.NAVIGATION_BAR_DIVIDER_COLOR_DARK"
android:resource="@color/navigationDividerColorDark" />
<meta-data
android:name="android.support.customtabs.trusted.SPLASH_IMAGE_DRAWABLE"
android:resource="@drawable/splash" />
<meta-data
android:name="android.support.customtabs.trusted.SPLASH_SCREEN_BACKGROUND_COLOR"
android:resource="@color/backgroundColor" />
<meta-data
android:name="android.support.customtabs.trusted.SPLASH_SCREEN_FADE_OUT_DURATION"
android:value="@integer/splashScreenFadeOutDuration" />
<meta-data
android:name="android.support.customtabs.trusted.FILE_PROVIDER_AUTHORITY"
android:value="@string/providerAuthority" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<meta-data
android:name="android.support.customtabs.trusted.FALLBACK_STRATEGY"
android:value="@string/fallbackType" />
<meta-data
android:name="android.support.customtabs.trusted.SCREEN_ORIENTATION"
android:value="@string/orientation" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true" >
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="@string/hostName"
android:scheme="https" />
</intent-filter>
</activity>
<activity android:name="com.google.androidbrowserhelper.trusted.FocusActivity" />
<activity
android:name="com.google.androidbrowserhelper.trusted.WebViewFallbackActivity"
android:configChanges="orientation|screenSize" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/providerAuthority"
android:exported="false"
android:grantUriPermissions="true" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
<service
android:name="io.truckwash.twa.DelegationService"
android:enabled="@bool/enableNotification"
android:exported="@bool/enableNotification" >
<meta-data
android:name="android.support.customtabs.trusted.SMALL_ICON"
android:resource="@drawable/ic_notification_icon" />
<intent-filter>
<action android:name="android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<activity android:name="com.google.androidbrowserhelper.trusted.NotificationPermissionRequestActivity" />
<activity android:name="com.google.androidbrowserhelper.locationdelegation.PermissionRequestActivity" />
<activity
android:name="com.google.android.gms.common.api.GoogleApiActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="io.truckwash.twa.androidx-startup"
android:exported="false" >
<meta-data
android:name="androidx.emoji2.text.EmojiCompatInitializer"
android:value="androidx.startup" />
<meta-data
android:name="androidx.lifecycle.ProcessLifecycleInitializer"
android:value="androidx.startup" />
<meta-data
android:name="androidx.profileinstaller.ProfileInstallerInitializer"
android:value="androidx.startup" />
</provider>
<receiver
android:name="androidx.profileinstaller.ProfileInstallReceiver"
android:directBootAware="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.DUMP" >
<intent-filter>
<action android:name="androidx.profileinstaller.action.INSTALL_PROFILE" />
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.SKIP_FILE" />
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.SAVE_PROFILE" />
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION" />
</intent-filter>
</receiver>
</application>
</manifest>
@@ -1,873 +0,0 @@
Landroidx/activity/a;
Landroidx/activity/ComponentActivity$4;
Landroidx/lifecycle/j;
Landroidx/lifecycle/k;
HSPLandroidx/activity/ComponentActivity$4;->h(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
Landroidx/activity/b;
Landroidx/core/app/b;
Landroidx/lifecycle/l;
Landroidx/lifecycle/E;
Landroidx/lifecycle/f;
LI/d;
Landroidx/activity/c;
HSPLandroidx/activity/c;-><init>(Z)V
HSPLandroidx/activity/c;->a()V
PLandroidx/activity/c;->b(Landroidx/activity/a;)V
HSPLandroidx/activity/c;->c(Z)V
Landroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;
PLandroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;->d()V
HSPLandroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;->h(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
Landroidx/activity/OnBackPressedDispatcher;
HSPLandroidx/activity/OnBackPressedDispatcher;->a(Landroidx/activity/c;)Landroidx/activity/a;
Lc/a;
Lc/d;
Lc/e;
Lc/f;
Lc/g;
Lc/h;
Lc/i;
HSPLc/i;-><clinit>()V
Ld/a;
HSPLd/a;-><init>(II)V
Le/a;
Lg/b;
Lh/a;
HSPLh/a;-><init>(Landroid/content/Context;)V
HSPLh/a;->a(Landroid/content/Context;)Lh/a;
HSPLh/a;->b()I
HSPLh/a;->c()I
HSPLh/a;->d()Z
Lh/b;
HSPLh/b;-><clinit>()V
HSPLh/b;-><init>(Landroid/content/Context;)V
Li/a;
Ls/b;
HSPLi/a;-><init>(Landroid/content/Context;IIIILjava/lang/CharSequence;)V
Landroidx/appcompat/view/menu/a;
Landroidx/appcompat/view/menu/h;
HSPLandroidx/appcompat/view/menu/a;-><init>(Landroid/content/Context;II)V
HSPLandroidx/appcompat/view/menu/a;->k(Landroid/content/Context;Landroidx/appcompat/view/menu/d;)V
PLandroidx/appcompat/view/menu/a;->a(Landroidx/appcompat/view/menu/d;Z)V
HSPLandroidx/appcompat/view/menu/a;->f(Landroidx/appcompat/view/menu/h$a;)V
HSPLandroidx/appcompat/view/menu/a;->j(Z)V
Landroidx/appcompat/view/menu/d$a;
Landroidx/appcompat/view/menu/d$b;
Landroidx/appcompat/view/menu/d;
Ls/a;
HSPLandroidx/appcompat/view/menu/d;-><clinit>()V
HSPLandroidx/appcompat/view/menu/d;-><init>(Landroid/content/Context;)V
HSPLandroidx/appcompat/view/menu/d;->b(Landroidx/appcompat/view/menu/h;Landroid/content/Context;)V
PLandroidx/appcompat/view/menu/d;->close()V
PLandroidx/appcompat/view/menu/d;->d(Z)V
HSPLandroidx/appcompat/view/menu/d;->h(Z)V
HSPLandroidx/appcompat/view/menu/d;->q()V
HSPLandroidx/appcompat/view/menu/d;->r()Ljava/util/ArrayList;
HSPLandroidx/appcompat/view/menu/d;->v()Ljava/util/ArrayList;
HSPLandroidx/appcompat/view/menu/d;->A()Ljava/util/ArrayList;
HSPLandroidx/appcompat/view/menu/d;->hasVisibleItems()Z
HSPLandroidx/appcompat/view/menu/d;->H(Z)V
HSPLandroidx/appcompat/view/menu/d;->M(Landroidx/appcompat/view/menu/d$a;)V
HSPLandroidx/appcompat/view/menu/d;->setQwertyMode(Z)V
HSPLandroidx/appcompat/view/menu/d;->U(Z)V
HSPLandroidx/appcompat/view/menu/d;->size()I
HSPLandroidx/appcompat/view/menu/d;->V()V
HSPLandroidx/appcompat/view/menu/d;->W()V
Landroidx/appcompat/view/menu/h$a;
Landroidx/appcompat/view/menu/i;
Landroidx/appcompat/widget/a$a;
HSPLandroidx/appcompat/widget/a$a;-><init>(Landroidx/appcompat/widget/a;)V
Landroidx/appcompat/widget/a;
HSPLandroidx/appcompat/widget/a;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
Landroidx/appcompat/widget/b;
HSPLandroidx/appcompat/widget/b;-><init>(Landroidx/appcompat/widget/ActionBarContainer;)V
HSPLandroidx/appcompat/widget/b;->draw(Landroid/graphics/Canvas;)V
HSPLandroidx/appcompat/widget/b;->getOpacity()I
HSPLandroidx/appcompat/widget/b;->getOutline(Landroid/graphics/Outline;)V
Landroidx/appcompat/widget/ActionBarContainer;
HSPLandroidx/appcompat/widget/ActionBarContainer;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/ActionBarContainer;->drawableStateChanged()V
HSPLandroidx/appcompat/widget/ActionBarContainer;->jumpDrawablesToCurrentState()V
HSPLandroidx/appcompat/widget/ActionBarContainer;->onFinishInflate()V
HSPLandroidx/appcompat/widget/ActionBarContainer;->onLayout(ZIIII)V
HSPLandroidx/appcompat/widget/ActionBarContainer;->onMeasure(II)V
HSPLandroidx/appcompat/widget/ActionBarContainer;->setTabContainer(Landroidx/appcompat/widget/G;)V
PLandroidx/appcompat/widget/ActionBarContainer;->verifyDrawable(Landroid/graphics/drawable/Drawable;)Z
Landroidx/appcompat/widget/ActionBarContextView;
HSPLandroidx/appcompat/widget/ActionBarContextView;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/ActionBarContextView;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
PLandroidx/appcompat/widget/ActionBarContextView;->onDetachedFromWindow()V
Landroidx/appcompat/widget/ActionBarOverlayLayout$a;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$a;-><init>(Landroidx/appcompat/widget/ActionBarOverlayLayout;)V
Landroidx/appcompat/widget/ActionBarOverlayLayout$b;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$b;-><init>(Landroidx/appcompat/widget/ActionBarOverlayLayout;)V
Landroidx/appcompat/widget/ActionBarOverlayLayout$c;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$c;-><init>(Landroidx/appcompat/widget/ActionBarOverlayLayout;)V
Landroidx/appcompat/widget/ActionBarOverlayLayout$d;
Landroidx/appcompat/widget/ActionBarOverlayLayout$e;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$e;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
Landroidx/appcompat/widget/ActionBarOverlayLayout;
Landroidx/core/view/o;
Landroidx/core/view/p;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;-><clinit>()V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->h(Landroid/view/View;Landroid/graphics/Rect;ZZZZ)Z
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->generateLayoutParams(Landroid/util/AttributeSet;)Landroid/view/ViewGroup$LayoutParams;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->k(Landroid/util/AttributeSet;)Landroidx/appcompat/widget/ActionBarOverlayLayout$e;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->l(Landroid/view/View;)Landroidx/appcompat/widget/u;
PLandroidx/appcompat/widget/ActionBarOverlayLayout;->m()V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->n(Landroid/content/Context;)V
PLandroidx/appcompat/widget/ActionBarOverlayLayout;->onDetachedFromWindow()V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onLayout(ZIIII)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onMeasure(II)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onStartNestedScroll(Landroid/view/View;Landroid/view/View;I)Z
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->f(Landroid/view/View;Landroid/view/View;II)Z
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onWindowVisibilityChanged(I)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->q()V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setActionBarVisibilityCallback(Landroidx/appcompat/widget/ActionBarOverlayLayout$d;)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setHasNonEmbeddedTabs(Z)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setWindowCallback(Landroid/view/Window$Callback;)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setWindowTitle(Ljava/lang/CharSequence;)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->shouldDelayChildPressedState()Z
Landroidx/appcompat/widget/c$d$a;
Landroidx/appcompat/widget/y;
HSPLandroidx/appcompat/widget/c$d$a;-><init>(Landroidx/appcompat/widget/c$d;Landroid/view/View;Landroidx/appcompat/widget/c;)V
Landroidx/appcompat/widget/c$d;
Landroidx/appcompat/widget/l;
Landroidx/appcompat/widget/ActionMenuView$a;
HSPLandroidx/appcompat/widget/c$d;-><init>(Landroidx/appcompat/widget/c;Landroid/content/Context;)V
Landroidx/appcompat/widget/c$f;
HSPLandroidx/appcompat/widget/c$f;-><init>(Landroidx/appcompat/widget/c;)V
Landroidx/appcompat/widget/c;
HSPLandroidx/appcompat/widget/c;-><init>(Landroid/content/Context;)V
PLandroidx/appcompat/widget/c;->w()Z
HSPLandroidx/appcompat/widget/c;->h()Z
PLandroidx/appcompat/widget/c;->z()Z
PLandroidx/appcompat/widget/c;->A()Z
HSPLandroidx/appcompat/widget/c;->k(Landroid/content/Context;Landroidx/appcompat/view/menu/d;)V
PLandroidx/appcompat/widget/c;->a(Landroidx/appcompat/view/menu/d;Z)V
HSPLandroidx/appcompat/widget/c;->D(Z)V
HSPLandroidx/appcompat/widget/c;->E(Landroidx/appcompat/widget/ActionMenuView;)V
HSPLandroidx/appcompat/widget/c;->j(Z)V
Landroidx/appcompat/widget/ActionMenuView$e;
Landroidx/appcompat/widget/ActionMenuView;
Landroidx/appcompat/widget/z;
HSPLandroidx/appcompat/widget/ActionMenuView;-><init>(Landroid/content/Context;)V
HSPLandroidx/appcompat/widget/ActionMenuView;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
PLandroidx/appcompat/widget/ActionMenuView;->w()V
HSPLandroidx/appcompat/widget/ActionMenuView;->C(Landroidx/appcompat/view/menu/d;)V
PLandroidx/appcompat/widget/ActionMenuView;->onDetachedFromWindow()V
HSPLandroidx/appcompat/widget/ActionMenuView;->onLayout(ZIIII)V
HSPLandroidx/appcompat/widget/ActionMenuView;->onMeasure(II)V
HSPLandroidx/appcompat/widget/ActionMenuView;->G()Landroidx/appcompat/view/menu/d;
HSPLandroidx/appcompat/widget/ActionMenuView;->H(Landroidx/appcompat/view/menu/h$a;Landroidx/appcompat/view/menu/d$a;)V
HSPLandroidx/appcompat/widget/ActionMenuView;->setOnMenuItemClickListener(Landroidx/appcompat/widget/ActionMenuView$e;)V
HSPLandroidx/appcompat/widget/ActionMenuView;->setOverflowReserved(Z)V
HSPLandroidx/appcompat/widget/ActionMenuView;->setPopupTheme(I)V
HSPLandroidx/appcompat/widget/ActionMenuView;->setPresenter(Landroidx/appcompat/widget/c;)V
Landroidx/appcompat/widget/e;
HSPLandroidx/appcompat/widget/e;-><init>(Landroid/view/View;)V
HSPLandroidx/appcompat/widget/e;->b()V
HSPLandroidx/appcompat/widget/e;->e(Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/e;->k()Z
Landroidx/appcompat/widget/f$a;
Landroidx/appcompat/widget/D$f;
HSPLandroidx/appcompat/widget/f$a;-><init>()V
HSPLandroidx/appcompat/widget/f$a;->f([II)Z
HSPLandroidx/appcompat/widget/f$a;->a(Landroidx/appcompat/widget/D;Landroid/content/Context;I)Landroid/graphics/drawable/Drawable;
HSPLandroidx/appcompat/widget/f$a;->b(Landroid/content/Context;I)Landroid/content/res/ColorStateList;
HSPLandroidx/appcompat/widget/f$a;->d(Landroid/content/Context;ILandroid/graphics/drawable/Drawable;)Z
HSPLandroidx/appcompat/widget/f$a;->c(Landroid/content/Context;ILandroid/graphics/drawable/Drawable;)Z
Landroidx/appcompat/widget/f;
HSPLandroidx/appcompat/widget/f;-><clinit>()V
HSPLandroidx/appcompat/widget/f;-><init>()V
HSPLandroidx/appcompat/widget/f;->a()Landroid/graphics/PorterDuff$Mode;
HSPLandroidx/appcompat/widget/f;->b()Landroidx/appcompat/widget/f;
HSPLandroidx/appcompat/widget/f;->e(Landroid/content/Context;I)Landroid/content/res/ColorStateList;
HSPLandroidx/appcompat/widget/f;->f()V
Landroidx/appcompat/widget/g;
HSPLandroidx/appcompat/widget/g;-><init>(Landroid/widget/EditText;)V
HSPLandroidx/appcompat/widget/g;->a(Landroid/text/method/KeyListener;)Landroid/text/method/KeyListener;
HSPLandroidx/appcompat/widget/g;->c(Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/g;->e(Z)V
Landroidx/appcompat/widget/h;
HSPLandroidx/appcompat/widget/h;-><init>(Landroid/widget/TextView;)V
HSPLandroidx/appcompat/widget/h;->a([Landroid/text/InputFilter;)[Landroid/text/InputFilter;
HSPLandroidx/appcompat/widget/h;->b(Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/h;->d(Z)V
Landroidx/appcompat/widget/j;
HSPLandroidx/appcompat/widget/j;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/j;->setBackgroundDrawable(Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/j;->setImageDrawable(Landroid/graphics/drawable/Drawable;)V
Landroidx/appcompat/widget/k;
HSPLandroidx/appcompat/widget/k;-><init>(Landroid/widget/ImageView;)V
HSPLandroidx/appcompat/widget/k;->b()V
HSPLandroidx/appcompat/widget/k;->c()V
HSPLandroidx/appcompat/widget/k;->g(Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/l;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/l;->setBackgroundDrawable(Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/l;->setImageDrawable(Landroid/graphics/drawable/Drawable;)V
Landroidx/appcompat/widget/n;
HSPLandroidx/appcompat/widget/n;-><init>(Landroid/widget/TextView;)V
Landroidx/appcompat/widget/o$a;
Landroidx/core/content/res/h$e;
HSPLandroidx/appcompat/widget/o$a;-><init>(Landroidx/appcompat/widget/o;IILjava/lang/ref/WeakReference;)V
HSPLandroidx/appcompat/widget/o$a;->f(I)V
Landroidx/appcompat/widget/o;
HSPLandroidx/appcompat/widget/o;-><init>(Landroid/widget/TextView;)V
HSPLandroidx/appcompat/widget/o;->b()V
HSPLandroidx/appcompat/widget/o;->d(Landroid/content/Context;Landroidx/appcompat/widget/f;I)Landroidx/appcompat/widget/J;
HSPLandroidx/appcompat/widget/o;->m(Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/o;->o(ZIIII)V
HSPLandroidx/appcompat/widget/o;->q(Landroid/content/Context;I)V
HSPLandroidx/appcompat/widget/o;->y(Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/o;->C(Landroid/content/Context;Landroidx/appcompat/widget/L;)V
Landroidx/appcompat/widget/p;
Landroidx/core/widget/h;
HSPLandroidx/appcompat/widget/p;-><init>(Landroid/content/Context;)V
HSPLandroidx/appcompat/widget/p;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/p;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/p;->r()V
HSPLandroidx/appcompat/widget/p;->drawableStateChanged()V
HSPLandroidx/appcompat/widget/p;->getEmojiTextViewHelper()Landroidx/appcompat/widget/h;
HSPLandroidx/appcompat/widget/p;->getText()Ljava/lang/CharSequence;
HSPLandroidx/appcompat/widget/p;->onLayout(ZIIII)V
HSPLandroidx/appcompat/widget/p;->onMeasure(II)V
HSPLandroidx/appcompat/widget/p;->onTextChanged(Ljava/lang/CharSequence;III)V
HSPLandroidx/appcompat/widget/p;->setCompoundDrawables(Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/p;->setCompoundDrawablesWithIntrinsicBounds(Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/p;->setFilters([Landroid/text/InputFilter;)V
HSPLandroidx/appcompat/widget/p;->setTextAppearance(Landroid/content/Context;I)V
HSPLandroidx/appcompat/widget/p;->setTypeface(Landroid/graphics/Typeface;I)V
Landroidx/appcompat/widget/q$b;
Landroidx/appcompat/widget/q$d;
HSPLandroidx/appcompat/widget/q$b;-><init>()V
Landroidx/appcompat/widget/q$c;
HSPLandroidx/appcompat/widget/q$c;-><init>()V
HSPLandroidx/appcompat/widget/q$d;-><init>()V
Landroidx/appcompat/widget/q;
HSPLandroidx/appcompat/widget/q;-><clinit>()V
HSPLandroidx/appcompat/widget/q;-><init>(Landroid/widget/TextView;)V
HSPLandroidx/appcompat/widget/q;->j()I
HSPLandroidx/appcompat/widget/q;->o(Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/q;->y()Z
Landroidx/appcompat/widget/ContentFrameLayout$a;
Landroidx/appcompat/widget/ContentFrameLayout;
HSPLandroidx/appcompat/widget/ContentFrameLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/ContentFrameLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/ContentFrameLayout;->getMinWidthMajor()Landroid/util/TypedValue;
HSPLandroidx/appcompat/widget/ContentFrameLayout;->getMinWidthMinor()Landroid/util/TypedValue;
HSPLandroidx/appcompat/widget/ContentFrameLayout;->onAttachedToWindow()V
PLandroidx/appcompat/widget/ContentFrameLayout;->onDetachedFromWindow()V
HSPLandroidx/appcompat/widget/ContentFrameLayout;->onMeasure(II)V
HSPLandroidx/appcompat/widget/ContentFrameLayout;->setAttachListener(Landroidx/appcompat/widget/ContentFrameLayout$a;)V
Landroidx/appcompat/widget/u;
Landroidx/appcompat/widget/v;
HSPLandroidx/appcompat/widget/y;-><init>(Landroid/view/View;)V
HSPLandroidx/appcompat/widget/z;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/z;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/z;->getVirtualChildCount()I
HSPLandroidx/appcompat/widget/z;->p(IIII)V
HSPLandroidx/appcompat/widget/z;->s(II)V
HSPLandroidx/appcompat/widget/z;->onInitializeAccessibilityNodeInfo(Landroid/view/accessibility/AccessibilityNodeInfo;)V
HSPLandroidx/appcompat/widget/z;->onLayout(ZIIII)V
HSPLandroidx/appcompat/widget/z;->onMeasure(II)V
HSPLandroidx/appcompat/widget/z;->setBaselineAligned(Z)V
HSPLandroidx/appcompat/widget/z;->setDividerDrawable(Landroid/graphics/drawable/Drawable;)V
Landroidx/appcompat/widget/D$c;
Lp/h;
Landroidx/appcompat/widget/D;
Landroidx/appcompat/widget/E;
Landroidx/appcompat/widget/F;
HSPLandroidx/appcompat/widget/F;-><init>()V
HSPLandroidx/appcompat/widget/F;->a()I
HSPLandroidx/appcompat/widget/F;->d()I
HSPLandroidx/appcompat/widget/F;->e(II)V
HSPLandroidx/appcompat/widget/F;->f(Z)V
HSPLandroidx/appcompat/widget/F;->g(II)V
Landroidx/appcompat/widget/H;
HSPLandroidx/appcompat/widget/H;-><clinit>()V
HSPLandroidx/appcompat/widget/H;->a(Landroid/view/View;Landroid/content/Context;)V
Landroidx/appcompat/widget/I;
HSPLandroidx/appcompat/widget/I;-><clinit>()V
HSPLandroidx/appcompat/widget/I;->a(Landroid/content/Context;)Z
HSPLandroidx/appcompat/widget/I;->b(Landroid/content/Context;)Landroid/content/Context;
Landroidx/appcompat/widget/K;
Landroidx/appcompat/widget/L;
HSPLandroidx/appcompat/widget/L;-><init>(Landroid/content/Context;Landroid/content/res/TypedArray;)V
HSPLandroidx/appcompat/widget/L;->a(IZ)Z
HSPLandroidx/appcompat/widget/L;->b(II)I
HSPLandroidx/appcompat/widget/L;->c(I)Landroid/content/res/ColorStateList;
HSPLandroidx/appcompat/widget/L;->d(II)I
HSPLandroidx/appcompat/widget/L;->e(II)I
HSPLandroidx/appcompat/widget/L;->f(I)Landroid/graphics/drawable/Drawable;
HSPLandroidx/appcompat/widget/L;->g(IF)F
HSPLandroidx/appcompat/widget/L;->h(IILandroidx/core/content/res/h$e;)Landroid/graphics/Typeface;
HSPLandroidx/appcompat/widget/L;->i(II)I
HSPLandroidx/appcompat/widget/L;->j(II)I
HSPLandroidx/appcompat/widget/L;->k(II)I
HSPLandroidx/appcompat/widget/L;->l(II)I
HSPLandroidx/appcompat/widget/L;->m(I)Ljava/lang/String;
HSPLandroidx/appcompat/widget/L;->n(I)Ljava/lang/CharSequence;
HSPLandroidx/appcompat/widget/L;->o()Landroid/content/res/TypedArray;
HSPLandroidx/appcompat/widget/L;->p(I)Z
HSPLandroidx/appcompat/widget/L;->q(Landroid/content/Context;I[I)Landroidx/appcompat/widget/L;
HSPLandroidx/appcompat/widget/L;->r(Landroid/content/Context;Landroid/util/AttributeSet;[I)Landroidx/appcompat/widget/L;
HSPLandroidx/appcompat/widget/L;->s(Landroid/content/Context;Landroid/util/AttributeSet;[III)Landroidx/appcompat/widget/L;
HSPLandroidx/appcompat/widget/L;->u()V
Landroidx/appcompat/widget/Toolbar$a;
HSPLandroidx/appcompat/widget/Toolbar$a;-><init>(Landroidx/appcompat/widget/Toolbar;)V
Landroidx/appcompat/widget/Toolbar$b;
HSPLandroidx/appcompat/widget/Toolbar$b;-><init>(Landroidx/appcompat/widget/Toolbar;)V
Landroidx/appcompat/widget/Toolbar$f;
HSPLandroidx/appcompat/widget/Toolbar$f;-><init>(Landroidx/appcompat/widget/Toolbar;)V
HSPLandroidx/appcompat/widget/Toolbar$f;->h()Z
HSPLandroidx/appcompat/widget/Toolbar$f;->k(Landroid/content/Context;Landroidx/appcompat/view/menu/d;)V
PLandroidx/appcompat/widget/Toolbar$f;->a(Landroidx/appcompat/view/menu/d;Z)V
HSPLandroidx/appcompat/widget/Toolbar$f;->j(Z)V
Landroidx/appcompat/widget/Toolbar$g;
HSPLandroidx/appcompat/widget/Toolbar$g;-><init>(II)V
Landroidx/appcompat/widget/Toolbar;
HSPLandroidx/appcompat/widget/Toolbar;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/Toolbar;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/Toolbar;->b(Ljava/util/List;I)V
HSPLandroidx/appcompat/widget/Toolbar;->c(Landroid/view/View;Z)V
HSPLandroidx/appcompat/widget/Toolbar;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
HSPLandroidx/appcompat/widget/Toolbar;->f()V
HSPLandroidx/appcompat/widget/Toolbar;->i()V
HSPLandroidx/appcompat/widget/Toolbar;->j()V
HSPLandroidx/appcompat/widget/Toolbar;->k()Landroidx/appcompat/widget/Toolbar$g;
HSPLandroidx/appcompat/widget/Toolbar;->o(Landroid/view/View;I)I
HSPLandroidx/appcompat/widget/Toolbar;->p(I)I
HSPLandroidx/appcompat/widget/Toolbar;->getContentInsetEnd()I
HSPLandroidx/appcompat/widget/Toolbar;->getContentInsetStart()I
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetEnd()I
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetLeft()I
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetRight()I
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetStart()I
HSPLandroidx/appcompat/widget/Toolbar;->q(Landroid/view/View;)I
HSPLandroidx/appcompat/widget/Toolbar;->getNavigationContentDescription()Ljava/lang/CharSequence;
HSPLandroidx/appcompat/widget/Toolbar;->getNavigationIcon()Landroid/graphics/drawable/Drawable;
HSPLandroidx/appcompat/widget/Toolbar;->getSubtitle()Ljava/lang/CharSequence;
HSPLandroidx/appcompat/widget/Toolbar;->getTitle()Ljava/lang/CharSequence;
HSPLandroidx/appcompat/widget/Toolbar;->r(Landroid/view/View;)I
HSPLandroidx/appcompat/widget/Toolbar;->s(Ljava/util/List;[I)I
HSPLandroidx/appcompat/widget/Toolbar;->getWrapper()Landroidx/appcompat/widget/u;
PLandroidx/appcompat/widget/Toolbar;->t()Z
HSPLandroidx/appcompat/widget/Toolbar;->w(Landroid/view/View;)Z
HSPLandroidx/appcompat/widget/Toolbar;->z(Landroid/view/View;I[II)I
HSPLandroidx/appcompat/widget/Toolbar;->A(Landroid/view/View;IIII[I)I
HSPLandroidx/appcompat/widget/Toolbar;->B(Landroid/view/View;IIIII)V
PLandroidx/appcompat/widget/Toolbar;->onDetachedFromWindow()V
HSPLandroidx/appcompat/widget/Toolbar;->onLayout(ZIIII)V
HSPLandroidx/appcompat/widget/Toolbar;->onMeasure(II)V
HSPLandroidx/appcompat/widget/Toolbar;->onRtlPropertiesChanged(I)V
HSPLandroidx/appcompat/widget/Toolbar;->setCollapsible(Z)V
HSPLandroidx/appcompat/widget/Toolbar;->F(II)V
HSPLandroidx/appcompat/widget/Toolbar;->setNavigationContentDescription(Ljava/lang/CharSequence;)V
HSPLandroidx/appcompat/widget/Toolbar;->setNavigationIcon(Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/Toolbar;->setNavigationOnClickListener(Landroid/view/View$OnClickListener;)V
HSPLandroidx/appcompat/widget/Toolbar;->setPopupTheme(I)V
HSPLandroidx/appcompat/widget/Toolbar;->setSubtitle(Ljava/lang/CharSequence;)V
HSPLandroidx/appcompat/widget/Toolbar;->G(Landroid/content/Context;I)V
HSPLandroidx/appcompat/widget/Toolbar;->setTitle(Ljava/lang/CharSequence;)V
HSPLandroidx/appcompat/widget/Toolbar;->H(Landroid/content/Context;I)V
HSPLandroidx/appcompat/widget/Toolbar;->I()Z
HSPLandroidx/appcompat/widget/Toolbar;->J(Landroid/view/View;)Z
Landroidx/appcompat/widget/P$a;
HSPLandroidx/appcompat/widget/P$a;-><init>(Landroidx/appcompat/widget/P;)V
Landroidx/appcompat/widget/P;
HSPLandroidx/appcompat/widget/P;-><init>(Landroidx/appcompat/widget/Toolbar;Z)V
HSPLandroidx/appcompat/widget/P;-><init>(Landroidx/appcompat/widget/Toolbar;ZII)V
HSPLandroidx/appcompat/widget/P;->e()Landroid/content/Context;
HSPLandroidx/appcompat/widget/P;->g(I)V
HSPLandroidx/appcompat/widget/P;->h(I)V
HSPLandroidx/appcompat/widget/P;->l(Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/P;->o(Ljava/lang/CharSequence;)V
HSPLandroidx/appcompat/widget/P;->c(Landroid/view/Window$Callback;)V
HSPLandroidx/appcompat/widget/P;->b(Ljava/lang/CharSequence;)V
HSPLandroidx/appcompat/widget/P;->q()V
Landroidx/appcompat/widget/Q;
HSPLandroidx/appcompat/widget/Q;->a(Landroid/view/View;Ljava/lang/CharSequence;)V
Landroidx/appcompat/widget/W;
HSPLandroidx/appcompat/widget/W;-><clinit>()V
HSPLandroidx/appcompat/widget/W;->b()Z
HSPLandroidx/appcompat/widget/W;->c()Z
Landroidx/appcompat/widget/X;
HSPLandroidx/appcompat/widget/X;-><clinit>()V
HSPLandroidx/appcompat/widget/X;->a(Landroid/view/View;)Z
LE/b;
LE/c;
HSPLE/c;-><clinit>()V
Landroidx/fragment/app/a;
Landroidx/fragment/app/y;
Landroidx/fragment/app/q$h;
HSPLandroidx/fragment/app/a;-><init>(Landroidx/fragment/app/q;)V
HSPLandroidx/fragment/app/a;->i(I)V
HSPLandroidx/fragment/app/a;->g(ILandroidx/fragment/app/Fragment;Ljava/lang/String;I)V
HSPLandroidx/fragment/app/a;->k()V
HSPLandroidx/fragment/app/a;->m(Ljava/util/ArrayList;Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/a;->a(Ljava/util/ArrayList;Ljava/util/ArrayList;)Z
HSPLandroidx/fragment/app/a;->n()V
Landroidx/fragment/app/d;
Landroidx/fragment/app/E;
HSPLandroidx/fragment/app/d;-><init>(Landroid/view/ViewGroup;)V
Landroidx/fragment/app/Fragment$a;
HSPLandroidx/fragment/app/Fragment$a;-><init>(Landroidx/fragment/app/Fragment;)V
Landroidx/fragment/app/Fragment$c;
Landroidx/fragment/app/g;
HSPLandroidx/fragment/app/Fragment$c;-><init>(Landroidx/fragment/app/Fragment;)V
Landroidx/fragment/app/Fragment$d;
HSPLandroidx/fragment/app/Fragment$d;-><init>()V
Landroidx/fragment/app/Fragment$e;
PLandroidx/fragment/app/Fragment$e;->a(Landroid/view/View;)V
Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/Fragment;-><clinit>()V
HSPLandroidx/fragment/app/Fragment;-><init>()V
HSPLandroidx/fragment/app/Fragment;->h()Landroidx/fragment/app/g;
HSPLandroidx/fragment/app/Fragment;->i()Landroidx/fragment/app/Fragment$d;
HSPLandroidx/fragment/app/Fragment;->equals(Ljava/lang/Object;)Z
HSPLandroidx/fragment/app/Fragment;->j()Landroidx/fragment/app/e;
HSPLandroidx/fragment/app/Fragment;->n()Landroidx/fragment/app/q;
HSPLandroidx/fragment/app/Fragment;->o()Landroid/content/Context;
HSPLandroidx/fragment/app/Fragment;->v()Landroid/view/View;
HSPLandroidx/fragment/app/Fragment;->w(Landroid/os/Bundle;)Landroid/view/LayoutInflater;
HSPLandroidx/fragment/app/Fragment;->g()Landroidx/lifecycle/g;
HSPLandroidx/fragment/app/Fragment;->x()I
HSPLandroidx/fragment/app/Fragment;->z()Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/Fragment;->A()Landroidx/fragment/app/q;
HSPLandroidx/fragment/app/Fragment;->E()F
HSPLandroidx/fragment/app/Fragment;->b()Landroidx/savedstate/a;
HSPLandroidx/fragment/app/Fragment;->M()Landroid/view/View;
HSPLandroidx/fragment/app/Fragment;->c()Landroidx/lifecycle/D;
HSPLandroidx/fragment/app/Fragment;->N()V
PLandroidx/fragment/app/Fragment;->O()V
HSPLandroidx/fragment/app/Fragment;->P()Z
HSPLandroidx/fragment/app/Fragment;->S()Z
HSPLandroidx/fragment/app/Fragment;->U(Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->V(Landroid/os/Bundle;)V
PLandroidx/fragment/app/Fragment;->b0()V
PLandroidx/fragment/app/Fragment;->c0()V
HSPLandroidx/fragment/app/Fragment;->d0(Landroid/os/Bundle;)Landroid/view/LayoutInflater;
HSPLandroidx/fragment/app/Fragment;->f0(Landroid/content/Context;Landroid/util/AttributeSet;Landroid/os/Bundle;)V
PLandroidx/fragment/app/Fragment;->h0()V
HSPLandroidx/fragment/app/Fragment;->j0(Z)V
HSPLandroidx/fragment/app/Fragment;->k0()V
HSPLandroidx/fragment/app/Fragment;->m0()V
PLandroidx/fragment/app/Fragment;->n0()V
HSPLandroidx/fragment/app/Fragment;->o0(Landroid/view/View;Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->p0(Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->q0(Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->r0()V
HSPLandroidx/fragment/app/Fragment;->t0(Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->u0(Landroid/view/Menu;Landroid/view/MenuInflater;)Z
HSPLandroidx/fragment/app/Fragment;->v0(Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)V
PLandroidx/fragment/app/Fragment;->w0()V
PLandroidx/fragment/app/Fragment;->x0()V
HSPLandroidx/fragment/app/Fragment;->y0(Landroid/os/Bundle;)Landroid/view/LayoutInflater;
PLandroidx/fragment/app/Fragment;->B0()V
HSPLandroidx/fragment/app/Fragment;->C0(Landroid/view/Menu;)Z
HSPLandroidx/fragment/app/Fragment;->D0()V
HSPLandroidx/fragment/app/Fragment;->E0()V
HSPLandroidx/fragment/app/Fragment;->G0()V
PLandroidx/fragment/app/Fragment;->H0()V
HSPLandroidx/fragment/app/Fragment;->I0()V
HSPLandroidx/fragment/app/Fragment;->L0()Landroid/content/Context;
HSPLandroidx/fragment/app/Fragment;->M0()Landroid/view/View;
HSPLandroidx/fragment/app/Fragment;->N0(Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->O0()V
HSPLandroidx/fragment/app/Fragment;->P0(Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->Q0(IIII)V
HSPLandroidx/fragment/app/Fragment;->R0(Landroid/view/View;)V
HSPLandroidx/fragment/app/Fragment;->S0(I)V
HSPLandroidx/fragment/app/Fragment;->T0(Z)V
HSPLandroidx/fragment/app/Fragment;->U0(F)V
HSPLandroidx/fragment/app/Fragment;->V0(Ljava/util/ArrayList;Ljava/util/ArrayList;)V
HSPLandroidx/fragment/app/Fragment;->toString()Ljava/lang/String;
Landroidx/fragment/app/e;
HSPLandroidx/fragment/app/g;-><init>()V
Landroidx/fragment/app/h;
PLandroidx/fragment/app/h;->a(Landroid/view/View;)V
HSPLandroidx/fragment/app/h;->addView(Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;)V
HSPLandroidx/fragment/app/h;->dispatchDraw(Landroid/graphics/Canvas;)V
HSPLandroidx/fragment/app/h;->drawChild(Landroid/graphics/Canvas;Landroid/view/View;J)Z
PLandroidx/fragment/app/h;->removeView(Landroid/view/View;)V
Landroidx/fragment/app/i;
HSPLandroidx/fragment/app/i;-><clinit>()V
HSPLandroidx/fragment/app/i;-><init>()V
HSPLandroidx/fragment/app/i;->b(Ljava/lang/ClassLoader;Ljava/lang/String;)Z
HSPLandroidx/fragment/app/i;->c(Ljava/lang/ClassLoader;Ljava/lang/String;)Ljava/lang/Class;
Landroidx/fragment/app/j;
Landroidx/fragment/app/k;
HSPLandroidx/fragment/app/k;-><init>(Landroidx/fragment/app/q;)V
HSPLandroidx/fragment/app/k;->onCreateView(Landroid/view/View;Ljava/lang/String;Landroid/content/Context;Landroid/util/AttributeSet;)Landroid/view/View;
Landroidx/fragment/app/l;
HSPLandroidx/fragment/app/l;-><init>(Landroidx/fragment/app/q;)V
HSPLandroidx/fragment/app/l;->a(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Z)V
HSPLandroidx/fragment/app/l;->b(Landroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/l;->c(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Z)V
PLandroidx/fragment/app/l;->d(Landroidx/fragment/app/Fragment;Z)V
PLandroidx/fragment/app/l;->e(Landroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/l;->f(Landroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/l;->g(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Z)V
HSPLandroidx/fragment/app/l;->h(Landroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/l;->j(Landroidx/fragment/app/Fragment;Z)V
PLandroidx/fragment/app/l;->k(Landroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/l;->l(Landroidx/fragment/app/Fragment;Landroid/view/View;Landroid/os/Bundle;Z)V
PLandroidx/fragment/app/l;->m(Landroidx/fragment/app/Fragment;Z)V
Landroidx/fragment/app/q$a;
HSPLandroidx/fragment/app/q$a;-><init>(Landroidx/fragment/app/q;Z)V
Landroidx/fragment/app/q$b;
Landroidx/core/view/l;
HSPLandroidx/fragment/app/q$b;-><init>(Landroidx/fragment/app/q;)V
Landroidx/fragment/app/q$c;
HSPLandroidx/fragment/app/q$c;-><init>(Landroidx/fragment/app/q;)V
Landroidx/fragment/app/q$d;
Landroidx/fragment/app/F;
HSPLandroidx/fragment/app/q$d;-><init>(Landroidx/fragment/app/q;)V
Landroidx/fragment/app/FragmentManager$6;
Landroidx/fragment/app/q$f;
Landroidx/fragment/app/u;
Landroidx/fragment/app/q;
HSPLandroidx/fragment/app/q;-><clinit>()V
HSPLandroidx/fragment/app/q;-><init>()V
HSPLandroidx/fragment/app/q;->h(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/w;
HSPLandroidx/fragment/app/q;->i(Landroidx/fragment/app/u;)V
HSPLandroidx/fragment/app/q;->j(Landroidx/fragment/app/j;Landroidx/fragment/app/g;Landroidx/fragment/app/Fragment;)V
HSPLandroidx/fragment/app/q;->l()Landroidx/fragment/app/y;
HSPLandroidx/fragment/app/q;->m()Z
HSPLandroidx/fragment/app/q;->n()V
PLandroidx/fragment/app/q;->o()V
HSPLandroidx/fragment/app/q;->p()Ljava/util/Set;
HSPLandroidx/fragment/app/q;->q(Ljava/util/ArrayList;II)Ljava/util/Set;
HSPLandroidx/fragment/app/q;->r(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/w;
HSPLandroidx/fragment/app/q;->t()V
HSPLandroidx/fragment/app/q;->v()V
HSPLandroidx/fragment/app/q;->w(Landroid/view/Menu;Landroid/view/MenuInflater;)Z
PLandroidx/fragment/app/q;->x()V
PLandroidx/fragment/app/q;->y()V
HSPLandroidx/fragment/app/q;->C(Landroidx/fragment/app/Fragment;)V
PLandroidx/fragment/app/q;->D()V
HSPLandroidx/fragment/app/q;->E(Landroid/view/Menu;)Z
HSPLandroidx/fragment/app/q;->F()V
HSPLandroidx/fragment/app/q;->G()V
HSPLandroidx/fragment/app/q;->H()V
HSPLandroidx/fragment/app/q;->I(I)V
PLandroidx/fragment/app/q;->J()V
HSPLandroidx/fragment/app/q;->K()V
HSPLandroidx/fragment/app/q;->L()V
PLandroidx/fragment/app/q;->M()V
HSPLandroidx/fragment/app/q;->N(Z)V
HSPLandroidx/fragment/app/q;->O(Z)Z
HSPLandroidx/fragment/app/q;->Q(Ljava/util/ArrayList;Ljava/util/ArrayList;II)V
HSPLandroidx/fragment/app/q;->R(Ljava/util/ArrayList;Ljava/util/ArrayList;II)V
HSPLandroidx/fragment/app/q;->S(Ljava/lang/String;)Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/q;->T(I)Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/q;->Y(Ljava/util/ArrayList;Ljava/util/ArrayList;)Z
HSPLandroidx/fragment/app/q;->Z()I
HSPLandroidx/fragment/app/q;->a0(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/t;
HSPLandroidx/fragment/app/q;->b0()Landroidx/fragment/app/g;
HSPLandroidx/fragment/app/q;->c0(Landroidx/fragment/app/Fragment;)Landroid/view/ViewGroup;
HSPLandroidx/fragment/app/q;->d0()Landroidx/fragment/app/i;
HSPLandroidx/fragment/app/q;->e0()Landroidx/fragment/app/j;
HSPLandroidx/fragment/app/q;->f0()Landroidx/fragment/app/l;
HSPLandroidx/fragment/app/q;->g0()Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/q;->h0()Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/q;->i0()Landroidx/fragment/app/F;
HSPLandroidx/fragment/app/q;->j0()LF/c$c;
HSPLandroidx/fragment/app/q;->k0(Landroid/view/View;)Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/q;->l0(Landroidx/fragment/app/Fragment;)Landroidx/lifecycle/D;
PLandroidx/fragment/app/q;->o0()Z
HSPLandroidx/fragment/app/q;->p0(I)Z
HSPLandroidx/fragment/app/q;->q0(Landroidx/fragment/app/Fragment;)Z
HSPLandroidx/fragment/app/q;->t0(Landroidx/fragment/app/Fragment;)Z
HSPLandroidx/fragment/app/q;->u0(Landroidx/fragment/app/Fragment;)Z
HSPLandroidx/fragment/app/q;->v0(I)Z
HSPLandroidx/fragment/app/q;->w0()Z
HSPLandroidx/fragment/app/q;->x0(IZ)V
HSPLandroidx/fragment/app/q;->y0()V
HSPLandroidx/fragment/app/q;->A0(Landroidx/fragment/app/w;)V
HSPLandroidx/fragment/app/q;->C0(Ljava/util/ArrayList;Ljava/util/ArrayList;)V
HSPLandroidx/fragment/app/q;->H0(Landroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/q;->J0(Landroidx/fragment/app/Fragment;)V
HSPLandroidx/fragment/app/q;->M0()V
HSPLandroidx/fragment/app/q;->N0()V
Landroidx/fragment/app/r;
HSPLandroidx/fragment/app/r;-><init>()V
Landroidx/fragment/app/t$a;
Landroidx/lifecycle/A$b;
HSPLandroidx/fragment/app/t$a;-><init>()V
HSPLandroidx/fragment/app/t$a;->b(Ljava/lang/Class;)Landroidx/lifecycle/z;
Landroidx/fragment/app/t;
Landroidx/lifecycle/z;
HSPLandroidx/fragment/app/t;-><clinit>()V
HSPLandroidx/fragment/app/t;-><init>(Z)V
PLandroidx/fragment/app/t;->f(Landroidx/fragment/app/Fragment;)V
PLandroidx/fragment/app/t;->g(Ljava/lang/String;)V
HSPLandroidx/fragment/app/t;->i(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/t;
HSPLandroidx/fragment/app/t;->k(Landroidx/fragment/app/Fragment;)Landroidx/lifecycle/D;
PLandroidx/fragment/app/t;->d()V
HSPLandroidx/fragment/app/t;->m(Z)V
PLandroidx/fragment/app/t;->n(Landroidx/fragment/app/Fragment;)Z
Landroidx/fragment/app/w$a;
HSPLandroidx/fragment/app/w$a;-><init>(Landroidx/fragment/app/w;Landroid/view/View;)V
HSPLandroidx/fragment/app/w$a;->onViewAttachedToWindow(Landroid/view/View;)V
Landroidx/fragment/app/w$b;
HSPLandroidx/fragment/app/w$b;-><clinit>()V
Landroidx/fragment/app/w;
HSPLandroidx/fragment/app/w;-><init>(Landroidx/fragment/app/l;Landroidx/fragment/app/x;Landroidx/fragment/app/Fragment;)V
HSPLandroidx/fragment/app/w;->a()V
HSPLandroidx/fragment/app/w;->b()V
HSPLandroidx/fragment/app/w;->c()V
HSPLandroidx/fragment/app/w;->d()I
HSPLandroidx/fragment/app/w;->e()V
HSPLandroidx/fragment/app/w;->f()V
PLandroidx/fragment/app/w;->g()V
PLandroidx/fragment/app/w;->h()V
PLandroidx/fragment/app/w;->i()V
HSPLandroidx/fragment/app/w;->j()V
HSPLandroidx/fragment/app/w;->k()Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/w;->m()V
PLandroidx/fragment/app/w;->n()V
HSPLandroidx/fragment/app/w;->o()V
PLandroidx/fragment/app/w;->r()V
HSPLandroidx/fragment/app/w;->s(I)V
HSPLandroidx/fragment/app/w;->t()V
PLandroidx/fragment/app/w;->u()V
Landroidx/fragment/app/x;
HSPLandroidx/fragment/app/x;-><init>()V
HSPLandroidx/fragment/app/x;->a(Landroidx/fragment/app/Fragment;)V
HSPLandroidx/fragment/app/x;->b()V
HSPLandroidx/fragment/app/x;->c(Ljava/lang/String;)Z
HSPLandroidx/fragment/app/x;->d(I)V
HSPLandroidx/fragment/app/x;->e(Ljava/lang/String;)Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/x;->f(I)Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/x;->h(Landroidx/fragment/app/Fragment;)I
HSPLandroidx/fragment/app/x;->i()Ljava/util/List;
HSPLandroidx/fragment/app/x;->j()Ljava/util/List;
HSPLandroidx/fragment/app/x;->l(Ljava/lang/String;)Landroidx/fragment/app/w;
HSPLandroidx/fragment/app/x;->m()Ljava/util/List;
PLandroidx/fragment/app/x;->n()Landroidx/fragment/app/t;
HSPLandroidx/fragment/app/x;->p(Landroidx/fragment/app/w;)V
PLandroidx/fragment/app/x;->q(Landroidx/fragment/app/w;)V
HSPLandroidx/fragment/app/x;->r()V
HSPLandroidx/fragment/app/x;->y(Landroidx/fragment/app/t;)V
Landroidx/fragment/app/y$a;
HSPLandroidx/fragment/app/y$a;-><init>(ILandroidx/fragment/app/Fragment;)V
HSPLandroidx/fragment/app/y$a;-><init>(ILandroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/y;-><init>(Landroidx/fragment/app/i;Ljava/lang/ClassLoader;)V
HSPLandroidx/fragment/app/y;->d(Landroidx/fragment/app/y$a;)V
HSPLandroidx/fragment/app/y;->g(ILandroidx/fragment/app/Fragment;Ljava/lang/String;I)V
HSPLandroidx/fragment/app/y;->h(Z)Landroidx/fragment/app/y;
Landroidx/fragment/app/C;
HSPLandroidx/fragment/app/C;-><init>(Landroidx/fragment/app/Fragment;Landroidx/lifecycle/D;)V
HSPLandroidx/fragment/app/C;->g()Landroidx/lifecycle/g;
HSPLandroidx/fragment/app/C;->b()Landroidx/savedstate/a;
HSPLandroidx/fragment/app/C;->d(Landroidx/lifecycle/g$a;)V
HSPLandroidx/fragment/app/C;->e()V
HSPLandroidx/fragment/app/C;->h(Landroid/os/Bundle;)V
PLandroidx/fragment/app/C;->i(Landroid/os/Bundle;)V
Landroidx/fragment/app/E$a;
HSPLandroidx/fragment/app/E$a;-><init>(Landroidx/fragment/app/E;Landroidx/fragment/app/E$d;)V
HSPLandroidx/fragment/app/E$a;->run()V
Landroidx/fragment/app/E$b;
HSPLandroidx/fragment/app/E$b;-><init>(Landroidx/fragment/app/E;Landroidx/fragment/app/E$d;)V
HSPLandroidx/fragment/app/E$b;->run()V
Landroidx/fragment/app/E$c;
HSPLandroidx/fragment/app/E$c;-><clinit>()V
Landroidx/fragment/app/E$d;
Landroidx/fragment/app/E$e;
HSPLandroidx/fragment/app/E$d;-><init>(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;Landroidx/fragment/app/w;Landroidx/core/os/b;)V
HSPLandroidx/fragment/app/E$d;->c()V
HSPLandroidx/fragment/app/E$d;->l()V
Landroidx/fragment/app/E$e$a;
Landroidx/core/os/b$a;
HSPLandroidx/fragment/app/E$e$a;-><init>(Landroidx/fragment/app/E$e;)V
Landroidx/fragment/app/E$e$b;
HSPLandroidx/fragment/app/E$e$b;-><clinit>()V
HSPLandroidx/fragment/app/E$e$b;-><init>(Ljava/lang/String;I)V
HSPLandroidx/fragment/app/E$e$b;->values()[Landroidx/fragment/app/E$e$b;
Landroidx/fragment/app/E$e$c;
HSPLandroidx/fragment/app/E$e$c;-><clinit>()V
HSPLandroidx/fragment/app/E$e$c;-><init>(Ljava/lang/String;I)V
HSPLandroidx/fragment/app/E$e$c;->a(Landroid/view/View;)V
HSPLandroidx/fragment/app/E$e$c;->b(I)Landroidx/fragment/app/E$e$c;
HSPLandroidx/fragment/app/E$e$c;->values()[Landroidx/fragment/app/E$e$c;
HSPLandroidx/fragment/app/E$e;-><init>(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;Landroidx/fragment/app/Fragment;Landroidx/core/os/b;)V
HSPLandroidx/fragment/app/E$e;->a(Ljava/lang/Runnable;)V
HSPLandroidx/fragment/app/E$e;->b()V
HSPLandroidx/fragment/app/E$e;->c()V
HSPLandroidx/fragment/app/E$e;->e()Landroidx/fragment/app/E$e$c;
HSPLandroidx/fragment/app/E$e;->f()Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/E$e;->g()Landroidx/fragment/app/E$e$b;
HSPLandroidx/fragment/app/E$e;->h()Z
HSPLandroidx/fragment/app/E$e;->k(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;)V
HSPLandroidx/fragment/app/E;-><init>(Landroid/view/ViewGroup;)V
HSPLandroidx/fragment/app/E;->a(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;Landroidx/fragment/app/w;)V
HSPLandroidx/fragment/app/E;->b(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/w;)V
PLandroidx/fragment/app/E;->d(Landroidx/fragment/app/w;)V
HSPLandroidx/fragment/app/E;->g()V
HSPLandroidx/fragment/app/E;->h(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/E$e;
HSPLandroidx/fragment/app/E;->i(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/E$e;
HSPLandroidx/fragment/app/E;->j()V
HSPLandroidx/fragment/app/E;->l(Landroidx/fragment/app/w;)Landroidx/fragment/app/E$e$b;
HSPLandroidx/fragment/app/E;->n(Landroid/view/ViewGroup;Landroidx/fragment/app/q;)Landroidx/fragment/app/E;
HSPLandroidx/fragment/app/E;->o(Landroid/view/ViewGroup;Landroidx/fragment/app/F;)Landroidx/fragment/app/E;
HSPLandroidx/fragment/app/E;->p()V
HSPLandroidx/fragment/app/E;->q()V
HSPLandroidx/fragment/app/E;->r(Z)V
LF/c$a;
HSPLF/c$a;->a()[LF/c$a;
HSPLF/c$a;-><clinit>()V
HSPLF/c$a;-><init>(Ljava/lang/String;I)V
LF/c$c$a;
HSPLF/c$c$a;-><init>()V
HSPLF/c$c$a;-><init>(Lt0/d;)V
LF/c$c;
HSPLF/c$c;-><clinit>()V
HSPLF/c$c;-><init>(Ljava/util/Set;LF/c$b;Ljava/util/Map;)V
HSPLF/c$c;->a()Ljava/util/Set;
LF/c;
HSPLF/c;-><clinit>()V
HSPLF/c;-><init>()V
HSPLF/c;->b(Landroidx/fragment/app/Fragment;)LF/c$c;
HSPLF/c;->e(LF/d;)V
LF/d;
HSPLF/d;-><init>(Landroidx/fragment/app/Fragment;Ljava/lang/String;)V
Landroidx/lifecycle/d;
HSPLandroidx/lifecycle/d;-><init>()V
HSPLandroidx/lifecycle/d;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
PLandroidx/lifecycle/d;->onActivityDestroyed(Landroid/app/Activity;)V
PLandroidx/lifecycle/d;->onActivityPaused(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/d;->onActivityResumed(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/d;->onActivityStarted(Landroid/app/Activity;)V
PLandroidx/lifecycle/d;->onActivityStopped(Landroid/app/Activity;)V
Landroidx/lifecycle/i$a;
HSPLandroidx/lifecycle/i$a;-><init>()V
HSPLandroidx/lifecycle/i$a;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
Landroidx/lifecycle/i;
HSPLandroidx/lifecycle/i;-><clinit>()V
HSPLandroidx/lifecycle/i;->a(Landroid/content/Context;)V
Landroidx/lifecycle/m$b;
HSPLandroidx/lifecycle/m$b;-><init>(Landroidx/lifecycle/k;Landroidx/lifecycle/g$b;)V
HSPLandroidx/lifecycle/m$b;->a(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
Landroidx/lifecycle/m;
Landroidx/lifecycle/g;
HSPLandroidx/lifecycle/m;-><init>(Landroidx/lifecycle/l;)V
HSPLandroidx/lifecycle/m;-><init>(Landroidx/lifecycle/l;Z)V
HSPLandroidx/lifecycle/m;->a(Landroidx/lifecycle/k;)V
HPLandroidx/lifecycle/m;->d(Landroidx/lifecycle/l;)V
HSPLandroidx/lifecycle/m;->e(Landroidx/lifecycle/k;)Landroidx/lifecycle/g$b;
HSPLandroidx/lifecycle/m;->f(Ljava/lang/String;)V
HSPLandroidx/lifecycle/m;->g(Landroidx/lifecycle/l;)V
HSPLandroidx/lifecycle/m;->b()Landroidx/lifecycle/g$b;
HSPLandroidx/lifecycle/m;->h(Landroidx/lifecycle/g$a;)V
HSPLandroidx/lifecycle/m;->i()Z
HSPLandroidx/lifecycle/m;->j(Landroidx/lifecycle/g$b;)V
HSPLandroidx/lifecycle/m;->k()V
HSPLandroidx/lifecycle/m;->l(Landroidx/lifecycle/g$b;)V
HSPLandroidx/lifecycle/m;->c(Landroidx/lifecycle/k;)V
HSPLandroidx/lifecycle/m;->m()V
Landroidx/lifecycle/LiveData$a;
HSPLandroidx/lifecycle/LiveData$a;-><init>(Landroidx/lifecycle/LiveData;)V
HSPLandroidx/lifecycle/LiveData$a;->run()V
Landroidx/lifecycle/LiveData$LifecycleBoundObserver;
Landroidx/lifecycle/LiveData$b;
PLandroidx/lifecycle/LiveData$LifecycleBoundObserver;->i()V
HSPLandroidx/lifecycle/LiveData$LifecycleBoundObserver;->h(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
HSPLandroidx/lifecycle/LiveData$LifecycleBoundObserver;->j()Z
HSPLandroidx/lifecycle/LiveData$b;->d(Z)V
HSPLandroidx/lifecycle/LiveData$b;->i()V
Landroidx/lifecycle/LiveData;
HSPLandroidx/lifecycle/LiveData;-><clinit>()V
HSPLandroidx/lifecycle/LiveData;-><init>()V
HSPLandroidx/lifecycle/LiveData;->a(Ljava/lang/String;)V
HSPLandroidx/lifecycle/LiveData;->b(I)V
HSPLandroidx/lifecycle/LiveData;->c(Landroidx/lifecycle/LiveData$b;)V
HSPLandroidx/lifecycle/LiveData;->d(Landroidx/lifecycle/LiveData$b;)V
HSPLandroidx/lifecycle/LiveData;->e()V
HSPLandroidx/lifecycle/LiveData;->f()V
HSPLandroidx/lifecycle/LiveData;->g(Landroidx/lifecycle/q;)V
HSPLandroidx/lifecycle/LiveData;->h(Ljava/lang/Object;)V
Landroidx/lifecycle/p;
HSPLandroidx/lifecycle/p;-><init>()V
HSPLandroidx/lifecycle/p;->h(Ljava/lang/Object;)V
Landroidx/lifecycle/ProcessLifecycleInitializer;
LJ/a;
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;-><init>()V
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->c(Landroid/content/Context;)Landroidx/lifecycle/l;
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->b(Landroid/content/Context;)Ljava/lang/Object;
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->a()Ljava/util/List;
Landroidx/lifecycle/t;
HSPLandroidx/lifecycle/t;-><clinit>()V
HSPLandroidx/lifecycle/t;-><init>()V
HSPLandroidx/lifecycle/t;->g()Landroidx/lifecycle/g;
Landroidx/lifecycle/u$c;
HSPLandroidx/lifecycle/u$c;-><init>()V
HSPLandroidx/lifecycle/u$c;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
PLandroidx/lifecycle/u$c;->onActivityDestroyed(Landroid/app/Activity;)V
PLandroidx/lifecycle/u$c;->onActivityPaused(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/u$c;->onActivityPostCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
HSPLandroidx/lifecycle/u$c;->onActivityPostResumed(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/u$c;->onActivityPostStarted(Landroid/app/Activity;)V
PLandroidx/lifecycle/u$c;->onActivityPreDestroyed(Landroid/app/Activity;)V
PLandroidx/lifecycle/u$c;->onActivityPrePaused(Landroid/app/Activity;)V
PLandroidx/lifecycle/u$c;->onActivityPreStopped(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/u$c;->onActivityResumed(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/u$c;->onActivityStarted(Landroid/app/Activity;)V
PLandroidx/lifecycle/u$c;->onActivityStopped(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/u$c;->registerIn(Landroid/app/Activity;)V
Landroidx/lifecycle/u;
HSPLandroidx/lifecycle/u;-><init>()V
HSPLandroidx/lifecycle/u;->a(Landroidx/lifecycle/g$a;)V
HSPLandroidx/lifecycle/u;->b(Landroidx/lifecycle/u$a;)V
HSPLandroidx/lifecycle/u;->c(Landroidx/lifecycle/u$a;)V
HSPLandroidx/lifecycle/u;->d(Landroidx/lifecycle/u$a;)V
HSPLandroidx/lifecycle/u;->onActivityCreated(Landroid/os/Bundle;)V
PLandroidx/lifecycle/u;->onDestroy()V
PLandroidx/lifecycle/u;->onPause()V
HSPLandroidx/lifecycle/u;->onResume()V
HSPLandroidx/lifecycle/u;->onStart()V
PLandroidx/lifecycle/u;->onStop()V
HSPLandroidx/lifecycle/z;-><init>()V
PLandroidx/lifecycle/z;->a()V
PLandroidx/lifecycle/z;->d()V
Landroidx/lifecycle/A;
HSPLandroidx/lifecycle/A;-><init>(Landroidx/lifecycle/D;Landroidx/lifecycle/A$b;)V
HSPLandroidx/lifecycle/A;->a(Ljava/lang/Class;)Landroidx/lifecycle/z;
HSPLandroidx/lifecycle/A;->b(Ljava/lang/String;Ljava/lang/Class;)Landroidx/lifecycle/z;
Landroidx/lifecycle/D;
HSPLandroidx/lifecycle/D;-><init>()V
PLandroidx/lifecycle/D;->a()V
HSPLandroidx/lifecycle/D;->b(Ljava/lang/String;)Landroidx/lifecycle/z;
HSPLandroidx/lifecycle/D;->d(Ljava/lang/String;Landroidx/lifecycle/z;)V
Landroidx/lifecycle/F;
HSPLandroidx/lifecycle/F;->a(Landroid/view/View;Landroidx/lifecycle/l;)V
Landroidx/lifecycle/G;
HSPLandroidx/lifecycle/G;->a(Landroid/view/View;Landroidx/lifecycle/E;)V
Landroidx/startup/a;
HSPLandroidx/startup/a;-><clinit>()V
HSPLandroidx/startup/a;-><init>(Landroid/content/Context;)V
HSPLandroidx/startup/a;->a()V
HSPLandroidx/startup/a;->b(Landroid/os/Bundle;)V
HSPLandroidx/startup/a;->c(Ljava/lang/Class;)Ljava/lang/Object;
HSPLandroidx/startup/a;->d(Ljava/lang/Class;Ljava/util/Set;)Ljava/lang/Object;
HSPLandroidx/startup/a;->e(Landroid/content/Context;)Landroidx/startup/a;
HSPLandroidx/startup/a;->f(Ljava/lang/Class;)Ljava/lang/Object;
HSPLandroidx/startup/a;->g(Ljava/lang/Class;)Z
Landroidx/appcompat/widget/N;
HSPLandroidx/appcompat/widget/N;-><init>(Landroidx/appcompat/widget/Toolbar;)V
HSPLandroidx/appcompat/widget/N;->run()V
Landroidx/fragment/app/m;
Lw/a;
HSPLandroidx/fragment/app/m;-><init>(Landroidx/fragment/app/q;)V
Landroidx/fragment/app/n;
HSPLandroidx/fragment/app/n;-><init>(Landroidx/fragment/app/q;)V
Landroidx/fragment/app/o;
HSPLandroidx/fragment/app/o;-><init>(Landroidx/fragment/app/q;)V
Landroidx/fragment/app/p;
HSPLandroidx/fragment/app/p;-><init>(Landroidx/fragment/app/q;)V
Landroidx/lifecycle/s;
HSPLandroidx/lifecycle/s;-><init>(Landroidx/lifecycle/t;)V
Lh/c;
HSPLh/c;->a(Ljava/lang/Object;)V
@@ -1,10 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "COMPATIBLE_SCREEN_MANIFEST",
"kind": "Directory"
},
"applicationId": "io.truckwash.twa",
"variantName": "release",
"elements": []
}
@@ -1,89 +0,0 @@
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
# will be ignored by new version of the Android plugin for Gradle.
# Optimizations: If you don't want to optimize, use the proguard-android.txt configuration file
# instead of this one, which turns off the optimization flags.
-allowaccessmodification
# Preserve some attributes that may be required for reflection.
-keepattributes AnnotationDefault,
EnclosingMethod,
InnerClasses,
RuntimeVisibleAnnotations,
RuntimeVisibleParameterAnnotations,
RuntimeVisibleTypeAnnotations,
Signature
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
-keep public class com.google.android.vending.licensing.ILicensingService
-dontnote com.android.vending.licensing.ILicensingService
-dontnote com.google.vending.licensing.ILicensingService
-dontnote com.google.android.vending.licensing.ILicensingService
# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
# Keep setters in Views so that animations can still work.
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick.
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
# Preserve annotated Javascript interface methods.
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
# The support libraries contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontnote android.support.**
-dontnote androidx.**
-dontwarn android.support.**
-dontwarn androidx.**
# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
-dontnote org.apache.http.**
-dontnote android.net.http.**
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
-dontnote java.lang.invoke.**
@@ -1,95 +0,0 @@
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
# will be ignored by new version of the Android plugin for Gradle.
# Optimization is turned off by default. Dex does not like code run
# through the ProGuard optimize steps (and performs some
# of these optimizations on its own).
# Note that if you want to enable optimization, you cannot just
# include optimization flags in your own project configuration file;
# instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your
# project.properties file.
-dontoptimize
# Preserve some attributes that may be required for reflection.
-keepattributes AnnotationDefault,
EnclosingMethod,
InnerClasses,
RuntimeVisibleAnnotations,
RuntimeVisibleParameterAnnotations,
RuntimeVisibleTypeAnnotations,
Signature
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
-keep public class com.google.android.vending.licensing.ILicensingService
-dontnote com.android.vending.licensing.ILicensingService
-dontnote com.google.vending.licensing.ILicensingService
-dontnote com.google.android.vending.licensing.ILicensingService
# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
# Keep setters in Views so that animations can still work.
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick.
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
# Preserve annotated Javascript interface methods.
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
# The support libraries contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontnote android.support.**
-dontnote androidx.**
-dontwarn android.support.**
-dontwarn androidx.**
# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
-dontnote org.apache.http.**
-dontnote android.net.http.**
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
-dontnote java.lang.invoke.**
@@ -1,89 +0,0 @@
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
# will be ignored by new version of the Android plugin for Gradle.
# Optimizations can be turned on and off in the 'postProcessing' DSL block.
# The configuration below is applied if optimizations are enabled.
-allowaccessmodification
# Preserve some attributes that may be required for reflection.
-keepattributes AnnotationDefault,
EnclosingMethod,
InnerClasses,
RuntimeVisibleAnnotations,
RuntimeVisibleParameterAnnotations,
RuntimeVisibleTypeAnnotations,
Signature
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
-keep public class com.google.android.vending.licensing.ILicensingService
-dontnote com.android.vending.licensing.ILicensingService
-dontnote com.google.vending.licensing.ILicensingService
-dontnote com.google.android.vending.licensing.ILicensingService
# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
# Keep setters in Views so that animations can still work.
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick.
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
# Preserve annotated Javascript interface methods.
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
# The support libraries contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontnote android.support.**
-dontnote androidx.**
-dontwarn android.support.**
-dontwarn androidx.**
# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
-dontnote org.apache.http.**
-dontnote android.net.http.**
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
-dontnote java.lang.invoke.**
@@ -1,5 +0,0 @@
31=0/.dm
2147483647=0/.dm
28=1/.dm
29=1/.dm
30=1/.dm
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merger version="3"><dataSet config="main" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="C:\Users\2jepp\WebstormProjects\pleno-vue\app\src\main\assets"/></dataSet><dataSet config="release" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="C:\Users\2jepp\WebstormProjects\pleno-vue\app\src\release\assets"/></dataSet><dataSet config="generated" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="C:\Users\2jepp\WebstormProjects\pleno-vue\app\build\intermediates\shader_assets\release\compileReleaseShaders\out"/></dataSet></merger>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merger version="3"><dataSet config="main" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="C:\Users\2jepp\WebstormProjects\pleno-vue\app\src\main\jniLibs"/></dataSet><dataSet config="release" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="C:\Users\2jepp\WebstormProjects\pleno-vue\app\src\release\jniLibs"/></dataSet></merger>

Some files were not shown because too many files have changed in this diff Show More