Phase 5: alert subscriptions UI + multiple watchlists + queue fixes
CI / Test & Type-Check (push) Canceled after 0s

UI:
- /alerts page: event history with acknowledge, subscription create/manage with toggle
- /admin/smtp: SMTP config form (host, port, auth, test)
- Watchlist sidebar: dropdown selector for multiple watchlists, create/delete
- Sidebar: alerts count badge, SMTP link under admin
- Mobile tab nav: alerts tab added
- Client trpc.ts: all new API methods + types

Backend:
- watchlists.listByWatchlist procedure + listSymbolsByWatchlist repo fn
- yfinance min-interval 1500->2000ms to reduce Edge 429s
- Fixed e.date.slice error in yfinance-adjustments with typeof guard
- Removed defunct BITF from watchlist+queue
- Cleared 83 failed + 12 backoff queue jobs

Docs:
- FUNCTIONAL_DESIGN.md: alerts + multiple watchlists + SMTP documented
- TECH_DESIGN.md: new modules, tRPC procs, routes updated
This commit is contained in:
Investor Flow Build
2026-07-23 20:51:47 -04:00
parent f71987c119
commit 5b9f770aa4
23 changed files with 1567 additions and 146 deletions
+16 -1
View File
@@ -10,8 +10,10 @@ import {
Settings,
ChevronDown,
Search,
Bell,
type LucideIcon,
} from "lucide-react";
import { api } from "@/lib/trpc";
/**
* Left sidebar navigation with collapsible sections.
@@ -58,6 +60,7 @@ const SECTIONS: NavSection[] = [
{ label: "Risk Posture", href: "/risk" },
{ label: "Filings", href: "/filings" },
{ label: "Options DD", href: "/options-dd" },
{ label: "Alerts", href: "/alerts" },
],
},
{
@@ -69,6 +72,7 @@ const SECTIONS: NavSection[] = [
{ label: "Queue", href: "/admin/queue" },
{ label: "Audit Logs", href: "/admin/audit-logs" },
{ label: "X Accounts", href: "/admin/x-accounts" },
{ label: "SMTP", href: "/admin/smtp" },
],
},
{
@@ -101,6 +105,11 @@ export function SidebarNav() {
const normalizedPath = (pathname || "/").replace(/\/+$/, "") || "/";
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
const [initialized, setInitialized] = useState(false);
const [unackedCount, setUnackedCount] = useState(0);
useEffect(() => {
api.alerts.unackedCount().then((r) => setUnackedCount(r.count)).catch(() => {});
}, []);
useEffect(() => {
setCollapsed(getInitialCollapsed(pathname));
@@ -146,6 +155,7 @@ export function SidebarNav() {
<div className="mt-0.5 space-y-0.5">
{section.items.map((item) => {
const isActive = normalizedPath === item.href;
const showBadge = item.label === "Alerts" && unackedCount > 0;
return (
<Link
key={item.label}
@@ -156,7 +166,12 @@ export function SidebarNav() {
: "text-fg-muted hover:text-fg hover:bg-surface-overlay border-transparent"
}`}
>
{item.label}
<span className="flex items-center justify-between">
<span>{item.label}</span>
{showBadge && (
<span className="rounded-full bg-accent px-1.5 py-0.5 text-2xs text-white">{unackedCount}</span>
)}
</span>
</Link>
);
})}