feat: optimization and metadata

This commit is contained in:
trafficlunar 2025-12-18 21:52:25 +00:00
parent 5a07e249ac
commit 53079f5971
8 changed files with 123 additions and 20 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
*.db
data/

View file

@ -2,4 +2,3 @@
- [ ] View incidents
- [ ] Latency graphs?
- [ ] README
- [ ] Site metadata

View file

@ -5,7 +5,7 @@ title = "trafficlunar"
link_text = "go back"
link_url = "https://trafficlunar.net"
# Default timeline shown on the index page
# Default timeline shown
# Users can still switch views manually
#
# Available options:
@ -14,7 +14,7 @@ link_url = "https://trafficlunar.net"
# "days" - past 30 days
default_view = "hours"
# Default theme for the status page
# Default theme
# Available themes: https://github.com/trafficlunar/statsys/tree/main/www/themes
# Defaults to "monochrome"
default_theme = "color"
@ -22,6 +22,10 @@ default_theme = "color"
# Defaults to true
enable_theme_switcher = true
# Set to true to show a small statsys watermark
# Defaults to true
enable_watermark = true
# List of services to monitor
[[services]]
name = "website"

View file

@ -20,6 +20,7 @@ type Config struct {
DefaultView string `toml:"default_view"`
DefaultTheme string `toml:"default_theme"`
EnableThemeSwitcher bool `toml:"enable_theme_switcher"`
EnableWatermark bool `toml:"enable_watermark"`
Services []ServiceConfig `toml:"services"`
}
@ -39,6 +40,7 @@ func LoadConfig() {
templateData.LinkText = config.LinkText
templateData.LinkUrl = config.LinkUrl
templateData.EnableThemeSwitcher = config.EnableThemeSwitcher
templateData.EnableWatermark = config.EnableWatermark
templateData.Services = make([]Service, len(config.Services))
for index, ser := range config.Services {
// default to 1000ms

View file

@ -45,6 +45,8 @@ func InitDatabase() {
log.Fatalf("failed to create table: %v", err)
}
templateDataMu.Lock()
// load template data from database
for index, service := range templateData.Services {
// timelines
@ -97,6 +99,7 @@ func InitDatabase() {
templateData.Services[index] = service
}
templateDataMu.Unlock()
log.Printf("database initalized at '%s'", *DatabasePath)
}
@ -332,8 +335,10 @@ func AddToTimeline(serviceIndex int, status string) {
}
}
templateDataMu.Lock()
templateData.Services[serviceIndex] = service
calculateUptimePercentages(serviceIndex)
templateDataMu.Unlock()
}
func AddIncident(serviceIndex int, status string, startTime time.Time) {
@ -348,7 +353,9 @@ func AddIncident(serviceIndex int, status string, startTime time.Time) {
Status: status,
StartTime: startTime,
})
templateDataMu.Lock()
templateData.Services[serviceIndex] = service
templateDataMu.Unlock()
log.Println("(" + service.Name + ") incident started")
}
@ -364,6 +371,8 @@ func ResolveIncident(serviceIndex int, serviceName string, endTime time.Time) {
}
service.Incidents[len(service.Incidents)-1].EndTime = &endTime
templateDataMu.Lock()
templateData.Services[serviceIndex] = service
templateDataMu.Unlock()
log.Println("(" + service.Name + ") incident resolved")
}

View file

@ -5,6 +5,7 @@ import (
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/klauspost/compress/gzhttp"
@ -45,6 +46,7 @@ type TemplateData struct {
LinkText string
LinkUrl string
EnableThemeSwitcher bool
EnableWatermark bool
LastUpdated int64
IsOperational bool
Services []Service
@ -61,8 +63,10 @@ func ToUpper(s string) string {
var templateData = TemplateData{
EnableThemeSwitcher: true,
EnableWatermark: true,
LastUpdated: time.Now().UTC().UnixMilli(),
}
var templateDataMu sync.Mutex
var tmpl *template.Template
var errorTmpl *template.Template
@ -79,16 +83,16 @@ func renderError(w http.ResponseWriter, statusCode int, message string) {
}
}
func index(w http.ResponseWriter, req *http.Request) {
func index(w http.ResponseWriter, r *http.Request) {
// handle 404
if req.URL.Path != "/" {
if r.URL.Path != "/" {
renderError(w, http.StatusNotFound, "Page not found")
return
}
data := templateData
data.View = req.URL.Query().Get("view")
data.Theme = req.URL.Query().Get("theme")
data.View = r.URL.Query().Get("view")
data.Theme = r.URL.Query().Get("theme")
switch data.View {
case "hours", "minutes", "days":
@ -111,16 +115,19 @@ func index(w http.ResponseWriter, req *http.Request) {
}
}
func styles(w http.ResponseWriter, req *http.Request) {
http.ServeFile(w, req, "www/styles.css")
func styles(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", "public, max-age=31536000")
http.ServeFile(w, r, "www/styles.css")
}
func favicon(w http.ResponseWriter, req *http.Request) {
http.ServeFile(w, req, "www/favicon.ico")
func favicon(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", "public, max-age=31536000")
http.ServeFile(w, r, "www/favicon.ico")
}
func robots(w http.ResponseWriter, req *http.Request) {
http.ServeFile(w, req, "www/robots.txt")
func robots(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", "public, max-age=31536000")
http.ServeFile(w, r, "www/robots.txt")
}
func StartHttpServer() {
@ -147,8 +154,14 @@ func StartHttpServer() {
mux.HandleFunc("/styles.css", styles)
mux.HandleFunc("/favicon.ico", favicon)
mux.HandleFunc("/robots.txt", robots)
mux.Handle("/themes/", gzhttp.GzipHandler(http.StripPrefix("/themes/", http.FileServer(http.Dir("www/themes/")))))
mux.Handle("/fonts/", http.StripPrefix("/fonts/", http.FileServer(http.Dir("www/fonts/"))))
mux.Handle("/themes/", gzhttp.GzipHandler(http.StripPrefix("/themes/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", "public, max-age=31536000")
http.FileServer(http.Dir("www/themes/")).ServeHTTP(w, r)
}))))
mux.Handle("/fonts/", http.StripPrefix("/fonts/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", "public, max-age=31536000")
http.FileServer(http.Dir("www/fonts/")).ServeHTTP(w, r)
})))
// wrap with gzip middleware
handler := gzhttp.GzipHandler(mux)

View file

@ -25,10 +25,15 @@ func checkStatuses() {
startTime := time.Now().UTC()
res, err := client.Get(service.Url)
latency := time.Since(startTime).Milliseconds()
if res != nil {
defer res.Body.Close()
}
status := "Online"
if err != nil || res.StatusCode != 200 {
if err != nil {
status = "Offline"
} else if res.StatusCode != 200 {
status = "Offline"
} else if latency >= service.LatencyThreshold {
status = "Degraded"
@ -49,7 +54,9 @@ func checkStatuses() {
}
}
templateDataMu.Lock()
templateData.Services[index].Status = status
templateDataMu.Unlock()
}(index, service)
}
@ -65,17 +72,22 @@ func checkStatuses() {
}
}
templateDataMu.Lock()
templateData.IsOperational = isOperational
templateData.LastUpdated = time.Now().UTC().UnixMilli()
templateDataMu.Unlock()
}
func StartCheckingStatuses() {
log.Println("started checking statuses...")
go func() {
checkStatuses()
for range time.Tick(1 * time.Minute) {
go func() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for range ticker.C {
checkStatuses()
}
}()

View file

@ -3,7 +3,27 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>status - {{ .Title }}</title>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; connect-src 'self'; base-uri 'self'; form-action 'self';"
/>
<title>status — {{ .Title }}</title>
<meta name="title" content="{{ .Title }} Status" />
<meta name="description" content="Real-time status monitoring for {{ .Title }}" />
<meta name="keywords" content="status page, uptime, system status, service status, {{ .Title }}, monitoring, statsys" />
<meta name="author" content="{{ .Title }}" />
<meta name="robots" content="index, follow" />
<meta property="og:type" content="website" />
<meta property="og:title" content="{{ .Title }} Status" />
<meta property="og:description" content="Real-time status monitoring for {{ .Title }}" />
<meta property="og:site_name" content="{{ .Title }} Status" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="{{ .Title }} Status" />
<meta name="twitter:description" content="Real-time status monitoring for {{ .Title }}" />
<meta name="theme-color" content="#000000" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="/styles.css" />
@ -450,6 +470,23 @@
color: var(--accent-secondary);
}
#watermark {
position: absolute;
bottom: 1rem;
right: 1rem;
font-size: 0.875rem;
text-decoration: none;
color: var(--accent-faded);
word-break: break-word;
width: 15%;
text-align: right;
}
#watermark:hover {
color: var(--accent-primary);
opacity: 1;
}
@media (min-width: 39.625rem) {
body::before {
content: "";
@ -475,6 +512,20 @@
.bars-gradient {
display: block !important;
}
#watermark {
top: 2rem;
left: 2rem;
right: unset;
width: 20%;
text-align: left;
font-size: 0.75rem;
z-index: 10;
}
#watermark span:nth-child(2) {
opacity: 0.5;
}
}
@media (max-width: 30rem) {
@ -500,9 +551,14 @@
.uptime-percentage {
transform: translateX(0%);
}
#watermark {
left: 1.5rem;
}
}
</style>
<link rel="preload" as="style" href="/themes/{{ .Theme }}.css" />
<link rel="stylesheet" href="/themes/{{ .Theme }}.css" />
</head>
<body>
@ -674,6 +730,13 @@
</footer>
</main>
{{ if .EnableWatermark }}
<a id="watermark" href="https://github.com/trafficlunar/statsys">
<span>powered by</span>
<span style="font-family: 'JetBrains Mono', monospace; font-weight: 700; color: var(--accent-primary)">statsys</span>
</a>
{{ end }}
<script>
// responsiveness
const timelineLengthTexts = document.querySelectorAll(".timeline-length");