Skip to content

Commit 31855d5

Browse files
feat(status-pages): add colored sparkline chart to table view
Replace simple UP/DOWN text signal with a 30-day sparkline chart using colored Unicode block characters. Each character represents one day: green █ = operational, red █ = outage, dim · = before monitoring started. - buildSparkline() generates per-provider 30-day uptime chart - ANSI green/red/dim coloring for at-a-glance visual scanning - Injectable timeNow for deterministic test assertions - 23 tests including sparkline rendering, color output, edge cases Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1b01788 commit 31855d5

2 files changed

Lines changed: 226 additions & 71 deletions

File tree

cmd/status_pages_third_party.go

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,25 +153,76 @@ func filterProviders(providers []thirdPartyProvider, search string, activeOnly b
153153
return filtered
154154
}
155155

156-
// providerStatus returns an ASCII signal and status string for a provider.
157-
func providerStatus(p thirdPartyProvider) (signal string, status string) {
156+
const (
157+
sparklineWidth = 30 // number of day-buckets in the chart
158+
dayMs = int64(24 * 60 * 60 * 1000)
159+
160+
ansiGreen = "\033[32m"
161+
ansiRed = "\033[31m"
162+
ansiDim = "\033[2m"
163+
ansiReset = "\033[0m"
164+
)
165+
166+
// timeNow is injectable for testing.
167+
var timeNow = time.Now
168+
169+
// providerCurrentStatus returns the current status string for a provider.
170+
func providerCurrentStatus(p thirdPartyProvider) string {
158171
for _, o := range p.Outages {
159172
if o.Status != "resolved" {
160-
return "▼ DOWN", o.Status
173+
return o.Status
174+
}
175+
}
176+
return "operational"
177+
}
178+
179+
// buildSparkline generates a 30-character colored sparkline showing the last 30 days.
180+
// Green █ = operational, Red █ = outage, Dim · = before monitoring started.
181+
func buildSparkline(p thirdPartyProvider) string {
182+
now := timeNow().UnixMilli()
183+
var b strings.Builder
184+
185+
for i := sparklineWidth - 1; i >= 0; i-- {
186+
bucketStart := now - int64(i+1)*dayMs
187+
bucketEnd := now - int64(i)*dayMs
188+
189+
if bucketEnd <= p.MonitoringStartDate {
190+
b.WriteString(ansiDim + "·" + ansiReset)
191+
continue
192+
}
193+
194+
hasOutage := false
195+
for _, o := range p.Outages {
196+
outageEnd := o.End
197+
if outageEnd == 0 {
198+
outageEnd = now // active outage
199+
}
200+
if o.Start < bucketEnd && outageEnd > bucketStart {
201+
hasOutage = true
202+
break
203+
}
204+
}
205+
206+
if hasOutage {
207+
b.WriteString(ansiRed + "█" + ansiReset)
208+
} else {
209+
b.WriteString(ansiGreen + "█" + ansiReset)
161210
}
162211
}
163-
return "▲ UP", "operational"
212+
213+
return b.String()
164214
}
165215

166-
// formatThirdPartyTable renders providers as a custom table with signal indicators.
216+
// formatThirdPartyTable renders providers as a custom table with sparkline charts.
167217
func formatThirdPartyTable(providers []thirdPartyProvider) string {
168218
var buf bytes.Buffer
169219
table := tablewriter.NewWriter(&buf)
170-
table.Header("PROVIDER", "DISPLAY NAME", "SERVICE", "SIGNAL", "STATUS")
220+
table.Header("PROVIDER", "DISPLAY NAME", "SERVICE", "UPTIME", "STATUS")
171221

172222
for _, p := range providers {
173-
signal, status := providerStatus(p)
174-
_ = table.Append(p.ProviderName, p.DisplayName, p.ProviderService, signal, status)
223+
chart := buildSparkline(p)
224+
status := providerCurrentStatus(p)
225+
_ = table.Append(p.ProviderName, p.DisplayName, p.ProviderService, chart, status)
175226
}
176227

177228
_ = table.Render()

0 commit comments

Comments
 (0)