03-Development / 03.06.WUI-Development

03.06.WUI Development

03.06. WUI Development

Local Development Setup

Native

cd /opt/bnc/bnc-cpt/bnc-cpt-wui/src/vue/app
npm install
npm run dev          # http://localhost:3333

The dev server proxies /api to http://localhost:8100 (API backend).

cd /opt/bnc/bnc-cpt/bnc-cpt-utl
make do-setup-wui          # Build and start on port 3333
make do-setup-wui-no-cache # Rebuild from scratch

Using the shell action

cd /opt/bnc/bnc-cpt/bnc-cpt-wui
ENV=lcl ./run -a do_build_wui_vue   # Install deps + type-check + build

Project Structure

src/vue/app/
├── index.html              # HTML entry point
├── vite.config.ts          # Vite config (proxy, version injection plugin)
├── package.json            # Dependencies and scripts
├── tsconfig.json           # TypeScript config (strict mode)
├── tailwind.config.js      # Tailwind theme (CSS variables)
├── .env.dev                # Dev API URL (proxied)
├── .env.tst                # Test API URL
├── .env.prd                # Production API URL
└── src/
    ├── main.ts             # Entry point (Vue + Pinia + I18n)
    ├── App.vue             # Root component (step-based navigation)
    ├── style.css           # Global Tailwind styles
    ├── components/
    │   ├── Header.vue          # Navigation + language switcher
    │   ├── Hero.vue            # Landing page
    │   ├── StepIndicator.vue   # Progress bar
    │   ├── BrandSelector.vue   # Vehicle brand selection
    │   ├── BrandCarousel.vue   # Brand carousel display
    │   ├── PaymentStep.vue     # Plan + method selection
    │   ├── PaymentGateway.vue  # Payment processing
    │   ├── OAuthStep.vue       # Tesla authentication
    │   ├── GeneratingStep.vue  # Report generation progress
    │   ├── ReportDashboard.vue # PDF display + download
    │   ├── SupportPage.vue     # Support/help articles
    │   ├── ServiceAgreement.vue # Terms modal
    │   ├── SupportedBrands.vue # Brand grid
    │   ├── FeeRow.vue          # Price line item
    │   └── Footer.vue
    ├── config.ts            # Runtime configuration
    ├── services/
    │   └── api.ts           # Centralized HTTP client
    ├── stores/
    │   └── app.ts           # Pinia store with localStorage persistence
    ├── types/
    │   └── index.ts         # TypeScript interfaces
    ├── data/
    │   └── vehicles.types.ts # Vehicle type data
    └── i18n/
        ├── index.ts         # Vue I18n setup
        └── locales/
            ├── en.json      # English
            ├── fi.json      # Finnish
            ├── sv.json      # Swedish
            ├── de.json      # German
            ├── fr.json      # French
            ├── es.json      # Spanish
            ├── it.json      # Italian
            ├── da.json      # Danish
            └── no.json      # Norwegian

Application Flow

Landing → Payment → Payment Gateway → Payment Success → OAuth → Report

Step navigation managed by Pinia store (currentStep). Payment state persisted to localStorage to survive Stripe/PayPal redirects.

Key Patterns

Composition API

All components use <script setup lang="ts">:

<script setup lang="ts">
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAppStore } from '@/stores/app';
const { t } = useI18n();
const store = useAppStore();
</script>

State Persistence

The Pinia store auto-syncs to localStorage via watchers: - cpt-payment-id, cpt-selected-plan, cpt-payment-method - cpt-current-step, cpt-selected-brand-id, cpt-selected-model-id

API Service

All backend calls go through services/api.ts:

import { createPaymentIntent, verifyPayment } from '@/services/api';

Base URL from VITE_API_BASE_URL env var. JWT token from localStorage for authenticated requests.

Internationalization

9 languages: English (en), Finnish (fi), Swedish (sv), German (de), French (fr), Spanish (es), Italian (it), Danish (da), Norwegian (no)

{{ t('payment.selectPlan') }}

Tailwind Design System

CSS variable-based theming with: - Cyan/Indigo gradient theme - Glass morphism effects (.glass class) - Responsive mobile-first design (md: breakpoints) - Custom font families: Inter (base), Playfair Display (display)

Build Commands

npm run dev          # Dev server with hot reload (port 3333)
npm run build        # Default production build
npm run build:dev    # Build for dev environment
npm run build:tst    # Build for test environment
npm run build:prd    # Build for production environment
npm run type-check   # TypeScript validation only
npm run preview      # Preview production build

UI Testing (Puppeteer)

Headless UI tests using Mocha + Puppeteer + Chai, running in the the-bot Docker container.

Test Suites

src/nodejs/the-bot/test/
├── test.config.js                      # Base config (headless toggle)
├── test.config.bnc.cpt.lcl.js          # Local dev (http://cpt-wui:3333)
├── test.config.bnc.cpt.dev.js          # Dev (https://dev.carpulsetracker.com)
├── test.config.bnc.cpt.inf.js          # Inf (https://inf.carpulsetracker.com)
├── test.config.bnc.cpt.tst.js          # Test (https://tst.carpulsetracker.com)
├── test.config.bnc.cpt.prd.js          # Production
├── test.utils.js                       # Helpers: waitForApp, screenshotOnFail, delay
├── test-101.landing-page/              # Page load, version, brand/model select
├── test-102.payment-step/              # Plans, prices, payment methods, terms
├── test-103.language-switcher/         # i18n switching, persistence
├── test-104.i18n-completeness/         # Translation keys validated across locales
├── test-105.user-journey/              # Full landing→brand→plan→payment flow
├── test-106.back-navigation/           # Back button, step indicator clicks, round-trips
├── test-107.support-page/              # Support search, topic/article nav, breadcrumbs
├── test-108.service-agreement/         # Modal open/close, content per locale
├── test-109.error-states/              # Network failures, API errors, graceful degradation
├── test-110.session-persistence/       # Session state persistence across page reloads
├── test-201.payment-wizard/            # Payment flow + Stripe integration
├── test-202.payment-paypal/            # PayPal selection, switching, E2E
├── test-301.tesla-oauth-login/         # Tesla OAuth: login, MFA/TOTP, consent, callback
└── test-302.report-dashboard/          # Multi-vehicle tabs, report sections, PDF download

Running Tests

Option 1: Headless in Docker (primary, used in CI)

# From bnc-cpt-utl/
make test-wui                        # Run headless in the-bot container
./run -a do_run_wui_tests            # Same via shell action

# Against specific deployed environment
make test-wui-dev                    # Against https://dev.carpulsetracker.com
make test-wui-tst                    # Against https://tst.carpulsetracker.com

Option 2: Headed on Host (visible browser)

Runs Puppeteer directly on your machine (not in Docker). The browser opens visibly so you can watch tests execute.

# From bnc-cpt-utl/
make test-wui-headed                 # Visible browser, tests against local dev

# Via shell action (more control)
PUPPETEER_HEADLESS=false ./run -a do_run_wui_tests_local

# Against a deployed environment with visible browser
cd /opt/bnc/bnc-cpt/bnc-cpt-wui/src/nodejs/the-bot
PUPPETEER_HEADLESS=false ORG=bnc APP=cpt ENV=dev npm test

Prerequisites for headed mode on host: - Node.js 18+ installed on host - Chrome/Chromium installed on host (Puppeteer downloads its own if not set) - The target URL must be reachable from your machine - For lcl: the WUI dev server must be running on localhost:3333

Option 3: Container Browser Displayed on Host (X11 forwarding, Linux only)

This runs Chromium inside the Docker container but displays the browser window on your host's screen. Useful when you want the container's exact environment but need visual feedback.

# 1. Allow Docker to access your X11 display
xhost +local:docker

# 2. Start the-bot with X11 forwarding
docker run --rm -it \
  -e DISPLAY=$DISPLAY \
  -v /tmp/.X11-unix:/tmp/.X11-unix:rw \
  -v /opt/bnc/bnc-cpt:/opt/bnc/bnc-cpt \
  -e PUPPETEER_HEADLESS=false \
  -e PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
  --network all_lcl_docker \
  img-the-bot bash -c '
    cd /opt/bnc/bnc-cpt/bnc-cpt-wui/src/nodejs/the-bot
    ORG=bnc APP=cpt ENV=lcl npm test
  '

# 3. Revoke X11 access when done
xhost -local:docker

Requirements: - Linux host with X11 (not Wayland — for Wayland use XDG_RUNTIME_DIR) - xhost command available (apt install x11-xserver-utils) - Container image img-the-bot must be built (make do-setup-wui)

Limitations: - Linux only — does not work on macOS or Windows (WSL2 needs extra setup) - Security: xhost +local:docker grants display access to all local Docker containers - The container's Chromium may render slightly differently than your host's Chrome

How the Headless Toggle Works

All test configs read PUPPETEER_HEADLESS from the environment:

// test.config.js
const puppeteer = {
  headless: process.env.PUPPETEER_HEADLESS !== 'false',  // true by default
  args: ["--no-sandbox", "--disable-setuid-sandbox"],
};

Set PUPPETEER_HEADLESS=false to open a visible browser. Any other value (or unset) = headless.

Test Reports

Debugging Tests

# Shell into the-bot container
make test-wui-shell

# Then run tests manually
cd /opt/bnc/bnc-cpt/bnc-cpt-wui/src/nodejs/the-bot
ORG=bnc APP=cpt ENV=lcl npm test

# Run a single test file
npx mocha --timeout 30000 test/test-101.landing-page/test-101.landing-page.spec.js

# View container logs
make test-wui-logs

Versioning

The app version is derived from git tags and injected at build time as an HTML meta tag.

How it works

  1. Git tag (e.g., v0.2.3) is the source of truth
  2. Deploy script reads the tag and sets VITE_APP_VERSION env var
  3. Vite plugin (versionInjectionPlugin in vite.config.ts) injects: html <meta name="version" content="v0.2.3" />
  4. Visible via View Page Source (right-click in browser)

Bumping the version

cd /opt/bnc/bnc-cpt/bnc-cpt-utl

# Bump patch: v0.2.3 -> v0.2.4
./run -a do_version_bump

# Bump minor: v0.2.3 -> v0.3.0
BUMP=minor ./run -a do_version_bump

# Push the tag
git -C /opt/bnc/bnc-cpt/bnc-cpt-wui push origin v0.2.4

Increment logic: patch 0-9 then minor rolls, minor 0-9 then major rolls.

Checking deployed version

curl -s https://dev.carpulsetracker.com/index.html | grep 'meta name="version"'
# <meta name="version" content="v0.2.3" />

CI/CD

Pipeline (ci.yaml)

Single workflow with 4 jobs:

  1. build-and-test (all pushes): Uses do_build_wui_vue shell action (npm install + type-check + build). Runs UI tests against local dev server.
  2. prepare-deploy (master only): Determines target environments
  3. deploy (master or dispatch): Deploys to GCS bucket via do_gcp_deploy_wui
  4. post-deploy-test (after deploy, skips prd): Runs the-bot Puppeteer suite against the deployed URL with test credentials from GCP Secret Manager

Auto-deploy on master push: inf + dev. Manual deploy via workflow_dispatch: any env.

Post-Deploy Testing

After deployment, the post-deploy-test job: - Fetches test credentials (Stripe test card, PayPal test user) from GCP Secret Manager - Builds the-bot container with Puppeteer - Runs full UI test suite against the deployed environment URL (e.g., https://dev.carpulsetracker.com) - Passes secrets via docker exec -e (in-memory only, masked in CI logs) - Uploads Mochawesome HTML report as a downloadable artifact - Skips prd environment (production is never auto-tested)

# Manual deploy to tst
gh workflow run ci.yaml -f environment=tst --repo csitea/bnc-cpt-wui

# Monitor
gh run list --repo csitea/bnc-cpt-wui

Deployment Flow

  1. Build Vue app inside Docker container with VITE_APP_VERSION from git tag
  2. gsutil rsync dist/ to gs://bnc-cpt-{env}-site-static
  3. CDN cache invalidation
  4. Health check on https://{fqdn}/index.html

Environment Configuration

Env Site URL API URL
dev https://dev.carpulsetracker.com https://dev.api.carpulsetracker.com/api/v1
tst https://tst.carpulsetracker.com https://tst.api.carpulsetracker.com/api/v1
prd https://carpulsetracker.com https://api.carpulsetracker.com/api/v1
inf https://inf.carpulsetracker.com https://inf.api.carpulsetracker.com/api/v1

Adding a New Component

  1. Create src/components/NewComponent.vue with <script setup lang="ts">
  2. Import in App.vue or parent component
  3. Add i18n keys to all locale files in i18n/locales/ (en, fi, sv, de, fr, es, it, da, no)
  4. Use @/ alias for imports from src/