From 46f8024d4e3a6d0da10d1fc501f7f9184851caae Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:38:23 +0200 Subject: [PATCH] Register apps before queued deployment --- app/db/apps.py | 107 ++++++++++++++++++++++++++++++++++++++++ app/routes/apps.py | 110 ++++++++++++++++++++++++++++++++++++------ app/static/portal.js | 82 +++++++++++++++++++++++++++++++ app/static/styles.css | 50 +++++++++++++++++++ 4 files changed, 334 insertions(+), 15 deletions(-) diff --git a/app/db/apps.py b/app/db/apps.py index 367c138..0b67654 100644 --- a/app/db/apps.py +++ b/app/db/apps.py @@ -174,6 +174,113 @@ def update_app_template_metadata(app_id: str, metadata: dict): con.close() +def upsert_created_app(app_id: str, metadata: dict): + run_migrations() + con = get_connection() + + values = ( + metadata.get("name") or app_id, + metadata.get("language") or None, + metadata.get("version") or "1.0.0", + metadata.get("status") or "created", + metadata.get("memory") or None, + metadata.get("cpus") or None, + metadata.get("description") or None, + metadata.get("owner") or None, + metadata.get("template") or None, + metadata.get("runtime") or None, + metadata.get("repository_url") or None, + metadata.get("repository_name") or app_id, + metadata.get("default_branch") or "main", + metadata.get("health_url") or None, + metadata.get("container_port"), + 1 if metadata.get("is_public") else 0, + 1 if metadata.get("is_enabled") else 0, + ) + + try: + cur = con.execute( + """ + UPDATE apps + SET name = ?, + language = ?, + version = ?, + status = ?, + memory = ?, + cpus = ?, + description = ?, + owner = ?, + template = ?, + runtime = ?, + repository_url = ?, + repository_name = ?, + default_branch = ?, + health_url = ?, + container_port = ?, + is_public = ?, + is_enabled = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + """, + (*values, app_id), + ) + + if cur.rowcount == 0: + con.execute( + """ + INSERT INTO apps ( + id, + name, + language, + version, + status, + memory, + cpus, + updated_at, + description, + owner, + template, + runtime, + repository_url, + repository_name, + default_branch, + health_url, + container_port, + is_public, + is_enabled + ) + VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + app_id, + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8], + values[9], + values[10], + values[11], + values[12], + values[13], + values[14], + values[15], + values[16], + ), + ) + + con.commit() + except Exception: + con.rollback() + raise + finally: + con.close() + + def update_app_metadata(app_id: str, metadata: dict): run_migrations() con = get_connection() diff --git a/app/routes/apps.py b/app/routes/apps.py index 3eb7b63..58f8be5 100644 --- a/app/routes/apps.py +++ b/app/routes/apps.py @@ -25,7 +25,7 @@ from ..db.apps import ( get_apps, update_app_metadata, update_app_resources, - update_app_template_metadata, + upsert_created_app, update_app_variable, ) from ..db.audit import log_audit_event @@ -893,7 +893,7 @@ def new_app_form(request: Request, user=Depends(require_user)):

Vytvořit novou službu

Vytvoří Gitea repozitář, webhook, lokální workspace, první commit a nasadí službu.

-
+


@@ -909,8 +909,16 @@ def new_app_form(request: Request, user=Depends(require_user)):

- +
+ + +
+
+

← Zpět

@@ -924,6 +932,10 @@ def create_app( app_id: str = Form(...), app_name: str = Form(...), template: str = Form(...), + description: str = Form(""), + owner: str = Form(""), + memory: str = Form(""), + cpus: str = Form(""), user=Depends(require_user), ): selected_template = get_app_template(template, create_enabled=True) @@ -934,22 +946,91 @@ def create_app( return HTMLResponse("Šablona nemá nastavený create script", status_code=400) create_result = run_command([create_script, app_id, app_name]) - if create_result.returncode == 0: - update_app_template_metadata( + if create_result.returncode != 0: + log_audit_event( + user, + action="create_app", + target_type="app", + target_id=app_id, + metadata={ + "app_id": app_id, + "app_name": app_name, + "template": selected_template.get("id"), + "status": "FAILED", + "create_returncode": create_result.returncode, + }, + ) + return render_result( + title="Vytvoření služby: FAILED", + back_url="/portal/apps", + sections=[ + ("Výstup vytvoření", create_result.stdout), + ("Chyba vytvoření", create_result.stderr), + ], + user=user, + ) + + gitea_url = read_env_value("GITEA_URL", "").rstrip("/") + gitea_org = read_env_value("GITEA_ORG", DEFAULT_GITEA_ORG) + repository_url = f"{gitea_url}/{gitea_org}/{app_id}.git" if gitea_url else None + owner_value = clean_optional(owner) or user.get("display_name") or user.get("username") or None + try: + upsert_created_app( app_id, { "name": app_name, "template": selected_template.get("id"), "runtime": selected_template.get("runtime"), "language": selected_template.get("language"), + "version": "1.0.0", + "status": "created", + "memory": clean_optional(memory), + "cpus": clean_optional(cpus), + "description": clean_optional(description), + "owner": owner_value, + "repository_name": app_id, + "repository_url": repository_url, + "default_branch": "main", "health_url": selected_template.get("default_health_path"), "container_port": selected_template.get("default_port"), + "is_public": True, + "is_enabled": True, }, ) - deploy_script = clean_optional(selected_template.get("deploy_script")) or DEPLOY_SCRIPT - deploy_result = run_command([deploy_script, app_id]) + except Exception as exc: + log_audit_event( + user, + action="create_app", + target_type="app", + target_id=app_id, + metadata={ + "app_id": app_id, + "app_name": app_name, + "template": selected_template.get("id"), + "status": "FAILED", + "create_returncode": create_result.returncode, + "registration_error": str(exc), + }, + ) + return render_result( + title="Registrace služby: FAILED", + back_url="/portal/apps", + sections=[ + ("Výstup vytvoření", create_result.stdout), + ("Chyba vytvoření", create_result.stderr), + ("Chyba registrace", str(exc)), + ], + user=user, + ) - status = "OK" if create_result.returncode == 0 and deploy_result.returncode == 0 else "FAILED" + job_id = create_job( + job_type="deploy_app", + target_type="app", + target_id=app_id, + payload={"template": selected_template.get("id")}, + user=user, + source="portal", + ) log_audit_event( user, action="create_app", @@ -959,23 +1040,22 @@ def create_app( "app_id": app_id, "app_name": app_name, "template": selected_template.get("id"), - "status": status, + "status": "OK", "create_returncode": create_result.returncode, - "deploy_returncode": deploy_result.returncode, + "job_id": job_id, }, ) return render_result( - title=f"Vytvoření služby: {status}", + title="Vytvoření služby: OK", back_url="/portal/apps", sections=[ ("Výstup vytvoření", create_result.stdout), ("Chyba vytvoření", create_result.stderr), - ("Výstup nasazení", deploy_result.stdout), - ("Chyba nasazení", deploy_result.stderr), + ("Nasazení", f"Deploy job #{job_id} byl zařazen do fronty."), ], - extra_link=f"/apps/{html.escape(app_id)}/docs", - extra_label="Otevřít Swagger", + extra_link=f"/portal/jobs/{job_id}", + extra_label="Otevřít deploy job", user=user, ) diff --git a/app/static/portal.js b/app/static/portal.js index 247d88c..e2c663c 100644 --- a/app/static/portal.js +++ b/app/static/portal.js @@ -117,3 +117,85 @@ document.addEventListener("click", (event) => { modal.close(); } }); + +function setFormDisabled(form, disabled) { + for (const field of form.querySelectorAll("input, select, textarea, button")) { + field.disabled = disabled; + } +} + +function extractMainHtml(responseText) { + const parser = new DOMParser(); + const doc = parser.parseFromString(responseText, "text/html"); + const main = doc.querySelector("main"); + return main ? main.innerHTML : ""; +} + +document.addEventListener("DOMContentLoaded", () => { + const form = document.querySelector('[data-ajax-form="new-app"]'); + if (!form) { + return; + } + + const loader = document.getElementById("new-app-loader"); + const status = document.getElementById("new-app-status"); + const result = document.getElementById("new-app-result"); + + form.addEventListener("submit", async (event) => { + event.preventDefault(); + + const formData = new FormData(form); + if (status) { + status.className = "async-status"; + status.textContent = "Vytv\u00e1\u0159\u00edm slu\u017ebu, neobnovujte str\u00e1nku."; + } + if (result) { + result.innerHTML = ""; + } + if (loader) { + loader.hidden = false; + } + setFormDisabled(form, true); + + try { + const response = await fetch(form.action, { + method: "POST", + body: formData, + headers: { + "X-Requested-With": "fetch", + }, + }); + const responseText = await response.text(); + const mainHtml = extractMainHtml(responseText); + + if (mainHtml) { + const main = document.querySelector("main"); + if (main) { + main.innerHTML = mainHtml; + } + return; + } + + if (!response.ok) { + throw new Error(responseText || "Vytvo\u0159en\u00ed slu\u017eby selhalo."); + } + + if (result) { + result.innerHTML = responseText; + } + if (status) { + status.textContent = ""; + } + } catch (error) { + if (status) { + status.className = "async-status async-status-error"; + status.textContent = error.message || "Vytvo\u0159en\u00ed slu\u017eby selhalo."; + } + setFormDisabled(form, false); + } finally { + if (loader) { + loader.hidden = true; + } + } + }); +}); diff --git a/app/static/styles.css b/app/static/styles.css index b8ec775..427c54c 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -489,6 +489,56 @@ textarea { grid-column: 2; } +button:disabled, +input:disabled, +select:disabled, +textarea:disabled { + cursor: wait; + opacity: 0.72; +} + +.form-loader { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--muted); + font-weight: 700; +} + +.form-loader[hidden] { + display: none; +} + +.spinner { + width: 16px; + height: 16px; + border: 2px solid var(--border); + border-top-color: var(--primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.async-status { + grid-column: 2; + min-height: 22px; + color: var(--muted); + font-weight: 700; +} + +.async-status-error { + color: var(--danger); +} + +#new-app-result { + margin-top: 18px; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + .detail-tabs { display: flex; flex-wrap: wrap;