Jobs dashboard additions.

This commit is contained in:
Michael Woods
2025-12-26 16:03:06 -05:00
parent ec0cb0ce45
commit c060ddb060
4 changed files with 136 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi import APIRouter, Depends, HTTPException, Request, Form, UploadFile, File
from fastapi.responses import HTMLResponse, RedirectResponse
from typing import List, Optional, Union, Tuple, Dict, Any
from pydantic import BaseModel
from datetime import datetime
@@ -26,11 +26,13 @@ class JobSummary(BaseModel):
finished_at: Optional[datetime] = None
status: str # JobStatus.name
return_code: int
env: Dict[str, str] = {}
class JobDetail(JobSummary):
output: str # base64-encoded
errors: str # base64-encoded
artifacts: List[Tuple[str, str]] # list of (filename, base64_data)
env: Dict[str, str] = {}
from typing import Dict
@@ -64,7 +66,8 @@ async def list_user_jobs(
started_at=j.started_at,
finished_at=j.finished_at,
status=j.status.name,
return_code=j.return_code
return_code=j.return_code,
env=j.env
))
except Exception as e:
@@ -104,7 +107,8 @@ async def get_job_detail(
return_code=job.return_code,
output=job_dict["output"],
errors=job_dict["errors"],
artifacts=job_dict["artifacts"]
artifacts=job_dict["artifacts"],
env=job_dict.get("env", {})
)
except HTTPException:
@@ -200,4 +204,58 @@ async def create_job(
raise
except Exception as e:
logging.error(f"Job creation failed for {username}: {e}\n{format_exc()}")
raise HTTPException(status_code=500, detail="Failed to queue job")
raise HTTPException(status_code=500, detail="Failed to queue job")
@dashboard_router.get("/jobs/new", response_class=HTMLResponse)
async def new_job_form(
request: Request,
current_user: HttpUser = Depends(get_current_http_user)
):
return templates.TemplateResponse(
"job_new.html",
{
"request": request,
"current_user": current_user.username
}
)
@dashboard_router.post("/jobs/new")
async def create_job_from_form(
db: DbDependency,
request: Request,
cmd: str = Form(...),
env_keys: List[str] = Form(default=[]),
env_values: List[str] = Form(default=[]),
files: List[UploadFile] = File(default=[]),
current_user: HttpUser = Depends(get_current_http_user)
):
# Build env dict from parallel lists
env = {}
for k, v in zip(env_keys, env_values):
if k.strip():
env[k.strip()] = v.strip()
# Build files dict for API (filename → base64)
files_dict = {}
for upload in files:
if upload.filename:
content = await upload.read()
files_dict[upload.filename] = base64.b64encode(content).decode('ascii')
# Prepare payload for the existing API
payload = {
"cmd": [part.strip() for part in cmd.split() if part.strip()], # split on whitespace, like shell
"env": env if env else None,
"files": files_dict if files_dict else None
}
# Call the API internally
from packetserver.http.routers.jobs import create_job as api_create_job
response = await api_create_job(
payload=JobCreate(**{k: v for k, v in payload.items() if v is not None}),
db=db,
current_user=current_user
)
# Redirect to the new job detail page
return RedirectResponse(url=f"/jobs/{response.id}", status_code=303)

View File

@@ -90,6 +90,18 @@
<li class="list-group-item"><strong>Started:</strong> {% if job.started_at %}{{ job.started_at.strftime('%H:%M:%S') }}{% else %}-{% endif %}</li>
<li class="list-group-item"><strong>Finished:</strong> {% if job.finished_at %}{{ job.finished_at.strftime('%H:%M:%S') }}{% else %}-{% endif %}</li>
<li class="list-group-item"><strong>Return Code:</strong> {% if job.return_code is not none %}{{ job.return_code }}{% else %}-{% endif %}</li>
<li class="list-group-item">
<strong>Environment Variables:</strong>
{% if job.env %}
<ul class="list-unstyled mt-2 mb-0">
{% for key, value in job.env.items() %}
<li><code>{{ key }}={{ value }}</code></li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block title %}New Job - {{ current_user }}{% endblock %}
{% block content %}
<h2 class="mb-4">Queue New Job</h2>
<div class="card">
<div class="card-body">
<form method="post" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label">Command</label>
<textarea name="cmd" class="form-control" rows="3" placeholder="e.g. python script.py --input data.txt" required></textarea>
<div class="form-text">Space-separated command and arguments (like a shell command).</div>
</div>
<div class="mb-3">
<label class="form-label">Environment Variables</label>
<div id="env-fields">
<div class="row mb-2 env-row">
<div class="col"><input type="text" name="env_keys" class="form-control" placeholder="KEY"></div>
<div class="col"><input type="text" name="env_values" class="form-control" placeholder="value"></div>
<div class="col-auto"><button type="button" class="btn btn-outline-danger btn-sm" onclick="this.parentElement.parentElement.remove()">Remove</button></div>
</div>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="addEnvRow()">+ Add Env Var</button>
</div>
<div class="mb-3">
<label class="form-label">Files (scripts, config, data)</label>
<input type="file" name="files" class="form-control" multiple>
<div class="form-text">Uploaded files will be available in the container's working directory.</div>
</div>
<button type="submit" class="btn btn-primary me-2">Queue Job</button>
<a href="/jobs" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
<script>
function addEnvRow() {
const container = document.getElementById('env-fields');
const row = document.createElement('div');
row.className = 'row mb-2 env-row';
row.innerHTML = `
<div class="col"><input type="text" name="env_keys" class="form-control" placeholder="KEY"></div>
<div class="col"><input type="text" name="env_values" class="form-control" placeholder="value"></div>
<div class="col-auto"><button type="button" class="btn btn-outline-danger btn-sm" onclick="this.parentElement.parentElement.remove()">Remove</button></div>
`;
container.appendChild(row);
}
</script>
{% endblock %}

View File

@@ -3,7 +3,13 @@
{% block title %}My Jobs - {{ current_user }}{% endblock %}
{% block content %}
<h2 class="mb-4">My Jobs</h2>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">My Jobs</h2>
<a href="/jobs/new" class="btn btn-success">
<i class="bi bi-plus-lg"></i> New Job
</a>
</div>
{% if jobs %}
<table class="table table-striped table-hover">