AGENTS.md:
- Add "./gradlew coverage" to All-in-one quick-start section
- Add "npm run test:coverage" to Frontend commands
- Add Coverage section: command, threshold table (70% lines, 60%
branches, 70% functions), HTML report paths for both layers
- Note that coverage is enforced during ./gradlew check
CODING_GUIDELINES.md:
- Section 1 (General Principles): add "Treat warnings as mistakes"
rule — LSP diagnostics, compiler warnings, and lint warnings are
bugs that must be fixed before commit
- Known false positives (Lombok, getActivePinia) must be suppressed
explicitly at the narrowest scope with a comment explaining why
- Uncommented suppressions are treated as errors
- Section 7 (Testing): add Coverage subsection with thresholds table,
command reference, report paths, and enforcement rule (PRs must
maintain or improve coverage)
- frontendCoverage: runs 'npm run test:coverage' in frontend directory
(vitest with coverage, enforces thresholds internally)
- coverage: group='verification', runs backend jacocoTestReport and
frontendCoverage sequentially — single command for both layers:
./gradlew coverage
- check task continues to run only: frontendLint → frontendTest
(coverage verification is added per-module: jacocoTestCoverage
Verification on backend, vitest thresholds on frontend)
- Add jacoco plugin (bundled with Gradle, no extra dependency)
- jacocoTestReport: generates HTML + XML reports, runs after test
- jacocoTestCoverageVerification: enforces 70% line coverage and
60% branch coverage at the bundle level
- Wire jacocoTestCoverageVerification into tasks.named('check') so
./gradlew check blocks if coverage drops below thresholds
- HTML report output: backend/build/reports/jacoco/index.html
- test task finalizedBy jacocoTestReport so report is always
available after running tests
Vitest (14 tests) — AdminDashboard.spec.ts:
- renders heading, subtitle, table columns, order data in rows
- shows loading, empty, and error states
- fetches GET /api/admin/orders on mount
- expands row on click to reveal letter content (Brevtext label)
- collapses row on second click
- only one row expanded at a time (clicking row 2 closes row 1)
- status dropdown change fires PATCH /api/admin/orders/{id}/status
with correct URL, method, and JSON body
- shows error message on failed status update
Playwright E2E (8 tests) — admin-dashboard.spec.ts:
- admin login (admin@bilhalsning.se / test1234) before each test
- admin can navigate to /admin and see heading
- non-admin user (test@bilhalsning.se) is redirected away from /admin
- table renders Datum/E-post/Regnr/Status column headers
- seeded order plates visible (ABC123, DEF456, GHI789)
- click row expands letter content
- click again collapses letter content
- status dropdown change persists (selectOption delivered)
- unauthenticated access redirects to login with ?redirect=/admin
- api/admin.ts: AdminOrder interface (id, email, plate, letterText,
status, trackingId, amountPaid, createdAt), fetchAllOrders() calls
GET /api/admin/orders, updateOrderStatus(orderId, status) calls
PATCH /api/admin/orders/{id}/status
- AdminPage.vue replaces placeholder with full dashboard:
- Table columns: Datum, E-post, Regnr, Status, expand chevron
- Click any row to toggle expanded letter preview below the row
- Only one row expanded at a time; second click collapses
- Status column has a <select> dropdown showing Swedish labels
- Changing dropdown fires PATCH API immediately (no save button)
- On API failure the dropdown reverts to previous value and a
red inline error "Kunde inte uppdatera status" appears
- Loading, empty, and API error states with Swedish messages
- Responsive table wrapper for horizontal scroll on small screens
- Expanded rows use a separate <tr> with colspan(5) for clean
table semantics
- AdminOrderResponse DTO: extends OrderResponse with email (from User
relation) and letterText fields, exposing the full order for admin review
- UpdateStatusRequest DTO: single "status" field validated against all
six OrderStatus values (pending_payment|paid|lookup_started|sent|
delivered|failed) with Swedish error messages
- OrderService.getAllOrders(): delegates to OrderRepository
.findAllByOrderByCreatedAtDesc() which uses @EntityGraph to eagerly
fetch the user relationship in a single query
- OrderService.updateOrderStatus(orderId, statusString): looks up order,
converts status string to OrderStatus enum via case-insensitive
valueOf(), saves updated entity
- AdminController /api/admin:
GET /orders → list all orders with user email (admin only)
PATCH /orders/{id}/status → update order status (admin only)
- toAdminResponse() mapper safely handles null user (empty email fallback)
- JwtAuthenticationFilter now extracts the "role" claim from the JWT
token and creates a SimpleGrantedAuthority("ROLE_" + role.toUpperCase())
on the authentication token. Previously the authorities list was
always empty (only userDetails.getAuthorities() which returned List.of())
- SecurityConfig adds .requestMatchers("/api/admin/**").hasRole("ADMIN")
so admin endpoints require the ROLE_ADMIN authority
- All existing endpoints remain authenticated() only — no existing user
flow is affected
- Public endpoints (auth, webhooks, vehicles) still permitAll()
- Add @ManyToOne(fetch = LAZY) + @JoinColumn(name = "user_id",
insertable = false, updatable = false) to Order entity so ORM can
navigate order.getUser().getEmail() for admin responses
- Keep userId as writable UUID field; the relationship is read-only
to preserve backward compatibility with existing setUserId() calls
- Add getUser() / setUser() accessors
- Replace handwritten @Query JOIN FETCH with Spring Data derived method
findAllByOrderByCreatedAtDesc() annotated with @EntityGraph(attributePaths
= {"user"}) — same eager-load behavior, zero custom JPQL
- No database schema change: user_id FK already exists
Templates serve as a brand shield (showing the platform facilitates all
kinds of messaging), not as a compose-flow form control. Remove them from
the data model and compose page. Templates will live as branding elements
on the landing page in a future commit.
Backend:
- Remove template field from Order entity (getter/setter removed)
- Remove template from CreateOrderRequest DTO
- Remove template from OrderResponse DTO
- Remove template param from OrderService.createOrder()
- Remove template passthrough in OrderController
- Remove /api/templates permitAll from SecurityConfig
- Edit V5 migration: remove template column from orders table
- Edit V6 migration: remove template from seed data
- Update OrderControllerTest (remove template from assertions/requests)
- Update OrderServiceTest (remove template from createOrder calls)
Frontend:
- Remove template from Order interface in api/orders.ts
- Remove template param from createOrder() function
- Remove template display from OrdersPage.vue cards
- Rewrite ComposePage.vue: remove template selector, keep textarea + preview + submit
- Update ComposePage.spec.ts (remove template tests, add preview/GDPR tests)
- Update OrdersPage.spec.ts (remove template from mock data and display test)
- Update compose.spec.ts E2E (remove template selector interactions)
- Update order-history.spec.ts E2E (remove template names test)
- Fix unused import in Router.spec.ts
- Also includes minor Prettier formatting in AppHeader.spec.ts, AdminPage.vue, authStore.ts
- Add createOrder(plate, template, letterText) to frontend api/orders.ts
- Create data/templates.ts with 6 Swedish letter templates (Komplimang,
Jag vill köpa din bil, Tips / servicebehov, Synpunkter på körbeteende,
Tuta / frustration, Fritt meddelande) with pre-filled body text
- Rewrite ComposePage.vue with full compose flow:
- Template selector dropdown (Fritt meddelande selected by default)
- Textarea with 1000-char limit and live character counter
- Inline A4 letter preview with plate, body, and GDPR Art. 14 footer
- 'Skicka brev (49 kr)' submit button, disabled when empty
- On success: redirects to /orders; on error: shows error message
- Shows error with back link if no plate in route query
- Add 12 Vitest tests for ComposePage (template fill, char counter, submit
validation, createOrder call, navigation, null template for Fritt meddelande)
- Add 8 Playwright E2E tests (auth guard, no-plate error, template selection,
textarea edit, submit button state, order creation, preview content)
- Create CreateOrderRequest DTO with jakarta.validation annotations
- Validate plate format (ABC123 or ABC12A) via @Pattern regex
- Validate letter text: @NotBlank, @Size(min=1, max=1000)
- Validate template name: optional, @Size(max=50)
- Add POST /api/orders endpoint to OrderController (auth required)
- Return 201 Created with OrderResponse on success
- Add 5 controller tests: no auth (403), create success, invalid plate,
empty text, text over 1000 chars
- All messages in Swedish (Ogiltigt registreringsnummer, Brevtext krävs, etc.)
- Add 'Mina beställningar' RouterLink to AppHeader in authenticated template
- Add Vitest tests: link visible when authenticated, hidden when not
- Add Playwright E2E test: shows orders link when authenticated
- Add Playwright E2E test: can navigate from home to orders via header link
Update AppHeader to reflect authentication state. When not authenticated,
show Logga in and Registrera links. When authenticated, show the user's
email address and a Logga ut button. Uses v-if/v-else with template blocks
for clean conditional rendering without wrapper elements.
Changes:
- authStore: add email computed that extracts sub claim from JWT payload
- AppHeader: conditional nav with v-if/v-else (guest vs authenticated)
- AppHeader: add email display and logout button with styles
- App.spec.ts: add Pinia to test setup (required by AppHeader now)
- AppHeader.spec.ts: rewrite with tests for both auth states
- authStore.spec.ts: add tests for email extraction and clearing
- header-auth.spec.ts: 5 Playwright E2E tests for header auth state
Add a named volume for backend build artifacts to prevent root-owned files
created inside the container from blocking host Gradle builds. This follows
the same pattern as the existing backend-gradle-project volume.
Configure OpenCode with LSP, formatter, auto-compaction, and file watcher
settings for improved development experience.
Changes:
- docker-compose.yml: add backend-build:/app/backend/build volume
- opencode.json: enable lsp, formatter, auto-compaction, prune, and
file watcher with ignore patterns for node_modules, .git, dist, build
Implement client-side route protection with role-based access control. The auth
store now extracts the role claim from JWT tokens and exposes isAdmin. Router
guards enforce three levels of access: guestOnly (redirect authenticated users),
requiresAuth (redirect unauthenticated to login with redirect param), and
requiresAdmin (redirect non-admin users to home).
Changes:
- utils/jwt.ts: JWT payload parser using base64url decode (new file)
- authStore: add role ref, isAdmin computed, extractRole from JWT payload
- router: add route metadata (requiresAuth, requiresAdmin, guestOnly) and
beforeEach guard with getActivePinia() safety for test environments
- OrdersPage.vue, AdminPage.vue: placeholder pages (new files)
- LoginPage.vue, RegisterPage.vue: use route.query.redirect after auth
- Router.spec.ts: 14 tests covering all guard scenarios
- authStore.spec.ts: tests for role extraction, isAdmin, role persistence
- LoginPage.spec.ts: test for redirect query param after login
- auth-guards.spec.ts: 7 Playwright E2E tests for guard behavior
- login.spec.ts: fix seed user credentials (test@bilhalsning.se)
Add role-based access control to the backend authentication system. The User
entity now carries a role field (default 'user'), JWT tokens include a 'role'
claim, and the login endpoint populates it from the database.
Changes:
- User entity: add role column (VARCHAR(20), default 'user') with getter/setter
- JwtService: add generateToken(email, role) overload and extractRole(token)
- AuthController: pass user.getRole() on login, 'user' on register
- Flyway V3: ALTER TABLE users ADD COLUMN role
- Flyway V4: seed admin user (admin@bilhalsning.se, role='admin')
- AuthControllerTest: add tests for admin role in token, role from DB on login
- JwtServiceTest: add tests for role extraction and default role
- UserServiceTest: assert role defaults to 'user' on createUser
Add Flyway migration V2 that inserts a pre-seeded test user for manual
testing. This avoids having to register a new account every time the
environment is reset.
- Email: test@bilhalsning.se
- Password: test1234
- Password hash: bcrypt ($2b$12$)
The migration uses a plain INSERT (no ON CONFLICT) since it runs on
fresh databases only. H2-compatible — no PostgreSQL-specific syntax.
To re-seed after deletion: docker compose down -v && docker compose up -d
Add infrastructure for running Playwright E2E tests in Docker and fix
Gradle lock conflicts between host and container builds.
Changes:
- Add docker-compose.ci.yml that starts postgres, backend, frontend,
and a Playwright service for CI pipelines. Uses official
mcr.microsoft.com/playwright:v1.60.0-noble image.
- Add backend-gradle-project named volume to docker-compose.yml so the
container's .gradle/ directory is isolated from the host's. This
prevents stale lock files from host Gradle builds (e.g. ./gradlew
:backend:test) crashing the container's bootRun.
- Add .dockerignore excluding .gradle, .env, .git, frontend/node_modules,
and backend/build from the Docker build context.
- Add frontendE2E Gradle task that runs npm run test:e2e:ci.
Add the frontend login page (LoginPage.vue) with email and password
fields, Swedish UI strings, and integration with the backend login
endpoint. Also sets up Playwright as the E2E testing framework with
browser tests for both login and registration flows.
Frontend login implementation:
- Add LoginPage.vue with form validation, error handling, and link to
registration page
- Add login() API function in auth.ts
- Add loginUser() method to authStore that stores JWT token
- Add /logga-in route to Vue Router
- Add 'Logga in' nav link to AppHeader alongside existing 'Registrera'
- Add 10 unit tests for LoginPage component
- Add 4 unit tests for loginUser auth store method
- Add 1 route resolution test and 1 AppHeader link test
Playwright E2E setup and tests:
- Install @playwright/test and configure playwright.config.ts
- Add npm scripts: test:e2e (local) and test:e2e:ci (Docker CI)
- Exclude e2e/ directory from Vitest to prevent test runner conflicts
- Add .gitignore entries for test-results/ and playwright-report/
- Add 5 E2E tests for login (navigation, invalid credentials, success
redirect, navigation to register, input types)
- Add 6 E2E tests for register (navigation, success redirect, validation
errors for invalid email/short password/mismatched passwords,
navigation to login)
Add POST /api/auth/login endpoint that authenticates users by email and
password, returning a JWT token on success. Also fixes a critical bug
where expired or malformed JWT tokens in the Authorization header caused
unhandled exceptions, crashing requests to all endpoints including public
ones like registration.
Changes:
- Add AuthController.login() endpoint with LoginRequest DTO
- Add UserService.authenticate() that validates credentials and throws
InvalidCredentialsException on failure
- Add InvalidCredentialsException and GlobalExceptionHandler handler
that maps it to 401 with Swedish error message
- Fix JwtAuthenticationFilter to catch JwtException (expired, malformed)
and pass through without crashing — the filter now acts as a graceful
enricher rather than a gatekeeper
- Add 5 controller tests for login endpoint (success, 401, validation)
- Add 4 service tests for authenticate() (success, email not found,
password mismatch, email normalization)
- Add 2 filter tests for expired and malformed token pass-through
Update all references to match the new repo-root Gradle layout
after moving the wrapper out of backend/.
- Quick Start: add ./gradlew up alternative and hint at ./gradlew check
- Spring profiles: ./gradlew bootRun → ./gradlew :backend:bootRun
- Development section: add All-in-one subsection with check/up/down/reset
- Backend dev: cd backend && ./gradlew bootRun → ./gradlew :backend:bootRun
- Development vs Production table: ./gradlew bootRun → ./gradlew :backend:bootRun
- Project Structure tree: add gradlew, gradle/, settings.gradle, build.gradle
- Remove ARCHITECTURE.md reference (file never existed)
- Add Database reset section with ./gradlew reset
Also add .gradle/ and build/ to .gitignore with gradle-wrapper.jar
exception (was staged but not committed with previous refactor).
Move gradlew, gradle/wrapper, and settings.gradle from backend/ to
the repo root so build commands run from the top-level directory.
This follows the standard multi-project Gradle layout where the build
tool lives alongside docker-compose.yml and all submodules.
- Move gradlew + gradle/wrapper/* from backend/ to repo root
- Move settings.gradle to root with rootProject.name and include 'backend'
- Create root build.gradle with convenience tasks: check, up, down, reset
- check task chains frontend lint → frontend test → backend check
- Update docker-compose.yml backend volume from ./backend:/app to .:/app
- Update backend.Dockerfile entrypoint to ./gradlew :backend:bootRun
- Update AGENTS.md: document ./gradlew check, up, down, reset
- Delete backend/settings.gradle (now at root)
- Add .gradle/ and build/ to .gitignore
- Add !gradle/wrapper/gradle-wrapper.jar exception (blocked by *.jar rule)
All 38 frontend tests and 33 backend tests pass via ./gradlew check.
Add AppHeader and AppFooter to give the site a consistent chrome
around the core page content. Add ComposePage stub reachable via
"Skicka ett brev till ägaren" CTA on HomePage after vehicle lookup
succeeds. Add stub pages for about, contact, and privacy.
- Create AppHeader.vue with logo link (BilHälsning) and Hem nav link
- Create AppFooter.vue with 4 links: Om oss, Kontakt, Integritetspolicy, Villkor
- Create ComposePage.vue stub that reads plate from route query params
- Create AboutPage.vue and ContactPage.vue stub pages
- Add 4 new routes: /compose, /om, /kontakt, /integritetspolicy
- Update App.vue to render AppHeader + <main> + AppFooter around RouterView
- Add home__cta RouterLink button to HomePage, visible only when vehicle
lookup succeeds, linking to /compose?plate=<plate>
- Remove BilHälsning h1 from HomePage (moved to header)
- Add 17 new tests: AppHeader (2), AppFooter (1), ComposePage (3),
AboutPage (1), ContactPage (1), HomePage rewrite (6), App update (2)
- Update App.spec.ts to verify header/footer components render
Move vehicle-info display logic out of HomePage into a reusable
VehicleInfo component. The component accepts vehicle, loading,
notFound, and plate props and renders the correct state with
priority: vehicle card > loading > not found. Follows the
small-page-component pattern from CODING_GUIDELINES.md.
- Create VehicleInfo.vue with 3-state v-if chain and scoped styles
- Define and export VehicleInfo interface (make/model/year/color)
- Add VehicleInfo.spec.ts with 7 tests covering all states and
priority edge cases
- Update HomePage.vue to use VehicleInfo, replacing 3 inline
v-if/else-if blocks with a single component tag
- Remove 5 unused CSS classes from HomePage (home__status,
home__vehicle, home__vehicle-text, home__not-found,
home__not-found p)
- Update AGENTS.md to require thorough commit messages with bullet
points
- V1__create_users_table.sql replaces placeholder: creates users table with
id UUID PK, email UNIQUE NOT NULL, password_hash NOT NULL, subscription
VARCHAR(20) DEFAULT 'none' with CHECK constraint (none/basic/pro),
created_at/updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP.
Compatible with both H2 and PostgreSQL.
- SecurityConfig: minimal @Configuration providing BCryptPasswordEncoder
bean. Required because Spring Boot 4 no longer auto-configures a
PasswordEncoder.
- Subscription enum: NONE, BASIC, PRO with string values matching the DB
CHECK constraint.
- User entity: @PrePersist generates UUID and timestamps in application
code, @PreUpdate refreshes updated_at. Email setter normalizes to
lowercase for case-insensitive uniqueness. Explicit getters/setters
(no Lombok per guidelines).
- UserRepository: Spring Data JPA extending JpaRepository<User, UUID>.
findByEmail(Optional) and existsByEmail for duplicate checks.
- UserService: @RequiredArgsConstructor with constructor-injected
UserRepository and PasswordEncoder. createUser normalizes email,
checks duplicates via existsByEmail, throws EmailAlreadyExistsException,
hashes password with BCrypt, saves. findByEmail returns Optional<User>.
- EmailAlreadyExistsException: custom RuntimeException for duplicate
registration attempts. ControllerAdvice handler deferred to auth ticket.
Verification: ./gradlew test passes (Flyway + H2 context loads).
docker compose up -d succeeds, Flyway applies V1 against PostgreSQL 16.
\d users confirms all columns, constraints, defaults, and indexes.
- Generate from Spring Initializr with Gradle Groovy DSL, Java 21, Spring Boot 4.0.6
- Dependencies: Web, Security, Data JPA, PostgreSQL Driver, Flyway, Validation, Lombok
- Add H2 runtime dependency for zero-setup local development
- Configure application.yml: H2 in-memory database, port 8080, Flyway with ddl-auto=validate
- Create placeholder Flyway migration V1__init_schema.sql
- Verify ./gradlew test passes and ./gradlew bootRun starts on port 8080
- Update AGENTS.md and README.md: Maven → Gradle commands, Spring Boot 3 → 4