Změny:
Nový DB helper bez ORM: app/db/scheduled_scripts.py
Nové route/UI: app/routes/scheduled_scripts.py
Router registrovaný v app/main.py
Menu doplněné v app/templates/layout.py
Job filtr rozšířený o run_script v app/routes/jobs.py
Implementováno:
GET /portal/scheduled-scripts
GET /portal/scheduled-scripts/{id}
GET/POST /portal/scheduled-scripts/new
GET/POST /portal/scheduled-scripts/{id}/edit
POST /portal/scheduled-scripts/{id}/run-now
POST /portal/scheduled-scripts/{id}/toggle
POST /portal/scheduled-scripts/{id}/delete
Ruční spuštění vytváří run_script job přes existující create_job(...) se source="portal_manual" a payloadem podle zadání. Mazání je blokované, pokud is_running = 1. Validace script_name, schedule_type a timeout_seconds je v route vrstvě.
Audit eventy jsou přidané:
scheduled_script.created
scheduled_script.updated
scheduled_script.deleted
scheduled_script.enabled
scheduled_script.disabled
scheduled_script.run_now
This commit is contained in:
@@ -0,0 +1,172 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from app.db.database import get_connection
|
||||||
|
from app.db.migrations import run_migrations
|
||||||
|
|
||||||
|
|
||||||
|
def get_scheduled_scripts():
|
||||||
|
run_migrations()
|
||||||
|
con = get_connection()
|
||||||
|
|
||||||
|
rows = con.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
script_name,
|
||||||
|
schedule_type,
|
||||||
|
schedule_time,
|
||||||
|
timeout_seconds,
|
||||||
|
COALESCE(is_enabled, 1) AS is_enabled,
|
||||||
|
COALESCE(is_running, 0) AS is_running,
|
||||||
|
last_run_at,
|
||||||
|
last_status,
|
||||||
|
last_job_id,
|
||||||
|
last_error_text,
|
||||||
|
next_run_at,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM scheduled_scripts
|
||||||
|
ORDER BY name
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def get_scheduled_script(script_id: int):
|
||||||
|
run_migrations()
|
||||||
|
con = get_connection()
|
||||||
|
|
||||||
|
row = con.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
script_name,
|
||||||
|
schedule_type,
|
||||||
|
schedule_time,
|
||||||
|
timeout_seconds,
|
||||||
|
COALESCE(is_enabled, 1) AS is_enabled,
|
||||||
|
COALESCE(is_running, 0) AS is_running,
|
||||||
|
last_run_at,
|
||||||
|
last_status,
|
||||||
|
last_job_id,
|
||||||
|
last_error_text,
|
||||||
|
next_run_at,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM scheduled_scripts
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(script_id,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def create_scheduled_script(metadata: dict[str, Any]) -> int:
|
||||||
|
run_migrations()
|
||||||
|
con = get_connection()
|
||||||
|
|
||||||
|
cur = con.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO scheduled_scripts (
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
script_name,
|
||||||
|
schedule_type,
|
||||||
|
schedule_time,
|
||||||
|
timeout_seconds,
|
||||||
|
is_enabled,
|
||||||
|
is_running,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
metadata.get("name"),
|
||||||
|
metadata.get("description") or None,
|
||||||
|
metadata.get("script_name"),
|
||||||
|
metadata.get("schedule_type"),
|
||||||
|
metadata.get("schedule_time") or None,
|
||||||
|
metadata.get("timeout_seconds"),
|
||||||
|
1 if metadata.get("is_enabled") else 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
script_id = cur.lastrowid
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
return script_id
|
||||||
|
|
||||||
|
|
||||||
|
def update_scheduled_script(script_id: int, metadata: dict[str, Any]) -> None:
|
||||||
|
run_migrations()
|
||||||
|
con = get_connection()
|
||||||
|
|
||||||
|
con.execute(
|
||||||
|
"""
|
||||||
|
UPDATE scheduled_scripts
|
||||||
|
SET name = ?,
|
||||||
|
description = ?,
|
||||||
|
schedule_type = ?,
|
||||||
|
schedule_time = ?,
|
||||||
|
timeout_seconds = ?,
|
||||||
|
is_enabled = ?,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
metadata.get("name"),
|
||||||
|
metadata.get("description") or None,
|
||||||
|
metadata.get("schedule_type"),
|
||||||
|
metadata.get("schedule_time") or None,
|
||||||
|
metadata.get("timeout_seconds"),
|
||||||
|
1 if metadata.get("is_enabled") else 0,
|
||||||
|
script_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
|
||||||
|
def set_scheduled_script_enabled(script_id: int, is_enabled: bool) -> None:
|
||||||
|
run_migrations()
|
||||||
|
con = get_connection()
|
||||||
|
|
||||||
|
con.execute(
|
||||||
|
"""
|
||||||
|
UPDATE scheduled_scripts
|
||||||
|
SET is_enabled = ?,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(1 if is_enabled else 0, script_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_scheduled_script(script_id: int) -> bool:
|
||||||
|
run_migrations()
|
||||||
|
con = get_connection()
|
||||||
|
|
||||||
|
cur = con.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM scheduled_scripts
|
||||||
|
WHERE id = ?
|
||||||
|
AND COALESCE(is_running, 0) = 0
|
||||||
|
""",
|
||||||
|
(script_id,),
|
||||||
|
)
|
||||||
|
deleted = cur.rowcount == 1
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
return deleted
|
||||||
+2
-1
@@ -5,7 +5,7 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
|
||||||
from .config import read_env_value
|
from .config import read_env_value
|
||||||
from .routes import apps, audit, auth, backups, deployments, health, incidents, jobs, operations, workers
|
from .routes import apps, audit, auth, backups, deployments, health, incidents, jobs, operations, scheduled_scripts, workers
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
@@ -29,6 +29,7 @@ def create_app() -> FastAPI:
|
|||||||
app.include_router(deployments.router)
|
app.include_router(deployments.router)
|
||||||
app.include_router(incidents.router)
|
app.include_router(incidents.router)
|
||||||
app.include_router(operations.router)
|
app.include_router(operations.router)
|
||||||
|
app.include_router(scheduled_scripts.router)
|
||||||
app.include_router(jobs.router)
|
app.include_router(jobs.router)
|
||||||
app.include_router(workers.router)
|
app.include_router(workers.router)
|
||||||
app.include_router(audit.router)
|
app.include_router(audit.router)
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ LIVE_STATUSES = {"queued", "running", "cancelled_requested"}
|
|||||||
FINISHED_STATUSES = {"success", "failed", "cancelled"}
|
FINISHED_STATUSES = {"success", "failed", "cancelled"}
|
||||||
JOB_STATUSES = {"queued", "running", "cancelled_requested", "cancelled", "success", "failed"}
|
JOB_STATUSES = {"queued", "running", "cancelled_requested", "cancelled", "success", "failed"}
|
||||||
JOB_FILTER_STATUSES = ("queued", "running", "success", "failed", "cancelled")
|
JOB_FILTER_STATUSES = ("queued", "running", "success", "failed", "cancelled")
|
||||||
JOB_FILTER_TYPES = ("deploy_app", "deploy_core_service")
|
JOB_FILTER_TYPES = ("deploy_app", "deploy_core_service", "run_script")
|
||||||
IGNORED_RETRY_REPOSITORIES = {"appfactory-tools", "appfactory-infrastructure"}
|
IGNORED_RETRY_REPOSITORIES = {"appfactory-tools", "appfactory-infrastructure"}
|
||||||
DEFAULT_PAGE_SIZE = 20
|
DEFAULT_PAGE_SIZE = 20
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,456 @@
|
|||||||
|
import html
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||||
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
|
from app.auth import require_user
|
||||||
|
from app.db.audit import log_audit_event
|
||||||
|
from app.db.jobs import create_job
|
||||||
|
from app.db.scheduled_scripts import (
|
||||||
|
create_scheduled_script,
|
||||||
|
delete_scheduled_script,
|
||||||
|
get_scheduled_script,
|
||||||
|
get_scheduled_scripts,
|
||||||
|
set_scheduled_script_enabled,
|
||||||
|
update_scheduled_script,
|
||||||
|
)
|
||||||
|
from app.routes.deployments import render_status_pill
|
||||||
|
from app.templates.layout import page
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
SCHEDULE_TYPES = ("hourly", "daily", "weekly", "monthly")
|
||||||
|
SCHEDULE_LABELS = {
|
||||||
|
"hourly": "každou hodinu",
|
||||||
|
"daily": "denně",
|
||||||
|
"weekly": "týdně",
|
||||||
|
"monthly": "měsíčně",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def clean_optional(value: str | None) -> str:
|
||||||
|
return (value or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def render_bool(value) -> str:
|
||||||
|
return "Ano" if value else "Ne"
|
||||||
|
|
||||||
|
|
||||||
|
def render_schedule_type(value: str | None) -> str:
|
||||||
|
value = value or ""
|
||||||
|
return SCHEDULE_LABELS.get(value, html.escape(value))
|
||||||
|
|
||||||
|
|
||||||
|
def render_schedule_options(selected: str) -> str:
|
||||||
|
options = []
|
||||||
|
for value in SCHEDULE_TYPES:
|
||||||
|
selected_attr = " selected" if selected == value else ""
|
||||||
|
options.append(f'<option value="{value}"{selected_attr}>{render_schedule_type(value)}</option>')
|
||||||
|
return "".join(options)
|
||||||
|
|
||||||
|
|
||||||
|
def render_enabled_pill(value) -> str:
|
||||||
|
if value:
|
||||||
|
return '<span class="pill pill-success">zapnuto</span>'
|
||||||
|
return '<span class="pill pill-muted">vypnuto</span>'
|
||||||
|
|
||||||
|
|
||||||
|
def render_running_pill(value) -> str:
|
||||||
|
if value:
|
||||||
|
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ý")
|
||||||
|
if not script_name.endswith(".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")
|
||||||
|
return script_name
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
return schedule_type
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
if timeout_seconds < 10 or timeout_seconds > 3600:
|
||||||
|
raise HTTPException(status_code=400, detail="Timeout musí být mezi 10 a 3600 sekundami")
|
||||||
|
return timeout_seconds
|
||||||
|
|
||||||
|
|
||||||
|
def form_metadata(
|
||||||
|
name: str,
|
||||||
|
description: str,
|
||||||
|
schedule_type: str,
|
||||||
|
schedule_time: str,
|
||||||
|
timeout_seconds: str,
|
||||||
|
is_enabled: str | None,
|
||||||
|
) -> dict:
|
||||||
|
name_value = clean_optional(name)
|
||||||
|
if not name_value:
|
||||||
|
raise HTTPException(status_code=400, detail="Název je povinný")
|
||||||
|
return {
|
||||||
|
"name": name_value,
|
||||||
|
"description": clean_optional(description),
|
||||||
|
"schedule_type": validate_schedule_type(schedule_type),
|
||||||
|
"schedule_time": clean_optional(schedule_time),
|
||||||
|
"timeout_seconds": validate_timeout(timeout_seconds),
|
||||||
|
"is_enabled": bool(is_enabled),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def render_form(script: dict | None, action: str, include_script_name: bool) -> str:
|
||||||
|
script = script or {}
|
||||||
|
name = html.escape(script.get("name", "") or "")
|
||||||
|
description = html.escape(script.get("description", "") or "")
|
||||||
|
script_name = html.escape(script.get("script_name", "") or "")
|
||||||
|
schedule_type = script.get("schedule_type", "daily") or "daily"
|
||||||
|
schedule_time = html.escape(script.get("schedule_time", "") or "")
|
||||||
|
timeout_seconds = html.escape(str(script.get("timeout_seconds") or 300))
|
||||||
|
enabled_checked = " checked" if script.get("is_enabled", True) else ""
|
||||||
|
script_name_input = ""
|
||||||
|
if include_script_name:
|
||||||
|
script_name_input = f"""
|
||||||
|
<label>Skript</label>
|
||||||
|
<input name="script_name" value="{script_name}" placeholder="maintenance.sh" required>
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
script_name_input = f"""
|
||||||
|
<label>Skript</label>
|
||||||
|
<input value="{script_name}" disabled>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
<form method="post" action="{action}" class="metadata-form">
|
||||||
|
<label>Název</label>
|
||||||
|
<input name="name" value="{name}" required>
|
||||||
|
|
||||||
|
<label>Popis</label>
|
||||||
|
<textarea name="description" rows="3">{description}</textarea>
|
||||||
|
|
||||||
|
{script_name_input}
|
||||||
|
|
||||||
|
<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>Timeout</label>
|
||||||
|
<input name="timeout_seconds" value="{timeout_seconds}" inputmode="numeric" required>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/scheduled-scripts", response_class=HTMLResponse)
|
||||||
|
def scheduled_scripts_page(request: Request, user=Depends(require_user)):
|
||||||
|
scripts = get_scheduled_scripts()
|
||||||
|
rows = ""
|
||||||
|
for script in scripts:
|
||||||
|
script_id_raw = int(script.get("id"))
|
||||||
|
script_id = html.escape(str(script_id_raw))
|
||||||
|
name = html.escape(script.get("name", "") or "")
|
||||||
|
script_name = html.escape(script.get("script_name", "") or "")
|
||||||
|
schedule_type = render_schedule_type(script.get("schedule_type"))
|
||||||
|
schedule_time = html.escape(script.get("schedule_time", "") or "")
|
||||||
|
last_status = render_status_pill(script.get("last_status")) if script.get("last_status") else ""
|
||||||
|
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"
|
||||||
|
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>
|
||||||
|
"""
|
||||||
|
|
||||||
|
rows += f"""
|
||||||
|
<tr>
|
||||||
|
<td><strong><a href="/portal/scheduled-scripts/{script_id}">{name}</a></strong></td>
|
||||||
|
<td>{script_name}</td>
|
||||||
|
<td>{schedule_type}</td>
|
||||||
|
<td>{schedule_time}</td>
|
||||||
|
<td>{render_enabled_pill(enabled)}</td>
|
||||||
|
<td>{render_running_pill(script.get("is_running"))}</td>
|
||||||
|
<td>{last_status}</td>
|
||||||
|
<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>
|
||||||
|
<form method="post" action="/portal/scheduled-scripts/{script_id}/run-now" class="inline-form">
|
||||||
|
<button type="submit" class="btn btn-secondary">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>
|
||||||
|
</form>
|
||||||
|
{delete_button}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
rows = '<tr><td colspan="10">Zatím nejsou evidované žádné plánované skripty.</td></tr>'
|
||||||
|
|
||||||
|
return page(
|
||||||
|
"Plánované skripty",
|
||||||
|
f"""
|
||||||
|
<div class="card">
|
||||||
|
<h2>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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Seznam</h2>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Název</th>
|
||||||
|
<th>Skript</th>
|
||||||
|
<th>Frekvence</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>Akce</th>
|
||||||
|
</tr>
|
||||||
|
{rows}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
user=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",
|
||||||
|
f"""
|
||||||
|
<div class="card">
|
||||||
|
<h2>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)}
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/scheduled-scripts/new")
|
||||||
|
def create_scheduled_script_action(
|
||||||
|
name: str = Form(...),
|
||||||
|
description: str = Form(""),
|
||||||
|
script_name: str = Form(...),
|
||||||
|
schedule_type: str = Form(...),
|
||||||
|
schedule_time: str = Form(""),
|
||||||
|
timeout_seconds: str = Form("300"),
|
||||||
|
is_enabled: str | None = Form(None),
|
||||||
|
user=Depends(require_user),
|
||||||
|
):
|
||||||
|
metadata = form_metadata(name, description, schedule_type, schedule_time, timeout_seconds, is_enabled)
|
||||||
|
metadata["script_name"] = validate_script_name(script_name)
|
||||||
|
script_id = create_scheduled_script(metadata)
|
||||||
|
log_audit_event(
|
||||||
|
user,
|
||||||
|
action="scheduled_script.created",
|
||||||
|
target_type="scheduled_script",
|
||||||
|
target_id=script_id,
|
||||||
|
metadata={"script_name": metadata["script_name"]},
|
||||||
|
)
|
||||||
|
return RedirectResponse(url=f"/portal/scheduled-scripts/{script_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/scheduled-scripts/{script_id}", response_class=HTMLResponse)
|
||||||
|
def scheduled_script_detail(script_id: int, request: Request, user=Depends(require_user)):
|
||||||
|
script = get_scheduled_script(script_id)
|
||||||
|
if not script:
|
||||||
|
raise HTTPException(status_code=404, detail="Scheduled script not found")
|
||||||
|
|
||||||
|
last_job_id = script.get("last_job_id")
|
||||||
|
last_job = ""
|
||||||
|
if last_job_id:
|
||||||
|
last_job_html = html.escape(str(last_job_id))
|
||||||
|
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"
|
||||||
|
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>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return page(
|
||||||
|
html.escape(script.get("name", "") or "Plánovaný skript"),
|
||||||
|
f"""
|
||||||
|
<div class="card">
|
||||||
|
<h2>{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>
|
||||||
|
</p>
|
||||||
|
<div class="inline-form">
|
||||||
|
<form method="post" action="/portal/scheduled-scripts/{script_id}/run-now">
|
||||||
|
<button type="submit">Spustit nyní</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/portal/scheduled-scripts/{script_id}/toggle">
|
||||||
|
<button type="submit" class="btn-secondary">{toggle_label}</button>
|
||||||
|
</form>
|
||||||
|
{delete_form}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Souhrn</h2>
|
||||||
|
<table>
|
||||||
|
<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>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>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/scheduled-scripts/{script_id}/edit", response_class=HTMLResponse)
|
||||||
|
def edit_scheduled_script_form(script_id: int, request: Request, user=Depends(require_user)):
|
||||||
|
script = get_scheduled_script(script_id)
|
||||||
|
if not script:
|
||||||
|
raise HTTPException(status_code=404, detail="Scheduled script not found")
|
||||||
|
return page(
|
||||||
|
"Upravit plánovaný skript",
|
||||||
|
f"""
|
||||||
|
<div class="card">
|
||||||
|
<h2>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)}
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/scheduled-scripts/{script_id}/edit")
|
||||||
|
def update_scheduled_script_action(
|
||||||
|
script_id: int,
|
||||||
|
name: str = Form(...),
|
||||||
|
description: str = Form(""),
|
||||||
|
schedule_type: str = Form(...),
|
||||||
|
schedule_time: str = Form(""),
|
||||||
|
timeout_seconds: str = Form("300"),
|
||||||
|
is_enabled: str | None = Form(None),
|
||||||
|
user=Depends(require_user),
|
||||||
|
):
|
||||||
|
script = get_scheduled_script(script_id)
|
||||||
|
if not script:
|
||||||
|
raise HTTPException(status_code=404, detail="Scheduled script not found")
|
||||||
|
metadata = form_metadata(name, description, schedule_type, schedule_time, timeout_seconds, is_enabled)
|
||||||
|
update_scheduled_script(script_id, metadata)
|
||||||
|
log_audit_event(
|
||||||
|
user,
|
||||||
|
action="scheduled_script.updated",
|
||||||
|
target_type="scheduled_script",
|
||||||
|
target_id=script_id,
|
||||||
|
metadata={"script_name": script.get("script_name")},
|
||||||
|
)
|
||||||
|
return RedirectResponse(url=f"/portal/scheduled-scripts/{script_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/scheduled-scripts/{script_id}/run-now")
|
||||||
|
def run_scheduled_script_now(script_id: int, user=Depends(require_user)):
|
||||||
|
script = get_scheduled_script(script_id)
|
||||||
|
if not script:
|
||||||
|
raise HTTPException(status_code=404, detail="Scheduled script not found")
|
||||||
|
job_id = create_job(
|
||||||
|
job_type="run_script",
|
||||||
|
target_type="scheduled_script",
|
||||||
|
target_id=script.get("script_name"),
|
||||||
|
payload={
|
||||||
|
"scheduled_script_id": script.get("id"),
|
||||||
|
"script_name": script.get("script_name"),
|
||||||
|
"timeout_seconds": script.get("timeout_seconds"),
|
||||||
|
},
|
||||||
|
user=user,
|
||||||
|
source="portal_manual",
|
||||||
|
)
|
||||||
|
log_audit_event(
|
||||||
|
user,
|
||||||
|
action="scheduled_script.run_now",
|
||||||
|
target_type="scheduled_script",
|
||||||
|
target_id=script_id,
|
||||||
|
metadata={"script_name": script.get("script_name"), "job_id": job_id},
|
||||||
|
)
|
||||||
|
return RedirectResponse(url=f"/portal/jobs/{job_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/scheduled-scripts/{script_id}/toggle")
|
||||||
|
def toggle_scheduled_script(script_id: int, user=Depends(require_user)):
|
||||||
|
script = get_scheduled_script(script_id)
|
||||||
|
if not script:
|
||||||
|
raise HTTPException(status_code=404, detail="Scheduled script not found")
|
||||||
|
enabled = not bool(script.get("is_enabled"))
|
||||||
|
set_scheduled_script_enabled(script_id, enabled)
|
||||||
|
log_audit_event(
|
||||||
|
user,
|
||||||
|
action="scheduled_script.enabled" if enabled else "scheduled_script.disabled",
|
||||||
|
target_type="scheduled_script",
|
||||||
|
target_id=script_id,
|
||||||
|
metadata={"script_name": script.get("script_name")},
|
||||||
|
)
|
||||||
|
return RedirectResponse(url=f"/portal/scheduled-scripts/{script_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/scheduled-scripts/{script_id}/delete")
|
||||||
|
def delete_scheduled_script_action(script_id: int, user=Depends(require_user)):
|
||||||
|
script = get_scheduled_script(script_id)
|
||||||
|
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")
|
||||||
|
if not delete_scheduled_script(script_id):
|
||||||
|
raise HTTPException(status_code=409, detail="Skript nelze smazat")
|
||||||
|
log_audit_event(
|
||||||
|
user,
|
||||||
|
action="scheduled_script.deleted",
|
||||||
|
target_type="scheduled_script",
|
||||||
|
target_id=script_id,
|
||||||
|
metadata={"script_name": script.get("script_name")},
|
||||||
|
)
|
||||||
|
return RedirectResponse(url="/portal/scheduled-scripts", status_code=303)
|
||||||
@@ -20,6 +20,7 @@ def page(title: str, body: str, user=None) -> str:
|
|||||||
<button type="button" class="nav-menu-button">Provoz</button>
|
<button type="button" class="nav-menu-button">Provoz</button>
|
||||||
<div class="nav-menu-panel">
|
<div class="nav-menu-panel">
|
||||||
<a href="/portal/jobs">Úlohy</a>
|
<a href="/portal/jobs">Úlohy</a>
|
||||||
|
<a href="/portal/scheduled-scripts">Plánované skripty</a>
|
||||||
<a href="/portal/deployments">Nasazení</a>
|
<a href="/portal/deployments">Nasazení</a>
|
||||||
<a href="/portal/incidents">Incidenty</a>
|
<a href="/portal/incidents">Incidenty</a>
|
||||||
<a href="/portal/workers">Workery</a>
|
<a href="/portal/workers">Workery</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user