// 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 }}
Time |
Guild |
Type |
Channel |
User |
Extra |
{{ range .RecentEvents }}
|
{{ .GuildID }} |
{{ .Type }} |
{{ .ChannelID }} |
{{ .UserID }} |
{{ .Extra }} |
{{ end }}
{{ if not .RecentEvents }}No events yet. |
{{ end }}
{{ else }}
Event table not present.
{{ end }}
`))