disclosure-bureau/web/components/doc-bureau-panel.tsx
Luiz Gustavo ab4fe2a334
Some checks failed
CI / Retrieval — golden set (Recall@5 + MRR) (push) Waiting to run
CI / Web — typecheck + lint + build (push) Failing after 39s
CI / Scripts — Python smoke (push) Failing after 4s
CI / Web — npm audit (push) Has been cancelled
W5.1: enthusiast pivot — strip detective surfacing, magazine homepage
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 <EntityListPage> 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) <noreply@anthropic.com>
2026-05-24 14:09:46 -03:00

74 lines
2.7 KiB
TypeScript

/**
* DocCasePanel — links from a document page to any narrative case files
* that reference it.
*
* No detective surfacing. No evidence/hypothesis/contradiction IDs. Just
* the cases that drew on this document, presented as story cards.
*/
import Link from "next/link";
interface ReportRow { slug: string; topic: string; topic_pt_br: string | null; opening: string }
export async function DocBureauPanel({ docId }: { docId: string }) {
const { readdir, readFile } = await import("node:fs/promises");
const path = await import("node:path");
const dir = path.join(process.env.CASE_ROOT || "/data/ufo/case", "reports");
let reports: ReportRow[] = [];
try {
const files = await readdir(dir);
for (const f of files) {
if (!f.endsWith(".md")) continue;
const md = await readFile(path.join(dir, f), "utf-8");
if (!md.includes(docId)) continue;
const topicMatch = md.match(/topic:\s*"([^"]+)"/);
const topicPtMatch = md.match(/topic_pt_br:\s*"([^"]+)"/);
const bodyMatch = md.match(/^---[\s\S]+?\n---\n([\s\S]+)$/);
const body = bodyMatch?.[1] ?? "";
// First non-heading paragraph as opening hook.
const opening = body
.split("\n")
.find((l) => l.trim().length > 0 && !l.startsWith("#") && !l.startsWith(">") && !l.startsWith("|") && !l.startsWith("---"))
?.slice(0, 240) ?? "";
reports.push({
slug: f.replace(/\.md$/, ""),
topic: topicMatch?.[1] ?? f,
topic_pt_br: topicPtMatch?.[1] ?? null,
opening,
});
}
} catch { /* fine — no reports yet */ }
if (reports.length === 0) {
return null;
}
return (
<section className="mb-6 rounded-lg border border-[rgba(224,192,128,0.18)] bg-gradient-to-br from-[rgba(224,192,128,0.04)] to-transparent p-4">
<div className="text-[10px] font-mono text-[#e0c080] uppercase tracking-wider mb-3">
// Casos narrados que citam este documento
</div>
<div className="grid md:grid-cols-2 gap-3">
{reports.map((r) => (
<Link
key={r.slug}
href={`/c/${r.slug}`}
className="block rounded border border-[rgba(224,192,128,0.18)] bg-[#0d1220] p-3 hover:bg-[rgba(224,192,128,0.04)]"
>
<div className="text-[14px] text-[#e7ecf3] font-medium mb-1">
{r.topic_pt_br || r.topic}
</div>
{r.opening && (
<div className="text-[11px] text-[#9aa6b8] leading-relaxed line-clamp-3">
{r.opening}
</div>
)}
<div className="mt-2 text-[10px] font-mono text-[#5a6678]">
/c/{r.slug}
</div>
</Link>
))}
</div>
</section>
);
}