Merge branch 't3code/expand-research-management-plan'

# Conflicts:
#	app/analysis/page.tsx
#	app/watchlist/page.tsx
#	components/shell/app-shell.tsx
#	lib/api.ts
#	lib/query/options.ts
#	lib/server/api/app.ts
#	lib/server/db/index.test.ts
#	lib/server/db/index.ts
#	lib/server/db/schema.ts
#	lib/server/repos/research-journal.ts
#	lib/types.ts
This commit is contained in:
2026-03-07 20:39:49 -05:00
38 changed files with 5533 additions and 427 deletions

View File

@@ -16,6 +16,7 @@ import {
import {
BrainCircuit,
ChartNoAxesCombined,
NotebookTabs,
NotebookPen,
RefreshCcw,
Search,
@@ -29,6 +30,7 @@ import { Input } from '@/components/ui/input';
import { Panel } from '@/components/ui/panel';
import { useAuthGuard } from '@/hooks/use-auth-guard';
import { useLinkPrefetch } from '@/hooks/use-link-prefetch';
import { buildGraphingHref } from '@/lib/graphing/catalog';
import {
createResearchJournalEntry,
deleteResearchJournalEntry,
@@ -440,6 +442,14 @@ function AnalysisPageContent() {
>
Open filing stream
</Link>
<Link
href={buildGraphingHref(analysis.company.ticker)}
onMouseEnter={() => prefetchResearchTicker(analysis.company.ticker)}
onFocus={() => prefetchResearchTicker(analysis.company.ticker)}
className="text-sm text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]"
>
Open graphing
</Link>
</>
) : null}
</form>
@@ -855,123 +865,70 @@ function AnalysisPageContent() {
)}
</Panel>
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[1fr_1.3fr]">
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[0.95fr_1.3fr]">
<Panel
title={editingJournalId === null ? 'Research Journal' : 'Edit Journal Entry'}
subtitle="Private markdown notes for this company. Linked filing notes update the coverage review timestamp."
title="Research Summary"
subtitle="The full thesis workflow now lives in the dedicated Research workspace."
actions={(
<Link
href={`/research?ticker=${encodeURIComponent(activeTicker)}`}
onMouseEnter={() => prefetchResearchTicker(activeTicker)}
onFocus={() => prefetchResearchTicker(activeTicker)}
className="inline-flex items-center gap-2 rounded-lg border border-[color:var(--line-weak)] px-3 py-2 text-xs uppercase tracking-[0.12em] text-[color:var(--accent)] transition hover:border-[color:var(--line-strong)] hover:text-[color:var(--accent-strong)]"
>
<NotebookTabs className="size-4" />
Open research
</Link>
)}
>
<form onSubmit={saveJournalEntry} className="space-y-3">
<div>
<label className="mb-1 block text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">Title</label>
<Input
value={journalForm.title}
aria-label="Journal title"
onChange={(event) => setJournalForm((prev) => ({ ...prev, title: event.target.value }))}
placeholder="Investment thesis checkpoint, risk note, follow-up..."
/>
<div className="space-y-4">
<div className="rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-4">
<p className="text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">Workspace focus</p>
<p className="mt-2 text-sm leading-6 text-[color:var(--terminal-bright)]">
Use the research surface to manage the typed library, attach evidence to memo sections, upload diligence files, and assemble the packet view for investor review.
</p>
</div>
<div>
<label className="mb-1 block text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">Linked Filing (optional)</label>
<Input
value={journalForm.accessionNumber}
aria-label="Journal linked filing"
onChange={(event) => setJournalForm((prev) => ({ ...prev, accessionNumber: event.target.value }))}
placeholder="0000000000-26-000001"
/>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<div className="rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-4">
<p className="text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">Stored research entries</p>
<p className="mt-2 text-2xl font-semibold text-[color:var(--terminal-bright)]">{journalEntries.length}</p>
</div>
<div className="rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-4">
<p className="text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">Latest update</p>
<p className="mt-2 text-sm text-[color:var(--terminal-bright)]">{journalEntries[0] ? formatDateTime(journalEntries[0].updated_at) : 'No research activity yet'}</p>
</div>
</div>
<div>
<label className="mb-1 block text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">Body</label>
<textarea
value={journalForm.bodyMarkdown}
aria-label="Journal body"
onChange={(event) => setJournalForm((prev) => ({ ...prev, bodyMarkdown: event.target.value }))}
placeholder="Write your thesis update, questions, risks, and next steps..."
className="min-h-[220px] w-full rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2 text-sm text-[color:var(--terminal-bright)] outline-none transition placeholder:text-[color:var(--terminal-muted)] focus:border-[color:var(--line-strong)] focus:shadow-[0_0_0_3px_rgba(0,255,180,0.14)]"
required
/>
</div>
<div className="flex flex-wrap gap-2">
<Button type="submit" className="w-full sm:w-auto">
<NotebookPen className="size-4" />
{editingJournalId === null ? 'Save note' : 'Update note'}
</Button>
{editingJournalId !== null ? (
<Button type="button" variant="ghost" className="w-full sm:w-auto" onClick={resetJournalForm}>
Cancel edit
</Button>
) : null}
</div>
</form>
</div>
</Panel>
<Panel title="Journal Timeline" subtitle={`${journalEntries.length} stored entries for ${activeTicker}.`}>
<Panel title="Recent Research Feed" subtitle={`Previewing the latest ${Math.min(journalEntries.length, 4)} research entries for ${activeTicker}.`}>
{journalLoading ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Loading journal entries...</p>
<p className="text-sm text-[color:var(--terminal-muted)]">Loading research entries...</p>
) : journalEntries.length === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No journal notes yet. Start a thesis log or attach a filing note from the filings stream.</p>
<p className="text-sm text-[color:var(--terminal-muted)]">No research saved yet. Use AI memo saves from reports or open the Research workspace to start building the thesis.</p>
) : (
<div className="space-y-3">
{journalEntries.map((entry) => {
const canEdit = entry.entry_type !== 'status_change';
return (
<article
key={entry.id}
ref={(node) => {
journalEntryRefs.current.set(entry.id, node);
}}
className={`rounded-xl border bg-[color:var(--panel-soft)] p-4 transition ${
highlightedJournalId === entry.id
? 'border-[color:var(--line-strong)] shadow-[0_0_0_1px_rgba(0,255,180,0.14),0_0_28px_rgba(0,255,180,0.16)]'
: 'border-[color:var(--line-weak)]'
}`}
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<p className="text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">
{entry.entry_type.replace('_', ' ')} · {formatDateTime(entry.updated_at)}
</p>
<h4 className="mt-1 text-sm font-semibold text-[color:var(--terminal-bright)]">
{entry.title ?? 'Untitled entry'}
</h4>
</div>
{entry.accession_number ? (
<Link
href={`/filings?ticker=${activeTicker}`}
onMouseEnter={() => prefetchResearchTicker(activeTicker)}
onFocus={() => prefetchResearchTicker(activeTicker)}
className="text-xs uppercase tracking-[0.12em] text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]"
>
Open filing stream
</Link>
) : null}
{journalEntries.slice(0, 4).map((entry) => (
<article key={entry.id} className="rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<p className="text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">
{entry.entry_type.replace('_', ' ')} · {formatDateTime(entry.updated_at)}
</p>
<h4 className="mt-1 text-sm font-semibold text-[color:var(--terminal-bright)]">{entry.title ?? 'Untitled entry'}</h4>
</div>
<p className="mt-3 whitespace-pre-wrap text-sm leading-6 text-[color:var(--terminal-bright)]">{entry.body_markdown}</p>
{canEdit ? (
<div className="mt-4 flex flex-wrap items-center gap-2">
<Button
variant="ghost"
className="px-2 py-1 text-xs"
onClick={() => beginEditJournalEntry(entry)}
>
<SquarePen className="size-3" />
Edit
</Button>
<Button
variant="danger"
className="px-2 py-1 text-xs"
onClick={() => {
void removeJournalEntry(entry);
}}
>
<Trash2 className="size-3" />
Delete
</Button>
</div>
) : null}
</article>
);
})}
<Link
href={`/research?ticker=${encodeURIComponent(activeTicker)}`}
onMouseEnter={() => prefetchResearchTicker(activeTicker)}
onFocus={() => prefetchResearchTicker(activeTicker)}
className="text-xs uppercase tracking-[0.12em] text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]"
>
Open workspace
</Link>
</div>
<p className="mt-3 whitespace-pre-wrap text-sm leading-6 text-[color:var(--terminal-bright)]">{entry.body_markdown}</p>
</article>
))}
</div>
)}
</Panel>
@@ -980,7 +937,7 @@ function AnalysisPageContent() {
<Panel>
<div className="flex items-center gap-2 text-xs uppercase tracking-[0.24em] text-[color:var(--terminal-muted)]">
<ChartNoAxesCombined className="size-4" />
Analysis scope: price + filings + ai synthesis + research journal
Analysis scope: price + filings + ai synthesis + research workspace
</div>
</Panel>
</AppShell>