diff --git a/internal/api/types.go b/internal/api/types.go index 3ce4f5e..b5f2ecb 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -294,8 +294,10 @@ type Database struct { Label string `json:"label"` Path string `json:"path"` Bytes int64 `json:"bytes"` - // Rows is how many answers the query log holds (statistics only). - QueryLogRows *int64 `json:"query_log_rows,omitempty"` + // Rows is how many answers the query log holds (statistics only), + // and QueryLogRetention how long it keeps them ("7d"). + QueryLogRows *int64 `json:"query_log_rows,omitempty"` + QueryLogRetention string `json:"query_log_retention,omitempty"` } // Disk is a file system Jarl uses: what of Jarl's it holds, its size, diff --git a/internal/server/h_storage.go b/internal/server/h_storage.go index d8139b1..a758ac4 100644 --- a/internal/server/h_storage.go +++ b/internal/server/h_storage.go @@ -41,6 +41,7 @@ func (s *Server) storage(ctx context.Context) *api.Storage { if d := dbFile(statsDB, p); d != nil && s.cfg.Stats.Options().RetainQueries > 0 { if n, err := s.cfg.Stats.Store().QueryLogSize(ctx); err == nil { d.QueryLogRows = &n + d.QueryLogRetention = s.cfg.Stats.Status().Retention.Queries } } add(statsDB, filepath.Dir(p)) diff --git a/internal/web/static/assets/pages/dashboard.js b/internal/web/static/assets/pages/dashboard.js index 84092f3..92bbf74 100644 --- a/internal/web/static/assets/pages/dashboard.js +++ b/internal/web/static/assets/pages/dashboard.js @@ -157,7 +157,7 @@ function storageCard(sg) { function storagePanel(sg) { const dbRows = sg.databases.map((d) => [h("dt", null, d.label), h("dd", null, h("strong", null, bytes(d.bytes)), " ", h("span", { class: "mono small muted" }, d.path), - d.query_log_rows != null ? h("div", { class: "small muted" }, "the query log holds " + d.query_log_rows.toLocaleString() + " answers (stats.retention.queries)") : null)]); + d.query_log_rows != null ? h("div", { class: "small muted" }, "query log: " + d.query_log_rows.toLocaleString() + (d.query_log_rows === 1 ? " answer" : " answers") + " so far, each kept for " + retentionWords(d.query_log_retention) + " (stats.retention.queries)") : null)]); return h("div", { class: "panel", id: "storage" }, h("h2", null, "Storage"), h("dl", { class: "kv" }, dbRows, h("dt", null, "Together"), h("dd", null, h("strong", null, bytes(sg.total)))), @@ -183,3 +183,11 @@ function bytes(n) { while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } return (n >= 100 || i === 0 ? Math.round(n) : n.toFixed(1)) + " " + units[i]; } + +// retentionWords: "7d" -> "7 days". +function retentionWords(r) { + const m = /^(\d+)([dhm])$/.exec(r || ""); + if (!m) return r || "a while"; + const word = { d: "day", h: "hour", m: "minute" }[m[2]]; + return m[1] + " " + word + (m[1] === "1" ? "" : "s"); +} diff --git a/internal/web/static/assets/pages/stats.js b/internal/web/static/assets/pages/stats.js index 7bf321f..d048208 100644 --- a/internal/web/static/assets/pages/stats.js +++ b/internal/web/static/assets/pages/stats.js @@ -319,7 +319,7 @@ function exploreSince(since) { function dbTile(db, status) { const keep = ((status || {}).retention || {}).queries; const sub = db.query_log_rows != null - ? "the query log holds " + compact(db.query_log_rows) + " answers (" + keep + ")" + ? "query log: " + compact(db.query_log_rows) + (db.query_log_rows === 1 ? " answer" : " answers") + " so far, each kept for " + retentionWords(keep) : "counters and top lists; no query log"; return h("a", { class: "card link", href: "#/", title: db.path + " · all of Jarl's databases: Dashboard → Storage" }, h("div", { class: "label" }, "Statistics database"), h("div", { class: "value" }, bytes(db.bytes)), h("div", { class: "sub" }, sub)); @@ -335,3 +335,11 @@ function diskTile(d) { h("div", { class: "seg other" + (u > 0.9 ? " full" : ""), style: { width: u * 100 + "%" } })), h("div", { class: "sub" }, h("span", { class: u > 0.9 ? "warn-text" : "" }, Math.round(u * 100) + "% of " + bytes(d.total) + " used"), ", for " + d.holds.join(", "))); } + +// retentionWords: "7d" -> "7 days". +function retentionWords(r) { + const m = /^(\d+)([dhm])$/.exec(r || ""); + if (!m) return r || "a while"; + const word = { d: "day", h: "hour", m: "minute" }[m[2]]; + return m[1] + " " + word + (m[1] === "1" ? "" : "s"); +}