e40dccbb90
- Remove GitHub Actions workflows (ci.yml, docker.yml, release.yml) - Remove GoReleaser configuration - Remove RELEASE.md (replaced by /release_strix skill) - Add HA options.json support in config.go (reads /data/options.json) - Add Version field to Config, pass real version to health endpoint - Change Version from const to var, inject via ldflags at build time - Add ARG VERSION to Dockerfile for build-time version injection - Reset webui/package.json version to 0.0.0 (not used functionally) - Clear probe fields on back navigation in frontend - Add /release_strix and /release_strix_dev skills
262 lines
6.7 KiB
Go
262 lines
6.7 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Config holds application configuration
|
|
type Config struct {
|
|
Version string // Application version, set by caller after Load()
|
|
Server ServerConfig
|
|
Database DatabaseConfig
|
|
Scanner ScannerConfig
|
|
Logger LoggerConfig
|
|
}
|
|
|
|
// ServerConfig contains HTTP server settings
|
|
type ServerConfig struct {
|
|
Listen string // Address to listen on (e.g., ":4567" or "0.0.0.0:4567")
|
|
ReadTimeout time.Duration
|
|
WriteTimeout time.Duration
|
|
}
|
|
|
|
// DatabaseConfig contains database settings
|
|
type DatabaseConfig struct {
|
|
DataPath string
|
|
BrandsPath string
|
|
PatternsPath string
|
|
ParametersPath string
|
|
OUIPath string
|
|
CacheEnabled bool
|
|
CacheTTL time.Duration
|
|
}
|
|
|
|
// ScannerConfig contains stream scanner settings
|
|
type ScannerConfig struct {
|
|
DefaultTimeout time.Duration
|
|
MaxStreams int
|
|
ModelSearchLimit int
|
|
WorkerPoolSize int
|
|
FFProbeTimeout time.Duration
|
|
RetryAttempts int
|
|
RetryDelay time.Duration
|
|
// Validation settings
|
|
StrictValidation bool // Enable strict validation mode
|
|
MinImageSize int // Minimum bytes for valid image (JPEG/PNG)
|
|
MinVideoStreams int // Minimum video streams required
|
|
}
|
|
|
|
// LoggerConfig contains logging settings
|
|
type LoggerConfig struct {
|
|
Level string
|
|
Format string // "text" or "json"
|
|
}
|
|
|
|
// yamlConfig represents the structure of strix.yaml
|
|
type yamlConfig struct {
|
|
API struct {
|
|
Listen string `yaml:"listen"`
|
|
} `yaml:"api"`
|
|
}
|
|
|
|
// Load returns configuration with defaults
|
|
func Load() *Config {
|
|
dataPath := getEnv("STRIX_DATA_PATH", "./data")
|
|
|
|
cfg := &Config{
|
|
Server: ServerConfig{
|
|
Listen: ":4567", // Default listen address
|
|
ReadTimeout: 30 * time.Second,
|
|
WriteTimeout: 5 * time.Minute, // Increased for SSE long-polling
|
|
},
|
|
Database: DatabaseConfig{
|
|
DataPath: dataPath,
|
|
BrandsPath: filepath.Join(dataPath, "brands"),
|
|
PatternsPath: filepath.Join(dataPath, "popular_stream_patterns.json"),
|
|
ParametersPath: filepath.Join(dataPath, "query_parameters.json"),
|
|
OUIPath: filepath.Join(dataPath, "camera_oui.json"),
|
|
CacheEnabled: true,
|
|
CacheTTL: 5 * time.Minute,
|
|
},
|
|
Scanner: ScannerConfig{
|
|
DefaultTimeout: 4 * time.Minute,
|
|
MaxStreams: 10,
|
|
ModelSearchLimit: 6,
|
|
WorkerPoolSize: 20,
|
|
FFProbeTimeout: 30 * time.Second,
|
|
RetryAttempts: 2,
|
|
RetryDelay: 500 * time.Millisecond,
|
|
// Strict validation enabled by default
|
|
StrictValidation: true,
|
|
MinImageSize: 5120, // 5KB minimum for valid images
|
|
MinVideoStreams: 1, // At least 1 video stream required
|
|
},
|
|
Logger: LoggerConfig{
|
|
Level: getEnv("STRIX_LOG_LEVEL", "info"),
|
|
Format: getEnv("STRIX_LOG_FORMAT", "json"),
|
|
},
|
|
}
|
|
|
|
// Load from Home Assistant options.json if running as HA add-on
|
|
// Priority: defaults < HA options < strix.yaml < ENV
|
|
configSource := "default"
|
|
if err := loadHAOptions(cfg); err == nil {
|
|
configSource = "/data/options.json (Home Assistant)"
|
|
}
|
|
|
|
// Load from strix.yaml if exists (overrides HA options)
|
|
if err := loadYAML(cfg); err == nil {
|
|
configSource = "strix.yaml"
|
|
}
|
|
|
|
// Environment variable overrides everything
|
|
if envListen := os.Getenv("STRIX_API_LISTEN"); envListen != "" {
|
|
cfg.Server.Listen = envListen
|
|
configSource = "environment variable STRIX_API_LISTEN"
|
|
}
|
|
|
|
// Validate listen address
|
|
if err := validateListen(cfg.Server.Listen); err != nil {
|
|
fmt.Fprintf(os.Stderr, "ERROR: Invalid listen address '%s': %v\n", cfg.Server.Listen, err)
|
|
fmt.Fprintf(os.Stderr, "Using default: :4567\n")
|
|
cfg.Server.Listen = ":4567"
|
|
configSource = "default (validation failed)"
|
|
}
|
|
|
|
// Log configuration source
|
|
fmt.Printf("INFO: API listen address '%s' loaded from %s\n", cfg.Server.Listen, configSource)
|
|
|
|
return cfg
|
|
}
|
|
|
|
// loadYAML attempts to load configuration from strix.yaml
|
|
func loadYAML(cfg *Config) error {
|
|
data, err := os.ReadFile("./strix.yaml")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var yamlCfg yamlConfig
|
|
if err := yaml.Unmarshal(data, &yamlCfg); err != nil {
|
|
return fmt.Errorf("failed to parse strix.yaml: %w", err)
|
|
}
|
|
|
|
// Apply yaml configuration
|
|
if yamlCfg.API.Listen != "" {
|
|
cfg.Server.Listen = yamlCfg.API.Listen
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// haOptions represents the structure of Home Assistant /data/options.json.
|
|
// When Strix runs as a Home Assistant add-on, HA creates this file from the
|
|
// add-on configuration UI. Fields are optional -- zero values are ignored.
|
|
type haOptions struct {
|
|
LogLevel string `json:"log_level"`
|
|
Port int `json:"port"`
|
|
}
|
|
|
|
// loadHAOptions loads configuration from Home Assistant's /data/options.json.
|
|
// This file only exists when running inside the HA add-on environment.
|
|
// Returns an error if the file doesn't exist or can't be parsed (callers
|
|
// should treat errors as "not running in HA" and silently continue).
|
|
func loadHAOptions(cfg *Config) error {
|
|
data, err := os.ReadFile("/data/options.json")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var opts haOptions
|
|
if err := json.Unmarshal(data, &opts); err != nil {
|
|
return fmt.Errorf("failed to parse /data/options.json: %w", err)
|
|
}
|
|
|
|
if opts.LogLevel != "" {
|
|
cfg.Logger.Level = opts.LogLevel
|
|
}
|
|
if opts.Port > 0 {
|
|
cfg.Server.Listen = fmt.Sprintf(":%d", opts.Port)
|
|
}
|
|
|
|
// Home Assistant add-on always uses JSON logging for the HA log viewer
|
|
cfg.Logger.Format = "json"
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateListen validates the listen address format and port range
|
|
func validateListen(listen string) error {
|
|
if listen == "" {
|
|
return fmt.Errorf("listen address cannot be empty")
|
|
}
|
|
|
|
// Parse the listen address
|
|
parts := strings.Split(listen, ":")
|
|
if len(parts) < 2 {
|
|
return fmt.Errorf("invalid format, expected ':port' or 'host:port', got '%s'", listen)
|
|
}
|
|
|
|
// Get port (last part)
|
|
portStr := parts[len(parts)-1]
|
|
if portStr == "" {
|
|
return fmt.Errorf("port cannot be empty")
|
|
}
|
|
|
|
// Validate port number
|
|
port, err := strconv.Atoi(portStr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid port number '%s': %w", portStr, err)
|
|
}
|
|
|
|
if port < 1 || port > 65535 {
|
|
return fmt.Errorf("port %d out of valid range (1-65535)", port)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetupLogger configures the global logger
|
|
func (c *Config) SetupLogger() *slog.Logger {
|
|
var level slog.Level
|
|
switch c.Logger.Level {
|
|
case "debug":
|
|
level = slog.LevelDebug
|
|
case "warn":
|
|
level = slog.LevelWarn
|
|
case "error":
|
|
level = slog.LevelError
|
|
default:
|
|
level = slog.LevelInfo
|
|
}
|
|
|
|
opts := &slog.HandlerOptions{
|
|
Level: level,
|
|
}
|
|
|
|
var handler slog.Handler
|
|
if c.Logger.Format == "json" {
|
|
handler = slog.NewJSONHandler(os.Stdout, opts)
|
|
} else {
|
|
handler = slog.NewTextHandler(os.Stdout, opts)
|
|
}
|
|
|
|
return slog.New(handler)
|
|
}
|
|
|
|
func getEnv(key, defaultValue string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|