cd2efe3587
Phase 1 of frontend_tasks.md - stop showing fabricated/broken UI:
- Dashboard now fetches real stats (projects, tasks, users, pending
approvals, API keys, developer stats, pending reviews, admin
activity) instead of hardcoded numbers. Added services/developer.ts
and services/review.ts wrappers for previously-unused backend
endpoints.
- Wired ActivityFeed into every project's Overview page in place of
the "coming soon" placeholder.
- Registered the missing /projects/:id/technical-specs route (view
and service already existed, just unreachable).
- Fixed AssetDeleteConfirmDialog's raw styled divs to use the shared
Alert component and wired it into AssetBrowser, matching
ShotBrowser's impact-summary + type-to-confirm safety pattern
(asset deletion was previously less safe than shot deletion).
- Fixed a shared bug in both delete dialogs where the impact-summary
section never rendered (watch on the open prop needed
{ immediate: true }).
- Replaced native confirm()/alert() with styled AlertDialog/Dialog in
NoteItem, TaskAttachments, and UserMenu's keyboard-shortcuts item.
- Removed dead-end UI: Google OAuth stub buttons, UserMenu items
pointing at non-existent routes, the /developer/docs dead link, and
the no-op action button on the API Keys placeholder page.
- Removed leftover debug console logging across 8 files.
Added frontend_report.md (full audit) and frontend_tasks.md (phased
checklist) as the reference for this and future phases.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
204 lines
23 KiB
Markdown
204 lines
23 KiB
Markdown
# LinkDesk Frontend Report
|
||
|
||
**Scope:** `frontend/src/` only (Vue 3 + TypeScript + Pinia + Vue Router + shadcn-vue/reka-ui + TanStack Table). No backend changes proposed. Compiled from a full-codebase audit (grep sweeps + file reads across stores, views, components, router, services).
|
||
|
||
**Purpose:** give a concrete, prioritized punch list to plan the next phase of frontend development — what's broken or fake, what's inconsistent, and what's architecturally risky.
|
||
|
||
---
|
||
|
||
## Executive Summary
|
||
|
||
The app is functionally broad but shows clear signs of iterative, deadline-driven development without cleanup passes: **the dashboard's data is entirely fabricated**, **one advertised route 404s**, **six asset detail-panel actions are no-ops**, and there are **at least eight dead/orphaned component files** still sitting in the tree. Beyond that, the same UI concept (column-visibility toggle, delete confirmation, toolbar, checkbox) is often implemented 2-4 different ways across shot/asset/task domains, and there is **no test suite and no lint config** guarding any of it.
|
||
|
||
None of this is catastrophic — the core shot/asset/task browsing flows work — but it means the next phase of work should prioritize **truth-in-UI fixes and dead-code removal first** (cheap, high trust impact), **then control unification** (moderate effort, big maintainability payoff), **then architecture/tooling investment** (testing, shared composables, performance) before adding more features on top of the current duplication.
|
||
|
||
---
|
||
|
||
## 1. Unfinished / Incomplete Functionality
|
||
|
||
### 1.1 Misleading or broken right now (fix first)
|
||
|
||
| Issue | Where | Impact |
|
||
|---|---|---|
|
||
| **Dashboard shows fabricated data** | `views/DashboardView.vue:445-472, 564-589` | `activeTasks`, `pendingReviews`, `completedTasks`, `systemActivity`, etc. are hardcoded constants, not fetched — every user sees the same fake numbers on the app's landing page regardless of role or real state. |
|
||
| **"Manage Technical Specs" 404s** | Called from `views/ProjectDetailView.vue:145`, `views/EpisodesView.vue:309`, `views/ProjectsView.vue:547` → `router.push('/projects/:id/technical-specs')` | `router/index.ts` has no route registered for this path, even though `views/ProjectTechnicalSpecsView.vue` and its backing `services/project.ts:161-171` calls are fully built. Three navigation call sites currently dead-end at `NotFoundView`. |
|
||
| **Google OAuth buttons do nothing** | `views/auth/LoginView.vue:106-109`, `views/auth/RegisterView.vue:220-223` | Both `// TODO: Implement Google OAuth` + `console.log(...)`. Either finish it or remove the button so users don't try a dead flow. |
|
||
| **Asset detail-panel actions are all stubs** | `components/asset/AssetBrowser.vue:571-574, 680-703` | `viewAssetTasks`, `handleCreateTask`, `handleSelectTask`, `handleCreateNote`, `handleUploadReference`, `handlePublishVersion` — six handlers, all `console.log` only. The entire "act on an asset" surface is non-functional. |
|
||
| **Shot detail panel has partial equivalents** | `components/shot/ShotBrowser.vue:848-856` | `handleCreateTask`/`handleSelectTask` are also `// TODO: Navigate to task creation...` stubs — smaller gap than assets but same pattern. |
|
||
| **"Manage Members" does nothing** | `views/ProjectDetailView.vue:149-151` | `console.log('Manage members')` — while two separate real member-management components already exist (see 1.3), this entry point isn't wired to either. |
|
||
| **Task-status usage counts are fake** | `components/settings/CustomTaskStatusManager.vue:275-295` | `// TODO: Implement actual task count fetching` — always shows 0 regardless of real usage, which could mislead an admin into deleting a status that's actually in use. |
|
||
| **Episode progress bars are a lookup table, not real data** | `components/episode/EpisodeCard.vue:117-128` and `components/episode/EpisodeList.vue:267-278` (duplicated logic) | `switch(status) { case 'in_progress': return 45; ... }` — fixed numbers per status, not derived from actual shot completion. |
|
||
| **Native browser `confirm()` used for real deletes** | `components/task/NoteItem.vue:178`, `components/task/TaskAttachments.vue:201` | Unstyled OS dialog, breaks the app's own "type to confirm" / `AlertDialog` conventions used everywhere else. |
|
||
|
||
### 1.2 Stubbed/placeholder pages
|
||
|
||
| Page | File | State |
|
||
|---|---|---|
|
||
| Reviews | `views/ReviewsView.vue` (22 lines) | Empty state only ("No pending reviews"), no workflow at all. |
|
||
| Developer: API Keys / Projects / Tasks / Analytics | `views/developer/*.vue` (22-24 lines each) | All four are `EmptyState` shells with no data fetching. `APIKeysView.vue` additionally has a dead `@action="() => {}"` on its only button. |
|
||
| Project Overview activity feed | `views/project/ProjectOverviewView.vue:107-118` | Static "Activity feed coming soon" text — even though a fully-built `ActivityFeed.vue` component exists and is simply never wired in (see 1.3). |
|
||
| User menu items | `components/layout/UserMenu.vue:68, 84` | Link to `/settings/preferences` and `/help`, neither of which is a registered route — both 404. |
|
||
| User menu "Notifications"/"Keyboard Shortcuts" | `components/layout/UserMenu.vue:201-219` | Toggle doesn't persist anything; keyboard-shortcuts item falls back to a browser `alert()`. |
|
||
|
||
### 1.3 Dead / orphaned component files (safe to delete or finish)
|
||
|
||
These are fully-written components with **zero references anywhere else in `src`** (confirmed via content grep, not just filename):
|
||
|
||
- `components/shot/ShotColumnVisibilityControl.vue` — superseded by `SidebarColumnSwitch.vue`.
|
||
- `components/asset/ColumnVisibilityControl.vue` — superseded by the inline toolbar Popover+Command control.
|
||
- `components/project/ShotsTable.vue` (280 lines) — an earlier, abandoned shot-browser implementation whose CRUD handlers are all TODO stubs, fully superseded by `ShotBrowser.vue`.
|
||
- `components/activity/ActivityFeed.vue` and `components/activity/TaskActivityTimeline.vue` — both fully built (pagination, service calls, formatting) but never imported anywhere; `ProjectOverviewView.vue` shows a "coming soon" placeholder instead of using either (see 1.2).
|
||
- `components/examples/FileUploadExample.vue` — demo/example component shipped in production `src`.
|
||
- `views/GlobalSettingsView.vue` — byte-for-byte duplicate of `views/SettingsView.vue`, not routed anywhere.
|
||
- `views/HomeView.vue` — orphaned landing page scaffold, superseded by `DashboardView.vue`, not routed anywhere.
|
||
- `components/asset/AssetDeleteConfirmDialog.vue` — fully built, never imported; `AssetBrowser.vue` uses a plain generic `AlertDialog` instead (see §2.3).
|
||
|
||
### 1.4 Feature parity gaps (asset lags shot)
|
||
|
||
- **User assignment**: `components/shot/EditableTaskStatus.vue` (413 lines) has a full assignee popover; `components/asset/EditableTaskStatus.vue` (197 lines) has none — assets can't be assigned to a user from the table.
|
||
- **Column locking**: `ShotTableToolbar.vue` has a "lock first columns" toggle; `AssetTableToolbar.vue` has no equivalent.
|
||
- **Row actions menu**: shot/asset `columns.ts` both have a `DropdownMenu` row-actions menu; `components/task/columns.ts` doesn't — tasks have no per-row "…" actions.
|
||
|
||
### 1.5 Debug artifacts left in shipped code
|
||
|
||
Heavy `console.log`/`console.error` tracing left in real (non-debug) code paths — worth a cleanup pass regardless of the features above:
|
||
`views/UsersView.vue:257-267`, `components/settings/CustomTaskTypeManager.vue:430-483`, `views/project/ProjectAssetsView.vue:38-54`, `components/project/ProjectTabs.vue:104-147`, `services/asset.ts:153-159`, `services/user.ts:89`, `views/admin/DeletedItemsManagementView.vue:918-937`, plus a stray commented-out debug block in `components/asset/EditableTaskStatus.vue:16-19`.
|
||
|
||
---
|
||
|
||
## 2. Unified Controls (Consistency)
|
||
|
||
The recurring theme: **the same interaction gets reinvented per domain (shot/asset/task)** instead of shared once. Below, each pattern lists what exists today and which version should become the standard.
|
||
|
||
### 2.1 Column visibility — 4 competing implementations, 2 of them dead
|
||
|
||
| Implementation | Status | Notes |
|
||
|---|---|---|
|
||
| `components/ui/sidebar/SidebarColumnSwitch.vue` | **Live** (used in `AppSidebar.vue`) | Reads from `useColumnVisibilityStore` directly; hand-rolled checkbox div in expanded mode, real `DropdownMenuCheckboxItem` in collapsed mode (this repo's newest sidebar work). |
|
||
| Inline Popover+Command block in `ShotTableToolbar.vue` / `AssetTableToolbar.vue` / `TaskTableToolbar.vue` | **Live** | Same 10-line hand-rolled checkbox-div markup copy-pasted 3x; takes `columnVisibility` as a prop/emit pair instead of the store. |
|
||
| `components/shot/ShotColumnVisibilityControl.vue` | **Dead** | `Select` + native `<input type="checkbox">` nested inside a `SelectItem` — also has a real double-toggle bug (checkbox `@change` and parent `@click` both fire, flipping state twice) and an ARIA violation (interactive control inside `role="option"`). |
|
||
| `components/asset/ColumnVisibilityControl.vue` | **Dead** | Same pattern as above. |
|
||
|
||
**Recommendation:** delete both dead files; extract one `ColumnToggleList` component (built on `DropdownMenuCheckboxItem`, not the div+Check hack) and have the sidebar and all three toolbars consume it against the single `useColumnVisibilityStore`.
|
||
|
||
### 2.2 Toolbars — near-duplicate files with drifting features
|
||
|
||
`ShotTableToolbar.vue`, `AssetTableToolbar.vue`, and `TaskTableToolbar.vue` share the same skeleton (debounced 300ms search reimplemented in each file, hidden-columns-count logic, detail-panel toggle) but have diverged:
|
||
- Shot has a 3-way grid/list/table view toggle + column-lock toggle + bulk-create; Asset has only grid/list + a thumbnail show/hide toggle; neither toggle group is shared, each is a hand-built segmented control.
|
||
- Task has its own third segmented-control implementation (all/shots/assets context filter) plus a bespoke bulk-actions menu (`TaskBulkActionsMenu.vue`) built from `Popover` + ad hoc `<button>`s rather than `DropdownMenuItem`.
|
||
- All three end with an icon-only "create" button styled as `size="sm" class="h-8 w-8 p-0"` reinventing the existing `size="icon-sm"` button variant, and — unlike sibling icon buttons in the same toolbar — without a `title` tooltip, making the single most important CTA the least discoverable control in the toolbar.
|
||
|
||
**Recommendation:** extract a shared `EntityTableToolbar` (search, column-visibility trigger, detail-panel toggle, clear-filters) parameterized by column/filter definitions; extract one `SegmentedToggle` component for the three hand-built view-mode switches; standardize primary create buttons on either icon+label or icon-only+tooltip, not a mix.
|
||
|
||
### 2.3 Delete confirmation — inconsistent safety, not just style
|
||
|
||
- `components/shot/ShotDeleteConfirmDialog.vue`: rich `Dialog` + `Alert`, fetches deletion-impact summary (task/submission/attachment counts, affected users), requires typing the shot name to confirm. **This is the most mature pattern and should be the standard for any cascading delete.**
|
||
- `components/asset/AssetDeleteConfirmDialog.vue`: a built near-clone of the above, but re-implements alerts as raw `<div>`s with manual color classes instead of the shared `Alert` component — **and it's dead code**, never imported.
|
||
- `AssetBrowser.vue`'s actual delete flow uses a bare generic `AlertDialog` with a one-line message — no impact summary, no affected-users list, no type-to-confirm — meaning **asset deletion is materially less safe than shot deletion** today.
|
||
- Task-related deletes (`NoteItem.vue`, `TaskAttachments.vue`) bypass the Vue dialog system entirely via native `confirm()` (see 1.1).
|
||
|
||
**Recommendation:** wire `AssetDeleteConfirmDialog.vue` into `AssetBrowser.vue` (fixing its `Alert` divergence first), or delete it and consciously accept the lighter `AlertDialog` for assets; replace the two native `confirm()` calls with `AlertDialog`.
|
||
|
||
### 2.4 Hand-rolled checkbox vs. real `Checkbox` component
|
||
|
||
A real, accessible `Checkbox` component already exists at `components/ui/checkbox/Checkbox.vue` (Reka UI-backed, same visual spec) and is correctly used for table row-selection. But every Popover/Command filter and column-toggle list instead hand-rolls the same look as a plain `<div>` + conditional class + `Check` icon — no keyboard support, no ARIA state. Affected files: `SidebarColumnSwitch.vue`, `ShotTableToolbar.vue`, `AssetTableToolbar.vue`, `TaskTableToolbar.vue`, `ShotTaskStatusFilter.vue`, `asset/TaskStatusFilter.vue`. Low-risk, high-value fix since the visual tokens already match byte-for-byte.
|
||
|
||
### 2.5 Button sizing
|
||
|
||
`components/ui/button/index.ts` already defines `icon`, `icon-sm`, `icon-lg` variants, yet toolbar icon buttons across shot/asset/task/`ShotBrowser.vue`/`ProjectMemberManagement.vue` write `size="sm" class="h-8 w-8 p-0"` instead of `size="icon-sm"`. Separately, primary "create" CTAs are inconsistent in kind: `ProjectsView.vue`/`EpisodeList.vue` use labeled default buttons, while the equivalent shot/asset create buttons are icon-only with no tooltip — the more important action is the less discoverable one.
|
||
|
||
### 2.6 Detail panels — good state layer, duplicated shell
|
||
|
||
`composables/useDetailPanel.ts` is a well-designed, correctly shared composable (auto-enable, keyboard toggle, mobile sheet, persistence) used consistently by all three browsers — this part is **already unified and worth keeping as the model** for other unification work. But the actual panel markup (`ShotDetailPanel.vue` 521 lines, `AssetDetailPanel.vue` 481 lines, `TaskDetailPanel.vue` 535 lines) each independently re-implement the loading spinner, error block, header/close button, and `Tabs` scaffold — Task's panel notably has no error state at all where Shot/Asset do.
|
||
|
||
**Recommendation:** extract a `DetailPanelShell.vue` (header, close button, loading/error states, slide-in transition wrapper, Tabs scaffold) that the three domain panels plug tab content into.
|
||
|
||
### 2.7 Dropdown/menu primitive choice
|
||
|
||
Filter dropdowns are consistently `Popover` + `Command` (good). But row-actions and bulk-actions menus are not: shot/asset `columns.ts` use real `DropdownMenu`/`DropdownMenuItem` for row actions; `task/columns.ts` has no row-actions menu at all; `TaskBulkActionsMenu.vue` builds its own context menu from `Popover` + ad hoc `<button>` elements instead of `DropdownMenuItem`. Standardize row/bulk actions on `DropdownMenu`.
|
||
|
||
### 2.8 Project member management — two parallel components
|
||
|
||
`components/project/ProjectMembersManager.vue` (322 lines, used from `ProjectsView.vue`) and `components/project/ProjectMemberManagement.vue` (414 lines, used from `ProjectSettingsView.vue`) both implement "manage project members" independently, and neither is wired to the still-stubbed `ProjectDetailView.vue` "Manage Members" action (§1.1). Consolidate to one component with one entry point.
|
||
|
||
---
|
||
|
||
## 3. Architecture & Smarter Design
|
||
|
||
### 3.1 State management (`stores/*.ts`)
|
||
|
||
No store is individually huge (largest is `assets.ts` at 380 lines), but **every store re-implements the same `isLoading`/`error`/try-catch-finally boilerplate** with no shared `useAsyncAction`-style composable. Caching strategy is also inconsistent: most stores (`assets`, `tasks`, `projects`, `user`, `episodes`) have none — every view re-fetches on mount — while `taskStatuses.ts` invents its own bespoke `Map` cache with a 5-minute TTL and, notably, de-duplicates in-flight requests via a **100ms `setInterval` poll loop up to a 10s timeout** rather than memoizing the in-flight promise — a real correctness/perf smell and a one-off pattern not reused elsewhere.
|
||
|
||
`assets.ts` also duplicates its optimistic-update/rollback logic almost verbatim between the single- and bulk-update task-status actions (lines 156-234 vs 236-356) — a good candidate to unify into one parameterized function.
|
||
|
||
`projects.ts` stores Vue/lucide icon components directly in reactive state without `markRaw` (`assignProjectIcon`), even though `ShotBrowser.vue` correctly uses `markRaw` for its column defs elsewhere — inconsistent application of a pattern the team already knows. `ProjectsView.vue` then puts a `deep: true` watch on this same array, compounding the cost.
|
||
|
||
### 3.2 Type safety
|
||
|
||
`types/` holds only 150 lines across 3 files (`auth.ts`, `notification.ts`, `activity.ts`); nearly all domain types (`Shot`, `Asset`, `Task`, `Project`, etc.) live ad hoc inside `services/*.ts` files instead, blurring the service/type boundary. `catch (err: any)` appears **104 times across 27 files** with no shared typed `ApiError` helper — every call site re-derives `err.response?.data?.detail` by hand. `as any` casts (13 occurrences) mostly work around the `TaskStatus`/task-type union types not supporting dynamic field access. `tsconfig.json` has `strict: true` but `noUnusedLocals: false` (explicitly turned off), and **there is no ESLint config anywhere in the repo** — `vue-tsc --noEmit` is the only automated quality gate.
|
||
|
||
### 3.3 Performance
|
||
|
||
- **No table virtualization anywhere** — no `@tanstack/vue-virtual` dependency, no pagination row model; `ShotsDataTable.vue`/`AssetsDataTable.vue`/`TasksDataTable.vue` render every row as real DOM. This is the top scaling risk for productions with thousands of shots.
|
||
- **N+1 per-cell API calls**: `EditableTaskStatus.vue` is mounted once per (row × task-type column); its `onMounted` calls `getProjectMembers()` with no shared cache. A 50-row × 5-column table view fires ~250 redundant identical member-list requests. (Status data itself is correctly cached via `taskStatusesStore` — only the member list isn't.)
|
||
- `deep: true` watchers appear in 12 places, mostly harmless on small flat objects, but `ProjectsView.vue:770`'s deep watch on the full (non-`markRaw`'d) projects array is a real concern.
|
||
- Client-side filtering of full shot/asset arrays will get slower as data grows, even though the backend already partially supports server-side filtering (`taskStatusFilter`, `episodeId` params).
|
||
|
||
### 3.4 Component size
|
||
|
||
Largest files: `views/project/ShotDetailView.vue` (1658 lines), `views/admin/DeletedItemsManagementView.vue` (1279 lines), `ShotBrowser.vue` (949), `ProjectsView.vue` (801), `AssetBrowser.vue` (800), `ProfileView.vue` (774). The Browser/DetailView/DetailPanel triad repeats a similar shape across shot/asset/task domains (`ShotBrowser.vue` and `AssetBrowser.vue` share roughly 70% structure) — a shared "entity browser" composable/component would shrink both duplication and file size together.
|
||
|
||
### 3.5 Error handling / loading / empty states
|
||
|
||
A `Skeleton` component exists but is used in only 5 files, versus 62 files using ad hoc spinner divs — no consistent loading convention. Empty-state messaging is hand-rolled in 31 files instead of a shared `EmptyState` component. Most importantly: only 38 of 284 `.vue` files use `useToast`, versus 60 files with 130 total `console.error` calls — secondary data loads (episodes, task types, task statuses, project context in `ShotBrowser.vue`, notification stats, auth-init failures in the router guard) fail silently with no user-facing feedback.
|
||
|
||
### 3.6 Testing
|
||
|
||
**No test infrastructure exists at all** — no `*.test.ts`/`*.spec.ts` files, no Vitest/Jest config, no test dependencies in `package.json`, no `test` script. All the optimistic-update/rollback logic in `assets.ts` and the auth token lifecycle in `auth.ts` are verified only by manual QA. This is the single biggest structural gap for a production-management tool.
|
||
|
||
### 3.7 Accessibility
|
||
|
||
reka-ui-based primitives (Checkbox, Select, dialogs) provide correct ARIA/keyboard behavior out of the box, but custom composition on top of them introduces gaps: the checkbox-inside-`SelectItem` anti-pattern noted in §2.1/2.4, hand-rolled clickable `<div>`s with no keyboard support (`SubmissionCard.vue`, `EditableTaskStatus.vue`), and only 7 of 256 component files reference any `aria-*`/`tabindex` attribute. Image `alt` text is handled reasonably well where sampled.
|
||
|
||
### 3.8 Routing/guards
|
||
|
||
`router/index.ts`'s single `beforeEach` reading `meta.requiresAuth`/`roles`/`adminPermission` is a genuinely good, centralized pattern — no per-view guard duplication. The gap is that role/permission checks are *also* duplicated ad hoc across ~20 components (to hide nav items/buttons) with no shared `usePermission()`/`can()` composable, so role-rule changes require touching both the router meta and scattered component checks.
|
||
|
||
---
|
||
|
||
## 4. Recommended Development Roadmap
|
||
|
||
Ordered by "cheapest + most trust-restoring first," so each phase is shippable on its own.
|
||
|
||
### Phase 1 — Truth-in-UI & dead code (low effort, high trust impact)
|
||
- Wire the dashboard to real data or clearly label it as a placeholder; same for episode progress bars and task-status usage counts.
|
||
- Register the missing `/projects/:projectId/technical-specs` route.
|
||
- Delete the 8 confirmed-dead files (§1.3), or finish wiring `ActivityFeed.vue` into `ProjectOverviewView.vue` since it's already built.
|
||
- Replace the two native `confirm()` calls with `AlertDialog`.
|
||
- Remove leftover debug `console.log`/`console.error` traces (§1.5).
|
||
- Either finish or remove: Google OAuth buttons, "Manage Members" stub, `/developer/*` placeholder pages, dead `/help`/`/settings/preferences` menu links.
|
||
|
||
### Phase 2 — Close feature gaps
|
||
- Bring asset-side up to shot-side parity: user assignment in `EditableTaskStatus`, column locking in the asset toolbar, row-actions menu for tasks.
|
||
- Finish the six stubbed asset detail-panel actions (create task/note, select task, upload reference, publish version) or hide the affordances until built.
|
||
- Consolidate `ProjectMembersManager.vue`/`ProjectMemberManagement.vue` into one component and wire it to the "Manage Members" entry point.
|
||
|
||
### Phase 3 — Unify controls
|
||
- Extract `ColumnToggleList` (built on `DropdownMenuCheckboxItem`) and point the sidebar + all three toolbars at it.
|
||
- Extract `EntityTableToolbar` + `SegmentedToggle` to de-duplicate shot/asset/task toolbars.
|
||
- Extract `DetailPanelShell` for the shared header/loading/error/Tabs scaffold across the three detail panels.
|
||
- Standardize delete confirmation on the shot pattern (`Dialog` + `Alert` + impact summary) for cascading deletes; fix or retire the asset variant.
|
||
- Swap hand-rolled checkbox divs for the real `Checkbox`/`DropdownMenuCheckboxItem`; replace ad hoc `h-8 w-8 p-0` button classes with `size="icon-sm"`.
|
||
|
||
### Phase 4 — Architecture & tooling investment
|
||
- Add a shared `useAsyncAction`-style composable to cut store boilerplate; replace `taskStatuses.ts`'s polling-based in-flight de-dup with promise memoization; extend a similar cache to `assets`/`tasks`/`projects`/`episodes` stores.
|
||
- Add `markRaw` where components are stored in reactive state (`projects.ts`); audit `deep: true` watchers.
|
||
- Fix the `EditableTaskStatus` N+1 member-fetch by hoisting to a shared cache/prop.
|
||
- Introduce a shared `EmptyState` component and standardize on `Skeleton` for loading; adopt a consistent toast-on-error convention for secondary data loads.
|
||
- Centralize a `usePermission()`/`can()` composable to consolidate role checks currently scattered across ~20 components.
|
||
|
||
### Phase 5 — Testing & quality gates
|
||
- Add Vitest + Vue Test Utils, starting with the optimistic-update/rollback logic in `assets.ts` and the auth token lifecycle in `auth.ts` (highest-risk untested logic).
|
||
- Add an ESLint config (flag `any`, unused locals) and re-enable `noUnusedLocals` in `tsconfig.json`.
|
||
- Introduce a typed `ApiError` helper to replace the 104 `catch (err: any)` sites incrementally as touched.
|