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/ data/

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,27 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />
@ -450,6 +470,23 @@
color: var(--accent-secondary); 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) { @media (min-width: 39.625rem) {
body::before { body::before {
content: ""; content: "";
@ -475,6 +512,20 @@
.bars-gradient { .bars-gradient {
display: block !important; 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) { @media (max-width: 30rem) {
@ -500,9 +551,14 @@
.uptime-percentage { .uptime-percentage {
transform: translateX(0%); transform: translateX(0%);
} }
#watermark {
left: 1.5rem;
}
} }
</style> </style>
<link rel="preload" as="style" href="/themes/{{ .Theme }}.css" />
<link rel="stylesheet" href="/themes/{{ .Theme }}.css" /> <link rel="stylesheet" href="/themes/{{ .Theme }}.css" />
</head> </head>
<body> <body>
@ -674,6 +730,13 @@
</footer> </footer>
</main> </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> <script>
// responsiveness // responsiveness
const timelineLengthTexts = document.querySelectorAll(".timeline-length"); const timelineLengthTexts = document.querySelectorAll(".timeline-length");