fixes + guest
This commit is contained in:
+15
-15
@@ -196,7 +196,7 @@ def render_rule_form(rule: dict | None, action: str) -> str:
|
||||
<label class="checkbox-label"><input type="checkbox" name="is_enabled" value="1"{enabled_checked}> Zapnuto</label>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit">Uložit</button>
|
||||
<button type="submit"><i class="fa-solid fa-floppy-disk" aria-hidden="true"></i> Uložit</button>
|
||||
</div>
|
||||
</form>
|
||||
"""
|
||||
@@ -230,7 +230,7 @@ def render_event_rows(events: list[dict], empty_colspan: int = 9, include_rule:
|
||||
<td>{render_status(event.get("status"))}</td>
|
||||
<td>{job_link}</td>
|
||||
<td>{html.escape(event.get("created_at", "") or "")}</td>
|
||||
<td><a class="btn" href="/portal/alerting/events/{event_id}">Detail</a></td>
|
||||
<td><a class="btn" href="/portal/alerting/events/{event_id}"><i class="fa-solid fa-eye" aria-hidden="true"></i> Detail</a></td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
@@ -273,7 +273,7 @@ def render_script_editor(rule: dict, user: dict) -> str:
|
||||
<label>Obsah souboru</label>
|
||||
<textarea name="content" rows="22" spellcheck="false">{content}</textarea>
|
||||
<div class="form-actions">
|
||||
<button type="submit">Uložit skript</button>
|
||||
<button type="submit"><i class="fa-solid fa-floppy-disk" aria-hidden="true"></i> Uložit skript</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -307,13 +307,13 @@ def alert_rules_page(request: Request, user=Depends(require_user)):
|
||||
<td>{last_event}</td>
|
||||
<td>{html.escape(rule.get("updated_at", "") or "")}</td>
|
||||
<td class="actions-cell service-actions">
|
||||
<a class="btn" href="/portal/alerting/rules/{rule_id}">Detail</a>
|
||||
<a class="btn btn-secondary" href="/portal/alerting/rules/{rule_id}/edit">Upravit</a>
|
||||
<a class="btn" href="/portal/alerting/rules/{rule_id}"><i class="fa-solid fa-eye" aria-hidden="true"></i> Detail</a>
|
||||
<a class="btn btn-secondary" href="/portal/alerting/rules/{rule_id}/edit"><i class="fa-solid fa-pen-to-square" aria-hidden="true"></i> Upravit</a>
|
||||
<form method="post" action="/portal/alerting/rules/{rule_id}/toggle" class="inline-form">
|
||||
<button type="submit" class="btn btn-secondary">{toggle_label}</button>
|
||||
<button type="submit" class="btn btn-secondary"><i class="fa-solid fa-power-off" aria-hidden="true"></i> {toggle_label}</button>
|
||||
</form>
|
||||
<form method="post" action="/portal/alerting/rules/{rule_id}/delete" class="inline-form" onsubmit="return confirm('Smazat alert pravidlo?');">
|
||||
<button type="submit" class="danger">Smazat</button>
|
||||
<button type="submit" class="danger"><i class="fa-solid fa-trash" aria-hidden="true"></i> Smazat</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -329,8 +329,8 @@ def alert_rules_page(request: Request, user=Depends(require_user)):
|
||||
<h2><i class="fa-solid fa-bell" aria-hidden="true"></i> Alert pravidla</h2>
|
||||
<p class="muted">Správa pravidel pro spouštění alert skriptů při událostech incidentů.</p>
|
||||
<p>
|
||||
<a class="btn" href="/portal/alerting/rules/new">+ Nové alert pravidlo</a>
|
||||
<a class="btn btn-secondary" href="/portal/alerting/events">Alert eventy</a>
|
||||
<a class="btn" href="/portal/alerting/rules/new"><i class="fa-solid fa-plus" aria-hidden="true"></i> Nové alert pravidlo</a>
|
||||
<a class="btn btn-secondary" href="/portal/alerting/events"><i class="fa-solid fa-bell-concierge" aria-hidden="true"></i> Alert eventy</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -415,15 +415,15 @@ def alert_rule_detail(rule_id: int, request: Request, user=Depends(require_user)
|
||||
<h2><i class="fa-solid fa-bell" aria-hidden="true"></i> {html.escape(rule.get("name", "") or "")}</h2>
|
||||
<p>
|
||||
<a class="btn" href="/portal/alerting/rules">← Zpět na alert pravidla</a>
|
||||
<a class="btn btn-secondary" href="/portal/alerting/rules/{rule_id}/edit">Upravit</a>
|
||||
<a class="btn btn-secondary" href="/portal/alerting/events">Alert eventy</a>
|
||||
<a class="btn btn-secondary" href="/portal/alerting/rules/{rule_id}/edit"><i class="fa-solid fa-pen-to-square" aria-hidden="true"></i> Upravit</a>
|
||||
<a class="btn btn-secondary" href="/portal/alerting/events"><i class="fa-solid fa-bell-concierge" aria-hidden="true"></i> Alert eventy</a>
|
||||
</p>
|
||||
<div class="inline-form">
|
||||
<form method="post" action="/portal/alerting/rules/{rule_id}/toggle">
|
||||
<button type="submit" class="btn-secondary">{toggle_label}</button>
|
||||
<button type="submit" class="btn-secondary"><i class="fa-solid fa-power-off" aria-hidden="true"></i> {toggle_label}</button>
|
||||
</form>
|
||||
<form method="post" action="/portal/alerting/rules/{rule_id}/delete" onsubmit="return confirm('Smazat alert pravidlo?');">
|
||||
<button type="submit" class="danger">Smazat</button>
|
||||
<button type="submit" class="danger"><i class="fa-solid fa-trash" aria-hidden="true"></i> Smazat</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -580,7 +580,7 @@ def alert_events_page(request: Request, user=Depends(require_user)):
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-bell-concierge" aria-hidden="true"></i> Alert eventy</h2>
|
||||
<p class="muted">Historie vyvolaných alertů a jejich zpracování.</p>
|
||||
<p><a class="btn" href="/portal/alerting/rules">Alert pravidla</a></p>
|
||||
<p><a class="btn" href="/portal/alerting/rules"><i class="fa-solid fa-bell" aria-hidden="true"></i> Alert pravidla</a></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -632,7 +632,7 @@ def alert_event_detail(event_id: int, request: Request, user=Depends(require_use
|
||||
<h2><i class="fa-solid fa-bell-concierge" aria-hidden="true"></i> Alert event #{html.escape(str(event_id))}</h2>
|
||||
<p>
|
||||
<a class="btn" href="/portal/alerting/events">← Zpět na alert eventy</a>
|
||||
<a class="btn btn-secondary" href="/portal/alerting/rules">Alert pravidla</a>
|
||||
<a class="btn btn-secondary" href="/portal/alerting/rules"><i class="fa-solid fa-bell" aria-hidden="true"></i> Alert pravidla</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
+52
-52
@@ -144,7 +144,7 @@ def ensure_variable_key_allowed(app_id: str, key: str, variable_id: int | None =
|
||||
|
||||
|
||||
def render_template_options(templates: list[dict], selected_template: str, include_blank: bool = True) -> str:
|
||||
options = ['<option value="">Bez šablony</option>'] if include_blank else []
|
||||
options = ['<option value="">Bez šablony</option>'] if include_blank else []
|
||||
selected_exists = not selected_template
|
||||
for template in templates:
|
||||
template_id = template.get("id", "") or ""
|
||||
@@ -164,7 +164,7 @@ def render_template_options(templates: list[dict], selected_template: str, inclu
|
||||
)
|
||||
if not selected_exists:
|
||||
options.append(
|
||||
f'<option value="{html.escape(selected_template)}" selected>Neznámá šablona</option>'
|
||||
f'<option value="{html.escape(selected_template)}" selected>Neznámá šablona</option>'
|
||||
)
|
||||
return "".join(options)
|
||||
|
||||
@@ -175,7 +175,7 @@ def render_template_label(templates: list[dict], template_id: str) -> str:
|
||||
for template in templates:
|
||||
if template.get("id") == template_id:
|
||||
return html.escape(template.get("name", "") or template_id)
|
||||
return "Neznámá šablona"
|
||||
return "Neznámá šablona"
|
||||
|
||||
|
||||
def render_environment_usage_example(language: str, runtime: str, template: str) -> str:
|
||||
@@ -361,8 +361,8 @@ def apps_page(
|
||||
<label>CPU</label>
|
||||
<input name="cpus" value="{cpus}" placeholder="např. 0.50, 1.00 nebo prázdné pro výchozí">
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeResourceModal('{resource_modal_id}')">Zrušit</button>
|
||||
<button type="submit">Použít</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="closeResourceModal('{resource_modal_id}')"><i class="fa-solid fa-xmark" aria-hidden="true"></i> Zrušit</button>
|
||||
<button type="submit"><i class="fa-solid fa-check" aria-hidden="true"></i> Použít</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>"""
|
||||
@@ -377,13 +377,13 @@ def apps_page(
|
||||
<label class="muted">HTTP</label>
|
||||
<div class="cmd-row">
|
||||
<input readonly value="{http_clone}" id="http-{app_id}">
|
||||
<button type="button" onclick="return copyText(this)">Kopírovat</button>
|
||||
<button type="button" onclick="return copyText(this)"><i class="fa-solid fa-copy" aria-hidden="true"></i> Kopírovat</button>
|
||||
</div>
|
||||
|
||||
<label class="muted">SSH</label>
|
||||
<div class="cmd-row">
|
||||
<input readonly value="{ssh_clone}" id="ssh-{app_id}">
|
||||
<button type="button" onclick="return copyText(this)">Kopírovat</button>
|
||||
<button type="button" onclick="return copyText(this)"><i class="fa-solid fa-copy" aria-hidden="true"></i> Kopírovat</button>
|
||||
</div>
|
||||
</details>"""
|
||||
else:
|
||||
@@ -470,12 +470,12 @@ def apps_page(
|
||||
return f"/portal/apps?{urlencode(params)}"
|
||||
|
||||
previous_link = (
|
||||
f'<a class="btn btn-secondary" href="{page_url(page_number - 1)}">Předchozí</a>'
|
||||
f'<a class="btn btn-secondary" href="{page_url(page_number - 1)}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Předchozí</a>'
|
||||
if page_number > 1
|
||||
else ""
|
||||
)
|
||||
next_link = (
|
||||
f'<a class="btn btn-secondary" href="{page_url(page_number + 1)}">Další</a>'
|
||||
f'<a class="btn btn-secondary" href="{page_url(page_number + 1)}">Další <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>'
|
||||
if page_number < total_pages
|
||||
else ""
|
||||
)
|
||||
@@ -502,7 +502,7 @@ def apps_page(
|
||||
"""
|
||||
<p><a class="btn" href="/portal/new-app"><i class="fa-solid fa-plus" aria-hidden="true"></i> Nová služba</a></p>
|
||||
<form method="post" action="/portal/apps/environment/apply-all" class="inline-form">
|
||||
<button type="submit" class="btn-secondary"><i class="fa-solid fa-arrows-rotate" aria-hidden="true"></i> Regenerovat všechny .env z databáze</button>
|
||||
<button type="submit" class="btn-secondary"><i class="fa-solid fa-arrows-rotate" aria-hidden="true"></i> Regenerovat všechny .env z databáze</button>
|
||||
</form>
|
||||
"""
|
||||
if can_manage
|
||||
@@ -709,17 +709,17 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
<input name="key" value="{variable_key}" required>
|
||||
<input name="value" value="{value_input}"{secret_hint}>
|
||||
<label class="checkbox-label"><input type="checkbox" name="is_secret" value="1"{secret_checked}> Secret</label>
|
||||
<button type="submit" class="btn btn-compact">Uložit</button>
|
||||
<button type="submit" class="btn btn-compact"><i class="fa-solid fa-floppy-disk" aria-hidden="true"></i> Uložit</button>
|
||||
</form>
|
||||
<form method="post" action="/portal/apps/{app_url_id}/variables/{variable_id}/delete" class="inline-form" onsubmit="return confirm('Smazat promennou {variable_key}?');">
|
||||
<button type="submit" class="btn btn-secondary btn-compact">Smazat</button>
|
||||
<button type="submit" class="btn btn-secondary btn-compact"><i class="fa-solid fa-trash" aria-hidden="true"></i> Smazat</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if not variable_rows:
|
||||
variable_rows = '<tr><td colspan="4">Zatím nejsou evidované žádné proměnné.</td></tr>'
|
||||
variable_rows = '<tr><td colspan="4">Zatím nejsou evidované žádné proměnné.</td></tr>'
|
||||
|
||||
def render_method_options(selected: str) -> str:
|
||||
selected_upper = (selected or "").upper()
|
||||
@@ -749,10 +749,10 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
<td><input name="ip_cidr" value="{rule_ip}" form="{form_id}" required></td>
|
||||
<td><select name="methods" form="{form_id}">{render_method_options(rule.get("methods", ""))}</select></td>
|
||||
<td><input name="description" value="{rule_desc}" form="{form_id}"></td>
|
||||
<td><label class="checkbox-label"><input type="checkbox" name="is_enabled" value="1" form="{form_id}"{enabled_checked}> Aktivní</label></td>
|
||||
<td><label class="checkbox-label"><input type="checkbox" name="is_enabled" value="1" form="{form_id}"{enabled_checked}> Aktivní</label></td>
|
||||
<td>
|
||||
<form method="post" action="/portal/apps/{app_url_id}/ip-access-rules/{rid}/update" id="{form_id}" class="inline-form"></form>
|
||||
<button type="submit" form="{form_id}" class="btn btn-compact"><i class="fa-solid fa-floppy-disk" aria-hidden="true"></i> Uložit</button>
|
||||
<button type="submit" form="{form_id}" class="btn btn-compact"><i class="fa-solid fa-floppy-disk" aria-hidden="true"></i> Uložit</button>
|
||||
<form method="post" action="/portal/apps/{app_url_id}/ip-access-rules/{rid}/delete" class="inline-form" onsubmit="return confirm('Smazat pravidlo {ip_label}?');">
|
||||
<button type="submit" class="btn btn-secondary btn-compact"><i class="fa-solid fa-trash" aria-hidden="true"></i> Smazat</button>
|
||||
</form>
|
||||
@@ -761,7 +761,7 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
"""
|
||||
|
||||
if not ip_access_rule_rows:
|
||||
ip_access_rule_rows = '<tr><td colspan="5">Zatím nejsou evidovaná žádná IP access pravidla.</td></tr>'
|
||||
ip_access_rule_rows = '<tr><td colspan="5">Zatím nejsou evidovaná žádná IP access pravidla.</td></tr>'
|
||||
|
||||
notice = ""
|
||||
if message:
|
||||
@@ -774,7 +774,7 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
redeploy_block = (
|
||||
f"""
|
||||
<form method="post" action="/portal/apps/{app_url_id}/redeploy" onsubmit="return confirm('Spustit nov\u00e9 nasazen\u00ed slu\u017eby {escaped_app_id}?');">
|
||||
<button type="submit">Nasadit znovu</button>
|
||||
<button type="submit"><i class="fa-solid fa-rotate-right" aria-hidden="true"></i> Nasadit znovu</button>
|
||||
</form>
|
||||
"""
|
||||
if can_manage
|
||||
@@ -783,17 +783,17 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
|
||||
detail_tabs = (
|
||||
"""
|
||||
<div class="detail-tabs" aria-label="Sekce detailu služby">
|
||||
<a class="btn btn-secondary" href="#metadata">Metadata</a>
|
||||
<a class="btn btn-secondary" href="#promenne">Proměnné</a>
|
||||
<a class="btn btn-secondary" href="#ip-access">Security / IP Access</a>
|
||||
<a class="btn btn-secondary" href="#historie">Historie</a>
|
||||
<div class="detail-tabs" aria-label="Sekce detailu služby">
|
||||
<a class="btn btn-secondary" href="#metadata"><i class="fa-solid fa-table-list" aria-hidden="true"></i> Metadata</a>
|
||||
<a class="btn btn-secondary" href="#promenne"><i class="fa-solid fa-sliders" aria-hidden="true"></i> Proměnné</a>
|
||||
<a class="btn btn-secondary" href="#ip-access"><i class="fa-solid fa-shield-halved" aria-hidden="true"></i> Security / IP Access</a>
|
||||
<a class="btn btn-secondary" href="#historie"><i class="fa-solid fa-clock-rotate-left" aria-hidden="true"></i> Historie</a>
|
||||
</div>
|
||||
"""
|
||||
if can_manage
|
||||
else """
|
||||
<div class="detail-tabs" aria-label="Sekce detailu služby">
|
||||
<a class="btn btn-secondary" href="#historie">Historie</a>
|
||||
<div class="detail-tabs" aria-label="Sekce detailu služby">
|
||||
<a class="btn btn-secondary" href="#historie"><i class="fa-solid fa-clock-rotate-left" aria-hidden="true"></i> Historie</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
@@ -803,13 +803,13 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
<div class="card" id="metadata">
|
||||
<h2>Metadata</h2>
|
||||
<form method="post" action="/portal/apps/{app_url_id}/metadata" class="metadata-form">
|
||||
<label>Název</label>
|
||||
<label>Název</label>
|
||||
<input name="name" value="{name}" required>
|
||||
|
||||
<label>Popis</label>
|
||||
<textarea name="description" rows="3">{description}</textarea>
|
||||
|
||||
<label>Vlastník</label>
|
||||
<label>Vlastník</label>
|
||||
<input name="owner" value="{owner}">
|
||||
|
||||
<label>Template</label>
|
||||
@@ -836,11 +836,11 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
<label>Container Port</label>
|
||||
<input name="container_port" value="{container_port}" inputmode="numeric">
|
||||
|
||||
<label class="checkbox-label"><input type="checkbox" name="is_public" value="1"{bool_checked(is_public)}> Veřejná služba</label>
|
||||
<label class="checkbox-label"><input type="checkbox" name="is_enabled" value="1"{bool_checked(is_enabled)}> Aktivní služba</label>
|
||||
<label class="checkbox-label"><input type="checkbox" name="is_public" value="1"{bool_checked(is_public)}> Veřejná služba</label>
|
||||
<label class="checkbox-label"><input type="checkbox" name="is_enabled" value="1"{bool_checked(is_enabled)}> Aktivní služba</label>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit">Uložit metadata</button>
|
||||
<button type="submit"><i class="fa-solid fa-floppy-disk" aria-hidden="true"></i> Uložit metadata</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -852,9 +852,9 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
variables_card = (
|
||||
f"""
|
||||
<div class="card" id="promenne">
|
||||
<h2>Proměnné</h2>
|
||||
<h2><i class="fa-solid fa-sliders" aria-hidden="true"></i> Proměnné</h2>
|
||||
<form method="post" action="/portal/apps/{app_url_id}/environment/apply" class="inline-form">
|
||||
<button type="submit" class="btn-secondary">Regenerovat .env z databáze</button>
|
||||
<button type="submit" class="btn-secondary"><i class="fa-solid fa-rotate" aria-hidden="true"></i> Regenerovat .env z databáze</button>
|
||||
</form>
|
||||
{environment_usage_example}
|
||||
<table>
|
||||
@@ -868,14 +868,14 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
</table>
|
||||
|
||||
<form method="post" action="/portal/apps/{app_url_id}/variables" class="metadata-form add-variable-form">
|
||||
<h3>Přidat proměnnou</h3>
|
||||
<h3>Přidat proměnnou</h3>
|
||||
<label>Key</label>
|
||||
<input name="key" required>
|
||||
<label>Value</label>
|
||||
<input name="value">
|
||||
<label class="checkbox-label"><input type="checkbox" name="is_secret" value="1"> Secret</label>
|
||||
<div class="form-actions">
|
||||
<button type="submit">Přidat proměnnou</button>
|
||||
<button type="submit"><i class="fa-solid fa-plus" aria-hidden="true"></i> Přidat proměnnou</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -889,9 +889,9 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
<div class="card" id="ip-access">
|
||||
<h2><i class="fa-solid fa-shield-halved" aria-hidden="true"></i> Security / IP Access</h2>
|
||||
<p class="muted">
|
||||
Pravidla určují, z jakých IP/CIDR adres lze volat dané HTTP metody.
|
||||
<strong>WRITE</strong> = POST, PUT, PATCH, DELETE; <strong>ALL</strong> = všechny běžné metody včetně GET.
|
||||
Pravidla se zatím pouze ukládají (Caddy je začne vynucovat později).
|
||||
Pravidla určují, z jakých IP/CIDR adres lze volat dané HTTP metody.
|
||||
<strong>WRITE</strong> = POST, PUT, PATCH, DELETE; <strong>ALL</strong> = všechny běžné metody včetně GET.
|
||||
Pravidla se zatím pouze ukládají (Caddy je začne vynucovat později).
|
||||
</p>
|
||||
<table>
|
||||
<tr>
|
||||
@@ -905,16 +905,16 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
</table>
|
||||
|
||||
<form method="post" action="/portal/apps/{app_url_id}/ip-access-rules" class="metadata-form add-variable-form">
|
||||
<h3>Přidat pravidlo</h3>
|
||||
<h3>Přidat pravidlo</h3>
|
||||
<label>IP/CIDR</label>
|
||||
<input name="ip_cidr" placeholder="např. 185.10.20.30 nebo 10.0.0.0/24" required>
|
||||
<input name="ip_cidr" placeholder="např. 185.10.20.30 nebo 10.0.0.0/24" required>
|
||||
<label>Methods</label>
|
||||
<select name="methods">{render_method_options("WRITE")}</select>
|
||||
<label>Description</label>
|
||||
<input name="description" placeholder="např. Office IP">
|
||||
<label class="checkbox-label"><input type="checkbox" name="is_enabled" value="1" checked> Aktivní</label>
|
||||
<input name="description" placeholder="např. Office IP">
|
||||
<label class="checkbox-label"><input type="checkbox" name="is_enabled" value="1" checked> Aktivní</label>
|
||||
<div class="form-actions">
|
||||
<button type="submit"><i class="fa-solid fa-plus" aria-hidden="true"></i> Přidat pravidlo</button>
|
||||
<button type="submit"><i class="fa-solid fa-plus" aria-hidden="true"></i> Přidat pravidlo</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -932,8 +932,8 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
{notice}
|
||||
<p>
|
||||
<a class="btn" href="/portal/apps">← Zpět na služby</a>
|
||||
<a class="btn btn-secondary" href="/portal/deployments?app_id={app_url_id}">Nasazení služby</a>
|
||||
<a class="btn btn-secondary" href="/portal/jobs?target={app_url_id}">Úlohy služby</a>
|
||||
<a class="btn btn-secondary" href="/portal/deployments?app_id={app_url_id}"><i class="fa-solid fa-rocket" aria-hidden="true"></i> Nasazení služby</a>
|
||||
<a class="btn btn-secondary" href="/portal/jobs?target={app_url_id}"><i class="fa-solid fa-list-check" aria-hidden="true"></i> Úlohy služby</a>
|
||||
</p>
|
||||
{redeploy_block}
|
||||
</div>
|
||||
@@ -952,7 +952,7 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
<tr><th>ID</th><td>{escaped_app_id}</td></tr>
|
||||
<tr><th>Název</th><td>{name}</td></tr>
|
||||
<tr><th>Popis</th><td>{description}</td></tr>
|
||||
<tr><th>Vlastník</th><td>{owner}</td></tr>
|
||||
<tr><th>Vlastník</th><td>{owner}</td></tr>
|
||||
<tr><th>Template</th><td>{template_value}</td></tr>
|
||||
<tr><th>Runtime</th><td>{runtime}</td></tr>
|
||||
<tr><th>Repository URL</th><td>{repository_url}</td></tr>
|
||||
@@ -961,8 +961,8 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
<tr><th>Domain</th><td>{domain}</td></tr>
|
||||
<tr><th>Health URL</th><td>{health_url}</td></tr>
|
||||
<tr><th>Container Port</th><td>{container_port}</td></tr>
|
||||
<tr><th>Veřejná služba</th><td>{"Ano" if is_public else "Ne"}</td></tr>
|
||||
<tr><th>Aktivní služba</th><td>{"Ano" if is_enabled else "Ne"}</td></tr>
|
||||
<tr><th>Veřejná služba</th><td>{"Ano" if is_public else "Ne"}</td></tr>
|
||||
<tr><th>Aktivní služba</th><td>{"Ano" if is_enabled else "Ne"}</td></tr>
|
||||
<tr><th>Jazyk</th><td>{language}</td></tr>
|
||||
<tr><th>Verze</th><td>{version}</td></tr>
|
||||
<tr><th>Status</th><td><span class="pill">{status}</span></td></tr>
|
||||
@@ -998,13 +998,13 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Incidenty služby</h2>
|
||||
<h2>Incidenty služby</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Název</th>
|
||||
<th>Začátek</th>
|
||||
<th>Název</th>
|
||||
<th>Začátek</th>
|
||||
<th>Konec</th>
|
||||
<th>Trvání</th>
|
||||
<th>Trvání</th>
|
||||
<th>Stav</th>
|
||||
</tr>
|
||||
{render_incident_history_rows(incidents, include_service=False)}
|
||||
@@ -1364,10 +1364,10 @@ def new_app_form(request: Request, user=Depends(require_developer)):
|
||||
</p>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit">Vytvořit službu</button>
|
||||
<button type="submit"><i class="fa-solid fa-circle-plus" aria-hidden="true"></i> Vytvořit službu</button>
|
||||
<span class="form-loader" id="new-app-loader" hidden>
|
||||
<span class="spinner" aria-hidden="true"></span>
|
||||
Vytvářím službu...
|
||||
Vytvářím službu...
|
||||
</span>
|
||||
</div>
|
||||
<div class="async-status" id="new-app-status" aria-live="polite"></div>
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ def audit_page(request: Request, user=Depends(require_user)):
|
||||
"Audit",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>Audit</h2>
|
||||
<h2><i class="fa-solid fa-clipboard-list" aria-hidden="true"></i> Audit</h2>
|
||||
<p class="muted">Přehled interních akcí provedených v portálu.</p>
|
||||
</div>
|
||||
|
||||
|
||||
+24
-2
@@ -259,6 +259,28 @@ def google_callback(request: Request, code: str = "", state: str = "", error: st
|
||||
return _render_login("Google login failed.")
|
||||
|
||||
|
||||
@router.get("/guest", response_class=HTMLResponse)
|
||||
def guest_page(request: Request):
|
||||
user = current_user(request)
|
||||
if not user:
|
||||
return RedirectResponse(url="/portal/login", status_code=303)
|
||||
# Účet s přidělenými právy sem nepatří – pošli ho na běžnou úvodní stránku.
|
||||
if (user.get("role") or "").lower() != "guest":
|
||||
return RedirectResponse(url="/portal/apps", status_code=303)
|
||||
|
||||
return page(
|
||||
"Čekání na schválení",
|
||||
"""
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-user-clock" aria-hidden="true"></i> Účet zatím nemá přidělená práva</h2>
|
||||
<p>Požádejte o přidělení práv administrátora.</p>
|
||||
<p class="muted">Jakmile vám administrátor přiřadí roli, obnovte stránku a portál se vám zpřístupní.</p>
|
||||
</div>
|
||||
""",
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
def logout(request: Request):
|
||||
user = current_user(request)
|
||||
@@ -424,7 +446,7 @@ def _render_login(error: str | None = None) -> str:
|
||||
<input type="password" name="password" autocomplete="current-password" required>
|
||||
</p>
|
||||
|
||||
<button type="submit" class="btn-secondary">Sign in locally</button>
|
||||
<button type="submit" class="btn-secondary"><i class="fa-solid fa-right-to-bracket" aria-hidden="true"></i> Sign in locally</button>
|
||||
</form>
|
||||
"""
|
||||
|
||||
@@ -464,7 +486,7 @@ def _render_login(error: str | None = None) -> str:
|
||||
<input type="password" name="password" autocomplete="current-password" required>
|
||||
</p>
|
||||
|
||||
<button type="submit" class="btn-secondary">Přihlásit lokálně</button>
|
||||
<button type="submit" class="btn-secondary"><i class="fa-solid fa-right-to-bracket" aria-hidden="true"></i> Přihlásit lokálně</button>
|
||||
</form>
|
||||
</div>
|
||||
""",
|
||||
|
||||
@@ -211,7 +211,7 @@ async def deployments_page(
|
||||
<td>{returncode_label}</td>
|
||||
<td>{finished_at}</td>
|
||||
<td class="actions-cell">
|
||||
<a class="btn" href="/portal/deployments/{deployment_id}">Detail</a>
|
||||
<a class="btn" href="/portal/deployments/{deployment_id}"><i class="fa-solid fa-eye" aria-hidden="true"></i> Detail</a>
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
@@ -226,12 +226,12 @@ async def deployments_page(
|
||||
first_item = offset + 1 if total_deployments else 0
|
||||
last_item = min(offset + len(deployments), total_deployments)
|
||||
previous_link = (
|
||||
f'<a class="btn btn-secondary" href="{pagination_url(page_number - 1, selected_status, selected_app, selected_source)}">Předchozí</a>'
|
||||
f'<a class="btn btn-secondary" href="{pagination_url(page_number - 1, selected_status, selected_app, selected_source)}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Předchozí</a>'
|
||||
if page_number > 1
|
||||
else ""
|
||||
)
|
||||
next_link = (
|
||||
f'<a class="btn btn-secondary" href="{pagination_url(page_number + 1, selected_status, selected_app, selected_source)}">Další</a>'
|
||||
f'<a class="btn btn-secondary" href="{pagination_url(page_number + 1, selected_status, selected_app, selected_source)}">Další <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>'
|
||||
if page_number < total_pages
|
||||
else ""
|
||||
)
|
||||
@@ -253,7 +253,7 @@ async def deployments_page(
|
||||
f"""
|
||||
{refresh}
|
||||
<div class="card">
|
||||
<h2>Historie nasazení</h2>
|
||||
<h2><i class="fa-solid fa-rocket" aria-hidden="true"></i> Historie nasazení</h2>
|
||||
<p class="muted">Přehled posledních běhů nasazení a jejich výsledků.</p>
|
||||
<a class="btn" href="/portal/operations">← Zpět na přehled</a>
|
||||
</div>
|
||||
@@ -272,8 +272,8 @@ async def deployments_page(
|
||||
<select name="app_id">{app_options}</select>
|
||||
<select name="source">{source_options}</select>
|
||||
<input type="hidden" name="page" value="1">
|
||||
<button type="submit">Filtrovat</button>
|
||||
<a class="btn btn-secondary" href="/portal/deployments">Reset</a>
|
||||
<button type="submit"><i class="fa-solid fa-filter" aria-hidden="true"></i> Filtrovat</button>
|
||||
<a class="btn btn-secondary" href="/portal/deployments"><i class="fa-solid fa-arrow-rotate-left" aria-hidden="true"></i> Reset</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -361,8 +361,8 @@ async def deployment_detail_page(
|
||||
{failed_notice}
|
||||
<p>
|
||||
<a class="btn" href="/portal/deployments">← Zpět na nasazení</a>
|
||||
<a class="btn btn-secondary" href="/portal/apps/{app_url_id}">Detail služby</a>
|
||||
<a class="btn btn-secondary" href="/portal/deployments/{html.escape(str(deployment_id))}/logs/raw">Surové logy</a>
|
||||
<a class="btn btn-secondary" href="/portal/apps/{app_url_id}"><i class="fa-solid fa-server" aria-hidden="true"></i> Detail služby</a>
|
||||
<a class="btn btn-secondary" href="/portal/deployments/{html.escape(str(deployment_id))}/logs/raw"><i class="fa-solid fa-file-lines" aria-hidden="true"></i> Surové logy</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ PUBLIC_ENDPOINTS = [
|
||||
|
||||
# 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."),
|
||||
@@ -120,7 +121,7 @@ def developers_page(user=Depends(require_user)):
|
||||
<strong>Naklonujte repozitář.</strong>
|
||||
<div class="cmd-row">
|
||||
<input readonly value="{clone_cmd}" id="publish-clone">
|
||||
<button type="button" onclick="return copyText(this)">Kopírovat</button>
|
||||
<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>
|
||||
|
||||
@@ -29,7 +29,7 @@ def _render_rows(entries: list[tuple[str, str]]) -> str:
|
||||
<td><input name="key" value="{key_html}" class="env-key" readonly></td>
|
||||
<td><input name="value" value="{value_html}" class="env-value"></td>
|
||||
<td class="actions-cell">
|
||||
<button type="button" class="btn-secondary" onclick="this.closest('tr').remove()">Smazat</button>
|
||||
<button type="button" class="btn-secondary" onclick="this.closest('tr').remove()"><i class="fa-solid fa-trash" aria-hidden="true"></i> Smazat</button>
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
@@ -109,7 +109,7 @@ def _render_page(user: dict, entries: list[tuple[str, str]], message: str = "",
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = '<td><input name="key" class="env-key" placeholder="NOVY_KLIC"></td>'
|
||||
+ '<td><input name="value" class="env-value" placeholder="hodnota"></td>'
|
||||
+ '<td class="actions-cell"><button type="button" class="btn-secondary" onclick="this.closest(\\'tr\\').remove()">Smazat</button></td>';
|
||||
+ '<td class="actions-cell"><button type="button" class="btn-secondary" onclick="this.closest(\\'tr\\').remove()"><i class="fa-solid fa-trash" aria-hidden="true"></i> Smazat</button></td>';
|
||||
tbody.appendChild(tr);
|
||||
}}
|
||||
</script>
|
||||
|
||||
@@ -111,7 +111,7 @@ def incidents_page(request: Request, user=Depends(require_user)):
|
||||
"Incidenty",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>Incidenty</h2>
|
||||
<h2><i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i> Incidenty</h2>
|
||||
<p class="muted">P\u0159ehled incident\u016f vytv\u00e1\u0159en\u00fdch a uzav\u00edran\u00fdch monitorem.</p>
|
||||
</div>
|
||||
|
||||
|
||||
+9
-9
@@ -198,7 +198,7 @@ def jobs_page(
|
||||
<td>{created_at}</td>
|
||||
<td>{started_at}</td>
|
||||
<td>{finished_at}</td>
|
||||
<td class="actions-cell"><a class="btn" href="/portal/jobs/{job_id}">Detail</a></td>
|
||||
<td class="actions-cell"><a class="btn" href="/portal/jobs/{job_id}"><i class="fa-solid fa-eye" aria-hidden="true"></i> Detail</a></td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
@@ -222,12 +222,12 @@ def jobs_page(
|
||||
return f"/portal/jobs?{urlencode(params)}"
|
||||
|
||||
previous_link = (
|
||||
f'<a class="btn btn-secondary" href="{page_url(page_number - 1)}">Předchozí</a>'
|
||||
f'<a class="btn btn-secondary" href="{page_url(page_number - 1)}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Předchozí</a>'
|
||||
if page_number > 1
|
||||
else ""
|
||||
)
|
||||
next_link = (
|
||||
f'<a class="btn btn-secondary" href="{page_url(page_number + 1)}">Další</a>'
|
||||
f'<a class="btn btn-secondary" href="{page_url(page_number + 1)}">Další <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>'
|
||||
if page_number < total_pages
|
||||
else ""
|
||||
)
|
||||
@@ -249,7 +249,7 @@ def jobs_page(
|
||||
f"""
|
||||
{refresh}
|
||||
<div class="card">
|
||||
<h2>Úlohy</h2>
|
||||
<h2><i class="fa-solid fa-list-check" aria-hidden="true"></i> Úlohy</h2>
|
||||
<p class="muted">Fronta portálových a webhook úloh připravená pro centrální worker.</p>
|
||||
</div>
|
||||
|
||||
@@ -267,8 +267,8 @@ def jobs_page(
|
||||
<select name="type">{type_options}</select>
|
||||
<input name="target" value="{target_value}" placeholder="Cíl">
|
||||
<input type="hidden" name="page" value="1">
|
||||
<button type="submit">Filtrovat</button>
|
||||
<a class="btn btn-secondary" href="/portal/jobs">Reset</a>
|
||||
<button type="submit"><i class="fa-solid fa-filter" aria-hidden="true"></i> Filtrovat</button>
|
||||
<a class="btn btn-secondary" href="/portal/jobs"><i class="fa-solid fa-arrow-rotate-left" aria-hidden="true"></i> Reset</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -335,7 +335,7 @@ def job_detail_page(job_id: int, request: Request, user=Depends(require_user)):
|
||||
if status_value == "failed" and can_retry_job(job):
|
||||
actions += f"""
|
||||
<form method="post" action="/portal/jobs/{html.escape(str(job_id))}/retry" onsubmit="return confirm('Retry job #{html.escape(str(job_id))}?');">
|
||||
<button type="submit">Spustit znovu</button>
|
||||
<button type="submit"><i class="fa-solid fa-rotate-right" aria-hidden="true"></i> Spustit znovu</button>
|
||||
</form>
|
||||
"""
|
||||
elif status_value == "failed" and (job.get("target_id") or "") in IGNORED_RETRY_REPOSITORIES:
|
||||
@@ -343,7 +343,7 @@ def job_detail_page(job_id: int, request: Request, user=Depends(require_user)):
|
||||
if status_value in {"queued", "running"}:
|
||||
actions += f"""
|
||||
<form method="post" action="/portal/jobs/{html.escape(str(job_id))}/cancel" onsubmit="return confirm('Cancel job #{html.escape(str(job_id))}?');">
|
||||
<button type="submit" class="danger">Zrušit úlohu</button>
|
||||
<button type="submit" class="danger"><i class="fa-solid fa-ban" aria-hidden="true"></i> Zrušit úlohu</button>
|
||||
</form>
|
||||
"""
|
||||
if actions:
|
||||
@@ -441,7 +441,7 @@ def job_detail_page(job_id: int, request: Request, user=Depends(require_user)):
|
||||
<h2>{title}</h2>
|
||||
<p>
|
||||
<a class="btn" href="/portal/jobs">← Zpět na úlohy</a>
|
||||
<a class="btn btn-secondary" href="{target_url}">Detail cíle</a>
|
||||
<a class="btn btn-secondary" href="{target_url}"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i> Detail cíle</a>
|
||||
</p>
|
||||
{retry_blocked_notice}
|
||||
{actions}
|
||||
|
||||
@@ -157,19 +157,19 @@ def migration_readiness_page(request: Request, user=Depends(require_user)):
|
||||
"Migration Readiness",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>Migration Readiness</h2>
|
||||
<h2><i class="fa-solid fa-diagram-project" aria-hidden="true"></i> Migration Readiness</h2>
|
||||
<p class="muted">Poslední výsledek maintenance skriptu {html.escape(PREFLIGHT_SCRIPT)} uložený v databázi. Stav READY / NOT READY vychází pouze z return code jobu.</p>
|
||||
<div class="inline-form">
|
||||
<form method="post" action="/portal/migration-readiness/run">
|
||||
<button type="submit">Spustit preflight kontrolu</button>
|
||||
<button type="submit"><i class="fa-solid fa-play" aria-hidden="true"></i> Spustit preflight kontrolu</button>
|
||||
</form>
|
||||
<form method="post" action="/portal/migration-readiness/bootstrap">
|
||||
<input type="hidden" name="mode" value="check">
|
||||
<button type="submit" class="btn-secondary">Run Bootstrap Check</button>
|
||||
<button type="submit" class="btn-secondary"><i class="fa-solid fa-clipboard-check" aria-hidden="true"></i> Run Bootstrap Check</button>
|
||||
</form>
|
||||
<form method="post" action="/portal/migration-readiness/bootstrap" onsubmit="return confirm('Spustit Bootstrap v2 repair? Tato akce opravuje práva přes maintenance job.');">
|
||||
<input type="hidden" name="mode" value="repair">
|
||||
<button type="submit" class="btn-secondary">Run Bootstrap Repair</button>
|
||||
<button type="submit" class="btn-secondary"><i class="fa-solid fa-screwdriver-wrench" aria-hidden="true"></i> Run Bootstrap Repair</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,7 +189,7 @@ def migration_readiness_page(request: Request, user=Depends(require_user)):
|
||||
<p>Režim deploy deployuje core services a může změnit stav systému. Spouštějte ho jen po kontrole dopadu.</p>
|
||||
<form method="post" action="/portal/migration-readiness/bootstrap" onsubmit="return confirm('Spustit plný Bootstrap v2 deploy? Tato akce deployuje core services přes maintenance job.');">
|
||||
<input type="hidden" name="mode" value="deploy">
|
||||
<button type="submit" class="danger">Run Bootstrap Deploy</button>
|
||||
<button type="submit" class="danger"><i class="fa-solid fa-rocket" aria-hidden="true"></i> Run Bootstrap Deploy</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ def operations_dashboard(request: Request, user=Depends(require_user)):
|
||||
"Přehled",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>Přehled systému</h2>
|
||||
<h2><i class="fa-solid fa-gauge-high" aria-hidden="true"></i> Přehled systému</h2>
|
||||
<p class="muted">Rychlá odpověď na otázku, zda jsou služby, úlohy a nasazení v pořádku.</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,10 +14,12 @@ from app.templates.layout import page
|
||||
router = APIRouter()
|
||||
|
||||
# Portál NEMÁ vlastní deploy logiku. Všechny provozní akce deleguje do existujících shell skriptů
|
||||
# v appfactory-tools přes existující job systém (job_type="run_script"; worker spustí scripts/<script>).
|
||||
DEPLOY_CORE_SCRIPT = "deploy-core-service.sh"
|
||||
CADDY_SCRIPT = "generate-caddyfile.sh"
|
||||
ENABLED_APPS_SCRIPT = "redeploy-enabled-apps.sh"
|
||||
# v appfactory-tools přes existující job systém (job_type="run_script").
|
||||
# Skripty leží v appfactory-tools/scripts/, proto je předáváme i s prefixem scripts/, jinak je
|
||||
# worker hledá ve špatné složce a job selže s "skript nenalezen".
|
||||
DEPLOY_CORE_SCRIPT = "scripts/deploy-core-service.sh"
|
||||
CADDY_SCRIPT = "scripts/generate-caddyfile.sh"
|
||||
ENABLED_APPS_SCRIPT = "scripts/redeploy-enabled-apps.sh"
|
||||
|
||||
# Skripty považované za "runtime operace" (filtr dashboardu).
|
||||
RUNTIME_SCRIPTS = (DEPLOY_CORE_SCRIPT, CADDY_SCRIPT, ENABLED_APPS_SCRIPT)
|
||||
|
||||
@@ -30,10 +30,10 @@ echo "TODO"
|
||||
"""
|
||||
SCHEDULE_TYPES = ("hourly", "daily", "weekly", "monthly")
|
||||
SCHEDULE_LABELS = {
|
||||
"hourly": "každou hodinu",
|
||||
"daily": "denně",
|
||||
"weekly": "týdně",
|
||||
"monthly": "měsíčně",
|
||||
"hourly": "každou hodinu",
|
||||
"daily": "denně",
|
||||
"weekly": "týdně",
|
||||
"monthly": "měsíčně",
|
||||
}
|
||||
|
||||
|
||||
@@ -70,18 +70,18 @@ def render_enabled_pill(value) -> str:
|
||||
|
||||
def render_running_pill(value) -> str:
|
||||
if value:
|
||||
return '<span class="pill pill-warning">běží</span>'
|
||||
return '<span class="pill pill-muted">neběží</span>'
|
||||
return '<span class="pill pill-warning">běží</span>'
|
||||
return '<span class="pill pill-muted">neběží</span>'
|
||||
|
||||
|
||||
def validate_script_name(value: str) -> str:
|
||||
script_name = clean_optional(value)
|
||||
if not script_name:
|
||||
raise HTTPException(status_code=400, detail="Název skriptu je povinný")
|
||||
raise HTTPException(status_code=400, detail="Název skriptu je povinný")
|
||||
if not script_name.endswith(".sh"):
|
||||
raise HTTPException(status_code=400, detail="Název skriptu musí končit .sh")
|
||||
raise HTTPException(status_code=400, detail="Název skriptu musí končit .sh")
|
||||
if "/" in script_name or "\\" in script_name or ".." in script_name:
|
||||
raise HTTPException(status_code=400, detail="Název skriptu nesmí obsahovat cestu")
|
||||
raise HTTPException(status_code=400, detail="Název skriptu nesmí obsahovat cestu")
|
||||
return script_name
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ def script_path(script_name: str) -> Path:
|
||||
try:
|
||||
path.relative_to(base)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Neplatný název skriptu")
|
||||
raise HTTPException(status_code=400, detail="Neplatný název skriptu")
|
||||
return path
|
||||
|
||||
|
||||
@@ -103,33 +103,33 @@ def read_script_file(script_name: str) -> dict:
|
||||
"exists": False,
|
||||
"content": DEFAULT_SCRIPT_CONTENT,
|
||||
"error": None,
|
||||
"warning": "Soubor zatím neexistuje. Můžete ho vytvořit z výchozího obsahu.",
|
||||
"warning": "Soubor zatím neexistuje. Můžete ho vytvořit z výchozího obsahu.",
|
||||
}
|
||||
if not path.is_file():
|
||||
return {"exists": False, "content": "", "error": "Cesta není soubor.", "warning": None}
|
||||
return {"exists": False, "content": "", "error": "Cesta není soubor.", "warning": None}
|
||||
try:
|
||||
if path.stat().st_size > MAX_SCRIPT_BYTES:
|
||||
return {"exists": True, "content": "", "error": "Soubor je větší než 100 KB.", "warning": None}
|
||||
return {"exists": True, "content": "", "error": "Soubor je větší než 100 KB.", "warning": None}
|
||||
content = path.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
return {"exists": True, "content": "", "error": "Soubor se nepodařilo načíst.", "warning": None}
|
||||
return {"exists": True, "content": "", "error": "Soubor se nepodařilo načíst.", "warning": None}
|
||||
|
||||
warning = None
|
||||
if not content.startswith("#!/usr/bin/env bash"):
|
||||
warning = "První řádek by měl být #!/usr/bin/env bash."
|
||||
warning = "První řádek by měl být #!/usr/bin/env bash."
|
||||
return {"exists": True, "content": content, "error": None, "warning": warning}
|
||||
|
||||
|
||||
def normalize_script_content(content: str) -> str:
|
||||
value = content.replace("\r\n", "\n").replace("\r", "\n")
|
||||
if not value.strip():
|
||||
raise HTTPException(status_code=400, detail="Obsah skriptu nesmí být prázdný")
|
||||
raise HTTPException(status_code=400, detail="Obsah skriptu nesmí být prázdný")
|
||||
if len(value.encode("utf-8")) > MAX_SCRIPT_BYTES:
|
||||
raise HTTPException(status_code=400, detail="Obsah skriptu je větší než 100 KB")
|
||||
raise HTTPException(status_code=400, detail="Obsah skriptu je větší než 100 KB")
|
||||
if not value.startswith("#!/usr/bin/env bash"):
|
||||
value = "#!/usr/bin/env bash\n" + value.lstrip("\n")
|
||||
if len(value.encode("utf-8")) > MAX_SCRIPT_BYTES:
|
||||
raise HTTPException(status_code=400, detail="Obsah skriptu je větší než 100 KB")
|
||||
raise HTTPException(status_code=400, detail="Obsah skriptu je větší než 100 KB")
|
||||
return value
|
||||
|
||||
|
||||
@@ -137,16 +137,23 @@ def save_script_file(script_name: str, content: str) -> None:
|
||||
path = script_path(script_name)
|
||||
value = normalize_script_content(content)
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(value, encoding="utf-8", newline="\n")
|
||||
except OSError as exc:
|
||||
reason = exc.strerror or str(exc)
|
||||
raise HTTPException(status_code=500, detail=f"Soubor se nepodařilo uložit: {reason}")
|
||||
# Spustitelná práva nastavujeme best-effort – soubor už je uložený, takže selhání chmod
|
||||
# (jiný vlastník, FS bez podpory práv) nesmí hlásit chybu uložení.
|
||||
try:
|
||||
os.chmod(path, 0o755)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Soubor se nepodařilo uložit")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def validate_schedule_type(value: str) -> str:
|
||||
schedule_type = clean_optional(value)
|
||||
if schedule_type not in SCHEDULE_TYPES:
|
||||
raise HTTPException(status_code=400, detail="Neplatná frekvence")
|
||||
raise HTTPException(status_code=400, detail="Neplatná frekvence")
|
||||
return schedule_type
|
||||
|
||||
|
||||
@@ -154,9 +161,9 @@ def validate_timeout(value: str | int) -> int:
|
||||
try:
|
||||
timeout_seconds = int(value)
|
||||
except (TypeError, ValueError):
|
||||
raise HTTPException(status_code=400, detail="Timeout musí být číslo")
|
||||
raise HTTPException(status_code=400, detail="Timeout musí být číslo")
|
||||
if timeout_seconds < 10 or timeout_seconds > 3600:
|
||||
raise HTTPException(status_code=400, detail="Timeout musí být mezi 10 a 3600 sekundami")
|
||||
raise HTTPException(status_code=400, detail="Timeout musí být mezi 10 a 3600 sekundami")
|
||||
return timeout_seconds
|
||||
|
||||
|
||||
@@ -170,7 +177,7 @@ def form_metadata(
|
||||
) -> dict:
|
||||
name_value = clean_optional(name)
|
||||
if not name_value:
|
||||
raise HTTPException(status_code=400, detail="Název je povinný")
|
||||
raise HTTPException(status_code=400, detail="Název je povinný")
|
||||
return {
|
||||
"name": name_value,
|
||||
"description": clean_optional(description),
|
||||
@@ -204,7 +211,7 @@ def render_form(script: dict | None, action: str, include_script_name: bool) ->
|
||||
|
||||
return f"""
|
||||
<form method="post" action="{action}" class="metadata-form">
|
||||
<label>Název</label>
|
||||
<label>Název</label>
|
||||
<input name="name" value="{name}" required>
|
||||
|
||||
<label>Popis</label>
|
||||
@@ -215,8 +222,8 @@ def render_form(script: dict | None, action: str, include_script_name: bool) ->
|
||||
<label>Frekvence</label>
|
||||
<select name="schedule_type">{render_schedule_options(schedule_type)}</select>
|
||||
|
||||
<label>Čas</label>
|
||||
<input name="schedule_time" value="{schedule_time}" placeholder="např. 02:30">
|
||||
<label>Čas</label>
|
||||
<input name="schedule_time" value="{schedule_time}" placeholder="např. 02:30">
|
||||
|
||||
<label>Timeout</label>
|
||||
<input name="timeout_seconds" value="{timeout_seconds}" inputmode="numeric" required>
|
||||
@@ -224,7 +231,7 @@ def render_form(script: dict | None, action: str, include_script_name: bool) ->
|
||||
<label class="checkbox-label"><input type="checkbox" name="is_enabled" value="1"{enabled_checked}> Zapnuto</label>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit">Uložit</button>
|
||||
<button type="submit"><i class="fa-solid fa-floppy-disk" aria-hidden="true"></i> Uložit</button>
|
||||
</div>
|
||||
</form>
|
||||
"""
|
||||
@@ -239,7 +246,7 @@ def render_script_content_section(script: dict, user: dict) -> str:
|
||||
script_file = {
|
||||
"exists": False,
|
||||
"content": "",
|
||||
"error": "Název skriptu není bezpečný.",
|
||||
"error": "Název skriptu není bezpečný.",
|
||||
"warning": None,
|
||||
}
|
||||
content = html.escape(script_file.get("content", "") or "")
|
||||
@@ -251,7 +258,7 @@ def render_script_content_section(script: dict, user: dict) -> str:
|
||||
error = f'<p class="alert alert-danger">{script_file["error"]}</p>'
|
||||
|
||||
if not is_admin(user):
|
||||
readonly_hint = '<p class="muted">Obsah skriptu je dostupný pouze pro čtení. Ukládat může jen administrátor.</p>'
|
||||
readonly_hint = '<p class="muted">Obsah skriptu je dostupný pouze pro čtení. Ukládat může jen administrátor.</p>'
|
||||
return f"""
|
||||
<div class="card">
|
||||
<h2>Obsah skriptu</h2>
|
||||
@@ -273,7 +280,7 @@ def render_script_content_section(script: dict, user: dict) -> str:
|
||||
<label>Obsah souboru</label>
|
||||
<textarea name="content" rows="22" spellcheck="false">{content}</textarea>
|
||||
<div class="form-actions">
|
||||
<button type="submit">Uložit skript</button>
|
||||
<button type="submit"><i class="fa-solid fa-floppy-disk" aria-hidden="true"></i> Uložit skript</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -295,12 +302,12 @@ def scheduled_scripts_page(request: Request, user=Depends(require_user)):
|
||||
last_run_at = html.escape(script.get("last_run_at", "") or "")
|
||||
next_run_at = html.escape(script.get("next_run_at", "") or "")
|
||||
enabled = bool(script.get("is_enabled"))
|
||||
toggle_label = "Zakázat" if enabled else "Povolit"
|
||||
toggle_label = "Zakázat" if enabled else "Povolit"
|
||||
delete_button = ""
|
||||
if not script.get("is_running"):
|
||||
delete_button = f"""
|
||||
<form method="post" action="/portal/scheduled-scripts/{script_id}/delete" class="inline-form" onsubmit="return confirm('Smazat plánovaný skript {name}?');">
|
||||
<button type="submit" class="danger">Smazat</button>
|
||||
<form method="post" action="/portal/scheduled-scripts/{script_id}/delete" class="inline-form" onsubmit="return confirm('Smazat plánovaný skript {name}?');">
|
||||
<button type="submit" class="danger"><i class="fa-solid fa-trash" aria-hidden="true"></i> Smazat</button>
|
||||
</form>
|
||||
"""
|
||||
|
||||
@@ -316,12 +323,12 @@ def scheduled_scripts_page(request: Request, user=Depends(require_user)):
|
||||
<td>{last_run_at}</td>
|
||||
<td>{next_run_at}</td>
|
||||
<td class="actions-cell service-actions">
|
||||
<a class="btn" href="/portal/scheduled-scripts/{script_id}">Detail</a>
|
||||
<a class="btn" href="/portal/scheduled-scripts/{script_id}"><i class="fa-solid fa-eye" aria-hidden="true"></i> Detail</a>
|
||||
<form method="post" action="/portal/scheduled-scripts/{script_id}/run-now" class="inline-form">
|
||||
<button type="submit" class="btn btn-secondary">Spustit nyní</button>
|
||||
<button type="submit" class="btn btn-secondary"><i class="fa-solid fa-play" aria-hidden="true"></i> Spustit nyní</button>
|
||||
</form>
|
||||
<form method="post" action="/portal/scheduled-scripts/{script_id}/toggle" class="inline-form">
|
||||
<button type="submit" class="btn btn-secondary">{toggle_label}</button>
|
||||
<button type="submit" class="btn btn-secondary"><i class="fa-solid fa-power-off" aria-hidden="true"></i> {toggle_label}</button>
|
||||
</form>
|
||||
{delete_button}
|
||||
</td>
|
||||
@@ -329,30 +336,30 @@ def scheduled_scripts_page(request: Request, user=Depends(require_user)):
|
||||
"""
|
||||
|
||||
if not rows:
|
||||
rows = '<tr><td colspan="10">Zatím nejsou evidované žádné plánované skripty.</td></tr>'
|
||||
rows = '<tr><td colspan="10">Zatím nejsou evidované žádné plánované skripty.</td></tr>'
|
||||
|
||||
return page(
|
||||
"Plánované skripty",
|
||||
"Plánované skripty",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-calendar-days" aria-hidden="true"></i> Plánované skripty</h2>
|
||||
<p class="muted">Přehled skriptů spouštěných schedulerem nebo ručně z portálu.</p>
|
||||
<p><a class="btn" href="/portal/scheduled-scripts/new">+ Nový plánovaný skript</a></p>
|
||||
<h2><i class="fa-solid fa-calendar-days" aria-hidden="true"></i> Plánované skripty</h2>
|
||||
<p class="muted">Přehled skriptů spouštěných schedulerem nebo ručně z portálu.</p>
|
||||
<p><a class="btn" href="/portal/scheduled-scripts/new"><i class="fa-solid fa-plus" aria-hidden="true"></i> Nový plánovaný skript</a></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Seznam</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Název</th>
|
||||
<th>Název</th>
|
||||
<th>Skript</th>
|
||||
<th>Frekvence</th>
|
||||
<th>Čas</th>
|
||||
<th>Čas</th>
|
||||
<th>Zapnuto</th>
|
||||
<th>Běží</th>
|
||||
<th>Poslední stav</th>
|
||||
<th>Poslední běh</th>
|
||||
<th>Další běh</th>
|
||||
<th>Běží</th>
|
||||
<th>Poslední stav</th>
|
||||
<th>Poslední běh</th>
|
||||
<th>Další běh</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
{rows}
|
||||
@@ -366,11 +373,11 @@ def scheduled_scripts_page(request: Request, user=Depends(require_user)):
|
||||
@router.get("/scheduled-scripts/new", response_class=HTMLResponse)
|
||||
def new_scheduled_script_form(request: Request, user=Depends(require_user)):
|
||||
return page(
|
||||
"Nový plánovaný skript",
|
||||
"Nový plánovaný skript",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-circle-plus" aria-hidden="true"></i> Nový plánovaný skript</h2>
|
||||
<p><a class="btn" href="/portal/scheduled-scripts">← Zpět</a></p>
|
||||
<h2><i class="fa-solid fa-circle-plus" aria-hidden="true"></i> Nový plánovaný skript</h2>
|
||||
<p><a class="btn" href="/portal/scheduled-scripts">← Zpět</a></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
{render_form({"is_enabled": True, "timeout_seconds": 300, "schedule_type": "daily"}, "/portal/scheduled-scripts/new", True)}
|
||||
@@ -397,7 +404,7 @@ def create_scheduled_script_action(
|
||||
created_name = metadata["script_name"]
|
||||
if not script_path(created_name).exists():
|
||||
save_script_file(created_name, DEFAULT_SCRIPT_CONTENT)
|
||||
commit_and_push(f"maintenance/{created_name}", f"Vytvořen skript {created_name}", user)
|
||||
commit_and_push(f"maintenance/{created_name}", f"Vytvořen skript {created_name}", user)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="scheduled_script.created",
|
||||
@@ -421,31 +428,31 @@ def scheduled_script_detail(script_id: int, request: Request, user=Depends(requi
|
||||
last_job = f'<a href="/portal/jobs/{last_job_html}">#{last_job_html}</a>'
|
||||
|
||||
enabled = bool(script.get("is_enabled"))
|
||||
toggle_label = "Zakázat" if enabled else "Povolit"
|
||||
toggle_label = "Zakázat" if enabled else "Povolit"
|
||||
delete_form = ""
|
||||
if not script.get("is_running"):
|
||||
delete_form = f"""
|
||||
<form method="post" action="/portal/scheduled-scripts/{script_id}/delete" onsubmit="return confirm('Smazat plánovaný skript?');">
|
||||
<button type="submit" class="danger">Smazat</button>
|
||||
<form method="post" action="/portal/scheduled-scripts/{script_id}/delete" onsubmit="return confirm('Smazat plánovaný skript?');">
|
||||
<button type="submit" class="danger"><i class="fa-solid fa-trash" aria-hidden="true"></i> Smazat</button>
|
||||
</form>
|
||||
"""
|
||||
script_content_section = render_script_content_section(script, user)
|
||||
|
||||
return page(
|
||||
html.escape(script.get("name", "") or "Plánovaný skript"),
|
||||
html.escape(script.get("name", "") or "Plánovaný skript"),
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-calendar-day" aria-hidden="true"></i> {html.escape(script.get("name", "") or "")}</h2>
|
||||
<p>
|
||||
<a class="btn" href="/portal/scheduled-scripts">← Zpět na plánované skripty</a>
|
||||
<a class="btn btn-secondary" href="/portal/scheduled-scripts/{script_id}/edit">Upravit</a>
|
||||
<a class="btn" href="/portal/scheduled-scripts">← Zpět na plánované skripty</a>
|
||||
<a class="btn btn-secondary" href="/portal/scheduled-scripts/{script_id}/edit"><i class="fa-solid fa-pen-to-square" aria-hidden="true"></i> Upravit</a>
|
||||
</p>
|
||||
<div class="inline-form">
|
||||
<form method="post" action="/portal/scheduled-scripts/{script_id}/run-now">
|
||||
<button type="submit">Spustit nyní</button>
|
||||
<button type="submit"><i class="fa-solid fa-play" aria-hidden="true"></i> Spustit nyní</button>
|
||||
</form>
|
||||
<form method="post" action="/portal/scheduled-scripts/{script_id}/toggle">
|
||||
<button type="submit" class="btn-secondary">{toggle_label}</button>
|
||||
<button type="submit" class="btn-secondary"><i class="fa-solid fa-power-off" aria-hidden="true"></i> {toggle_label}</button>
|
||||
</form>
|
||||
{delete_form}
|
||||
</div>
|
||||
@@ -454,19 +461,19 @@ def scheduled_script_detail(script_id: int, request: Request, user=Depends(requi
|
||||
<div class="card">
|
||||
<h2>Souhrn</h2>
|
||||
<table>
|
||||
<tr><th>Název</th><td>{html.escape(script.get("name", "") or "")}</td></tr>
|
||||
<tr><th>Název</th><td>{html.escape(script.get("name", "") or "")}</td></tr>
|
||||
<tr><th>Popis</th><td>{html.escape(script.get("description", "") or "")}</td></tr>
|
||||
<tr><th>Skript</th><td>{html.escape(script.get("script_name", "") or "")}</td></tr>
|
||||
<tr><th>Frekvence</th><td>{render_schedule_type(script.get("schedule_type"))}</td></tr>
|
||||
<tr><th>Čas</th><td>{html.escape(script.get("schedule_time", "") or "")}</td></tr>
|
||||
<tr><th>Čas</th><td>{html.escape(script.get("schedule_time", "") or "")}</td></tr>
|
||||
<tr><th>Timeout</th><td>{html.escape(str(script.get("timeout_seconds") or ""))}</td></tr>
|
||||
<tr><th>Zapnuto</th><td>{render_bool(script.get("is_enabled"))}</td></tr>
|
||||
<tr><th>Běží</th><td>{render_bool(script.get("is_running"))}</td></tr>
|
||||
<tr><th>Poslední stav</th><td>{render_status_pill(script.get("last_status")) if script.get("last_status") else ""}</td></tr>
|
||||
<tr><th>Poslední chyba</th><td>{html.escape(script.get("last_error_text", "") or "")}</td></tr>
|
||||
<tr><th>Poslední běh</th><td>{html.escape(script.get("last_run_at", "") or "")}</td></tr>
|
||||
<tr><th>Další běh</th><td>{html.escape(script.get("next_run_at", "") or "")}</td></tr>
|
||||
<tr><th>Poslední úloha</th><td>{last_job}</td></tr>
|
||||
<tr><th>Běží</th><td>{render_bool(script.get("is_running"))}</td></tr>
|
||||
<tr><th>Poslední stav</th><td>{render_status_pill(script.get("last_status")) if script.get("last_status") else ""}</td></tr>
|
||||
<tr><th>Poslední chyba</th><td>{html.escape(script.get("last_error_text", "") or "")}</td></tr>
|
||||
<tr><th>Poslední běh</th><td>{html.escape(script.get("last_run_at", "") or "")}</td></tr>
|
||||
<tr><th>Další běh</th><td>{html.escape(script.get("next_run_at", "") or "")}</td></tr>
|
||||
<tr><th>Poslední úloha</th><td>{last_job}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -482,11 +489,11 @@ def edit_scheduled_script_form(script_id: int, request: Request, user=Depends(re
|
||||
if not script:
|
||||
raise HTTPException(status_code=404, detail="Scheduled script not found")
|
||||
return page(
|
||||
"Upravit plánovaný skript",
|
||||
"Upravit plánovaný skript",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-pen-to-square" aria-hidden="true"></i> Upravit plánovaný skript</h2>
|
||||
<p><a class="btn" href="/portal/scheduled-scripts/{script_id}">← Zpět</a></p>
|
||||
<h2><i class="fa-solid fa-pen-to-square" aria-hidden="true"></i> Upravit plánovaný skript</h2>
|
||||
<p><a class="btn" href="/portal/scheduled-scripts/{script_id}">← Zpět</a></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
{render_form(script, f"/portal/scheduled-scripts/{script_id}/edit", False)}
|
||||
@@ -556,13 +563,13 @@ def update_scheduled_script_file(
|
||||
user=Depends(require_user),
|
||||
):
|
||||
if not is_admin(user):
|
||||
raise HTTPException(status_code=403, detail="Skript může upravit pouze administrátor")
|
||||
raise HTTPException(status_code=403, detail="Skript může upravit pouze administrátor")
|
||||
script = get_scheduled_script(script_id)
|
||||
if not script:
|
||||
raise HTTPException(status_code=404, detail="Scheduled script not found")
|
||||
script_name = validate_script_name(script.get("script_name", "") or "")
|
||||
save_script_file(script_name, content)
|
||||
commit_and_push(f"maintenance/{script_name}", f"Úprava skriptu {script_name}", user)
|
||||
commit_and_push(f"maintenance/{script_name}", f"Úprava skriptu {script_name}", user)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="scheduled_script.file_updated",
|
||||
@@ -599,7 +606,7 @@ def delete_scheduled_script_action(script_id: int, user=Depends(require_user)):
|
||||
if not script:
|
||||
raise HTTPException(status_code=404, detail="Scheduled script not found")
|
||||
if script.get("is_running"):
|
||||
raise HTTPException(status_code=409, detail="Běžící skript nelze smazat")
|
||||
raise HTTPException(status_code=409, detail="Běžící skript nelze smazat")
|
||||
if not delete_scheduled_script(script_id):
|
||||
raise HTTPException(status_code=409, detail="Skript nelze smazat")
|
||||
script_name = clean_optional(script.get("script_name"))
|
||||
@@ -607,7 +614,7 @@ def delete_scheduled_script_action(script_id: int, user=Depends(require_user)):
|
||||
path = script_path(script_name)
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
commit_and_push(f"maintenance/{script_name}", f"Smazán skript {script_name}", user)
|
||||
commit_and_push(f"maintenance/{script_name}", f"Smazán skript {script_name}", user)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="scheduled_script.deleted",
|
||||
|
||||
+7
-6
@@ -47,7 +47,8 @@ def render_role_overview() -> str:
|
||||
role_rows = (
|
||||
("admin", "Plný přístup: vše co developer + mazání služeb, správa záloh a jejich obnova, správa uživatelů a rolí, admin-only operace."),
|
||||
("developer", "Vidí běžící služby a dokumentaci jako viewer + provozní workflow služeb: Git/klonování, nasazení, proměnné, prostředky, metadata. Nemůže mazat služby ani do Administrace / Uživatelů."),
|
||||
("viewer", "Pouze pro čtení: vidí jaké služby běží a jejich dokumentaci (Swagger). Bez Gitu, úprav, nasazení a mazání. Bez přístupu do Administrace / Uživatelů. Výchozí role pro Google přihlášení."),
|
||||
("viewer", "Pouze pro čtení: vidí jaké služby běží a jejich dokumentaci (Swagger). Bez Gitu, úprav, nasazení a mazání. Bez přístupu do Administrace / Uživatelů."),
|
||||
("guest", "Výchozí role nového účtu po prvním přihlášení. Nemá přístup nikam – vidí jen výzvu, aby požádal administrátora o přidělení práv."),
|
||||
)
|
||||
rows = ""
|
||||
for role, description in role_rows:
|
||||
@@ -118,7 +119,7 @@ def users_page(request: Request, message: str = "", error: str = "", user=Depend
|
||||
<td>
|
||||
<form method="post" action="/portal/admin/users/{user_id_html}/role" class="inline-form">
|
||||
<select name="role">{render_role_options(role)}</select>
|
||||
<button type="submit" class="btn-secondary">Save</button>
|
||||
<button type="submit" class="btn-secondary"><i class="fa-solid fa-floppy-disk" aria-hidden="true"></i> Save</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>{render_enabled_pill(enabled)}</td>
|
||||
@@ -127,13 +128,13 @@ def users_page(request: Request, message: str = "", error: str = "", user=Depend
|
||||
<td>{last_login_at}</td>
|
||||
<td class="actions-cell">
|
||||
<form method="post" action="/portal/admin/users/{user_id_html}/{toggle_action}" class="inline-form">
|
||||
<button type="submit" class="btn-secondary">{toggle_label}</button>
|
||||
<button type="submit" class="btn-secondary"><i class="fa-solid fa-power-off" aria-hidden="true"></i> {toggle_label}</button>
|
||||
</form>
|
||||
<form method="post" action="/portal/admin/users/{user_id_html}/sync-gitea" class="inline-form">
|
||||
<button type="submit" class="btn-secondary">Sync Gitea</button>
|
||||
<button type="submit" class="btn-secondary"><i class="fa-solid fa-rotate" aria-hidden="true"></i> Sync Gitea</button>
|
||||
</form>
|
||||
<form method="post" action="/portal/admin/users/{user_id_html}/delete" class="inline-form" onsubmit="return confirm('Delete this user? This removes the portal account.');">
|
||||
<button type="submit" class="btn-secondary">Delete</button>
|
||||
<button type="submit" class="btn-secondary"><i class="fa-solid fa-trash" aria-hidden="true"></i> Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -146,7 +147,7 @@ def users_page(request: Request, message: str = "", error: str = "", user=Depend
|
||||
"Users",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>Users</h2>
|
||||
<h2><i class="fa-solid fa-users" aria-hidden="true"></i> Users</h2>
|
||||
<p class="muted">Administration of portal users and roles.</p>
|
||||
{notice}
|
||||
</div>
|
||||
|
||||
@@ -146,7 +146,7 @@ def workers_page(
|
||||
"Workery",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>Workery</h2>
|
||||
<h2><i class="fa-solid fa-gears" aria-hidden="true"></i> Workery</h2>
|
||||
<p class="muted">Přehled worker procesů obsluhujících úlohy portálu.</p>
|
||||
</div>
|
||||
|
||||
@@ -162,8 +162,8 @@ def workers_page(
|
||||
<select name="status">{status_options}</select>
|
||||
<input name="q" value="{html.escape(query)}" placeholder="ID workeru">
|
||||
<input type="hidden" name="page" value="1">
|
||||
<button type="submit">Filtrovat</button>
|
||||
<a class="btn btn-secondary" href="/portal/workers">Reset</a>
|
||||
<button type="submit"><i class="fa-solid fa-filter" aria-hidden="true"></i> Filtrovat</button>
|
||||
<a class="btn btn-secondary" href="/portal/workers"><i class="fa-solid fa-arrow-rotate-left" aria-hidden="true"></i> Reset</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user