feat: add server-side pagination and status filtering to the job history page
Build / Build and Push (push) Successful in 52s

This commit is contained in:
2026-06-06 10:47:39 +01:00
parent 10134f2904
commit 58f760f41d
5 changed files with 181 additions and 17 deletions
+20 -3
View File
@@ -124,13 +124,30 @@ func AddJobReport(j Job, status string, encodedSize int64, sizeSaved int64, proc
return err
}
func GetJobReports(limit, offset int) ([]JobReport, int, error) {
func GetJobReports(limit, offset int, statusFilter string) ([]JobReport, int, error) {
var total int
if err := DB.QueryRow(`SELECT COUNT(*) FROM job_reports`).Scan(&total); err != nil {
queryArgs := []interface{}{}
countQuery := `SELECT COUNT(*) FROM job_reports`
selectQuery := `SELECT id, file_path, media_type, status, original_size, encoded_size, size_saved, processing_time, target_resolution, ffmpeg_flags, error_message, created_at FROM job_reports`
whereClause := ""
if statusFilter != "" && statusFilter != "all" {
if statusFilter == "exclude_skipped" {
whereClause = ` WHERE status != 'skipped'`
} else {
whereClause = ` WHERE status = ?`
queryArgs = append(queryArgs, statusFilter)
}
}
if err := DB.QueryRow(countQuery+whereClause, queryArgs...).Scan(&total); err != nil {
return nil, 0, err
}
rows, err := DB.Query(`SELECT id, file_path, media_type, status, original_size, encoded_size, size_saved, processing_time, target_resolution, ffmpeg_flags, error_message, created_at FROM job_reports ORDER BY created_at DESC LIMIT ? OFFSET ?`, limit, offset)
selectQuery += whereClause + ` ORDER BY created_at DESC LIMIT ? OFFSET ?`
queryArgs = append(queryArgs, limit, offset)
rows, err := DB.Query(selectQuery, queryArgs...)
if err != nil {
return nil, 0, err
}
+39 -1
View File
@@ -9,6 +9,7 @@ import (
"log"
"net/http"
"path/filepath"
"strconv"
"goencode/internal/config"
"goencode/internal/db"
@@ -254,20 +255,57 @@ func (s *Server) handlePage(tmplName string) http.HandlerFunc {
}
func (s *Server) handleHistory(w http.ResponseWriter, r *http.Request) {
reports, total, err := db.GetJobReports(50, 0) // Basic pagination for now
limitStr := r.URL.Query().Get("limit")
pageStr := r.URL.Query().Get("page")
statusFilter := r.URL.Query().Get("status")
limit := 50
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
reports, total, err := db.GetJobReports(limit, offset, statusFilter)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
totalPages := (total + limit - 1) / limit
if totalPages == 0 {
totalPages = 1
}
data := struct {
AuthEnabled bool
Reports []db.JobReport
Total int
CurrentPage int
TotalPages int
HasNext bool
HasPrev bool
PrevPage int
NextPage int
FilterStatus string
Limit int
}{
AuthEnabled: s.cfg.Auth.Username != "",
Reports: reports,
Total: total,
CurrentPage: page,
TotalPages: totalPages,
HasNext: page < totalPages,
HasPrev: page > 1,
PrevPage: page - 1,
NextPage: page + 1,
FilterStatus: statusFilter,
Limit: limit,
}
tmpl, err := template.New("layout").Funcs(template.FuncMap{
+67
View File
@@ -59,6 +59,14 @@ a {
color: var(--text-secondary);
font-weight: 500;
transition: color 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.nav-links a svg {
width: 1.25rem;
height: 1.25rem;
}
.nav-links a:hover, .nav-links a.active {
@@ -216,6 +224,65 @@ tbody tr:hover {
border-color: var(--accent);
}
/* Pagination */
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--border);
}
.pagination-controls {
display: flex;
gap: 0.5rem;
}
.page-btn {
background: var(--surface);
color: var(--text-primary);
border: 1px solid var(--border);
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-size: 0.875rem;
transition: all 0.2s;
}
.page-btn:hover {
background: var(--surface-hover);
border-color: var(--text-secondary);
}
.page-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* Filter Bar */
.filter-bar {
display: flex;
gap: 0.5rem;
align-items: center;
}
.filter-select {
padding: 0.5rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-primary);
font-size: 0.875rem;
outline: none;
}
.filter-select:focus {
border-color: var(--accent);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.navbar {
+29 -2
View File
@@ -2,8 +2,17 @@
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem;">
<h2 style="margin: 0;">History</h2>
<div style="display: flex; align-items: center; gap: 1rem;">
<input type="text" id="history-search" class="form-control" placeholder="Search history..." onkeyup="filterHistory()" style="max-width: 300px;">
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
<div class="filter-bar">
<label for="status-filter" style="font-size: 0.875rem; color: var(--text-secondary);">Status:</label>
<select id="status-filter" class="filter-select" onchange="window.location.href='?status=' + this.value">
<option value="all" {{if eq .FilterStatus "all"}}selected{{end}}>All</option>
<option value="exclude_skipped" {{if eq .FilterStatus "exclude_skipped"}}selected{{end}}>Exclude Skipped</option>
<option value="success" {{if eq .FilterStatus "success"}}selected{{end}}>Success Only</option>
<option value="failed" {{if eq .FilterStatus "failed"}}selected{{end}}>Failed Only</option>
</select>
</div>
<input type="text" id="history-search" class="form-control" placeholder="Search page..." onkeyup="filterHistory()" style="max-width: 200px;">
<span style="color: var(--text-secondary); font-size: 0.875rem; white-space: nowrap;">Total Records: <span id="record-count">{{.Total}}</span></span>
</div>
</div>
@@ -46,6 +55,24 @@
</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">
{{if .HasPrev}}
<a href="?page={{.PrevPage}}&status={{.FilterStatus}}" class="page-btn">Previous</a>
{{else}}
<span class="page-btn disabled">Previous</span>
{{end}}
{{if .HasNext}}
<a href="?page={{.NextPage}}&status={{.FilterStatus}}" class="page-btn">Next</a>
{{else}}
<span class="page-btn disabled">Next</span>
{{end}}
</div>
</div>
</div>
<script>
+20 -5
View File
@@ -26,12 +26,27 @@
Go<span>Encode</span>
</div>
<div class="nav-links">
<a href="/" id="nav-dashboard">Dashboard</a>
<a href="/folders" id="nav-folders">Watch Folders</a>
<a href="/history" id="nav-history">History</a>
<a href="/logs" id="nav-logs">Logs</a>
<a href="/" id="nav-dashboard">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Dashboard
</a>
<a href="/folders" id="nav-folders">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/></svg>
Watch Folders
</a>
<a href="/history" id="nav-history">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg>
History
</a>
<a href="/logs" id="nav-logs">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>
Logs
</a>
{{if .AuthEnabled}}
<a href="/logout" style="margin-left: 2rem; color: #ff4444; border-color: #ff4444;">Sign Out</a>
<a href="/logout" style="color: #ff4444;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/></svg>
Sign Out
</a>
{{end}}
</div>
</nav>