// VocalForge Minimal Status Dashboard (Bootstrap) // Run: DB_PATH=./guild_config.db ADDR=:8080 go run vocalforge_status_dashboard.go package main import ( "database/sql" "encoding/json" "html/template" "log" "net/http" "os" "strconv" "strings" "time" _ "modernc.org/sqlite" ) var ( dbDsn = GetENV("DB_PATH", "/data/guild_config.db") HTTP_PORT string = "8080" HTTP_TLS bool = false HTTP_TLS_PRIVATEKEY string = "" HTTP_TLS_CERTIFICATE string = "" ) type Status struct { StartedAt time.Time Now time.Time DBPath string GuildCount int RecentEvents []EventRow HasEventTable bool } type EventRow struct { TS int64 `json:"ts"` GuildID string `json:"guild_id"` Type string `json:"type"` ChannelID string `json:"channel_id,omitempty"` UserID string `json:"user_id,omitempty"` Extra string `json:"extra,omitempty"` } var ( db *sql.DB startedAt = time.Now() ) func GetENV(k, d string) string { if v := os.Getenv(k); v != "" { return v } return d } func Enabled(k string, def bool) bool { b, err := strconv.ParseBool(strings.ToLower(os.Getenv(k))) if err != nil { return def } return b } func main() { var err error db, err = sql.Open("sqlite", dbDsn+"?_pragma=journal_mode(WAL)&_pragma=busy_timeout=5000&_pragma=synchronous=NORMAL") if err != nil { log.Fatalf("open db: %v", err) } defer db.Close() HTTP_PORT = GetENV("HTTP_PORT", "8080") HTTP_TLS = Enabled("HTTP_TLS", false) HTTP_TLS_CERTIFICATE = GetENV("HTTP_TLS_CERTIFICATE", "./server-cert.pem") HTTP_TLS_PRIVATEKEY = GetENV("HTTP_TLS_PRIVATEKEY", "./server-key.pem") mux := http.NewServeMux() mux.HandleFunc("/", handleIndex) mux.HandleFunc("/api/status", handleAPIStatus) mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("/data/static")))) log.Printf("VocalForge status dashboard listening on %s (DB=%s)", HTTP_PORT, dbDsn) if HTTP_TLS { log.Printf("WoL server listening on %s (DB: %s)", ":"+HTTP_PORT, dbDsn) if err := http.ListenAndServeTLS(":"+HTTP_PORT, HTTP_TLS_CERTIFICATE, HTTP_TLS_PRIVATEKEY, withSecurity(mux)); err != nil { log.Fatalf("http server error: %v", err) } } else { log.Printf("WoL server listening on %s (DB: %s)", ":"+HTTP_PORT, dbDsn) if err := http.ListenAndServe(":"+HTTP_PORT, withSecurity(mux)); err != nil { log.Fatalf("http server error: %v", err) } } } func handleIndex(w http.ResponseWriter, r *http.Request) { st := collectStatus() w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := indexTmpl.Execute(w, st); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } func handleAPIStatus(w http.ResponseWriter, r *http.Request) { st := collectStatus() w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(st) } func collectStatus() Status { st := Status{ StartedAt: startedAt, Now: time.Now(), DBPath: dbDsn, } // guild count (from guild_config) if row := db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='guild_config'`); row != nil { var exists int _ = row.Scan(&exists) if exists == 1 { row2 := db.QueryRow(`SELECT COUNT(*) FROM guild_config`) _ = row2.Scan(&st.GuildCount) } } // recent events if table exists st.HasEventTable = tableExists("eventlog") if st.HasEventTable { rows, err := db.Query(`SELECT ts, guild_id, type, COALESCE(channel_id,''), COALESCE(user_id,''), COALESCE(extra,'') FROM eventlog ORDER BY ts DESC LIMIT 25`) if err == nil { defer rows.Close() for rows.Next() { var e EventRow if err := rows.Scan(&e.TS, &e.GuildID, &e.Type, &e.ChannelID, &e.UserID, &e.Extra); err == nil { st.RecentEvents = append(st.RecentEvents, e) } } } } return st } func tableExists(name string) bool { row := db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?`, name) var n int if err := row.Scan(&n); err != nil { return false } return n > 0 } func withSecurity(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Simple security headers w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("X-XSS-Protection", "1; mode=block") next.ServeHTTP(w, r) }) } var indexTmpl = template.Must(template.New("index").Parse(` VocalForge · Status
Uptime

Started: {{ .StartedAt.Format "2006-01-02 15:04:05" }}
Guilds

{{ .GuildCount }}

from guild_config
Event Log
{{ if .HasEventTable }}

{{ len .RecentEvents }} recent

from eventlog {{ else }}

N/A

create table eventlog to enable {{ end }}
Recent Events
{{ if .HasEventTable }}
{{ range .RecentEvents }} {{ end }} {{ if not .RecentEvents }}{{ end }}
Time Guild Type Channel User Extra
{{ .GuildID }} {{ .Type }} {{ .ChannelID }} {{ .UserID }} {{ .Extra }}
No events yet.
{{ else }}

Event table not present.

{{ end }}
`))