Update admin theme/layout and refresh README details.

Align the project baseline with the latest admin interface styling and layout structure while documenting setup and usage updates in README.
This commit is contained in:
vlad
2026-07-14 17:12:28 +03:00
commit 86cc3fa541
278 changed files with 19416 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from app.core.install_secrets import ensure_install_secrets
def main() -> None:
status = ensure_install_secrets()
if status.created:
print(f"Created install secrets at {status.path}")
print(
"\nIf docker compose was already started without install.env, reset the Postgres volume "
"before the next start (local dev only — deletes DB data):\n"
" docker compose --profile docker-web down -v\n"
" docker compose --profile docker-web up -d --build"
)
else:
print(f"Install secrets already exist at {status.path}")
if __name__ == "__main__":
main()
+101
View File
@@ -0,0 +1,101 @@
"""Docker entrypoint: wait for DB, reconcile Alembic state, migrate, start API."""
from __future__ import annotations
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from alembic import command
from alembic.config import Config
from sqlalchemy import create_engine, inspect, text
from app.core.install_secrets import ensure_install_secrets
from app.core.config import settings
INITIAL_REVISION = "20260711_0001"
SCHEMA_TABLES = (
"content_pages",
"email_verification_tokens",
"password_reset_tokens",
"refresh_tokens",
"user_profiles",
"users",
)
def wait_for_database(max_attempts: int = 30, delay_seconds: float = 1.0):
engine = create_engine(settings.database_url)
last_error: Exception | None = None
for _ in range(max_attempts):
try:
with engine.connect() as connection:
connection.execute(text("SELECT 1"))
return engine
except Exception as exc:
last_error = exc
time.sleep(delay_seconds)
detail = str(last_error or "unknown error")
hint = ""
if "password authentication failed" in detail or "does not exist" in detail:
hint = (
"\n\nPostgres credentials in install.env do not match the existing database volume "
"(common if docker compose ran before bootstrap_install.py).\n"
"Local dev fix — deletes Docker DB data:\n"
" docker compose --profile docker-web down -v\n"
" docker compose --profile docker-web up -d --build\n"
"See docs/secrets-recovery.md"
)
raise RuntimeError(f"Database is unavailable: {detail}{hint}") from last_error
def current_revision(engine) -> str | None:
inspector = inspect(engine)
if "alembic_version" not in inspector.get_table_names():
return None
with engine.connect() as connection:
return connection.execute(text("SELECT version_num FROM alembic_version")).scalar()
def _reset_schema(engine) -> None:
cascade = " CASCADE" if engine.dialect.name == "postgresql" else ""
with engine.begin() as connection:
for table in SCHEMA_TABLES:
connection.execute(text(f'DROP TABLE IF EXISTS "{table}"{cascade}'))
connection.execute(text(f'DROP TABLE IF EXISTS "alembic_version"{cascade}'))
def run_migrations(engine) -> None:
config = Config("alembic.ini")
tables = set(inspect(engine).get_table_names())
revision = current_revision(engine)
schema_tables = set(SCHEMA_TABLES)
existing_schema = tables & schema_tables
if existing_schema:
if schema_tables.issubset(tables):
if revision is None:
command.stamp(config, INITIAL_REVISION)
else:
_reset_schema(engine)
command.upgrade(config, "head")
def main() -> None:
ensure_install_secrets()
engine = wait_for_database()
run_migrations(engine)
subprocess.run(
[sys.executable, "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"],
check=True,
)
if __name__ == "__main__":
main()
+14
View File
@@ -0,0 +1,14 @@
from pathlib import Path
import json
from app.main import app
def main() -> None:
schema = app.openapi()
output = Path(__file__).resolve().parent.parent / "openapi.json"
output.write_text(json.dumps(schema, indent=2), encoding="utf-8")
if __name__ == "__main__":
main()
+13
View File
@@ -0,0 +1,13 @@
"""Apply Alembic migrations."""
from alembic import command
from alembic.config import Config
def main() -> None:
config = Config("alembic.ini")
command.upgrade(config, "head")
if __name__ == "__main__":
main()
+30
View File
@@ -0,0 +1,30 @@
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
API_DIR = Path(__file__).resolve().parents[1]
os.chdir(API_DIR)
sys.path.insert(0, str(API_DIR))
db_file = API_DIR / ".e2e.sqlite"
if db_file.exists():
db_file.unlink()
os.environ["DATABASE_URL"] = f"sqlite+pysqlite:///{db_file.as_posix()}"
os.environ["EMAIL_DELIVERY_MODE"] = "memory"
os.environ["STORAGE_MODE"] = "memory"
os.environ["ENABLE_RATE_LIMIT"] = "false"
os.environ["ENABLE_TEST_ROUTES"] = "true"
web_port = os.environ.get("E2E_WEB_PORT", "5175")
os.environ["CORS_ORIGINS"] = f'["http://127.0.0.1:{web_port}","http://localhost:{web_port}"]'
port = os.environ.get("E2E_API_PORT", "8001")
subprocess.run(
[sys.executable, "-m", "uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", port],
cwd=API_DIR,
check=True,
)