Initial commit

This commit is contained in:
Martino Ferrari
2026-04-24 15:09:14 +02:00
commit 9aa89cc0cf
34 changed files with 3378 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
package config
import (
"fmt"
"os"
"strings"
"github.com/BurntSushi/toml"
)
type Config struct {
Server ServerConfig `toml:"server"`
EPICS EPICSConfig `toml:"datasource.epics"`
Synthetic SyntheticConfig `toml:"datasource.synthetic"`
}
type ServerConfig struct {
Listen string `toml:"listen"`
StorageDir string `toml:"storage_dir"`
}
type EPICSConfig struct {
Enabled bool `toml:"enabled"`
CAAddrList string `toml:"ca_addr_list"`
ArchiveURL string `toml:"archive_url"`
}
type SyntheticConfig struct {
Enabled bool `toml:"enabled"`
DefinitionsFile string `toml:"definitions_file"`
}
func Default() Config {
return Config{
Server: ServerConfig{
Listen: ":8080",
StorageDir: "./interfaces",
},
EPICS: EPICSConfig{
Enabled: true,
},
Synthetic: SyntheticConfig{
Enabled: true,
DefinitionsFile: "./synthetic.json",
},
}
}
// Load reads a TOML config file and applies environment variable overrides.
// Environment variables use the prefix UOPI_ with __ as separator, e.g.
// UOPI_SERVER_LISTEN=":9090".
func Load(path string) (Config, error) {
cfg := Default()
if path != "" {
if _, err := toml.DecodeFile(path, &cfg); err != nil {
return cfg, fmt.Errorf("reading config %q: %w", path, err)
}
}
applyEnv(&cfg)
return cfg, nil
}
func applyEnv(cfg *Config) {
if v := env("UOPI_SERVER_LISTEN"); v != "" {
cfg.Server.Listen = v
}
if v := env("UOPI_SERVER_STORAGE_DIR"); v != "" {
cfg.Server.StorageDir = v
}
if v := env("UOPI_EPICS_CA_ADDR_LIST"); v != "" {
cfg.EPICS.CAAddrList = v
}
if v := env("UOPI_EPICS_ARCHIVE_URL"); v != "" {
cfg.EPICS.ArchiveURL = v
}
if v := env("UOPI_SYNTHETIC_DEFINITIONS_FILE"); v != "" {
cfg.Synthetic.DefinitionsFile = v
}
}
func env(key string) string {
return strings.TrimSpace(os.Getenv(key))
}
+63
View File
@@ -0,0 +1,63 @@
package server
import (
"context"
"io/fs"
"log/slog"
"net/http"
"time"
)
type Server struct {
httpServer *http.Server
log *slog.Logger
}
// New creates an HTTP server that serves the embedded frontend and a health
// check endpoint. Additional routes (API, WebSocket) will be registered in
// later phases.
func New(addr string, webFS fs.FS, log *slog.Logger) *Server {
mux := http.NewServeMux()
// Health check
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
// Serve embedded frontend for everything else
mux.Handle("/", http.FileServerFS(webFS))
return &Server{
httpServer: &http.Server{
Addr: addr,
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
},
log: log,
}
}
// Start listens and serves until ctx is cancelled.
func (s *Server) Start(ctx context.Context) error {
s.log.Info("listening", "addr", s.httpServer.Addr)
errCh := make(chan error, 1)
go func() {
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- err
}
}()
select {
case err := <-errCh:
return err
case <-ctx.Done():
shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
s.log.Info("shutting down")
return s.httpServer.Shutdown(shutCtx)
}
}