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 09:21:37 +02:00
co-authored by Claude Opus 5
parent 68d3a43191
commit 784b76baf7
346 changed files with 43430 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
import { expect, test, type Page } from '@playwright/test';
import { login as loginAs } from './helpers';
// Writing-first capture as a USER sees it: pick a documentation type, write in
// the editor, get a refined version of the section at the cursor, accept it,
// read the diff before saving, and publish. The refine step drives the real
// LLM, so it can take a few seconds.
test.describe.configure({ mode: 'serial' });
test.setTimeout(180_000);
// Template names are matched in German on purpose: a template is product
// CONTENT in the instance's own language (PABLAN_DEFAULT_LOCALE), not UI copy
// that follows the reader's language setting.
/** Draft documents this spec created, so cleanup removes only its own. */
let created: string[] = [];
async function newDraft(page: Page, templateText: string): Promise<string> {
await page.goto('/documents/new');
await page.locator('body[data-hydrated]').waitFor();
await page.getByTestId('capture-template').filter({ hasText: templateText }).first().click();
await expect(page).toHaveURL(/\/documents\/[0-9a-f-]{36}\/edit$/);
const id = page.url().split('/')[4];
created.push(id);
await page.locator('.cm-content').waitFor();
return id;
}
test.afterEach(async ({ page }) => {
for (const id of created) {
await page.request.delete(`/api/documents/${id}`);
}
created = [];
await page.request.post('/api/auth/logout');
});
test('picking a type opens the editor on the template skeleton', async ({ page }) => {
await loginAs(page, 'pablo@pablan.dev');
await newDraft(page, 'Über dich');
// The editor opens on the skeleton headings, not an empty box, and nothing
// else: the suggestion is a block inside the text that appears when one
// streams, so before any typing there is no widget and nothing to accept.
await expect(page.getByTestId('editor-source')).toContainText('##');
await expect(page.getByTestId('editor-suggestion')).toHaveCount(0);
await expect(page.getByTestId('editor-accept')).toHaveCount(0);
});
test('a typing pause yields a section suggestion that overwrites on accept', async ({ page }) => {
await loginAs(page, 'pablo@pablan.dev');
await newDraft(page, 'Über dich');
// Write rough notes under the first heading.
await page.locator('.cm-content').click();
await page.keyboard.press('Control+Home');
await page.keyboard.press('ArrowDown');
await page.keyboard.type(
'also der neue kollege wartet die cnc maschinen und ist ansprechpartner für den einkauf.'
);
// After the pause the model streams a refined version of the section.
await expect(page.getByTestId('editor-accept')).toBeVisible({ timeout: 40_000 });
await page.getByTestId('editor-accept').click();
// Accepting consumes the suggestion (the pane returns to its empty state)
// and replaces the rough notes — the lowercase draft phrasing is gone.
await expect(page.getByTestId('editor-accept')).toHaveCount(0);
await expect(page.getByTestId('editor-source')).not.toContainText('also der neue kollege');
// Saving shows what changed first, VSCode-style, and asks again.
await page.getByTestId('editor-open-save').click();
await expect(page.getByTestId('editor-diff')).toBeVisible();
});
test('a draft is published from the editor in one step', async ({ page }) => {
await loginAs(page, 'pablo@pablan.dev');
const id = await newDraft(page, 'Notiz');
await page.locator('.cm-content').click();
await page.keyboard.type('\nEin kurzer, brauchbarer Inhalt zum Veröffentlichen.');
await page.getByTestId('editor-open-save').click();
await page.getByTestId('editor-publish').click();
// Published, and the author is offered a colleague to check it.
await expect(page.getByTestId('capture-success')).toBeVisible();
await page.getByTestId('success-view').click();
await expect(page).toHaveURL(new RegExp(`/documents/${id}$`));
await expect(page.getByTestId('document-status')).toHaveAttribute('data-status', 'published');
});