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
@@ -0,0 +1,405 @@
"""initial schema
Revision ID: 563ae5ac1d0d
Revises:
Create Date: 2026-07-18 14:41:47.620604
"""
from typing import Sequence, Union
import pgvector.sqlalchemy
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "563ae5ac1d0d"
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.execute("CREATE EXTENSION IF NOT EXISTS vector")
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"departments",
sa.Column("name", sa.String(length=200), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("name"),
)
op.create_table(
"jobs",
sa.Column("type", sa.String(length=100), nullable=False),
sa.Column("payload", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column(
"status",
sa.Enum(
"pending",
"running",
"done",
"failed",
name="jobstatus",
native_enum=False,
length=32,
),
nullable=False,
),
sa.Column(
"run_after",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column("attempts", sa.Integer(), nullable=False),
sa.Column("last_error", sa.Text(), nullable=True),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"ix_jobs_status_run_after", "jobs", ["status", "run_after"], unique=False
)
op.create_table(
"templates",
sa.Column("name", sa.String(length=200), nullable=False),
sa.Column("version", sa.String(length=20), nullable=False),
sa.Column("config", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column("is_builtin", sa.Boolean(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"users",
sa.Column("email", sa.String(length=320), nullable=False),
sa.Column("name", sa.String(length=200), nullable=False),
sa.Column(
"role",
sa.Enum("member", "admin", name="userrole", native_enum=False, length=32),
nullable=False,
),
sa.Column("password_hash", sa.String(length=255), nullable=False),
sa.Column("department_id", sa.Uuid(), nullable=True),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(
["department_id"], ["departments.id"], ondelete="SET NULL"
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
op.create_table(
"auth_sessions",
sa.Column("user_id", sa.Uuid(), nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_auth_sessions_user_id"), "auth_sessions", ["user_id"], unique=False
)
op.create_table(
"documents",
sa.Column("title", sa.String(length=500), nullable=False),
sa.Column(
"status",
sa.Enum(
"draft",
"published",
"archived",
name="documentstatus",
native_enum=False,
length=32,
),
nullable=False,
),
sa.Column(
"visibility",
sa.Enum(
"public",
"department",
"restricted",
name="documentvisibility",
native_enum=False,
length=32,
),
nullable=False,
),
sa.Column("content_md", sa.Text(), nullable=False),
sa.Column("meta", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column("author_id", sa.Uuid(), nullable=True),
sa.Column("department_id", sa.Uuid(), nullable=True),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(["author_id"], ["users.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(
["department_id"], ["departments.id"], ondelete="SET NULL"
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"chunks",
sa.Column("document_id", sa.Uuid(), nullable=False),
sa.Column("chunk_index", sa.Integer(), nullable=False),
sa.Column("content", sa.Text(), nullable=False),
sa.Column(
"embedding", pgvector.sqlalchemy.vector.VECTOR(dim=1024), nullable=False
),
sa.Column(
"tsv",
postgresql.TSVECTOR(),
sa.Computed(
"to_tsvector('german'::regconfig, "
"content || ' ' || coalesce(meta->>'heading_path', ''))",
persisted=True,
),
nullable=True,
),
sa.Column("meta", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(["document_id"], ["documents.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("document_id", "chunk_index"),
)
op.create_index(
op.f("ix_chunks_document_id"), "chunks", ["document_id"], unique=False
)
op.create_index(
"ix_chunks_embedding_hnsw",
"chunks",
["embedding"],
unique=False,
postgresql_using="hnsw",
postgresql_ops={"embedding": "vector_cosine_ops"},
)
op.create_index(
"ix_chunks_tsv", "chunks", ["tsv"], unique=False, postgresql_using="gin"
)
op.create_table(
"conversations",
sa.Column(
"mode",
sa.Enum(
"query",
"insight",
name="conversationmode",
native_enum=False,
length=32,
),
nullable=False,
),
sa.Column(
"status",
sa.Enum(
"active",
"completed",
"abandoned",
name="conversationstatus",
native_enum=False,
length=32,
),
nullable=False,
),
sa.Column("user_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_conversations_user_id"), "conversations", ["user_id"], unique=False
)
op.create_table(
"doc_permissions",
sa.Column("document_id", sa.Uuid(), nullable=False),
sa.Column("department_id", sa.Uuid(), nullable=False),
sa.Column(
"level",
sa.Enum("read", name="permissionlevel", native_enum=False, length=32),
nullable=False,
),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(
["department_id"], ["departments.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(["document_id"], ["documents.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("document_id", "department_id"),
)
op.create_table(
"messages",
sa.Column("conversation_id", sa.Uuid(), nullable=False),
sa.Column(
"role",
sa.Enum(
"user",
"assistant",
"system",
name="messagerole",
native_enum=False,
length=32,
),
nullable=False,
),
sa.Column("content", sa.Text(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(
["conversation_id"], ["conversations.id"], ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_messages_conversation_id"),
"messages",
["conversation_id"],
unique=False,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_messages_conversation_id"), table_name="messages")
op.drop_table("messages")
op.drop_table("doc_permissions")
op.drop_index(op.f("ix_conversations_user_id"), table_name="conversations")
op.drop_table("conversations")
op.drop_index("ix_chunks_tsv", table_name="chunks", postgresql_using="gin")
op.drop_index(
"ix_chunks_embedding_hnsw",
table_name="chunks",
postgresql_using="hnsw",
postgresql_ops={"embedding": "vector_cosine_ops"},
)
op.drop_index(op.f("ix_chunks_document_id"), table_name="chunks")
op.drop_table("chunks")
op.drop_table("documents")
op.drop_index(op.f("ix_auth_sessions_user_id"), table_name="auth_sessions")
op.drop_table("auth_sessions")
op.drop_index(op.f("ix_users_email"), table_name="users")
op.drop_table("users")
op.drop_table("templates")
op.drop_index("ix_jobs_status_run_after", table_name="jobs")
op.drop_table("jobs")
op.drop_table("departments")
# ### end Alembic commands ###