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
+129
View File
@@ -0,0 +1,129 @@
import { expect, test } from '@playwright/test';
import { login, DEV_PASSWORD } from './helpers';
// Uses max@pablan.dev so a failure here cannot lock the other specs out of
// pablo@pablan.dev. The password is changed back at the end (zero residue).
const NEW_PASSWORD = 'ben-neues-geheimnis';
test('a user changes their own password and stays signed in', async ({ page }) => {
await login(page, 'max@pablan.dev');
await page.getByTestId('user-menu').click();
await page.getByTestId('change-password').click();
await page.getByLabel('Current password').fill(DEV_PASSWORD);
await page.getByLabel('New password', { exact: true }).fill(NEW_PASSWORD);
await page.getByLabel('Repeat new password').fill(NEW_PASSWORD);
await page.getByTestId('submit-password').click();
await expect(page.getByTestId('password-changed')).toBeVisible({ timeout: 15_000 });
// This session survived the change — no redirect to the login page.
await page.reload();
await page.locator('body[data-hydrated]').waitFor();
await expect(page).toHaveURL('/');
// The new password is the one that works now.
await page.request.post('/api/auth/logout');
const stale = await page.request.post('/api/auth/login', {
data: { email: 'max@pablan.dev', password: DEV_PASSWORD }
});
expect(stale.status()).toBe(401);
// Change it back, so the suite can run again.
await login(page, 'max@pablan.dev', NEW_PASSWORD);
await page.getByTestId('user-menu').click();
await page.getByTestId('change-password').click();
await page.getByLabel('Current password').fill(NEW_PASSWORD);
await page.getByLabel('New password', { exact: true }).fill(DEV_PASSWORD);
await page.getByLabel('Repeat new password').fill(DEV_PASSWORD);
await page.getByTestId('submit-password').click();
await expect(page.getByTestId('password-changed')).toBeVisible({ timeout: 15_000 });
await page.request.post('/api/auth/logout');
});
test('the wrong current password is rejected', async ({ page }) => {
await login(page, 'max@pablan.dev');
await page.getByTestId('user-menu').click();
await page.getByTestId('change-password').click();
await page.getByLabel('Current password').fill('definitely-wrong');
await page.getByLabel('New password', { exact: true }).fill(NEW_PASSWORD);
await page.getByLabel('Repeat new password').fill(NEW_PASSWORD);
await page.getByTestId('submit-password').click();
await expect(page.getByRole('alert')).toContainText('current password');
// The old password still works.
await page.request.post('/api/auth/logout');
const still = await page.request.post('/api/auth/login', {
data: { email: 'max@pablan.dev', password: DEV_PASSWORD }
});
expect(still.status()).toBe(200);
await page.request.post('/api/auth/logout');
});
test('the theme survives a reload without flashing the other one', async ({ page }) => {
await login(page, 'max@pablan.dev');
await page.getByTestId('user-menu').click();
await page.getByTestId('settings-dialog').waitFor();
await page.getByTestId('theme-switch').getByRole('button', { name: 'Light' }).click();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
// The boot script applies it before hydration, so it is already correct
// on the very first frame after a reload.
await page.reload();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
await page.locator('body[data-hydrated]').waitFor();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
// Back to following the OS, so the next spec starts clean.
await page.getByTestId('user-menu').click();
await page.getByTestId('theme-switch').getByRole('button', { name: 'System' }).click();
await expect(page.locator('html')).not.toHaveAttribute('data-theme', /.*/);
await page.request.post('/api/auth/logout');
});
test('the language switch flips the interface in place, with no reload', async ({ page }) => {
await login(page, 'max@pablan.dev');
await page.getByTestId('user-menu').click();
const dialog = page.getByTestId('settings-dialog');
const locales = page.getByTestId('locale-switch');
// Start from English, whatever a previous run left behind.
await locales.getByRole('button', { name: 'English' }).click();
await expect(dialog).toContainText('Settings');
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
// A marker on window survives a re-render but not a reload, and a typed
// value survives neither if the DOM is rebuilt: together they are the
// assertion that "no reload artifacts" actually holds.
await page.evaluate(() => ((window as unknown as Record<string, string>).__i18nMarker = 'alive'));
await page.getByTestId('change-password').click();
await page.getByLabel('Current password').fill('typed-before-switch');
await locales.getByRole('button', { name: 'Deutsch' }).click();
await expect(dialog).toContainText('Einstellungen');
await expect(dialog).toContainText('Passwort ändern');
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
expect(
await page.evaluate(() => (window as unknown as Record<string, string>).__i18nMarker)
).toBe('alive');
await expect(page.getByLabel('Aktuelles Passwort')).toHaveValue('typed-before-switch');
// The choice is on the account, not just in the tab.
await expect
.poll(async () => (await (await page.request.get('/api/auth/me')).json()).locale)
.toBe('de');
// Zero residue: back to following the browser.
await locales.getByRole('button', { name: 'Automatisch' }).click();
await expect
.poll(async () => (await (await page.request.get('/api/auth/me')).json()).locale)
.toBe(null);
await page.request.post('/api/auth/logout');
});