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:
@@ -0,0 +1,38 @@
|
||||
"""message meta
|
||||
|
||||
Revision ID: 15bc390bcabb
|
||||
Revises: 563ae5ac1d0d
|
||||
Create Date: 2026-07-20 09:05:10.023514
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "15bc390bcabb"
|
||||
down_revision: Union[str, Sequence[str], None] = "563ae5ac1d0d"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.add_column(
|
||||
"messages",
|
||||
sa.Column(
|
||||
"meta",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default="{}",
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_column("messages", "meta")
|
||||
@@ -0,0 +1,38 @@
|
||||
"""dismissed hints
|
||||
|
||||
Revision ID: 50f054decf55
|
||||
Revises: 72ef34f36387
|
||||
Create Date: 2026-07-20 13:14:22.851339
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "50f054decf55"
|
||||
down_revision: Union[str, Sequence[str], None] = "72ef34f36387"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.add_column(
|
||||
"users",
|
||||
sa.Column(
|
||||
"dismissed_hints",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default="[]",
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_column("users", "dismissed_hints")
|
||||
@@ -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 ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""builtin help documents
|
||||
|
||||
Revision ID: 717591478a2a
|
||||
Revises: 15bc390bcabb
|
||||
Create Date: 2026-07-20 11:28:26.085209
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "717591478a2a"
|
||||
down_revision: Union[str, Sequence[str], None] = "15bc390bcabb"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.add_column(
|
||||
"documents",
|
||||
sa.Column("is_builtin", sa.Boolean(), server_default="false", nullable=False),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_column("documents", "is_builtin")
|
||||
@@ -0,0 +1,50 @@
|
||||
"""llm settings
|
||||
|
||||
Revision ID: 72ef34f36387
|
||||
Revises: 717591478a2a
|
||||
Create Date: 2026-07-20 12:58:00.212399
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "72ef34f36387"
|
||||
down_revision: Union[str, Sequence[str], None] = "717591478a2a"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.create_table(
|
||||
"llm_settings",
|
||||
sa.Column("role", sa.String(length=32), nullable=False),
|
||||
sa.Column("base_url", sa.String(length=500), nullable=True),
|
||||
sa.Column("model", sa.String(length=200), nullable=True),
|
||||
sa.Column("api_key", 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"),
|
||||
sa.UniqueConstraint("role"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_table("llm_settings")
|
||||
@@ -0,0 +1,45 @@
|
||||
"""user locale, drop dismissed hints
|
||||
|
||||
Revision ID: 8c31d0a4e7b2
|
||||
Revises: 50f054decf55
|
||||
Create Date: 2026-07-20 16:02:10.114872
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "8c31d0a4e7b2"
|
||||
down_revision: Union[str, Sequence[str], None] = "50f054decf55"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# The first-contact hints were removed: a dismissable callout that has to
|
||||
# be explained ("what hints?") is not guidance. Guidance that matters is
|
||||
# now static text at the place it belongs.
|
||||
op.drop_column("users", "dismissed_hints")
|
||||
# NULL = follow the browser's Accept-Language; a value pins the interface
|
||||
# language for this person on every device.
|
||||
op.add_column("users", sa.Column("locale", sa.String(length=5), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_column("users", "locale")
|
||||
op.add_column(
|
||||
"users",
|
||||
sa.Column(
|
||||
"dismissed_hints",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
server_default="[]",
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""templates are never builtin
|
||||
|
||||
Revision ID: 9f4a71c60d38
|
||||
Revises: 8c31d0a4e7b2
|
||||
Create Date: 2026-07-20 16:41:03.529117
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "9f4a71c60d38"
|
||||
down_revision: Union[str, Sequence[str], None] = "8c31d0a4e7b2"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# templates/ became a catalog of blueprints that an admin adds from,
|
||||
# rather than content re-imported over the customer's rows on every
|
||||
# start. Nothing in this table is read-only any more, so the flag that
|
||||
# marked it has no meaning left. (documents.is_builtin is untouched —
|
||||
# the help pages really are the product's own.)
|
||||
op.drop_column("templates", "is_builtin")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.add_column(
|
||||
"templates",
|
||||
sa.Column(
|
||||
"is_builtin",
|
||||
sa.Boolean(),
|
||||
server_default=sa.false(),
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
"""llm settings field provenance
|
||||
|
||||
Revision ID: a71e3c92fd45
|
||||
Revises: 9f4a71c60d38
|
||||
Create Date: 2026-07-20 18:07:44.902113
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "a71e3c92fd45"
|
||||
down_revision: Union[str, Sequence[str], None] = "9f4a71c60d38"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
_FIELDS = ("base_url", "model", "api_key")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# The table changed meaning: it used to hold sparse per-field overrides
|
||||
# on top of the environment, and now holds the full configuration,
|
||||
# seeded from the environment once at first start.
|
||||
for field in _FIELDS:
|
||||
op.add_column(
|
||||
"llm_settings",
|
||||
sa.Column(
|
||||
f"{field}_from_env",
|
||||
sa.Boolean(),
|
||||
server_default=sa.false(),
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
|
||||
# The table changed meaning: it used to hold sparse per-field overrides
|
||||
# on top of the environment, and now holds the full configuration,
|
||||
# seeded from the environment at startup. Pre-existing rows are dev
|
||||
# leftovers — nothing is in production yet, so they are dropped rather
|
||||
# than translated, and `bootstrap_llm_settings()` recreates them from
|
||||
# `.env` on the next start.
|
||||
op.execute("DELETE FROM llm_settings")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
for field in _FIELDS:
|
||||
op.drop_column("llm_settings", f"{field}_from_env")
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Review requests: an open question about a document, not a status
|
||||
|
||||
Publishing becomes the author's own action, so `pending_approval` disappears
|
||||
and the delegated-approver column with it. What replaces both is a request
|
||||
that can sit on a draft OR on a published document: "please check this", with
|
||||
the question attached.
|
||||
|
||||
Revision ID: b8e14d7c05a3
|
||||
Revises: f3c8d5a92b47
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "b8e14d7c05a3"
|
||||
down_revision: Union[str, Sequence[str], None] = "f3c8d5a92b47"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"review_requests",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("document_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("requester_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("reviewer_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("question", sa.Text(), nullable=True),
|
||||
sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("resolved_by_id", sa.Uuid(), nullable=True),
|
||||
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.ForeignKeyConstraint(["requester_id"], ["users.id"], ondelete="SET NULL"),
|
||||
sa.ForeignKeyConstraint(["reviewer_id"], ["users.id"], ondelete="SET NULL"),
|
||||
sa.ForeignKeyConstraint(["resolved_by_id"], ["users.id"], ondelete="SET NULL"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_review_requests_document_id", "review_requests", ["document_id"]
|
||||
)
|
||||
op.create_index(
|
||||
"ix_review_requests_reviewer_id", "review_requests", ["reviewer_id"]
|
||||
)
|
||||
|
||||
# A document waiting for approval is simply an unpublished draft now; the
|
||||
# ask that used to be implied by the status is an explicit request.
|
||||
op.execute(
|
||||
"UPDATE documents SET status = 'draft' WHERE status = 'pending_approval'"
|
||||
)
|
||||
op.drop_column("documents", "reviewer_id")
|
||||
|
||||
# The audit trail keeps its shape: approving WAS publishing, and
|
||||
# submitting has no counterpart in a world where the author publishes.
|
||||
op.execute(
|
||||
"UPDATE document_events SET action = 'published' WHERE action = 'approved'"
|
||||
)
|
||||
op.execute("DELETE FROM document_events WHERE action = 'submitted'")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.add_column("documents", sa.Column("reviewer_id", sa.Uuid(), nullable=True))
|
||||
op.create_foreign_key(
|
||||
"documents_reviewer_id_fkey",
|
||||
"documents",
|
||||
"users",
|
||||
["reviewer_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
op.drop_index("ix_review_requests_reviewer_id", table_name="review_requests")
|
||||
op.drop_index("ix_review_requests_document_id", table_name="review_requests")
|
||||
op.drop_table("review_requests")
|
||||
@@ -0,0 +1,42 @@
|
||||
"""document reviewer
|
||||
|
||||
An author can delegate approval: `documents.reviewer_id` names the user asked
|
||||
to review a pending document. Nullable (self-approval leaves it null), SET NULL
|
||||
so a document survives the reviewer leaving.
|
||||
|
||||
Revision ID: c4f7a1b2e9d3
|
||||
Revises: a71e3c92fd45
|
||||
Create Date: 2026-07-22 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "c4f7a1b2e9d3"
|
||||
down_revision: Union[str, Sequence[str], None] = "a71e3c92fd45"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.add_column("documents", sa.Column("reviewer_id", sa.Uuid(), nullable=True))
|
||||
op.create_foreign_key(
|
||||
"documents_reviewer_id_fkey",
|
||||
"documents",
|
||||
"users",
|
||||
["reviewer_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_constraint("documents_reviewer_id_fkey", "documents", type_="foreignkey")
|
||||
op.drop_column("documents", "reviewer_id")
|
||||
@@ -0,0 +1,94 @@
|
||||
"""document events audit trail
|
||||
|
||||
An append-only history of who changed or reviewed a document, and when.
|
||||
Content-bearing events (created / edited) snapshot the Markdown source of truth
|
||||
so a past version can be viewed or diffed; the disposable chunks are never
|
||||
snapshotted. `actor_id` is SET NULL so the trail outlives its actor's account;
|
||||
events cascade with their document.
|
||||
|
||||
Revision ID: d5a9c1e3b7f2
|
||||
Revises: c4f7a1b2e9d3
|
||||
Create Date: 2026-07-22 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "d5a9c1e3b7f2"
|
||||
down_revision: Union[str, Sequence[str], None] = "c4f7a1b2e9d3"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.create_table(
|
||||
"document_events",
|
||||
sa.Column("document_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("actor_id", sa.Uuid(), nullable=True),
|
||||
sa.Column(
|
||||
"action",
|
||||
sa.Enum(
|
||||
"created",
|
||||
"edited",
|
||||
"published",
|
||||
"archived",
|
||||
"visibility_changed",
|
||||
"review_requested",
|
||||
"review_resolved",
|
||||
name="documenteventaction",
|
||||
native_enum=False,
|
||||
length=32,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("content_md", sa.Text(), nullable=True),
|
||||
sa.Column("title", sa.String(length=500), nullable=True),
|
||||
sa.Column(
|
||||
"visibility",
|
||||
sa.Enum(
|
||||
"public",
|
||||
"department",
|
||||
"restricted",
|
||||
name="documentvisibility",
|
||||
native_enum=False,
|
||||
length=32,
|
||||
),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("meta", postgresql.JSONB(astext_type=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.ForeignKeyConstraint(["document_id"], ["documents.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["actor_id"], ["users.id"], ondelete="SET NULL"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_document_events_document_id"),
|
||||
"document_events",
|
||||
["document_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_index(op.f("ix_document_events_document_id"), table_name="document_events")
|
||||
op.drop_table("document_events")
|
||||
@@ -0,0 +1,51 @@
|
||||
"""prompt settings
|
||||
|
||||
Admin overrides for shipped system prompts. A row exists only when an admin has
|
||||
changed a prompt from its code default; `content` is the full replacement text.
|
||||
|
||||
Revision ID: f3c8d5a92b47
|
||||
Revises: e7b2c4a1f6d9
|
||||
Create Date: 2026-07-22 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "f3c8d5a92b47"
|
||||
down_revision: Union[str, Sequence[str], None] = "d5a9c1e3b7f2"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.create_table(
|
||||
"prompt_settings",
|
||||
sa.Column("key", sa.String(length=64), 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.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("key"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_table("prompt_settings")
|
||||
Reference in New Issue
Block a user