feat: add server-side pagination and status filtering to the job history page
Build / Build and Push (push) Successful in 52s
Build / Build and Push (push) Successful in 52s
This commit is contained in:
+20
-3
@@ -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
|
||||
}
|
||||
|
||||
+45
-7
@@ -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
|
||||
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,
|
||||
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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user