Files
resolutionflow/backend/alembic/versions/026_add_admin_panel_tables.py
Michael Chihlas b570f8415f feat: implement full admin panel with dashboard, user management, and platform settings
Adds complete super_admin panel with 9 pages and account owner categories page.
Backend includes 5 new DB tables, ~25 API endpoints, settings manager with
in-memory cache, and 29 integration tests. Frontend includes reusable admin
components (DataTable, Pagination, ActionMenu, etc.) with code-split lazy loading.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 06:05:59 -05:00

103 lines
5.1 KiB
Python

"""add admin panel tables
Revision ID: 026
Revises: 025
Create Date: 2026-02-08
Creates tables for admin panel:
- account_limit_overrides: Per-account plan limit overrides
- feature_flags: Feature flag definitions
- plan_feature_defaults: Which features each plan gets
- account_feature_overrides: Per-account feature exceptions
- platform_settings: Runtime configuration storage
"""
revision = "026"
down_revision = "025"
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
def upgrade() -> None:
# Account limit overrides
op.create_table(
"account_limit_overrides",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("account_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("accounts.id", ondelete="CASCADE"), unique=True, nullable=False),
sa.Column("override_max_trees", sa.Integer(), nullable=True),
sa.Column("override_max_sessions_per_month", sa.Integer(), nullable=True),
sa.Column("override_max_users", sa.Integer(), nullable=True),
sa.Column("note", sa.Text(), nullable=True),
sa.Column("created_by_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), 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),
)
op.create_index("ix_account_limit_overrides_account_id", "account_limit_overrides", ["account_id"])
# Feature flags
op.create_table(
"feature_flags",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("flag_key", sa.String(100), unique=True, nullable=False),
sa.Column("display_name", sa.String(255), nullable=False),
sa.Column("description", sa.Text(), 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),
)
# Plan feature defaults
op.create_table(
"plan_feature_defaults",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("plan", sa.String(50), sa.ForeignKey("plan_limits.plan"), nullable=False),
sa.Column("flag_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("feature_flags.id", ondelete="CASCADE"), nullable=False),
sa.Column("enabled", sa.Boolean(), server_default=sa.text("false"), nullable=False),
sa.UniqueConstraint("plan", "flag_id", name="uq_plan_feature_defaults_plan_flag"),
)
op.create_index("ix_plan_feature_defaults_plan", "plan_feature_defaults", ["plan"])
# Account feature overrides
op.create_table(
"account_feature_overrides",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("account_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False),
sa.Column("flag_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("feature_flags.id", ondelete="CASCADE"), nullable=False),
sa.Column("enabled", sa.Boolean(), nullable=False),
sa.Column("note", sa.Text(), nullable=True),
sa.Column("created_by_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), 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.UniqueConstraint("account_id", "flag_id", name="uq_account_feature_overrides_account_flag"),
)
op.create_index("ix_account_feature_overrides_account_id", "account_feature_overrides", ["account_id"])
# Platform settings
op.create_table(
"platform_settings",
sa.Column("setting_key", sa.String(100), primary_key=True),
sa.Column("setting_value", sa.Text(), nullable=True),
sa.Column("data_type", sa.String(20), nullable=False, server_default="string"),
sa.Column("is_sensitive", sa.Boolean(), server_default=sa.text("false"), nullable=False),
sa.Column("updated_by_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=True),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
)
# Seed default platform settings
op.execute(
"INSERT INTO platform_settings (setting_key, setting_value, data_type) VALUES "
"('maintenance_mode', 'false', 'boolean'), "
"('maintenance_message', 'We''re performing scheduled maintenance. We''ll be back soon!', 'string')"
)
def downgrade() -> None:
op.drop_table("platform_settings")
op.drop_table("account_feature_overrides")
op.drop_table("plan_feature_defaults")
op.drop_table("feature_flags")
op.drop_table("account_limit_overrides")