Trying fixes.

This commit is contained in:
Michael Woods
2025-12-26 15:19:56 -05:00
parent e3213d9611
commit ac7569833a
5 changed files with 312 additions and 1 deletions

View File

@@ -0,0 +1,142 @@
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from typing import List, Optional, Union, Tuple
from pydantic import BaseModel
from datetime import datetime
import logging
from traceback import format_exc
from packetserver.http.dependencies import get_current_http_user
from packetserver.http.auth import HttpUser
from packetserver.http.database import DbDependency
from packetserver.server.jobs import Job, JobStatus
from packetserver.http.server import templates
router = APIRouter(prefix="/api/v1", tags=["jobs"])
dashboard_router = APIRouter(tags=["jobs"])
class JobSummary(BaseModel):
id: int
cmd: Union[str, List[str]]
owner: str
created_at: datetime
started_at: Optional[datetime] = None
finished_at: Optional[datetime] = None
status: str # JobStatus.name
return_code: int
class JobDetail(JobSummary):
output: str # base64-encoded
errors: str # base64-encoded
artifacts: List[Tuple[str, str]] # list of (filename, base64_data)
@router.get("/jobs", response_model=List[JobSummary])
async def list_user_jobs(
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()
user_jobs = Job.get_jobs_by_username(username, root)
# Sort newest first
user_jobs.sort(key=lambda j: j.created_at, reverse=True)
summaries = []
for j in user_jobs:
summaries.append(JobSummary(
id=j.id,
cmd=j.cmd,
owner=j.owner,
created_at=j.created_at,
started_at=j.started_at,
finished_at=j.finished_at,
status=j.status.name,
return_code=j.return_code
))
except Exception as e:
logging.error(f"Job list failed for {username}: {e}\n{format_exc()}")
raise HTTPException(status_code=500, detail="Failed to list jobs")
return summaries
@router.get("/jobs/{jid}", response_model=JobDetail)
async def get_job_detail(
jid: int,
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()
job = Job.get_job_by_id(jid, root)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
if job.owner != username:
raise HTTPException(status_code=403, detail="Not authorized to view this job")
job_dict = job.to_dict(include_data=True, binary_safe=True)
return JobDetail(
id=job.id,
cmd=job.cmd,
owner=job.owner,
created_at=job.created_at,
started_at=job.started_at,
finished_at=job.finished_at,
status=job.status.name,
return_code=job.return_code,
output=job_dict["output"],
errors=job_dict["errors"],
artifacts=job_dict["artifacts"]
)
except HTTPException:
raise
except Exception as e:
logging.error(f"Job detail failed for {username} on {jid}: {e}\n{format_exc()}")
raise HTTPException(status_code=500, detail="Failed to retrieve job")
return summaries
@dashboard_router.get("/jobs", response_class=HTMLResponse)
async def jobs_list_page(
request: Request,
db: DbDependency,
current_user: HttpUser = Depends(get_current_http_user)
):
jobs = await list_user_jobs(db=db, current_user=current_user)
return templates.TemplateResponse(
"jobs.html",
{
"request": request,
"current_user": current_user.username,
"jobs": jobs
}
)
@dashboard_router.get("/jobs/{jid}", response_class=HTMLResponse)
async def job_detail_page(
request: Request,
jid: int,
db: DbDependency,
current_user: HttpUser = Depends(get_current_http_user)
):
job = await get_job_detail(jid=jid, db=db, current_user=current_user)
return templates.TemplateResponse(
"job_detail.html",
{
"request": request,
"current_user": current_user.username,
"job": job
}
)

View File

@@ -44,6 +44,8 @@ from .routers.message_detail import router as message_detail_router
from .routers.messages import html_router from .routers.messages import html_router
from .routers.objects import router as objects_router from .routers.objects import router as objects_router
from .routers import objects_html from .routers import objects_html
from .routers.jobs import router as jobs_router
from .routers.jobs import dashboard_router as jobs_html_router
# initialize database # initialize database
init_db() init_db()
@@ -60,5 +62,6 @@ app.include_router(message_detail_router)
app.include_router(html_router) app.include_router(html_router)
app.include_router(objects_router) app.include_router(objects_router)
app.include_router(objects_html.router) app.include_router(objects_html.router)
app.include_router(jobs_router)
app.include_router(jobs_html_router)

View File

@@ -20,6 +20,7 @@
<a href="/messages" class="btn btn-outline-light btn-sm me-2">Messages</a> <a href="/messages" class="btn btn-outline-light btn-sm me-2">Messages</a>
<a href="/bulletins" class="btn btn-outline-light btn-sm me-2">Bulletins</a> <a href="/bulletins" class="btn btn-outline-light btn-sm me-2">Bulletins</a>
<a href="/objects" class="btn btn-outline-light btn-sm me-2">Objects</a> <a href="/objects" class="btn btn-outline-light btn-sm me-2">Objects</a>
<a href="/jobs" class="btn btn-outline-light btn-sm me-2">Jobs</a>
</div> </div>
</nav> </nav>

View File

@@ -0,0 +1,93 @@
{% extends "base.html" %}
{% block title %}Job {{ job.id }} - {{ job.cmd|truncate(50) }}{% endblock %}
{% block content %}
<h2>Job #{{ job.id }}</h2>
<div class="row">
<div class="col-lg-8">
<div class="card mb-4">
<div class="card-header">
Command
</div>
<div class="card-body">
<pre><code>{% if job.cmd is string %}{{ job.cmd }}{% else %}{{ job.cmd|join(' ') }}{% endif %}</code></pre>
</div>
</div>
{% if job.output or job.errors %}
<div class="card mb-4">
<div class="card-header">
Output & Errors
</div>
<ul class="nav nav-tabs card-header-tabs">
{% if job.output %}
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#output">Stdout</a>
</li>
{% endif %}
{% if job.errors %}
<li class="nav-item">
<a class="nav-link{% if not job.output %} active{% endif %}" data-bs-toggle="tab" href="#errors">Stderr</a>
</li>
{% endif %}
</ul>
<div class="card-body tab-content">
{% if job.output %}
<div class="tab-pane fade show active" id="output">
<pre><code>{{ job.output | b64decode | forceescape }}</code></pre>
</div>
{% endif %}
{% if job.errors %}
<div class="tab-pane fade{% if job.output %} show{% else %} show active{% endif %}" id="errors">
<pre><code>{{ job.errors | b64decode | forceescape }}</code></pre>
</div>
{% endif %}
</div>
</div>
{% endif %}
{% if job.artifacts %}
<div class="card">
<div class="card-header">Artifacts</div>
<div class="card-body">
<ul class="list-group">
{% for name, b64 in job.artifacts %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ name }}
<a href="data:application/octet-stream;base64,{{ b64 }}" download="{{ name }}" class="btn btn-sm btn-primary">Download</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div>
<div class="col-lg-4">
<div class="card">
<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>
{% endif %}
</li>
<li class="list-group-item"><strong>Owner:</strong> {{ job.owner }}</li>
<li class="list-group-item"><strong>Created:</strong> {{ job.created_at.strftime('%b %d, %Y %H:%M') }}</li>
<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>
</ul>
</div>
</div>
</div>
<p class="mt-4">
<a href="/jobs">← Back to Jobs</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,72 @@
{% extends "base.html" %}
{% block title %}My Jobs - {{ current_user }}{% endblock %}
{% block content %}
<h2 class="mb-4">My Jobs</h2>
{% if jobs %}
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Command</th>
<th>Status</th>
<th>Created</th>
<th>Started</th>
<th>Finished</th>
<th>Return Code</th>
</tr>
</thead>
<tbody>
{% for job in jobs %}
<tr class="clickable-row" style="cursor: pointer;" onclick="window.location='/jobs/{{ job.id }}'">
<td>{{ job.id }}</td>
<td>
<code>
{% if job.cmd is string %}
{{ job.cmd[:80] }}{% if job.cmd|length > 80 %}...{% endif %}
{% else %}
{{ job.cmd|join(' ')[:80] }}{% if job.cmd|join(' ')|length > 80 %}...{% endif %}
{% endif %}
</code>
</td>
<td>
{% 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>
{% endif %}
</td>
<td>{{ job.created_at.strftime('%b %d, %Y %H:%M') }}</td>
<td>{% if job.started_at %}{{ job.started_at.strftime('%H:%M') }}{% else %}-{% endif %}</td>
<td>{% if job.finished_at %}{{ job.finished_at.strftime('%H:%M') }}{% else %}-{% endif %}</td>
<td>
{% if job.return_code is not none %}
<code>{{ job.return_code }}</code>
{% else %}
-
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-info">
No jobs yet. When you run containerized tasks (e.g., packet processing scripts), they'll appear here.
</div>
{% endif %}
<style>
.clickable-row:hover {
background-color: rgba(0,123,255,0.1);
}
</style>
{% endblock %}