2026-05-18 01:44:36 +00:00
|
|
|
/**
|
|
|
|
|
* TimelineView — chronological event list grouped by decade with filter controls.
|
|
|
|
|
*/
|
|
|
|
|
"use client";
|
|
|
|
|
import Link from "next/link";
|
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
|
|
|
|
|
|
|
interface TimelineEntry {
|
|
|
|
|
entity_class: string;
|
|
|
|
|
entity_id: string;
|
|
|
|
|
canonical_name: string;
|
|
|
|
|
date_start: string | null;
|
|
|
|
|
date_end: string | null;
|
|
|
|
|
primary_location: string | null;
|
|
|
|
|
narrative_summary: string | null;
|
phase-0: kill stubs, ship 20 curated anchor events, configure SMTP
- scripts/03-dedup-entities.py: stop emitting placeholder narrative ("Stub. Will
be enriched in Phase 7"); write summary_status=none + null fields instead.
- scripts/maintain/41_strip_stubs.py: idempotent migration that cleaned the
22,096 entity .md files (now zero stub strings in wiki/).
- scripts/synthesize/01_anchor_events.py: curated 20 anchor UAP events
(Roswell, Nimitz Tic-Tac, Phoenix Lights, Operação Prato, AATIP, etc.) with
bilingual Holmes-Watson narrative via claude -p --model sonnet
(CLAUDE_CODE_OAUTH_TOKEN). All summary_status=curated, confidence=high.
- web/api/timeline + timeline-view: filter narrative-less events by default,
render "curado" badge for hand-vetted ones, drop the date display alone.
- CLAUDE-schema-full.md: document the summary_status enum and the four states.
- docker-compose.yml: SMTP_HOST=mail.spacemail.com configured;
GOTRUE_MAILER_AUTOCONFIRM flipped to false (real email confirmation working).
- .nirvana/outputs/.../systems-atelier/: 5 deliverables of the architecture
audit that produced this roadmap.
2026-05-18 03:44:17 +00:00
|
|
|
summary_status?: "none" | "synthesized" | "curated" | "red_teamed";
|
|
|
|
|
total_mentions?: number;
|
2026-05-18 01:44:36 +00:00
|
|
|
href: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function TimelineView({ initialSearch }: { initialSearch?: string }) {
|
|
|
|
|
const [q, setQ] = useState(initialSearch ?? "");
|
|
|
|
|
const [from, setFrom] = useState("1940");
|
|
|
|
|
const [to, setTo] = useState("2026");
|
|
|
|
|
const [data, setData] = useState<TimelineEntry[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
class: "event",
|
|
|
|
|
from,
|
|
|
|
|
to,
|
|
|
|
|
limit: "500",
|
|
|
|
|
});
|
|
|
|
|
if (q.trim()) params.set("q", q.trim());
|
|
|
|
|
fetch(`/api/timeline?${params}`)
|
|
|
|
|
.then((r) => r.json())
|
|
|
|
|
.then((j: { entries: TimelineEntry[] }) => {
|
|
|
|
|
setData(j.entries ?? []);
|
|
|
|
|
setLoading(false);
|
|
|
|
|
})
|
|
|
|
|
.catch(() => setLoading(false));
|
|
|
|
|
}, [q, from, to]);
|
|
|
|
|
|
|
|
|
|
const byDecade = useMemo(() => {
|
|
|
|
|
const map = new Map<string, TimelineEntry[]>();
|
|
|
|
|
for (const e of data) {
|
|
|
|
|
const year = (e.date_start ?? "").slice(0, 4);
|
|
|
|
|
if (!year) continue;
|
|
|
|
|
const decade = `${year.slice(0, 3)}0s`;
|
|
|
|
|
if (!map.has(decade)) map.set(decade, []);
|
|
|
|
|
map.get(decade)!.push(e);
|
|
|
|
|
}
|
|
|
|
|
return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
|
|
|
}, [data]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex flex-wrap items-end gap-3 mb-6 p-3 border border-[rgba(0,255,156,0.15)] bg-[#0a121e] rounded">
|
|
|
|
|
<div className="flex-1 min-w-[200px]">
|
|
|
|
|
<label className="font-mono text-[10px] uppercase tracking-widest text-[#5a6678] block mb-1">
|
|
|
|
|
buscar
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
value={q}
|
|
|
|
|
onChange={(e) => setQ(e.target.value)}
|
|
|
|
|
placeholder="ex. Roswell, Nimitz, Hoover..."
|
|
|
|
|
className="w-full bg-transparent border border-[rgba(0,255,156,0.20)] focus:border-[#00ff9c] rounded px-2 py-1.5 font-mono text-sm text-[#c8d4e6] outline-none"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="font-mono text-[10px] uppercase tracking-widest text-[#5a6678] block mb-1">
|
|
|
|
|
de
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
value={from}
|
|
|
|
|
onChange={(e) => setFrom(e.target.value)}
|
|
|
|
|
placeholder="1940"
|
|
|
|
|
className="w-20 bg-transparent border border-[rgba(0,255,156,0.20)] focus:border-[#00ff9c] rounded px-2 py-1.5 font-mono text-sm text-[#c8d4e6] outline-none"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="font-mono text-[10px] uppercase tracking-widest text-[#5a6678] block mb-1">
|
|
|
|
|
até
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
value={to}
|
|
|
|
|
onChange={(e) => setTo(e.target.value)}
|
|
|
|
|
placeholder="2026"
|
|
|
|
|
className="w-20 bg-transparent border border-[rgba(0,255,156,0.20)] focus:border-[#00ff9c] rounded px-2 py-1.5 font-mono text-sm text-[#c8d4e6] outline-none"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="font-mono text-xs text-[#5a6678]">
|
|
|
|
|
{loading ? "…" : `${data.length} eventos`}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-10">
|
|
|
|
|
{byDecade.map(([decade, items]) => (
|
|
|
|
|
<section key={decade}>
|
|
|
|
|
<h2 className="font-mono text-sm text-[#7fdbff] uppercase tracking-widest mb-3 border-l-2 border-[#7fdbff] pl-3 sticky top-0 bg-[#040810]/95 py-1 z-10">
|
|
|
|
|
{decade} <span className="text-[#5a6678]">· {items.length}</span>
|
|
|
|
|
</h2>
|
|
|
|
|
<ol className="space-y-2 relative before:absolute before:left-3 before:top-0 before:bottom-0 before:w-px before:bg-[rgba(255,165,0,0.30)] pl-8">
|
|
|
|
|
{items.map((e) => (
|
|
|
|
|
<li key={e.entity_id} className="relative">
|
|
|
|
|
<span className="absolute -left-[26px] top-1.5 w-3 h-3 rounded-full bg-[#ffa500] border-2 border-[#040810]" />
|
|
|
|
|
<Link
|
|
|
|
|
href={e.href}
|
|
|
|
|
className="block py-1 px-3 rounded border border-transparent hover:border-[rgba(0,255,156,0.30)] hover:bg-[rgba(0,255,156,0.04)] transition"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-baseline gap-3 font-mono text-xs">
|
|
|
|
|
<span className="text-[#ffa500] font-bold tabular-nums">
|
|
|
|
|
{e.date_start}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-[#c8d4e6] font-bold flex-1 truncate">
|
|
|
|
|
{e.canonical_name}
|
|
|
|
|
</span>
|
phase-0: kill stubs, ship 20 curated anchor events, configure SMTP
- scripts/03-dedup-entities.py: stop emitting placeholder narrative ("Stub. Will
be enriched in Phase 7"); write summary_status=none + null fields instead.
- scripts/maintain/41_strip_stubs.py: idempotent migration that cleaned the
22,096 entity .md files (now zero stub strings in wiki/).
- scripts/synthesize/01_anchor_events.py: curated 20 anchor UAP events
(Roswell, Nimitz Tic-Tac, Phoenix Lights, Operação Prato, AATIP, etc.) with
bilingual Holmes-Watson narrative via claude -p --model sonnet
(CLAUDE_CODE_OAUTH_TOKEN). All summary_status=curated, confidence=high.
- web/api/timeline + timeline-view: filter narrative-less events by default,
render "curado" badge for hand-vetted ones, drop the date display alone.
- CLAUDE-schema-full.md: document the summary_status enum and the four states.
- docker-compose.yml: SMTP_HOST=mail.spacemail.com configured;
GOTRUE_MAILER_AUTOCONFIRM flipped to false (real email confirmation working).
- .nirvana/outputs/.../systems-atelier/: 5 deliverables of the architecture
audit that produced this roadmap.
2026-05-18 03:44:17 +00:00
|
|
|
{e.summary_status === "curated" && (
|
|
|
|
|
<span className="px-1.5 py-0.5 rounded border border-[#00ff9c] text-[#00ff9c] text-[9px]" title="curado manualmente">
|
|
|
|
|
curado
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-05-18 01:44:36 +00:00
|
|
|
{e.primary_location && (
|
|
|
|
|
<span className="text-[#3fde6a] text-[10px]">{e.primary_location}</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{e.narrative_summary && (
|
|
|
|
|
<p className="mt-0.5 text-[#8896aa] text-xs line-clamp-2">
|
|
|
|
|
{e.narrative_summary}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</Link>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ol>
|
|
|
|
|
</section>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{!loading && data.length === 0 && (
|
|
|
|
|
<div className="text-center text-[#5a6678] font-mono text-sm py-12">
|
|
|
|
|
nenhum evento encontrado nesse filtro
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|