TypeScript 55.4%
Python 43.2%
SQL 1.2%
1import Link from "next/link";2import type { Metadata } from "next";3import { EventRow } from "@/components/event-row";4import { Badge, Chip, Empty, Flag, Kbd, PageHeader, Panel, Score, StateBadge, TierBadge } from "@/components/ui";5import { api } from "@/lib/api";6import { relTime, typeLabel, utcDateTime } from "@/lib/format";78export const dynamic = "force-dynamic";9export const metadata: Metadata = { title: "Search", robots: { index: false } };1011const SYNTAX: { key: string; example: string; help: string }[] = [12 { key: "entity", example: "entity:openai", help: "events linked to an entity" },13 { key: "type", example: "type:pricing_change", help: "event type" },14 { key: "after", example: "after:7d", help: "detected after (7d, 24h, 2026-09-01)" },15 { key: "silent", example: "silent:true", help: "silent changes only" },16 { key: "importance", example: "importance:>70", help: "minimum importance" },17 { key: "country", example: "country:CA", help: "source country" },18 { key: "first_party", example: "first_party:true", help: "first-party evidence only" },19];2021function fmtFilter(k: string, v: unknown): string {22 if (v === null || v === undefined) return "";23 if (typeof v === "string" && /^\d{4}-\d{2}-\d{2}T/.test(v)) return utcDateTime(v);24 if (Array.isArray(v)) return v.join(", ");25 return String(v);26}2728/** Search (spec §30): full-text + structured filters, results across events, clusters, entities, sources and URLs. */29export default async function SearchPage({ searchParams }: { searchParams: Promise<{ q?: string }> }) {30 const { q = "" } = await searchParams;31 const query = q.trim();32 const r = query.length >= 2 ? await api.search(query) : null;33 const filters = Object.entries(r?.parsed?.filters ?? {}).filter(([, v]) => v !== undefined && v !== null && v !== "");34 const text = r?.parsed?.text?.trim() ?? "";35 const clusters = r?.clusters ?? [];36 const total = r ? r.events.length + clusters.length + r.entities.length + r.sources.length + r.urls.length : 0;37 return (38 <>39 <PageHeader compact kicker="Search everything" title={query ? <span>Results for “{query}”</span> : "Search"} description="Events, clusters, entities, sources and URLs. Full-text over titles, summaries and keywords; structured filters narrow the event set." />40 <form action="/search" className="mb-2 flex gap-2">41 <input name="q" defaultValue={q} autoFocus placeholder="OpenAI, CVE-2026, pricing, status.…" aria-label="Search" className="h-9 w-full min-w-0 max-w-2xl rounded-md border border-line bg-panel px-3 font-mono text-[13px] placeholder:text-fg-subtle" />42 <button type="submit" className="h-9 rounded-md border border-line bg-panel-2 px-3 text-[13px] hover:border-line-strong">Search</button>43 </form>44 <div className="mb-4 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11.5px] text-fg-subtle">45 <span>Syntax</span>46 {SYNTAX.map((s) => (47 <Link key={s.key} href={`/search?q=${encodeURIComponent(`${query ? query + " " : ""}${s.example}`)}`} title={s.help} className="rounded-sm border border-line bg-panel-2 px-1.5 py-px font-mono text-[10.5px] text-fg-muted hover:text-fg">{s.example}</Link>48 ))}49 <span className="hidden items-center gap-1 sm:inline-flex"><Kbd>⌘K</Kbd> anywhere</span>50 </div>5152 {r && (53 <>54 <div className="mb-4 flex flex-wrap items-center gap-1.5 text-[12px]">55 <span className="text-fg-subtle">{total} result{total === 1 ? "" : "s"}</span>56 {text && <Chip tone="default" title="Full-text term">“{text}”</Chip>}57 {filters.map(([k, v]) => (58 <Chip key={k} tone="info" className="font-mono" title={`Filter ${k}`}>{k}: {fmtFilter(k, v)}</Chip>59 ))}60 {filters.length > 0 && <Link href={`/search?q=${encodeURIComponent(text)}`} className="text-[11px] text-fg-subtle hover:text-fg">clear filters</Link>}61 </div>62 <div className="grid gap-4 xl:grid-cols-[1fr_360px]">63 <div className="flex min-w-0 flex-col gap-4">64 <Panel title={`Events · ${r.events.length}`} dense action={r.events.length ? <Link href={`/live?q=${encodeURIComponent(text || query)}`} className="text-[11px] text-fg-subtle hover:text-fg">live →</Link> : undefined}>65 {r.events.length ? r.events.map((e) => <EventRow key={e.id} ev={e} showDate />) : <Empty>No event matches{filters.length ? " these filters" : ""}. Try a broader term, remove a filter, or extend the window with <span className="font-mono">after:30d</span>.</Empty>}66 </Panel>67 <Panel title={`Clusters · ${clusters.length}`} dense>68 {clusters.length ? (69 <ul className="divide-y divide-line">70 {clusters.map((c) => (71 <li key={c.id} className="grid grid-cols-[auto_1fr] items-start gap-x-3 px-3 py-2 text-[13px]">72 <Score value={c.max_importance} size="sm" />73 <div className="min-w-0">74 <div className="flex flex-wrap items-center gap-x-2 text-[11px] text-fg-subtle">75 <StateBadge state={c.state} />76 <span>{c.event_count} signal{c.event_count === 1 ? "" : "s"} · {c.source_count} source{c.source_count === 1 ? "" : "s"}</span>77 <span className="ml-auto font-mono tabular">{relTime(c.last_at)}</span>78 </div>79 <Link href={`/cluster/${c.slug ?? c.id}`} className="mt-0.5 block font-medium leading-snug hover:underline">{c.title}</Link>80 </div>81 </li>82 ))}83 </ul>84 ) : (85 <Empty>No cluster matches. Clusters group related signals across sources.</Empty>86 )}87 </Panel>88 </div>89 <aside className="flex min-w-0 flex-col gap-4">90 <Panel title={`Entities · ${r.entities.length}`} dense>91 {r.entities.length ? (92 <ul className="divide-y divide-line">93 {r.entities.map((e) => (94 <li key={e.id} className="flex items-center gap-2 px-3 py-1.5 text-[13px]">95 <div className="min-w-0 flex-1">96 <Link href={`/entity/${e.id}`} className="block truncate font-medium hover:underline">{e.name}</Link>97 <div className="truncate font-mono text-[10.5px] text-fg-subtle">{e.domain ?? e.id}{e.event_count ? ` · ${e.event_count} ev` : ""}</div>98 </div>99 <Chip>{e.type.replace(/_/g, " ")}</Chip>100 </li>101 ))}102 </ul>103 ) : (104 <Empty>No entity matches.</Empty>105 )}106 </Panel>107 <Panel title={`Sources · ${r.sources.length}`} dense>108 {r.sources.length ? (109 <ul className="divide-y divide-line">110 {r.sources.map((s) => (111 <li key={s.id} className="flex items-center gap-2 px-3 py-1.5 text-[13px]">112 <TierBadge tier={s.tier} />113 <div className="min-w-0 flex-1">114 <Link href={`/source/${s.id}`} className="block truncate font-medium hover:underline">{s.name}</Link>115 <Link href={`/domain/${s.domain}`} className="block truncate font-mono text-[10.5px] text-fg-subtle hover:underline">{s.domain}</Link>116 </div>117 {s.country && <Flag code={s.country} className="text-[12px]" />}118 {s.first_party === false ? <Badge kind="external" compact /> : <Badge kind="first-party" compact />}119 </li>120 ))}121 </ul>122 ) : (123 <Empty>No source matches.</Empty>124 )}125 </Panel>126 <Panel title={`URLs · ${r.urls.length}`} dense>127 {r.urls.length ? (128 <ul className="divide-y divide-line">129 {r.urls.map((u) => (130 <li key={u.url} className="px-3 py-1.5 text-[12px]">131 <Link href={`/url?u=${encodeURIComponent(u.url)}`} className="block truncate font-mono hover:underline" title={u.url}>{u.url}</Link>132 <span className="flex items-center gap-2 text-[11px] text-fg-subtle">133 <span>{u.change_count} change{u.change_count === 1 ? "" : "s"}</span>134 <span>·</span>135 <span>{relTime(u.last_seen_at)}</span>136 {u.status && u.status !== "active" && <Chip tone="warn" className="font-mono">{typeLabel(u.status)}</Chip>}137 </span>138 </li>139 ))}140 </ul>141 ) : (142 <Empty>No monitored URL matches.</Empty>143 )}144 </Panel>145 </aside>146 </div>147 </>148 )}149 {!r && query.length > 0 && query.length < 2 && <Empty>Type at least two characters.</Empty>}150 </>151 );152}153