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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetJobReports(limit, offset int) ([]JobReport, int, error) {
|
func GetJobReports(limit, offset int, statusFilter string) ([]JobReport, int, error) {
|
||||||
var total int
|
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
|
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 {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-7
@@ -9,6 +9,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"goencode/internal/config"
|
"goencode/internal/config"
|
||||||
"goencode/internal/db"
|
"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) {
|
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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalPages := (total + limit - 1) / limit
|
||||||
|
if totalPages == 0 {
|
||||||
|
totalPages = 1
|
||||||
|
}
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
AuthEnabled bool
|
AuthEnabled bool
|
||||||
Reports []db.JobReport
|
Reports []db.JobReport
|
||||||
Total int
|
Total int
|
||||||
|
CurrentPage int
|
||||||
|
TotalPages int
|
||||||
|
HasNext bool
|
||||||
|
HasPrev bool
|
||||||
|
PrevPage int
|
||||||
|
NextPage int
|
||||||
|
FilterStatus string
|
||||||
|
Limit int
|
||||||
}{
|
}{
|
||||||
AuthEnabled: s.cfg.Auth.Username != "",
|
AuthEnabled: s.cfg.Auth.Username != "",
|
||||||
Reports: reports,
|
Reports: reports,
|
||||||
Total: total,
|
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{
|
tmpl, err := template.New("layout").Funcs(template.FuncMap{
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ a {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: color 0.2s;
|
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 {
|
.nav-links a:hover, .nav-links a.active {
|
||||||
@@ -216,6 +224,65 @@ tbody tr:hover {
|
|||||||
border-color: var(--accent);
|
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 */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.navbar {
|
.navbar {
|
||||||
|
|||||||
@@ -2,8 +2,17 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem;">
|
<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>
|
<h2 style="margin: 0;">History</h2>
|
||||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||||
<input type="text" id="history-search" class="form-control" placeholder="Search history..." onkeyup="filterHistory()" style="max-width: 300px;">
|
<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>
|
<span style="color: var(--text-secondary); font-size: 0.875rem; white-space: nowrap;">Total Records: <span id="record-count">{{.Total}}</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,6 +55,24 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -26,12 +26,27 @@
|
|||||||
Go<span>Encode</span>
|
Go<span>Encode</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="/" id="nav-dashboard">Dashboard</a>
|
<a href="/" id="nav-dashboard">
|
||||||
<a href="/folders" id="nav-folders">Watch Folders</a>
|
<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>
|
||||||
<a href="/history" id="nav-history">History</a>
|
Dashboard
|
||||||
<a href="/logs" id="nav-logs">Logs</a>
|
</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}}
|
{{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}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
Reference in New Issue
Block a user