Rework chart gen. Add Health, Tanscode, Task Status Charts
All checks were successful
/ release (push) Successful in 32s
All checks were successful
/ release (push) Successful in 32s
This commit is contained in:
parent
091ef322d5
commit
8e15cdd6e8
3 changed files with 164 additions and 81 deletions
159
server/stats.go
159
server/stats.go
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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"}}
|
||||||
|
|
Loading…
Add table
Reference in a new issue