ui: per-host Jobs sub-tab; drop unused Settings stub

Adds /hosts/{id}/jobs page listing recent jobs for the host (newest
first, capped at 100) with click-through to /jobs/{id}. Converts the
Jobs placeholder <div> to a real <a> nav link; removes the Settings
stub entirely. Also registers durationHuman template func and a
.jobs-row CSS grid to match the existing .schd-row idiom.
This commit is contained in:
2026-05-07 22:49:10 +01:00
parent 7b390e9e5e
commit 42eeabea9a
10 changed files with 411 additions and 3 deletions
+81
View File
@@ -288,6 +288,87 @@ func (s *Store) HasJobOfKind(ctx context.Context, hostID, kind string) (bool, er
return n > 0, nil
}
// ListJobsByHost returns recent jobs for hostID, ordered by
// created_at DESC, limited to at most `limit` rows. limit ≤ 0 is
// treated as no limit.
func (s *Store) ListJobsByHost(ctx context.Context, hostID string, limit int) ([]Job, error) {
q := `SELECT id, host_id, kind, status, scheduled_id, source_group_id,
actor_kind, actor_id, started_at, finished_at, exit_code,
stats, error, created_at
FROM jobs
WHERE host_id = ?
ORDER BY created_at DESC`
args := []any{hostID}
if limit > 0 {
q += ` LIMIT ?`
args = append(args, limit)
}
rows, err := s.db.QueryContext(ctx, q, args...)
if err != nil {
return nil, fmt.Errorf("store: list jobs by host: %w", err)
}
defer func() { _ = rows.Close() }()
var out []Job
for rows.Next() {
var (
j Job
schedID sql.NullString
groupID sql.NullString
actorID sql.NullString
startedAt sql.NullString
finishedAt sql.NullString
exitCode sql.NullInt64
stats sql.NullString
errMsg sql.NullString
createdAt string
)
if err := rows.Scan(&j.ID, &j.HostID, &j.Kind, &j.Status, &schedID, &groupID,
&j.ActorKind, &actorID, &startedAt, &finishedAt,
&exitCode, &stats, &errMsg, &createdAt); err != nil {
return nil, fmt.Errorf("store: scan job row: %w", err)
}
if schedID.Valid {
v := schedID.String
j.ScheduledID = &v
}
if groupID.Valid {
v := groupID.String
j.SourceGroupID = &v
}
if actorID.Valid {
v := actorID.String
j.ActorID = &v
}
if startedAt.Valid {
t, _ := time.Parse(time.RFC3339Nano, startedAt.String)
j.StartedAt = &t
}
if finishedAt.Valid {
t, _ := time.Parse(time.RFC3339Nano, finishedAt.String)
j.FinishedAt = &t
}
if exitCode.Valid {
i := int(exitCode.Int64)
j.ExitCode = &i
}
if stats.Valid && stats.String != "" {
j.Stats = json.RawMessage(stats.String)
}
if errMsg.Valid {
v := errMsg.String
j.Error = &v
}
t, _ := time.Parse(time.RFC3339Nano, createdAt)
j.CreatedAt = t
out = append(out, j)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("store: iterate jobs by host: %w", err)
}
return out, nil
}
func nullableStr(s string) any {
if s == "" {
return nil