Co se změnilo:

app_templates je teď jediný source of truth.
Detail služby načítá šablony přes is_enabled = 1.
Vytvoření služby načítá šablony přes is_enabled = 1 AND create_enabled = 1.
Select hodnoty používají app_templates.id, takže fastapi odpovídá existující službě microsoft-365-service.
Při vytvoření služby se z vybrané šablony nastaví template, runtime, language, container_port a health_url.
Neznámá šablona se zobrazí jako Neznámá šablona a detail nespadne.
Hardcoded python-fastapi / seznam šablon byl odstraněn.
This commit is contained in:
JiriUhlir
2026-06-03 09:02:36 +02:00
parent 786d055f6b
commit 54c86702d2
3 changed files with 135 additions and 15 deletions
+75 -3
View File
@@ -82,14 +82,26 @@ def get_app(app_id: str):
return dict(row) if row else None return dict(row) if row else None
def get_app_templates(): def get_app_templates(create_enabled: bool = False):
run_migrations() run_migrations()
con = get_connection() con = get_connection()
create_filter = "AND create_enabled = 1" if create_enabled else ""
rows = con.execute( rows = con.execute(
""" f"""
SELECT name, runtime, description SELECT
id,
name,
runtime,
language,
description,
default_port,
default_health_path,
COALESCE(is_enabled, 1) AS is_enabled,
COALESCE(create_enabled, 0) AS create_enabled
FROM app_templates FROM app_templates
WHERE COALESCE(is_enabled, 1) = 1
{create_filter}
ORDER BY name ORDER BY name
""" """
).fetchall() ).fetchall()
@@ -98,6 +110,66 @@ def get_app_templates():
return [dict(row) for row in rows] return [dict(row) for row in rows]
def get_app_template(template_id: str, create_enabled: bool = False):
run_migrations()
con = get_connection()
create_filter = "AND COALESCE(create_enabled, 0) = 1" if create_enabled else ""
row = con.execute(
f"""
SELECT
id,
name,
runtime,
language,
description,
default_port,
default_health_path,
COALESCE(is_enabled, 1) AS is_enabled,
COALESCE(create_enabled, 0) AS create_enabled
FROM app_templates
WHERE id = ?
AND COALESCE(is_enabled, 1) = 1
{create_filter}
""",
(template_id,),
).fetchone()
con.close()
return dict(row) if row else None
def update_app_template_metadata(app_id: str, metadata: dict):
run_migrations()
con = get_connection()
con.execute(
"""
UPDATE apps
SET name = ?,
template = ?,
runtime = ?,
language = ?,
health_url = ?,
container_port = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
""",
(
metadata.get("name") or None,
metadata.get("template") or None,
metadata.get("runtime") or None,
metadata.get("language") or None,
metadata.get("health_url") or None,
metadata.get("container_port"),
app_id,
),
)
con.commit()
con.close()
def update_app_metadata(app_id: str, metadata: dict): def update_app_metadata(app_id: str, metadata: dict):
run_migrations() run_migrations()
con = get_connection() con = get_connection()
+18 -1
View File
@@ -88,9 +88,15 @@ def run_migrations():
con.execute( con.execute(
""" """
CREATE TABLE IF NOT EXISTS app_templates ( CREATE TABLE IF NOT EXISTS app_templates (
name TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
name TEXT NOT NULL,
runtime TEXT, runtime TEXT,
language TEXT,
description TEXT, description TEXT,
default_port INTEGER,
default_health_path TEXT,
is_enabled INTEGER NOT NULL DEFAULT 1,
create_enabled INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) )
@@ -125,8 +131,15 @@ def run_migrations():
""" """
) )
for statement in ( for statement in (
"ALTER TABLE app_templates ADD COLUMN id TEXT",
"ALTER TABLE app_templates ADD COLUMN name TEXT",
"ALTER TABLE app_templates ADD COLUMN runtime TEXT", "ALTER TABLE app_templates ADD COLUMN runtime TEXT",
"ALTER TABLE app_templates ADD COLUMN language TEXT",
"ALTER TABLE app_templates ADD COLUMN description TEXT", "ALTER TABLE app_templates ADD COLUMN description TEXT",
"ALTER TABLE app_templates ADD COLUMN default_port INTEGER",
"ALTER TABLE app_templates ADD COLUMN default_health_path TEXT",
"ALTER TABLE app_templates ADD COLUMN is_enabled INTEGER NOT NULL DEFAULT 1",
"ALTER TABLE app_templates ADD COLUMN create_enabled INTEGER NOT NULL DEFAULT 0",
"ALTER TABLE app_templates ADD COLUMN created_at TEXT", "ALTER TABLE app_templates ADD COLUMN created_at TEXT",
"ALTER TABLE app_templates ADD COLUMN updated_at TEXT", "ALTER TABLE app_templates ADD COLUMN updated_at TEXT",
"ALTER TABLE app_variables ADD COLUMN created_at TEXT", "ALTER TABLE app_variables ADD COLUMN created_at TEXT",
@@ -136,6 +149,10 @@ def run_migrations():
con.execute(statement) con.execute(statement)
except Exception: except Exception:
pass pass
try:
con.execute("UPDATE app_templates SET id = name WHERE id IS NULL OR id = ''")
except Exception:
pass
con.execute("CREATE INDEX IF NOT EXISTS idx_app_variables_app_id ON app_variables(app_id)") con.execute("CREATE INDEX IF NOT EXISTS idx_app_variables_app_id ON app_variables(app_id)")
con.execute("CREATE INDEX IF NOT EXISTS idx_service_incidents_status_started ON service_incidents(status, started_at)") con.execute("CREATE INDEX IF NOT EXISTS idx_service_incidents_status_started ON service_incidents(status, started_at)")
con.execute("CREATE INDEX IF NOT EXISTS idx_service_incidents_service_started ON service_incidents(service_id, started_at)") con.execute("CREATE INDEX IF NOT EXISTS idx_service_incidents_service_started ON service_incidents(service_id, started_at)")
+42 -11
View File
@@ -19,12 +19,14 @@ from ..db.apps import (
create_app_variable, create_app_variable,
delete_app_variable, delete_app_variable,
get_app, get_app,
get_app_template,
get_app_deployments, get_app_deployments,
get_app_templates, get_app_templates,
get_app_variables, get_app_variables,
get_apps, get_apps,
update_app_metadata, update_app_metadata,
update_app_resources, update_app_resources,
update_app_template_metadata,
update_app_variable, update_app_variable,
) )
from ..db.audit import log_audit_event from ..db.audit import log_audit_event
@@ -73,9 +75,11 @@ def bool_checked(value) -> str:
return " checked" if value else "" return " checked" if value else ""
def render_template_options(templates: list[dict], selected_template: str) -> str: def render_template_options(templates: list[dict], selected_template: str, include_blank: bool = True) -> str:
options = ['<option value="">Bez &scaron;ablony</option>'] options = ['<option value="">Bez &scaron;ablony</option>'] if include_blank else []
selected_exists = not selected_template
for template in templates: for template in templates:
template_id = template.get("id", "") or ""
name = template.get("name", "") or "" name = template.get("name", "") or ""
runtime = template.get("runtime", "") or "" runtime = template.get("runtime", "") or ""
description = template.get("description", "") or "" description = template.get("description", "") or ""
@@ -84,13 +88,28 @@ def render_template_options(templates: list[dict], selected_template: str) -> st
label_parts.append(runtime) label_parts.append(runtime)
if description: if description:
label_parts.append(description) label_parts.append(description)
selected = " selected" if selected_template == name else "" selected = " selected" if selected_template == template_id else ""
if selected:
selected_exists = True
options.append( options.append(
f'<option value="{html.escape(name)}"{selected}>{html.escape(" - ".join(label_parts))}</option>' f'<option value="{html.escape(template_id)}"{selected}>{html.escape(" - ".join(label_parts))}</option>'
)
if not selected_exists:
options.append(
f'<option value="{html.escape(selected_template)}" selected>Nezn&aacute;m&aacute; &scaron;ablona</option>'
) )
return "".join(options) return "".join(options)
def render_template_label(templates: list[dict], template_id: str) -> str:
if not template_id:
return ""
for template in templates:
if template.get("id") == template_id:
return html.escape(template.get("name", "") or template_id)
return "Nezn&aacute;m&aacute; &scaron;ablona"
def diff_metadata(before: dict, after: dict) -> dict: def diff_metadata(before: dict, after: dict) -> dict:
changes = {} changes = {}
for field in METADATA_FIELDS: for field in METADATA_FIELDS:
@@ -404,7 +423,6 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
updated_at = html.escape(app.get("updated_at", "") or "") updated_at = html.escape(app.get("updated_at", "") or "")
description = html.escape(app.get("description", "") or "") description = html.escape(app.get("description", "") or "")
owner = html.escape(app.get("owner", "") or "") owner = html.escape(app.get("owner", "") or "")
template_value = html.escape(app.get("template", "") or "")
runtime = html.escape(app.get("runtime", "") or "") runtime = html.escape(app.get("runtime", "") or "")
repository_url = html.escape(app.get("repository_url", "") or "") repository_url = html.escape(app.get("repository_url", "") or "")
repository_name = html.escape(app.get("repository_name", "") or "") repository_name = html.escape(app.get("repository_name", "") or "")
@@ -416,6 +434,7 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
is_enabled = bool(app.get("is_enabled")) is_enabled = bool(app.get("is_enabled"))
templates = get_app_templates() templates = get_app_templates()
template_options = render_template_options(templates, app.get("template", "") or "") template_options = render_template_options(templates, app.get("template", "") or "")
template_value = render_template_label(templates, app.get("template", "") or "")
variables = get_app_variables(app.get("id", "")) variables = get_app_variables(app.get("id", ""))
incidents = get_service_incidents(app.get("id", ""), limit=20) incidents = get_service_incidents(app.get("id", ""), limit=20)
current_health = get_service_health(app.get("id", "")) current_health = get_service_health(app.get("id", ""))
@@ -867,9 +886,10 @@ def redeploy_app(app_id: str, user=Depends(require_user)):
@router.get("/new-app", response_class=HTMLResponse) @router.get("/new-app", response_class=HTMLResponse)
def new_app_form(request: Request, user=Depends(require_user)): def new_app_form(request: Request, user=Depends(require_user)):
template_options = render_template_options(get_app_templates(create_enabled=True), "", include_blank=False)
return page( return page(
"Nová služba", "Nová služba",
""" f"""
<div class="card"> <div class="card">
<h2>Vytvořit novou službu</h2> <h2>Vytvořit novou službu</h2>
<p class="muted">Vytvoří Gitea repozitář, webhook, lokální workspace, první commit a nasadí službu.</p> <p class="muted">Vytvoří Gitea repozitář, webhook, lokální workspace, první commit a nasadí službu.</p>
@@ -887,9 +907,7 @@ def new_app_form(request: Request, user=Depends(require_user)):
<p> <p>
<label>Šablona</label><br> <label>Šablona</label><br>
<select name="template"> <select name="template" required>{template_options}</select>
<option value="python-fastapi">Python FastAPI</option>
</select>
</p> </p>
<button type="submit">Vytvořit službu</button> <button type="submit">Vytvořit službu</button>
@@ -909,10 +927,23 @@ def create_app(
template: str = Form(...), template: str = Form(...),
user=Depends(require_user), user=Depends(require_user),
): ):
if template != "python-fastapi": selected_template = get_app_template(template, create_enabled=True)
if not selected_template:
return HTMLResponse("Nepodporovaná šablona", 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])
if create_result.returncode == 0:
update_app_template_metadata(
app_id,
{
"name": app_name,
"template": selected_template.get("id"),
"runtime": selected_template.get("runtime"),
"language": selected_template.get("language"),
"health_url": selected_template.get("default_health_path"),
"container_port": selected_template.get("default_port"),
},
)
deploy_result = run_command([DEPLOY_SCRIPT, app_id]) deploy_result = run_command([DEPLOY_SCRIPT, app_id])
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"
@@ -924,7 +955,7 @@ def create_app(
metadata={ metadata={
"app_id": app_id, "app_id": app_id,
"app_name": app_name, "app_name": app_name,
"template": template, "template": selected_template.get("id"),
"status": status, "status": status,
"create_returncode": create_result.returncode, "create_returncode": create_result.returncode,
"deploy_returncode": deploy_result.returncode, "deploy_returncode": deploy_result.returncode,