Jobs dashboard changes.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from typing import List, Optional, Union, Tuple
|
||||
from typing import List, Optional, Union, Tuple, Dict, Any
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
import logging
|
||||
@@ -30,6 +30,13 @@ class JobDetail(JobSummary):
|
||||
errors: str # base64-encoded
|
||||
artifacts: List[Tuple[str, str]] # list of (filename, base64_data)
|
||||
|
||||
class JobCreate(BaseModel):
|
||||
cmd: Union[str, List[str]]
|
||||
description: Optional[str] = None
|
||||
env: Optional[Dict[str, str]] = None
|
||||
workdir: Optional[str] = None
|
||||
artifact_paths: Optional[List[str]] = None
|
||||
|
||||
@router.get("/jobs", response_model=List[JobSummary])
|
||||
async def list_user_jobs(
|
||||
db: DbDependency,
|
||||
@@ -139,4 +146,45 @@ async def job_detail_page(
|
||||
"current_user": current_user.username,
|
||||
"job": job
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@router.post("/jobs", response_model=JobSummary, status_code=201)
|
||||
async def create_job(
|
||||
payload: JobCreate,
|
||||
db: DbDependency,
|
||||
current_user: HttpUser = Depends(get_current_http_user)
|
||||
):
|
||||
username = current_user.username.upper().strip()
|
||||
|
||||
try:
|
||||
with db.transaction() as conn:
|
||||
root = conn.root()
|
||||
|
||||
# Queue the job using existing method
|
||||
new_job = Job.queue(
|
||||
cmd=payload.cmd,
|
||||
owner=username,
|
||||
description=payload.description or "",
|
||||
env=payload.env or {},
|
||||
workdir=payload.workdir or "",
|
||||
artifact_paths=payload.artifact_paths or [],
|
||||
db=root # or root, depending on your queue() signature
|
||||
)
|
||||
|
||||
logging.info(f"User {username} queued job {new_job.id}: {payload.cmd}")
|
||||
|
||||
# Return summary (reuse the same format as list)
|
||||
return JobSummary(
|
||||
id=new_job.id,
|
||||
cmd=new_job.cmd,
|
||||
owner=new_job.owner,
|
||||
created_at=new_job.created_at,
|
||||
started_at=new_job.started_at,
|
||||
finished_at=new_job.finished_at,
|
||||
status=new_job.status.name,
|
||||
return_code=new_job.return_code
|
||||
)
|
||||
|
||||
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")
|
||||
@@ -3,6 +3,7 @@ from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pathlib import Path
|
||||
import base64
|
||||
|
||||
from .database import init_db
|
||||
from .routers import public, profile, messages, send
|
||||
@@ -21,6 +22,16 @@ app = FastAPI(
|
||||
# Define templates EARLY (before importing dashboard)
|
||||
templates = Jinja2Templates(directory=BASE_DIR / "templates")
|
||||
|
||||
def b64decode_filter(value: str) -> str:
|
||||
try:
|
||||
decoded_bytes = base64.b64decode(value)
|
||||
# Assume UTF-8 text (common for job output/errors)
|
||||
return decoded_bytes.decode('utf-8', errors='replace')
|
||||
except Exception:
|
||||
return "[Invalid base64 data]"
|
||||
|
||||
templates.env.filters["b64decode"] = b64decode_filter
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
def timestamp_to_date(ts):
|
||||
|
||||
@@ -70,11 +70,19 @@
|
||||
<div class="card-header">Job Details</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item"><strong>Status:</strong>
|
||||
{% if job.status == "QUEUED" %}<span class="badge bg-secondary">Queued</span>
|
||||
{% elif job.status == "RUNNING" %}<span class="badge bg-primary">Running</span>
|
||||
{% elif job.status == "COMPLETED" %}<span class="badge bg-success">Completed</span>
|
||||
{% elif job.status == "FAILED" %}<span class="badge bg-danger">Failed</span>
|
||||
{% elif job.status == "CANCELLED" %}<span class="badge bg-warning">Cancelled</span>
|
||||
{% set status = job.status | upper %}
|
||||
{% if status == "QUEUED" %}
|
||||
<span class="badge bg-secondary">Queued</span>
|
||||
{% elif status == "RUNNING" %}
|
||||
<span class="badge bg-primary">Running</span>
|
||||
{% elif status == "COMPLETED" %}
|
||||
<span class="badge bg-success">Completed</span>
|
||||
{% elif status == "FAILED" %}
|
||||
<span class="badge bg-danger">Failed</span>
|
||||
{% elif status == "CANCELLED" %}
|
||||
<span class="badge bg-warning">Cancelled</span>
|
||||
{% else %}
|
||||
<span class="badge bg-light text-dark">{{ job.status }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="list-group-item"><strong>Owner:</strong> {{ job.owner }}</li>
|
||||
|
||||
@@ -25,23 +25,26 @@
|
||||
<td>
|
||||
<code>
|
||||
{% if job.cmd is string %}
|
||||
{{ job.cmd[:80] }}{% if job.cmd|length > 80 %}...{% endif %}
|
||||
{{ job.cmd | truncate(80, True, '...') }}
|
||||
{% else %}
|
||||
{{ job.cmd|join(' ')[:80] }}{% if job.cmd|join(' ')|length > 80 %}...{% endif %}
|
||||
{{ job.cmd | join(' ') | truncate(80, True, '...') }}
|
||||
{% endif %}
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
{% if job.status == "QUEUED" %}
|
||||
{% set status = job.status | upper %}
|
||||
{% if status == "QUEUED" %}
|
||||
<span class="badge bg-secondary">Queued</span>
|
||||
{% elif job.status == "RUNNING" %}
|
||||
{% elif status == "RUNNING" %}
|
||||
<span class="badge bg-primary">Running</span>
|
||||
{% elif job.status == "COMPLETED" %}
|
||||
{% elif status == "COMPLETED" %}
|
||||
<span class="badge bg-success">Completed</span>
|
||||
{% elif job.status == "FAILED" %}
|
||||
{% elif status == "FAILED" %}
|
||||
<span class="badge bg-danger">Failed</span>
|
||||
{% elif job.status == "CANCELLED" %}
|
||||
{% elif status == "CANCELLED" %}
|
||||
<span class="badge bg-warning">Cancelled</span>
|
||||
{% else %}
|
||||
<span class="badge bg-light text-dark">{{ job.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ job.created_at.strftime('%b %d, %Y %H:%M') }}</td>
|
||||
|
||||
Reference in New Issue
Block a user