Explorar o código

3k release: massively improve config loading, reduce log spam, etc (#700)

James Read hai 8 meses
pai
achega
209856eda9

+ 10 - 13
frontend/resources/vue/App.vue

@@ -8,11 +8,11 @@
 
         <template #user-info>
             <div class="flex-row user-info" style="gap: .5em;">
-                <span id="link-login" v-if="!isLoggedIn"><router-link to="/login">Login</router-link></span>
-                <router-link v-else to="/user" class="user-link">
+                <span id="link-login" v-if="!isLoggedIn && showLoginLink"><router-link to="/login">Login</router-link></span>
+                <router-link v-else to="/user" class="user-link" v-if="isLoggedIn">
                     <span id="username-text">{{ username }}</span>
                 </router-link>
-                <HugeiconsIcon :icon="UserCircle02Icon" width = "1.5em" height = "1.5em" />
+                <HugeiconsIcon :icon="UserCircle02Icon" width = "1.5em" height = "1.5em" v-if="isLoggedIn" />
             </div>
 
         </template>
@@ -71,7 +71,7 @@ import logoUrl from '../../OliveTinLogo.png';
 const router = useRouter();
 
 const sidebar = ref(null);
-const username = ref('guest');
+const username = ref('notset');
 const isLoggedIn = ref(false);
 const serverConnection = ref('Connected');
 const currentVersion = ref('?');
@@ -84,6 +84,7 @@ const showLogs = ref(true)
 const showDiagnostics = ref(true)
 const initError = ref(false)
 const initErrorMessage = ref('')
+const showLoginLink = ref(true)
 
 function toggleSidebar() {
     if (sidebar.value && showNavigation.value) {
@@ -102,6 +103,10 @@ function updateHeaderFromInit() {
         showNavigation.value = window.initResponse.showNavigation
         showLogs.value = window.initResponse.showLogList
         showDiagnostics.value = window.initResponse.showDiagnostics
+
+        if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
+            showLoginLink.value = false
+        }
     }
 }
 
@@ -124,15 +129,7 @@ async function requestInit() {
         window.initErrorMessage = ''
         window.initCompleted = true
 
-        username.value = initResponse.authenticatedUser
-        isLoggedIn.value = initResponse.authenticatedUser !== '' && initResponse.authenticatedUser !== 'guest'
-        currentVersion.value = initResponse.currentVersion
-		bannerMessage.value = initResponse.bannerMessage || '';
-		bannerCss.value = initResponse.bannerCss || '';
-		showFooter.value = initResponse.showFooter
-        showNavigation.value = initResponse.showNavigation
-        showLogs.value = initResponse.showLogList
-        showDiagnostics.value = initResponse.showDiagnostics
+        window.updateHeaderFromInit()
 
         if (showNavigation.value && sidebar.value) {
             for (const rootDashboard of initResponse.rootDashboards) {

+ 2 - 0
integration-tests/proxies/haproxy/Makefile

@@ -0,0 +1,2 @@
+make:
+	haproxy -f ./haproxy.conf

+ 25 - 8
integration-tests/proxies/haproxy/haproxy.conf

@@ -1,15 +1,32 @@
+global
+    stats socket /var/run/haproxy/admin.sock mode 660 level admin
+    log stdout local0
+
+
 frontend cleartext_frontend
-    bind 0.0.0.0:80
+    bind 0.0.0.0:8080
 
-    option httplog
+    timeout client 10s
 
-    use_backend be_olivetin_webs if { hdr(Host) -i olivetin.example.com && path_beg /websocket }
-    use_backend be_olivetin_http if { hdr(Host) -i olivetin.example.com }
+    stats enable
+    stats uri /stats
+    stats refresh 30s
 
-backend be_olivetin_http
-    server olivetinServer 127.0.0.1:1337 check
+    log global
+    option httplog
+    option dontlognull
+    log-format "[%t] %ci:%cp -> BACKEND:%b/%s | %HM %HU | Status:%ST | Bytes:%B"
+    mode http
+
+    use_backend be_olivetin if { hdr(Host) -i -m beg olivetin.example.com }
 
-backend be_olivetin_webs
+backend be_olivetin
+    mode http
+    timeout connect 10s
+    timeout server 10s
     timeout tunnel 1h
     option http-server-close
-    server olivetinServer 127.0.0.1:1337
+    http-request set-header X-Forwarded-User "Alice"
+    http-request set-header X-Forwarded-Group "Group1,Group2"
+    server olivetinServer 127.0.0.1:1337 check
+

+ 8 - 1
service/internal/acl/acl.go

@@ -208,12 +208,19 @@ func UserFromContext[T any](ctx context.Context, req *connect.Request[T], cfg *c
 	} else {
 		buildUserAcls(cfg, &user)
 	}
+
+	path := ""
+	if req != nil {
+		path = req.Spec().Procedure
+	}
+
 	log.WithFields(log.Fields{
 		"username":      user.Username,
 		"usergroupLine": user.UsergroupLine,
 		"provider":      user.Provider,
 		"acls":          user.Acls,
-	}).Debugf("UserFromContext")
+		"path":          path,
+	}).Debugf("Authenticated API request")
 	return &user
 }
 

+ 18 - 12
service/internal/api/api.go

@@ -481,16 +481,20 @@ func (api *oliveTinAPI) GetActionLogs(ctx ctx.Context, req *connect.Request[apiv
 		ret.StartOffset = page.start
 		return connect.NewResponse(ret), nil
 	}
-    // Newest-first slicing: compute reversed indices
-    startIdx := page.total - page.end
-    endIdx := page.total - page.start
-    if startIdx < 0 { startIdx = 0 }
-    if endIdx > int64(len(filtered)) { endIdx = int64(len(filtered)) }
-    for _, le := range filtered[startIdx:endIdx] {
-        ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
-    }
-    // Entries older than the returned newest page
-    ret.CountRemaining = page.start
+	// Newest-first slicing: compute reversed indices
+	startIdx := page.total - page.end
+	endIdx := page.total - page.start
+	if startIdx < 0 {
+		startIdx = 0
+	}
+	if endIdx > int64(len(filtered)) {
+		endIdx = int64(len(filtered))
+	}
+	for _, le := range filtered[startIdx:endIdx] {
+		ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
+	}
+	// Entries older than the returned newest page
+	ret.CountRemaining = page.start
 	ret.PageSize = page.size
 	ret.TotalCount = page.total
 	ret.StartOffset = page.start
@@ -662,7 +666,9 @@ func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.
 		AuthenticatedUser: user,
 	}
 
-	log.Infof("EventStream: client connected: %v", client.AuthenticatedUser.Username)
+	log.WithFields(log.Fields{
+		"authenticatedUser": user.Username,
+	}).Debugf("EventStream: client connected")
 
 	api.streamingClientsMutex.Lock()
 	api.streamingClients[client] = struct{}{}
@@ -814,7 +820,7 @@ func (api *oliveTinAPI) buildRootDashboards(user *acl.AuthenticatedUser, dashboa
 func (api *oliveTinAPI) addDefaultDashboardIfNeeded(rootDashboards *[]string, rr *DashboardRenderRequest) {
 	defaultDashboard := buildDefaultDashboard(rr)
 	if defaultDashboard != nil && len(defaultDashboard.Contents) > 0 {
-		log.Infof("defaultDashboard: %+v", defaultDashboard.Contents)
+		log.Tracef("defaultDashboard: %+v", defaultDashboard.Contents)
 		*rootDashboards = append(*rootDashboards, "Actions")
 	}
 }

+ 147 - 147
service/internal/config/config.go

@@ -7,212 +7,212 @@ import (
 // Action represents the core functionality of OliveTin - commands that show up
 // as buttons in the UI.
 type Action struct {
-	ID                     string
-	Title                  string
-	Icon                   string
-	Shell                  string
-	Exec                   []string
-	ShellAfterCompleted    string
-	Timeout                int
-	Acls                   []string
-	Entity                 string
-	Hidden                 bool
-	ExecOnStartup          bool
-	ExecOnCron             []string
-	ExecOnFileCreatedInDir []string
-	ExecOnFileChangedInDir []string
-	ExecOnCalendarFile     string
-	Triggers               []string
-	MaxConcurrent          int
-	MaxRate                []RateSpec
-	Arguments              []ActionArgument
-	PopupOnStart           string
-	SaveLogs               SaveLogsConfig
+	ID                     string           `koanf:"id"`
+	Title                  string           `koanf:"title"`
+	Icon                   string           `koanf:"icon"`
+	Shell                  string           `koanf:"shell"`
+	Exec                   []string         `koanf:"exec"`
+	ShellAfterCompleted    string           `koanf:"shellAfterCompleted"`
+	Timeout                int              `koanf:"timeout"`
+	Acls                   []string         `koanf:"acls"`
+	Entity                 string           `koanf:"entity"`
+	Hidden                 bool             `koanf:"hidden"`
+	ExecOnStartup          bool             `koanf:"execOnStartup"`
+	ExecOnCron             []string         `koanf:"execOnCron"`
+	ExecOnFileCreatedInDir []string         `koanf:"execOnFileCreatedInDir"`
+	ExecOnFileChangedInDir []string         `koanf:"execOnFileChangedInDir"`
+	ExecOnCalendarFile     string           `koanf:"execOnCalendarFile"`
+	Triggers               []string         `koanf:"triggers"`
+	MaxConcurrent          int              `koanf:"maxConcurrent"`
+	MaxRate                []RateSpec       `koanf:"maxRate"`
+	Arguments              []ActionArgument `koanf:"arguments"`
+	PopupOnStart           string           `koanf:"popupOnStart"`
+	SaveLogs               SaveLogsConfig   `koanf:"saveLogs"`
 }
 
 // ActionArgument objects appear on Actions.
 type ActionArgument struct {
-	Name        string
-	Title       string
-	Description string
-	Type        string
-	Default     string
-	Choices     []ActionArgumentChoice
-	Entity      string
-	RejectNull  bool
-	Suggestions map[string]string
+	Name        string                 `koanf:"name"`
+	Title       string                 `koanf:"title"`
+	Description string                 `koanf:"description"`
+	Type        string                 `koanf:"type"`
+	Default     string                 `koanf:"default"`
+	Choices     []ActionArgumentChoice `koanf:"choices"`
+	Entity      string                 `koanf:"entity"`
+	RejectNull  bool                   `koanf:"rejectNull"`
+	Suggestions map[string]string      `koanf:"suggestions"`
 }
 
 // ActionArgumentChoice represents a predefined choice for an argument.
 type ActionArgumentChoice struct {
-	Value string
-	Title string
+	Value string `koanf:"value"`
+	Title string `koanf:"title"`
 }
 
 // RateSpec allows you to set a max frequency for an action.
 type RateSpec struct {
-	Limit    int
-	Duration string
+	Limit    int    `koanf:"limit"`
+	Duration string `koanf:"duration"`
 }
 
 // Entity represents a "thing" that can have multiple actions associated with it.
 // for example, a media player with a start and stop action.
 type EntityFile struct {
-	File string
-	Name string
-	Icon string
+	File string `koanf:"file"`
+	Name string `koanf:"name"`
+	Icon string `koanf:"icon"`
 }
 
 // PermissionsList defines what users can do with an action.
 type PermissionsList struct {
-	View bool `mapstructure:"view"`
-	Exec bool `mapstructure:"exec"`
-	Logs bool `mapstructure:"logs"`
-	Kill bool `mapstructure:"kill"`
+	View bool `koanf:"view"`
+	Exec bool `koanf:"exec"`
+	Logs bool `koanf:"logs"`
+	Kill bool `koanf:"kill"`
 }
 
 // AccessControlList defines what permissions apply to a user or user group.
 type AccessControlList struct {
-	Name             string
-	AddToEveryAction bool
-	MatchUsergroups  []string
-	MatchUsernames   []string
-	Permissions      PermissionsList
-	Policy           ConfigurationPolicy
+	Name             string              `koanf:"name"`
+	AddToEveryAction bool                `koanf:"addToEveryAction"`
+	MatchUsergroups  []string            `koanf:"matchUsergroups"`
+	MatchUsernames   []string            `koanf:"matchUsernames"`
+	Permissions      PermissionsList     `koanf:"permissions"`
+	Policy           ConfigurationPolicy `koanf:"policy"`
 }
 
 // ConfigurationPolicy defines global settings which are overridden with an ACL.
 type ConfigurationPolicy struct {
-	ShowDiagnostics bool `mapstructure:"showDiagnostics"`
-	ShowLogList     bool `mapstructure:"showLogList"`
+	ShowDiagnostics bool `koanf:"showDiagnostics"`
+	ShowLogList     bool `koanf:"showLogList"`
 }
 
 type PrometheusConfig struct {
-	Enabled          bool `mapstructure:"enabled"`
-	DefaultGoMetrics bool `mapstructure:"defaultGoMetrics"`
+	Enabled          bool `koanf:"enabled"`
+	DefaultGoMetrics bool `koanf:"defaultGoMetrics"`
 }
 
 // Config is the global config used through the whole app.
 type Config struct {
-	UseSingleHTTPFrontend           bool                       `mapstructure:"useSingleHTTPFrontend"`
-	ThemeName                       string                     `mapstructure:"themeName"`
-	ThemeCacheDisabled              bool                       `mapstructure:"themeCacheDisabled"`
-	ListenAddressSingleHTTPFrontend string                     `mapstructure:"listenAddressSingleHTTPFrontend"`
-	ListenAddressWebUI              string                     `mapstructure:"listenAddressWebUI"`
-	ListenAddressRestActions        string                     `mapstructure:"listenAddressRestActions"`
-	ListenAddressPrometheus         string                     `mapstructure:"listenAddressPrometheus"`
-	ExternalRestAddress             string                     `mapstructure:"externalRestAddress"`
-	LogLevel                        string                     `mapstructure:"logLevel"`
-	LogDebugOptions                 LogDebugOptions            `mapstructure:"logDebugOptions"`
-	LogHistoryPageSize              int64                      `mapstructure:"logHistoryPageSize"`
-	Actions                         []*Action                  `mapstructure:"actions"`
-	Entities                        []*EntityFile              `mapstructure:"entities"`
-	Dashboards                      []*DashboardComponent      `mapstructure:"dashboards"`
-	CheckForUpdates                 bool                       `mapstructure:"checkForUpdates"`
-	PageTitle                       string                     `mapstructure:"pageTitle"`
-	ShowFooter                      bool                       `mapstructure:"showFooter"`
-	ShowNavigation                  bool                       `mapstructure:"showNavigation"`
-	ShowNewVersions                 bool                       `mapstructure:"showNewVersions"`
-	EnableCustomJs                  bool                       `mapstructure:"enableCustomJs"`
-	AuthJwtCookieName               string                     `mapstructure:"authJwtCookieName"`
-	AuthJwtHeader                   string                     `mapstructure:"authJwtHeader"`
-	AuthJwtAud                      string                     `mapstructure:"authJwtAud"`
-	AuthJwtDomain                   string                     `mapstructure:"authJwtDomain"`
-	AuthJwtCertsURL                 string                     `mapstructure:"authJwtCertsUrl"`
-	AuthJwtHmacSecret               string                     `mapstructure:"authJwtHmacSecret"` // mutually exclusive with pub key config fields
-	AuthJwtClaimUsername            string                     `mapstructure:"authJwtClaimUsername"`
-	AuthJwtClaimUserGroup           string                     `mapstructure:"authJwtClaimUserGroup"`
-	AuthJwtPubKeyPath               string                     `mapstructure:"authJwtPubKeyPath"` // will read pub key from file on disk
-	AuthHttpHeaderUsername          string                     `mapstructure:"authHttpHeaderUsername"`
-	AuthHttpHeaderUserGroup         string                     `mapstructure:"authHttpHeaderUserGroup"`
-	AuthHttpHeaderUserGroupSep      string                     `mapstructure:"authHttpHeaderUserGroupSep"`
-	AuthLocalUsers                  AuthLocalUsersConfig       `mapstructure:"authLocalUsers"`
-	AuthLoginUrl                    string                     `mapstructure:"authLoginUrl"`
-	AuthRequireGuestsToLogin        bool                       `mapstructure:"authRequireGuestsToLogin"`
-	AuthOAuth2RedirectURL           string                     `mapstructure:"authOAuth2RedirectUrl"`
-	AuthOAuth2Providers             map[string]*OAuth2Provider `mapstructure:"authOAuth2Providers"`
-	DefaultPermissions              PermissionsList            `mapstructure:"defaultPermissions"`
-	DefaultPolicy                   ConfigurationPolicy        `mapstructure:"defaultPolicy"`
-	AccessControlLists              []*AccessControlList       `mapstructure:"accessControlLists"`
-	WebUIDir                        string                     `mapstructure:"webUIDir"`
-	CronSupportForSeconds           bool                       `mapstructure:"cronSupportForSeconds"`
-	SectionNavigationStyle          string                     `mapstructure:"sectionNavigationStyle"`
-	DefaultPopupOnStart             string                     `mapstructure:"defaultPopupOnStart"`
-	InsecureAllowDumpOAuth2UserData bool                       `mapstructure:"insecureAllowDumpOAuth2UserData"`
-	InsecureAllowDumpVars           bool                       `mapstructure:"insecureAllowDumpVars"`
-	InsecureAllowDumpSos            bool                       `mapstructure:"insecureAllowDumpSos"`
-	InsecureAllowDumpActionMap      bool                       `mapstructure:"insecureAllowDumpActionMap"`
-	InsecureAllowDumpJwtClaims      bool                       `mapstructure:"insecureAllowDumpJwtClaims"`
-	Prometheus                      PrometheusConfig           `mapstructure:"prometheus"`
-	SaveLogs                        SaveLogsConfig             `mapstructure:"saveLogs"`
-	DefaultIconForActions           string                     `mapstructure:"defaultIconForActions"`
-	DefaultIconForDirectories       string                     `mapstructure:"defaultIconForDirectories"`
-	DefaultIconForBack              string                     `mapstructure:"defaultIconForBack"`
-	AdditionalNavigationLinks       []*NavigationLink          `mapstructure:"additionalNavigationLinks"`
-	ServiceHostMode                 string                     `mapstructure:"serviceHostMode"`
-	StyleMods                       []string                   `mapstructure:"styleMods"`
-	BannerMessage                   string                     `mapstructure:"bannerMessage"`
-	BannerCSS                       string                     `mapstructure:"bannerCss"`
-	Include                         string                     `mapstructure:"include"`
+	UseSingleHTTPFrontend           bool                       `koanf:"useSingleHTTPFrontend"`
+	ThemeName                       string                     `koanf:"themeName"`
+	ThemeCacheDisabled              bool                       `koanf:"themeCacheDisabled"`
+	ListenAddressSingleHTTPFrontend string                     `koanf:"listenAddressSingleHTTPFrontend"`
+	ListenAddressWebUI              string                     `koanf:"listenAddressWebUI"`
+	ListenAddressRestActions        string                     `koanf:"listenAddressRestActions"`
+	ListenAddressPrometheus         string                     `koanf:"listenAddressPrometheus"`
+	ExternalRestAddress             string                     `koanf:"externalRestAddress"`
+	LogLevel                        string                     `koanf:"logLevel"`
+	LogDebugOptions                 LogDebugOptions            `koanf:"logDebugOptions"`
+	LogHistoryPageSize              int64                      `koanf:"logHistoryPageSize"`
+	Actions                         []*Action                  `koanf:"actions"`
+	Entities                        []*EntityFile              `koanf:"entities"`
+	Dashboards                      []*DashboardComponent      `koanf:"dashboards"`
+	CheckForUpdates                 bool                       `koanf:"checkForUpdates"`
+	PageTitle                       string                     `koanf:"pageTitle"`
+	ShowFooter                      bool                       `koanf:"showFooter"`
+	ShowNavigation                  bool                       `koanf:"showNavigation"`
+	ShowNewVersions                 bool                       `koanf:"showNewVersions"`
+	EnableCustomJs                  bool                       `koanf:"enableCustomJs"`
+	AuthJwtCookieName               string                     `koanf:"authJwtCookieName"`
+	AuthJwtHeader                   string                     `koanf:"authJwtHeader"`
+	AuthJwtAud                      string                     `koanf:"authJwtAud"`
+	AuthJwtDomain                   string                     `koanf:"authJwtDomain"`
+	AuthJwtCertsURL                 string                     `koanf:"authJwtCertsUrl"`
+	AuthJwtHmacSecret               string                     `koanf:"authJwtHmacSecret"` // mutually exclusive with pub key config fields
+	AuthJwtClaimUsername            string                     `koanf:"authJwtClaimUsername"`
+	AuthJwtClaimUserGroup           string                     `koanf:"authJwtClaimUserGroup"`
+	AuthJwtPubKeyPath               string                     `koanf:"authJwtPubKeyPath"` // will read pub key from file on disk
+	AuthHttpHeaderUsername          string                     `koanf:"authHttpHeaderUsername"`
+	AuthHttpHeaderUserGroup         string                     `koanf:"authHttpHeaderUserGroup"`
+	AuthHttpHeaderUserGroupSep      string                     `koanf:"authHttpHeaderUserGroupSep"`
+	AuthLocalUsers                  AuthLocalUsersConfig       `koanf:"authLocalUsers"`
+	AuthLoginUrl                    string                     `koanf:"authLoginUrl"`
+	AuthRequireGuestsToLogin        bool                       `koanf:"authRequireGuestsToLogin"`
+	AuthOAuth2RedirectURL           string                     `koanf:"authOAuth2RedirectUrl"`
+	AuthOAuth2Providers             map[string]*OAuth2Provider `koanf:"authOAuth2Providers"`
+	DefaultPermissions              PermissionsList            `koanf:"defaultPermissions"`
+	DefaultPolicy                   ConfigurationPolicy        `koanf:"defaultPolicy"`
+	AccessControlLists              []*AccessControlList       `koanf:"accessControlLists"`
+	WebUIDir                        string                     `koanf:"webUIDir"`
+	CronSupportForSeconds           bool                       `koanf:"cronSupportForSeconds"`
+	SectionNavigationStyle          string                     `koanf:"sectionNavigationStyle"`
+	DefaultPopupOnStart             string                     `koanf:"defaultPopupOnStart"`
+	InsecureAllowDumpOAuth2UserData bool                       `koanf:"insecureAllowDumpOAuth2UserData"`
+	InsecureAllowDumpVars           bool                       `koanf:"insecureAllowDumpVars"`
+	InsecureAllowDumpSos            bool                       `koanf:"insecureAllowDumpSos"`
+	InsecureAllowDumpActionMap      bool                       `koanf:"insecureAllowDumpActionMap"`
+	InsecureAllowDumpJwtClaims      bool                       `koanf:"insecureAllowDumpJwtClaims"`
+	Prometheus                      PrometheusConfig           `koanf:"prometheus"`
+	SaveLogs                        SaveLogsConfig             `koanf:"saveLogs"`
+	DefaultIconForActions           string                     `koanf:"defaultIconForActions"`
+	DefaultIconForDirectories       string                     `koanf:"defaultIconForDirectories"`
+	DefaultIconForBack              string                     `koanf:"defaultIconForBack"`
+	AdditionalNavigationLinks       []*NavigationLink          `koanf:"additionalNavigationLinks"`
+	ServiceHostMode                 string                     `koanf:"serviceHostMode"`
+	StyleMods                       []string                   `koanf:"styleMods"`
+	BannerMessage                   string                     `koanf:"bannerMessage"`
+	BannerCSS                       string                     `koanf:"bannerCss"`
+	Include                         string                     `koanf:"include"`
 
 	sourceFiles []string
 }
 
 type AuthLocalUsersConfig struct {
-	Enabled bool         `mapstructure:"enabled"`
-	Users   []*LocalUser `mapstructure:"users"`
+	Enabled bool         `koanf:"enabled"`
+	Users   []*LocalUser `koanf:"users"`
 }
 
 type LocalUser struct {
-	Username  string `mapstructure:"username"`
-	Usergroup string `mapstructure:"usergroup"`
-	Password  string `mapstructure:"password"`
+	Username  string `koanf:"username"`
+	Usergroup string `koanf:"usergroup"`
+	Password  string `koanf:"password"`
 }
 
 type OAuth2Provider struct {
-	Name               string
-	Title              string
-	ClientID           string
-	ClientSecret       string
-	Icon               string
-	Scopes             []string
-	AuthUrl            string
-	TokenUrl           string
-	WhoamiUrl          string
-	UsernameField      string
-	UserGroupField     string
-	InsecureSkipVerify bool
-	CallbackTimeout    int
-	CertBundlePath     string
+	Name               string   `koanf:"name"`
+	Title              string   `koanf:"title"`
+	ClientID           string   `koanf:"clientId"`
+	ClientSecret       string   `koanf:"clientSecret"`
+	Icon               string   `koanf:"icon"`
+	Scopes             []string `koanf:"scopes"`
+	AuthUrl            string   `koanf:"authUrl"`
+	TokenUrl           string   `koanf:"tokenUrl"`
+	WhoamiUrl          string   `koanf:"whoamiUrl"`
+	UsernameField      string   `koanf:"usernameField"`
+	UserGroupField     string   `koanf:"userGroupField"`
+	InsecureSkipVerify bool     `koanf:"insecureSkipVerify"`
+	CallbackTimeout    int      `koanf:"callbackTimeout"`
+	CertBundlePath     string   `koanf:"certBundlePath"`
 }
 
 type NavigationLink struct {
-	Title  string `mapstructure:"title"`
-	Url    string `mapstructure:"url"`
-	Target string `mapstructure:"target"`
+	Title  string `koanf:"title"`
+	Url    string `koanf:"url"`
+	Target string `koanf:"target"`
 }
 
 type SaveLogsConfig struct {
-	ResultsDirectory string `mapstructure:"resultsDirectory"`
-	OutputDirectory  string `mapstructure:"outputDirectory"`
+	ResultsDirectory string `koanf:"resultsDirectory"`
+	OutputDirectory  string `koanf:"outputDirectory"`
 }
 
 type LogDebugOptions struct {
-	SingleFrontendRequests       bool
-	SingleFrontendRequestHeaders bool
-	AclCheckStarted              bool
-	AclMatched                   bool
-	AclNotMatched                bool
-	AclNoneMatched               bool
+	SingleFrontendRequests       bool `koanf:"singleFrontendRequests"`
+	SingleFrontendRequestHeaders bool `koanf:"singleFrontendRequestHeaders"`
+	AclCheckStarted              bool `koanf:"aclCheckStarted"`
+	AclMatched                   bool `koanf:"aclMatched"`
+	AclNotMatched                bool `koanf:"aclNotMatched"`
+	AclNoneMatched               bool `koanf:"aclNoneMatched"`
 }
 
 type DashboardComponent struct {
-	Title    string
-	Type     string
-	Entity   string
-	Icon     string
-	CssClass string
-	Contents []*DashboardComponent
+	Title    string                `koanf:"title"`
+	Type     string                `koanf:"type"`
+	Entity   string                `koanf:"entity"`
+	Icon     string                `koanf:"icon"`
+	CssClass string                `koanf:"cssClass"`
+	Contents []*DashboardComponent `koanf:"contents"`
 }
 
 func DefaultConfig() *Config {

+ 47 - 233
service/internal/config/config_reloader.go

@@ -34,90 +34,32 @@ func AddListener(l func()) {
 	listeners = append(listeners, l)
 }
 
-// AppendSourceWithIncludes loads base config and any included configs
-func AppendSourceWithIncludes(cfg *Config, k *koanf.Koanf, configPath string) {
-	// Load base config first
-	AppendSource(cfg, k, configPath)
-
-	// Load included configs if specified
-	if cfg.Include != "" {
-		LoadIncludedConfigs(cfg, k, configPath)
-	}
-}
-
 func AppendSource(cfg *Config, k *koanf.Koanf, configPath string) {
-	log.Infof("Appending cfg source: %s", configPath)
+	log.WithFields(log.Fields{
+		"configPath": configPath,
+	}).Info("Appending cfg source")
+
+	loadIncludedConfigsFromDir(k, configPath)
 
 	if !unmarshalRoot(k, cfg) {
 		return
 	}
 
-	loadCollectionsFallbacks(k, cfg)
-
-	applyConfigOverrides(k, cfg)
-
 	afterLoadFinalize(cfg, configPath)
 }
 
 func unmarshalRoot(k *koanf.Koanf, cfg *Config) bool {
-	if err := k.Unmarshal(".", cfg); err != nil {
+	err := k.UnmarshalWithConf("", cfg, koanf.UnmarshalConf{
+		Tag: "koanf",
+	})
+
+	if err != nil {
 		log.Errorf("Error unmarshalling config: %v", err)
 		return false
 	}
 	return true
 }
 
-func loadCollectionsFallbacks(k *koanf.Koanf, cfg *Config) {
-	maybeUnmarshalActions(k, cfg)
-	maybeUnmarshalDashboards(k, cfg)
-	maybeUnmarshalEntities(k, cfg)
-	maybeUnmarshalAuthLocalUsers(k, cfg)
-}
-
-func maybeUnmarshalActions(k *koanf.Koanf, cfg *Config) {
-	if len(cfg.Actions) != 0 || !k.Exists("actions") {
-		return
-	}
-	var actions []*Action
-	if err := k.Unmarshal("actions", &actions); err == nil {
-		cfg.Actions = actions
-		log.Debugf("Manually loaded %d actions", len(actions))
-	}
-}
-
-func maybeUnmarshalDashboards(k *koanf.Koanf, cfg *Config) {
-	if len(cfg.Dashboards) != 0 || !k.Exists("dashboards") {
-		return
-	}
-	var dashboards []*DashboardComponent
-	if err := k.Unmarshal("dashboards", &dashboards); err == nil {
-		cfg.Dashboards = dashboards
-		log.Debugf("Manually loaded %d dashboards", len(dashboards))
-	}
-}
-
-func maybeUnmarshalEntities(k *koanf.Koanf, cfg *Config) {
-	if len(cfg.Entities) != 0 || !k.Exists("entities") {
-		return
-	}
-	var entities []*EntityFile
-	if err := k.Unmarshal("entities", &entities); err == nil {
-		cfg.Entities = entities
-		log.Debugf("Manually loaded %d entities", len(entities))
-	}
-}
-
-func maybeUnmarshalAuthLocalUsers(k *koanf.Koanf, cfg *Config) {
-	if len(cfg.AuthLocalUsers.Users) != 0 || !k.Exists("authLocalUsers") {
-		return
-	}
-	var authLocalUsers AuthLocalUsersConfig
-	if err := k.Unmarshal("authLocalUsers", &authLocalUsers); err == nil {
-		cfg.AuthLocalUsers = authLocalUsers
-		log.Debugf("Manually loaded local auth config")
-	}
-}
-
 func afterLoadFinalize(cfg *Config, configPath string) {
 	metricConfigReloadedCount.Inc()
 	metricConfigActionCount.Set(float64(len(cfg.Actions)))
@@ -130,39 +72,19 @@ func afterLoadFinalize(cfg *Config, configPath string) {
 	}
 }
 
-func applyConfigOverrides(k *koanf.Koanf, cfg *Config) {
-	// Override fields that should be read from config
-	// mapstructure tags should make most of this unnecessary, but keep for safety
-	boolVal(k, "showFooter", &cfg.ShowFooter)
-	boolVal(k, "showNavigation", &cfg.ShowNavigation)
-	boolVal(k, "checkForUpdates", &cfg.CheckForUpdates)
-	boolVal(k, "useSingleHTTPFrontend", &cfg.UseSingleHTTPFrontend)
-	stringVal(k, "logLevel", &cfg.LogLevel)
-	stringVal(k, "pageTitle", &cfg.PageTitle)
-	boolVal(k, "authRequireGuestsToLogin", &cfg.AuthRequireGuestsToLogin)
-	stringVal(k, "include", &cfg.Include)
+// loadIncludedConfigsFromDir loads configuration files from an include directory and merges them
+func loadIncludedConfigsFromDir(k *koanf.Koanf, baseConfigPath string) {
+	relativeIncludePath := k.String("include")
 
-	// Handle nested defaultPolicy struct
-	if k.Exists("defaultPolicy") {
-		boolVal(k, "defaultPolicy.showDiagnostics", &cfg.DefaultPolicy.ShowDiagnostics)
-		boolVal(k, "defaultPolicy.showLogList", &cfg.DefaultPolicy.ShowLogList)
-	}
-
-	// Handle nested prometheus struct
-	if k.Exists("prometheus") {
-		boolVal(k, "prometheus.enabled", &cfg.Prometheus.Enabled)
-		boolVal(k, "prometheus.defaultGoMetrics", &cfg.Prometheus.DefaultGoMetrics)
-	}
-}
-
-// LoadIncludedConfigs loads configuration files from an include directory and merges them
-func LoadIncludedConfigs(cfg *Config, k *koanf.Koanf, baseConfigPath string) {
-	if cfg.Include == "" {
+	if relativeIncludePath == "" {
 		return
 	}
 
-	includePath := filepath.Join(filepath.Dir(baseConfigPath), cfg.Include)
-	log.Infof("Loading included configs from: %s", includePath)
+	includePath := filepath.Join(filepath.Dir(baseConfigPath), relativeIncludePath)
+
+	log.WithFields(log.Fields{
+		"includePath": includePath,
+	}).Infof("Loading included configs from dir")
 
 	yamlFiles, ok := listYamlFiles(includePath)
 	if !ok || len(yamlFiles) == 0 {
@@ -171,11 +93,10 @@ func LoadIncludedConfigs(cfg *Config, k *koanf.Koanf, baseConfigPath string) {
 
 	sort.Strings(yamlFiles)
 	for _, filename := range yamlFiles {
-		loadAndMergeIncludedFile(cfg, includePath, filename)
+		loadAndMergeIncludedFile(k, includePath, filename)
 	}
 
 	log.Infof("Finished loading %d included config file(s)", len(yamlFiles))
-	cfg.Sanitize()
 }
 
 func listYamlFiles(includePath string) ([]string, bool) {
@@ -209,152 +130,45 @@ func listYamlFiles(includePath string) ([]string, bool) {
 	return yamlFiles, true
 }
 
-func loadAndMergeIncludedFile(cfg *Config, includePath, filename string) {
+func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) {
 	filePath := filepath.Join(includePath, filename)
-	log.Infof("Loading included config file: %s", filePath)
 
-	includeK := koanf.New(".")
-	if err := includeK.Load(file.Provider(filePath), yaml.Parser()); err != nil {
+	if err := k.Load(file.Provider(filePath), yaml.Parser(), koanf.WithMergeFunc(mergeFunc)); err != nil {
 		log.Errorf("Error loading included config file %s: %v", filePath, err)
 		return
 	}
 
-	tempCfg := &Config{}
-	if err := includeK.Unmarshal(".", tempCfg); err != nil {
-		log.Errorf("Error unmarshalling included config file %s: %v", filePath, err)
-		return
-	}
-
-	loadCollectionsFallbacks(includeK, tempCfg)
-
-	mergeConfig(cfg, tempCfg)
-	log.Infof("Successfully loaded and merged %s", filename)
-}
-
-func mergeConfig(base *Config, overlay *Config) {
-	mergeSlices(base, overlay)
-	overrideSimple(base, overlay)
-	overrideNested(base, overlay)
-	overrideStrings(base, overlay)
-}
-
-func mergeSlices(base *Config, overlay *Config) {
-	if len(overlay.Actions) > 0 {
-		base.Actions = append(base.Actions, overlay.Actions...)
-	}
-	if len(overlay.Dashboards) > 0 {
-		base.Dashboards = append(base.Dashboards, overlay.Dashboards...)
-		log.Debugf("Merged %d dashboards from include", len(overlay.Dashboards))
-	}
-	if len(overlay.Entities) > 0 {
-		base.Entities = append(base.Entities, overlay.Entities...)
-		log.Debugf("Merged %d entities from include", len(overlay.Entities))
-	}
-	if len(overlay.AccessControlLists) > 0 {
-		base.AccessControlLists = append(base.AccessControlLists, overlay.AccessControlLists...)
-		log.Debugf("Merged %d access control lists from include", len(overlay.AccessControlLists))
-	}
-	if len(overlay.AuthLocalUsers.Users) > 0 {
-		base.AuthLocalUsers.Users = append(base.AuthLocalUsers.Users, overlay.AuthLocalUsers.Users...)
-		log.Debugf("Merged %d local users from include", len(overlay.AuthLocalUsers.Users))
-	}
-	if len(overlay.StyleMods) > 0 {
-		base.StyleMods = append(base.StyleMods, overlay.StyleMods...)
-	}
-	if len(overlay.AdditionalNavigationLinks) > 0 {
-		base.AdditionalNavigationLinks = append(base.AdditionalNavigationLinks, overlay.AdditionalNavigationLinks...)
-	}
-}
-
-func overrideSimple(base *Config, overlay *Config) {
-	if overlay.LogLevel != "" {
-		base.LogLevel = overlay.LogLevel
-	}
-	if overlay.PageTitle != "" {
-		base.PageTitle = overlay.PageTitle
-	}
-	if overlay.ShowFooter != base.ShowFooter {
-		base.ShowFooter = overlay.ShowFooter
-	}
-	if overlay.ShowNavigation != base.ShowNavigation {
-		base.ShowNavigation = overlay.ShowNavigation
-	}
-	if overlay.CheckForUpdates != base.CheckForUpdates {
-		base.CheckForUpdates = overlay.CheckForUpdates
-	}
-	if overlay.UseSingleHTTPFrontend != base.UseSingleHTTPFrontend {
-		base.UseSingleHTTPFrontend = overlay.UseSingleHTTPFrontend
-	}
-	if overlay.AuthRequireGuestsToLogin != base.AuthRequireGuestsToLogin {
-		base.AuthRequireGuestsToLogin = overlay.AuthRequireGuestsToLogin
-	}
-	if overlay.AuthLocalUsers.Enabled {
-		base.AuthLocalUsers.Enabled = overlay.AuthLocalUsers.Enabled
-	}
-}
-
-func overrideNested(base *Config, overlay *Config) {
-	// Only apply overrides when overlay explicitly enables the option.
-	// This mirrors the presence-check pattern used elsewhere to avoid
-	// unintentionally disabling an already-enabled base setting with a default false.
-	if overlay.DefaultPolicy.ShowDiagnostics {
-		base.DefaultPolicy.ShowDiagnostics = true
-	}
-	if overlay.DefaultPolicy.ShowLogList {
-		base.DefaultPolicy.ShowLogList = true
-	}
-	if overlay.Prometheus.Enabled {
-		base.Prometheus.Enabled = true
-	}
-	if overlay.Prometheus.DefaultGoMetrics {
-		base.Prometheus.DefaultGoMetrics = true
+	log.WithFields(log.Fields{
+		"filePath": filePath,
+	}).Info("Successfully loaded included config file")
+}
+
+func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error {
+	// Handle actions merging - koanf provides []interface{} not []*Action
+	// Merge src (new) into dest (existing) by appending src's actions to dest's actions
+	if srcActions, ok := src["actions"]; ok {
+		if destActions, ok := dest["actions"]; ok {
+			// Both have actions - append src to dest
+			srcSlice, ok1 := srcActions.([]interface{})
+			destSlice, ok2 := destActions.([]interface{})
+			if ok1 && ok2 {
+				dest["actions"] = append(destSlice, srcSlice...)
+			} else {
+				// Fallback: if types don't match, just use src
+				dest["actions"] = srcActions
+			}
+		} else {
+			// dest doesn't have actions, so use src's actions
+			dest["actions"] = srcActions
+		}
 	}
-}
+	// If src doesn't have actions, leave dest unchanged
 
-func overrideStrings(base *Config, overlay *Config) {
-	overrideString(&base.BannerMessage, overlay.BannerMessage)
-	overrideString(&base.BannerCSS, overlay.BannerCSS)
-	overrideString(&base.LogLevel, overlay.LogLevel)
-	overrideString(&base.PageTitle, overlay.PageTitle)
-	overrideString(&base.SectionNavigationStyle, overlay.SectionNavigationStyle)
-	overrideString(&base.DefaultPopupOnStart, overlay.DefaultPopupOnStart)
-}
-
-func overrideString(base *string, overlay string) {
-	if overlay != "" {
-		*base = overlay
-	}
-}
-
-func getActionTitles(actions []*Action) []string {
-	titles := make([]string, len(actions))
-	for i, action := range actions {
-		titles[i] = action.Title
-	}
-	return titles
+	return nil
 }
 
 var envRegex = regexp.MustCompile(`\${{ *?(\S+) *?}}`)
 
-// Helper functions to reduce repetitive if/set chains
-func stringVal(k *koanf.Koanf, key string, dest *string) {
-	if k.Exists(key) {
-		*dest = k.String(key)
-	}
-}
-
-func boolVal(k *koanf.Koanf, key string, dest *bool) {
-	if k.Exists(key) {
-		*dest = k.Bool(key)
-	}
-}
-
-func int64Val(k *koanf.Koanf, key string, dest *int64) {
-	if k.Exists(key) {
-		*dest = k.Int64(key)
-	}
-}
-
 func envDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error) {
 	log.Debugf("envDecodeHookFunc called: from=%v, to=%v, data=%v", from, to, data)
 	if from.Kind() != reflect.String {

+ 3 - 1
service/internal/executor/executor_actions.go

@@ -53,7 +53,9 @@ func (e *Executor) RebuildActionMap() {
 
 	findDashboardActionTitles(req)
 
-	log.Infof("dashboardActionTitles: %v", req.DashboardActionTitles)
+	log.WithFields(log.Fields{
+		"titles": req.DashboardActionTitles,
+	}).Trace("dashboardActionTitles")
 
 	for configOrder, action := range e.Cfg.Actions {
 		if action.Entity != "" {

+ 5 - 1
service/internal/httpservers/singleFrontend.go

@@ -53,7 +53,11 @@ func StartSingleHTTPFrontend(cfg *config.Config, ex *executor.Executor) {
 
 		r.URL.Path = apiPath + fn
 
-		log.Debugf("SingleFrontend HTTP API Req URL after rewrite: %v", r.URL.Path)
+		log.WithFields(log.Fields{
+			"path": r.URL.Path,
+		}).Tracef("SingleFrontend HTTP API Req URL after rewrite")
+
+		logDebugRequest(cfg, "api", r)
 
 		apiHandler.ServeHTTP(w, r)
 	}))

+ 1 - 1
service/internal/httpservers/webuiServer.go

@@ -45,7 +45,7 @@ func (s *webUIServer) handleWebui(w http.ResponseWriter, r *http.Request) {
 
 		http.ServeFile(w, r, path.Join(s.webuiDir, "index.html"))
 	} else {
-		log.Infof("Serving webui from %s for %s", s.webuiDir, r.URL.Path)
+		log.Tracef("Serving webui from %s for %s", s.webuiDir, r.URL.Path)
 		http.ServeFile(w, r, path.Join(s.webuiDir, r.URL.Path))
 		//		http.StripPrefix(dirName, http.FileServer(http.Dir(s.webuiDir))).ServeHTTP(w, r)
 	}

+ 0 - 185
service/internal/websocket/websocket.go

@@ -1,185 +0,0 @@
-package websocket
-
-import (
-	"net/http"
-	"sync"
-
-	apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
-	"github.com/OliveTin/OliveTin/internal/executor"
-	ws "github.com/gorilla/websocket"
-	log "github.com/sirupsen/logrus"
-	"google.golang.org/protobuf/encoding/protojson"
-	"google.golang.org/protobuf/reflect/protoreflect"
-)
-
-var upgrader = ws.Upgrader{
-	CheckOrigin: checkOriginPermissive,
-}
-
-var (
-	sendmutex = sync.Mutex{}
-)
-
-type WebsocketClient struct {
-	conn *ws.Conn
-}
-
-var clients []*WebsocketClient
-
-var marshalOptions = protojson.MarshalOptions{
-	UseProtoNames:   false, // eg: canExec for js instead of can_exec from protobuf
-	EmitUnpopulated: true,
-}
-
-var ExecutionListener WebsocketExecutionListener
-
-type WebsocketExecutionListener struct{}
-
-func (WebsocketExecutionListener) OnExecutionStarted(ile *executor.InternalLogEntry) {
-	broadcast(&apiv1.EventExecutionStarted{
-		LogEntry: internalLogEntryToPb(ile),
-	})
-}
-
-func OnEntityChanged() {
-	broadcast(&apiv1.EventEntityChanged{})
-}
-
-func (WebsocketExecutionListener) OnActionMapRebuilt() {
-	broadcast(&apiv1.EventConfigChanged{})
-}
-
-/*
-The default checkOrigin function checks that the origin (browser) matches the
-request origin. However in OliveTin we expect many users to deliberately proxy
-the connection with reverse proxies.
-
-So, we just permit any origin. After some searching I'm not sure if this exposes
-OliveTin to security issues, but it seems probably not. It would be possible to
-create a config option like PermitWebsocketConnectionsFrom or something, but
-I'd prefer if OliveTin works as much as possible "out of the box".
-
-If this does expose OliveTin to security issues, it will be changed in the
-future obviously.
-*/
-func checkOriginPermissive(r *http.Request) bool {
-	return true
-}
-
-func (WebsocketExecutionListener) OnOutputChunk(chunk []byte, executionTrackingId string) {
-	log.Tracef("outputchunk: %s", string(chunk))
-
-	oc := &apiv1.EventOutputChunk{
-		Output:              string(chunk),
-		ExecutionTrackingId: executionTrackingId,
-	}
-
-	broadcast(oc)
-}
-
-func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.InternalLogEntry) {
-	evt := &apiv1.EventExecutionFinished{
-		LogEntry: internalLogEntryToPb(logEntry),
-	}
-
-	log.Infof("WS Execution finished: %v", evt.LogEntry)
-
-	broadcast(evt)
-}
-
-func broadcast(pbmsg protoreflect.ProtoMessage) {
-	payload, err := marshalOptions.Marshal(pbmsg)
-
-	if err != nil {
-		log.Errorf("websocket marshal error: %v", err)
-		return
-	}
-
-	messageType := pbmsg.ProtoReflect().Descriptor().FullName()
-
-	// <EVIL>
-	// So, the websocket wants to encode messages using the same protomarshaller
-	// as the REST API - this gives consistency instead of using encoding/json
-	// and allows us to set specific marshalOptions.
-	//
-	// However, the protomarshaller will marshal the type, but the JavaScript at
-	// the other end has no idea what type this object is - as we're just sending
-	// it as JSON over the websocket.
-	//
-	// Therefore, we wrap the nicely marsheled bytes in a hacky JSON string
-	// literal and encode that string just with a byte array cast.
-	hackyMessageEnvelope := "{\"type\": \"" + messageType + "\", \"payload\": "
-
-	hackyMessage := []byte{}
-	hackyMessage = append(hackyMessage, []byte(hackyMessageEnvelope)...)
-	hackyMessage = append(hackyMessage, payload...)
-	hackyMessage = append(hackyMessage, []byte("}")...)
-	// </EVIL>
-
-	sendmutex.Lock()
-	for _, client := range clients {
-		err := client.conn.WriteMessage(ws.TextMessage, hackyMessage)
-
-		if err != nil {
-			log.Warnf("websocket send error: %v", err)
-		}
-	}
-	sendmutex.Unlock()
-}
-
-func (c *WebsocketClient) messageLoop() {
-	for {
-		mt, message, err := c.conn.ReadMessage()
-
-		if err != nil {
-			log.Debugf("err: %v", err)
-			break
-		}
-
-		log.Tracef("websocket recv: %s %d", message, mt)
-	}
-}
-
-func HandleWebsocket(w http.ResponseWriter, r *http.Request) bool {
-	c, err := upgrader.Upgrade(w, r, nil)
-
-	if err != nil {
-		log.Warnf("Websocket issue: %v", err)
-		return false
-	}
-
-	//	defer c.Close()
-
-	wsclient := &WebsocketClient{
-		conn: c,
-	}
-
-	sendmutex.Lock()
-
-	clients = append(clients, wsclient)
-
-	sendmutex.Unlock()
-
-	go wsclient.messageLoop()
-
-	return true
-}
-
-func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *apiv1.LogEntry {
-	return &apiv1.LogEntry{
-		ActionTitle:         logEntry.ActionTitle,
-		ActionIcon:          logEntry.ActionIcon,
-		ActionId:            logEntry.ActionId,
-		DatetimeStarted:     logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
-		DatetimeFinished:    logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
-		Output:              logEntry.Output,
-		TimedOut:            logEntry.TimedOut,
-		Blocked:             logEntry.Blocked,
-		ExitCode:            logEntry.ExitCode,
-		Tags:                logEntry.Tags,
-		ExecutionTrackingId: logEntry.ExecutionTrackingID,
-		ExecutionStarted:    logEntry.ExecutionStarted,
-		ExecutionFinished:   logEntry.ExecutionFinished,
-		User:                logEntry.Username,
-	}
-}

+ 23 - 14
service/main.go

@@ -18,7 +18,6 @@ import (
 	"github.com/OliveTin/OliveTin/internal/onstartup"
 	"github.com/OliveTin/OliveTin/internal/servicehost"
 	updatecheck "github.com/OliveTin/OliveTin/internal/updatecheck"
-	"github.com/OliveTin/OliveTin/internal/websocket"
 
 	"os"
 	"strconv"
@@ -147,23 +146,33 @@ func initConfig(configDir string) {
 		)
 	}
 
-	var firstConfigPath string
+	var baseConfigPath string
 
 	for _, directory := range directories {
 		configPath := getConfigPath(directory)
 
-		log.Debugf("Checking config path: %s", configPath)
-
+		found := true
 		if _, err := os.Stat(configPath); err != nil {
-			log.Debugf("Config file not found at %s: %v", configPath, err)
+			found = false
+		}
+
+		log.WithFields(log.Fields{
+			"configPath": configPath,
+			"found":      found,
+		}).Debug("Checking base config path")
+
+		if !found {
 			continue
 		}
 
-		if firstConfigPath == "" {
-			firstConfigPath = configPath
+		if baseConfigPath == "" {
+			baseConfigPath = configPath
 		}
 
-		log.Infof("Loading config from %s", configPath)
+		log.WithFields(log.Fields{
+			"configPath": configPath,
+		}).Info("Loading config from path")
+
 		f := file.Provider(configPath)
 
 		if err := k.Load(f, yaml.Parser()); err != nil {
@@ -177,16 +186,18 @@ func initConfig(configDir string) {
 			k.Load(f, yaml.Parser())
 			config.AppendSource(cfg, k, configPath)
 		})
+
+		break
 	}
 
 	cfg = config.DefaultConfigWithBasePort(getBasePort())
 
-	if firstConfigPath != "" {
-		config.AppendSourceWithIncludes(cfg, k, firstConfigPath)
-	} else {
-		config.AppendSource(cfg, k, "base")
+	if baseConfigPath == "" {
+		log.Fatalf("No base config file found")
+		os.Exit(1)
 	}
 
+	config.AppendSource(cfg, k, baseConfigPath)
 }
 
 func initInstallationInfo() {
@@ -225,7 +236,6 @@ func main() {
 
 	executor := executor.DefaultExecutor(cfg)
 	executor.RebuildActionMap()
-	executor.AddListener(websocket.ExecutionListener)
 	config.AddListener(executor.RebuildActionMap)
 
 	go onstartup.Execute(cfg, executor)
@@ -233,7 +243,6 @@ func main() {
 	go onfileindir.WatchFilesInDirectory(cfg, executor)
 	go oncalendarfile.Schedule(cfg, executor)
 
-	entities.AddListener(websocket.OnEntityChanged)
 	entities.AddListener(executor.RebuildActionMap)
 	go entities.SetupEntityFileWatchers(cfg)