Skip to content

Customer Portal — feature audits ​

PrimeVue + @aadalen/ui · en/nb. 37 features audited. Totals across this project: Blocker 11 · High 70 · Medium 180 · Low 79.

See the feature queue for the inventory and the project-level findings for architectural root-causes. Severity counts are parsed from each draft's finding headers.

Every feature below links to its full audit.

Account selection ​

Blocker 0 · High 4 · Medium 4 · Low 2

The picker page itself is thin but the real problem is the state it writes. The selected account is persisted to localStorage under admin-selected-account, and nothing ever clears or validates it — not sign-out, not the router guard, not the store. …

Admin analytics ​

Blocker 0 · High 0 · Medium 3 · Low 3

admin.analytics is a single, self-contained page (AnalyticsPage.vue) that renders three KPI-tile groups (Users & Accounts, Operations, Sync Health) from one adminStatsGetStats query polled every 30s. There are no charts despite the assigned `data-…

Admin configuration ​

Blocker 0 · High 1 · Medium 5 · Low 2

Three sibling admin config screens — Settings (default role + notification emails), Standards (CRUD list of global skills/machine categories), and Terms (per-locale rich-text editor with a publish toggle). They are functionally sound and follow the repo's t…

Bids ​

Blocker 0 · High 4 · Medium 6 · Low 4

Bids is the app's money path — a bid is a financial commitment to buy a machine — and it is the least defended surface audited so far. The same rounding defect found in members exists here: a shared Intl.NumberFormat with maximumFractionDigits: 0 on…

Book service ​

Blocker 0 · High 6 · Medium 4 · Low 2

BookServicePage.vue is a single-page form (not a wizard, despite the queue's advanced/wizard tag), and it is the point where a customer commits to a real service booking. The single most important thing: **once "Send Request" is clicked the request beco…

Catalog ​

Blocker 0 · High 1 · Medium 7 · Low 2

The Catalog is a well-built feature by this repo's own standards: it uses <PageLayout> + <DataTable card-only> rather than hand-rolled shells, SafeImage on the card thumbnail, useBackNavigation on the detail page, and full en/nb key parity for every…

Charging (internal) ​

Blocker 0 · High 0 · Medium 4 · Low 2

A well-considered, mobile-first select-then-confirm screen for warehouse staff to batch-confirm lifts put on charge. Error handling for the write path is genuinely good (persistent role="alert" notice, category-mapped copy, retry that preserves selectio…

Commands (admin) ​

Blocker 0 · High 1 · Medium 3 · Low 1

A read-only admin table of Dataverse commands plus a detail modal whose only action is "Retry" for terminally-failed commands. The retry path works but is completely silent — success closes the modal with no confirmation and failure shows the admin nothing …

Contact Aadalen ​

Blocker 0 · High 0 · Medium 2 · Low 1

A small, clean single-purpose contact form (subject + message → POST /api/v1/contact) built on PrimeVue InputText/Textarea/Message/Button inside @aadalen/ui PageLayout. Both labels are correctly associated, an idempotency key is sent, and copy…

Contacts ​

Blocker 2 · High 1 · Medium 6 · Low 2

Contacts is a small card-list page over a shared DataTable, plus a five-field create/edit sheet. The list half is unremarkable and inherits the known packages/ui defects. The write half is not: **two independent Blockers mean the feature cannot work tod…

Customer assets ​

Blocker 1 · High 4 · Medium 7 · Low 2

The three asset surfaces split sharply. CustomerAssetsPage / CustomerAssetDetailPage are competent — PageLayout, DataTable with a card slot, SafeImage, real skeletons, a history-aware back affordance — with a cluster of ordinary defects around err…

Dashboard ​

Blocker 0 · High 1 · Medium 5 · Low 5

HomePage.vue is the post-sign-in landing surface for every authenticated user and is in better shape than most of this app: the skeleton is correctly gated on an isLoading load state (not on a data value), the error branch is reachable and carries a ret…

Email verification ​

Blocker 0 · High 1 · Medium 5 · Low 2

verify-email is a hard gate: router/index.ts:664-669 bounces every non-internal, unverified user back to this page from any route they attempt, and the page itself offers only two controls — Resend and I've verified my email. There is no sign-out, n…

Emails (admin) ​

Blocker 0 · High 0 · Medium 3 · Low 2

A read-only admin log: EmailsPage.vue renders a server-paginated @aadalen/ui DataTable of email messages with status/type Select filters and a row-click detail modal (EmailDetailModal.vue). The data flow, sandboxed HTML preview and design-token us…

Error states ​

Blocker 0 · High 0 · Medium 2 · Low 0

The 403 surface (ForbiddenPage.vue, route forbidden) is in good shape on the things that matter most: it renders inside AppLayout (full sidebar present), its copy tells the user what to do next ("contact an administrator"), it offers an explicit non-l…

Landing page ​

Blocker 0 · High 0 · Medium 2 · Low 2

The landing page is in good shape for a marketing front door: it is the one audited customer-portal page that actually has a real <h1>, its heading levels do not skip, its copy is fully translated in both en and nb with no key drift, and the decorativ…

Locations ​

Blocker 0 · High 3 · Medium 5 · Low 2

Locations is a small, well-structured card list built on the shared @aadalen/ui DataTable + PageLayout shells, so it inherits the project-level list defects rather than adding new ones at the list level. The specific problems are all in the **write pa…

Marketplace ​

Blocker 0 · High 3 · Medium 8 · Low 3

The marketplace grid and listing detail are well-built by this repo's own conventions — PageLayout/DataTable shells, server-side filters that really do reach the API, and complete en/nb parity for every marketplace.* key. The problems are concentrated…

Onboarding ​

Blocker 1 · High 3 · Medium 6 · Low 3

OnboardingPage.vue is a single 7-field form that has to serve three different outcomes of the auto-link guard (no_match, multiple_matches, and "auto-link threw"), and it only really serves one of them. The most important defect is that a *successful…

Orders logistics (internal) ​

Blocker 0 · High 2 · Medium 3 · Low 1

Internal "Ready for logistics" queue: a card list of work orders, each confirmed as picked-up by drawing a signature in a dialog that fires a durable Dataverse command. The durable-command error handling (CommandErrorNotice, persistent, role="alert", ca…

Partner service orders ​

Blocker 0 · High 3 · Medium 7 · Low 1

This is the service partner's whole working surface — the list of jobs assigned to their resource and the screen where they confirm, schedule and complete each one — and it is built to the repo's own conventions almost everywhere (PageLayout + DataTable she…

Password reset ​

Blocker 1 · High 4 · Medium 9 · Low 3

The backend half of this flow is largely right — better-auth is configured with a 1-hour token TTL, per-endpoint rate limits and revokeSessionsOnPasswordReset: true (SEC-03 passes cleanly, server-side). The frontend half is the weak side: the reset page n…

Profile ​

Blocker 0 · High 1 · Medium 6 · Low 2

A well-built, single-page settings screen (identity, password, roles/links, language) with real error/loading states, a working strength meter, full en/nb i18n parity, and a password change that correctly requires the current password. The most important is…

Roles & permissions (admin) ​

Blocker 0 · High 1 · Medium 4 · Low 1

A read-only admin surface: a roles DataTable and a per-role detail page that renders a permission matrix (PermissionGrid) plus the users in that role. Roles are code-defined, so there is genuinely nothing to save — the whole editing half of `PermissionG…

Service overview ​

Blocker 1 · High 3 · Medium 5 · Low 2

The Service overview merges bookings and service-history rows into one paginated list with two server-side filters, and its detail route (bookings.detail) is a well-built read-only page. Two things are actually broken rather than merely rough: the **merge…

Service plans & checklist builder ​

Blocker 2 · High 4 · Medium 7 · Low 3

The checklist builder can only be operated with an HTML5 mouse drag: there is no click-to-add, no move-up/down, no keyboard path and no touch fallback, so a keyboard user cannot create a single field and — because HTML5 drag events do not fire on touch and …

Shipping orders (internal) ​

Blocker 0 · High 0 · Medium 3 · Low 3

A well-built internal goods-reception flow: a card-only list with pending/received tabs, and a detail page whose per-line decisions, partial-quantity steppers, optimistic mutations and server-polled write-back progress are unusually careful, with persistent…

Sign in ​

Blocker 0 · High 1 · Medium 5 · Low 4

The form itself is largely correct: real <label for> pairs, autocomplete="email" and autocomplete="current-password", paste unblocked, submit never gated on validity, and PrimeVue Message for errors. The two things that matter are (a) a server-side …

Sign up ​

Blocker 0 · High 1 · Medium 4 · Low 3

SignUpPage.vue is a clean, well-labelled four-field form that gets the basics right (associated <label>s, correct autocomplete tokens, paste allowed, nothing focusable between the last field and submit). Everything around the password, however, is m…

Skills & employees ​

Blocker 0 · High 1 · Medium 3 · Low 1

The list surface (SkillsPage, @aadalen/ui DataTable) is keyboard-accessible on desktop, but the employee-detail surface is not: the skill and training lists are hand-rolled <li @click> rows with no role, tabindex or key handler, so a keyboard user c…

Sync monitoring (admin) ​

Blocker 0 · High 0 · Medium 5 · Low 2

Sync monitoring is a competent admin console — five surfaces (status, runs, events, failures) behind a tab strip, live polling that backs off when idle, server-side entity/status/mode filters, and toast feedback on every trigger. Its weakest points are that…

Terms ​

Blocker 0 · High 0 · Medium 3 · Low 2

Terms is a thin read-only page: useTermsQuery fetches published, locale-resolved HTML and TermsPage.vue renders it through PageLayout after DOMPurify.sanitize. The sanitisation is correct and is the right call (SEC/XSS handled). The defects are all …

Trade-in machines ​

Blocker 1 · High 5 · Medium 6 · Low 1

The list and detail pages are competent; the create page is not. Selecting or removing the main image sends a PUT that the API expands into field ?? null for every other column, so an ordinary "add a photo" click **silently deletes every text field alread…

User administration (admin) ​

Blocker 1 · High 5 · Medium 7 · Low 0

The two admin screens that govern who can sign in and what they can see have one outright break and a cluster of silent-failure defects around destructive actions. admin.users cannot create a user at all: DataTable gates its create button on the `:c…

Warehouse take-out (internal) ​

Blocker 0 · High 0 · Medium 2 · Low 3

A well-built internal pick flow: real <button> rows (not click-only <li>), 44px tap targets, mapped command-error copy (no raw SDK strings), full en/nb i18n parity, and correct camera-stream teardown on unmount. The defects are a11y-flavoured polish on …

Warranty submission (public) ​

Blocker 1 · High 3 · Medium 7 · Low 3

The token verification itself is sound — verifyWarrantyToken runs server-side on every endpoint before any data is returned, and the form only renders after getContext succeeds, so SEC-02 passes. What fails is everything around it: the page carrying…

Work orders ​

Blocker 0 · High 3 · Medium 7 · Low 2

Work orders is the customer's "where is my order" screen, and its two distinguishing pieces — the status/type filter bar and the pipeline stepper — are both wrong in ways the shared DataTable defects do not explain. The filters run in the page over the cu…