diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-27 11:17:03 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-27 11:17:03 +0300 |
| commit | 8c540b6bdc745ee0c0eab7cb6075d5289ec45873 (patch) | |
| tree | cb91620b13ed7aab48f58643ce5ea7fd293b6f50 /internal | |
| parent | 161549ef9832b55983ae8ec3851f0c8f7536b72a (diff) | |
Restyle HTML output to stats.foo.zone style; bump to 0.5.30.5.3
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/daemon/daemon_test.go | 2 | ||||
| -rw-r--r-- | internal/goprecords/report.go | 132 | ||||
| -rw-r--r-- | internal/goprecords/report_format.go | 71 | ||||
| -rw-r--r-- | internal/goprecords/report_test.go | 4 | ||||
| -rw-r--r-- | internal/version/version.go | 2 |
5 files changed, 154 insertions, 57 deletions
diff --git a/internal/daemon/daemon_test.go b/internal/daemon/daemon_test.go index 44f9888..5d642b6 100644 --- a/internal/daemon/daemon_test.go +++ b/internal/daemon/daemon_test.go @@ -312,7 +312,7 @@ func TestReportHTTPTable(t *testing.T) { query: "OutputFormat=HTML&limit=2", wantCode: http.StatusOK, wantCTPfx: "text/html", - bodyNeedle: []string{"<!DOCTYPE html>", "<pre>"}, + bodyNeedle: []string{"<!DOCTYPE html>", "<table>"}, }, { name: "gemtext", diff --git a/internal/goprecords/report.go b/internal/goprecords/report.go index 12c3951..8916caa 100644 --- a/internal/goprecords/report.go +++ b/internal/goprecords/report.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "sort" + "strings" ) type metricExtractor struct { @@ -61,38 +62,33 @@ func WriteReports(w io.Writer, aggregates *Aggregates, cfg ReportConfig) error { if cfg.Category != CategoryHost && (cfg.Metric == MetricDowntime || cfg.Metric == MetricLifespan) { return fmt.Errorf("Category %s only supports: Boots, Uptime, Score", cfg.Category) } - if cfg.Category == CategoryHost { - if err := writeReportString(w, NewHostReporter(aggregates, cfg.Limit, cfg.Metric, cfg.OutputFormat, 1).Report()); err != nil { - return err - } - } else { - if err := writeReportString(w, NewReporter(aggregates, cfg.Category, cfg.Limit, cfg.Metric, cfg.OutputFormat, 1).Report()); err != nil { - return err - } - } - return nil + s := reportForPair(aggregates, cfg.Category, cfg.Metric, cfg.Limit, 1, cfg.OutputFormat) + return writeReportString(w, wrapIfHTML(s, cfg.OutputFormat)) } order, err := StatsOrderList(cfg.StatsOrder) if err != nil { return err } headerIndent := uint(2) - for _, pair := range order { - c, m := pair.Category, pair.Metric - if !cfg.IncludeKernel && c == CategoryKernel { - continue + if cfg.OutputFormat == FormatHTML { + var parts []string + for _, pair := range order { + if skipPair(cfg, pair.Category, pair.Metric) { + continue + } + if s := reportForPair(aggregates, pair.Category, pair.Metric, cfg.Limit, headerIndent, cfg.OutputFormat); s != "" { + parts = append(parts, s) + } } - if c != CategoryHost && (m == MetricDowntime || m == MetricLifespan) { + return writeReportString(w, wrapHTMLDocument(strings.Join(parts, "\n"))) + } + for _, pair := range order { + if skipPair(cfg, pair.Category, pair.Metric) { continue } - if c == CategoryHost { - if err := writeReportString(w, NewHostReporter(aggregates, cfg.Limit, m, cfg.OutputFormat, headerIndent).Report()); err != nil { - return err - } - } else { - if err := writeReportString(w, NewReporter(aggregates, c, cfg.Limit, m, cfg.OutputFormat, headerIndent).Report()); err != nil { - return err - } + s := reportForPair(aggregates, pair.Category, pair.Metric, cfg.Limit, headerIndent, cfg.OutputFormat) + if err := writeReportString(w, s); err != nil { + return err } if err := writeReportString(w, "\n"); err != nil { return err @@ -101,6 +97,96 @@ func WriteReports(w io.Writer, aggregates *Aggregates, cfg ReportConfig) error { return nil } +func reportForPair(aggregates *Aggregates, c Category, m Metric, limit, headerIndent uint, outputFormat OutputFormat) string { + if c == CategoryHost { + return NewHostReporter(aggregates, limit, m, outputFormat, headerIndent).Report() + } + return NewReporter(aggregates, c, limit, m, outputFormat, headerIndent).Report() +} + +func skipPair(cfg ReportConfig, c Category, m Metric) bool { + if !cfg.IncludeKernel && c == CategoryKernel { + return true + } + if c != CategoryHost && (m == MetricDowntime || m == MetricLifespan) { + return true + } + return false +} + +func wrapIfHTML(s string, f OutputFormat) string { + if f != FormatHTML { + return s + } + return wrapHTMLDocument(s) +} + +func wrapHTMLDocument(body string) string { + var b strings.Builder + b.WriteString("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n") + b.WriteString("<meta charset=\"UTF-8\">\n") + b.WriteString("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n") + b.WriteString("<title>goprecords uptime report</title>\n") + b.WriteString(htmlStyle) + b.WriteString("</head>\n<body>\n") + b.WriteString(body) + b.WriteString("</body>\n</html>\n") + return b.String() +} + +const htmlStyle = `<style> + /* Compact, full-width layout */ + :root { + --pad: 8px; + } + html, body { + height: 100%; + } + body { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + line-height: 1.3; + margin: 0; + padding: var(--pad); + background: #fff; + color: #000; + } + /* Headings: smaller and tighter */ + h1, h2, h3 { margin: 0.5em 0 0.25em; font-weight: 600; } + h1 { font-size: 1em; } + h2 { font-size: 0.95em; } + h3 { font-size: 0.9em; } + /* Paragraphs and lists: minimal vertical rhythm */ + p { margin: 0.2em 0; } + ul { margin: 0.3em 0; padding-left: 1.2em; } + li { margin: 0.1em 0; } + /* Code blocks and tables */ + pre { + overflow-x: auto; + white-space: pre; + margin: 0.3em 0; + } + table { + border-collapse: collapse; + table-layout: auto; /* size columns by content */ + width: auto; /* do not stretch to full width */ + max-width: 100%; + margin: 0.5em 0; + font-size: 0.95em; + display: inline-table; /* keep as compact as content allows */ + } + th, td { + padding: 0.1em 0.3em; + text-align: left; + white-space: nowrap; /* avoid wide columns caused by wrapping */ + } + /* Links */ + a { color: #06c; text-decoration: underline; } + a:visited { color: #639; } + /* Rules */ + hr { border: none; border-top: 1px solid #ccc; margin: 0.5em 0; } + </style> +` + func writeReportString(w io.Writer, s string) error { _, err := io.WriteString(w, s) return err diff --git a/internal/goprecords/report_format.go b/internal/goprecords/report_format.go index 8d06b04..079fe94 100644 --- a/internal/goprecords/report_format.go +++ b/internal/goprecords/report_format.go @@ -21,22 +21,6 @@ func (r reportBuilder) formatReport(rows []tableRow, hasLastKernel, hasLastUpdat } func (r reportBuilder) formatReportHTML(rows []tableRow, hasLastKernel, hasLastUpdated bool) string { - cW, nW, vW, lkW, luW := r.reportWidths(rows, hasLastKernel, hasLastUpdated) - border := r.buildBorder(cW, nW, vW, lkW, luW, hasLastKernel, hasLastUpdated) - fmtStr := r.buildFormatStr(cW, nW, vW, lkW, luW, hasLastKernel, hasLastUpdated) - var headRow string - if hasLastKernel && hasLastUpdated { - headRow = fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String(), "Last Kernel", "Updated") - } else if hasLastKernel { - headRow = fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String(), "Last Kernel") - } else if hasLastUpdated { - headRow = fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String(), "Updated") - } else { - headRow = fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String()) - } - body := r.buildReportBody(rows, fmtStr, hasLastKernel, hasLastUpdated) - ascii := border + headRow + border + body + border - hl := int(r.headerIndent) if hl < 1 { hl = 1 @@ -46,27 +30,54 @@ func (r reportBuilder) formatReportHTML(rows []tableRow, hasLastKernel, hasLastU } title := fmt.Sprintf("Top %d %s's by %s", r.limit, r.metric, r.category) desc := MetricDescription(r.metric) + htag := strconv.Itoa(hl) var b strings.Builder - b.WriteString("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n") - b.WriteString("<link rel=\"shortcut icon\" type=\"image/gif\" href=\"https://foo.zone/favicon.ico\" />\n") - b.WriteString("<link rel=\"stylesheet\" href=\"https://foo.zone/style.css\" />\n<title>") - b.WriteString(template.HTMLEscapeString(title)) - b.WriteString("</title>\n</head>\n<body>\n<h") - b.WriteString(strconv.Itoa(hl)) - b.WriteString(" style=\"display: inline\">") + b.WriteString("<h") + b.WriteString(htag) + b.WriteString(">") b.WriteString(template.HTMLEscapeString(title)) b.WriteString("</h") - b.WriteString(strconv.Itoa(hl)) - b.WriteString("><br />\n<br />\n") + b.WriteString(htag) + b.WriteString(">\n") if desc != "" { - b.WriteString("<span class=\"quote\">") + b.WriteString("<p>") b.WriteString(template.HTMLEscapeString(desc)) - b.WriteString("</span><br />\n<br />\n") + b.WriteString("</p>\n") + } + b.WriteString("<table>\n<tr><th>Pos</th><th>") + b.WriteString(template.HTMLEscapeString(r.category.String())) + b.WriteString("</th><th>") + b.WriteString(template.HTMLEscapeString(r.metric.String())) + b.WriteString("</th>") + if hasLastKernel { + b.WriteString("<th>Last Kernel</th>") + } + if hasLastUpdated { + b.WriteString("<th>Updated</th>") + } + b.WriteString("</tr>\n") + for _, row := range rows { + b.WriteString("<tr><td>") + b.WriteString(template.HTMLEscapeString(row.Pos)) + b.WriteString("</td><td>") + b.WriteString(template.HTMLEscapeString(row.Name)) + b.WriteString("</td><td>") + b.WriteString(template.HTMLEscapeString(row.Value)) + b.WriteString("</td>") + if hasLastKernel { + b.WriteString("<td>") + b.WriteString(template.HTMLEscapeString(row.LastKernel)) + b.WriteString("</td>") + } + if hasLastUpdated { + b.WriteString("<td>") + b.WriteString(template.HTMLEscapeString(row.LastUpdated)) + b.WriteString("</td>") + } + b.WriteString("</tr>\n") } - b.WriteString("<pre>") - b.WriteString(template.HTMLEscapeString(ascii)) - b.WriteString("</pre>\n</body>\n</html>\n") + b.WriteString("</table>\n") return b.String() } diff --git a/internal/goprecords/report_test.go b/internal/goprecords/report_test.go index fccb0d2..962afdb 100644 --- a/internal/goprecords/report_test.go +++ b/internal/goprecords/report_test.go @@ -114,8 +114,8 @@ func TestReportHTML(t *testing.T) { t.Fatalf("expected HTMLReporter, got %T", reporter) } report := reporter.Report() - if !strings.Contains(report, "<!DOCTYPE html>") || !strings.Contains(report, "<pre>") { - t.Fatalf("expected HTML document with pre, got %q", report) + if !strings.Contains(report, "<table>") || !strings.Contains(report, "<h2>") { + t.Fatalf("expected HTML fragment with table, got %q", report) } if !strings.Contains(report, "host1") { t.Error("expected report to contain host1") diff --git a/internal/version/version.go b/internal/version/version.go index 7ee9a7c..1b13709 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,4 +1,4 @@ package version // Tag is the application release version. -const Tag = "0.5.2" +const Tag = "0.5.3" |
