Files
RoleForge/app/routers/ui.py
Sergey Antropoff 01d598eea5 - Админка: настройка pull-реестра (Hub / Harbor / Nexus) в БД, шифрование секретов;
обновлён /admin/config и API для os_registry.
- Molecule/раннер: env из конфигурации, ensure roleforge-os (ensure_roleforge_os.yml),
  os_registry_pull и доработки executors / runner / create.yml.
- /admin/os-images: выбор реестра, buildx (в т.ч. split amd64+arm64 + imagetools),
  опция --no-cache, стрим логов; domain.py: план команд build, ретраи push.
- UI: брендинг (app_name, app_tagline) из app_config через get_ui_branding_context;
  base.xhtml, role-create / role-view, core.js, pages-main, стили.
- Dockerfiles: требование Python ≥3.9 (assert), доработки alt9/astra/debian9/ubuntu20
  и др.; новые Dockerfile.arm64 для centos7/centos8.
- Конфиг: .env.example, config.py, pyproject.toml.
2026-05-06 07:52:29 +03:00

503 lines
16 KiB
Python

from fastapi import APIRouter, Query, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from app.services.ui_branding import get_ui_branding_context
router = APIRouter(tags=["ui"])
templates = Jinja2Templates(directory="app/templates")
_PROV_SUBS = ("k3s", "group-all", "group-addons", "group-vault", "inventory", "host-vars", "addons-enable")
_MOL_SUBS = ("run", "details", "reports")
async def _ctx(request: Request, page: str, **extra: object) -> dict:
branding = await get_ui_branding_context()
context = {"request": request, "page": page, **branding}
context.update(extra)
return context
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
async def dashboard_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(request=request, name="dashboard.xhtml", context=await _ctx(request, "dashboard"))
@router.get("/clusters", response_class=HTMLResponse)
async def cluster_list_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="cluster-list.xhtml",
context=await _ctx(request, "clusters"),
)
@router.get("/tasks", response_class=HTMLResponse)
async def tasks_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(request=request, name="tasks.xhtml", context=await _ctx(request, "tasks"))
@router.get("/tasks/{task_id}", response_class=HTMLResponse)
async def task_logs_page(request: Request, task_id: str) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="task-logs.xhtml",
context=await _ctx(request, "task-logs", task_id=task_id),
)
@router.get("/tasks/{task_id}/console", response_class=HTMLResponse)
async def task_console_page(request: Request, task_id: str) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="task-console.xhtml",
context=await _ctx(request, "task-console", task_id=task_id),
)
@router.get("/molecule", response_class=RedirectResponse)
async def molecule_index() -> RedirectResponse:
return RedirectResponse(url="/molecule/run", status_code=302)
@router.get("/molecule/{molecule_sub}", response_class=HTMLResponse)
async def molecule_page(
request: Request,
molecule_sub: str,
cluster_id: str | None = Query(default=None),
) -> HTMLResponse:
if molecule_sub not in _MOL_SUBS:
return templates.TemplateResponse(
request=request,
name="error.xhtml",
context=await _ctx(
request,
"error",
status_code=404,
message="Not Found",
details="Unknown Molecule page",
),
status_code=404,
)
return templates.TemplateResponse(
request=request,
name="molecule-tests.xhtml",
context=await _ctx(request, "molecule", molecule_sub=molecule_sub, cluster_id=cluster_id),
)
@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(request=request, name="login.xhtml", context=await _ctx(request, "login"))
@router.get("/register", response_class=HTMLResponse)
async def register_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(request=request, name="register.xhtml", context=await _ctx(request, "register"))
@router.get("/reset-password", response_class=HTMLResponse)
async def reset_password_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="reset-password.xhtml",
context=await _ctx(request, "reset-password"),
)
@router.get("/profile", response_class=HTMLResponse)
async def profile_me_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="profile-me.xhtml",
context=await _ctx(
request,
"profile",
title="My profile",
description="Your account details and public profile.",
),
)
@router.get("/profile/edit", response_class=HTMLResponse)
async def profile_edit_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="profile-edit.xhtml",
context=await _ctx(
request,
"profile-edit",
title="Edit profile",
description="Update display name, username, and bio.",
),
)
@router.get("/profile/avatar", response_class=HTMLResponse)
async def profile_avatar_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="profile-avatar.xhtml",
context=await _ctx(
request,
"profile-avatar",
title="Profile photo",
description="Upload and manage your profile photo.",
),
)
@router.get("/profile/password", response_class=HTMLResponse)
async def profile_password_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="profile-password.xhtml",
context=await _ctx(
request,
"profile-password",
title="Change password",
description="Update your account password while signed in.",
),
)
@router.get("/profile/user/{user_id}", response_class=HTMLResponse)
async def profile_user_public_page(request: Request, user_id: str) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="profile-user.xhtml",
context=await _ctx(
request,
"profile-user",
user_id=user_id,
title="User profile",
description="Public profile.",
),
)
@router.get("/teams", response_class=HTMLResponse)
async def teams_list_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="teams.xhtml",
context=await _ctx(
request,
"teams",
title="Teams",
description="Create teams, request membership, and share team-scoped Ansible roles.",
),
)
@router.get("/teams/{team_id}", response_class=HTMLResponse)
async def team_detail_page(request: Request, team_id: str) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="team-detail.xhtml",
context=await _ctx(
request,
"team-detail",
team_id=team_id,
title="Team",
description="Members, join requests, invitations, and team roles.",
),
)
@router.get("/error", response_class=HTMLResponse)
async def error_page(request: Request, status_code: int = 500, message: str = "Error") -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="error.xhtml",
context=await _ctx(request, "error", status_code=status_code, message=message, details=""),
status_code=status_code,
)
@router.get("/roles", response_class=HTMLResponse)
async def roles_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="roles.xhtml",
context=await _ctx(
request,
"roles",
title="Roles",
description="Role catalog grouped by categories with quick import and management.",
),
)
@router.get("/roles/create", response_class=HTMLResponse)
async def role_create_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="role-create.xhtml",
context=await _ctx(
request,
"roles-create",
title="Create Role",
description="Create a role and its catalog; after save you open Role details.",
),
)
@router.get("/roles/view/{role_id}", response_class=HTMLResponse)
async def role_view_page(request: Request, role_id: str) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="role-view.xhtml",
context=await _ctx(
request,
"roles-view",
role_id=role_id,
title="Role Details",
description="View and edit role files, then run Molecule tests.",
),
)
@router.get("/inventories", response_class=HTMLResponse)
async def inventories_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="workspace.xhtml",
context=await _ctx(
request,
"inventories",
title="Inventories",
description="Manage inventory sources and infrastructure templates.",
),
)
@router.get("/environment", response_class=HTMLResponse)
async def environment_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="workspace.xhtml",
context=await _ctx(
request,
"environment",
title="Environment",
description="Define the target environment and baseline context for composition.",
),
)
@router.get("/composition", response_class=HTMLResponse)
async def composition_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="workspace.xhtml",
context=await _ctx(
request,
"composition",
title="Composition",
description="Compose inventories, roles, and reusable building blocks.",
),
)
@router.get("/flow", response_class=HTMLResponse)
async def flow_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="workspace.xhtml",
context=await _ctx(
request,
"flow",
title="Flow",
description="Define orchestration flow and transition rules between steps.",
),
)
@router.get("/pipeline", response_class=HTMLResponse)
async def pipeline_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="workspace.xhtml",
context=await _ctx(
request,
"pipeline",
title="Pipeline",
description="Package flow into a repeatable pipeline with checks and gates.",
),
)
@router.get("/execution", response_class=HTMLResponse)
async def execution_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="workspace.xhtml",
context=await _ctx(
request,
"execution",
title="Execution",
description="Run the full system and observe runtime progress and outcomes.",
),
)
@router.get("/playbooks", response_class=HTMLResponse)
async def playbooks_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="workspace.xhtml",
context=await _ctx(
request,
"playbooks",
title="Playbooks",
description="Build, store, and import or export playbooks as YAML.",
),
)
@router.get("/jobs", response_class=HTMLResponse)
async def jobs_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="workspace.xhtml",
context=await _ctx(
request,
"jobs",
title="Jobs",
description="Run playbooks and track job execution.",
),
)
@router.get("/tests", response_class=HTMLResponse)
async def tests_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="workspace.xhtml",
context=await _ctx(
request,
"tests",
title="Molecule Tests",
description="Test roles and playbooks in a dynamic runtime.",
),
)
@router.get("/runners", response_class=HTMLResponse)
async def runners_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="workspace.xhtml",
context=await _ctx(
request,
"runners",
title="Runners",
description="Active runner instances, status, and manual controls.",
),
)
@router.get("/users", response_class=HTMLResponse)
async def users_admin_list_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="users.xhtml",
context=await _ctx(
request,
"users",
title="Users",
description="Registered accounts (administrators).",
),
)
@router.get("/users/{user_id}", response_class=HTMLResponse)
async def user_admin_detail_page(request: Request, user_id: str) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="user-detail.xhtml",
context=await _ctx(
request,
"user-detail",
user_id=user_id,
title="User",
description="Administrator view of a user account.",
),
)
@router.get("/admin/config", response_class=HTMLResponse)
async def admin_config_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="admin-config.xhtml",
context=await _ctx(
request,
"admin-config",
title="Admin Config",
description="Project-wide settings available to root admin users.",
),
)
@router.get("/admin/categories", response_class=HTMLResponse)
async def admin_categories_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="admin-categories.xhtml",
context=await _ctx(
request,
"admin-categories",
title="Admin Categories",
description="Manage global role categories used across the project.",
),
)
@router.get("/admin/os-images", response_class=HTMLResponse)
async def admin_os_images_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="admin-os-images.xhtml",
context=await _ctx(
request,
"admin-os-images",
title="Admin OS Images",
description="Build all Molecule test OS images from dockerfiles directory.",
),
)
@router.get("/admin/lint-config", include_in_schema=False)
async def admin_lint_config_redirect() -> RedirectResponse:
return RedirectResponse(url="/admin/yaml-lint-config", status_code=307)
@router.get("/admin/yaml-lint-config", response_class=HTMLResponse)
async def admin_yaml_lint_config_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="admin-lint.xhtml",
context=await _ctx(
request,
"admin-yaml-lint-config",
title="YamlLint config",
description="yamllint rules for the role file editor (admin only).",
),
)
@router.get("/admin/json-lint-config", response_class=HTMLResponse)
async def admin_json_lint_config_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(
request=request,
name="admin-json-lint.xhtml",
context=await _ctx(
request,
"admin-json-lint-config",
title="JSONLint config",
description="JSON lint rules for the role file editor (admin only).",
),
)