summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/daemon/daemon_test.go2
-rw-r--r--internal/goprecords/report.go132
-rw-r--r--internal/goprecords/report_format.go71
-rw-r--r--internal/goprecords/report_test.go4
-rw-r--r--internal/version/version.go2
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"