Files
JiriUhlir bd256d565c c
2026-06-17 12:47:53 +02:00

559 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import html
from fastapi import APIRouter, Depends
from fastapi.responses import HTMLResponse
from ..auth import require_user
from ..config import DEFAULT_GITEA_ORG, get_gitea_public_url, read_env_value
from ..templates.layout import page
router = APIRouter()
# Portal sections (the admin/agenda links shown in the navigation). role: minimum role that can
# actually use the section; viewers may see read-only data where a section allows it.
PORTAL_SECTIONS = [
("fa-server", "Služby", "/portal/apps",
"Přehled nasazených služeb: stav, dokumentace, prostředky (RAM/CPU), Git a akce.", "viewer (čtení) / developer / admin"),
("fa-list-check", "Úlohy", "/portal/jobs",
"Fronta a historie úloh (nasazení, skripty) včetně stavů a logů.", "developer / admin"),
("fa-file-lines", "Logy", "/portal/logs",
"Souhrnné čtení logů napříč všemi úlohami; filtr chyb a proklik na konkrétní řádek úlohy.", "developer / admin"),
("fa-calendar-days", "Plánované skripty", "/portal/scheduled-scripts",
"Cron-like skripty spouštěné na pozadí; úpravy jen pro administrátory.", "developer / admin"),
("fa-bell", "Alerting", "/portal/alerting/rules",
"Pravidla alertů a jejich skripty; úpravy jen pro administrátory.", "developer / admin"),
("fa-sliders", "Environment", "/portal/admin/environment",
"Bezpečná úprava hlavního appfactory.env (backup, validace) + navazující redeploy core služeb (Portal, Worker, Webhook, Monitor, Gateway, Gitea, Registry) přes job frontu.", "admin"),
("fa-rocket", "Nasazení", "/portal/deployments",
"Historie deploy běhů, stavů a výstupů z deploy procesu.", "developer / admin"),
("fa-triangle-exclamation", "Incidenty", "/portal/incidents",
"Evidované incidenty služeb.", "developer / admin"),
("fa-gears", "Workery", "/portal/workers",
"Stav workerů zpracovávajících úlohy.", "admin"),
("fa-users", "Users", "/portal/admin/users",
"Správa uživatelů a rolí (viewer / developer / admin).", "admin"),
("fa-clipboard-list", "Audit", "/portal/audit",
"Auditní log akcí provedených v portálu.", "admin"),
("fa-box-archive", "Zálohy", "/portal/backups",
"Vytváření, stažení a obnova záloh AppFactory. Obnova je jen pro administrátory.", "admin"),
]
# Public / API endpoints not gated by the portal login.
PUBLIC_ENDPOINTS = [
("GET", "/apps", "Veřejný JSON katalog služeb (název + odkaz na dokumentaci). Routuje se přes Caddy."),
("GET", "/portal/health", "Health endpoint portálu (HTTP 200, pokud portál běží)."),
("GET", "/apps/<app-id>/docs", "Swagger/OpenAPI dokumentace konkrétní služby."),
("GET", "/apps/<app-id>/health", "Health endpoint konkrétní služby."),
]
# Roles model.
ROLES = [
("guest", "Výchozí role nového účtu po prvním přihlášení. Bez přístupu vidí jen výzvu požádat administrátora o přidělení práv."),
("viewer", "Pouze pro čtení: vidí jaké služby běží a jejich dokumentaci (Swagger). Bez Gitu, úprav, nasazení a mazání."),
("developer", "Vše co viewer + provozní workflow služeb: Git/klonování, nasazení, proměnné, prostředky, metadata. Nemůže mazat služby ani do Administrace."),
("admin", "Plný přístup: vše co developer + mazání služeb, správa záloh a jejich obnova, správa uživatelů, audit a admin-only operace."),
]
@router.get("/developers", response_class=HTMLResponse)
def developers_page(user=Depends(require_user)):
section_rows = ""
for icon, title, href, desc, roles in PORTAL_SECTIONS:
section_rows += f"""
<tr>
<td><a href="{href}"><i class="fa-solid {icon}" aria-hidden="true"></i> {html.escape(title)}</a></td>
<td><code>{html.escape(href)}</code></td>
<td>{html.escape(desc)}</td>
<td><span class="muted">{html.escape(roles)}</span></td>
</tr>
"""
endpoint_rows = ""
for method, path, desc in PUBLIC_ENDPOINTS:
endpoint_rows += f"""
<tr>
<td><span class="pill">{html.escape(method)}</span></td>
<td><code>{path}</code></td>
<td>{html.escape(desc)}</td>
</tr>
"""
role_rows = ""
for role, desc in ROLES:
role_rows += f"""
<tr>
<td><span class="pill pill-muted">{html.escape(role)}</span></td>
<td>{html.escape(desc)}</td>
</tr>
"""
gitea_url = get_gitea_public_url()
gitea_org = read_env_value("GITEA_ORG", DEFAULT_GITEA_ORG)
clone_base = gitea_url or "<gitea-url>"
clone_cmd = html.escape(f"git clone {clone_base}/{gitea_org}/<app-id>.git", quote=True)
return page(
"Pro vývojáře",
f"""
<div class="card">
<h2><i class="fa-solid fa-code" aria-hidden="true"></i> Pro vývojáře</h2>
<p class="muted">
Přehled všech sekcí portálu, veřejných endpointů a rolí. Portál běží za reverzní proxy
na cestě <code>/portal</code>; jednotlivé služby na <code>/apps/&lt;app-id&gt;</code>.
</p>
</div>
<div class="card">
<h2><i class="fa-solid fa-server" aria-hidden="true"></i> Replikace serveru</h2>
<p class="muted">
Checklist a kopírovatelné příkazy pro čistou instalaci nebo obnovu AppFactory na nový Linux server.
</p>
<p><a class="btn" href="/portal/developers/server-replication"><i class="fa-solid fa-list-check" aria-hidden="true"></i> Replikace serveru / požadavky na nový server</a></p>
</div>
<div class="card">
<h2><i class="fa-solid fa-cloud-arrow-up" aria-hidden="true"></i> Jak publikovat</h2>
<p class="muted">Postup od založení nové služby po nasazení změn.</p>
<ol class="publish-steps">
<li>
<strong>Vytvořte novou službu.</strong>
Otevřete <a href="/portal/new-app" target="_blank" rel="noopener">formulář Nová služba</a>;
vznikne Gitea repozitář, workspace a první nasazení.
</li>
<li>
<strong>Naklonujte repozitář.</strong>
<div class="cmd-row">
<input readonly value="{clone_cmd}" id="publish-clone">
<button type="button" onclick="return copyText(this)"><i class="fa-solid fa-copy" aria-hidden="true"></i> Kopírovat</button>
</div>
<span class="muted">Přesný příkaz pro konkrétní službu (HTTP i SSH) najdete také na stránce
<a href="/portal/apps">Služby</a>.</span>
</li>
<li>
<strong>Upravte kód.</strong>
Buď se řiďte souborem <code>AGENTS.md</code> v kořenové složce projektu, nebo úpravy
nechte provést AI (agent si <code>AGENTS.md</code> přečte sám).
</li>
<li>
<strong>Commit a push.</strong>
Zacommitujte a pushněte změny — build a nasazení se spustí automaticky
(worker je vždy spuštěný a změnu rovnou zpracuje).
</li>
<li>
<strong>Zkontrolujte výsledek.</strong>
Stav nasazení a běh služby ověříte na stránce
<a href="/portal/apps">Služby</a> (stav, health, dokumentace).
</li>
</ol>
</div>
<div class="card">
<h2><i class="fa-solid fa-diagram-project" aria-hidden="true"></i> Sekce portálu (agendy)</h2>
<table>
<tr>
<th>Sekce</th>
<th>Odkaz</th>
<th>Popis</th>
<th>Role</th>
</tr>
{section_rows}
</table>
</div>
<div class="card">
<h2><i class="fa-solid fa-plug" aria-hidden="true"></i> Veřejné / API endpointy</h2>
<table>
<tr>
<th>Metoda</th>
<th>Cesta</th>
<th>Popis</th>
</tr>
{endpoint_rows}
</table>
<p class="muted">
Veřejný katalog <code>/apps</code> vrací JSON se seznamem služeb a odkazy na jejich dokumentaci.
</p>
</div>
<div class="card">
<h2><i class="fa-solid fa-user-shield" aria-hidden="true"></i> Role a oprávnění</h2>
<table>
<tr>
<th>Role</th>
<th>Co může</th>
</tr>
{role_rows}
</table>
</div>
""",
user=user,
)
# --- Replikace serveru / požadavky na nový server -------------------------------------------------
# Čistě dokumentační stránka (checklist + kopírovatelné příkazy). Žádná Docker logika v portálu.
# Bash/text bloky jsou běžné (ne f-string) řetězce, aby složené závorky ({}, {{.Names}}) zůstaly
# literálně a copy tlačítka kopírovala přesný obsah.
def _code_block(code: str) -> str:
"""Kopírovatelný blok příkazu (tmavý <pre> + tlačítko Kopírovat)."""
escaped = html.escape(code.strip("\n"))
return (
'<div class="code-block">'
'<button type="button" class="copy-button code-copy" onclick="return copyText(this)">'
'<i class="fa-solid fa-copy" aria-hidden="true"></i> Kopírovat</button>'
f'<pre>{escaped}</pre>'
'</div>'
)
def _text_block(text: str) -> str:
"""Blok prostého výpisu (např. chybová hláška) bez kopírování."""
return f'<pre class="text-block">{html.escape(text.strip(chr(10)))}</pre>'
APT_INSTALL = r"""apt update
apt install -y \
git \
curl \
wget \
jq \
sqlite3 \
unzip \
zip \
tar \
gzip \
ca-certificates \
gnupg \
lsb-release \
bash \
sudo \
openssh-client \
openssh-server \
nano \
vim \
rsync \
cron \
procps \
net-tools \
dnsutils \
iputils-ping \
software-properties-common"""
DOCKER_INSTALL = r"""curl -fsSL https://get.docker.com | sh
apt install -y docker-compose-plugin"""
DOCKER_VERIFY = r"""docker --version
docker compose version"""
DOCKER_GROUP = r"""groupadd docker || true
usermod -aG docker root"""
DOCKER_GROUP_ADMIN = r"""usermod -aG docker jiri"""
UTILITIES = """git
curl
wget
jq
sqlite3
tar
zip
unzip
bash
cron
rsync
ssh
docker
docker compose"""
DNS_LIST = """services.csbot.cz
git.csbot.cz
registry.csbot.cz"""
DNS_CHECK = r"""nslookup services.csbot.cz
nslookup git.csbot.cz
nslookup registry.csbot.cz"""
FIREWALL = """22 SSH
80 HTTP
443 HTTPS"""
STRUCTURE = """/opt/appfactory
/opt/appfactory/config
/opt/appfactory/data
/opt/appfactory/workspace
/opt/appfactory/backups
/opt/appfactory/logs
/opt/appfactory/tools"""
GIT_CONFIG_HOST = r"""git config --system --add safe.directory "*" """
SERVER_VERIFY = r"""git --version
docker --version
docker compose version
jq --version
sqlite3 --version
curl --version"""
FIX_PERMISSIONS = r"""cat > /opt/appfactory/tools/fix-appfactory-server-permissions.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
APPFACTORY_ROOT="/opt/appfactory"
echo "[1/9] Create appfactory group..."
getent group appfactory >/dev/null || groupadd appfactory
echo "[2/9] Add known users to appfactory group..."
for u in root jiri git caddy www-data; do
if id "$u" >/dev/null 2>&1; then
usermod -aG appfactory "$u" || true
fi
done
echo "[3/9] Ensure base directories exist..."
mkdir -p "$APPFACTORY_ROOT/config"
mkdir -p "$APPFACTORY_ROOT/data"
mkdir -p "$APPFACTORY_ROOT/workspace"
mkdir -p "$APPFACTORY_ROOT/backups"
mkdir -p "$APPFACTORY_ROOT/logs"
mkdir -p "$APPFACTORY_ROOT/tools"
echo "[4/9] Set ownership under /opt/appfactory..."
chown -R root:appfactory "$APPFACTORY_ROOT"
echo "[5/9] Set directory permissions..."
find "$APPFACTORY_ROOT" -type d -exec chmod 2775 {} \;
echo "[6/9] Set file permissions..."
find "$APPFACTORY_ROOT" -type f -exec chmod 664 {} \;
echo "[7/9] Make shell scripts executable..."
find "$APPFACTORY_ROOT" -type f -name "*.sh" -exec chmod 775 {} \;
echo "[8/9] Fix Git safe.directory on host..."
git config --system --add safe.directory "*" || true
echo "[9/9] Fix Gitea sessions directory if present..."
if [ -d "$APPFACTORY_ROOT/data/gitea/gitea" ]; then
rm -rf "$APPFACTORY_ROOT/data/gitea/gitea/sessions"
mkdir -p "$APPFACTORY_ROOT/data/gitea/gitea/sessions"
chmod 777 "$APPFACTORY_ROOT/data/gitea/gitea/sessions"
fi
echo "Done."
EOF
bash /opt/appfactory/tools/fix-appfactory-server-permissions.sh"""
CONTAINER_GIT = r"""for c in appfactory-portal appfactory-worker appfactory-gitea; do
if docker ps -a --format '{{.Names}}' | grep -qx "$c"; then
docker exec "$c" sh -lc 'git config --system --add safe.directory "*" || git config --global --add safe.directory "*" || true' || true
fi
done"""
GITEA_PANIC = """PANIC: session(start): chtimes /data/gitea/sessions/... operation not permitted"""
GITEA_SESSIONS_FIX = r"""docker stop appfactory-gitea
rm -rf /opt/appfactory/data/gitea/gitea/sessions
mkdir -p /opt/appfactory/data/gitea/gitea/sessions
chmod 777 /opt/appfactory/data/gitea/gitea/sessions
docker start appfactory-gitea"""
GITEA_HOOKS_BROKEN = """Git hooks of this repository seem to be broken"""
GITEA_HOOKS_FIX = r"""docker exec -u git -it appfactory-gitea sh -lc '
gitea admin regenerate hooks --config /data/gitea/conf/app.ini
'
docker restart appfactory-gitea"""
COMPOSE_ENV_ERRORS = """APPFACTORY_UID variable is not set
APPFACTORY_GID variable is not set
APPFACTORY_DOCKER_GID variable is not set
Unable to find group"""
COMPOSE_ENV_EXPORT = r"""export APPFACTORY_UID="$(id -u)"
export APPFACTORY_GID="$(id -g)"
export APPFACTORY_DOCKER_GID="$(getent group docker | cut -d: -f3)" """
MOUNT_YAML = """volumes:
- /opt/appfactory/config:/opt/appfactory/config:rw"""
READONLY_PATH = """/opt/appfactory/config/appfactory.env"""
READONLY_ERROR = """Read-only file system"""
@router.get("/developers/server-replication", response_class=HTMLResponse)
def server_replication_page(user=Depends(require_user)):
body = "".join([
"""
<div class="card">
<h2><i class="fa-solid fa-server" aria-hidden="true"></i> Replikace serveru / požadavky na nový server</h2>
<p class="muted">
Checklist pro čistou obnovu nebo instalaci AppFactory na nový Linux server. Jde o dokumentaci pro
administrátora příkazy se spouštějí na serveru, ne z portálu.
</p>
<p><a href="/portal/developers">&larr; Zpět na Pro vývojáře</a></p>
</div>
<div class="card">
<h2><i class="fa-solid fa-clipboard-check" aria-hidden="true"></i> 1. Požadavky na nový server</h2>
<p class="muted">Nový server musí mít:</p>
<ul class="req-list">
<li>Linux server, ideálně Ubuntu/Debian</li>
<li>Docker</li>
<li>Docker Compose plugin</li>
<li>Git</li>
<li>curl</li>
<li>bash</li>
<li>sqlite3</li>
<li>openssh-client</li>
<li>přístup k repozitářům v Gitea</li>
<li>funkční DNS pro:
<ul>
<li>services.csbot.cz</li>
<li>git.csbot.cz</li>
<li>registry.csbot.cz</li>
</ul>
</li>
<li>adresář <code>/opt/appfactory</code></li>
<li>Docker síť používanou AppFactory</li>
<li>volumes/adresáře pro:
<ul>
<li>/opt/appfactory/config</li>
<li>/opt/appfactory/data</li>
<li>/opt/appfactory/workspace</li>
<li>/opt/appfactory/backups</li>
<li>/opt/appfactory/logs</li>
</ul>
</li>
</ul>
</div>
<div class="card">
<h2><i class="fa-solid fa-download" aria-hidden="true"></i> 2. Základní instalace nového serveru</h2>
<p class="muted">Před spuštěním bootstrapu musí být na serveru nainstalováno:</p>
""",
_code_block(APT_INSTALL),
"""
<h3>Docker a Docker Compose plugin</h3>
""",
_code_block(DOCKER_INSTALL),
"<p class=\"muted\">Ověření:</p>",
_code_block(DOCKER_VERIFY),
"""
<h3>Docker skupina</h3>
""",
_code_block(DOCKER_GROUP),
"<p class=\"muted\">Případně i pro administrátora:</p>",
_code_block(DOCKER_GROUP_ADMIN),
"""
<h3>Povinné utility používané AppFactory</h3>
<p class="muted">Používají je skripty AppFactory:</p>
""",
_text_block(UTILITIES),
"""
<h3>DNS požadavky</h3>
<p class="muted">Musí fungovat:</p>
""",
_text_block(DNS_LIST),
"<p class=\"muted\">Kontrola:</p>",
_code_block(DNS_CHECK),
"""
<h3>Firewall</h3>
<p class="muted">Musí být otevřené:</p>
""",
_text_block(FIREWALL),
"""
<h3>Struktura AppFactory</h3>
<p class="muted">Musí existovat:</p>
""",
_text_block(STRUCTURE),
"""
<h3>Git konfigurace (host)</h3>
""",
_code_block(GIT_CONFIG_HOST),
"""
<h3>Ověření serveru</h3>
<p class="muted">Před bootstrapem musí projít:</p>
""",
_code_block(SERVER_VERIFY),
"<p class=\"muted\">Pokud některý příkaz selže, server není připraven pro AppFactory.</p>",
"</div>",
"""
<div class="card">
<h2><i class="fa-solid fa-lock" aria-hidden="true"></i> 3. Povinné nastavení práv</h2>
<p class="muted">
V AppFactory nechceme řešit opakované chyby typu:
</p>
<ul class="req-list">
<li>Permission denied</li>
<li>Read-only file system</li>
<li>detected dubious ownership</li>
<li>operation not permitted</li>
<li>Git safe.directory problém</li>
</ul>
<p class="muted">
Proto má být pro AppFactory část serveru sjednocené právo zápisu pro relevantní procesy.
</p>
<p class="alert alert-danger">
<strong>Důležité:</strong> Neaplikovat <code>chmod 777</code> na celý Linux server.
Aplikovat pouze na <code>/opt/appfactory</code> a na konkrétní problematické runtime adresáře.
</p>
<h3>Serverový příkaz (oprava práv)</h3>
""",
_code_block(FIX_PERMISSIONS),
"""
<h3>Oprava Git safe.directory v kontejnerech</h3>
""",
_code_block(CONTAINER_GIT),
"</div>",
"""
<div class="card">
<h2><i class="fa-solid fa-code-branch" aria-hidden="true"></i> 4. Gitea sessions</h2>
<p class="muted">Pokud Gitea spadne na chybu:</p>
""",
_text_block(GITEA_PANIC),
"<p class=\"muted\">Použij:</p>",
_code_block(GITEA_SESSIONS_FIX),
"</div>",
"""
<div class="card">
<h2><i class="fa-solid fa-link-slash" aria-hidden="true"></i> 5. Gitea hooks</h2>
<p class="muted">Pokud Gitea ukazuje:</p>
""",
_text_block(GITEA_HOOKS_BROKEN),
"<p class=\"muted\">Oprava:</p>",
_code_block(GITEA_HOOKS_FIX),
"</div>",
"""
<div class="card">
<h2><i class="fa-solid fa-gears" aria-hidden="true"></i> 6. docker compose env proměnné</h2>
<p class="muted">Pokud docker compose hlásí:</p>
""",
_text_block(COMPOSE_ENV_ERRORS),
"<p class=\"muted\">Použij:</p>",
_code_block(COMPOSE_ENV_EXPORT),
"</div>",
"""
<div class="card">
<h2><i class="fa-solid fa-hard-drive" aria-hidden="true"></i> 7. Mounty</h2>
<p class="muted">Portál musí mít RW přístup ke konfiguraci:</p>
""",
_code_block(MOUNT_YAML),
"<p class=\"muted\">Nepoužívat read-only mount pro:</p>",
_text_block(READONLY_PATH),
"<p class=\"muted\">protože editor appfactory.env pak končí chybou:</p>",
_text_block(READONLY_ERROR),
"</div>",
])
return page("Replikace serveru", body, user=user)