Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor whose sections an LLM refines while you write, and RAG question answering over the documents that result. FastAPI + Postgres/pgvector on the back, SvelteKit on the front, everything OpenAI-compatible and self-hostable. Squashed into a single commit; the development history stays local. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
This commit is contained in:
co-authored by
Claude Opus 5
parent
68d3a43191
commit
784b76baf7
@@ -0,0 +1,179 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1240 880" width="1240" height="880" role="img" aria-label="Pablan components">
|
||||
<title>Pablan components</title>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg: #fbfaf9;
|
||||
--surface: #ffffff;
|
||||
--sunken: #f2f0ed;
|
||||
--ink: #1a1815;
|
||||
--muted: #63605a;
|
||||
--line: #c5bfb6;
|
||||
--line-soft: #e5e1db;
|
||||
--accent: #b8770a;
|
||||
--secondary: #b04a00;
|
||||
--note: #faf3e6;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0f0e0d;
|
||||
--surface: #1a1817;
|
||||
--sunken: #232120;
|
||||
--ink: #efece8;
|
||||
--muted: #a49d94;
|
||||
--line: #4a443d;
|
||||
--line-soft: #332f2b;
|
||||
--accent: #e6b422;
|
||||
--secondary: #e08030;
|
||||
--note: #241f16;
|
||||
}
|
||||
}
|
||||
text { font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; fill: var(--ink); }
|
||||
.t-title { font-size: 15px; font-weight: 650; }
|
||||
.t-sub { font-size: 11.5px; fill: var(--muted); }
|
||||
.t-node { font-size: 12.5px; font-weight: 600; }
|
||||
.t-body { font-size: 11.5px; }
|
||||
.t-muted { font-size: 11px; fill: var(--muted); }
|
||||
.t-edge { font-size: 10.5px; fill: var(--muted); }
|
||||
.t-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; }
|
||||
.box { fill: var(--surface); stroke: var(--line); stroke-width: 1.2; }
|
||||
.box-sunken { fill: var(--sunken); stroke: var(--line-soft); stroke-width: 1; }
|
||||
.box-accent { fill: var(--surface); stroke: var(--accent); stroke-width: 1.6; }
|
||||
.note { fill: var(--note); stroke: var(--line-soft); stroke-width: 1; }
|
||||
.group { fill: none; stroke: var(--line-soft); stroke-width: 1.2;
|
||||
stroke-dasharray: 5 4; }
|
||||
.lifeline { stroke: var(--line); stroke-width: 1; stroke-dasharray: 4 4; }
|
||||
.edge { stroke: var(--line); stroke-width: 1.3; fill: none; }
|
||||
.edge-accent { stroke: var(--secondary); stroke-width: 1.5; fill: none; }
|
||||
.edge-soft { stroke: var(--line); stroke-width: 1.1; fill: none;
|
||||
stroke-dasharray: 5 4; }
|
||||
.edge-soft-accent { stroke: var(--secondary); stroke-width: 1.3; fill: none;
|
||||
stroke-dasharray: 5 4; }
|
||||
.planned { stroke-dasharray: 6 4; opacity: 0.62; }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5"
|
||||
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 1 L 9 5 L 0 9 z" fill="var(--line)"/>
|
||||
</marker>
|
||||
<marker id="arrow-accent" viewBox="0 0 10 10" refX="9" refY="5"
|
||||
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 1 L 9 5 L 0 9 z" fill="var(--secondary)"/>
|
||||
</marker>
|
||||
<marker id="arrow-open" viewBox="0 0 10 10" refX="9" refY="5"
|
||||
markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||
<path d="M 0 1 L 9 5 L 0 9" fill="none" stroke="var(--line)"
|
||||
stroke-width="1.3"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="1240" height="880" fill="var(--bg)"/>
|
||||
<text x="30" y="32" class="t-title" text-anchor="start">Components</text>
|
||||
<text x="30" y="52" class="t-sub" text-anchor="start">One deployment: browser, one proxy, two app processes, one database, and whatever speaks the OpenAI API.</text>
|
||||
<rect x="40" y="80" width="170" height="38" rx="7" class="box" />
|
||||
<text x="51" y="99" class="t-node" text-anchor="start">Browser</text>
|
||||
<rect x="40" y="148" width="1160" height="46" rx="7" class="box" />
|
||||
<text x="51" y="167" class="t-node" text-anchor="start">Reverse proxy: Caddy</text>
|
||||
<text x="51" y="182" class="t-muted" text-anchor="start">/api/* to the backend, everything else to the frontend; TLS for PABLAN_DOMAIN</text>
|
||||
<line x1="125" y1="118" x2="125" y2="146" class="edge" marker-end="url(#arrow)"/>
|
||||
<line x1="240" y1="194" x2="240" y2="240" class="edge" marker-end="url(#arrow)"/>
|
||||
<line x1="860" y1="194" x2="860" y2="240" class="edge" marker-end="url(#arrow)"/>
|
||||
<rect x="40" y="250" width="410" height="330" rx="10" class="group" />
|
||||
<rect x="54" y="239" width="284.4" height="22" rx="11" class="box-sunken" />
|
||||
<text x="196.2" y="254" class="t-muted" text-anchor="middle">SvelteKit frontend (no DB, no auth logic)</text>
|
||||
<rect x="56" y="272" width="378" height="48" rx="7" class="box" />
|
||||
<text x="67" y="291" class="t-node" text-anchor="start">hooks.server.ts</text>
|
||||
<text x="67" y="306" class="t-muted" text-anchor="start">sequence(auth gate, paraglide i18n); cookie passthrough</text>
|
||||
<rect x="56" y="332" width="378" height="48" rx="7" class="box" />
|
||||
<text x="67" y="351" class="t-node" text-anchor="start">typed API client</text>
|
||||
<text x="67" y="366" class="t-muted" text-anchor="start">openapi-fetch over generated types, never hand-written</text>
|
||||
<rect x="56" y="392" width="183" height="62" rx="7" class="box" />
|
||||
<text x="67" y="411" class="t-node" text-anchor="start">chat</text>
|
||||
<text x="67" y="426" class="t-muted" text-anchor="start">/chat, /chat/[id],</text>
|
||||
<text x="67" y="439" class="t-muted" text-anchor="start">one ChatView, SSE</text>
|
||||
<rect x="251" y="392" width="183" height="62" rx="7" class="box" />
|
||||
<text x="262" y="411" class="t-node" text-anchor="start">writing editor</text>
|
||||
<text x="262" y="426" class="t-muted" text-anchor="start">documents/[id]/edit,</text>
|
||||
<text x="262" y="439" class="t-muted" text-anchor="start">CodeMirror + refine SSE</text>
|
||||
<rect x="56" y="466" width="183" height="62" rx="7" class="box" />
|
||||
<text x="67" y="485" class="t-node" text-anchor="start">documents, people,</text>
|
||||
<text x="67" y="500" class="t-muted" text-anchor="start">admin, account</text>
|
||||
<text x="67" y="513" class="t-muted" text-anchor="start"></text>
|
||||
<rect x="251" y="466" width="183" height="62" rx="7" class="box" />
|
||||
<text x="262" y="485" class="t-node" text-anchor="start">paraglide messages</text>
|
||||
<text x="262" y="500" class="t-muted" text-anchor="start">de source, en in sync;</text>
|
||||
<text x="262" y="513" class="t-muted" text-anchor="start">app.css design tokens</text>
|
||||
<rect x="520" y="250" width="690" height="350" rx="10" class="group" />
|
||||
<rect x="534" y="239" width="444.4" height="22" rx="11" class="box-sunken" />
|
||||
<text x="756.2" y="254" class="t-muted" text-anchor="middle">FastAPI backend (single worker: queue and metrics are per process)</text>
|
||||
<rect x="536" y="272" width="658" height="48" rx="7" class="box" />
|
||||
<text x="547" y="291" class="t-node" text-anchor="start">api/ routers</text>
|
||||
<text x="547" y="306" class="t-muted" text-anchor="start">auth, account, people, documents, templates, admin, conversations (SSE), authoring (SSE)</text>
|
||||
<rect x="536" y="334" width="160" height="58" rx="7" class="box" />
|
||||
<text x="547" y="353" class="t-node" text-anchor="start">auth/</text>
|
||||
<text x="547" y="368" class="t-muted" text-anchor="start">argon2,</text>
|
||||
<text x="547" y="381" class="t-muted" text-anchor="start">auth_sessions</text>
|
||||
<rect x="708" y="334" width="160" height="58" rx="7" class="box" />
|
||||
<text x="719" y="353" class="t-node" text-anchor="start">modes/</text>
|
||||
<text x="719" y="368" class="t-muted" text-anchor="start">registry, query;</text>
|
||||
<text x="719" y="381" class="t-muted" text-anchor="start">yields ModeEvents</text>
|
||||
<rect x="880" y="334" width="160" height="58" rx="7" class="box" />
|
||||
<text x="891" y="353" class="t-node" text-anchor="start">authoring/</text>
|
||||
<text x="891" y="368" class="t-muted" text-anchor="start">template schema,</text>
|
||||
<text x="891" y="381" class="t-muted" text-anchor="start">section refinement</text>
|
||||
<rect x="1044" y="334" width="150" height="58" rx="7" class="box" />
|
||||
<text x="1055" y="353" class="t-node" text-anchor="start">ingestion/</text>
|
||||
<text x="1055" y="368" class="t-muted" text-anchor="start">jobs queue</text>
|
||||
<text x="1055" y="381" class="t-muted" text-anchor="start">+ handlers</text>
|
||||
<rect x="536" y="406" width="658" height="52" rx="7" class="box" />
|
||||
<text x="547" y="425" class="t-node" text-anchor="start">rag/</text>
|
||||
<text x="547" y="440" class="t-muted" text-anchor="start">chunking, indexing, permissions, retrieval (hybrid + text-only + similarity)</text>
|
||||
<rect x="536" y="470" width="494" height="52" rx="7" class="box" />
|
||||
<text x="547" y="489" class="t-node" text-anchor="start">llm/client.py</text>
|
||||
<text x="547" y="504" class="t-muted" text-anchor="start">the ONLY caller of an endpoint: chat_stream, chat_json, embed, probe</text>
|
||||
<rect x="1042" y="470" width="152" height="52" rx="7" class="box" />
|
||||
<text x="1053" y="489" class="t-node" text-anchor="start">llm/overrides.py</text>
|
||||
<text x="1053" y="504" class="t-muted" text-anchor="start">env bootstrap,</text>
|
||||
<text x="1053" y="517" class="t-muted" text-anchor="start">then the DB wins</text>
|
||||
<rect x="536" y="534" width="323" height="48" rx="7" class="box" />
|
||||
<text x="547" y="553" class="t-node" text-anchor="start">config, log, metrics</text>
|
||||
<text x="547" y="568" class="t-muted" text-anchor="start">content never reaches a log line</text>
|
||||
<rect x="871" y="534" width="323" height="48" rx="7" class="box planned" />
|
||||
<text x="882" y="553" class="t-node" text-anchor="start">ee_hooks.py</text>
|
||||
<text x="882" y="568" class="t-muted" text-anchor="start">optional pablan_ee import; core never imports ee/</text>
|
||||
<line x1="616" y1="320" x2="616" y2="332" class="edge-soft" marker-end="url(#arrow)"/>
|
||||
<line x1="783" y1="320" x2="783" y2="332" class="edge-soft" marker-end="url(#arrow)"/>
|
||||
<line x1="950" y1="320" x2="950" y2="332" class="edge-soft" marker-end="url(#arrow)"/>
|
||||
<line x1="1111" y1="320" x2="1111" y2="332" class="edge-soft" marker-end="url(#arrow)"/>
|
||||
<line x1="783" y1="392" x2="783" y2="404" class="edge" marker-end="url(#arrow)"/>
|
||||
<line x1="950" y1="392" x2="950" y2="404" class="edge" marker-end="url(#arrow)"/>
|
||||
<line x1="1111" y1="392" x2="1111" y2="404" class="edge" marker-end="url(#arrow)"/>
|
||||
<line x1="616" y1="458" x2="616" y2="468" class="edge" marker-end="url(#arrow)"/>
|
||||
<line x1="950" y1="458" x2="950" y2="468" class="edge" marker-end="url(#arrow)"/>
|
||||
<path d="M 450 296 H 520" class="edge-accent" marker-end="url(#arrow)"/>
|
||||
<text x="485" y="288" class="t-edge" text-anchor="middle">/api/auth/me</text>
|
||||
<path d="M 450 356 H 520" class="edge-accent" marker-end="url(#arrow)"/>
|
||||
<text x="485" y="348" class="t-edge" text-anchor="middle">same origin</text>
|
||||
<rect x="40" y="760" width="560" height="70" rx="7" class="box" />
|
||||
<text x="51" y="779" class="t-node" text-anchor="start">PostgreSQL + pgvector</text>
|
||||
<text x="51" y="794" class="t-muted" text-anchor="start">documents (Markdown, the source of truth), chunks (embedding + tsvector),</text>
|
||||
<text x="51" y="807" class="t-muted" text-anchor="start">auth_sessions, conversations, jobs, llm_settings, templates</text>
|
||||
<rect x="640" y="760" width="560" height="70" rx="7" class="box" />
|
||||
<text x="651" y="779" class="t-node" text-anchor="start">OpenAI-compatible endpoints</text>
|
||||
<text x="651" y="794" class="t-muted" text-anchor="start">three roles, each its own base_url + key + model:</text>
|
||||
<text x="651" y="807" class="t-muted" text-anchor="start">chat, utility, embedding (bge-m3). llama.cpp locally, any cloud API in production</text>
|
||||
<line x1="560" y1="600" x2="560" y2="758" class="edge" marker-end="url(#arrow)"/>
|
||||
<text x="566" y="668" class="t-edge" text-anchor="start">every table</text>
|
||||
<line x1="1185" y1="600" x2="1185" y2="758" class="edge" marker-end="url(#arrow)"/>
|
||||
<text x="1179" y="745" class="t-edge" text-anchor="end">llm/client.py only</text>
|
||||
<rect x="660" y="630" width="240" height="78" rx="7" class="box" />
|
||||
<text x="671" y="649" class="t-node" text-anchor="start">templates/</text>
|
||||
<text x="671" y="664" class="t-muted" text-anchor="start">blueprint catalog, de + en;</text>
|
||||
<text x="671" y="677" class="t-muted" text-anchor="start">inert until an admin adds one</text>
|
||||
<rect x="920" y="630" width="240" height="78" rx="7" class="box" />
|
||||
<text x="931" y="649" class="t-node" text-anchor="start">help/</text>
|
||||
<text x="931" y="664" class="t-muted" text-anchor="start">built-in help pages, German;</text>
|
||||
<text x="931" y="677" class="t-muted" text-anchor="start">re-imported on every start</text>
|
||||
<path d="M 780 630 V 604" class="edge-soft" marker-end="url(#arrow)"/>
|
||||
<path d="M 1040 630 V 604" class="edge-soft" marker-end="url(#arrow)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
Reference in New Issue
Block a user