feat: implement server-side pagination for job queue and history views
Build / Build and Push (push) Successful in 13s

This commit is contained in:
2026-06-06 12:20:36 +01:00
parent 58f760f41d
commit 8d8649f531
4 changed files with 112 additions and 14 deletions
+27
View File
@@ -79,6 +79,33 @@ func GetPendingJobs() ([]Job, error) {
return jobs, nil
}
func GetJobsPaginated(limit, offset int) ([]Job, int, error) {
var total int
if err := DB.QueryRow(`SELECT COUNT(*) FROM jobs`).Scan(&total); err != nil {
return nil, 0, err
}
rows, err := DB.Query(`SELECT id, file_path, media_type, status, priority, original_size, target_resolution, ffmpeg_flags, error_message, created_at, updated_at FROM jobs ORDER BY priority DESC, created_at ASC LIMIT ? OFFSET ?`, limit, offset)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var jobs []Job
for rows.Next() {
var j Job
var targetRes, ffmpegFlags, errMsg sql.NullString
if err := rows.Scan(&j.ID, &j.FilePath, &j.MediaType, &j.Status, &j.Priority, &j.OriginalSize, &targetRes, &ffmpegFlags, &errMsg, &j.CreatedAt, &j.UpdatedAt); err != nil {
return nil, 0, err
}
if targetRes.Valid { j.TargetResolution = targetRes.String }
if ffmpegFlags.Valid { j.FFmpegFlags = ffmpegFlags.String }
if errMsg.Valid { j.ErrorMessage = errMsg.String }
jobs = append(jobs, j)
}
return jobs, total, nil
}
func UpdateJobStatus(id int, status, errMsg string) error {
_, err := DB.Exec(`UPDATE jobs SET status = ?, error_message = ? WHERE id = ?`, status, nullStr(errMsg), id)
return err
+31 -2
View File
@@ -10,13 +10,42 @@ import (
)
func (s *Server) handleGetQueue(w http.ResponseWriter, r *http.Request) {
jobs, err := db.GetPendingJobs()
limitStr := r.URL.Query().Get("limit")
pageStr := r.URL.Query().Get("page")
limit := 10
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
limit = l
}
page := 1
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
offset := (page - 1) * limit
jobs, total, err := db.GetJobsPaginated(limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
totalPages := (total + limit - 1) / limit
if totalPages == 0 {
totalPages = 1
}
response := map[string]interface{}{
"jobs": jobs,
"total": total,
"page": page,
"totalPages": totalPages,
"limit": limit,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(jobs)
json.NewEncoder(w).Encode(response)
}
func (s *Server) handleBumpJob(w http.ResponseWriter, r *http.Request) {
+38 -3
View File
@@ -33,15 +33,50 @@
</tbody>
</table>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem;">
<div>
<label for="limit-select" style="color: var(--text-secondary); font-size: 0.875rem; margin-right: 0.5rem;">Rows per page:</label>
<select id="limit-select" onchange="changeLimit()" style="background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); border-radius: 4px; padding: 0.25rem 0.5rem;">
<option value="10">10</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<button id="prev-page" class="btn btn-sm btn-outline" onclick="changePage(-1)">Previous</button>
<span id="page-info" style="color: var(--text-secondary); font-size: 0.875rem;">Page 1 of 1</span>
<button id="next-page" class="btn btn-sm btn-outline" onclick="changePage(1)">Next</button>
</div>
</div>
</div>
<script>
const queueBody = document.getElementById('queue-body');
let currentPage = 1;
let currentLimit = 10;
async function loadQueue() {
const res = await fetch('/api/queue');
const jobs = await res.json();
renderQueue(jobs || []);
const res = await fetch(`/api/queue?page=${currentPage}&limit=${currentLimit}`);
const data = await res.json();
renderQueue(data.jobs || []);
document.getElementById('page-info').innerText = `Page ${data.page} of ${data.totalPages}`;
document.getElementById('prev-page').disabled = data.page <= 1;
document.getElementById('next-page').disabled = data.page >= data.totalPages;
currentPage = data.page;
}
function changeLimit() {
currentLimit = document.getElementById('limit-select').value;
currentPage = 1;
loadQueue();
}
function changePage(delta) {
currentPage += delta;
loadQueue();
}
function renderQueue(jobs) {
+16 -9
View File
@@ -55,21 +55,28 @@
</tbody>
</table>
</div>
<div class="pagination">
<span style="color: var(--text-secondary); font-size: 0.875rem;">
Page {{.CurrentPage}} of {{.TotalPages}}
</span>
<div class="pagination-controls">
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem;">
<div>
<label for="limit-select" style="color: var(--text-secondary); font-size: 0.875rem; margin-right: 0.5rem;">Rows per page:</label>
<select id="limit-select" onchange="window.location.href='?limit=' + this.value + '&status={{.FilterStatus}}'" style="background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); border-radius: 4px; padding: 0.25rem 0.5rem;">
<option value="10" {{if eq .Limit 10}}selected{{end}}>10</option>
<option value="50" {{if eq .Limit 50}}selected{{end}}>50</option>
<option value="100" {{if eq .Limit 100}}selected{{end}}>100</option>
</select>
</div>
<div style="display: flex; gap: 0.5rem; align-items: center;">
{{if .HasPrev}}
<a href="?page={{.PrevPage}}&status={{.FilterStatus}}" class="page-btn">Previous</a>
<a href="?page={{.PrevPage}}&limit={{.Limit}}&status={{.FilterStatus}}" class="btn btn-sm btn-outline" style="text-decoration: none;">Previous</a>
{{else}}
<span class="page-btn disabled">Previous</span>
<button class="btn btn-sm btn-outline" disabled>Previous</button>
{{end}}
<span style="color: var(--text-secondary); font-size: 0.875rem;">Page {{.CurrentPage}} of {{.TotalPages}}</span>
{{if .HasNext}}
<a href="?page={{.NextPage}}&status={{.FilterStatus}}" class="page-btn">Next</a>
<a href="?page={{.NextPage}}&limit={{.Limit}}&status={{.FilterStatus}}" class="btn btn-sm btn-outline" style="text-decoration: none;">Next</a>
{{else}}
<span class="page-btn disabled">Next</span>
<button class="btn btn-sm btn-outline" disabled>Next</button>
{{end}}
</div>
</div>