This commit is contained in:
JiriUhlir
2026-05-27 13:09:34 +02:00
parent dcc1a2f3ad
commit 3e1d1f5360
4 changed files with 79 additions and 79 deletions
+54 -54
View File
@@ -41,22 +41,22 @@ def index():
memory_options = "" memory_options = ""
for value, label in [ for value, label in [
("", "Default"), ("", "Výchozí"),
("256m", "256 MB - small service"), ("256m", "256 MB - malá služba"),
("512m", "512 MB - normal service"), ("512m", "512 MB - běžná služba"),
("1g", "1 GB - larger service"), ("1g", "1 GB - větší služba"),
("2g", "2 GB - heavy service"), ("2g", "2 GB - náročná služba"),
]: ]:
selected = "selected" if memory == value else "" selected = "selected" if memory == value else ""
memory_options += f'<option value="{value}" {selected}>{label}</option>' memory_options += f'<option value="{value}" {selected}>{label}</option>'
cpu_options = "" cpu_options = ""
for value, label in [ for value, label in [
("", "Default"), ("", "Výchozí"),
("0.25", "0.25 CPU - very small"), ("0.25", "0,25 CPU - velmi malá služba"),
("0.50", "0.50 CPU - normal"), ("0.50", "0,50 CPU - běžná služba"),
("1.00", "1 CPU - full core"), ("1.00", "1 CPU - celé jádro"),
("2.00", "2 CPU - heavy"), ("2.00", "2 CPU - náročná služba"),
]: ]:
selected = "selected" if cpus == value else "" selected = "selected" if cpus == value else ""
cpu_options += f'<option value="{value}" {selected}>{label}</option>' cpu_options += f'<option value="{value}" {selected}>{label}</option>'
@@ -75,68 +75,68 @@ def index():
<div class="inline-form"> <div class="inline-form">
<select name="memory">{memory_options}</select> <select name="memory">{memory_options}</select>
<select name="cpus">{cpu_options}</select> <select name="cpus">{cpu_options}</select>
<button type="submit">Apply</button> <button type="submit">Použít</button>
</div> </div>
<div class="resource-help"> <div class="resource-help">
Memory is RAM limit. CPU is max compute share. Paměť je limit RAM. CPU určuje maximální podíl výpočetního výkonu.
Example: 0.50 = half CPU core, 1.00 = one full core. Příklad: 0,50 = polovina jádra, 1,00 = celé jádro.
</div> </div>
</form> </form>
</td> </td>
<td> <td>
<details> <details>
<summary>Clone commands</summary> <summary>Příkazy pro klonování</summary>
<label class="muted">HTTP</label> <label class="muted">HTTP</label>
<div class="cmd-row"> <div class="cmd-row">
<input readonly value="{http_clone}" id="http-{app_id}"> <input readonly value="{http_clone}" id="http-{app_id}">
<button onclick="copyValue('http-{app_id}')">Copy</button> <button onclick="copyValue('http-{app_id}')">Kopírovat</button>
</div> </div>
<label class="muted">SSH</label> <label class="muted">SSH</label>
<div class="cmd-row"> <div class="cmd-row">
<input readonly value="{ssh_clone}" id="ssh-{app_id}"> <input readonly value="{ssh_clone}" id="ssh-{app_id}">
<button onclick="copyValue('ssh-{app_id}')">Copy</button> <button onclick="copyValue('ssh-{app_id}')">Kopírovat</button>
</div> </div>
</details> </details>
</td> </td>
<td> <td>
<form method="post" action="/portal/delete-app" onsubmit="return confirm('Delete {app_id}? This removes container, image, workspace, catalog entry and Gitea repo.');"> <form method="post" action="/portal/delete-app" onsubmit="return confirm('Smazat {app_id}? Tím se odstraní kontejner, image, workspace, záznam v katalogu a Gitea repozitář.');">
<input type="hidden" name="app_id" value="{app_id}"> <input type="hidden" name="app_id" value="{app_id}">
<button type="submit" class="danger">Delete</button> <button type="submit" class="danger">Smazat</button>
</form> </form>
</td> </td>
</tr> </tr>
""" """
if not rows: if not rows:
rows = '<tr><td colspan="6">No apps deployed yet.</td></tr>' rows = '<tr><td colspan="6">Zatím nejsou nasazené žádné aplikace.</td></tr>'
return page( return page(
"Apps", "Aplikace",
f""" f"""
<div class="grid"> <div class="grid">
<div class="card"> <div class="card">
<h2>Apps</h2> <h2>Aplikace</h2>
<p class="muted">Create, deploy, clone, tune resources and delete services.</p> <p class="muted">Vytváření, nasazení, klonování, nastavení prostředků a mazání služeb.</p>
<a class="btn" href="/portal/new-app">+ New App</a> <a class="btn" href="/portal/new-app">+ Nová aplikace</a>
</div> </div>
<div class="card"> <div class="card">
<h2>Backups</h2> <h2>Zálohy</h2>
<p class="muted">Create backups and copy restore commands. Restore is intentionally manual and guarded.</p> <p class="muted">Vytváření záloh a kopírování příkazů pro obnovu. Obnova je záměrně ruční a chráněná.</p>
<a class="btn" href="/portal/backups">Manage Backups</a> <a class="btn" href="/portal/backups">Spravovat zálohy</a>
</div> </div>
</div> </div>
<div class="card"> <div class="card">
<h2>Deployed Apps</h2> <h2>Nasazené aplikace</h2>
<table> <table>
<tr> <tr>
<th>App</th> <th>Aplikace</th>
<th>Status</th> <th>Status</th>
<th>Docs</th> <th>Dokumentace</th>
<th>Resources</th> <th>Prostředky</th>
<th>Git</th> <th>Git</th>
<th>Actions</th> <th>Akce</th>
</tr> </tr>
{rows} {rows}
</table> </table>
@@ -148,34 +148,34 @@ def index():
@router.get("/new-app", response_class=HTMLResponse) @router.get("/new-app", response_class=HTMLResponse)
def new_app_form(): def new_app_form():
return page( return page(
"New App", "Nová aplikace",
""" """
<div class="card"> <div class="card">
<h2>Create New App</h2> <h2>Vytvořit novou aplikaci</h2>
<p class="muted">This creates Gitea repo, webhook, local workspace, initial commit and deploys the service.</p> <p class="muted">Vytvoří Gitea repozitář, webhook, lokální workspace, první commit a nasadí službu.</p>
<form method="post" action="/portal/new-app"> <form method="post" action="/portal/new-app">
<p> <p>
<label>App ID</label><br> <label>ID aplikace</label><br>
<input name="app_id" placeholder="gmail-service" required> <input name="app_id" placeholder="gmail-service" required>
</p> </p>
<p> <p>
<label>App Name</label><br> <label>Název aplikace</label><br>
<input name="app_name" placeholder="Gmail Service" required> <input name="app_name" placeholder="Gmail služba" required>
</p> </p>
<p> <p>
<label>Template</label><br> <label>Šablona</label><br>
<select name="template"> <select name="template">
<option value="python-fastapi">Python FastAPI</option> <option value="python-fastapi">Python FastAPI</option>
</select> </select>
</p> </p>
<button type="submit">Create App</button> <button type="submit">Vytvořit aplikaci</button>
</form> </form>
<p><a href="/portal">&larr; Back</a></p> <p><a href="/portal">&larr; Zpět</a></p>
</div> </div>
""", """,
) )
@@ -184,7 +184,7 @@ def new_app_form():
@router.post("/new-app", response_class=HTMLResponse) @router.post("/new-app", response_class=HTMLResponse)
def create_app(app_id: str = Form(...), app_name: str = Form(...), template: str = Form(...)): def create_app(app_id: str = Form(...), app_name: str = Form(...), template: str = Form(...)):
if template != "python-fastapi": if template != "python-fastapi":
return HTMLResponse("Unsupported template", status_code=400) return HTMLResponse("Nepodporovaná šablona", status_code=400)
create_result = run_command([NEW_APP_SCRIPT, app_id, app_name]) create_result = run_command([NEW_APP_SCRIPT, app_id, app_name])
deploy_result = run_command([DEPLOY_SCRIPT, app_id]) deploy_result = run_command([DEPLOY_SCRIPT, app_id])
@@ -192,16 +192,16 @@ def create_app(app_id: str = Form(...), app_name: str = Form(...), template: str
status = "OK" if create_result.returncode == 0 and deploy_result.returncode == 0 else "FAILED" status = "OK" if create_result.returncode == 0 and deploy_result.returncode == 0 else "FAILED"
return render_result( return render_result(
title=f"Create App: {status}", title=f"Vytvoření aplikace: {status}",
back_url="/portal", back_url="/portal",
sections=[ sections=[
("Create Output", create_result.stdout), ("Výstup vytvoření", create_result.stdout),
("Create Error", create_result.stderr), ("Chyba vytvoření", create_result.stderr),
("Deploy Output", deploy_result.stdout), ("Výstup nasazení", deploy_result.stdout),
("Deploy Error", deploy_result.stderr), ("Chyba nasazení", deploy_result.stderr),
], ],
extra_link=f"/apps/{html.escape(app_id)}/docs", extra_link=f"/apps/{html.escape(app_id)}/docs",
extra_label="Open Swagger", extra_label="Otevřít Swagger",
) )
@@ -211,9 +211,9 @@ def delete_app(app_id: str = Form(...)):
status = "OK" if result.returncode == 0 else "FAILED" status = "OK" if result.returncode == 0 else "FAILED"
return render_result( return render_result(
title=f"Delete App: {status}", title=f"Smazání aplikace: {status}",
back_url="/portal", back_url="/portal",
sections=[("Output", result.stdout), ("Error", result.stderr)], sections=[("Výstup", result.stdout), ("Chyba", result.stderr)],
) )
@@ -244,12 +244,12 @@ def update_resources(app_id: str = Form(...), memory: str = Form(""), cpus: str
status = "OK" if compose_result.returncode == 0 and deploy_result.returncode == 0 else "FAILED" status = "OK" if compose_result.returncode == 0 and deploy_result.returncode == 0 else "FAILED"
return render_result( return render_result(
title=f"Update Resources: {status}", title=f"Úprava prostředků: {status}",
back_url="/portal", back_url="/portal",
sections=[ sections=[
("Compose Output", compose_result.stdout), ("Výstup compose", compose_result.stdout),
("Compose Error", compose_result.stderr), ("Chyba compose", compose_result.stderr),
("Deploy Output", deploy_result.stdout), ("Výstup nasazení", deploy_result.stdout),
("Deploy Error", deploy_result.stderr), ("Chyba nasazení", deploy_result.stderr),
], ],
) )
+20 -20
View File
@@ -35,52 +35,52 @@ def backups_page():
<td> <td>
<div class="copy-row"> <div class="copy-row">
<input readonly value="{backup_path}"> <input readonly value="{backup_path}">
<button type="button" class="copy-button" data-copy-value="{backup_path}" onclick="copyText(this)">Copy</button> <button type="button" class="copy-button" data-copy-value="{backup_path}" onclick="copyText(this)">Kopírovat</button>
</div> </div>
</td> </td>
<td> <td>
<div class="copy-row"> <div class="copy-row">
<input readonly value="{restore_cmd}"> <input readonly value="{restore_cmd}">
<button type="button" class="copy-button" data-copy-value="{restore_cmd}" onclick="copyText(this)">Copy</button> <button type="button" class="copy-button" data-copy-value="{restore_cmd}" onclick="copyText(this)">Kopírovat</button>
</div> </div>
<div class="muted">Restore is dangerous and must be run manually over SSH.</div> <div class="muted">Obnova je nebezpečná a musí se spustit ručně přes SSH.</div>
</td> </td>
<td class="actions-cell"> <td class="actions-cell">
<form method="post" action="/portal/backups/delete" onsubmit="return confirm('Delete backup {backup_name}?');"> <form method="post" action="/portal/backups/delete" onsubmit="return confirm('Smazat zálohu {backup_name}?');">
<input type="hidden" name="backup_path" value="{backup_path}"> <input type="hidden" name="backup_path" value="{backup_path}">
<button type="submit" class="danger">Delete</button> <button type="submit" class="danger">Smazat</button>
</form> </form>
</td> </td>
</tr> </tr>
""" """
if not rows: if not rows:
rows = '<tr><td colspan="4">No backups found.</td></tr>' rows = '<tr><td colspan="4">Nebyly nalezeny žádné zálohy.</td></tr>'
return page( return page(
"Backups", "Zálohy",
f""" f"""
<div class="card"> <div class="card">
<h2>Backup Management</h2> <h2>Správa záloh</h2>
<p class="muted"> <p class="muted">
Backups do not include the backup directory itself. Zálohy neobsahují samotný adresář se zálohami.
Restoring an older backup should not delete newer .tar.gz files. Obnova starší zálohy by neměla smazat novější soubory .tar.gz.
Restore remains manual to avoid accidental overwrite. Obnova zůstává ruční, aby nedošlo k nechtěnému přepsání dat.
</p> </p>
<form method="post" action="/portal/backups/create"> <form method="post" action="/portal/backups/create">
<button type="submit">Create Backup</button> <button type="submit">Vytvořit zálohu</button>
</form> </form>
</div> </div>
<div class="card"> <div class="card">
<h2>Available Backups</h2> <h2>Dostupné zálohy</h2>
<table> <table>
<tr> <tr>
<th>Backup</th> <th>Záloha</th>
<th>Path</th> <th>Cesta</th>
<th>Restore Command</th> <th>Příkaz pro obnovu</th>
<th>Actions</th> <th>Akce</th>
</tr> </tr>
{rows} {rows}
</table> </table>
@@ -95,9 +95,9 @@ def create_backup():
status = "OK" if result.returncode == 0 else "FAILED" status = "OK" if result.returncode == 0 else "FAILED"
return render_result( return render_result(
title=f"Create Backup: {status}", title=f"Vytvoření zálohy: {status}",
back_url="/portal/backups", back_url="/portal/backups",
sections=[("Output", result.stdout), ("Error", result.stderr)], sections=[("Výstup", result.stdout), ("Chyba", result.stderr)],
) )
@@ -106,7 +106,7 @@ def delete_backup(backup_path: str = Form(...)):
target = Path(backup_path) target = Path(backup_path)
if not is_backup_path(target): if not is_backup_path(target):
return HTMLResponse("Invalid backup path", status_code=400) return HTMLResponse("Neplatná cesta k záloze", status_code=400)
target = target.resolve() target = target.resolve()
if target.exists(): if target.exists():
+1 -1
View File
@@ -10,7 +10,7 @@ function copyText(button) {
navigator.clipboard.writeText(value); navigator.clipboard.writeText(value);
const label = button.textContent; const label = button.textContent;
button.textContent = "Copied"; button.textContent = "Zkopírováno";
window.setTimeout(() => { window.setTimeout(() => {
button.textContent = label; button.textContent = label;
}, 1200); }, 1200);
+4 -4
View File
@@ -18,9 +18,9 @@ def page(title: str, body: str) -> str:
<span>AppFactory</span> <span>AppFactory</span>
</a> </a>
<nav> <nav>
<a href="/portal">Apps</a> <a href="/portal">Aplikace</a>
<a href="/portal/new-app">New App</a> <a href="/portal/new-app">Nová aplikace</a>
<a href="/portal/backups">Backups</a> <a href="/portal/backups">Zálohy</a>
</nav> </nav>
</header> </header>
<main> <main>
@@ -50,7 +50,7 @@ def render_result(title, back_url, sections, extra_link=None, extra_label=None):
<div class="card"> <div class="card">
<h2>{html.escape(title)}</h2> <h2>{html.escape(title)}</h2>
{extra} {extra}
<p><a href="{back_url}">&larr; Back</a></p> <p><a href="{back_url}">&larr; Zpět</a></p>
</div> </div>
<div class="card"> <div class="card">
{rendered_sections} {rendered_sections}