From 8e15cdd6e828ca27613d4a7cc528cd776b2b250c Mon Sep 17 00:00:00 2001 From: Samuel Lorch Date: Sat, 13 Jul 2024 23:18:47 +0200 Subject: [PATCH] Rework chart gen. Add Health, Tanscode, Task Status Charts --- server/stats.go | 159 ++++++++++++++++++++++++++++++++--------- static/style/style.css | 63 ++++++++-------- tmpl/stats.tmpl | 23 ++---- 3 files changed, 164 insertions(+), 81 deletions(-) diff --git a/server/stats.go b/server/stats.go index d7e028e..19b5033 100644 --- a/server/stats.go +++ b/server/stats.go @@ -2,9 +2,12 @@ package server import ( "bytes" + "context" + "fmt" "html/template" "log/slog" "net/http" + "strings" "git.lastassault.de/speatzle/morffix/constants" "github.com/go-echarts/go-echarts/v2/charts" @@ -13,40 +16,25 @@ import ( ) type StatsDisplay struct { - CodecCounts []CodecCount - Element template.HTML - Script template.HTML + Charts []ChartData } -type CodecCount struct { - Codec string - Count int +type ChartData struct { + Element template.HTML + Script template.HTML } -func handleStats(w http.ResponseWriter, r *http.Request) { - data := StatsDisplay{} +type PieValue struct { + 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');`) - if err != nil { - slog.ErrorContext(r.Context(), "Query Stats", "err", err) - 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 +type PieIntValue struct { + Id int + Value int +} - chartData := []opts.PieData{} - for _, c := range codecCounts { - chartData = append(chartData, opts.PieData{ - Name: c.Codec, - Value: c.Count, - }) - } +func generatePie(name string, data []opts.PieData) ChartData { pie := charts.NewPie() pie.SetGlobalOptions( @@ -55,10 +43,10 @@ func handleStats(w http.ResponseWriter, r *http.Request) { BackgroundColor: "#111", }), charts.WithTitleOpts(opts.Title{ - Title: "Codecs", + Title: name, })) - pie.AddSeries("Codecs", chartData).SetSeriesOptions( + pie.AddSeries(name, data).SetSeriesOptions( charts.WithLabelOpts( opts.Label{ Show: opts.Bool(true), @@ -68,11 +56,116 @@ func handleStats(w http.ResponseWriter, r *http.Request) { snippet := pie.RenderSnippet() - data.Element = template.HTML(snippet.Element) - data.Script = template.HTML(snippet.Script) + return ChartData{ + 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{} - err = templates.ExecuteTemplate(&buf, constants.STATS_TEMPLATE_NAME, data) + err = templates.ExecuteTemplate(&buf, constants.STATS_TEMPLATE_NAME, StatsDisplay{ + Charts: data, + }) if err != nil { slog.ErrorContext(r.Context(), "Executing Stats Template", "err", err) http.Error(w, "Error Executing Template: "+err.Error(), http.StatusInternalServerError) diff --git a/static/style/style.css b/static/style/style.css index 6a9d299..6a2ed29 100644 --- a/static/style/style.css +++ b/static/style/style.css @@ -3,58 +3,63 @@ @import url("component.css"); :root { - font-family: monospace; - font-size: 12px; - overflow-y: auto; + font-family: monospace; + font-size: 12px; + overflow-y: auto; } body { - padding: 0.5rem; - gap: 0.5rem; + padding: 0.5rem; + gap: 0.5rem; } .counter-image { - image-rendering: pixelated; - image-rendering: -moz-crisp-edges; - image-rendering: crisp-edges; - flex-grow: 1; + image-rendering: pixelated; + image-rendering: -moz-crisp-edges; + image-rendering: crisp-edges; + flex-grow: 1; } .counter { - flex-flow: row nowrap; - align-items: end; - font-size: 1.5rem; - font-weight: bold; + flex-flow: row nowrap; + align-items: end; + font-size: 1.5rem; + font-weight: bold; } .workers { - flex-flow: row wrap; - gap: 2rem; + flex-flow: row wrap; + gap: 2rem; } -.workers>* { - gap: 0.5rem; - border: 1px solid var(--fg); +.workers > * { + gap: 0.5rem; + border: 1px solid var(--fg); } -.log>p { - padding: 0.25rem 0; - user-select: text; +.log > p { + padding: 0.25rem 0; + user-select: text; } -.log>p:hover { - background: var(--cl-hl); +.log > p:hover { + background: var(--cl-hl); } -nav> :first-child { - font-size: 2rem; - font-weight: bold; +nav > :first-child { + font-size: 2rem; + font-weight: bold; } .short-button { - align-self: start; + align-self: start; } .button-list { - flex-direction: row; -} \ No newline at end of file + flex-direction: row; +} + +.stats { + flex-flow: row wrap; + gap: 2rem; +} diff --git a/tmpl/stats.tmpl b/tmpl/stats.tmpl index 9c040b2..8ea32da 100644 --- a/tmpl/stats.tmpl +++ b/tmpl/stats.tmpl @@ -1,23 +1,8 @@ {{template "head"}}

Stats

-

Codec Counts

-
- - - - - - {{range $f := .CodecCounts}} - - - - - {{end}} -
CodecCount
- {{ $f.Codec }} - - {{ $f.Count }} -
+
+ {{range $c := .Charts}} + {{$c.Element}} {{$c.Script}} + {{end}}
-{{.Element}} {{.Script}} {{template "tail"}}