Categorises remaining apps/app eslint problems after the Tier 1 + Tier 2 autofix and whitespace cleanup. Six buckets: - X. Tier 3 deferred (vite.config.ts, themeConfig.ts, vitest.config.ts) — 134 - A. Trivial-fix (second-pass autofix residue + unused imports) — 42 - B. Type safety (mostly no-explicit-any in vendored Vuexy code) — 34 - C. Style preference / rule-config — 8 - D. Vuetify / Vuexy idiom (ml-/pl- restricted-class) — 5 - E. Bug-shaped (security, isAxiosError, missing-return, fire-and-forget) — 8 Sum check: 134 + 42 + 34 + 8 + 5 + 8 = 231 ✓ Five open questions for Bert + Claude Chat decisions before 1b-ii: - Should src/@core/** + src/@layouts/** be in ignorePatterns? - vue/no-restricted-class regex scope (RTL-aware vs ml/pl only)? - Stance on vue/prefer-true-attribute-shorthand? - Bucket E.5 axios fire-and-forget pattern (void / .catch / disable)? - Decoupling pnpm lint from --fix? Lint baseline: 1451 → 231 this session, with Tier 3 (134) + non-fixable (66) + second-pass-residue (31) deferred to 1b-ii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
21 KiB
WS-3 Lint baseline audit — apps/app
Date: 2026-04-29
Source: pnpm exec eslint . -c .eslintrc.cjs --ext .ts,.js,.cjs,.vue,.tsx,.jsx --no-fix
after WS-3 session 1b-i Tasks 1, 2, 3 (Tier 1 + Tier 2 autofix +
trailing-whitespace cleanup).
Counts
| Stage | Total | Errors | Warnings | Fixable |
|---|---|---|---|---|
Pre-1b-i (main, dd8430f) |
1451 | 919 | 532 | 1370 |
Post-Tier 1 (47bd533) |
422 | 408 | 14 | 354 |
Post-Tier 2 (a7eaf0f) |
246 | 232 | 14 | 180 |
Post-whitespace (4976b4e) |
231 | 217 | 14 | 165 |
Reduction: 1451 → 231 mechanical, with Tier 3 deferred.
The 165 still-fixable items break down as:
- 134 in Tier 3 files (vite.config.ts, themeConfig.ts, vitest.config.ts) — explicitly excluded from this session per the risk-tier policy.
- 31 in Tier 1/2 paths — second-pass-autofix residue (mainly
24
indenterrors in one file, pluslines-around-commentedge cases on Vue SFC<script>-tag-adjacent comments).
Bucket summary
| Bucket | Count | Recommended disposition |
|---|---|---|
| X. Tier 3 deferred | 134 | 1b-ii, hand-reviewed per file |
| A. Trivial-fix | 42 | 1b-ii, mechanical (second-pass --fix or simple delete) |
| B. Type safety | 34 | 1b-ii, per-instance review |
| C. Style / rule-config | 8 | 1b-ii, rule-level decisions |
| D. Vuetify / Vuexy idiom | 5 | 1b-ii, per-pattern overrides |
| E. Bug-shaped | 8 | Investigate as bugs |
| Sum | 231 |
Sum check: X(134) + A(42) + B(34) + C(8) + D(5) + E(8) = 231 ✓ (matches total).
Bucket X — Tier 3 deferred (134 items, all fixable)
All in three configuration files, all autofix-mechanical. Excluded from this session per the risk-tier policy because the autofix could in principle alter Vite plugin order, theme-token resolution, or vitest setup-hook ordering.
apps/app/vite.config.ts — 91 items
58 @typescript-eslint/quotes
15 semi
15 @typescript-eslint/semi
2 arrow-parens
1 curly
apps/app/themeConfig.ts — 42 items
28 @typescript-eslint/quotes
7 semi
7 @typescript-eslint/semi
apps/app/vitest.config.ts — 1 item
1 lines-around-comment
Recommendation for 1b-ii: per-file diff review before applying
--fix. Focus areas:
vite.config.ts: confirm no plugin-order changes (the autofix is pure quotes/semi/arrow-parens; in principle no semantic effect). Smokepnpm devandpnpm buildafter.themeConfig.ts: confirm no theme-token reordering. Visually check the running app afterwards (login + dashboard).vitest.config.ts: trivial — singlelines-around-commentfix.
Bucket A — Trivial-fix (42 items)
A.1 Second-pass autofix residue (31 items, all fixable)
Items where Tier 1 / Tier 2 --fix ran but ESLint's default 10-pass
cap left a cascade of dependent fixes unresolved. Re-running --fix
on the same file resolves them (verified in Task 3 on
DefaultLayoutWithVerticalNav.vue).
By rule:
24 indent (all in useTimeSlotDropdown.ts)
3 vue/max-attributes-per-line (App.vue — outside Tier 1 glob)
3 lines-around-comment (PortalLayout/PublicLayout/AppKpiCard — see A.2)
1 (no rule) (VNodeRenderer.tsx — eslint-disable comment that's now unused)
By file:
24 apps/app/src/composables/useTimeSlotDropdown.ts
3 apps/app/src/App.vue
1 apps/app/src/@layouts/components/VNodeRenderer.tsx
1 apps/app/src/components/AppKpiCard.vue
1 apps/app/src/layouts/PortalLayout.vue
1 apps/app/src/layouts/PublicLayout.vue
Recommendation for 1b-ii:
- Re-run
pnpm exec eslint <file> --fixon each (single command per file is safest) and verifygit diff --checkclean afterwards. - For App.vue: it's at
src/App.vue, missed by Tier 1'ssrc/{components,pages,layouts,views}/**/*.vueglob. Fix by includingsrc/App.vuein the Tier 1 paths in the next pass. - For VNodeRenderer.tsx (
src/@layouts/): vendored Vuexy code; the fix is just removing aneslint-disabledirective that's no longer needed. Low risk.
A.2 Vue-SFC lines-around-comment autofix gap (3 of the 31 above)
PortalLayout.vue, PublicLayout.vue, and AppKpiCard.vue show
error Expected line before comment lines-around-comment at line 2:1
because the comment block sits immediately after <script setup lang="ts"> on line 1. The rule has a fixer and the files were inside
the Tier 1 glob, but the fix did not apply — likely a vue-eslint-parser
boundary quirk (the script-tag opening doesn't register as a "block
start" the way a { does for allowBlockStart: true).
Recommendation for 1b-ii: either (a) hand-add a blank line before
the comment in each file, or (b) extend .eslintrc.cjs with an
overrides block that disables lines-around-comment for *.vue files
(or sets allowBlockStart: true more aggressively).
A.3 Unused-imports / unused-vars (10 items, manual)
Trivial deletes. Each item below: file:line, the unused identifier, and recommended action.
apps/app/src/components/layout/OrganisationSwitcher.vue:30:10
'toggleMenu' is defined but never used.
→ Delete the destructured `toggleMenu` from the line 30 destructure.
(Both unused-imports/no-unused-vars and @typescript-eslint/no-unused-vars
flag the same identifier — count of 2 above is the same identifier.)
apps/app/src/components/sections/CreateShiftDialog.vue:40:9
'scenario' is assigned a value but never used.
→ Delete the `const scenario = ...` line.
(Two-rule overlap — same identifier.)
apps/app/src/pages/events/[id]/time-slots/index.vue:242:27
'event' is defined but never used. (vue/no-unused-vars)
→ Replace with `_event` in the slot scope, or remove from destructure.
apps/app/src/pages/organisation/companies.vue:8:7
'authStore' is assigned a value but never used.
→ Delete `const authStore = useAuthStore()` on line 8.
(Two-rule overlap.)
apps/app/src/pages/platform/activity-log/index.vue:11:7
'searchDebounced' is assigned a value but never used.
→ Delete the line. (Two-rule overlap.)
apps/app/src/components/persons/PersonDetailPanel.vue:77:58
Unnecessary { after 'if' condition. (curly)
→ Strip the redundant brace block (one-statement if).
Note on the rule overlap: unused-imports/no-unused-vars and
@typescript-eslint/no-unused-vars both fire on the same identifier
in 4 of the 5 cases. Removing the identifier will resolve both per
report — the 42 Bucket A total correctly counts these as 2 items
each (matching ESLint's actual emit). Effective unique deletes: 6.
Bucket B — Type safety (34 items)
B.1 @typescript-eslint/no-explicit-any (23 items)
Per CLAUDE.md frontend rule and .eslintrc.cjs line 62, this is
erroneously committed to error for the project. Yet 23 instances
exist, predominantly in src/@core/ and src/@layouts/ (the vendored
Vuexy code) and a few in our own files.
Distribution:
src/@core/**: 13 (DropZone.vue, I18n.vue, Notifications.vue, AppDateTimePicker.vue, createUrl.ts, useCookie.ts ×4, apexCharConfig.ts ×3, types.ts)src/@layouts/**: 7 (index.ts ×1, types.ts ×6)src/layouts/blank.vue: 1 (line 9 —ref<any>)src/layouts/default.vue: 1 (line 21 —ref<any>)src/layouts/components/NavSearchBar.vue: 1
The 13 + 7 = 20 inside @core and @layouts are vendored Vuexy
template code. CLAUDE.md says: "Never: TypeScript any type" — but
this rule was clearly intended for OUR code, not the vendored copy
of Vuexy that we don't maintain.
Recommendation for 1b-ii — judgment call needed:
- Option a (preferred for honesty): add
src/@core/**andsrc/@layouts/**toignorePatternsin.eslintrc.cjs. This is the same pragma we already apply tosrc/plugins/iconify/*.js(vendored). Reduces the 23 down to 3 (blank.vue, default.vue, NavSearchBar.vue) — allref<any>for theAppLoadingIndicatortemplate ref. Those 3 should be properly typed. - Option b: keep the rule on, narrow the 20 vendored items to proper types one by one. This is real work and risks breaking the vendored components' contract.
- Option c: per-file
// eslint-disable-next-linemarkers on the vendoredanys. Noisy but localised.
The .eslintrc.cjs already says (lines 60-62):
// Project rule (CLAUDE.md frontend rules): no `any`. Override the
// Vuexy reference (which sets this off) — Crewli's stricter posture.
'@typescript-eslint/no-explicit-any': 'error',
So the choice was deliberate when the rule was set. Option a is the cleanest re-statement: "the ban applies to our code, not vendored."
B.2 @typescript-eslint/no-use-before-define (7 items)
All in src/components/shifts/ShiftDetailPanel.vue (lines 249-261).
The pattern: handler functions defined at the top of <script setup>
reference cancellingAssignment and isCancelDialogOpen refs
declared further down in the file.
apps/app/src/components/shifts/ShiftDetailPanel.vue:249:3
'cancellingAssignment' was used before it was defined.
[...6 more in lines 250-261]
This is idiomatic Composition API: declare reactive state at the top,
then handlers, then watchers. The rule fires because handler
functions are hoisted but the ref(...) call results aren't.
Recommendation for 1b-ii: either (a) reorder the file to declare
the refs before the functions that reference them (mechanical, same
file only), or (b) add an override clause to .eslintrc.cjs allowing
this pattern in <script setup>. Option a is simpler and one-file.
B.3 @typescript-eslint/no-shadow (1 item)
apps/app/src/stores/useImpersonationStore.ts:119:11
'stored' is already declared in the upper scope on line 18 column 9.
Local stored shadowing an outer stored — usually fine but
defensive practice is to rename. Recommendation: rename the
inner stored to storedSnapshot or similar.
B.4 camelcase (3 items, same identifier)
apps/app/src/composables/api/useFormSchemas.ts:97:30
apps/app/src/composables/api/useFormSchemas.ts:99:17
apps/app/src/composables/api/useFormSchemas.ts:99:36
Identifier 'confirmed_name' is not in camel case.
Context (lines 95-100):
return useMutation({
mutationFn: async ({ id, confirmed_name }: { id: string; confirmed_name?: string }) => {
await apiClient.delete(`/organisations/${orgId.value}/forms/schemas/${id}`, {
params: confirmed_name ? { confirmed_name } : undefined,
})
},
confirmed_name is an API query parameter that the backend reads
in snake_case. CLAUDE.md says: "DB columns: snake_case" and
"TypeScript / JS variables: camelCase". The boundary is at the API
layer — the query-param key MUST be confirmed_name (snake) on the
wire. The local TypeScript variable confirmed_name matches the
wire format for symmetry (rather than confirmedName aliased).
Recommendation for 1b-ii — judgment call: rename the local
variable to confirmedName and keep the wire key snake_case
({ confirmed_name: confirmedName }). Three rule violations resolved
with no behaviour change.
Bucket C — Style / rule-config (8 items)
apps/app/src/components/form-failures/DismissFailureDialog.vue:43:3
sonarjs/prefer-single-boolean-return
"Replace this if-then-else flow by a single return statement."
apps/app/src/components/form-failures/FormFailureDetail.vue:44:5
no-void
"Expected 'undefined' and instead saw 'void'."
apps/app/src/components/sections/AssignShiftDialog.vue:41:3
sonarjs/prefer-single-boolean-return
apps/app/src/components/sections/SectionsShiftsPanel.vue:333:11
vue/prefer-true-attribute-shorthand
"Boolean prop with 'true' value should be written in shorthand form."
apps/app/src/components/shifts/AssignPersonDialog.vue:120:5
sonarjs/no-collapsible-if
"Merge this if statement with the nested one."
apps/app/src/components/shifts/AssignPersonDialog.vue:125:5
sonarjs/no-collapsible-if
apps/app/src/pages/events/[id]/settings/registration-fields.vue:335:13
vue/prefer-true-attribute-shorthand
apps/app/src/stores/useImpersonationStore.ts:105:12
sonarjs/no-collapsible-if
All readable-style preferences. Recommendation for 1b-ii: apply the suggested rewrite per item; all are local single-file edits with no behaviour change.
Bucket D — Vuetify / Vuexy idiom (5 items)
apps/app/src/components/persons/PersonDetailPanel.vue:272:25
vue/no-restricted-class — "'ml-1' class is not allowed."
apps/app/src/components/sections/SectionsShiftsPanel.vue:357:25
vue/no-restricted-class — "'ml-1' class is not allowed."
apps/app/src/components/shifts/AssignPersonDialog.vue:461:29
vue/no-restricted-class — "'pl-4' class is not allowed."
apps/app/src/components/shifts/AssignPersonDialog.vue:475:23
vue/no-restricted-class — "'ml-auto' class is not allowed."
apps/app/src/components/shifts/AssignPersonDialog.vue:500:27
vue/no-restricted-class — "'ml-1' class is not allowed."
The rule (.eslintrc.cjs line 176) bans m{l,r}- and p{l,r}-
Tailwind utility-class names because the project is Vuetify-only
("Use Vuetify utility class … never custom CSS" per CLAUDE.md).
Vuetify's spacing utilities are ms-*, me-*, ps-*, pe-* (start/
end LTR-aware), or mx-*/px-* for both sides.
Recommendation for 1b-ii: rewrite the 5 occurrences:
ml-1→ms-1pl-4→ps-4ml-auto→ms-auto
Vuetify supports both m{l,r,s,e}-N directly; the project preference
per the rule is the start/end form. Mechanical 5-line change across
3 files.
Bucket E — Bug-shaped (8 items)
E.1 import/no-named-as-default-member × 2 — EventTabsNav.vue
apps/app/src/components/events/EventTabsNav.vue:53:8
apps/app/src/components/events/EventTabsNav.vue:76:9
"Caution: `axios` also has a named export `isAxiosError`. Check if
you meant to write `import { isAxiosError } from 'axios'` instead."
The file accesses axios.isAxiosError(...) via the default export
namespace; ESLint flags that there's a named export of the same name.
Both forms work, but the named import is the modern axios pattern and
narrower (no full default-axios import overhead).
Recommendation for 1b-ii: change import axios from 'axios' to
import { isAxiosError } from 'axios' (or add the named import
alongside the default). Two-line edit. Zero behaviour change.
E.2 vue/component-api-style × 1 — vitest spec
apps/app/src/composables/api/__tests__/useFormFailures.spec.ts:38:5
"Options API is not allowed in your project. `render` function is
part of the Options API. Use Composition API instead."
A test mock uses Options-API render function. CLAUDE.md frontend
rules: "Always <script setup lang='ts'> — never the Options API".
Recommendation for 1b-ii: rewrite the mock as Composition API,
or add a per-spec-file override.
E.3 vue/return-in-computed-property × 1 — useTimeSlotDropdown
apps/app/src/composables/useTimeSlotDropdown.ts:80:32
"Expected to return a value in computed function."
A computed callback is missing a return path on at least one
branch — a real bug-class. Recommendation for 1b-ii: read line 80
of useTimeSlotDropdown.ts and ensure every code path returns.
E.4 vue/no-template-target-blank × 1 — security
apps/app/src/pages/organisation/index.vue:342:21
"Using target='_blank' without rel='noopener noreferrer' is a
security risk."
External anchor with target="_blank" and no rel="noopener" —
classic reverse-tabnabbing exposure. Recommendation: add
rel="noopener noreferrer" to the anchor.
E.5 promise/no-promise-in-callback × 3 — axios.ts
apps/app/src/lib/axios.ts:42:12
apps/app/src/lib/axios.ts:61:7
apps/app/src/lib/axios.ts:73:7
"Avoid using promises inside of callbacks."
In each case the response interceptor reaches into a dynamic import()
to lazy-load a store, then chains .then(). Pattern:
import('@/stores/useImpersonationStore').then(({ useImpersonationStore }) => {
const impersonationStore = useImpersonationStore()
impersonationStore.clearState()
window.location.href = '/platform'
})
The lint rule fires because the interceptor itself is a callback and
the promise inside isn't returned/awaited. In practice the
interceptor doesn't need to wait for the store dynamic-import (the
side effect — set window.location — is fire-and-forget). It's not a
real bug, but the rule is correctly identifying that errors inside
those .then() blocks would be silently swallowed.
Recommendation for 1b-ii — judgment call:
- Option a: wrap each in a
voidto mark the fire-and-forget intent:void import('@/stores/...').then(...). Tells ESLint and future-readers that the promise discard is intentional. - Option b: add
.catch(err => console.error(...))to each so errors aren't swallowed. - Option c: disable the rule for
lib/axios.tsonly — but loses defence everywhere else.
Option b is most defensive; option a is most honest about current behaviour. Both are 3-line single-file edits.
Action plan for session 1b-ii
| Step | Bucket | Effort | Risk | Notes |
|---|---|---|---|---|
| 1. Tier 3 hand-review + autofix | X | 1-2h | medium | Smoke pnpm dev + pnpm build after vite.config.ts |
| 2. Bucket A second-pass autofix on Tier 1/2 paths | A.1 | trivial | low | Rerun --fix on the 6 listed files; verify git diff --check |
| 3. Bucket A unused-imports/vars manual deletes | A.3 | trivial | low | 6 unique deletes |
4. Bucket A lines-around-comment Vue SFC fix |
A.2 | trivial | low | Either hand-add blank lines or override rule for *.vue |
5. Bucket B no-explicit-any ignore-patterns decision |
B.1 | trivial config | low | Likely add @core/** + @layouts/** to ignorePatterns |
6. Bucket B no-explicit-any real fixes (3 in our code) |
B.1 | low | low | blank.vue / default.vue / NavSearchBar.vue refs |
7. Bucket B no-use-before-define reorder |
B.2 | low | low | Reorder ShiftDetailPanel.vue script setup |
8. Bucket B no-shadow rename |
B.3 | trivial | low | useImpersonationStore.ts |
9. Bucket B camelcase rename |
B.4 | low | low | useFormSchemas.ts — 3-line edit |
| 10. Bucket C style fixes | C | low | low | 8 mechanical rewrites |
| 11. Bucket D Vuetify class renames | D | low | low | ml-/pl-/etc → ms-/ps-/etc, 5 occurrences |
| 12. Bucket E.1 isAxiosError import | E.1 | trivial | low | One file, two lines |
| 13. Bucket E.2 spec rewrite to Composition API | E.2 | low | low | Test only |
| 14. Bucket E.3 fix missing return in computed | E.3 | low | medium | Real semantic bug — verify with smoke |
| 15. Bucket E.4 security: add rel="noopener" | E.4 | trivial | low | One attribute add |
| 16. Bucket E.5 axios fire-and-forget pattern | E.5 | low | low | 3 spots, design choice between options a/b/c |
After 1b-ii completes, the lint count should drop from 231 to a small number (single digits, mostly Bucket-B-judgment-call leftovers if any — depending on the @core/@layouts ignorePatterns decision).
Open questions for Bert + Claude Chat
-
Vendored Vuexy code under
src/@core/andsrc/@layouts/— should it be included in.eslintrc.cjs'signorePatterns? 20 of the 23no-explicit-anyitems would silently disappear, plus a small number of other rules. The trade-off: future drift in those directories goes uncaught. Counter-trade-off: spending time correctinganys in code we copy-pasted is low value and high churn. -
vue/no-restricted-classregex/^(p|m)(l|r)-/— the rule bans bothmr-andml-(andpr-,pl-). Vuetify supportsmr-Nnatively (it's not just a Tailwind name); the migration tome-Nis for LTR/RTL flexibility. Confirm this is the intended ban, or relax it topl/mlonly if RTL isn't a project goal. -
Bucket C
vue/prefer-true-attribute-shorthand— the rule prefers<comp prop>over<comp :prop="true">for boolean props. Some teams find the explicit form clearer when it distinguishes from omitted. Confirm preference. -
Bucket E.5 axios callback-promises — pick option a/b/c per above before 1b-ii starts. Option b (
.catch) is the most defensive but introduces error-handling code; option a (void) is the most honest about today's behaviour. -
pnpm lintscript inpackage.json— currently aliased toeslint . --fix. After 1b-ii lands a clean baseline, consider splitting intopnpm lint(no-fix) andpnpm lint:fix(with --fix). The current aliasing is what made the original 105 reference number wrong (someone ranpnpm lintand reported the post-fix remainder).
Artifacts
/tmp/eslint-pre.json— pre-1b-i baseline (1451 problems)/tmp/eslint-current.json— post-Task-3 state (231 problems)
Both retained on the dev machine for 1b-ii reference. Not committed.