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>
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:
-
Stats card: replace the three
statsentries with counts derived fromobjectsfiltered by workspace. Suggested:- "Open tasks" — count of
objectswheretype = 'task'andstatusis not in a "done" set, scoped by workspace_id. - "Due this week" — same scope, additionally filtered by
due_at <= now + 7 days. Ifobjectsdoesn't have a due-date column yet, drop this card rather than fake it. - "Lists" / "Projects" — count of
objectswheretypeis in('project', 'space', 'group').
- "Open tasks" — count of
-
Recent list: replace the static
recentarray with the 5 most-recently-updated objects in the workspace. Use the existingobjectsrouter. If no suitable procedure exists, add one asobjects.listRecent({ limit: 5 })— workspace-scoped viaworkspaceProcedureinapps/web/server/trpc.ts. -
Loading + empty states: use the same
Cardskeleton pattern already inapps/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 existingCreateObjectDialog.
Constraints
- Don't add a new column to
objectsunless you also add a Drizzle migration. Prefer to drop a card rather than schema-creep this task. - All queries MUST filter by
workspace_id. UseworkspaceProcedure, never a raw procedure that pulls the slug from the URL without re-checking. - No raw
fetchagainst/api/trpc— useapi.objects.<...>.useQuery()via@trpc/react-query.
Subtasks
- Audit
apps/web/server/routers/objects.ts— nostatsorlistRecentexisted. - Added
objects.stats(open-tasks count + container-types count) andobjects.listRecent({ limit }). Both go throughworkspaceProcedure, so theworkspace_idfilter is enforced by the middleware, not just by the query body. - Replaced
statsandrecentarrays in[workspaceSlug]/page.tsxwith 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
CreateObjectDialoginstance withdefaultType: "task". The page-level dialog is independent of the globalAppShellcreate dialog so it can pre-seed itsdefaultTypewithout coordinating shared state.
Decisions made vs. the scaffold
- "Due this week" card dropped.
objectshas nodue_atcolumn. 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
workspaceandgrouprows. 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:doneandclosedare 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-fnsor 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 throughworkspaceProcedure).
Links to related Epic / Plan
- Epic:
./Epic-shipping-the-shell.md - Plan:
../Plan-daily-driver-finish.md