From ab4fe2a334cd56f995ea009913a6449fb7d0a423 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Date: Sun, 24 May 2026 14:09:46 -0300 Subject: [PATCH] =?UTF-8?q?W5.1:=20enthusiast=20pivot=20=E2=80=94=20strip?= =?UTF-8?q?=20detective=20surfacing,=20magazine=20homepage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User explicit: "1 bilhão de entusiastas pelo mundo ovni" — site is for the UFO-curious public, not for skeptics. The 8-detective scaffolding becomes invisible plumbing; the reader sees stories about what was observed. Reader-facing changes: New homepage (web/app/page.tsx) - SiteHeader: magazine-style top nav (no detective tiles) - HeroBanner: full-bleed editorial opener with declassified-page art background, display-serif headline, live stats row (122 docs, 2047 events, 1861 witnesses, 867 craft catalogued) - FeaturedCase: cover-story treatment of the most recent case_report, uses a real document page as hero image, links to /c/[slug] - PortalGrid: 6 thematic doorways into the archive — Sightings, Witnesses, Craft, Hot spots, Programs, Documents — each tile shows a real entity count and short editorial blurb - GreatestHits: top 9 most-cited events from the corpus (Kenneth Arnold 1947, Mantell 1948, …) as a magazine grid - Doc list kept but reframed as "the primary record" New sub-pages (5) - /sightings → events (2047), magazine grid - /witnesses → people (1861), compact table - /objects → uap_objects (867), magazine grid - /locations → locations (1757), compact table - /operations → organizations (1596), compact table - /documents → full doc list with thumbnails (mirrors homepage section for direct deep-link) All share shell with per-page i18n + JSON-LD ItemList Stripped detective surfacing - /jobs/[id]: "Sherlock Holmes / Dr. Watson" → "Investigation in progress" - chat-bubble: detective-named card → neutral "Investigação em andamento" - quick-launch: 7-kind detective dropdown → single "investigar um caso" input (kind=case_report hardcoded) - /bureau: rewritten as the case-file library (no artefact dumps) Typography + design - Fraunces variable serif loaded for display headings (`.font-display` class) - Gold-amber accent (#e0c080) unified as the brand colour - Asymmetric magazine grids (1+2+3 column, generous whitespace) - Hover micro-interactions (image scale on featured case, translateX on portal arrows) SEO + GEO - layout.tsx metadataBase + title.template + per-route Metadata exports - Organization JSON-LD on root layout - WebSite + SearchAction JSON-LD on homepage - CollectionPage + ItemList JSON-LD on every entity list page - openGraph + twitter cards, pt-BR primary + en-US alternate - ai:purpose meta tag for Generative Engine Optimization — declares the site as a citation-linked primary-source archive - robots: index + follow with large image preview The detectives themselves remain alive in the backend (runtime, DB, audit log), but the reader never sees "Holmes / Sun-Tzu / Watson" in the UI. The next phase will reorient case-writer to write as a single best-seller voice synthesising all the internal sources. Co-Authored-By: Claude Opus 4.7 (1M context) --- web/app/bureau/page.tsx | 321 ++-------------------------- web/app/documents/page.tsx | 66 ++++++ web/app/globals.css | 5 + web/app/jobs/[id]/page.tsx | 54 +---- web/app/layout.tsx | 85 +++++++- web/app/locations/page.tsx | 26 +++ web/app/objects/page.tsx | 26 +++ web/app/operations/page.tsx | 26 +++ web/app/page.tsx | 153 +++++++------ web/app/sightings/page.tsx | 26 +++ web/app/witnesses/page.tsx | 26 +++ web/components/case-library.tsx | 203 ++++++++++++++++++ web/components/chat-bubble.tsx | 29 +-- web/components/doc-bureau-panel.tsx | 260 +++++----------------- web/components/entity-list-page.tsx | 182 ++++++++++++++++ web/components/featured-case.tsx | 140 ++++++++++++ web/components/greatest-hits.tsx | 87 ++++++++ web/components/hero-banner.tsx | 120 +++++++++++ web/components/portal-grid.tsx | 122 +++++++++++ web/components/quick-launch.tsx | 48 ++--- web/components/site-header.tsx | 45 ++++ 21 files changed, 1365 insertions(+), 685 deletions(-) create mode 100644 web/app/documents/page.tsx create mode 100644 web/app/locations/page.tsx create mode 100644 web/app/objects/page.tsx create mode 100644 web/app/operations/page.tsx create mode 100644 web/app/sightings/page.tsx create mode 100644 web/app/witnesses/page.tsx create mode 100644 web/components/case-library.tsx create mode 100644 web/components/entity-list-page.tsx create mode 100644 web/components/featured-case.tsx create mode 100644 web/components/greatest-hits.tsx create mode 100644 web/components/hero-banner.tsx create mode 100644 web/components/portal-grid.tsx create mode 100644 web/components/site-header.tsx diff --git a/web/app/bureau/page.tsx b/web/app/bureau/page.tsx index a4055ba..4e7a782 100644 --- a/web/app/bureau/page.tsx +++ b/web/app/bureau/page.tsx @@ -1,327 +1,38 @@ /** - * /bureau — Investigation Bureau hub. + * /bureau — Case file library. * - * Full listing of every artefact: evidence, hypotheses, contradictions, - * witnesses, outliers, case reports, recent jobs. Each anchor-section - * matches the hash-links from the homepage's counter row. + * Reader-facing list of every assembled narrative. No detective surfacing, + * no artefact dumps. Just stories with hooks. */ -import Link from "next/link"; -import { pgQuery } from "@/lib/retrieval/db"; import { AuthBar } from "@/components/auth-bar"; import { BureauNav } from "@/components/bureau-nav"; +import { CaseLibrary } from "@/components/case-library"; +import { getLocale } from "@/components/locale-toggle"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; -interface EvidenceRow { - evidence_id: string; - grade: string; - verbatim_excerpt: string; - source_page_id: string; - confidence_band: string | null; -} -interface HypothesisRow { - hypothesis_id: string; - question: string; - question_pt_br: string | null; - position: string; - position_pt_br: string | null; - prior: number | string | null; - posterior: number | string | null; - confidence_band: string | null; - status: string; - reviewed_by: string | null; - created_at: string; -} -interface ContradictionRow { - contradiction_id: string; - topic: string; - topic_pt_br: string | null; - resolution_status: string; - chunks: unknown; -} -interface GapRow { - gap_id: string; - description: string; - description_pt_br: string | null; - scope: unknown; - status: string; - suggested_next_move: string | null; - suggested_next_move_pt_br: string | null; -} -interface WitnessRow { - witness_id: string; - canonical_name: string | null; - entity_id: string | null; - credibility: string | null; - verdict: string | null; - verdict_pt_br: string | null; -} -interface JobRow { - job_id: string; - kind: string; - status: string; - payload: Record | null; - created_at: string; - finished_at: string | null; - triggered_by: string | null; -} - -const BAND_TONE: Record = { - high: "text-[#06d6a0] border-[#06d6a0]", - medium: "text-[#3fde6a] border-[#3fde6a]", - low: "text-[#ffa500] border-[#ffa500]", - speculation: "text-[#ff6ec7] border-[#ff6ec7]", -}; - -const GRADE_TONE: Record = { - A: "text-[#06d6a0] border-[#06d6a0]", - B: "text-[#3fde6a] border-[#3fde6a]", - C: "text-[#ffa500] border-[#ffa500]", -}; - export default async function BureauPage() { - // All artefacts. Server component — single round per query, no n+1. - const [hyp, ev, ctr, gap, wit, jobs] = await Promise.all([ - pgQuery( - `SELECT hypothesis_id, question, question_pt_br, position, position_pt_br, - prior, posterior, confidence_band, status, reviewed_by, created_at - FROM public.hypotheses ORDER BY created_at DESC LIMIT 100`, - ).catch(() => []), - pgQuery( - `SELECT evidence_id, grade, verbatim_excerpt, source_page_id, confidence_band - FROM public.evidence ORDER BY created_at DESC LIMIT 100`, - ).catch(() => []), - pgQuery( - `SELECT contradiction_id, topic, topic_pt_br, resolution_status, chunks - FROM public.contradictions ORDER BY created_at DESC LIMIT 100`, - ).catch(() => []), - pgQuery( - `SELECT gap_id, description, description_pt_br, scope, status, - suggested_next_move, suggested_next_move_pt_br - FROM public.gaps ORDER BY created_at DESC LIMIT 100`, - ).catch(() => []), - pgQuery( - `SELECT w.witness_id, e.canonical_name, e.entity_id, w.credibility, - w.verdict, w.verdict_pt_br - FROM public.witnesses w - LEFT JOIN public.entities e ON e.entity_pk = w.person_entity_pk - ORDER BY w.created_at DESC LIMIT 100`, - ).catch(() => []), - pgQuery( - `SELECT job_id, kind, status, payload, created_at, finished_at, triggered_by - FROM public.investigation_jobs ORDER BY created_at DESC LIMIT 25`, - ).catch(() => []), - ]); - - const { readdir, stat, readFile } = await import("node:fs/promises"); - const path = await import("node:path"); - const reportsDir = path.join(process.env.CASE_ROOT || "/data/ufo/case", "reports"); - let reports: Array<{ slug: string; topic: string; mtimeMs: number; n_evidence: number; n_hypotheses: number }> = []; - try { - const files = await readdir(reportsDir); - const items = await Promise.all( - files.filter((f) => f.endsWith(".md")).map(async (f) => { - const full = path.join(reportsDir, f); - const st = await stat(full); - const head = (await readFile(full, "utf-8")).slice(0, 2000); - const topicMatch = head.match(/topic:\s*"([^"]+)"/); - const eMatch = head.match(/n_evidence:\s*(\d+)/); - const hMatch = head.match(/n_hypotheses:\s*(\d+)/); - return { - slug: f.replace(/\.md$/, ""), - topic: topicMatch?.[1] ?? f, - mtimeMs: st.mtimeMs, - n_evidence: Number(eMatch?.[1] ?? 0), - n_hypotheses: Number(hMatch?.[1] ?? 0), - }; - }), - ); - reports = items.sort((a, b) => b.mtimeMs - a.mtimeMs); - } catch { /* fine */ } + const locale = (await getLocale()) === "en" ? "en" : "pt-br"; return (
- + -
- -
-

▍ The Investigation Bureau

+
+
+

+ {locale === "en" ? "▍ The Case Files" : "▍ Os Arquivos do Caso"} +

- Live case folder · {ev.length} evidence · {hyp.length} hypotheses · {ctr.length} contradictions ·{" "} - {wit.length} witnesses · {gap.length} outliers · {reports.length} reports + {locale === "en" + ? "Narratives assembled from the declassified record. Each case is a story — written from primary documents, with citations linked to source pages." + : "Narrativas montadas a partir do registro desclassificado. Cada caso é uma história — escrita a partir de documentos primários, com citações vinculadas às páginas-fonte."}

- {/* Case reports */} -
- {reports.length === 0 ? : reports.map((r) => ( - -
/c/{r.slug}
-
{r.topic}
-
- {r.n_hypotheses} hypotheses · {r.n_evidence} evidence -
- - ))} -
- - {/* Hypotheses */} -
- {hyp.length === 0 ? : hyp.map((h) => { - const post = h.posterior !== null ? Number(h.posterior) : null; - const prior = h.prior !== null ? Number(h.prior) : null; - const delta = post !== null && prior !== null ? post - prior : null; - const bandTone = (h.confidence_band && BAND_TONE[h.confidence_band]) || "text-[#9aa6b8] border-[#9aa6b8]"; - return ( - -
- {h.hypothesis_id} · {h.status} -
- {h.confidence_band && ( - {h.confidence_band} - )} - {h.reviewed_by && ( - ↳ {h.reviewed_by} - )} -
-
-
{h.position_pt_br || h.position}
-
- prior {prior?.toFixed(2) ?? "—"} → posterior {post?.toFixed(2) ?? "—"} - {delta !== null && 0 ? " text-[#06d6a0]" : delta < 0 ? " text-[#ff6ec7]" : ""}> · Δ {delta >= 0 ? "+" : ""}{delta.toFixed(3)}} -
- - ); - })} -
- - {/* Evidence */} -
- {ev.length === 0 ? : ev.map((e) => { - const tone = (e.grade && GRADE_TONE[e.grade]) || "text-[#9aa6b8] border-[#9aa6b8]"; - return ( -
-
- {e.evidence_id} · {e.source_page_id} - Grade {e.grade} -
-
- “{e.verbatim_excerpt}” -
-
- ); - })} -
- - {/* Contradictions */} -
- {ctr.length === 0 ? : ctr.map((c) => { - const n = Array.isArray(c.chunks) ? c.chunks.length : 0; - return ( -
-
- {c.contradiction_id} - {n} positions · {c.resolution_status} -
-
{c.topic_pt_br || c.topic}
-
- ); - })} -
- - {/* Outliers */} -
- {gap.length === 0 ? : gap.map((g) => { - const s = (g.scope ?? {}) as Record; - const title = (s.title_pt_br as string) || (s.title as string) || g.description_pt_br || g.description; - const why = (s.why_surprising_pt_br as string) || (s.why_surprising as string) || null; - const isOutlier = s.kind === "outlier"; - const nextMove = g.suggested_next_move_pt_br || g.suggested_next_move; - return ( -
-
- {g.gap_id}{isOutlier && " · outlier"} - {g.status} -
-
{title}
- {why && ( -
{why}
- )} - {nextMove && ( -
→ {nextMove}
- )} -
- ); - })} -
- - {/* Witnesses */} -
- {wit.length === 0 ? : wit.map((w) => ( -
-
- {w.witness_id} - {w.credibility ?? "—"} -
-
- {w.entity_id ? {w.canonical_name ?? w.entity_id} : (w.canonical_name ?? "—")} -
- {(w.verdict_pt_br || w.verdict) &&
{w.verdict_pt_br || w.verdict}
} -
- ))} -
- - {/* Recent jobs */} -
- {jobs.length === 0 ? : ( -
- - - - - - - - - - - {jobs.map((j) => ( - - - - - - - ))} - -
jobkindstatuscreated
- {j.job_id.slice(0, 8)}… - {j.kind}{j.status}{new Date(j.created_at).toLocaleString("pt-BR")}
-
- )} -
+
); } - -function Section({ id, title, color, children }: { id: string; title: string; color: string; children: React.ReactNode }) { - return ( -
-

// {title}

-
{children}
-
- ); -} - -function Empty() { - return
_(none yet)_
; -} diff --git a/web/app/documents/page.tsx b/web/app/documents/page.tsx new file mode 100644 index 0000000..849be46 --- /dev/null +++ b/web/app/documents/page.tsx @@ -0,0 +1,66 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { listDocuments, readDocument } from "@/lib/wiki"; +import { getLocale } from "@/components/locale-toggle"; +import { SiteHeader } from "@/components/site-header"; +import { BureauNav } from "@/components/bureau-nav"; +import { summarize, pickPitch } from "@/lib/doc-summary"; +import { DocListFilters } from "@/components/doc-list-filters"; + +export const dynamic = "force-dynamic"; + +export const metadata: Metadata = { + title: "Documentos desclassificados — Arquivo UAP/UFO", + description: + "Cada memorando, telegrama e relatório desclassificado do Departamento de Guerra dos EUA sobre UAP/UFO. " + + "Páginas indexadas, citações vinculadas, busca por texto.", +}; + +export default async function DocumentsPage() { + const locale = (await getLocale()) === "en" ? "en" : "pt-br"; + const ids = await listDocuments(); + const summaryLang: "pt" | "en" = locale === "en" ? "en" : "pt"; + + const docs = await Promise.all( + ids.map(async (id) => { + const f = await readDocument(id); + return { + id, + title: (f?.fm.canonical_title as string) ?? id, + pages: (f?.fm.page_count as number) ?? 0, + collection: (f?.fm.collection as string) ?? "uncategorized", + classification: (f?.fm.highest_classification as string) ?? "—", + summary: pickPitch(f?.fm as Record | undefined, summaryLang) ?? (f?.body ? summarize(f.body, summaryLang) : ""), + }; + }), + ); + const totalPages = docs.reduce((s, d) => s + d.pages, 0); + + return ( +
+ + + +
+
+
+ {locale === "en" ? "// The primary record" : "// O registro primário"} +
+

+ {locale === "en" ? "Documents" : "Documentos"} +

+

+ {locale === "en" + ? `${ids.length} declassified files · ${totalPages.toLocaleString("pt-BR")} pages · every memo, telegram and report.` + : `${ids.length} arquivos desclassificados · ${totalPages.toLocaleString("pt-BR")} páginas · cada memorando, telegrama e relatório.`} +

+
+ + +
+ +
+ ); +} diff --git a/web/app/globals.css b/web/app/globals.css index 6cab103..308dedf 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -24,6 +24,11 @@ html, body { min-height: 100vh; } +.font-display { + font-family: var(--font-display), "Fraunces", "Georgia", "Times New Roman", serif; + font-feature-settings: "ss01", "ss02"; +} + body { background-image: radial-gradient(ellipse 60% 50% at 92% 8%, rgba(0, 255, 156, 0.05) 0%, transparent 65%), diff --git a/web/app/jobs/[id]/page.tsx b/web/app/jobs/[id]/page.tsx index 1ab52e7..912791e 100644 --- a/web/app/jobs/[id]/page.tsx +++ b/web/app/jobs/[id]/page.tsx @@ -52,50 +52,16 @@ export default async function JobPage({ const job = rows[0]; if (!job) notFound(); - const detective = job.kind === "hypothesis_tournament" ? "holmes" - : job.kind === "contradiction_scan" ? "dupin" - : job.kind === "red_team_review" ? "schneier" - : job.kind === "witness_analysis" ? "poirot" - : job.kind === "outlier_scan" ? "taleb" - : job.kind === "calibrate_hypothesis" ? "tetlock" - : job.kind === "case_report" ? "case-writer" - : "locard"; - const detectiveName = - detective === "holmes" ? "Sherlock Holmes" : - detective === "dupin" ? "C. Auguste Dupin" : - detective === "schneier" ? "Bruce Schneier" : - detective === "poirot" ? "Hercule Poirot" : - detective === "taleb" ? "Nassim Taleb" : - detective === "tetlock" ? "Philip Tetlock" : - detective === "case-writer" ? "Dr. Watson (Case-Writer)" : - "Edmond Locard"; + // Reader-facing: no detective surfacing. Every kind reads as "case + // investigation in progress" with neutral copy. Detective identities + // remain internal in the runtime (audit log) but never reach the UI. + const detectiveName = "Investigation in progress"; const detectiveSubtitle = - detective === "holmes" ? "Hypothesis tournament · rival hypotheses with Bayesian update" : - detective === "dupin" ? "Contradiction scan · pairs of chunks in irreconcilable tension" : - detective === "schneier" ? "Red-team review · hidden assumptions, failure modes, alt explanations" : - detective === "poirot" ? "Witness analysis · credibility / access / bias / corroboration" : - detective === "taleb" ? "Outlier scan · chunks that violate the dominant model" : - detective === "tetlock" ? "Calibration · honest Bayesian update with action recommendation" : - detective === "case-writer" ? "Case narrative · five-act Watson assembly of all bureau artefacts" : - "Evidence chain · verbatim quotes with chain of custody (Locard)"; - const detectiveTone = - detective === "holmes" ? "text-[#7fdbff]" : - detective === "dupin" ? "text-[#ff8a4d]" : - detective === "schneier" ? "text-[#ff3344]" : - detective === "poirot" ? "text-[#9b5de5]" : - detective === "taleb" ? "text-[#ffd23f]" : - detective === "tetlock" ? "text-[#26d4cc]" : - detective === "case-writer" ? "text-[#e0c080]" : - "text-[#06d6a0]"; - const detectiveBg = - detective === "holmes" ? "from-[rgba(127,219,255,0.08)]" : - detective === "dupin" ? "from-[rgba(255,138,77,0.08)]" : - detective === "schneier" ? "from-[rgba(255,51,68,0.08)]" : - detective === "poirot" ? "from-[rgba(155,93,229,0.08)]" : - detective === "taleb" ? "from-[rgba(255,210,63,0.08)]" : - detective === "tetlock" ? "from-[rgba(38,212,204,0.08)]" : - detective === "case-writer" ? "from-[rgba(224,192,128,0.08)]" : - "from-[rgba(6,214,160,0.08)]"; + job.kind === "case_report" ? "Assembling a narrative case file from the primary record" : + job.kind === "evidence_chain" ? "Pulling verbatim citations from the document" : + "Reading the archive for this case"; + const detectiveTone = "text-[#e0c080]"; + const detectiveBg = "from-[rgba(224,192,128,0.06)]"; const payload = (job.payload ?? {}) as Record; const question = (payload.question ?? payload.topic ?? payload.hypothesis_id ?? payload.person_id) as string | undefined; const questionLabel = @@ -127,7 +93,7 @@ export default async function JobPage({

{detectiveSubtitle}

- {detective} + {job.kind} diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 0626963..a125c51 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,25 +1,98 @@ import type { Metadata } from "next"; -import { JetBrains_Mono, Inter } from "next/font/google"; +import { JetBrains_Mono, Inter, Fraunces } from "next/font/google"; import "./globals.css"; import { CommandPalette } from "@/components/command-palette"; import { LocaleToggle, getLocale } from "@/components/locale-toggle"; const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); const mono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono" }); +const fraunces = Fraunces({ + subsets: ["latin"], + variable: "--font-display", + weight: ["400", "600", "700", "900"], + axes: ["SOFT", "WONK"], +}); + +const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://disclosure.top"; export const metadata: Metadata = { - title: "The Disclosure Bureau", + metadataBase: new URL(SITE_URL), + title: { + default: "The Disclosure Bureau — UAP/UFO desclassificado", + template: "%s · The Disclosure Bureau", + }, description: - "Investigative case folder of declassified UAP/UFO documents, " + - "worked by 8 AI detectives (Holmes · Locard · Dupin · Schneier · " + - "Poirot · Taleb · Tetlock · Case-Writer).", + "122 documentos desclassificados do Departamento de Guerra dos EUA sobre UAP/UFO. " + + "Pilotos, oficiais e físicos relatam o que viram. Avistamentos, testemunhas, " + + "objetos catalogados — arquivos abertos da divulgação.", + keywords: [ + "UAP", "UFO", "ovni", "desclassificado", "war.gov", "Pentagon", + "Kenneth Arnold", "Mantell", "green fireballs", "Sandia", + "Project Blue Book", "Robertson Panel", "AATIP", + "documentos desclassificados", "avistamento", "testemunha", + "disclosure", "divulgação UFO", + ], + authors: [{ name: "The Disclosure Bureau" }], + openGraph: { + type: "website", + siteName: "The Disclosure Bureau", + title: "The Disclosure Bureau — UAP/UFO desclassificado", + description: + "122 documentos desclassificados. Pilotos, oficiais, físicos relatam o que viram.", + locale: "pt_BR", + alternateLocale: ["en_US"], + url: SITE_URL, + }, + twitter: { + card: "summary_large_image", + title: "The Disclosure Bureau", + description: "Arquivos UAP/UFO desclassificados, narrados a partir do registro público.", + }, + alternates: { + canonical: "/", + languages: { "pt-BR": "/", "en-US": "/" }, + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + "max-image-preview": "large", + "max-snippet": -1, + }, + }, + other: { + // GEO (Generative Engine Optimization) — explicit primary-source statement + // so retrieval-augmented assistants understand what the site is. + "ai:purpose": + "Public, citation-linked archive of declassified UAP/UFO documents from the US Department of War. Each case file is grounded in primary-source memos with verbatim quotes and bbox-cropped imagery.", + "ai:license": "Documents are US Government works in the public domain; site narrative © Disclosure Bureau, CC-BY 4.0.", + }, }; export default async function RootLayout({ children }: { children: React.ReactNode }) { const locale = await getLocale(); return ( - + + {/* JSON-LD: organization-level schema. Per-page Article/Event schemas + are added in their own routes. */} +