ubiquitous-invention/plans/Plan-daily-driver-finish/Epic-shipping-the-shell/Task-wire-workspace-home-dashboard.md
Randall Stillwell f64d307f72 feat(web): wire workspace-home dashboard to real tRPC queries
Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and
hardcoded `recent` list on the workspace-home page with real
workspace-scoped data.

* server/routers/objects.ts: add two new procedures.
  - `objects.stats` returns { openTasks, containers }. Open-task count
    treats null status as open; only `done` and `closed` (per
    packages/shared object-statuses) are terminal. Container count
    aggregates project + space + group rows.
  - `objects.listRecent({ limit })` returns the N most-recently-updated
    rows, descending by updated_at. Excludes archived and excludes
    `workspace`/`group` from the activity feed (containers clutter
    "what did I just touch" recency).
  Both go through workspaceProcedure, so the workspace_id filter
  comes from the middleware-resolved ctx.workspace.id rather than
  any user input.

* app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new
  procedures via @trpc/react-query. Adds:
  - Skeleton loading state (no flash of zeros).
  - Empty state with a "New task" CTA on workspaces with no objects.
  - Real "X ago" labels on the recent feed.
  - Click-through links from recent rows to /{slug}/{id}.
  - A locally-mounted CreateObjectDialog instance independent of the
    global one in AppShell so the empty-state CTA can pre-seed
    defaultType="task" without coordinating shared state.

* components/ui/skeleton.tsx: new (standard shadcn pulse skeleton).
  Used by the dashboard but reusable across the app.

The scaffolded "Due this week" stat is dropped: `objects` has no
due_at column and the task explicitly preferred dropping a card to
schema-creep.

`pnpm lint && pnpm type-check` clean. Closes
plans/Plan-daily-driver-finish/Epic-shipping-the-shell/
Task-wire-workspace-home-dashboard.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 00:17:29 -05:00

4.2 KiB

kind slug title plan_slug epic_slug status priority tenant_id owner cursor_todo_id updated_at
task wire-workspace-home-dashboard Replace hardcoded dashboard mocks with real tRPC queries daily-driver-finish shipping-the-shell done P0 global unassigned null 2026-06-02

Task summary

apps/web/app/(app)/[workspaceSlug]/page.tsx currently renders a hardcoded stats array ("24", "8", "12") and a hardcoded recent list. Replace both with real tRPC calls so the home page reflects actual workspace state.

Description

The page is a Client Component ("use client"). It already pulls currentWorkspace from useWorkspaceStore. What it needs:

  1. Stats card: replace the three stats entries with counts derived from objects filtered by workspace. Suggested:

    • "Open tasks" — count of objects where type = 'task' and status is not in a "done" set, scoped by workspace_id.
    • "Due this week" — same scope, additionally filtered by due_at <= now + 7 days. If objects doesn't have a due-date column yet, drop this card rather than fake it.
    • "Lists" / "Projects" — count of objects where type is in ('project', 'space', 'group').
  2. Recent list: replace the static recent array with the 5 most-recently-updated objects in the workspace. Use the existing objects router. If no suitable procedure exists, add one as objects.listRecent({ limit: 5 }) — workspace-scoped via workspaceProcedure in apps/web/server/trpc.ts.

  3. Loading + empty states: use the same Card skeleton pattern already in apps/web/app/(app)/[workspaceSlug]/teams/page.tsx (MemberCardSkeleton). When the workspace has no objects, render a friendly empty state with a "Create your first project" CTA that opens the existing CreateObjectDialog.

Constraints

  • Don't add a new column to objects unless you also add a Drizzle migration. Prefer to drop a card rather than schema-creep this task.
  • All queries MUST filter by workspace_id. Use workspaceProcedure, never a raw procedure that pulls the slug from the URL without re-checking.
  • No raw fetch against /api/trpc — use api.objects.<...>.useQuery() via @trpc/react-query.

Subtasks

  • Audit apps/web/server/routers/objects.ts — no stats or listRecent existed.
  • Added objects.stats (open-tasks count + container-types count) and objects.listRecent({ limit }). Both go through workspaceProcedure, so the workspace_id filter is enforced by the middleware, not just by the query body.
  • Replaced stats and recent arrays in [workspaceSlug]/page.tsx with real queries.
  • Added skeleton states (apps/web/components/ui/skeleton.tsx, new) and an empty state with a CTA.
  • Wired the empty-state CTA to a locally-mounted CreateObjectDialog instance with defaultType: "task". The page-level dialog is independent of the global AppShell create dialog so it can pre-seed its defaultType without coordinating shared state.

Decisions made vs. the scaffold

  • "Due this week" card dropped. objects has no due_at column. Per the task's own constraint ("drop a card rather than schema-creep this task") it's gone, leaving a 2-up grid: open tasks + projects/spaces/groups.
  • Recent feed excludes workspace and group rows. Container objects clutter a "what did I touch lately" view; the user wants to see the tasks/docs/whiteboards they actually edited.
  • Status "done" semantics match packages/shared/src/types/objects.ts: done and closed are terminal. Null status is treated as open (a fresh task with no explicit status isn't done).
  • Time-ago helper inlined. Single use site; not worth pulling in date-fns or building a shared hook.

Owner or assignee

Unassigned

Status

done

Estimation

M

Acceptance criteria

  • No hardcoded numbers or hardcoded titles remain in apps/web/app/(app)/[workspaceSlug]/page.tsx.
  • Loading state renders skeletons rather than a flash of zeros.
  • Empty state for a brand-new workspace renders a "New task" CTA wired to CreateObjectDialog.
  • All queries filter by workspace_id (enforced through workspaceProcedure).
  • Epic: ./Epic-shipping-the-shell.md
  • Plan: ../Plan-daily-driver-finish.md