Rework chart gen. Add Health, Tanscode, Task Status Charts
All checks were successful
/ release (push) Successful in 32s

This commit is contained in:
Samuel Lorch 2024-07-13 23:18:47 +02:00
parent 091ef322d5
commit 8e15cdd6e8
3 changed files with 164 additions and 81 deletions

View file

@ -2,9 +2,12 @@ package server
import ( import (
"bytes" "bytes"
"context"
"fmt"
"html/template" "html/template"
"log/slog" "log/slog"
"net/http" "net/http"
"strings"
"git.lastassault.de/speatzle/morffix/constants" "git.lastassault.de/speatzle/morffix/constants"
"github.com/go-echarts/go-echarts/v2/charts" "github.com/go-echarts/go-echarts/v2/charts"
@ -13,40 +16,25 @@ import (
) )
type StatsDisplay struct { type StatsDisplay struct {
CodecCounts []CodecCount Charts []ChartData
Element template.HTML
Script template.HTML
} }
type CodecCount struct { type ChartData struct {
Codec string Element template.HTML
Count int Script template.HTML
} }
func handleStats(w http.ResponseWriter, r *http.Request) { type PieValue struct {
data := StatsDisplay{} Name string
Value int
}
rows, err := db.Query(r.Context(), `SELECT COALESCE(jsonb_path_query_first(ffprobe_data, '$.streams[*] ? (@.codec_type == "video") ? (@.disposition.attached_pic == 0).codec_name')::text, 'Unknown') AS codec, COUNT(*) AS count FROM files WHERE ffprobe_data IS NOT NULL GROUP BY jsonb_path_query_first(ffprobe_data, '$.streams[*] ? (@.codec_type == "video") ? (@.disposition.attached_pic == 0).codec_name');`) type PieIntValue struct {
if err != nil { Id int
slog.ErrorContext(r.Context(), "Query Stats", "err", err) Value int
http.Error(w, "Error Query Stats: "+err.Error(), http.StatusInternalServerError) }
return
}
codecCounts, err := pgx.CollectRows[CodecCount](rows, pgx.RowToStructByName[CodecCount])
if err != nil {
slog.ErrorContext(r.Context(), "Collect Rows", "err", err)
http.Error(w, "Error Query Libraries: "+err.Error(), http.StatusInternalServerError)
return
}
data.CodecCounts = codecCounts
chartData := []opts.PieData{} func generatePie(name string, data []opts.PieData) ChartData {
for _, c := range codecCounts {
chartData = append(chartData, opts.PieData{
Name: c.Codec,
Value: c.Count,
})
}
pie := charts.NewPie() pie := charts.NewPie()
pie.SetGlobalOptions( pie.SetGlobalOptions(
@ -55,10 +43,10 @@ func handleStats(w http.ResponseWriter, r *http.Request) {
BackgroundColor: "#111", BackgroundColor: "#111",
}), }),
charts.WithTitleOpts(opts.Title{ charts.WithTitleOpts(opts.Title{
Title: "Codecs", Title: name,
})) }))
pie.AddSeries("Codecs", chartData).SetSeriesOptions( pie.AddSeries(name, data).SetSeriesOptions(
charts.WithLabelOpts( charts.WithLabelOpts(
opts.Label{ opts.Label{
Show: opts.Bool(true), Show: opts.Bool(true),
@ -68,11 +56,116 @@ func handleStats(w http.ResponseWriter, r *http.Request) {
snippet := pie.RenderSnippet() snippet := pie.RenderSnippet()
data.Element = template.HTML(snippet.Element) return ChartData{
data.Script = template.HTML(snippet.Script) Element: template.HTML(snippet.Element),
Script: template.HTML(snippet.Script),
}
}
func generateStats(ctx context.Context) ([]ChartData, error) {
data := []ChartData{}
rows, err := db.Query(ctx,
`SELECT COALESCE(jsonb_path_query_first(ffprobe_data, '$.streams[*] ? (@.codec_type == "video") ? (@.disposition.attached_pic == 0).codec_name')::text, 'Unknown') AS name, COUNT(*) AS value
FROM files
WHERE ffprobe_data IS NOT NULL
GROUP BY jsonb_path_query_first(ffprobe_data, '$.streams[*] ? (@.codec_type == "video") ? (@.disposition.attached_pic == 0).codec_name');`)
if err != nil {
return nil, fmt.Errorf("Query Codecs: %w", err)
}
codecCounts, err := pgx.CollectRows(rows, pgx.RowToStructByName[PieValue])
if err != nil {
return nil, fmt.Errorf("Collect Codec Data: %w", err)
}
res := []opts.PieData{}
for _, v := range codecCounts {
res = append(res, opts.PieData{
Name: strings.ReplaceAll(v.Name, "\"", ""),
Value: v.Value,
})
}
data = append(data, generatePie("Codecs", res))
rows, err = db.Query(ctx,
`SELECT health AS id, COUNT(*) AS value
FROM files
GROUP BY health;`)
if err != nil {
return nil, fmt.Errorf("Query Health: %w", err)
}
healthCounts, err := pgx.CollectRows(rows, pgx.RowToStructByName[PieIntValue])
if err != nil {
return nil, fmt.Errorf("Collect Health Data: %w", err)
}
res = []opts.PieData{}
for _, v := range healthCounts {
res = append(res, opts.PieData{
Name: constants.FileHealth(v.Id).String(),
Value: v.Value,
})
}
data = append(data, generatePie("Health", res))
rows, err = db.Query(ctx,
`SELECT transcode AS id, COUNT(*) AS value
FROM files
GROUP BY transcode;`)
if err != nil {
return nil, fmt.Errorf("Query Transcode: %w", err)
}
transcodeCounts, err := pgx.CollectRows(rows, pgx.RowToStructByName[PieIntValue])
if err != nil {
return nil, fmt.Errorf("Collect Transcode Data: %w", err)
}
res = []opts.PieData{}
for _, v := range transcodeCounts {
res = append(res, opts.PieData{
Name: constants.FileTranscode(v.Id).String(),
Value: v.Value,
})
}
data = append(data, generatePie("Transcode Status", res))
rows, err = db.Query(ctx,
`SELECT status AS id, COUNT(*) AS value
FROM tasks
GROUP BY status;`)
if err != nil {
return nil, fmt.Errorf("Query Task Status: %w", err)
}
taskStatusCounts, err := pgx.CollectRows(rows, pgx.RowToStructByName[PieIntValue])
if err != nil {
return nil, fmt.Errorf("Collect Task Status Data: %w", err)
}
res = []opts.PieData{}
for _, v := range taskStatusCounts {
res = append(res, opts.PieData{
Name: constants.TaskStatus(v.Id).String(),
Value: v.Value,
})
}
data = append(data, generatePie("Task Status", res))
return data, nil
}
func handleStats(w http.ResponseWriter, r *http.Request) {
data, err := generateStats(r.Context())
if err != nil {
slog.ErrorContext(r.Context(), "Generate Stats:", "err", err)
http.Error(w, "Generate Stats: "+err.Error(), http.StatusInternalServerError)
return
}
buf := bytes.Buffer{} buf := bytes.Buffer{}
err = templates.ExecuteTemplate(&buf, constants.STATS_TEMPLATE_NAME, data) err = templates.ExecuteTemplate(&buf, constants.STATS_TEMPLATE_NAME, StatsDisplay{
Charts: data,
})
if err != nil { if err != nil {
slog.ErrorContext(r.Context(), "Executing Stats Template", "err", err) slog.ErrorContext(r.Context(), "Executing Stats Template", "err", err)
http.Error(w, "Error Executing Template: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Error Executing Template: "+err.Error(), http.StatusInternalServerError)

View file

@ -3,58 +3,63 @@
@import url("component.css"); @import url("component.css");
:root { :root {
font-family: monospace; font-family: monospace;
font-size: 12px; font-size: 12px;
overflow-y: auto; overflow-y: auto;
} }
body { body {
padding: 0.5rem; padding: 0.5rem;
gap: 0.5rem; gap: 0.5rem;
} }
.counter-image { .counter-image {
image-rendering: pixelated; image-rendering: pixelated;
image-rendering: -moz-crisp-edges; image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges; image-rendering: crisp-edges;
flex-grow: 1; flex-grow: 1;
} }
.counter { .counter {
flex-flow: row nowrap; flex-flow: row nowrap;
align-items: end; align-items: end;
font-size: 1.5rem; font-size: 1.5rem;
font-weight: bold; font-weight: bold;
} }
.workers { .workers {
flex-flow: row wrap; flex-flow: row wrap;
gap: 2rem; gap: 2rem;
} }
.workers>* { .workers > * {
gap: 0.5rem; gap: 0.5rem;
border: 1px solid var(--fg); border: 1px solid var(--fg);
} }
.log>p { .log > p {
padding: 0.25rem 0; padding: 0.25rem 0;
user-select: text; user-select: text;
} }
.log>p:hover { .log > p:hover {
background: var(--cl-hl); background: var(--cl-hl);
} }
nav> :first-child { nav > :first-child {
font-size: 2rem; font-size: 2rem;
font-weight: bold; font-weight: bold;
} }
.short-button { .short-button {
align-self: start; align-self: start;
} }
.button-list { .button-list {
flex-direction: row; flex-direction: row;
} }
.stats {
flex-flow: row wrap;
gap: 2rem;
}

View file

@ -1,23 +1,8 @@
{{template "head"}} {{template "head"}}
<h2>Stats</h2> <h2>Stats</h2>
<h2>Codec Counts</h2> <div class="stats">
<div> {{range $c := .Charts}}
<table> {{$c.Element}} {{$c.Script}}
<tr> {{end}}
<th>Codec</th>
<th>Count</th>
</tr>
{{range $f := .CodecCounts}}
<tr>
<td>
{{ $f.Codec }}
</td>
<td>
{{ $f.Count }}
</td>
</tr>
{{end}}
</table>
</div> </div>
{{.Element}} {{.Script}}
{{template "tail"}} {{template "tail"}}