From b1d69f16971b3f9c08094a7440fe1d433deb00a1 Mon Sep 17 00:00:00 2001 From: Gamer Date: Fri, 17 Apr 2026 19:32:33 +0200 Subject: [PATCH] added modules; user can disable, enable them --- main.go | 408 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 276 insertions(+), 132 deletions(-) diff --git a/main.go b/main.go index d696f05..5bd8cb4 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "html/template" + "io/fs" "log" "math" "net/http" @@ -29,7 +30,7 @@ import ( //go:embed static var staticFiles embed.FS -const version = "0.7.1" +const version = "0.8.0" // --------------------------------------------------------------------------- // Config structs @@ -42,6 +43,7 @@ type Config struct { Trend TrendConfig `yaml:"trend"` Press PressConfig `yaml:"press"` UI UIConfig `yaml:"ui"` + Modules ModulesConfig `yaml:"modules"` DB DBConfig `yaml:"db"` } @@ -76,9 +78,6 @@ type TrendConfig struct { Minutes int `yaml:"minutes"` } -// PressConfig: Go field is MaxTonnage (idiomatic). YAML tag kept as MAX_TONNAGE -// so existing config files need no changes. LegacyMaxTonnage handles old -// configs that used the lowercase "max_tonnage" key. type PressConfig struct { MaxTonnage float64 `yaml:"MAX_TONNAGE"` LegacyMaxTonnage float64 `yaml:"max_tonnage,omitempty"` @@ -93,6 +92,17 @@ type UIConfig struct { UnitPct string `yaml:"unit_percent"` } +type ModulesConfig struct { + ShowHeaderControls *bool `yaml:"show_header_controls,omitempty"` + ShowVerdict *bool `yaml:"show_verdict,omitempty"` + ShowSummaryBar *bool `yaml:"show_summary_bar,omitempty"` + ShowOverview *bool `yaml:"show_overview,omitempty"` + ShowIntelligence *bool `yaml:"show_intelligence,omitempty"` + ShowAlarmTimeline *bool `yaml:"show_alarm_timeline,omitempty"` + ShowGauges *bool `yaml:"show_gauges,omitempty"` + ShowTrendChart *bool `yaml:"show_trend_chart,omitempty"` +} + type DBConfig struct { Path string `yaml:"path"` BusyTimeoutMs int `yaml:"busy_timeout_ms"` @@ -106,6 +116,17 @@ type DBConfig struct { CleanupIntervalHr int `yaml:"cleanup_interval_hours"` } +func boolPtr(v bool) *bool { + return &v +} + +func boolValue(v *bool, def bool) bool { + if v == nil { + return def + } + return *v +} + func defaultConfig() Config { return Config{ Server: ServerConfig{ @@ -142,6 +163,16 @@ func defaultConfig() Config { UnitForce: "kN", UnitPct: "%", }, + Modules: ModulesConfig{ + ShowHeaderControls: boolPtr(true), + ShowVerdict: boolPtr(true), + ShowSummaryBar: boolPtr(true), + ShowOverview: boolPtr(true), + ShowIntelligence: boolPtr(true), + ShowAlarmTimeline: boolPtr(true), + ShowGauges: boolPtr(true), + ShowTrendChart: boolPtr(true), + }, DB: DBConfig{ Path: "force_monitor.db", BusyTimeoutMs: 5000, @@ -158,7 +189,7 @@ func defaultConfig() Config { } // --------------------------------------------------------------------------- -// Config normalisation helpers — eliminate repetitive if-chains +// Config normalisation helpers // --------------------------------------------------------------------------- func setIfZeroF(dst *float64, def float64) { @@ -179,6 +210,13 @@ func setIfEmpty(dst *string, def string) { } } +func setIfNilBool(dst **bool, def bool) { + if *dst == nil { + v := def + *dst = &v + } +} + func normalizeConfig(cfg *Config) { def := defaultConfig() @@ -186,12 +224,13 @@ func normalizeConfig(cfg *Config) { setIfEmpty(&cfg.PLC.IP, def.PLC.IP) setIfZeroI(&cfg.PLC.DBNum, def.PLC.DBNum) + setIfZeroI(&cfg.PLC.Rack, def.PLC.Rack) + setIfZeroI(&cfg.PLC.Slot, def.PLC.Slot) setIfZeroI(&cfg.PLC.PollMs, def.PLC.PollMs) setIfZeroI(&cfg.PLC.ConnectTimeoutSec, def.PLC.ConnectTimeoutSec) setIfZeroI(&cfg.PLC.IdleTimeoutSec, def.PLC.IdleTimeoutSec) setIfZeroI(&cfg.PLC.ReconnectDelaySec, def.PLC.ReconnectDelaySec) - // Legacy threshold key migration (warning_kn / critical_kn / max_kn) if cfg.Thresholds.WarningPercent <= 0 && cfg.Thresholds.LegacyWarningKn > 0 { cfg.Thresholds.WarningPercent = cfg.Thresholds.LegacyWarningKn } @@ -214,7 +253,6 @@ func normalizeConfig(cfg *Config) { setIfZeroI(&cfg.Trend.Minutes, def.Trend.Minutes) - // Legacy press tonnage key migration (max_tonnage lowercase) if cfg.Press.MaxTonnage <= 0 && cfg.Press.LegacyMaxTonnage > 0 { cfg.Press.MaxTonnage = cfg.Press.LegacyMaxTonnage } @@ -227,6 +265,15 @@ func normalizeConfig(cfg *Config) { setIfEmpty(&cfg.UI.UnitForce, def.UI.UnitForce) setIfEmpty(&cfg.UI.UnitPct, def.UI.UnitPct) + setIfNilBool(&cfg.Modules.ShowHeaderControls, boolValue(def.Modules.ShowHeaderControls, true)) + setIfNilBool(&cfg.Modules.ShowVerdict, boolValue(def.Modules.ShowVerdict, true)) + setIfNilBool(&cfg.Modules.ShowSummaryBar, boolValue(def.Modules.ShowSummaryBar, true)) + setIfNilBool(&cfg.Modules.ShowOverview, boolValue(def.Modules.ShowOverview, true)) + setIfNilBool(&cfg.Modules.ShowIntelligence, boolValue(def.Modules.ShowIntelligence, true)) + setIfNilBool(&cfg.Modules.ShowAlarmTimeline, boolValue(def.Modules.ShowAlarmTimeline, true)) + setIfNilBool(&cfg.Modules.ShowGauges, boolValue(def.Modules.ShowGauges, true)) + setIfNilBool(&cfg.Modules.ShowTrendChart, boolValue(def.Modules.ShowTrendChart, true)) + setIfEmpty(&cfg.DB.Path, def.DB.Path) setIfZeroI(&cfg.DB.BusyTimeoutMs, def.DB.BusyTimeoutMs) setIfZeroI(&cfg.DB.BatchSize, def.DB.BatchSize) @@ -388,6 +435,15 @@ type PageData struct { PollMs int DefaultWindow string DefaultTrendWindow string + + ShowHeaderControls bool + ShowVerdict bool + ShowSummaryBar bool + ShowOverview bool + ShowIntelligence bool + ShowAlarmTimeline bool + ShowGauges bool + ShowTrendChart bool } type NumericStats struct { @@ -430,11 +486,11 @@ var ( alarmCh chan AlarmEvent alarmTracker AlarmTracker uiTemplate = template.Must(template.New("ui").Parse(uiHTML)) - cachedUI []byte // pre-rendered template (PageData is immutable after startup) + cachedUI []byte ) // --------------------------------------------------------------------------- -// Force calculation — accepts maxTonnage explicitly (testable, no global dep) +// Force calculation // --------------------------------------------------------------------------- func calculateForces(leftPercent, rightPercent float32, maxTonnage float64) (leftKN, rightKN, sumPercent, sumKN float32) { @@ -509,9 +565,6 @@ func enqueueAlarm(a AlarmEvent) { // Database initialisation // --------------------------------------------------------------------------- -// ensureColumn adds a column to tableName if it does not already exist. -// NOTE: tableName and columnName are always hardcoded call-site constants — -// never derived from user input — so fmt.Sprintf is safe here. func ensureColumn(database *sql.DB, tableName, columnName, definition string) error { rows, err := database.Query(fmt.Sprintf("PRAGMA table_info(%s)", tableName)) if err != nil { @@ -646,7 +699,7 @@ CREATE INDEX IF NOT EXISTS idx_alarm_events_ts_unix_ns ON alarm_events(ts_unix_n } // --------------------------------------------------------------------------- -// DB writer goroutines — both now respect config values and context shutdown +// DB writer goroutines // --------------------------------------------------------------------------- func startDBWriter(ctx context.Context, database *sql.DB) { @@ -716,7 +769,6 @@ func startDBWriter(ctx context.Context, database *sql.DB) { for { select { case <-ctx.Done(): - // Drain any remaining queued samples before exit for { select { case s := <-sampleCh: @@ -738,7 +790,6 @@ func startDBWriter(ctx context.Context, database *sql.DB) { } func startAlarmWriter(ctx context.Context, database *sql.DB) { - // BUG FIX: was hardcoded 1000ms / 32 — now uses the same config values as startDBWriter ticker := time.NewTicker(time.Duration(cfg.DB.FlushIntervalMs) * time.Millisecond) defer ticker.Stop() @@ -803,7 +854,6 @@ func startAlarmWriter(ctx context.Context, database *sql.DB) { for { select { case <-ctx.Done(): - // Drain remaining alarm events before exit for { select { case a := <-alarmCh: @@ -1046,7 +1096,6 @@ func startPLCPoller(ctx context.Context) { reconnectDelay := time.Duration(cfg.PLC.ReconnectDelaySec) * time.Second for { - // Check for shutdown before attempting a new connection select { case <-ctx.Done(): return @@ -1073,8 +1122,6 @@ func startPLCPoller(ctx context.Context) { client := gos7.NewClient(handler) log.Println("PLC connected successfully") - // BUG FIX: buf was allocated inside the inner loop, causing a heap - // allocation every poll cycle. Moved outside — reused each iteration. buf := make([]byte, 8) for { @@ -1277,13 +1324,6 @@ func queryNumericStats(field string, fromNs, toNs int64) (NumericStats, error) { // Trend / stability classification // --------------------------------------------------------------------------- -// classifyDirection is a single generic direction classifier that replaces -// the two near-identical classifyForceDirection / classifyImbalanceDirection -// functions that existed previously. -// -// stableThreshold — abs(delta) below this value → "stable" -// posLabel — label when delta > threshold (e.g. "rising", "worsening") -// negLabel — label when delta < -threshold (e.g. "falling", "improving") func classifyDirection(delta float64, oldCount, newCount int, stableThreshold float64, posLabel, negLabel string) string { if oldCount < 3 || newCount < 3 { return "insufficient_data" @@ -1538,14 +1578,11 @@ func apiAlarms(w http.ResponseWriter, r *http.Request) { }) } -// serveUI serves the pre-rendered UI page. PageData is derived solely from -// the immutable config, so we render the template once at startup and reuse. func serveUI(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write(cachedUI) } -// initCachedUI renders the HTML template once at startup. func initCachedUI() { data := PageData{ Title: cfg.UI.Title, @@ -1563,6 +1600,15 @@ func initCachedUI() { PollMs: cfg.PLC.PollMs, DefaultWindow: fmt.Sprintf("%dm", cfg.Trend.Minutes), DefaultTrendWindow: "15m", + + ShowHeaderControls: boolValue(cfg.Modules.ShowHeaderControls, true), + ShowVerdict: boolValue(cfg.Modules.ShowVerdict, true), + ShowSummaryBar: boolValue(cfg.Modules.ShowSummaryBar, true), + ShowOverview: boolValue(cfg.Modules.ShowOverview, true), + ShowIntelligence: boolValue(cfg.Modules.ShowIntelligence, true), + ShowAlarmTimeline: boolValue(cfg.Modules.ShowAlarmTimeline, true), + ShowGauges: boolValue(cfg.Modules.ShowGauges, true), + ShowTrendChart: boolValue(cfg.Modules.ShowTrendChart, true), } var buf bytes.Buffer @@ -1600,7 +1646,7 @@ func main() { defer db.Close() sampleCh = make(chan Sample, cfg.DB.WriterQueueSize) - alarmCh = make(chan AlarmEvent, cfg.DB.AlarmQueueSize) // BUG FIX: was hardcoded 512 + alarmCh = make(chan AlarmEvent, cfg.DB.AlarmQueueSize) initCachedUI() @@ -1611,7 +1657,6 @@ func main() { cfg.PLC.IP, cfg.PLC.DBNum, cfg.PLC.Rack, cfg.PLC.Slot, cfg.PLC.PollMs) log.Printf("Press: MAX_TONNAGE=%.2f %s", cfg.Press.MaxTonnage, cfg.UI.UnitForce) - // Graceful shutdown via SIGINT / SIGTERM ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() @@ -1622,8 +1667,13 @@ func main() { go func() { defer wg.Done(); startDBCleanup(ctx, db) }() go func() { defer wg.Done(); startPLCPoller(ctx) }() + staticRoot, err := fs.Sub(staticFiles, "static") + if err != nil { + log.Fatalf("failed to mount embedded static files: %v", err) + } + mux := http.NewServeMux() - mux.Handle("/static/", http.FileServer(http.FS(staticFiles))) + mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticRoot)))) mux.HandleFunc("/", serveUI) mux.HandleFunc("/api/data", apiData) mux.HandleFunc("/api/history", apiHistory) @@ -2025,10 +2075,12 @@ const uiHTML = `
+ {{if .ShowHeaderControls}}
+ {{end}}
@@ -2043,6 +2095,7 @@ const uiHTML = `
+ {{if .ShowVerdict}}
Machine verdict
@@ -2050,7 +2103,9 @@ const uiHTML = `
Waiting for PLC data
+ {{end}} + {{if .ShowSummaryBar}}
@@ -2085,7 +2140,9 @@ const uiHTML = `
Disconnected
+ {{end}} + {{if .ShowOverview}}
@@ -2122,7 +2179,9 @@ const uiHTML = `
+ {{end}} + {{if .ShowIntelligence}}
@@ -2184,7 +2243,9 @@ const uiHTML = `
+ {{end}} + {{if .ShowAlarmTimeline}}
@@ -2214,7 +2275,9 @@ const uiHTML = `
+ {{end}} + {{if .ShowGauges}}
@@ -2260,7 +2323,9 @@ const uiHTML = `
+ {{end}} + {{if .ShowTrendChart}}
@@ -2289,6 +2354,7 @@ const uiHTML = `
+ {{end}}