Jobs dashboard changes.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from fastapi.responses import HTMLResponse
|
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 pydantic import BaseModel
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
@@ -30,6 +30,13 @@ class JobDetail(JobSummary):
|
|||||||
errors: str # base64-encoded
|
errors: str # base64-encoded
|
||||||
artifacts: List[Tuple[str, str]] # list of (filename, base64_data)
|
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])
|
@router.get("/jobs", response_model=List[JobSummary])
|
||||||
async def list_user_jobs(
|
async def list_user_jobs(
|
||||||
db: DbDependency,
|
db: DbDependency,
|
||||||
@@ -140,3 +147,44 @@ async def job_detail_page(
|
|||||||
"job": job
|
"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.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import base64
|
||||||
|
|
||||||
from .database import init_db
|
from .database import init_db
|
||||||
from .routers import public, profile, messages, send
|
from .routers import public, profile, messages, send
|
||||||
@@ -21,6 +22,16 @@ app = FastAPI(
|
|||||||
# Define templates EARLY (before importing dashboard)
|
# Define templates EARLY (before importing dashboard)
|
||||||
templates = Jinja2Templates(directory=BASE_DIR / "templates")
|
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
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
def timestamp_to_date(ts):
|
def timestamp_to_date(ts):
|
||||||
|
|||||||
@@ -70,11 +70,19 @@
|
|||||||
<div class="card-header">Job Details</div>
|
<div class="card-header">Job Details</div>
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
<li class="list-group-item"><strong>Status:</strong>
|
<li class="list-group-item"><strong>Status:</strong>
|
||||||
{% if job.status == "QUEUED" %}<span class="badge bg-secondary">Queued</span>
|
{% set status = job.status | upper %}
|
||||||
{% elif job.status == "RUNNING" %}<span class="badge bg-primary">Running</span>
|
{% if status == "QUEUED" %}
|
||||||
{% elif job.status == "COMPLETED" %}<span class="badge bg-success">Completed</span>
|
<span class="badge bg-secondary">Queued</span>
|
||||||
{% elif job.status == "FAILED" %}<span class="badge bg-danger">Failed</span>
|
{% elif status == "RUNNING" %}
|
||||||
{% elif job.status == "CANCELLED" %}<span class="badge bg-warning">Cancelled</span>
|
<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 %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item"><strong>Owner:</strong> {{ job.owner }}</li>
|
<li class="list-group-item"><strong>Owner:</strong> {{ job.owner }}</li>
|
||||||
|
|||||||
@@ -25,23 +25,26 @@
|
|||||||
<td>
|
<td>
|
||||||
<code>
|
<code>
|
||||||
{% if job.cmd is string %}
|
{% if job.cmd is string %}
|
||||||
{{ job.cmd[:80] }}{% if job.cmd|length > 80 %}...{% endif %}
|
{{ job.cmd | truncate(80, True, '...') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ job.cmd|join(' ')[:80] }}{% if job.cmd|join(' ')|length > 80 %}...{% endif %}
|
{{ job.cmd | join(' ') | truncate(80, True, '...') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</code>
|
</code>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if job.status == "QUEUED" %}
|
{% set status = job.status | upper %}
|
||||||
|
{% if status == "QUEUED" %}
|
||||||
<span class="badge bg-secondary">Queued</span>
|
<span class="badge bg-secondary">Queued</span>
|
||||||
{% elif job.status == "RUNNING" %}
|
{% elif status == "RUNNING" %}
|
||||||
<span class="badge bg-primary">Running</span>
|
<span class="badge bg-primary">Running</span>
|
||||||
{% elif job.status == "COMPLETED" %}
|
{% elif status == "COMPLETED" %}
|
||||||
<span class="badge bg-success">Completed</span>
|
<span class="badge bg-success">Completed</span>
|
||||||
{% elif job.status == "FAILED" %}
|
{% elif status == "FAILED" %}
|
||||||
<span class="badge bg-danger">Failed</span>
|
<span class="badge bg-danger">Failed</span>
|
||||||
{% elif job.status == "CANCELLED" %}
|
{% elif status == "CANCELLED" %}
|
||||||
<span class="badge bg-warning">Cancelled</span>
|
<span class="badge bg-warning">Cancelled</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-light text-dark">{{ job.status }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ job.created_at.strftime('%b %d, %Y %H:%M') }}</td>
|
<td>{{ job.created_at.strftime('%b %d, %Y %H:%M') }}</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user