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:
ProfessorNova
2026-09-04 08:36:17 +02:00
co-authored by Claude Opus 5
commit 97dbff309c
346 changed files with 43430 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 810" width="860" height="810" role="img" aria-label="Background jobs: one Postgres table, no broker">
<title>Background jobs: one Postgres table, no broker</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="860" height="810" fill="var(--bg)"/>
<text x="28" y="30" class="t-title" text-anchor="start">Background jobs: one Postgres table, no broker</text>
<text x="28" y="50" class="t-sub" text-anchor="start">FOR UPDATE SKIP LOCKED, and the handler runs inside the claim transaction.</text>
<rect x="114" y="74" width="96" height="30" rx="8" class="box" />
<text x="162" y="93" class="t-node" text-anchor="middle">Queue loop</text>
<line x1="162" y1="104" x2="162" y2="792" class="lifeline"/>
<rect x="382" y="74" width="96" height="30" rx="8" class="box" />
<text x="430" y="93" class="t-node" text-anchor="middle">Postgres</text>
<line x1="430" y1="104" x2="430" y2="792" class="lifeline"/>
<rect x="648.7" y="74" width="98.6" height="30" rx="8" class="box" />
<text x="698" y="93" class="t-node" text-anchor="middle">Job handler</text>
<line x1="698" y1="104" x2="698" y2="792" class="lifeline"/>
<text x="296" y="158" class="t-edge" text-anchor="middle">BEGIN</text>
<line x1="162" y1="175" x2="430" y2="175" class="edge" marker-end="url(#arrow)"/>
<text x="296" y="197" class="t-edge" text-anchor="middle">SELECT job WHERE status=pending AND run_after&lt;=now()</text>
<text x="296" y="210" class="t-edge" text-anchor="middle">ORDER BY run_after LIMIT 1 FOR UPDATE SKIP LOCKED</text>
<line x1="162" y1="227" x2="430" y2="227" class="edge" marker-end="url(#arrow)"/>
<text x="296" y="275" class="t-edge" text-anchor="middle">ROLLBACK</text>
<line x1="162" y1="292" x2="430" y2="292" class="edge" marker-end="url(#arrow)"/>
<text x="208" y="316" class="t-edge" text-anchor="start">wait one poll interval</text>
<path d="M 162.0 319 h 34 v 22 h -34" class="edge" marker-end="url(#arrow)"/>
<text x="430" y="403" class="t-edge" text-anchor="middle">handler(db, job), INSIDE the open claim transaction</text>
<line x1="162" y1="420" x2="698" y2="420" class="edge-accent" marker-end="url(#arrow-accent)"/>
<text x="296" y="468" class="t-edge" text-anchor="middle">status=done, attempts+1, COMMIT</text>
<text x="296" y="481" class="t-edge" text-anchor="middle">(the handler&#x27;s writes commit atomically with the job)</text>
<line x1="162" y1="498" x2="430" y2="498" class="edge" marker-end="url(#arrow)"/>
<text x="296" y="562" class="t-edge" text-anchor="middle">ROLLBACK (the handler&#x27;s writes go too)</text>
<line x1="162" y1="579" x2="430" y2="579" class="edge" marker-end="url(#arrow)"/>
<text x="296" y="601" class="t-edge" text-anchor="middle">follow-up tx: attempts+1, last_error (sanitized),</text>
<text x="296" y="614" class="t-edge" text-anchor="middle">run_after = now + 30s * 2^(attempts-1)</text>
<line x1="162" y1="631" x2="430" y2="631" class="edge" marker-end="url(#arrow)"/>
<rect x="456" y="647" width="222.8" height="27" rx="6" class="note" />
<text x="469" y="664" class="t-body" text-anchor="start">After 5 attempts: status=failed.</text>
<rect x="214.05" y="734" width="431.9" height="40" rx="6" class="note" />
<text x="227.05" y="751" class="t-body" text-anchor="start">Crash mid-job: the open transaction aborts, the row lock releases,</text>
<text x="227.05" y="764" class="t-body" text-anchor="start">and the job is still pending and claimable after the restart.</text>
<rect x="28" y="243" width="804" height="120" rx="8" class="group" />
<rect x="28" y="243" width="38.45" height="20" rx="6" class="box-sunken" />
<text x="47.225" y="257" class="t-muted" text-anchor="middle">alt</text>
<text x="78.45" y="257" class="t-muted" text-anchor="start">no claimable job</text>
<rect x="28" y="436" width="804" height="86" rx="8" class="group" />
<rect x="28" y="436" width="38.45" height="20" rx="6" class="box-sunken" />
<text x="47.225" y="450" class="t-muted" text-anchor="middle">alt</text>
<text x="78.45" y="450" class="t-muted" text-anchor="start">handler succeeds</text>
<rect x="28" y="530" width="804" height="164" rx="8" class="group" />
<rect x="28" y="530" width="44.6" height="20" rx="6" class="box-sunken" />
<text x="50.3" y="544" class="t-muted" text-anchor="middle">else</text>
<text x="84.6" y="544" class="t-muted" text-anchor="start">handler raises</text>
<rect x="28" y="371" width="804" height="339" rx="8" class="group" />
<rect x="28" y="371" width="44.6" height="20" rx="6" class="box-sunken" />
<text x="50.3" y="385" class="t-muted" text-anchor="middle">else</text>
<text x="84.6" y="385" class="t-muted" text-anchor="start">job claimed, row lock held</text>
<rect x="28" y="126" width="804" height="600" rx="8" class="group" />
<rect x="28" y="126" width="44.6" height="20" rx="6" class="box-sunken" />
<text x="50.3" y="140" class="t-muted" text-anchor="middle">loop</text>
<text x="84.6" y="140" class="t-muted" text-anchor="start">every poll interval, default 1s</text>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB