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
+105
View File
@@ -0,0 +1,105 @@
import { expect, test } from '@playwright/test';
import { login as loginAs, logout } from './helpers';
// Asking a colleague to check something, end to end and without the model:
// the author asks, the document is marked as unsettled for everyone, the
// colleague answers, and the mark goes away with the answer.
test.setTimeout(120_000);
test('a question travels with the document until the colleague answers it', async ({ page }) => {
await loginAs(page, 'pablo@pablan.dev');
// A published document everyone may read, so Max is a candidate reviewer.
const created = await (
await page.request.post('/api/documents', {
data: { title: 'Notfallnummern Halle 1', visibility: 'public' }
})
).json();
const documentId = created.id;
await page.request.patch(`/api/documents/${documentId}`, {
data: { content_md: '## Notfall\n\nDie Nummer der Instandhaltung ist die 4455.' }
});
await page.request.post(`/api/documents/${documentId}/publish`);
await page.goto(`/documents/${documentId}`);
await page.locator('body[data-hydrated]').waitFor();
await page.getByTestId('document-menu').click();
await page.getByTestId('document-ask-review').click();
await page.getByTestId('reviewer-select').selectOption({ label: 'Max' });
await page.getByTestId('review-question').fill('Stimmt die 4455 noch?');
await page.getByTestId('review-ask-send').click();
// The question is on the document, and it says who is waiting on whom.
await expect(page.getByTestId('open-reviews')).toContainText('Stimmt die 4455 noch?');
await expect(page.getByTestId('open-reviews')).toContainText('Max');
// The author cannot answer their own question, only drop it.
await expect(page.getByTestId('review-confirm')).toHaveCount(0);
await expect(page.getByTestId('review-close')).toBeVisible();
await logout(page);
// Max was asked: it is in his queue, he may edit it, and he answers.
await loginAs(page, 'max@pablan.dev');
await page.goto('/documents?review=1');
await page.locator('body[data-hydrated]').waitFor();
await expect(page.getByTestId('document-list')).toContainText('Notfallnummern Halle 1');
await expect(page.getByTestId('open-review-badge').first()).toBeVisible();
await page.goto(`/documents/${documentId}`);
await page.locator('body[data-hydrated]').waitFor();
await expect(page.getByTestId('document-edit')).toBeVisible();
await page.getByTestId('review-confirm').click();
// Answered: no open question left, and the record says who checked it.
await expect(page.getByTestId('open-reviews')).toHaveCount(0);
await expect(page.getByTestId('answered-reviews')).toContainText('Max');
// The grant went with the answer — Max may read it, not change it.
await expect(page.getByTestId('document-edit')).toHaveCount(0);
await logout(page);
await loginAs(page, 'pablo@pablan.dev');
await page.request.delete(`/api/documents/${documentId}`);
await page.request.post('/api/auth/logout');
});
test('answering on a draft hands it back instead of dropping you on a 404', async ({ page }) => {
await loginAs(page, 'pablo@pablan.dev');
const created = await (
await page.request.post('/api/documents', {
data: { title: 'Entwurf zum Gegenlesen', visibility: 'public' }
})
).json();
const documentId = created.id;
await page.request.patch(`/api/documents/${documentId}`, {
data: { content_md: '## Stand\n\nNoch nicht fertig.' }
});
const max = (
await (await page.request.get(`/api/documents/${documentId}/reviewers`)).json()
).find((candidate: { name: string }) => candidate.name === 'Max');
await page.request.post(`/api/documents/${documentId}/reviews`, {
data: { reviewer_id: max.id, question: 'Passt der Stand so?' }
});
await logout(page);
// Max only sees the draft because he was asked, and the page says so.
await loginAs(page, 'max@pablan.dev');
await page.goto(`/documents/${documentId}`);
await page.locator('body[data-hydrated]').waitFor();
await expect(page.getByTestId('open-reviews')).toContainText('Passt der Stand so?');
// He can see who may read it, and change neither that nor the publish state.
await page.getByTestId('access-chip').click();
await expect(page.getByTestId('access-controls')).toBeVisible();
await expect(page.getByTestId('visibility-select')).toHaveCount(0);
await expect(page.getByTestId('share-save')).toHaveCount(0);
await page.keyboard.press('Escape');
await expect(page.getByTestId('publish-button')).toHaveCount(0);
// Answering ends that access, so the page thanks him instead of 404ing.
await page.getByTestId('review-confirm').click();
await expect(page.getByTestId('review-done')).toBeVisible();
await logout(page);
await loginAs(page, 'pablo@pablan.dev');
await page.request.delete(`/api/documents/${documentId}`);
await page.request.post('/api/auth/logout');
});