From 891df3aa45353299b2ebfa1004dc08fc510d49b3 Mon Sep 17 00:00:00 2001 From: Sai Prakash Date: Wed, 15 Jul 2026 23:49:49 -0400 Subject: [PATCH 1/2] Add asset detail page with notes/ideation doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dedicated /assets/[id] route where an asset's full story comes together — previously assets existed only as cards + an edit sheet inside the Product page. - Header: type, health, tags, repo/docs links, owner avatars; summary cards for debt score (manual vs derived breakdown), open work items, and plan count. - Description and a new notes column (freeform ideation doc) with click-to-edit; notes are markdown-canonical and render via react-markdown, same dialect the MCP server reads/writes. - Tabs: Work Items, Tech Debt (with score-derivation note), Code Plans (surfacing per-plan join notes, branch, PR status), and Dependencies (both directions, with owners and health). - getAssetDetail query returns the whole context in one call with the same product-access guard as getCodePlan; content updates go through updateAssetContentAction (access-checked). - Asset references now link here: product-page cards (edit moved to a pencil button), Work Items table asset cells, Tech Debt Register group headers, and My Work "Assets I Own" rows. - MCP: notes on update_asset, new get_asset tool returning the same full context as the page. Co-Authored-By: Claude Fable 5 --- app/(dashboard)/actions.ts | 15 + .../assets/[id]/asset-content-cards.tsx | 93 + app/(dashboard)/assets/[id]/page.tsx | 441 ++++ app/(dashboard)/my-work/page.tsx | 2 +- .../products/[slug]/assets-section.tsx | 21 +- .../work-items/work-items-client.tsx | 18 +- app/api/mcp/[transport]/route.ts | 16 +- .../migrations/postgres/0012_asset_notes.sql | 1 + lib/db/migrations/postgres/meta/_journal.json | 7 + lib/db/migrations/sqlite/0012_asset_notes.sql | 1 + .../migrations/sqlite/meta/0012_snapshot.json | 2048 +++++++++++++++++ lib/db/migrations/sqlite/meta/_journal.json | 7 + lib/db/mutations.ts | 1 + lib/db/queries.ts | 159 ++ lib/db/schema.pg.ts | 2 + lib/db/schema.sqlite.ts | 2 + lib/types.ts | 2 + 17 files changed, 2826 insertions(+), 10 deletions(-) create mode 100644 app/(dashboard)/assets/[id]/asset-content-cards.tsx create mode 100644 app/(dashboard)/assets/[id]/page.tsx create mode 100644 lib/db/migrations/postgres/0012_asset_notes.sql create mode 100644 lib/db/migrations/sqlite/0012_asset_notes.sql create mode 100644 lib/db/migrations/sqlite/meta/0012_snapshot.json diff --git a/app/(dashboard)/actions.ts b/app/(dashboard)/actions.ts index a48682b..d5318f2 100644 --- a/app/(dashboard)/actions.ts +++ b/app/(dashboard)/actions.ts @@ -228,9 +228,24 @@ export async function setAssetOwnersAction(assetId: string, productSlug: string, if (!accessible.some((a) => a.id === assetId)) throw new Error('Asset not found or not accessible') await setAssetOwners(assetId, userIds) revalidatePath(`/products/${productSlug}`) + revalidatePath(`/assets/${assetId}`) revalidatePath('/my-work') } +/** Update just the long-form content (description / notes) from the asset detail page. */ +export async function updateAssetContentAction( + assetId: string, + productSlug: string, + content: { description?: string; notes?: string }, +) { + const authUser = await requireUser() + const accessible = await getAssetOptions(authUser.id) + if (!accessible.some((a) => a.id === assetId)) throw new Error('Asset not found or not accessible') + await updateAsset(assetId, content) + revalidatePath(`/assets/${assetId}`) + revalidatePath(`/products/${productSlug}`) +} + // --------------------------------------------------------------------------- // Code Plans // --------------------------------------------------------------------------- diff --git a/app/(dashboard)/assets/[id]/asset-content-cards.tsx b/app/(dashboard)/assets/[id]/asset-content-cards.tsx new file mode 100644 index 0000000..736c9d2 --- /dev/null +++ b/app/(dashboard)/assets/[id]/asset-content-cards.tsx @@ -0,0 +1,93 @@ +'use client' + +import { useState, useTransition } from 'react' +import ReactMarkdown from 'react-markdown' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Textarea } from '@/components/ui/textarea' +import { AlignLeft, NotebookPen, Pencil } from 'lucide-react' +import { toast } from 'sonner' +import { updateAssetContentAction } from '../../actions' + +type EditableCardProps = { + assetId: string + productSlug: string + field: 'description' | 'notes' + value: string +} + +const cardMeta = { + description: { title: 'Description', icon: AlignLeft, placeholder: 'What does this asset do?', empty: 'No description yet.' }, + notes: { + title: 'Notes & Ideation', + icon: NotebookPen, + placeholder: 'Design thinking, migration ideas, known quirks… (markdown)', + empty: 'No notes yet — capture design thinking, migration ideas, and known quirks here.', + }, +} as const + +/** Long-form asset content with click-to-edit. Notes render as markdown (markdown stays the canonical format). */ +export function AssetContentCard({ assetId, productSlug, field, value }: EditableCardProps) { + const [editing, setEditing] = useState(false) + const [draft, setDraft] = useState(value) + const [isPending, startTransition] = useTransition() + const meta = cardMeta[field] + const Icon = meta.icon + + function save() { + startTransition(async () => { + await updateAssetContentAction(assetId, productSlug, { [field]: draft }) + toast.success('Saved') + setEditing(false) + }) + } + + return ( + + + + + {meta.title} + {!editing && ( + + )} + + + + {editing ? ( +
+