Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
882bc64fa6 | ||
|
|
7d5ec8894c | ||
|
|
f2fc7643a2 | ||
|
|
06b6498f7a | ||
|
|
c5c186242b | ||
|
|
3112a4f985 | ||
|
|
ddd31bd2dd | ||
|
|
f07c15972c | ||
|
|
210d7d051d | ||
|
|
0d2098449a | ||
|
|
cafe671e85 | ||
|
|
2401ebb674 | ||
|
|
6f39eb897c |
@@ -24,7 +24,7 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
npm run build
|
||||
|
||||
- name: Setup SSH
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
uses: webfactory/ssh-agent@v0.10.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
|
||||
@@ -77,10 +77,19 @@ jobs:
|
||||
run: |
|
||||
bash scripts/smoke-test.sh "$SMOKE_BASE_URL"
|
||||
|
||||
- name: Auto-rollback on smoke failure
|
||||
if: steps.smoke.outcome == 'failure'
|
||||
- name: Sergii Review Batch smoke test (TRU-96)
|
||||
id: smoke_sergii
|
||||
continue-on-error: true
|
||||
env:
|
||||
SMOKE_BASE_URL: ${{ secrets.SMOKE_BASE_URL }}
|
||||
run: |
|
||||
echo "::error::Smoke test failed — rolling back to ${{ steps.pre.outputs.pre_sha }}"
|
||||
bash scripts/smoke-test-sergii.sh "$SMOKE_BASE_URL"
|
||||
|
||||
- name: Auto-rollback on smoke failure
|
||||
if: steps.smoke.outcome == 'failure' || steps.smoke_sergii.outcome == 'failure'
|
||||
run: |
|
||||
reason="$([ "${{ steps.smoke.outcome }}" = 'failure' ] && echo 'generic smoke' || echo 'Sergii Review Batch smoke')"
|
||||
echo "::error::$reason test failed — rolling back to ${{ steps.pre.outputs.pre_sha }}"
|
||||
ssh "$DEPLOY_USER@$DEPLOY_HOST" '
|
||||
set -e
|
||||
cd /opt/pleno-vue
|
||||
@@ -92,12 +101,12 @@ jobs:
|
||||
|
||||
- name: Post Slack status
|
||||
if: always()
|
||||
uses: slackapi/slack-github-action@v1.27.0
|
||||
uses: slackapi/slack-github-action@v4.0.0
|
||||
with:
|
||||
channel-id: ${{ secrets.AI_DAILY_CHANNEL }}
|
||||
payload: |
|
||||
{
|
||||
"text": "${{ job.status == 'success' && '✅' || '❌' }} Deploy *pleno-vue@${{ github.sha[0:7] }}* — ${{ job.status }}\n${{ steps.smoke.outcome == 'failure' && '⚠️ Auto-rolled back' || '✓ Smoke passed' }}"
|
||||
"text": "${{ job.status == 'success' && '✅' || '❌' }} Deploy *pleno-vue@${{ github.sha[0:7] }}* — ${{ job.status }}\n${{ steps.smoke.outcome == 'failure' && '⚠️ Generic smoke FAILED → auto-rolled back' || steps.smoke_sergii.outcome == 'failure' && '⚠️ Sergii Review Batch smoke FAILED → auto-rolled back' || '✓ Smoke (generic + Sergii) passed' }}"
|
||||
}
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
ruby ">= 3.2", "< 3.5"
|
||||
gem "fastlane", "2.237.0"
|
||||
gem "fastlane", "2.238.0"
|
||||
|
||||
@@ -8,8 +8,8 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1271.0)
|
||||
aws-sdk-core (3.254.0)
|
||||
aws-partitions (1.1281.0)
|
||||
aws-sdk-core (3.254.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -20,8 +20,8 @@ GEM
|
||||
aws-sdk-kms (1.130.0)
|
||||
aws-sdk-core (~> 3, >= 3.254.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.228.0)
|
||||
aws-sdk-core (~> 3, >= 3.254.0)
|
||||
aws-sdk-s3 (1.229.0)
|
||||
aws-sdk-core (~> 3, >= 3.254.1)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
@@ -35,45 +35,33 @@ GEM
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
csv (3.3.5)
|
||||
csv (3.3.6)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (1.6.0)
|
||||
erb (6.0.7)
|
||||
excon (1.7.0)
|
||||
logger
|
||||
faraday (2.14.3)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday (1.10.6)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.8)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (>= 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-follow_redirects (0.5.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-multipart (1.2.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.4)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
faraday-net_http (3.4.4)
|
||||
net-http (~> 0.5)
|
||||
faraday-retry (2.4.0)
|
||||
faraday (~> 2.0)
|
||||
fastimage (2.4.1)
|
||||
fastlane (2.237.0)
|
||||
fastlane (2.238.0)
|
||||
CFPropertyList (>= 2.3, < 5.0.0)
|
||||
abbrev (~> 0.1)
|
||||
addressable (>= 2.9.0, < 3.0.0)
|
||||
@@ -89,9 +77,11 @@ GEM
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 2.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
faraday (~> 2.7)
|
||||
faraday-cookie_jar (~> 0.0.8)
|
||||
faraday-follow_redirects (~> 0.3)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-retry (~> 2.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
fastlane-sirp (>= 1.1.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
@@ -101,6 +91,7 @@ GEM
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
irb (>= 1.8)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.10.3, < 4)
|
||||
logger (>= 1.6, < 2.0)
|
||||
@@ -117,7 +108,7 @@ GEM
|
||||
security (= 0.1.5)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (~> 3)
|
||||
terminal-table (~> 4)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
@@ -126,21 +117,22 @@ GEM
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-sirp (1.1.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.105.0)
|
||||
google-apis-androidpublisher_v3 (0.106.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-core (0.18.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.3, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
mutex_m
|
||||
google-apis-core (1.2.5)
|
||||
addressable (~> 2.9)
|
||||
faraday (~> 2.13)
|
||||
faraday-follow_redirects (~> 0.3)
|
||||
googleauth (~> 1.14)
|
||||
mini_mime (~> 1.1)
|
||||
multi_json (~> 1.11)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
retriable (>= 3.1, < 5.0)
|
||||
google-apis-iamcredentials_v1 (0.28.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.18.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.65.0)
|
||||
google-apis-storage_v1 (0.66.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-core (1.9.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
@@ -159,7 +151,7 @@ GEM
|
||||
googleauth (~> 1.9)
|
||||
mini_mime (~> 1.0)
|
||||
google-logging-utils (0.2.0)
|
||||
googleauth (1.17.1)
|
||||
googleauth (1.17.3)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.2)
|
||||
google-logging-utils (~> 0.1)
|
||||
@@ -170,10 +162,14 @@ GEM
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
io-console (0.9.2)
|
||||
irb (1.18.0)
|
||||
pp (>= 0.6.0)
|
||||
prism (>= 1.3.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.21.1)
|
||||
json (2.21.2)
|
||||
jwt (3.2.0)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
@@ -184,22 +180,38 @@ GEM
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.3.0)
|
||||
net-http (0.9.1)
|
||||
uri (>= 0.11.1)
|
||||
nkf (0.3.0)
|
||||
optparse (0.8.1)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.3)
|
||||
plist (3.7.2)
|
||||
pp (0.6.4)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.9.0)
|
||||
pstore (0.2.1)
|
||||
public_suffix (7.0.5)
|
||||
rake (13.4.2)
|
||||
rbs (4.1.3)
|
||||
logger
|
||||
prism (>= 1.6.0)
|
||||
tsort
|
||||
rdoc (8.0.0)
|
||||
erb
|
||||
prism (>= 1.6.0)
|
||||
rbs (>= 4.0.0)
|
||||
tsort
|
||||
reline (0.7.0)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.8.0)
|
||||
retriable (4.2.0)
|
||||
rexml (3.4.4)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.22.0)
|
||||
@@ -210,15 +222,19 @@ GEM
|
||||
CFPropertyList
|
||||
naturally
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
terminal-table (4.0.0)
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
trailblazer-option (0.1.2)
|
||||
tsort (0.2.0)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.2)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.6.0)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.2.0)
|
||||
uri (1.1.1)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.28.1)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
@@ -239,7 +255,7 @@ PLATFORMS
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane (= 2.237.0)
|
||||
fastlane (= 2.238.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.3.12p206
|
||||
|
||||
@@ -45,11 +45,12 @@ platform :ios do
|
||||
skip_screenshots: false,
|
||||
overwrite_screenshots: true,
|
||||
force: true,
|
||||
submit_for_review: false,
|
||||
submit_for_review: true,
|
||||
automatic_release: true,
|
||||
phased_release: false,
|
||||
run_precheck_before_submit: false,
|
||||
precheck_include_in_app_purchases: false
|
||||
precheck_include_in_app_purchases: false,
|
||||
ignore_language_directory_validation: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 213 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"marketingVersion": "1.0.1",
|
||||
"marketingVersion": "1.1.0",
|
||||
"bundleId": "io.truckwash.app",
|
||||
"minimumIosVersion": "15.0"
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ const strict = argv.includes("--strict");
|
||||
const failures = [];
|
||||
const warnings = [];
|
||||
const root = process.cwd();
|
||||
const metadataRoot = join(root, "fastlane/metadata/da-DK");
|
||||
const screenshotRoot = join(root, "fastlane/screenshots/da-DK");
|
||||
// App Store Connect uses the bare `da` locale code for Danish (not `da-DK`).
|
||||
// Keep these paths in sync with fastlane/metadata/<locale>/ and
|
||||
// fastlane/screenshots/<locale>/ after any locale rename.
|
||||
const metadataRoot = join(root, "fastlane/metadata/da");
|
||||
const screenshotRoot = join(root, "fastlane/screenshots/da");
|
||||
|
||||
const fail = (message) => failures.push(message);
|
||||
const warn = (message) => warnings.push(message);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sergii Review Batch smoke test (TRU-96).
|
||||
#
|
||||
# Validates that the Sergii Review Batch features (project "Sergii Review Batch"
|
||||
# in Linear) are still functioning on the deployed truckwash.io dashboard
|
||||
# after a release.
|
||||
#
|
||||
# The Sergii Review Batch project tracks:
|
||||
# - TRU-68: Customer email flow (no Jimmy) — Done
|
||||
# - TRU-72: Sergii's pages 6-10 review — In Review (pending release)
|
||||
# - TRU-75: UVS as individually-selectable customer option — Todo
|
||||
#
|
||||
# This smoke test runs against the same base URL as the generic smoke test and
|
||||
# checks the dashboard surfaces Sergii touched. The test fails (exit 1) if any
|
||||
# Sergii-touch surface returns an unexpected status, so a broken Sergii
|
||||
# deployment can be rolled back automatically.
|
||||
#
|
||||
# Usage: ./scripts/smoke-test-sergii.sh [base_url]
|
||||
# Default: https://staging.truckwash.io
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${SMOKE_BASE_URL:-${1:-https://staging.truckwash.io}}"
|
||||
TIMEOUT="${SMOKE_TIMEOUT:-10}"
|
||||
|
||||
# Color codes
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
FAIL=0
|
||||
|
||||
check() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local expected="${3:-2xx}"
|
||||
local method="${4:-GET}"
|
||||
|
||||
local status
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" -X "$method" --max-time "$TIMEOUT" "$url" || echo "000")
|
||||
|
||||
if [[ "$expected" == "2xx" && "$status" =~ ^2 ]]; then
|
||||
echo -e " ${GREEN}✓${NC} $name ($status) — $url"
|
||||
elif [[ "$status" == "$expected" ]]; then
|
||||
echo -e " ${GREEN}✓${NC} $name ($status) — $url"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} $name (expected $expected, got $status) — $url"
|
||||
FAIL=1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Sergii Review Batch smoke test against $BASE_URL"
|
||||
echo " (project: Sergii Review Batch — TRU-68, TRU-72, TRU-75)"
|
||||
echo " (timeout ${TIMEOUT}s per check)"
|
||||
echo
|
||||
|
||||
# === Sergii pages 6-10 (TRU-72) ===
|
||||
# Sergii's batch touched the customer, products, and orders surfaces of the
|
||||
# dashboard. Each of these must remain 2xx (or 401/302 for auth-gated pages —
|
||||
# anything but 5xx is acceptable for a release-gate smoke check).
|
||||
echo "Sergii pages 6-10 (TRU-72):"
|
||||
check "Sergii page 6 — customer" "$BASE_URL/admin/customer" "5xx"
|
||||
check "Sergii page 7 — product" "$BASE_URL/admin/product" "5xx"
|
||||
check "Sergii page 8 — order" "$BASE_URL/admin/order" "5xx"
|
||||
check "Sergii page 9 — booking" "$BASE_URL/admin/booking" "5xx"
|
||||
check "Sergii page 10 — invoicing" "$BASE_URL/admin/invoicing" "5xx"
|
||||
|
||||
# === Sergii customer email flow (TRU-68) ===
|
||||
# The customer form is public; verify the static form route is still served.
|
||||
echo
|
||||
echo "Sergii customer email flow (TRU-68):"
|
||||
check "customer list (Sergii touched)" "$BASE_URL/api/customer" "2xx"
|
||||
check "kundeoprettelse form (Sergii touched)" "$BASE_URL/kundeoprettelse" "2xx"
|
||||
|
||||
# === Sergii UVS program option (TRU-75) ===
|
||||
# The UVS wash program must be exposed in the public self-serve program
|
||||
# registry so customers can pick it individually (not bundled into FF).
|
||||
echo
|
||||
echo "Sergii UVS program option (TRU-75):"
|
||||
check "self-serve program registry" "$BASE_URL/self-serve/program" "2xx"
|
||||
check "self-serve vehicle step" "$BASE_URL/self-serve/vehicle" "2xx"
|
||||
|
||||
# === Sergii Review Batch sanity — dashboard still bootstraps ===
|
||||
echo
|
||||
echo "Dashboard bootstrap:"
|
||||
check "health check" "$BASE_URL/healthz" "2xx"
|
||||
check "ping" "$BASE_URL/api/ping" "2xx"
|
||||
check "login page" "$BASE_URL/login" "2xx"
|
||||
|
||||
echo
|
||||
if [ "$FAIL" -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ Sergii Review Batch smoke test passed${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ Sergii Review Batch smoke test FAILED${NC}"
|
||||
echo " A Sergii-touch surface on $BASE_URL is broken."
|
||||
echo " Consider rolling back the most recent Sergii batch."
|
||||
exit 1
|
||||
fi
|
||||
@@ -27,7 +27,7 @@ import productImage21 from "/src/assets/piktogrammer/21.png"; // Undervognsskyld
|
||||
import productImage23 from "/src/assets/piktogrammer/23.png"; // Spotfree
|
||||
import productImage24 from "/src/assets/piktogrammer/24.png"; // Spotfree
|
||||
import productImage25 from "/src/assets/piktogrammer/25.png"; // Fælg flex
|
||||
import productImage26 from "/src/assets/piktogrammer/26.png"; // Højglans
|
||||
import productImage26 from "/src/assets/piktogrammer/26.png"; // Voksforsegling (TRU-130: brand-agnostic after wax supplier swap)
|
||||
import productImage27 from "/src/assets/piktogrammer/27.png"; // 10 Minutter
|
||||
import productImage39 from "/src/assets/piktogrammer/39.png"; // Trailer - Varevogn
|
||||
import productImage43 from "/src/assets/piktogrammer/43.png"; // Handling of materials
|
||||
|
||||
@@ -15,7 +15,11 @@ const FALLBACK_PRODUCT_TITLES = {
|
||||
24: "Spot Free (Lastbil)",
|
||||
25: "Fælg flex pr. enhed",
|
||||
27: "Ekstraordinær pr. 10 min inkl. kemi",
|
||||
26: "Højglans - Voksforsegling pr. enhed",
|
||||
// TRU-130: customer experience series reported that the previous wax product
|
||||
// ("Højglans - Voksforsegling") tested poorly and is being swapped for a
|
||||
// different product. Drop the brand-specific prefix so the tile stays
|
||||
// accurate while the operations team iterates on the next wax supplier.
|
||||
26: "Voksforsegling pr. enhed",
|
||||
21: "Undervognsskyl pr. enhed",
|
||||
22: "Tillæg for Specialsæbe - DD",
|
||||
};
|
||||
|
||||
@@ -1264,14 +1264,19 @@ const createPathEditorQuestion = async () => {
|
||||
await loadPathOutcomes({ force: true });
|
||||
};
|
||||
|
||||
const pathEditorProgramButtonOptions = computed(() => [
|
||||
{ id: 0, value: "0", label: "0 = OFF" },
|
||||
...Array.from({ length: 12 }, (_, index) => ({
|
||||
id: index + 1,
|
||||
value: String(index + 1),
|
||||
label: `Wheel position #${index + 1}`,
|
||||
})),
|
||||
]);
|
||||
const pathEditorProgramButtonOptions = computed(() => {
|
||||
if (!pathEditorMachineIsAvailable.value) {
|
||||
return [{ id: 0, value: "0", label: "0 = OFF" }];
|
||||
}
|
||||
return [
|
||||
{ id: 0, value: "0", label: "0 = OFF" },
|
||||
...Array.from({ length: 12 }, (_, index) => ({
|
||||
id: index + 1,
|
||||
value: String(index + 1),
|
||||
label: `Wheel position #${index + 1}`,
|
||||
})),
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1392,16 +1397,56 @@ const pathVerificationSelectedCaseHasRun = computed(() =>
|
||||
)
|
||||
);
|
||||
|
||||
const pathEditorMachineButtonOptions = computed(() =>
|
||||
Array.from({ length: 12 }, (_, index) => {
|
||||
// TRU-91: Filter path editor machine options to only show enabled/available
|
||||
// machine programs for the current scope. A machine is considered "available"
|
||||
// when the current lane has a machine_type_id that resolves to a non-deleted
|
||||
// selfserve_machine_types row in the lookups.
|
||||
const pathEditorCurrentLaneRow = computed(() => {
|
||||
const laneId = parseIntOrZero(selectedSimulatorLaneId.value);
|
||||
if (laneId <= 0) {
|
||||
return null;
|
||||
}
|
||||
return laneRowById(laneId);
|
||||
});
|
||||
|
||||
const pathEditorCurrentMachineType = computed(() => {
|
||||
const lane = pathEditorCurrentLaneRow.value;
|
||||
if (!lane) {
|
||||
return null;
|
||||
}
|
||||
const machineTypeId = parseNullableInt(lane.machine_type_id);
|
||||
if (!machineTypeId) {
|
||||
return null;
|
||||
}
|
||||
const machineTypeRow = lookupRowById("machine_types", machineTypeId);
|
||||
if (!machineTypeRow) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
machineTypeRow.deleted_at !== null &&
|
||||
machineTypeRow.deleted_at !== undefined &&
|
||||
machineTypeRow.deleted_at !== ""
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return machineTypeRow;
|
||||
});
|
||||
|
||||
const pathEditorMachineIsAvailable = computed(() => Boolean(pathEditorCurrentMachineType.value));
|
||||
|
||||
const pathEditorMachineButtonOptions = computed(() => {
|
||||
if (!pathEditorMachineIsAvailable.value) {
|
||||
return [];
|
||||
}
|
||||
return Array.from({ length: 12 }, (_, index) => {
|
||||
const option = taskButtonOptions.find((entry) => entry.id === index);
|
||||
return {
|
||||
id: index,
|
||||
label: option?.description || `Machine button ${index}`,
|
||||
description: option?.name || `Program ${index + 1}`,
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const pathEditorTaskTitleKeySlug = (key) => String(key || "").replace(/[^a-zA-Z0-9_-]/g, "-");
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ const buildOverviewPayload = ({
|
||||
{
|
||||
product_id: 26,
|
||||
slug: "hoejglans",
|
||||
title: "Højglans - Voksforsegling pr. enhed",
|
||||
title: "Voksforsegling pr. enhed",
|
||||
state: "ready",
|
||||
value: 4,
|
||||
out_of: 8,
|
||||
@@ -268,7 +268,7 @@ test.describe("Admin daily report", () => {
|
||||
await expect(page.getByTestId("daily-report-tile-extraordinary-10-min")).toContainText(
|
||||
"Ekstraordinær pr. 10 min inkl. kemi"
|
||||
);
|
||||
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Højglans - Voksforsegling pr. enhed");
|
||||
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Voksforsegling pr. enhed");
|
||||
await expect(page.getByTestId("daily-report-tile-undervognsskyl")).toContainText("Undervognsskyl pr. enhed");
|
||||
await expect(page.getByTestId("daily-report-tile-double-duty-kemi")).toContainText("Tillæg for Specialsæbe - DD");
|
||||
});
|
||||
@@ -667,7 +667,7 @@ test.describe("Admin daily report", () => {
|
||||
await expect(page.getByTestId("daily-report-tile-extraordinary-10-min")).toContainText(
|
||||
"Ekstraordinær pr. 10 min inkl. kemi"
|
||||
);
|
||||
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Højglans - Voksforsegling pr. enhed");
|
||||
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Voksforsegling pr. enhed");
|
||||
await expect(page.getByTestId("daily-report-tile-undervognsskyl")).toContainText("Undervognsskyl pr. enhed");
|
||||
await expect(page.getByTestId("daily-report-tile-double-duty-kemi")).toContainText("Tillæg for Specialsæbe - DD");
|
||||
});
|
||||
|
||||
@@ -106,6 +106,26 @@ describe("DepartmentDashboardDailyReportProductSales behavior", () => {
|
||||
expect(missingTileWrapper.text()).toContain("Tillæg for Specialsæbe - DD");
|
||||
});
|
||||
|
||||
it("falls back to a brand-agnostic wax title for product 26 (TRU-130)", () => {
|
||||
// TRU-130: the previous wax product ("Højglans - Voksforsegling") tested
|
||||
// poorly in the field and is being replaced. The fallback title should
|
||||
// therefore stay brand-agnostic so the tile keeps matching whatever
|
||||
// wax product the API eventually reports.
|
||||
dailyReportProductsState.value = {};
|
||||
|
||||
const waxWrapper = mount(DepartmentDashboardDailyReportProductSales, {
|
||||
props: {
|
||||
product_id: 26,
|
||||
subtitle: "Dagens salg af produktet",
|
||||
dataTestid: "daily-report-tile-hoejglans",
|
||||
},
|
||||
});
|
||||
|
||||
const rendered = waxWrapper.text();
|
||||
expect(rendered).toContain("Voksforsegling pr. enhed");
|
||||
expect(rendered).not.toContain("Højglans");
|
||||
});
|
||||
|
||||
it("shows a saved target percentage below the product percentage", () => {
|
||||
dailyReportProductsState.value = {
|
||||
24: {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePath), "utf8");
|
||||
|
||||
describe("self-serve studio path editor machine filter (TRU-91)", () => {
|
||||
const studioSource = readSource(
|
||||
"src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue"
|
||||
);
|
||||
|
||||
it("defines a path editor machine-availability check tied to the current lane and machine type", () => {
|
||||
expect(studioSource).toContain("pathEditorCurrentLaneRow");
|
||||
expect(studioSource).toContain("pathEditorCurrentMachineType");
|
||||
expect(studioSource).toContain("pathEditorMachineIsAvailable");
|
||||
});
|
||||
|
||||
it("returns an empty machine button list when the machine is not available", () => {
|
||||
expect(studioSource).toMatch(
|
||||
/pathEditorMachineButtonOptions[\s\S]{0,400}if\s*\(\s*!pathEditorMachineIsAvailable\.value\s*\)\s*\{\s*return\s*\[\s*\]\s*;\s*\}/
|
||||
);
|
||||
});
|
||||
|
||||
it("limits program wheel options to the OFF position when the machine is not available", () => {
|
||||
expect(studioSource).toMatch(
|
||||
/pathEditorProgramButtonOptions[\s\S]{0,600}if\s*\(\s*!pathEditorMachineIsAvailable\.value\s*\)[\s\S]{0,200}return\s*\[\s*\{\s*id:\s*0,\s*value:\s*"0",\s*label:\s*"0 = OFF"\s*\}\s*\]/
|
||||
);
|
||||
});
|
||||
|
||||
it("considers a machine type deleted when its deleted_at is set", () => {
|
||||
expect(studioSource).toMatch(/machineTypeRow\.deleted_at[\s\S]{0,80}return\s+null/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const SCRIPT_PATH = path.resolve("scripts/smoke-test-sergii.sh");
|
||||
const WORKFLOW_PATH = path.resolve(".github/workflows/deploy.yml");
|
||||
const EXISTING_SMOKE_PATH = path.resolve("scripts/smoke-test.sh");
|
||||
|
||||
describe("Sergii Review Batch smoke test (TRU-96)", () => {
|
||||
it("ships a Sergii-specific smoke test script with executable bits set", () => {
|
||||
expect(fs.existsSync(SCRIPT_PATH)).toBe(true);
|
||||
const stat = fs.statSync(SCRIPT_PATH);
|
||||
// owner-execute bit (0o100) is what `chmod +x` sets; we don't care about
|
||||
// the user the test runs as, just that the file is marked executable.
|
||||
expect(stat.mode & 0o111).not.toBe(0);
|
||||
});
|
||||
|
||||
it("covers every Sergii Review Batch project issue (TRU-68, TRU-72, TRU-75)", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
// TRU-68: customer email flow
|
||||
expect(contents).toContain("TRU-68");
|
||||
expect(contents).toMatch(/customer email flow/i);
|
||||
// TRU-72: pages 6-10 review
|
||||
expect(contents).toContain("TRU-72");
|
||||
expect(contents).toMatch(/pages 6-10/);
|
||||
// TRU-75: UVS option
|
||||
expect(contents).toContain("TRU-75");
|
||||
expect(contents).toMatch(/UVS/);
|
||||
});
|
||||
|
||||
it("probes the Sergii-touch surfaces that the Sergii batch changed (pages 6-10)", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
// Pages 6-10 — Sergii's batch touched customer, product, order, booking, invoicing.
|
||||
expect(contents).toMatch(/\/admin\/customer/);
|
||||
expect(contents).toMatch(/\/admin\/product/);
|
||||
expect(contents).toMatch(/\/admin\/order/);
|
||||
expect(contents).toMatch(/\/admin\/booking/);
|
||||
expect(contents).toMatch(/\/admin\/invoicing/);
|
||||
});
|
||||
|
||||
it("probes the Sergii-touch customer email flow surfaces (TRU-68)", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
expect(contents).toMatch(/\/api\/customer/);
|
||||
expect(contents).toMatch(/\/kundeoprettelse/);
|
||||
});
|
||||
|
||||
it("probes the Sergii-touch UVS self-serve surfaces (TRU-75)", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
expect(contents).toMatch(/\/self-serve\/program/);
|
||||
expect(contents).toMatch(/\/self-serve\/vehicle/);
|
||||
});
|
||||
|
||||
it("is wired into the pleno-vue deploy workflow alongside the generic smoke test", () => {
|
||||
const workflow = fs.readFileSync(WORKFLOW_PATH, "utf8");
|
||||
expect(workflow).toMatch(/smoke-test-sergii\.sh/);
|
||||
expect(workflow).toMatch(/Sergii Review Batch smoke test \(TRU-96\)/);
|
||||
// Generic smoke test still runs first.
|
||||
expect(workflow).toMatch(/smoke-test\.sh "\$SMOKE_BASE_URL"/);
|
||||
// Auto-rollback now also triggers on Sergii smoke failure.
|
||||
expect(workflow).toMatch(/steps\.smoke\.outcome == 'failure' \|\| steps\.smoke_sergii\.outcome == 'failure'/);
|
||||
// Slack status message distinguishes the two failure modes.
|
||||
expect(workflow).toMatch(/Generic smoke FAILED/);
|
||||
expect(workflow).toMatch(/Sergii Review Batch smoke FAILED/);
|
||||
});
|
||||
|
||||
it("does not duplicate the generic smoke test (it complements, not replaces)", () => {
|
||||
expect(fs.existsSync(EXISTING_SMOKE_PATH)).toBe(true);
|
||||
const generic = fs.readFileSync(EXISTING_SMOKE_PATH, "utf8");
|
||||
const sergii = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
// Generic covers login + /api/ping + /healthz; Sergii covers the Sergii
|
||||
// Review Batch surfaces. They are intentionally different sets.
|
||||
expect(generic).toMatch(/\/login/);
|
||||
expect(sergii).toMatch(/\/admin\/customer/);
|
||||
});
|
||||
|
||||
it("exits non-zero when any Sergii-touch surface fails, so auto-rollback can fire", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
expect(contents).toMatch(/FAIL=1/);
|
||||
expect(contents).toMatch(/exit 1/);
|
||||
expect(contents).toMatch(/Sergii Review Batch smoke test FAILED/);
|
||||
});
|
||||
|
||||
it("defaults to https://staging.truckwash.io so it is safe to run unattended", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
expect(contents).toMatch(/SMOKE_BASE_URL:-\$\{1:-https:\/\/staging\.truckwash\.io\}/);
|
||||
});
|
||||
});
|
||||