Files
LinkDesk/frontend_report.md
T
indigo cd2efe3587 Truth-in-UI cleanup: wire real dashboard data, fix delete/dialog gaps
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>
2026-07-17 04:47:26 +08:00

23 KiB
Raw Blame History

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:547router.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 repovue-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.


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.