visuals: technology marks, area icons, client wall, two diagrams

Phase 7 (AUD-149), cheapest first.

- internal/brand: vendored Simple Icons path data (CC0) for 50 marks, looked up
  by the technology label the content already uses. Inline SVG, 16 px,
  currentColor, no request and no font. tools/gen_brand_icons.sh regenerates it.
- Six hand-drawn area icons replace the two-letter badges in the nav and on the
  landing cards; the evidence index gets a seventh.
- TechChips renders a mark where one exists and plain text where one does not,
  so a missing icon reads as a decision rather than a broken image.
- Client wall: 45 names as one typeface, one size, one grid of equal tiles, and
  the reason for no logo collage written on the page. No mark invented.
- Two diagrams, authored in-repo: homelab topology (platform-network.md) and
  the RAG pipeline (ai-rag-corpus.md), each referenced from the page it explains.
- Content images now carry intrinsic width/height, loading="lazy" and
  decoding="async" at render time; a site test fails the build if an image
  ships without them.
- Topbar is two rows at desktop: the area names plus marks no longer overlap the
  search form (Lighthouse target-size was failing at 1440 px).
- Favicon, so the browser stops requesting a missing /favicon.ico.

Satisfies: logos on every area page, area icons in nav and cards, two diagrams
referenced from their pages, images sized and lazy, Lighthouse >= 95 held.

(cherry picked from commit a473e4738a3751418c2097df2bcf0568fb6d0208)
This commit is contained in:
2026-10-28 15:50:08 +11:00
parent 59580b6e45
commit a76882de67
18 changed files with 914 additions and 51 deletions

View File

@@ -0,0 +1,106 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1120" height="580" viewBox="0 0 1120 580" role="img" aria-labelledby="ragTitle ragDesc" font-family="ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif">
<title id="ragTitle">Retrieval pipeline: ingest on top, four-node query path below</title>
<desc id="ragDesc">Ingest reads content and estate notes, excludes secrets, chunks and tags each block, embeds through the model gateway and upserts into two Qdrant collections on loopback. The query path classifies the question, routes it to an area, retrieves with a payload filter, composes an answer from the retrieved context only, and verifies that every citation resolves to a page.</desc>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 10 5 0 10z" fill="#6ee7c6"/>
</marker>
<marker id="arrowDim" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 10 5 0 10z" fill="#8b98a8"/>
</marker>
</defs>
<!-- ingest lane -->
<text x="30" y="42" font-size="14" font-weight="600" fill="#e6edf3">Ingest — one source of truth, one index</text>
<g fill="#12161b" stroke="#33404d" stroke-width="1.4">
<rect x="30" y="60" width="244" height="92" rx="9"/>
<rect x="302" y="60" width="244" height="92" rx="9"/>
<rect x="574" y="60" width="244" height="92" rx="9"/>
<rect x="846" y="60" width="244" height="92" rx="9"/>
</g>
<g fill="#e6edf3" font-size="13.5" font-weight="600">
<text x="48" y="86">Sources</text>
<text x="320" y="86">Normalise</text>
<text x="592" y="86">Chunk and tag</text>
<text x="864" y="86">Embed and upsert</text>
</g>
<g fill="#b3bcc8" font-size="12">
<text x="48" y="110">content/ Markdown</text>
<text x="48" y="129">estate notes</text>
<text x="48" y="148">platform guides</text>
<text x="320" y="110">front-matter parsed</text>
<text x="320" y="129">secrets and drafts</text>
<text x="320" y="148">excluded by rule</text>
<text x="592" y="110">blocks, not lines</text>
<text x="592" y="129">area · level · source</text>
<text x="592" y="148">title · tags</text>
<text x="864" y="110">model gateway only</text>
<text x="864" y="129">deterministic ids</text>
<text x="864" y="148">batched, resumable</text>
</g>
<g stroke="#6ee7c6" stroke-width="1.5" marker-end="url(#arrow)">
<line x1="277" y1="106" x2="298" y2="106"/>
<line x1="549" y1="106" x2="570" y2="106"/>
<line x1="821" y1="106" x2="842" y2="106"/>
</g>
<line x1="968" y1="152" x2="968" y2="184" stroke="#6ee7c6" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="30" y="186" width="1060" height="62" rx="9" fill="#12161b" stroke="#6ee7c6" stroke-width="1.2" stroke-dasharray="5 4"/>
<text x="50" y="210" font-size="13" font-weight="600" fill="#e6edf3">Qdrant on 127.0.0.1</text>
<text x="50" y="231" font-size="12" fill="#b3bcc8">resume_corpus · one point per chunk resume_graph · one point per node and edge HNSW and payloads on disk · within a 400 MiB cap</text>
<text x="50" y="272" font-size="11.5" fill="#8b98a8">A sync run re-embeds only the documents whose content, model or chunker version changed.</text>
<!-- query lane -->
<text x="30" y="316" font-size="14" font-weight="600" fill="#e6edf3">Query</text>
<text x="108" y="316" font-size="12" fill="#8b98a8">— four graph nodes. It grows past four only with a written reason.</text>
<line x1="776" y1="248" x2="776" y2="352" stroke="#8b98a8" stroke-width="1.3" stroke-dasharray="4 4" marker-end="url(#arrowDim)"/>
<text x="788" y="330" font-size="11" fill="#8b98a8">payload-filtered search</text>
<g fill="#12161b" stroke="#33404d" stroke-width="1.4">
<rect x="30" y="360" width="196" height="112" rx="9"/>
<rect x="246" y="360" width="196" height="112" rx="9"/>
<rect x="462" y="360" width="196" height="112" rx="9"/>
<rect x="678" y="360" width="196" height="112" rx="9"/>
<rect x="894" y="360" width="196" height="112" rx="9"/>
</g>
<g font-size="10.5" fill="#6ee7c6" font-family="ui-monospace, SFMono-Regular, Menlo, monospace">
<text x="46" y="382">CLASSIFY</text>
<text x="262" y="382">ROUTE</text>
<text x="478" y="382">RETRIEVE</text>
<text x="694" y="382">COMPOSE</text>
<text x="910" y="382">VERIFY</text>
</g>
<g fill="#e6edf3" font-size="13" font-weight="600">
<text x="46" y="406">Read the question</text>
<text x="262" y="406">Pick the areas</text>
<text x="478" y="406">Find the evidence</text>
<text x="694" y="406">Answer from it</text>
<text x="910" y="406">Check citations</text>
</g>
<g fill="#b3bcc8" font-size="11.5">
<text x="46" y="430">intent · injection</text>
<text x="46" y="448">off-topic · PII</text>
<text x="262" y="430">which pages can</text>
<text x="262" y="448">answer this</text>
<text x="478" y="430">vector + keyword</text>
<text x="478" y="448">ranked, top five</text>
<text x="694" y="430">context only</text>
<text x="694" y="448">no outside claims</text>
<text x="910" y="430">every claim maps</text>
<text x="910" y="448">to a real page</text>
</g>
<g stroke="#6ee7c6" stroke-width="1.5" marker-end="url(#arrow)">
<line x1="227" y1="416" x2="242" y2="416"/>
<line x1="443" y1="416" x2="458" y2="416"/>
<line x1="659" y1="416" x2="674" y2="416"/>
<line x1="875" y1="416" x2="890" y2="416"/>
</g>
<text x="30" y="500" font-size="12" fill="#b3bcc8">A refusal is a real outcome: an off-topic question, an injection attempt or a request over the cost cap stops at the first node with zero model spend.</text>
<text x="30" y="526" font-size="11.5" fill="#8b98a8">Ingest runs today. The query path is the Phase 5 design; until it ships, every page is pre-rendered and needs no model.</text>
<text x="30" y="550" font-size="11.5" fill="#8b98a8">The public AI has no tools, no filesystem, no network fetch and no memory between visitors — architectural, not a prompt instruction.</text>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB