From b9f55ab50842c9e90e03bcb66a33bf7871f0f8d5 Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 23 Jan 2026 14:18:30 +0000 Subject: [PATCH 01/20] fix: load logs on startup (#299) --- .../tests/logPersistence/config.yaml | 16 ++ .../tests/logPersistence/logPersistence.mjs | 258 ++++++++++++++++++ service/internal/executor/loadlogs.go | 216 +++++++++++++++ service/main.go | 2 + 4 files changed, 492 insertions(+) create mode 100644 integration-tests/tests/logPersistence/config.yaml create mode 100644 integration-tests/tests/logPersistence/logPersistence.mjs create mode 100644 service/internal/executor/loadlogs.go diff --git a/integration-tests/tests/logPersistence/config.yaml b/integration-tests/tests/logPersistence/config.yaml new file mode 100644 index 0000000..8871205 --- /dev/null +++ b/integration-tests/tests/logPersistence/config.yaml @@ -0,0 +1,16 @@ +# +# Integration Test Config: Log Persistence +# + +listenAddressSingleHTTPFrontend: 0.0.0.0:1337 + +logLevel: "DEBUG" +checkForUpdates: false + +saveLogs: + resultsDirectory: /tmp/olivetin-test-logs + +actions: +- title: Echo Test + shell: echo "Hello from persisted log test" + icon: test diff --git a/integration-tests/tests/logPersistence/logPersistence.mjs b/integration-tests/tests/logPersistence/logPersistence.mjs new file mode 100644 index 0000000..f7d93bd --- /dev/null +++ b/integration-tests/tests/logPersistence/logPersistence.mjs @@ -0,0 +1,258 @@ +import { describe, it, before, after } from 'mocha' +import { expect } from 'chai' +import { By, Condition } from 'selenium-webdriver' +import fs from 'fs' +import path from 'path' +import { + getRootAndWait, + getActionButtons, + takeScreenshotOnFailure, +} from '../../lib/elements.js' + +describe('config: logPersistence', function () { + const logsDir = '/tmp/olivetin-test-logs' + let firstExecutionTrackingId = null + + before(async function () { + // Clean up any existing test logs + if (fs.existsSync(logsDir)) { + fs.rmSync(logsDir, { recursive: true, force: true }) + } + fs.mkdirSync(logsDir, { recursive: true }) + + await runner.start('logPersistence') + }) + + after(async () => { + await runner.stop() + + // Clean up test logs directory + if (fs.existsSync(logsDir)) { + fs.rmSync(logsDir, { recursive: true, force: true }) + } + }) + + afterEach(function () { + takeScreenshotOnFailure(this.currentTest, webdriver) + }) + + it('Execute action and verify log is saved to disk', async function () { + this.timeout(30000) + await getRootAndWait() + + // Get initial log file count + const initialLogCount = fs.existsSync(logsDir) + ? fs.readdirSync(logsDir).filter(f => f.endsWith('.yaml')).length + : 0 + + // Wait for action button to be available + await webdriver.wait( + new Condition('wait for Echo Test button', async () => { + const buttons = await webdriver.findElements(By.css('.action-button button')) + for (const btn of buttons) { + const text = await btn.getText() + if (text.includes('Echo Test')) { + return true + } + } + return false + }), + 10000 + ) + + // Find and click the Echo Test button + const buttons = await webdriver.findElements(By.css('.action-button button')) + let echoButton = null + for (const btn of buttons) { + const text = await btn.getText() + if (text.includes('Echo Test')) { + echoButton = btn + break + } + } + expect(echoButton).to.not.be.null + + // Click the button to execute the action + await echoButton.click() + + // Wait for the log file to be written to disk + await webdriver.wait( + new Condition('wait for log file to appear', async () => { + if (!fs.existsSync(logsDir)) { + return false + } + const logFiles = fs.readdirSync(logsDir).filter(f => f.endsWith('.yaml')) + return logFiles.length > initialLogCount + }), + 10000 + ) + + // Wait a bit more to ensure file is fully written + await webdriver.sleep(1000) + + // Get the newest log file + const logFiles = fs.readdirSync(logsDir).filter(f => f.endsWith('.yaml')) + expect(logFiles.length).to.be.greaterThan(initialLogCount, 'At least one new log file should be saved') + + // Sort by modification time to get the newest + const logFilesWithStats = logFiles.map(f => { + const filePath = path.join(logsDir, f) + return { + name: f, + path: filePath, + mtime: fs.statSync(filePath).mtime + } + }).sort((a, b) => b.mtime - a.mtime) + + const newestLogFile = logFilesWithStats[0] + expect(newestLogFile).to.not.be.undefined + + // Read the log file to extract the tracking ID + const logFileContent = fs.readFileSync(newestLogFile.path, 'utf8') + + // Verify the log file contains expected content (action title might be in different fields) + expect(logFileContent.length).to.be.greaterThan(0, 'Log file should not be empty') + + // Extract tracking ID from filename first (most reliable) + // Filename format: .<timestamp>.<trackingId>.yaml + // Tracking IDs are UUIDs, so match UUID pattern at the end before .yaml + let uuidMatch = newestLogFile.name.match(/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\.yaml$/) + + if (uuidMatch) { + firstExecutionTrackingId = uuidMatch[1] + } else { + // Fallback: split by dots and get the last part before .yaml + const parts = newestLogFile.name.replace('.yaml', '').split('.') + if (parts.length >= 3) { + // The last part should be the tracking ID + firstExecutionTrackingId = parts[parts.length - 1] + } + } + + // If still not found, try to extract from YAML content + // Try different possible field name variations + if (!firstExecutionTrackingId) { + const patterns = [ + /executionTrackingID:\s*([^\s\n]+)/i, + /execution_tracking_id:\s*([^\s\n]+)/i, + /ExecutionTrackingID:\s*([^\s\n]+)/, + /executionTrackingId:\s*([^\s\n]+)/i, + ] + + for (const pattern of patterns) { + const match = logFileContent.match(pattern) + if (match) { + firstExecutionTrackingId = match[1].trim() + break + } + } + } + + expect(firstExecutionTrackingId).to.not.be.null + expect(firstExecutionTrackingId.length).to.be.greaterThan(0) + + // Verify the log file name contains the tracking ID + expect(newestLogFile.name).to.include(firstExecutionTrackingId) + + // Verify the log file content contains the action (might be in actionTitle, actionConfigTitle, or title field) + const hasActionReference = logFileContent.includes('Echo Test') || + logFileContent.includes('echo') || + logFileContent.includes('actionTitle') || + logFileContent.includes('actionConfigTitle') + expect(hasActionReference).to.be.true + }) + + it('Restart service and verify logs are loaded from disk', async function () { + this.timeout(60000) + + // Skip if first test didn't set the tracking ID + if (!firstExecutionTrackingId) { + this.skip() + } + + // Verify log file exists before restart + const logFilesBeforeRestart = fs.readdirSync(logsDir).filter(f => f.endsWith('.yaml')) + expect(logFilesBeforeRestart.length).to.be.greaterThan(0, 'Log file should exist before restart') + + // Find the log file for this execution + const matchingLogFileBefore = logFilesBeforeRestart.find(f => f.includes(firstExecutionTrackingId)) + expect(matchingLogFileBefore).to.not.be.undefined + + // Stop the current service instance + await runner.stop() + + // Wait a moment to ensure the process has fully stopped + await new Promise((resolve) => setTimeout(resolve, 2000)) + + // Verify log file still exists after stop (should not be deleted) + const logFilesAfterStop = fs.readdirSync(logsDir).filter(f => f.endsWith('.yaml')) + expect(logFilesAfterStop.length).to.be.greaterThan(0, 'Log file should still exist after service stop') + + const matchingLogFileAfter = logFilesAfterStop.find(f => f.includes(firstExecutionTrackingId)) + expect(matchingLogFileAfter).to.not.be.undefined + + // Start a new service instance (logs should be loaded from disk) + await runner.start('logPersistence') + + // Wait for the service to fully start and load logs + await new Promise((resolve) => setTimeout(resolve, 3000)) + + await getRootAndWait() + + // Navigate directly to the specific log entry (this verifies the log was loaded) + await webdriver.get(runner.baseUrl() + 'logs/' + firstExecutionTrackingId) + + // Wait for the log details page to load + await webdriver.wait( + new Condition('wait for log details to load', async () => { + try { + const body = await webdriver.findElement(By.tagName('body')) + const text = await body.getText() + // The log should contain the output from the echo command + return text.includes('Hello from persisted log test') || text.includes(firstExecutionTrackingId) + } catch (e) { + return false + } + }), + 15000 + ) + + // Verify the log content is displayed + const body = await webdriver.findElement(By.tagName('body')) + const bodyText = await body.getText() + + // The persisted log should be accessible and contain the expected output + expect(bodyText).to.include('Hello from persisted log test') + }) + + it('Verify log file still exists after restart', async function () { + // Skip if first test didn't set the tracking ID + if (!firstExecutionTrackingId) { + this.skip() + } + + // Verify the log file still exists on disk + const logFiles = fs.readdirSync(logsDir).filter(f => f.endsWith('.yaml')) + expect(logFiles.length).to.be.greaterThan(0, 'Log files should still exist after restart') + + // Find the log file for the first execution + const matchingLogFile = logFiles.find(f => f.includes(firstExecutionTrackingId)) + expect(matchingLogFile).to.not.be.undefined + expect(matchingLogFile).to.not.be.null + + // Verify the log file content is still valid + const logFilePath = path.join(logsDir, matchingLogFile) + const logFileContent = fs.readFileSync(logFilePath, 'utf8') + expect(logFileContent.length).to.be.greaterThan(0, 'Log file should not be empty') + + // The filename contains the tracking ID, so verify that + expect(matchingLogFile).to.include(firstExecutionTrackingId) + + // Verify the file contains some expected content (action reference) + const hasActionReference = logFileContent.includes('Echo Test') || + logFileContent.includes('echo') || + logFileContent.includes('actionTitle') || + logFileContent.includes('actionConfigTitle') + expect(hasActionReference).to.be.true + }) +}) diff --git a/service/internal/executor/loadlogs.go b/service/internal/executor/loadlogs.go new file mode 100644 index 0000000..6a03ae9 --- /dev/null +++ b/service/internal/executor/loadlogs.go @@ -0,0 +1,216 @@ +package executor + +import ( + "os" + "path/filepath" + "sort" + "strings" + + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" +) + +// LoadLogsFromDisk loads persisted logs from YAML files on disk and restores them to the executor. +// This should be called during startup if saveLogs is configured. +func (e *Executor) LoadLogsFromDisk() { + resultsDir := e.Cfg.SaveLogs.ResultsDirectory + if resultsDir == "" { + return + } + + entries, skippedCount := e.readLogDirectory(resultsDir) + if entries == nil { + return + } + + loadedLogs, skippedCount := e.parseLogFiles(resultsDir, entries, skippedCount) + + sort.Slice(loadedLogs, func(i, j int) bool { + return loadedLogs[i].DatetimeStarted.Before(loadedLogs[j].DatetimeStarted) + }) + + skippedCount = e.restoreLogsToExecutor(loadedLogs, skippedCount) + + log.WithFields(log.Fields{ + "loaded": len(loadedLogs), + "skipped": skippedCount, + }).Info("Finished loading persisted logs from disk") +} + +// readLogDirectory reads the log directory and returns entries, or nil if the directory doesn't exist or can't be read. +func (e *Executor) readLogDirectory(resultsDir string) ([]os.DirEntry, int) { + if _, err := os.Stat(resultsDir); os.IsNotExist(err) { + log.WithFields(log.Fields{ + "directory": resultsDir, + }).Debug("Logs directory does not exist, skipping log loading") + return nil, 0 + } + + log.WithFields(log.Fields{ + "directory": resultsDir, + }).Info("Loading persisted logs from disk") + + entries, err := os.ReadDir(resultsDir) + if err != nil { + log.WithFields(log.Fields{ + "directory": resultsDir, + "error": err, + }).Warnf("Failed to read logs directory") + return nil, 0 + } + + return entries, 0 +} + +// parseLogFiles parses YAML log files from the directory entries. +func (e *Executor) parseLogFiles(resultsDir string, entries []os.DirEntry, skippedCount int) ([]*InternalLogEntry, int) { + loadedLogs := make([]*InternalLogEntry, 0) + + for _, entry := range entries { + if !e.shouldProcessLogEntry(entry) { + continue + } + + logEntry, newSkippedCount := e.processLogFileEntry(resultsDir, entry.Name()) + skippedCount += newSkippedCount + if logEntry != nil { + loadedLogs = append(loadedLogs, logEntry) + } + } + + return loadedLogs, skippedCount +} + +// shouldProcessLogEntry checks if a directory entry should be processed as a log file. +func (e *Executor) shouldProcessLogEntry(entry os.DirEntry) bool { + return !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yaml") +} + +// processLogFileEntry processes a single log file entry and returns the log entry or nil if it should be skipped. +func (e *Executor) processLogFileEntry(resultsDir, filename string) (*InternalLogEntry, int) { + logEntry, ok := e.loadLogFileFromPath(resultsDir, filename) + if !ok { + return nil, 1 + } + + if logEntry.ExecutionTrackingID == "" { + log.WithFields(log.Fields{ + "file": filepath.Join(resultsDir, filename), + }).Warnf("Log file missing execution tracking ID, skipping") + return nil, 1 + } + + e.restoreBindingForLogEntry(logEntry, filepath.Join(resultsDir, filename)) + return logEntry, 0 +} + +// loadLogFileFromPath loads and unmarshals a single log file. +func (e *Executor) loadLogFileFromPath(resultsDir, filename string) (*InternalLogEntry, bool) { + filepath := filepath.Join(resultsDir, filename) + data, err := os.ReadFile(filepath) + if err != nil { + log.WithFields(log.Fields{ + "file": filepath, + "error": err, + }).Warnf("Failed to read log file") + return nil, false + } + + var logEntry InternalLogEntry + if err := yaml.Unmarshal(data, &logEntry); err != nil { + log.WithFields(log.Fields{ + "file": filepath, + "error": err, + }).Warnf("Failed to unmarshal log file") + return nil, false + } + + return &logEntry, true +} + +// restoreBindingForLogEntry attempts to restore the binding for a log entry if it's missing or invalid. +func (e *Executor) restoreBindingForLogEntry(logEntry *InternalLogEntry, filepath string) { + if e.hasValidBinding(logEntry) || logEntry.ActionConfigTitle == "" { + return + } + + binding := e.findBindingByActionTitle(logEntry.ActionConfigTitle, logEntry.EntityPrefix) + if binding != nil { + logEntry.Binding = binding + return + } + + e.logBindingNotFound(logEntry, filepath) + logEntry.Binding = nil +} + +// hasValidBinding checks if a log entry has a valid binding. +func (e *Executor) hasValidBinding(logEntry *InternalLogEntry) bool { + return logEntry.Binding != nil && logEntry.Binding.Action != nil +} + +// logBindingNotFound logs a debug message when a binding cannot be found for a log entry. +func (e *Executor) logBindingNotFound(logEntry *InternalLogEntry, filepath string) { + log.WithFields(log.Fields{ + "file": filepath, + "actionTitle": logEntry.ActionConfigTitle, + "entityPrefix": logEntry.EntityPrefix, + "trackingId": logEntry.ExecutionTrackingID, + }).Debug("Could not find binding for log entry, loading without binding") +} + +// restoreLogsToExecutor restores loaded logs to the executor's internal structures. +func (e *Executor) restoreLogsToExecutor(loadedLogs []*InternalLogEntry, skippedCount int) int { + e.logmutex.Lock() + defer e.logmutex.Unlock() + + for _, logEntry := range loadedLogs { + if _, exists := e.logs[logEntry.ExecutionTrackingID]; exists { + log.WithFields(log.Fields{ + "trackingId": logEntry.ExecutionTrackingID, + }).Debug("Log entry already exists, skipping") + skippedCount++ + continue + } + + logEntry.Index = int64(len(e.logsTrackingIdsByDate)) + e.logs[logEntry.ExecutionTrackingID] = logEntry + e.logsTrackingIdsByDate = append(e.logsTrackingIdsByDate, logEntry.ExecutionTrackingID) + + if logEntry.Binding != nil { + e.addLogToBindingMap(logEntry) + } + } + + return skippedCount +} + +// addLogToBindingMap adds a log entry to the LogsByBindingId map. +func (e *Executor) addLogToBindingMap(logEntry *InternalLogEntry) { + if _, containsKey := e.LogsByBindingId[logEntry.Binding.ID]; !containsKey { + e.LogsByBindingId[logEntry.Binding.ID] = make([]*InternalLogEntry, 0) + } + e.LogsByBindingId[logEntry.Binding.ID] = append(e.LogsByBindingId[logEntry.Binding.ID], logEntry) +} + +// findBindingByActionTitle attempts to find a binding by matching the action config title and entity prefix. +func (e *Executor) findBindingByActionTitle(actionConfigTitle string, entityPrefix string) *ActionBinding { + e.MapActionBindingsLock.RLock() + defer e.MapActionBindingsLock.RUnlock() + + for _, binding := range e.MapActionBindings { + if binding.Action.Title == actionConfigTitle && e.matchesEntityPrefix(binding, entityPrefix) { + return binding + } + } + + return nil +} + +// matchesEntityPrefix checks if a binding matches the given entity prefix. +func (e *Executor) matchesEntityPrefix(binding *ActionBinding, entityPrefix string) bool { + if entityPrefix == "" { + return binding.Entity == nil + } + return binding.Entity != nil && binding.Entity.UniqueKey == entityPrefix +} diff --git a/service/main.go b/service/main.go index dbf4878..eeb2ca8 100644 --- a/service/main.go +++ b/service/main.go @@ -257,6 +257,8 @@ func main() { executor.RebuildActionMap() config.AddListener(executor.RebuildActionMap) + executor.LoadLogsFromDisk() + go onstartup.Execute(cfg, executor) go oncron.Schedule(cfg, executor) go onfileindir.WatchFilesInDirectory(cfg, executor) From bd15941bc0d121f6c594eb6f1a47c602fbafca2e Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Fri, 23 Jan 2026 15:43:44 +0000 Subject: [PATCH 02/20] chore: Only release on main --- .releaserc.yaml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.releaserc.yaml b/.releaserc.yaml index 125c177..8aeb820 100644 --- a/.releaserc.yaml +++ b/.releaserc.yaml @@ -1,10 +1,8 @@ --- -#branches: -# - name: main -# range: '3000.x.x' - -# - name: release/2k -# range: '>=2000.0.0 <3000.0.0' +# Only allow releases on the main branch (for 3k) +# releases for 2k are published manaually. +branches: + - name: main plugins: - '@semantic-release/commit-analyzer' From b37f035ea653e6f238a7cdfa0f636f518a692dc4 Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Sun, 25 Jan 2026 10:32:22 +0000 Subject: [PATCH 03/20] fix: Move templating functionality to global, making it possible to replace templates across the config --- service/internal/api/api.go | 3 +- service/internal/api/apiActions.go | 23 ++++-- service/internal/api/api_test.go | 6 +- service/internal/api/dashboard_entities.go | 19 ++--- service/internal/api/dashboards.go | 3 +- service/internal/config/config.go | 3 +- service/internal/config/sanitize.go | 21 +++++ service/internal/entities/entities.go | 2 +- service/internal/entities/storage.go | 68 ++++------------ service/internal/executor/arguments.go | 7 +- service/internal/executor/executor.go | 3 +- .../internal/{entities => tpl}/templates.go | 78 +++++++++++++++---- 12 files changed, 145 insertions(+), 91 deletions(-) rename service/internal/{entities => tpl}/templates.go (65%) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index c7ee481..fc4857f 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -27,6 +27,7 @@ import ( entities "github.com/OliveTin/OliveTin/internal/entities" executor "github.com/OliveTin/OliveTin/internal/executor" installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo" + "github.com/OliveTin/OliveTin/internal/tpl" connectproto "go.akshayshah.org/connectproto" ) @@ -709,7 +710,7 @@ func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.Dum return connect.NewResponse(res), nil } - jsonstring, _ := json.MarshalIndent(entities.GetAll(), "", " ") + jsonstring, _ := json.MarshalIndent(tpl.GetNewGeneralTemplateContext(), "", " ") fmt.Printf("%s", &jsonstring) res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore" diff --git a/service/internal/api/apiActions.go b/service/internal/api/apiActions.go index 55728b9..3ca12de 100644 --- a/service/internal/api/apiActions.go +++ b/service/internal/api/apiActions.go @@ -13,6 +13,7 @@ import ( config "github.com/OliveTin/OliveTin/internal/config" entities "github.com/OliveTin/OliveTin/internal/entities" executor "github.com/OliveTin/OliveTin/internal/executor" + "github.com/OliveTin/OliveTin/internal/tpl" ) type DashboardRenderRequest struct { @@ -66,7 +67,7 @@ func evaluateEnabledExpression(action *config.Action, entity *entities.Entity) b return true } - result := entities.ParseTemplateWith(action.EnabledExpression, entity) + result := tpl.ParseTemplateWith(action.EnabledExpression, entity) result = strings.TrimSpace(result) if result == "" { @@ -105,6 +106,16 @@ func evaluateResultValue(result string) bool { return false } +func getDefaultValue(cfgArg config.ActionArgument, entity *entities.Entity) string { + defaultValue := cfgArg.Default + + if defaultValue != "" { + defaultValue = tpl.ParseTemplateWith(defaultValue, entity) + } + + return defaultValue +} + func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderRequest) *apiv1.Action { action := actionBinding.Action @@ -120,8 +131,8 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque btn := apiv1.Action{ BindingId: actionBinding.ID, - Title: entities.ParseTemplateWith(action.Title, actionBinding.Entity), - Icon: entities.ParseTemplateWith(action.Icon, actionBinding.Entity), + Title: tpl.ParseTemplateWith(action.Title, actionBinding.Entity), + Icon: tpl.ParseTemplateWith(action.Icon, actionBinding.Entity), CanExec: aclCanExec && enabledExprCanExec, PopupOnStart: action.PopupOnStart, Order: int32(actionBinding.ConfigOrder), @@ -135,7 +146,7 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque Title: cfgArg.Title, Type: cfgArg.Type, Description: cfgArg.Description, - DefaultValue: cfgArg.Default, + DefaultValue: getDefaultValue(cfgArg, actionBinding.Entity), Choices: buildChoices(cfgArg), Suggestions: cfgArg.Suggestions, SuggestionsBrowserKey: cfgArg.SuggestionsBrowserKey, @@ -162,8 +173,8 @@ func buildChoicesEntity(firstChoice config.ActionArgumentChoice, entityTitle str for _, ent := range entList { ret = append(ret, &apiv1.ActionArgumentChoice{ - Value: entities.ParseTemplateWith(firstChoice.Value, ent), - Title: entities.ParseTemplateWith(firstChoice.Title, ent), + Value: tpl.ParseTemplateWith(firstChoice.Value, ent), + Title: tpl.ParseTemplateWith(firstChoice.Title, ent), }) } diff --git a/service/internal/api/api_test.go b/service/internal/api/api_test.go index c2977f7..790e315 100644 --- a/service/internal/api/api_test.go +++ b/service/internal/api/api_test.go @@ -119,9 +119,9 @@ func TestGetEntities(t *testing.T) { } func setupTestEntities() { - entities.ClearEntities("server") - entities.ClearEntities("database") - entities.ClearEntities("application") + entities.ClearEntitiesOfType("server") + entities.ClearEntitiesOfType("database") + entities.ClearEntitiesOfType("application") entities.AddEntity("server", "zebra", map[string]any{"title": "Server Zebra", "hostname": "zebra.example.com"}) entities.AddEntity("server", "alpha", map[string]any{"title": "Server Alpha", "hostname": "alpha.example.com"}) diff --git a/service/internal/api/dashboard_entities.go b/service/internal/api/dashboard_entities.go index 68a053b..8ae21a0 100644 --- a/service/internal/api/dashboard_entities.go +++ b/service/internal/api/dashboard_entities.go @@ -4,6 +4,7 @@ import ( apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" config "github.com/OliveTin/OliveTin/internal/config" entities "github.com/OliveTin/OliveTin/internal/entities" + "github.com/OliveTin/OliveTin/internal/tpl" log "github.com/sirupsen/logrus" ) @@ -23,14 +24,14 @@ func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent, rr return ret } -func buildEntityFieldset(tpl *config.DashboardComponent, ent *entities.Entity, rr *DashboardRenderRequest) *apiv1.DashboardComponent { +func buildEntityFieldset(component *config.DashboardComponent, ent *entities.Entity, rr *DashboardRenderRequest) *apiv1.DashboardComponent { return &apiv1.DashboardComponent{ - Title: entities.ParseTemplateWith(tpl.Title, ent), + Title: tpl.ParseTemplateWith(component.Title, ent), Type: "fieldset", - Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(tpl.Contents, ent, tpl.Entity, rr)), - CssClass: entities.ParseTemplateWith(tpl.CssClass, ent), - Action: rr.findAction(tpl.Title), - EntityType: tpl.Entity, + Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(component.Contents, ent, component.Entity, rr)), + CssClass: tpl.ParseTemplateWith(component.CssClass, ent), + Action: rr.findAction(component.Title), + EntityType: component.Entity, EntityKey: ent.UniqueKey, } } @@ -68,7 +69,7 @@ func buildEntityFieldsetContents(contents []*config.DashboardComponent, ent *ent func cloneItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, rr *DashboardRenderRequest) *apiv1.DashboardComponent { clone := &apiv1.DashboardComponent{} - clone.CssClass = entities.ParseTemplateWith(subitem.CssClass, ent) + clone.CssClass = tpl.ParseTemplateWith(subitem.CssClass, ent) if isLinkType(subitem.Type) { return cloneLinkItem(subitem, ent, clone, rr) @@ -83,7 +84,7 @@ func isLinkType(itemType string) bool { func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent { clone.Type = "link" - clone.Title = entities.ParseTemplateWith(subitem.Title, ent) + clone.Title = tpl.ParseTemplateWith(subitem.Title, ent) // Prefer an entity-specific action when available, but fall back to a // non-entity-scoped action with the same title. This allows inline actions // defined inside entity dashboards to work without requiring an explicit @@ -98,7 +99,7 @@ func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clo } func cloneNonLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent { - clone.Title = entities.ParseTemplateWith(subitem.Title, ent) + clone.Title = tpl.ParseTemplateWith(subitem.Title, ent) clone.Type = subitem.Type if isDirectoryWithEntity(clone.Type, ent, entityType) { diff --git a/service/internal/api/dashboards.go b/service/internal/api/dashboards.go index 12ae2d5..08ef241 100644 --- a/service/internal/api/dashboards.go +++ b/service/internal/api/dashboards.go @@ -6,6 +6,7 @@ import ( apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" config "github.com/OliveTin/OliveTin/internal/config" entities "github.com/OliveTin/OliveTin/internal/entities" + "github.com/OliveTin/OliveTin/internal/tpl" log "github.com/sirupsen/logrus" "golang.org/x/exp/slices" ) @@ -236,7 +237,7 @@ func buildDashboardComponentSimpleWithEntity(subitem *config.DashboardComponent, title := subitem.Title if entity != nil { - title = entities.ParseTemplateWith(subitem.Title, entity) + title = tpl.ParseTemplateWith(subitem.Title, entity) } newitem := &apiv1.DashboardComponent{ diff --git a/service/internal/config/config.go b/service/internal/config/config.go index d22e86f..3082428 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -170,7 +170,8 @@ type Config struct { BannerCSS string `koanf:"bannerCss"` Include string `koanf:"include"` - sourceFiles []string + sourceFiles []string + passwordTemplateParser func(string, interface{}) string } type AuthLocalUsersConfig struct { diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index f626775..779cfe1 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -13,6 +13,7 @@ func (cfg *Config) Sanitize() { cfg.sanitizeLogLevel() cfg.sanitizeAuthRequireGuestsToLogin() cfg.sanitizeLogHistoryPageSize() + cfg.sanitizeLocalUserPasswords() // log.Infof("cfg %p", cfg) @@ -172,6 +173,26 @@ func (cfg *Config) sanitizeLogHistoryPageSize() { } } +// SetPasswordTemplateParser sets the function to use for parsing password templates. +// This is called from main.go to avoid import cycles (config can't import entities). +func (cfg *Config) SetPasswordTemplateParser(parser func(string, interface{}) string) { + cfg.passwordTemplateParser = parser +} + +func (cfg *Config) sanitizeLocalUserPasswords() { + if cfg.passwordTemplateParser == nil { + return + } + + for _, user := range cfg.AuthLocalUsers.Users { + if user.Password != "" { + // Parse password as template to support environment variables and other template values + // Note: .CurrentEntity is nil in this context as local users are not entity-bound + user.Password = cfg.passwordTemplateParser(user.Password, nil) + } + } +} + func getActionID(action *Action) string { if action.ID == "" { return uuid.NewString() diff --git a/service/internal/entities/entities.go b/service/internal/entities/entities.go index 639a4a9..386fb20 100644 --- a/service/internal/entities/entities.go +++ b/service/internal/entities/entities.go @@ -135,7 +135,7 @@ func loadEntityFileYaml(filename string, entityname string) { } func updateSvFromFile(entityname string, data []map[string]any) { - ClearEntities(entityname) + ClearEntitiesOfType(entityname) for i, mapp := range data { AddEntity(entityname, fmt.Sprintf("%d", i), mapp) diff --git a/service/internal/entities/storage.go b/service/internal/entities/storage.go index 53cdbbc..1ae9780 100644 --- a/service/internal/entities/storage.go +++ b/service/internal/entities/storage.go @@ -10,72 +10,31 @@ package entities */ import ( - "os" "strings" "sync" - - "github.com/OliveTin/OliveTin/internal/installationinfo" ) type entityInstancesByKey map[string]*Entity -type entitiesByClass map[string]entityInstancesByKey - -type variableBase struct { - OliveTin installationInfo - Entities entitiesByClass - - CurrentEntity interface{} - Arguments map[string]string - Env map[string]string -} - -type installationInfo struct { - Build *installationinfo.BuildInfo - Runtime *installationinfo.RuntimeInfo -} +type EntitiesByClass map[string]entityInstancesByKey var ( - contents *variableBase rwmutex = sync.RWMutex{} + Entities EntitiesByClass ) func init() { rwmutex.Lock() - - envMap := make(map[string]string) - for _, env := range os.Environ() { - parts := strings.SplitN(env, "=", 2) - if len(parts) == 2 { - envMap[parts[0]] = parts[1] - } - } - - contents = &variableBase{ - OliveTin: installationInfo{ - Build: installationinfo.Build, - Runtime: installationinfo.Runtime, - }, - Entities: make(entitiesByClass, 0), - Env: envMap, - } - + Entities = make(EntitiesByClass, 0) rwmutex.Unlock() } -func GetAll() *variableBase { - rwmutex.RLock() - defer rwmutex.RUnlock() - - return contents -} - -func GetEntities() entitiesByClass { +func GetEntities() EntitiesByClass { rwmutex.RLock() - copiedEntities := make(entitiesByClass, len(contents.Entities)) + copiedEntities := make(EntitiesByClass, len(Entities)) - for entityName, entityInstances := range contents.Entities { + for entityName, entityInstances := range Entities { copiedInstances := make(entityInstancesByKey, len(entityInstances)) for key, entity := range entityInstances { @@ -93,7 +52,7 @@ func GetEntityInstances(entityName string) entityInstancesByKey { rwmutex.RLock() defer rwmutex.RUnlock() - if entities, ok := contents.Entities[entityName]; ok { + if entities, ok := Entities[entityName]; ok { copiedInstances := make(entityInstancesByKey, len(entities)) for key, entity := range entities { @@ -108,11 +67,11 @@ func GetEntityInstances(entityName string) entityInstancesByKey { func AddEntity(entityName string, entityKey string, data any) { rwmutex.Lock() - if _, ok := contents.Entities[entityName]; !ok { - contents.Entities[entityName] = make(entityInstancesByKey, 0) + if _, ok := Entities[entityName]; !ok { + Entities[entityName] = make(entityInstancesByKey, 0) } - contents.Entities[entityName][entityKey] = &Entity{ + Entities[entityName][entityKey] = &Entity{ Data: data, UniqueKey: entityKey, Title: findEntityTitle(data), @@ -144,3 +103,10 @@ func findEntityTitle(data any) string { return "Untitled Entity" } + +func ClearEntitiesOfType(entityType string) { + rwmutex.Lock() + defer rwmutex.Unlock() + + delete(Entities, entityType) +} diff --git a/service/internal/executor/arguments.go b/service/internal/executor/arguments.go index 843ea16..89d984e 100644 --- a/service/internal/executor/arguments.go +++ b/service/internal/executor/arguments.go @@ -3,6 +3,7 @@ package executor import ( config "github.com/OliveTin/OliveTin/internal/config" "github.com/OliveTin/OliveTin/internal/entities" + "github.com/OliveTin/OliveTin/internal/tpl" log "github.com/sirupsen/logrus" "fmt" @@ -75,7 +76,7 @@ func parseSingleExec(a string, values map[string]string, entity *entities.Entity if err != nil { return "", err } - return entities.ParseTemplateWithArgs(arg, entity, values), nil + return tpl.ParseTemplateWithArgs(arg, entity, values), nil } func validateArguments(values map[string]string, action *config.Action) error { @@ -117,7 +118,7 @@ func parseActionArguments(values map[string]string, action *config.Action, entit }).Debugf("Arg assigned") } - parsedShellCommand := entities.ParseTemplateWithArgs(rawShellCommand, entity, values) + parsedShellCommand := tpl.ParseTemplateWithArgs(rawShellCommand, entity, values) redactedShellCommand := redactShellCommand(parsedShellCommand, action.Arguments, values) if err != nil { @@ -256,7 +257,7 @@ func typecheckChoiceEntity(value string, arg *config.ActionArgument) error { templateChoice := arg.Choices[0].Value for _, ent := range entities.GetEntityInstances(arg.Entity) { - choice := entities.ParseTemplateWith(templateChoice, ent) + choice := tpl.ParseTemplateWith(templateChoice, ent) if value == choice { return nil diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 21f862b..b9d7f77 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -6,6 +6,7 @@ import ( authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic" config "github.com/OliveTin/OliveTin/internal/config" "github.com/OliveTin/OliveTin/internal/entities" + "github.com/OliveTin/OliveTin/internal/tpl" "github.com/google/uuid" log "github.com/sirupsen/logrus" @@ -728,7 +729,7 @@ func stepRequestAction(req *ExecutionRequest) bool { req.logEntry.Binding = req.Binding req.logEntry.ActionConfigTitle = req.Binding.Action.Title - req.logEntry.ActionTitle = entities.ParseTemplateWith(req.Binding.Action.Title, req.Binding.Entity) + req.logEntry.ActionTitle = tpl.ParseTemplateWith(req.Binding.Action.Title, req.Binding.Entity) req.logEntry.ActionIcon = req.Binding.Action.Icon req.logEntry.Tags = req.Tags diff --git a/service/internal/entities/templates.go b/service/internal/tpl/templates.go similarity index 65% rename from service/internal/entities/templates.go rename to service/internal/tpl/templates.go index 21db7c0..a0aea20 100644 --- a/service/internal/entities/templates.go +++ b/service/internal/tpl/templates.go @@ -1,19 +1,75 @@ -package entities +package tpl import ( "fmt" + "os" "regexp" "strings" "text/template" + "github.com/OliveTin/OliveTin/internal/entities" + "github.com/OliveTin/OliveTin/internal/installationinfo" log "github.com/sirupsen/logrus" ) var tpl = template.New("tpl") +type olivetinInfo struct { + Build *installationinfo.BuildInfo + Runtime *installationinfo.RuntimeInfo +} + var legacyArgumentRegex = regexp.MustCompile(`{{ ([a-zA-Z0-9_]+) }}`) var legacyEntityPropertiesRegex = regexp.MustCompile(`{{ ([a-zA-Z0-9_]+)\.([a-zA-Z0-9_\.]+) }}`) +type generalTemplateContext struct { + OliveTin olivetinInfo + Env map[string]string +} + +type actionTemplateContext struct { + CurrentEntity interface{} + Arguments map[string]string + + // These are deliberately repeated because embedding structs + // won't work in text/template. + OliveTin olivetinInfo + Env map[string]string +} + +var ( + cachedOliveTinInfo olivetinInfo + cachedEnvMap map[string]string +) + +func init() { + cachedOliveTinInfo = olivetinInfo{ + Build: installationinfo.Build, + Runtime: installationinfo.Runtime, + } + + cachedEnvMap = buildEnvMap() +} + +func GetNewGeneralTemplateContext() *generalTemplateContext { + return &generalTemplateContext{ + OliveTin: cachedOliveTinInfo, + Env: cachedEnvMap, + } +} + +func buildEnvMap() map[string]string { + envMap := make(map[string]string) + for _, env := range os.Environ() { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + envMap[parts[0]] = parts[1] + } + } + + return envMap +} + func migrateLegacyEntityProperties(rawShellCommand string) string { foundArgumentNames := legacyEntityPropertiesRegex.FindAllStringSubmatch(rawShellCommand, -1) @@ -68,7 +124,7 @@ func migrateLegacyArgumentNames(rawShellCommand string) string { return rawShellCommand } -func ParseTemplateWithArgs(source string, ent *Entity, args map[string]string) string { +func ParseTemplateWithArgs(source string, ent *entities.Entity, args map[string]string) string { source = migrateLegacyArgumentNames(source) source = migrateLegacyEntityProperties(source) @@ -90,11 +146,12 @@ func ParseTemplateWithArgs(source string, ent *Entity, args map[string]string) s entdata = ent.Data } - templateVariables := &variableBase{ - OliveTin: GetAll().OliveTin, + templateVariables := &actionTemplateContext{ + OliveTin: cachedOliveTinInfo, + Env: cachedEnvMap, + Arguments: args, CurrentEntity: entdata, - Env: GetAll().Env, } var sb strings.Builder @@ -114,21 +171,14 @@ func ParseTemplateWithArgs(source string, ent *Entity, args map[string]string) s return ret } -func ParseTemplateWith(source string, ent *Entity) string { +func ParseTemplateWith(source string, ent *entities.Entity) string { return ParseTemplateWithArgs(source, ent, nil) } -func ParseTemplateBoolWith(source string, ent *Entity) bool { +func ParseTemplateBoolWith(source string, ent *entities.Entity) bool { source = strings.TrimSpace(source) tplBool := ParseTemplateWith(source, ent) return tplBool == "true" } - -func ClearEntities(entityType string) { - rwmutex.Lock() - defer rwmutex.Unlock() - - delete(contents.Entities, entityType) -} From e807cb5231bc47134f7fa2d6d381cf0e0c9b176e Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Sun, 25 Jan 2026 21:50:03 +0000 Subject: [PATCH 04/20] feat: configuration options for navigate-on-start icons --- .../scripts/gen/olivetin/api/v1/olivetin_pb.d.ts | 8 ++++++-- .../scripts/gen/olivetin/api/v1/olivetin_pb.js | 5 ++--- frontend/resources/vue/ActionButton.vue | 15 ++++++++++----- proto/olivetin/api/v1/olivetin.proto | 3 ++- service/gen/olivetin/api/v1/olivetin.pb.go | 13 +++++++++++-- service/internal/api/api.go | 1 + service/internal/config/config.go | 2 ++ 7 files changed, 34 insertions(+), 13 deletions(-) diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts index 1677b07..b28fdd9 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.10.2 +// @generated by protoc-gen-es v2.11.0 // @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3) /* eslint-disable */ @@ -1458,6 +1458,11 @@ export declare type InitResponse = Message<"olivetin.api.v1.InitResponse"> & { * @generated from field: repeated string available_themes = 24; */ availableThemes: string[]; + + /** + * @generated from field: bool show_navigate_on_start_icons = 25; + */ + showNavigateOnStartIcons: boolean; }; /** @@ -1841,4 +1846,3 @@ export declare const OliveTinApiService: GenService<{ output: typeof EntitySchema; }, }>; - diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js index 69405d7..02156ca 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.10.2 +// @generated by protoc-gen-es v2.11.0 // @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3) /* eslint-disable */ @@ -8,7 +8,7 @@ import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2 * Describes the file olivetin/api/v1/olivetin.proto. */ export const file_olivetin_api_v1_olivetin = /*@__PURE__*/ - fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLcAQoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiQgoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSJ9ChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJImcKGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IkoKGlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIsChdTdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiOQoYU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgCIAEoCSIzCh5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIk8KH1N0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IjsKDkdldExvZ3NSZXF1ZXN0EhQKDHN0YXJ0X29mZnNldBgBIAEoAxITCgtkYXRlX2ZpbHRlchgCIAEoCSKaAwoITG9nRW50cnkSGAoQZGF0ZXRpbWVfc3RhcnRlZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSDgoGb3V0cHV0GAMgASgJEhEKCXRpbWVkX291dBgFIAEoCBIRCglleGl0X2NvZGUYBiABKAUSDAoEdXNlchgHIAEoCRISCgp1c2VyX2NsYXNzGAggASgJEhMKC2FjdGlvbl9pY29uGAkgASgJEgwKBHRhZ3MYCiADKAkSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAsgASgJEhkKEWRhdGV0aW1lX2ZpbmlzaGVkGAwgASgJEhkKEWV4ZWN1dGlvbl9zdGFydGVkGA4gASgIEhoKEmV4ZWN1dGlvbl9maW5pc2hlZBgPIAEoCBIPCgdibG9ja2VkGBAgASgIEhYKDmRhdGV0aW1lX2luZGV4GBEgASgDEhAKCGNhbl9raWxsGBIgASgIEiMKG2RhdGV0aW1lX3JhdGVfbGltaXRfZXhwaXJlcxgTIAEoCRISCgpiaW5kaW5nX2lkGBQgASgJIpEBCg9HZXRMb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyI/ChRHZXRBY3Rpb25Mb2dzUmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSFAoMc3RhcnRfb2Zmc2V0GAIgASgDIpcBChVHZXRBY3Rpb25Mb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyJlChtWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QSDQoFdmFsdWUYASABKAkSDAoEdHlwZRgCIAEoCRISCgpiaW5kaW5nX2lkGAMgASgJEhUKDWFyZ3VtZW50X25hbWUYBCABKAkiQgocVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZRINCgV2YWxpZBgBIAEoCBITCgtkZXNjcmlwdGlvbhgCIAEoCSI2ChVXYXRjaEV4ZWN1dGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJIiYKFFdhdGNoRXhlY3V0aW9uVXBkYXRlEg4KBnVwZGF0ZRgBIAEoCSJKChZFeGVjdXRpb25TdGF0dXNSZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIRCglhY3Rpb25faWQYAiABKAkiRwoXRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5Ig8KDVdob0FtSVJlcXVlc3QibAoOV2hvQW1JUmVzcG9uc2USGgoSYXV0aGVudGljYXRlZF91c2VyGAEgASgJEhEKCXVzZXJncm91cBgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIMCgRhY2xzGAQgAygJEgsKA3NpZBgFIAEoCSISChBTb3NSZXBvcnRSZXF1ZXN0IiIKEVNvc1JlcG9ydFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJIhEKD0R1bXBWYXJzUmVxdWVzdCKVAQoQRHVtcFZhcnNSZXNwb25zZRINCgVhbGVydBgBIAEoCRJBCghjb250ZW50cxgCIAMoCzIvLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1Jlc3BvbnNlLkNvbnRlbnRzRW50cnkaLwoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIjsKDERlYnVnQmluZGluZxIUCgxhY3Rpb25fdGl0bGUYASABKAkSFQoNZW50aXR5X3ByZWZpeBgCIAEoCSIeChxEdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Is4BCh1EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZRINCgVhbGVydBgBIAEoCRJOCghjb250ZW50cxgCIAMoCzI8Lm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZS5Db250ZW50c0VudHJ5Gk4KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEiwKBXZhbHVlGAIgASgLMh0ub2xpdmV0aW4uYXBpLnYxLkRlYnVnQmluZGluZzoCOAEiEgoQR2V0UmVhZHl6UmVxdWVzdCIjChFHZXRSZWFkeXpSZXNwb25zZRIOCgZzdGF0dXMYASABKAkiFAoSRXZlbnRTdHJlYW1SZXF1ZXN0IuMCChNFdmVudFN0cmVhbVJlc3BvbnNlEj0KDmVudGl0eV9jaGFuZ2VkGAIgASgLMiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50RW50aXR5Q2hhbmdlZEgAEj0KDmNvbmZpZ19jaGFuZ2VkGAMgASgLMiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50Q29uZmlnQ2hhbmdlZEgAEkUKEmV4ZWN1dGlvbl9maW5pc2hlZBgEIAEoCzInLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvbkZpbmlzaGVkSAASQwoRZXhlY3V0aW9uX3N0YXJ0ZWQYBSABKAsyJi5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25TdGFydGVkSAASOQoMb3V0cHV0X2NodW5rGAYgASgLMiEub2xpdmV0aW4uYXBpLnYxLkV2ZW50T3V0cHV0Q2h1bmtIAEIHCgVldmVudCJBChBFdmVudE91dHB1dENodW5rEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZvdXRwdXQYAiABKAkiFAoSRXZlbnRFbnRpdHlDaGFuZ2VkIhQKEkV2ZW50Q29uZmlnQ2hhbmdlZCJGChZFdmVudEV4ZWN1dGlvbkZpbmlzaGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJFChVFdmVudEV4ZWN1dGlvblN0YXJ0ZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IjIKEUtpbGxBY3Rpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSJtChJLaWxsQWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBmtpbGxlZBgCIAEoCBIZChFhbHJlYWR5X2NvbXBsZXRlZBgDIAEoCBINCgVmb3VuZBgEIAEoCCI7ChVMb2NhbFVzZXJMb2dpblJlcXVlc3QSEAoIdXNlcm5hbWUYASABKAkSEAoIcGFzc3dvcmQYAiABKAkiKQoWTG9jYWxVc2VyTG9naW5SZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIicKE1Bhc3N3b3JkSGFzaFJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkiJAoUUGFzc3dvcmRIYXNoUmVzcG9uc2USDAoEaGFzaBgBIAEoCSIPCg1Mb2dvdXRSZXF1ZXN0IhAKDkxvZ291dFJlc3BvbnNlIhcKFUdldERpYWdub3N0aWNzUmVxdWVzdCJFChZHZXREaWFnbm9zdGljc1Jlc3BvbnNlEhMKC1NzaEZvdW5kS2V5GAEgASgJEhYKDlNzaEZvdW5kQ29uZmlnGAIgASgJIg0KC0luaXRSZXF1ZXN0IsUFCgxJbml0UmVzcG9uc2USEgoKc2hvd0Zvb3RlchgBIAEoCBIWCg5zaG93TmF2aWdhdGlvbhgCIAEoCBIXCg9zaG93TmV3VmVyc2lvbnMYAyABKAgSGAoQYXZhaWxhYmxlVmVyc2lvbhgEIAEoCRIWCg5jdXJyZW50VmVyc2lvbhgFIAEoCRIRCglwYWdlVGl0bGUYBiABKAkSHgoWc2VjdGlvbk5hdmlnYXRpb25TdHlsZRgHIAEoCRIaChJkZWZhdWx0SWNvbkZvckJhY2sYCCABKAkSFgoOZW5hYmxlQ3VzdG9tSnMYCSABKAgSFAoMYXV0aExvZ2luVXJsGAogASgJEhYKDmF1dGhMb2NhbExvZ2luGAsgASgIEhEKCXN0eWxlTW9kcxgMIAMoCRI4Cg9vQXV0aDJQcm92aWRlcnMYDSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuT0F1dGgyUHJvdmlkZXISOAoPYWRkaXRpb25hbExpbmtzGA4gAygLMh8ub2xpdmV0aW4uYXBpLnYxLkFkZGl0aW9uYWxMaW5rEhYKDnJvb3REYXNoYm9hcmRzGA8gAygJEhoKEmF1dGhlbnRpY2F0ZWRfdXNlchgQIAEoCRIjChthdXRoZW50aWNhdGVkX3VzZXJfcHJvdmlkZXIYESABKAkSOgoQZWZmZWN0aXZlX3BvbGljeRgSIAEoCzIgLm9saXZldGluLmFwaS52MS5FZmZlY3RpdmVQb2xpY3kSFgoOYmFubmVyX21lc3NhZ2UYEyABKAkSEgoKYmFubmVyX2NzcxgUIAEoCRIYChBzaG93X2RpYWdub3N0aWNzGBUgASgIEhUKDXNob3dfbG9nX2xpc3QYFiABKAgSFgoObG9naW5fcmVxdWlyZWQYFyABKAgSGAoQYXZhaWxhYmxlX3RoZW1lcxgYIAMoCSIsCg5BZGRpdGlvbmFsTGluaxINCgV0aXRsZRgBIAEoCRILCgN1cmwYAiABKAkiOgoOT0F1dGgyUHJvdmlkZXISDQoFdGl0bGUYASABKAkSDAoEaWNvbhgDIAEoCRILCgNrZXkYBCABKAkiLQoXR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCSJDChhHZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2USJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbiIUChJHZXRFbnRpdGllc1JlcXVlc3QiVAoTR2V0RW50aXRpZXNSZXNwb25zZRI9ChJlbnRpdHlfZGVmaW5pdGlvbnMYASADKAsyIS5vbGl2ZXRpbi5hcGkudjEuRW50aXR5RGVmaW5pdGlvbiJpChBFbnRpdHlEZWZpbml0aW9uEg0KBXRpdGxlGAEgASgJEioKCWluc3RhbmNlcxgCIAMoCzIXLm9saXZldGluLmFwaS52MS5FbnRpdHkSGgoSdXNlZF9vbl9kYXNoYm9hcmRzGAMgAygJIjQKEEdldEVudGl0eVJlcXVlc3QSEgoKdW5pcXVlX2tleRgBIAEoCRIMCgR0eXBlGAIgASgJIjUKFFJlc3RhcnRBY3Rpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCTLoEgoST2xpdmVUaW5BcGlTZXJ2aWNlEl0KDEdldERhc2hib2FyZBIkLm9saXZldGluLmFwaS52MS5HZXREYXNoYm9hcmRSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlc3BvbnNlIgASWgoLU3RhcnRBY3Rpb24SIy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJvChJTdGFydEFjdGlvbkFuZFdhaXQSKi5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBorLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZSIAEmkKEFN0YXJ0QWN0aW9uQnlHZXQSKC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlIgASfgoXU3RhcnRBY3Rpb25CeUdldEFuZFdhaXQSLy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0GjAub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2UiABJeCg1SZXN0YXJ0QWN0aW9uEiUub2xpdmV0aW4uYXBpLnYxLlJlc3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJXCgpLaWxsQWN0aW9uEiIub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXF1ZXN0GiMub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXNwb25zZSIAEmYKD0V4ZWN1dGlvblN0YXR1cxInLm9saXZldGluLmFwaS52MS5FeGVjdXRpb25TdGF0dXNSZXF1ZXN0Gigub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1Jlc3BvbnNlIgASTgoHR2V0TG9ncxIfLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVxdWVzdBogLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVzcG9uc2UiABJgCg1HZXRBY3Rpb25Mb2dzEiUub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXF1ZXN0GiYub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXNwb25zZSIAEnUKFFZhbGlkYXRlQXJndW1lbnRUeXBlEiwub2xpdmV0aW4uYXBpLnYxLlZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBotLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlIgASSwoGV2hvQW1JEh4ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuV2hvQW1JUmVzcG9uc2UiABJUCglTb3NSZXBvcnQSIS5vbGl2ZXRpbi5hcGkudjEuU29zUmVwb3J0UmVxdWVzdBoiLm9saXZldGluLmFwaS52MS5Tb3NSZXBvcnRSZXNwb25zZSIAElEKCER1bXBWYXJzEiAub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVxdWVzdBohLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1Jlc3BvbnNlIgASeAoVRHVtcFB1YmxpY0lkQWN0aW9uTWFwEi0ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QaLi5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UiABJUCglHZXRSZWFkeXoSIS5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVxdWVzdBoiLm9saXZldGluLmFwaS52MS5HZXRSZWFkeXpSZXNwb25zZSIAEmMKDkxvY2FsVXNlckxvZ2luEiYub2xpdmV0aW4uYXBpLnYxLkxvY2FsVXNlckxvZ2luUmVxdWVzdBonLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlc3BvbnNlIgASXQoMUGFzc3dvcmRIYXNoEiQub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuUGFzc3dvcmRIYXNoUmVzcG9uc2UiABJLCgZMb2dvdXQSHi5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVxdWVzdBofLm9saXZldGluLmFwaS52MS5Mb2dvdXRSZXNwb25zZSIAElwKC0V2ZW50U3RyZWFtEiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50U3RyZWFtUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlc3BvbnNlIgAwARJjCg5HZXREaWFnbm9zdGljcxImLm9saXZldGluLmFwaS52MS5HZXREaWFnbm9zdGljc1JlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXNwb25zZSIAEkUKBEluaXQSHC5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlcXVlc3QaHS5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlc3BvbnNlIgASaQoQR2V0QWN0aW9uQmluZGluZxIoLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVxdWVzdBopLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2UiABJaCgtHZXRFbnRpdGllcxIjLm9saXZldGluLmFwaS52MS5HZXRFbnRpdGllc1JlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXNwb25zZSIAEkkKCUdldEVudGl0eRIhLm9saXZldGluLmFwaS52MS5HZXRFbnRpdHlSZXF1ZXN0Ghcub2xpdmV0aW4uYXBpLnYxLkVudGl0eSIAQjhaNmdpdGh1Yi5jb20vT2xpdmVUaW4vT2xpdmVUaW4vZ2VuL29saXZldGluL2FwaS92MTthcGl2MWIGcHJvdG8z"); + fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLcAQoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiQgoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSJ9ChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJImcKGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IkoKGlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIsChdTdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiOQoYU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgCIAEoCSIzCh5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIk8KH1N0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IjsKDkdldExvZ3NSZXF1ZXN0EhQKDHN0YXJ0X29mZnNldBgBIAEoAxITCgtkYXRlX2ZpbHRlchgCIAEoCSKaAwoITG9nRW50cnkSGAoQZGF0ZXRpbWVfc3RhcnRlZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSDgoGb3V0cHV0GAMgASgJEhEKCXRpbWVkX291dBgFIAEoCBIRCglleGl0X2NvZGUYBiABKAUSDAoEdXNlchgHIAEoCRISCgp1c2VyX2NsYXNzGAggASgJEhMKC2FjdGlvbl9pY29uGAkgASgJEgwKBHRhZ3MYCiADKAkSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAsgASgJEhkKEWRhdGV0aW1lX2ZpbmlzaGVkGAwgASgJEhkKEWV4ZWN1dGlvbl9zdGFydGVkGA4gASgIEhoKEmV4ZWN1dGlvbl9maW5pc2hlZBgPIAEoCBIPCgdibG9ja2VkGBAgASgIEhYKDmRhdGV0aW1lX2luZGV4GBEgASgDEhAKCGNhbl9raWxsGBIgASgIEiMKG2RhdGV0aW1lX3JhdGVfbGltaXRfZXhwaXJlcxgTIAEoCRISCgpiaW5kaW5nX2lkGBQgASgJIpEBCg9HZXRMb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyI/ChRHZXRBY3Rpb25Mb2dzUmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSFAoMc3RhcnRfb2Zmc2V0GAIgASgDIpcBChVHZXRBY3Rpb25Mb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyJlChtWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QSDQoFdmFsdWUYASABKAkSDAoEdHlwZRgCIAEoCRISCgpiaW5kaW5nX2lkGAMgASgJEhUKDWFyZ3VtZW50X25hbWUYBCABKAkiQgocVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZRINCgV2YWxpZBgBIAEoCBITCgtkZXNjcmlwdGlvbhgCIAEoCSI2ChVXYXRjaEV4ZWN1dGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJIiYKFFdhdGNoRXhlY3V0aW9uVXBkYXRlEg4KBnVwZGF0ZRgBIAEoCSJKChZFeGVjdXRpb25TdGF0dXNSZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIRCglhY3Rpb25faWQYAiABKAkiRwoXRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5Ig8KDVdob0FtSVJlcXVlc3QibAoOV2hvQW1JUmVzcG9uc2USGgoSYXV0aGVudGljYXRlZF91c2VyGAEgASgJEhEKCXVzZXJncm91cBgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIMCgRhY2xzGAQgAygJEgsKA3NpZBgFIAEoCSISChBTb3NSZXBvcnRSZXF1ZXN0IiIKEVNvc1JlcG9ydFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJIhEKD0R1bXBWYXJzUmVxdWVzdCKVAQoQRHVtcFZhcnNSZXNwb25zZRINCgVhbGVydBgBIAEoCRJBCghjb250ZW50cxgCIAMoCzIvLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1Jlc3BvbnNlLkNvbnRlbnRzRW50cnkaLwoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIjsKDERlYnVnQmluZGluZxIUCgxhY3Rpb25fdGl0bGUYASABKAkSFQoNZW50aXR5X3ByZWZpeBgCIAEoCSIeChxEdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Is4BCh1EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZRINCgVhbGVydBgBIAEoCRJOCghjb250ZW50cxgCIAMoCzI8Lm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZS5Db250ZW50c0VudHJ5Gk4KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEiwKBXZhbHVlGAIgASgLMh0ub2xpdmV0aW4uYXBpLnYxLkRlYnVnQmluZGluZzoCOAEiEgoQR2V0UmVhZHl6UmVxdWVzdCIjChFHZXRSZWFkeXpSZXNwb25zZRIOCgZzdGF0dXMYASABKAkiFAoSRXZlbnRTdHJlYW1SZXF1ZXN0IuMCChNFdmVudFN0cmVhbVJlc3BvbnNlEj0KDmVudGl0eV9jaGFuZ2VkGAIgASgLMiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50RW50aXR5Q2hhbmdlZEgAEj0KDmNvbmZpZ19jaGFuZ2VkGAMgASgLMiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50Q29uZmlnQ2hhbmdlZEgAEkUKEmV4ZWN1dGlvbl9maW5pc2hlZBgEIAEoCzInLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvbkZpbmlzaGVkSAASQwoRZXhlY3V0aW9uX3N0YXJ0ZWQYBSABKAsyJi5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25TdGFydGVkSAASOQoMb3V0cHV0X2NodW5rGAYgASgLMiEub2xpdmV0aW4uYXBpLnYxLkV2ZW50T3V0cHV0Q2h1bmtIAEIHCgVldmVudCJBChBFdmVudE91dHB1dENodW5rEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZvdXRwdXQYAiABKAkiFAoSRXZlbnRFbnRpdHlDaGFuZ2VkIhQKEkV2ZW50Q29uZmlnQ2hhbmdlZCJGChZFdmVudEV4ZWN1dGlvbkZpbmlzaGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJFChVFdmVudEV4ZWN1dGlvblN0YXJ0ZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IjIKEUtpbGxBY3Rpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSJtChJLaWxsQWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBmtpbGxlZBgCIAEoCBIZChFhbHJlYWR5X2NvbXBsZXRlZBgDIAEoCBINCgVmb3VuZBgEIAEoCCI7ChVMb2NhbFVzZXJMb2dpblJlcXVlc3QSEAoIdXNlcm5hbWUYASABKAkSEAoIcGFzc3dvcmQYAiABKAkiKQoWTG9jYWxVc2VyTG9naW5SZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIicKE1Bhc3N3b3JkSGFzaFJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkiJAoUUGFzc3dvcmRIYXNoUmVzcG9uc2USDAoEaGFzaBgBIAEoCSIPCg1Mb2dvdXRSZXF1ZXN0IhAKDkxvZ291dFJlc3BvbnNlIhcKFUdldERpYWdub3N0aWNzUmVxdWVzdCJFChZHZXREaWFnbm9zdGljc1Jlc3BvbnNlEhMKC1NzaEZvdW5kS2V5GAEgASgJEhYKDlNzaEZvdW5kQ29uZmlnGAIgASgJIg0KC0luaXRSZXF1ZXN0IusFCgxJbml0UmVzcG9uc2USEgoKc2hvd0Zvb3RlchgBIAEoCBIWCg5zaG93TmF2aWdhdGlvbhgCIAEoCBIXCg9zaG93TmV3VmVyc2lvbnMYAyABKAgSGAoQYXZhaWxhYmxlVmVyc2lvbhgEIAEoCRIWCg5jdXJyZW50VmVyc2lvbhgFIAEoCRIRCglwYWdlVGl0bGUYBiABKAkSHgoWc2VjdGlvbk5hdmlnYXRpb25TdHlsZRgHIAEoCRIaChJkZWZhdWx0SWNvbkZvckJhY2sYCCABKAkSFgoOZW5hYmxlQ3VzdG9tSnMYCSABKAgSFAoMYXV0aExvZ2luVXJsGAogASgJEhYKDmF1dGhMb2NhbExvZ2luGAsgASgIEhEKCXN0eWxlTW9kcxgMIAMoCRI4Cg9vQXV0aDJQcm92aWRlcnMYDSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuT0F1dGgyUHJvdmlkZXISOAoPYWRkaXRpb25hbExpbmtzGA4gAygLMh8ub2xpdmV0aW4uYXBpLnYxLkFkZGl0aW9uYWxMaW5rEhYKDnJvb3REYXNoYm9hcmRzGA8gAygJEhoKEmF1dGhlbnRpY2F0ZWRfdXNlchgQIAEoCRIjChthdXRoZW50aWNhdGVkX3VzZXJfcHJvdmlkZXIYESABKAkSOgoQZWZmZWN0aXZlX3BvbGljeRgSIAEoCzIgLm9saXZldGluLmFwaS52MS5FZmZlY3RpdmVQb2xpY3kSFgoOYmFubmVyX21lc3NhZ2UYEyABKAkSEgoKYmFubmVyX2NzcxgUIAEoCRIYChBzaG93X2RpYWdub3N0aWNzGBUgASgIEhUKDXNob3dfbG9nX2xpc3QYFiABKAgSFgoObG9naW5fcmVxdWlyZWQYFyABKAgSGAoQYXZhaWxhYmxlX3RoZW1lcxgYIAMoCRIkChxzaG93X25hdmlnYXRlX29uX3N0YXJ0X2ljb25zGBkgASgIIiwKDkFkZGl0aW9uYWxMaW5rEg0KBXRpdGxlGAEgASgJEgsKA3VybBgCIAEoCSI6Cg5PQXV0aDJQcm92aWRlchINCgV0aXRsZRgBIAEoCRIMCgRpY29uGAMgASgJEgsKA2tleRgEIAEoCSItChdHZXRBY3Rpb25CaW5kaW5nUmVxdWVzdBISCgpiaW5kaW5nX2lkGAEgASgJIkMKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uIhQKEkdldEVudGl0aWVzUmVxdWVzdCJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uImkKEEVudGl0eURlZmluaXRpb24SDQoFdGl0bGUYASABKAkSKgoJaW5zdGFuY2VzGAIgAygLMhcub2xpdmV0aW4uYXBpLnYxLkVudGl0eRIaChJ1c2VkX29uX2Rhc2hib2FyZHMYAyADKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMugSChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASdQoUVmFsaWRhdGVBcmd1bWVudFR5cGUSLC5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Gi0ub2xpdmV0aW4uYXBpLnYxLlZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2UiABJLCgZXaG9BbUkSHi5vbGl2ZXRpbi5hcGkudjEuV2hvQW1JUmVxdWVzdBofLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXNwb25zZSIAElQKCVNvc1JlcG9ydBIhLm9saXZldGluLmFwaS52MS5Tb3NSZXBvcnRSZXF1ZXN0GiIub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlc3BvbnNlIgASUQoIRHVtcFZhcnMSIC5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXF1ZXN0GiEub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UiABJ4ChVEdW1wUHVibGljSWRBY3Rpb25NYXASLS5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdBouLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZSIAElQKCUdldFJlYWR5ehIhLm9saXZldGluLmFwaS52MS5HZXRSZWFkeXpSZXF1ZXN0GiIub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlc3BvbnNlIgASYwoOTG9jYWxVc2VyTG9naW4SJi5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkxvY2FsVXNlckxvZ2luUmVzcG9uc2UiABJdCgxQYXNzd29yZEhhc2gSJC5vbGl2ZXRpbi5hcGkudjEuUGFzc3dvcmRIYXNoUmVxdWVzdBolLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXNwb25zZSIAEksKBkxvZ291dBIeLm9saXZldGluLmFwaS52MS5Mb2dvdXRSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlc3BvbnNlIgASXAoLRXZlbnRTdHJlYW0SIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkV2ZW50U3RyZWFtUmVzcG9uc2UiADABEmMKDkdldERpYWdub3N0aWNzEiYub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVxdWVzdBonLm9saXZldGluLmFwaS52MS5HZXREaWFnbm9zdGljc1Jlc3BvbnNlIgASRQoESW5pdBIcLm9saXZldGluLmFwaS52MS5Jbml0UmVxdWVzdBodLm9saXZldGluLmFwaS52MS5Jbml0UmVzcG9uc2UiABJpChBHZXRBY3Rpb25CaW5kaW5nEigub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkJpbmRpbmdSZXF1ZXN0Gikub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkJpbmRpbmdSZXNwb25zZSIAEloKC0dldEVudGl0aWVzEiMub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5HZXRFbnRpdGllc1Jlc3BvbnNlIgASSQoJR2V0RW50aXR5EiEub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0eVJlcXVlc3QaFy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5IgBCOFo2Z2l0aHViLmNvbS9PbGl2ZVRpbi9PbGl2ZVRpbi9nZW4vb2xpdmV0aW4vYXBpL3YxO2FwaXYxYgZwcm90bzM"); /** * Describes the message olivetin.api.v1.Action. @@ -491,4 +491,3 @@ export const RestartActionRequestSchema = /*@__PURE__*/ */ export const OliveTinApiService = /*@__PURE__*/ serviceDesc(file_olivetin_api_v1_olivetin, 0); - diff --git a/frontend/resources/vue/ActionButton.vue b/frontend/resources/vue/ActionButton.vue index 2a35268..8f81521 100644 --- a/frontend/resources/vue/ActionButton.vue +++ b/frontend/resources/vue/ActionButton.vue @@ -3,7 +3,7 @@ <button :id="`actionButtonInner-${bindingId}`" :title="title" :disabled="!canExec || isDisabled" :class="combinedClasses" @click="handleClick"> - <div class="navigate-on-start-container"> + <div v-if="showNavigateOnStartIcons" class="navigate-on-start-container"> <div v-if="navigateOnStart == 'pop'" class="navigate-on-start" title="Opens a popup dialog on start"> <HugeiconsIcon :icon="ComputerTerminal01Icon" /> </div> @@ -69,6 +69,11 @@ let rateLimitInterval = null // Animation classes const buttonClasses = ref([]) +// Show navigate on start icons - defaults to true if not set +const showNavigateOnStartIcons = computed(() => { + return window.initResponse?.showNavigateOnStartIcons ?? true +}) + // Combined classes including custom cssClass const combinedClasses = computed(() => { const classes = [...buttonClasses.value] @@ -110,7 +115,7 @@ function constructFromJson(json) { isDisabled.value = !json.canExec displayTitle.value = title.value unicodeIcon.value = getUnicodeIcon(json.icon) - + // Initialize rate limit from action data (parse datetime string) if (json.datetimeRateLimitExpires) { const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T')) @@ -130,7 +135,7 @@ function updateFromJson(json) { // title - as the callback URL relies on it unicodeIcon.value = getUnicodeIcon(json.icon) - + // Update rate limiting if changed (parse datetime string) if (json.datetimeRateLimitExpires) { const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T')) @@ -171,7 +176,7 @@ function updateRateLimitStatus() { isRateLimited.value = true const secondsRemaining = expires - now rateLimitMessage.value = `Rate limited, available in ${secondsRemaining} second${secondsRemaining !== 1 ? 's' : ''}` - + // Set up interval to update every second if (!rateLimitInterval) { rateLimitInterval = setInterval(() => { @@ -282,7 +287,7 @@ function onExecStatusChanged() { onMounted(() => { constructFromJson(props.actionData) - + // Watch the central rate limit store for updates to this button's bindingId // Watch the entire rateLimits object to ensure reactivity with dynamic keys watch( diff --git a/proto/olivetin/api/v1/olivetin.proto b/proto/olivetin/api/v1/olivetin.proto index ced1982..cc12212 100644 --- a/proto/olivetin/api/v1/olivetin.proto +++ b/proto/olivetin/api/v1/olivetin.proto @@ -333,6 +333,7 @@ message InitResponse { bool show_log_list = 22; bool login_required = 23; repeated string available_themes = 24; // List of available theme names + bool show_navigate_on_start_icons = 25; } message AdditionalLink { @@ -394,7 +395,7 @@ service OliveTinApiService { rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) {} rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {} - + rpc GetActionLogs(GetActionLogsRequest) returns (GetActionLogsResponse) {} rpc ValidateArgumentType(ValidateArgumentTypeRequest) returns (ValidateArgumentTypeResponse) {} diff --git a/service/gen/olivetin/api/v1/olivetin.pb.go b/service/gen/olivetin/api/v1/olivetin.pb.go index 9516ebd..5614c01 100644 --- a/service/gen/olivetin/api/v1/olivetin.pb.go +++ b/service/gen/olivetin/api/v1/olivetin.pb.go @@ -3237,6 +3237,7 @@ type InitResponse struct { ShowLogList bool `protobuf:"varint,22,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"` LoginRequired bool `protobuf:"varint,23,opt,name=login_required,json=loginRequired,proto3" json:"login_required,omitempty"` AvailableThemes []string `protobuf:"bytes,24,rep,name=available_themes,json=availableThemes,proto3" json:"available_themes,omitempty"` // List of available theme names + ShowNavigateOnStartIcons bool `protobuf:"varint,25,opt,name=show_navigate_on_start_icons,json=showNavigateOnStartIcons,proto3" json:"show_navigate_on_start_icons,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3439,6 +3440,13 @@ func (x *InitResponse) GetAvailableThemes() []string { return nil } +func (x *InitResponse) GetShowNavigateOnStartIcons() bool { + if x != nil { + return x.ShowNavigateOnStartIcons + } + return false +} + type AdditionalLink struct { state protoimpl.MessageState `protogen:"open.v1"` Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` @@ -4096,7 +4104,7 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\x16GetDiagnosticsResponse\x12 \n" + "\vSshFoundKey\x18\x01 \x01(\tR\vSshFoundKey\x12&\n" + "\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\"\r\n" + - "\vInitRequest\"\xcd\b\n" + + "\vInitRequest\"\x8d\t\n" + "\fInitResponse\x12\x1e\n" + "\n" + "showFooter\x18\x01 \x01(\bR\n" + @@ -4125,7 +4133,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\x10show_diagnostics\x18\x15 \x01(\bR\x0fshowDiagnostics\x12\"\n" + "\rshow_log_list\x18\x16 \x01(\bR\vshowLogList\x12%\n" + "\x0elogin_required\x18\x17 \x01(\bR\rloginRequired\x12)\n" + - "\x10available_themes\x18\x18 \x03(\tR\x0favailableThemes\"8\n" + + "\x10available_themes\x18\x18 \x03(\tR\x0favailableThemes\x12>\n" + + "\x1cshow_navigate_on_start_icons\x18\x19 \x01(\bR\x18showNavigateOnStartIcons\"8\n" + "\x0eAdditionalLink\x12\x14\n" + "\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" + "\x03url\x18\x02 \x01(\tR\x03url\"L\n" + diff --git a/service/internal/api/api.go b/service/internal/api/api.go index c7ee481..6e4cbf1 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -905,6 +905,7 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq ShowLogList: user.EffectivePolicy.ShowLogList, LoginRequired: loginRequired, AvailableThemes: discoverAvailableThemes(api.cfg), + ShowNavigateOnStartIcons: api.cfg.ShowNavigateOnStartIcons, } return connect.NewResponse(res), nil diff --git a/service/internal/config/config.go b/service/internal/config/config.go index d22e86f..de5bdcc 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -128,6 +128,7 @@ type Config struct { ShowFooter bool `koanf:"showFooter"` ShowNavigation bool `koanf:"showNavigation"` ShowNewVersions bool `koanf:"showNewVersions"` + ShowNavigateOnStartIcons bool `koanf:"showNavigateOnStartIcons"` EnableCustomJs bool `koanf:"enableCustomJs"` AuthJwtCookieName string `koanf:"authJwtCookieName"` AuthJwtHeader string `koanf:"authJwtHeader"` @@ -244,6 +245,7 @@ func DefaultConfigWithBasePort(basePort int) *Config { config.ShowFooter = true config.ShowNavigation = true config.ShowNewVersions = true + config.ShowNavigateOnStartIcons = true config.EnableCustomJs = false config.ExternalRestAddress = "." config.LogLevel = "INFO" From 0e0bbd3fdb4225dd433b85834ea1abbb41bae4b1 Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Mon, 26 Jan 2026 00:34:39 +0000 Subject: [PATCH 05/20] fix: Nil binding on entity logs caused a NPE --- service/internal/api/api.go | 4 ++-- service/internal/executor/executor.go | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 6e4cbf1..f941028 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -307,11 +307,11 @@ func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry ExecutionStarted: logEntry.ExecutionStarted, ExecutionFinished: logEntry.ExecutionFinished, User: logEntry.Username, - BindingId: logEntry.Binding.ID, + BindingId: logEntry.GetBindingId(), DatetimeRateLimitExpires: calculateRateLimitExpires(api, logEntry), } - if !pble.ExecutionFinished { + if !pble.ExecutionFinished && logEntry.Binding != nil && logEntry.Binding.Action != nil { pble.CanKill = acl.IsAllowedKill(api.cfg, authenticatedUser, logEntry.Binding.Action) } diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 21f862b..b083e67 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -111,6 +111,15 @@ type InternalLogEntry struct { ActionIcon string } +// .Binding can be nil, so we need to handle that. +func (e *InternalLogEntry) GetBindingId() string { + if e.Binding == nil { + return "" + } + + return e.Binding.ID +} + type executorStepFunc func(*ExecutionRequest) bool // DefaultExecutor returns an Executor, with a sensible "chain of command" for From 561cd9c431e26ad85f935dcb94a7814042c23e9f Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Mon, 26 Jan 2026 00:46:17 +0000 Subject: [PATCH 06/20] fix: Various coderabbit suggestions on tpl ext --- .releaserc.yaml | 2 +- service/internal/api/api.go | 2 +- service/internal/entities/storage.go | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.releaserc.yaml b/.releaserc.yaml index 8aeb820..c3ddbdc 100644 --- a/.releaserc.yaml +++ b/.releaserc.yaml @@ -1,6 +1,6 @@ --- # Only allow releases on the main branch (for 3k) -# releases for 2k are published manaually. +# releases for 2k are published manually. branches: - name: main diff --git a/service/internal/api/api.go b/service/internal/api/api.go index c0a2411..53c58fa 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -711,7 +711,7 @@ func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.Dum } jsonstring, _ := json.MarshalIndent(tpl.GetNewGeneralTemplateContext(), "", " ") - fmt.Printf("%s", &jsonstring) + fmt.Printf("%s", jsonstring) res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore" diff --git a/service/internal/entities/storage.go b/service/internal/entities/storage.go index 1ae9780..e1fac78 100644 --- a/service/internal/entities/storage.go +++ b/service/internal/entities/storage.go @@ -20,21 +20,21 @@ type EntitiesByClass map[string]entityInstancesByKey var ( rwmutex = sync.RWMutex{} - Entities EntitiesByClass + entities EntitiesByClass ) func init() { rwmutex.Lock() - Entities = make(EntitiesByClass, 0) + entities = make(EntitiesByClass, 0) rwmutex.Unlock() } func GetEntities() EntitiesByClass { rwmutex.RLock() - copiedEntities := make(EntitiesByClass, len(Entities)) + copiedEntities := make(EntitiesByClass, len(entities)) - for entityName, entityInstances := range Entities { + for entityName, entityInstances := range entities { copiedInstances := make(entityInstancesByKey, len(entityInstances)) for key, entity := range entityInstances { @@ -52,7 +52,7 @@ func GetEntityInstances(entityName string) entityInstancesByKey { rwmutex.RLock() defer rwmutex.RUnlock() - if entities, ok := Entities[entityName]; ok { + if entities, ok := entities[entityName]; ok { copiedInstances := make(entityInstancesByKey, len(entities)) for key, entity := range entities { @@ -67,11 +67,11 @@ func GetEntityInstances(entityName string) entityInstancesByKey { func AddEntity(entityName string, entityKey string, data any) { rwmutex.Lock() - if _, ok := Entities[entityName]; !ok { - Entities[entityName] = make(entityInstancesByKey, 0) + if _, ok := entities[entityName]; !ok { + entities[entityName] = make(entityInstancesByKey, 0) } - Entities[entityName][entityKey] = &Entity{ + entities[entityName][entityKey] = &Entity{ Data: data, UniqueKey: entityKey, Title: findEntityTitle(data), @@ -108,5 +108,5 @@ func ClearEntitiesOfType(entityType string) { rwmutex.Lock() defer rwmutex.Unlock() - delete(Entities, entityType) + delete(entities, entityType) } From efbab6776f18f1afda2f8f65e3e8a5b49b08ebfc Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Mon, 26 Jan 2026 11:36:48 +0000 Subject: [PATCH 07/20] chore: Dep update Jan 2026 --- service/go.mod | 12 ++++++------ service/go.sum | 12 ++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/service/go.mod b/service/go.mod index 6242926..bc2a3db 100644 --- a/service/go.mod +++ b/service/go.mod @@ -23,7 +23,7 @@ require ( github.com/knadh/koanf/providers/env v1.1.0 github.com/knadh/koanf/providers/file v1.2.1 github.com/knadh/koanf/providers/rawbytes v1.0.0 - github.com/knadh/koanf/v2 v2.3.0 + github.com/knadh/koanf/v2 v2.3.2 github.com/prometheus/client_golang v1.23.2 github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.9.4 @@ -40,8 +40,8 @@ require ( buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 // indirect buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 // indirect buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect - buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251202164234-62b14f0b533c.2 // indirect - buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1 // indirect + buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260122161138-ab4e39a3c3bc.2 // indirect + buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260122161138-ab4e39a3c3bc.1 // indirect buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 // indirect buf.build/go/app v0.2.0 // indirect buf.build/go/bufplugin v0.9.0 // indirect @@ -65,7 +65,7 @@ require ( github.com/cli/browser v1.3.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cristalhq/acmd v0.12.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -157,8 +157,8 @@ require ( golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.41.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect google.golang.org/grpc v1.75.1 // indirect mvdan.cc/xurls/v2 v2.6.0 // indirect pluginrpc.com/pluginrpc v0.5.0 // indirect diff --git a/service/go.sum b/service/go.sum index 02e1f08..b938848 100644 --- a/service/go.sum +++ b/service/go.sum @@ -6,8 +6,12 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-202512091757 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251202164234-62b14f0b533c.2 h1:eQ6XRVUaYYZFOZvBsyrOYLWbw6464s5dVnHscxa0b8w= buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251202164234-62b14f0b533c.2/go.mod h1:omxVRch3jEPMINnUipLsuRWoEhND6LPXELKBG7xzyDw= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260122161138-ab4e39a3c3bc.2 h1:cMzWbIukJ5uk1M58CtqmBE7Ojacg/t2nAg4AbS78uX8= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260122161138-ab4e39a3c3bc.2/go.mod h1:GL3rFhQQsaI3PCBa0y5X71UHs6q5E/Xf9Q8WXBxE7a8= buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1 h1:PdfIJUbUVKdajMVYuMdvr2Wvo+wmzGnlPEYA4bhFaWI= buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260122161138-ab4e39a3c3bc.1 h1:yWmrELGX6l1GphG9kPVcrMQLjWfXGI5bLDxwE+SfbDw= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260122161138-ab4e39a3c3bc.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40= buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 h1:iGPvEJltOXUMANWf0zajcRcbiOXLD90ZwPUFvbcuv6Q= buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1/go.mod h1:nWVKKRA29zdt4uvkjka3i/y4mkrswyWwiu0TbdX0zts= buf.build/go/app v0.2.0 h1:NYaH13A+RzPb7M5vO8uZYZ2maBZI5+MS9A9tQm66fy8= @@ -89,6 +93,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8= github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q= +github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -203,6 +209,8 @@ github.com/knadh/koanf/providers/rawbytes v1.0.0 h1:MrKDh/HksJlKJmaZjgs4r8aVBb/z github.com/knadh/koanf/providers/rawbytes v1.0.0/go.mod h1:KxwYJf1uezTKy6PBtfE+m725NGp4GPVA7XoNTJ/PtLo= github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4= +github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -452,10 +460,14 @@ google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1: google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM= google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= +google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d h1:tUKoKfdZnSjTf5LW7xpG4c6SZ3Ozisn5eumcoTuMEN4= +google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= From 19641695f19cf20299775711c23b7ecf30af98d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 05:56:16 +0000 Subject: [PATCH 08/20] chore(deps): bump github.com/golang-jwt/jwt/v5 in /service Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.3.0 to 5.3.1. - [Release notes](https://github.com/golang-jwt/jwt/releases) - [Commits](https://github.com/golang-jwt/jwt/compare/v5.3.0...v5.3.1) --- updated-dependencies: - dependency-name: github.com/golang-jwt/jwt/v5 dependency-version: 5.3.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- service/go.mod | 2 +- service/go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/service/go.mod b/service/go.mod index bc2a3db..b3ba5fb 100644 --- a/service/go.mod +++ b/service/go.mod @@ -16,7 +16,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 github.com/fzipp/gocyclo v0.6.0 github.com/go-critic/go-critic v0.14.3 - github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac github.com/knadh/koanf/parsers/yaml v1.1.0 diff --git a/service/go.sum b/service/go.sum index b938848..f026f26 100644 --- a/service/go.sum +++ b/service/go.sum @@ -169,6 +169,8 @@ github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= From 052aab7f73d472189d0e4203b0416ce65f888744 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 05:56:16 +0000 Subject: [PATCH 09/20] chore(deps): bump vue-router from 4.6.4 to 5.0.1 in /frontend Bumps [vue-router](https://github.com/vuejs/router) from 4.6.4 to 5.0.1. - [Release notes](https://github.com/vuejs/router/releases) - [Commits](https://github.com/vuejs/router/compare/v4.6.4...v5.0.1) --- updated-dependencies: - dependency-name: vue-router dependency-version: 5.0.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --- frontend/package-lock.json | 336 +++++++++++++++++++++++++++++++++++-- frontend/package.json | 2 +- 2 files changed, 325 insertions(+), 13 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fc258ce..40612b6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,7 +23,7 @@ "vite": "^7.3.1", "vue": "^3.5.27", "vue-i18n": "^11.2.8", - "vue-router": "^4.6.4" + "vue-router": "^5.0.1" }, "devDependencies": { "process": "^0.11.10", @@ -40,6 +40,22 @@ "@babel/highlight": "^7.10.4" } }, + "node_modules/@babel/generator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -145,12 +161,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -160,9 +176,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1370,6 +1386,33 @@ "vue": "^3.2.25" } }, + "node_modules/@vue-macros/common": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz", + "integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==", + "license": "MIT", + "dependencies": { + "@vue/compiler-sfc": "^3.5.22", + "ast-kit": "^2.1.2", + "local-pkg": "^1.1.2", + "magic-string-ast": "^1.0.2", + "unplugin-utils": "^0.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/vue-macros" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.27", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", @@ -1426,6 +1469,30 @@ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "license": "MIT" }, + "node_modules/@vue/devtools-kit": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz", + "integrity": "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.0.5", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^2.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz", + "integrity": "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, "node_modules/@vue/reactivity": { "version": "3.5.27", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", @@ -1708,6 +1775,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz", + "integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.4", + "ast-kit": "^2.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -1747,6 +1846,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1935,6 +2043,21 @@ "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", "license": "MIT" }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -3470,6 +3593,12 @@ "node": ">= 0.4" } }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, "node_modules/hookified": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.13.0.tgz", @@ -3981,6 +4110,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -4026,6 +4167,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4232,6 +4385,21 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magic-string-ast": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz", + "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.19" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4317,6 +4485,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -4352,6 +4526,12 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -4661,6 +4841,12 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4746,6 +4932,21 @@ } } }, + "node_modules/picocrank/node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -5141,6 +5342,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -5270,6 +5477,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5470,6 +5683,15 @@ "node": ">=0.10.0" } }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/standard": { "version": "17.1.2", "resolved": "https://registry.npmjs.org/standard/-/standard-17.1.2.tgz", @@ -5888,6 +6110,18 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6478,18 +6712,81 @@ } }, "node_modules/vue-router": { - "version": "4.6.4", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", - "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.1.tgz", + "integrity": "sha512-t+lFugGXMdaq8lbn+vXG4j2H9UlsP205Tszz1wcDk9FyxqItBzcdJQ06IhpkQ2mHOfiTOHZeBshkskzPzHJkCw==", "license": "MIT", "dependencies": { - "@vue/devtools-api": "^6.6.4" + "@babel/generator": "^7.28.6", + "@vue-macros/common": "^3.1.1", + "@vue/devtools-api": "^8.0.0", + "ast-walker-scope": "^0.8.3", + "chokidar": "^5.0.0", + "json5": "^2.2.3", + "local-pkg": "^1.1.2", + "magic-string": "^0.30.21", + "mlly": "^1.8.0", + "muggle-string": "^0.4.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "scule": "^1.3.0", + "tinyglobby": "^0.2.15", + "unplugin": "^2.3.11", + "unplugin-utils": "^0.3.1", + "yaml": "^2.8.2" }, "funding": { "url": "https://github.com/sponsors/posva" }, "peerDependencies": { + "@pinia/colada": "^0.18.1", + "@vue/compiler-sfc": "^3.5.17", + "pinia": "^3.0.4", "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "pinia": { + "optional": true + } + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.5.tgz", + "integrity": "sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.0.5" + } + }, + "node_modules/vue-router/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/vue-router/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/webpack-virtual-modules": { @@ -6636,6 +6933,21 @@ "node": ">=8" } }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 715440c..d0cb079 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,6 @@ "vite": "^7.3.1", "vue": "^3.5.27", "vue-i18n": "^11.2.8", - "vue-router": "^4.6.4" + "vue-router": "^5.0.1" } } From b777d599aa1869871d67eb43cf64908d513976ee Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Sat, 7 Feb 2026 00:09:46 +0000 Subject: [PATCH 10/20] fix: Massive cleanup of template parsing --- service/internal/api/apiActions.go | 16 +-- service/internal/api/dashboard_entities.go | 10 +- service/internal/api/dashboards.go | 2 +- service/internal/executor/arguments.go | 61 +++------ service/internal/executor/arguments_test.go | 140 +++++++++++++------- service/internal/executor/executor.go | 6 +- service/internal/executor/executor_test.go | 28 ++-- service/internal/tpl/templates.go | 107 ++++++++++----- 8 files changed, 218 insertions(+), 152 deletions(-) diff --git a/service/internal/api/apiActions.go b/service/internal/api/apiActions.go index 3ca12de..23f8f40 100644 --- a/service/internal/api/apiActions.go +++ b/service/internal/api/apiActions.go @@ -67,7 +67,7 @@ func evaluateEnabledExpression(action *config.Action, entity *entities.Entity) b return true } - result := tpl.ParseTemplateWith(action.EnabledExpression, entity) + result := tpl.ParseTemplateOfActionBeforeExec(action.EnabledExpression, entity) result = strings.TrimSpace(result) if result == "" { @@ -106,11 +106,11 @@ func evaluateResultValue(result string) bool { return false } -func getDefaultValue(cfgArg config.ActionArgument, entity *entities.Entity) string { +func getDefaultArgumentValue(cfgArg config.ActionArgument, entity *entities.Entity) string { defaultValue := cfgArg.Default if defaultValue != "" { - defaultValue = tpl.ParseTemplateWith(defaultValue, entity) + defaultValue = tpl.ParseTemplateOfActionBeforeExec(defaultValue, entity) } return defaultValue @@ -131,8 +131,8 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque btn := apiv1.Action{ BindingId: actionBinding.ID, - Title: tpl.ParseTemplateWith(action.Title, actionBinding.Entity), - Icon: tpl.ParseTemplateWith(action.Icon, actionBinding.Entity), + Title: tpl.ParseTemplateOfActionBeforeExec(action.Title, actionBinding.Entity), + Icon: tpl.ParseTemplateOfActionBeforeExec(action.Icon, actionBinding.Entity), CanExec: aclCanExec && enabledExprCanExec, PopupOnStart: action.PopupOnStart, Order: int32(actionBinding.ConfigOrder), @@ -146,7 +146,7 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque Title: cfgArg.Title, Type: cfgArg.Type, Description: cfgArg.Description, - DefaultValue: getDefaultValue(cfgArg, actionBinding.Entity), + DefaultValue: getDefaultArgumentValue(cfgArg, actionBinding.Entity), Choices: buildChoices(cfgArg), Suggestions: cfgArg.Suggestions, SuggestionsBrowserKey: cfgArg.SuggestionsBrowserKey, @@ -173,8 +173,8 @@ func buildChoicesEntity(firstChoice config.ActionArgumentChoice, entityTitle str for _, ent := range entList { ret = append(ret, &apiv1.ActionArgumentChoice{ - Value: tpl.ParseTemplateWith(firstChoice.Value, ent), - Title: tpl.ParseTemplateWith(firstChoice.Title, ent), + Value: tpl.ParseTemplateOfActionBeforeExec(firstChoice.Value, ent), + Title: tpl.ParseTemplateOfActionBeforeExec(firstChoice.Title, ent), }) } diff --git a/service/internal/api/dashboard_entities.go b/service/internal/api/dashboard_entities.go index 8ae21a0..7bec464 100644 --- a/service/internal/api/dashboard_entities.go +++ b/service/internal/api/dashboard_entities.go @@ -26,10 +26,10 @@ func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent, rr func buildEntityFieldset(component *config.DashboardComponent, ent *entities.Entity, rr *DashboardRenderRequest) *apiv1.DashboardComponent { return &apiv1.DashboardComponent{ - Title: tpl.ParseTemplateWith(component.Title, ent), + Title: tpl.ParseTemplateOfActionBeforeExec(component.Title, ent), Type: "fieldset", Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(component.Contents, ent, component.Entity, rr)), - CssClass: tpl.ParseTemplateWith(component.CssClass, ent), + CssClass: tpl.ParseTemplateOfActionBeforeExec(component.CssClass, ent), Action: rr.findAction(component.Title), EntityType: component.Entity, EntityKey: ent.UniqueKey, @@ -69,7 +69,7 @@ func buildEntityFieldsetContents(contents []*config.DashboardComponent, ent *ent func cloneItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, rr *DashboardRenderRequest) *apiv1.DashboardComponent { clone := &apiv1.DashboardComponent{} - clone.CssClass = tpl.ParseTemplateWith(subitem.CssClass, ent) + clone.CssClass = tpl.ParseTemplateOfActionBeforeExec(subitem.CssClass, ent) if isLinkType(subitem.Type) { return cloneLinkItem(subitem, ent, clone, rr) @@ -84,7 +84,7 @@ func isLinkType(itemType string) bool { func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent { clone.Type = "link" - clone.Title = tpl.ParseTemplateWith(subitem.Title, ent) + clone.Title = tpl.ParseTemplateOfActionBeforeExec(subitem.Title, ent) // Prefer an entity-specific action when available, but fall back to a // non-entity-scoped action with the same title. This allows inline actions // defined inside entity dashboards to work without requiring an explicit @@ -99,7 +99,7 @@ func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clo } func cloneNonLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent { - clone.Title = tpl.ParseTemplateWith(subitem.Title, ent) + clone.Title = tpl.ParseTemplateOfActionBeforeExec(subitem.Title, ent) clone.Type = subitem.Type if isDirectoryWithEntity(clone.Type, ent, entityType) { diff --git a/service/internal/api/dashboards.go b/service/internal/api/dashboards.go index 08ef241..11c6cf3 100644 --- a/service/internal/api/dashboards.go +++ b/service/internal/api/dashboards.go @@ -237,7 +237,7 @@ func buildDashboardComponentSimpleWithEntity(subitem *config.DashboardComponent, title := subitem.Title if entity != nil { - title = tpl.ParseTemplateWith(subitem.Title, entity) + title = tpl.ParseTemplateOfActionBeforeExec(subitem.Title, entity) } newitem := &apiv1.DashboardComponent{ diff --git a/service/internal/executor/arguments.go b/service/internal/executor/arguments.go index 89d984e..a63eb0b 100644 --- a/service/internal/executor/arguments.go +++ b/service/internal/executor/arguments.go @@ -25,29 +25,12 @@ var ( } ) -func parseCommandForReplacements(shellCommand string, values map[string]string, entity any) (string, error) { - r := regexp.MustCompile(`{{ *?([a-zA-Z0-9_]+?) *?}}`) - foundArgumentNames := r.FindAllStringSubmatch(shellCommand, -1) - - for _, match := range foundArgumentNames { - argName := match[1] - argValue, argProvided := values[argName] - - if !argProvided { - return "", fmt.Errorf("required arg not provided: %v", argName) - } - - shellCommand = strings.ReplaceAll(shellCommand, match[0], argValue) - } - - return shellCommand, nil -} - // parseExecArray parses all exec arguments in the action. func parseExecArray(action *config.Action, values map[string]string, entity *entities.Entity) ([]string, error) { parsed := make([]string, len(action.Exec)) - for i, a := range action.Exec { - out, err := parseSingleExec(a, values, entity) + + for i, segment := range action.Exec { + out, err := parseExecSegment(segment, values, entity) if err != nil { return nil, err } @@ -63,20 +46,19 @@ func parseActionExec(values map[string]string, action *config.Action, entity *en if err := validateArguments(values, action); err != nil { return nil, err } + parsed, err := parseExecArray(action, values, entity) + if err != nil { return nil, err } + logParsedExec(action, parsed, values) return parsed, nil } -func parseSingleExec(a string, values map[string]string, entity *entities.Entity) (string, error) { - arg, err := parseCommandForReplacements(a, values, entity) - if err != nil { - return "", err - } - return tpl.ParseTemplateWithArgs(arg, entity, values), nil +func parseExecSegment(arg string, values map[string]string, entity *entities.Entity) (string, error) { + return tpl.ParseTemplateWithActionContext(arg, entity, values) } func validateArguments(values map[string]string, action *config.Action) error { @@ -94,19 +76,17 @@ func logParsedExec(action *config.Action, parsed []string, values map[string]str log.WithFields(log.Fields{"actionTitle": action.Title, "cmd": redacted}).Infof("Action parse args - After (Exec)") } -func parseActionArguments(values map[string]string, action *config.Action, entity *entities.Entity) (string, error) { +func parseActionArguments(req *ExecutionRequest) (string, error) { log.WithFields(log.Fields{ - "actionTitle": action.Title, - "cmd": action.Shell, + "actionTitle": req.Binding.Action.Title, + "cmd": req.Binding.Action.Shell, }).Infof("Action parse args - Before") - rawShellCommand, err := parseCommandForReplacements(action.Shell, values, entity) - - for _, arg := range action.Arguments { + for _, arg := range req.Binding.Action.Arguments { argName := arg.Name - argValue := values[argName] + argValue := req.Arguments[argName] - err := typecheckActionArgument(&arg, argValue, action) + err := typecheckActionArgument(&arg, argValue, req.Binding.Action) if err != nil { return "", err @@ -118,15 +98,16 @@ func parseActionArguments(values map[string]string, action *config.Action, entit }).Debugf("Arg assigned") } - parsedShellCommand := tpl.ParseTemplateWithArgs(rawShellCommand, entity, values) - redactedShellCommand := redactShellCommand(parsedShellCommand, action.Arguments, values) + parsedShellCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.Shell, req.Binding.Entity, req.Arguments) if err != nil { return "", err } + redactedShellCommand := redactShellCommand(parsedShellCommand, req.Binding.Action.Arguments, req.Arguments) + log.WithFields(log.Fields{ - "actionTitle": action.Title, + "actionTitle": req.Binding.Action.Title, "cmd": redactedShellCommand, }).Infof("Action parse args - After") @@ -173,7 +154,7 @@ func typecheckActionArgument(arg *config.ActionArgument, value string, action *c return fmt.Errorf("argument name cannot be empty") } - return typecheckActionArgumentFound(value, action, arg) + return typecheckActionArgumentFound(value, arg) } // ValidateArgument validates a single argument value using the same logic as the executor. @@ -195,7 +176,7 @@ func ValidateArgument(arg *config.ActionArgument, value string, action *config.A return typecheckActionArgument(arg, mangledValue, action) } -func typecheckActionArgumentFound(value string, action *config.Action, arg *config.ActionArgument) error { +func typecheckActionArgumentFound(value string, arg *config.ActionArgument) error { if value == "" { return typecheckNull(arg) } @@ -257,7 +238,7 @@ func typecheckChoiceEntity(value string, arg *config.ActionArgument) error { templateChoice := arg.Choices[0].Value for _, ent := range entities.GetEntityInstances(arg.Entity) { - choice := tpl.ParseTemplateWith(templateChoice, ent) + choice := tpl.ParseTemplateOfActionBeforeExec(templateChoice, ent) if value == choice { return nil diff --git a/service/internal/executor/arguments_test.go b/service/internal/executor/arguments_test.go index 3e8242b..877b10c 100644 --- a/service/internal/executor/arguments_test.go +++ b/service/internal/executor/arguments_test.go @@ -6,6 +6,7 @@ import ( config "github.com/OliveTin/OliveTin/internal/config" "github.com/OliveTin/OliveTin/internal/entities" + "github.com/OliveTin/OliveTin/internal/tpl" log "github.com/sirupsen/logrus" "testing" @@ -114,36 +115,47 @@ func TestValidateArgumentCheckboxWithChoices(t *testing.T) { assert.NotNil(t, err, "Expected unknown checkbox title to be rejected against choices") } +func newExecRequest() *ExecutionRequest { + return &ExecutionRequest{ + Arguments: make(map[string]string), + Binding: &ActionBinding{ + Action: &config.Action{}, + }, + } +} + func TestArgumentValueNullable(t *testing.T) { - a1 := config.Action{ + req := newExecRequest() + req.Binding.Action = &config.Action{ Title: "Release the hounds", Shell: "echo 'Releasing {{ count }} hounds'", Arguments: []config.ActionArgument{ { - Name: "count", - Type: "int", + Name: "count", + Type: "int", + RejectNull: false, }, }, } - - values := map[string]string{ + req.Arguments = map[string]string{ "count": "", } - out, err := parseActionArguments(values, &a1, nil) + out, err := parseActionArguments(req) assert.Equal(t, "echo 'Releasing hounds'", out) assert.Nil(t, err) - a1.Arguments[0].RejectNull = true + req.Binding.Action.Arguments[0].RejectNull = true - _, err = parseActionArguments(values, &a1, nil) + _, err = parseActionArguments(req) assert.NotNil(t, err) } func TestArgumentNameNumbers(t *testing.T) { - a1 := config.Action{ + req := newExecRequest() + req.Binding.Action = &config.Action{ Title: "Do some tickles", Shell: "echo 'Tickling {{ person1name }}'", Arguments: []config.ActionArgument{ @@ -154,18 +166,19 @@ func TestArgumentNameNumbers(t *testing.T) { }, } - values := map[string]string{ + req.Arguments = map[string]string{ "person1name": "Fred", } - out, err := parseActionArguments(values, &a1, nil) + out, err := parseActionArguments(req) assert.Equal(t, "echo 'Tickling Fred'", out) assert.Nil(t, err) } func TestArgumentNotProvided(t *testing.T) { - a1 := config.Action{ + req := newExecRequest() + req.Binding.Action = &config.Action{ Title: "Do some tickles", Shell: "echo 'Tickling {{ personName }}'", Arguments: []config.ActionArgument{ @@ -176,24 +189,25 @@ func TestArgumentNotProvided(t *testing.T) { }, } - values := map[string]string{} + req.Arguments = map[string]string{} - out, err := parseActionArguments(values, &a1, nil) + out, err := parseActionArguments(req) assert.Equal(t, "", out) assert.Equal(t, err.Error(), "required arg not provided: personName") } func TestExecArrayParsing(t *testing.T) { - a1 := config.Action{ + req := newExecRequest() + req.Binding.Action = &config.Action{ Title: "List files", Exec: []string{"ls", "-alh"}, Arguments: []config.ActionArgument{}, } - values := map[string]string{} + req.Arguments = map[string]string{} - out, err := parseActionExec(values, &a1, nil) + out, err := parseActionExec(req.Arguments, req.Binding.Action, req.Binding.Entity) assert.Nil(t, err) assert.Equal(t, []string{"ls", "-alh"}, out) @@ -636,7 +650,7 @@ func TestParseCommandForReplacements(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - output, err := parseCommandForReplacements(tt.shellCommand, tt.values, nil) + output, err := tpl.ParseTemplateWithActionContext(tt.shellCommand, nil, tt.values) if tt.expectError { assert.NotNil(t, err, "Expected error but got none") @@ -654,56 +668,87 @@ func TestParseCommandForReplacements(t *testing.T) { func TestArgumentChoicesValidation(t *testing.T) { tests := []struct { name string - action config.Action - values map[string]string + req *ExecutionRequest expectError bool description string }{ { name: "Valid choice", - action: config.Action{ - Title: "Test choices", - Shell: "echo {{ option }}", - Arguments: []config.ActionArgument{ - { - Name: "option", - Type: "ascii", - Choices: []config.ActionArgumentChoice{ - {Value: "option1", Title: "Option 1"}, - {Value: "option2", Title: "Option 2"}, + req: &ExecutionRequest{ + Binding: &ActionBinding{ + Action: &config.Action{ + Title: "Test choices", + Shell: "echo {{ option }}", + Arguments: []config.ActionArgument{ + { + Name: "option", + Type: "ascii", + Choices: []config.ActionArgumentChoice{ + {Value: "option1", Title: "Option 1"}, + {Value: "option2", Title: "Option 2"}, + }, + }, }, }, }, + Arguments: map[string]string{"option": "option1"}, }, - values: map[string]string{"option": "option1"}, expectError: false, description: "Should accept valid choice", }, { name: "Invalid choice", - action: config.Action{ - Title: "Test choices", - Shell: "echo {{ option }}", - Arguments: []config.ActionArgument{ - { - Name: "option", - Type: "ascii", - Choices: []config.ActionArgumentChoice{ - {Value: "option1", Title: "Option 1"}, - {Value: "option2", Title: "Option 2"}, + req: &ExecutionRequest{ + Binding: &ActionBinding{ + Action: &config.Action{ + Title: "Test choices", + Shell: "echo {{ option }}", + Arguments: []config.ActionArgument{ + { + Name: "option", + Type: "ascii", + Choices: []config.ActionArgumentChoice{ + {Value: "option1", Title: "Option 1"}, + {Value: "option2", Title: "Option 2"}, + }, + }, }, }, }, + Arguments: map[string]string{"option": "invalid_option"}, }, - values: map[string]string{"option": "invalid_option"}, expectError: true, description: "Should reject invalid choice", }, + { + name: "Invalid choice", + req: &ExecutionRequest{ + Binding: &ActionBinding{ + Action: &config.Action{ + Title: "Test choices", + Shell: "echo {{ option }}", + Arguments: []config.ActionArgument{ + { + Name: "option", + Type: "ascii", + Choices: []config.ActionArgumentChoice{ + {Value: "option1", Title: "Option 1"}, + {Value: "option2", Title: "Option 2"}, + }, + }, + }, + }, + }, + Arguments: map[string]string{"option": "option1"}, + }, + expectError: false, + description: "Should accept valid choice", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := parseActionArguments(tt.values, &tt.action, nil) + _, err := parseActionArguments(tt.req) if tt.expectError { assert.NotNil(t, err, tt.description) @@ -737,7 +782,8 @@ func TestTypeSafetyCheckVeryDangerousRawString(t *testing.T) { } func TestParseActionArgumentsWithEntityPrefix(t *testing.T) { - action := config.Action{ + req := newExecRequest() + req.Binding.Action = &config.Action{ Title: "Test entity prefix", Shell: "echo 'Processing {{ name }} for entity'", Arguments: []config.ActionArgument{ @@ -745,16 +791,16 @@ func TestParseActionArgumentsWithEntityPrefix(t *testing.T) { }, } - values := map[string]string{ + req.Arguments = map[string]string{ "name": "testuser", } - ent := &entities.Entity{ + req.Binding.Entity = &entities.Entity{ Title: "entity_123", } // Test with entity prefix - output, err := parseActionArguments(values, &action, ent) + output, err := parseActionArguments(req) assert.Nil(t, err) assert.Contains(t, output, "testuser") } diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 4214201..274d4b1 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -690,7 +690,7 @@ func handleShellBranch(req *ExecutionRequest) bool { return fail(req, err) } - cmd, err := parseActionArguments(req.Arguments, req.Binding.Action, req.Binding.Entity) + cmd, err := parseActionArguments(req) if err != nil { return fail(req, err) @@ -738,7 +738,7 @@ func stepRequestAction(req *ExecutionRequest) bool { req.logEntry.Binding = req.Binding req.logEntry.ActionConfigTitle = req.Binding.Action.Title - req.logEntry.ActionTitle = tpl.ParseTemplateWith(req.Binding.Action.Title, req.Binding.Entity) + req.logEntry.ActionTitle = tpl.ParseTemplateOfActionBeforeExec(req.Binding.Action.Title, req.Binding.Entity) req.logEntry.ActionIcon = req.Binding.Action.Icon req.logEntry.Tags = req.Tags @@ -903,7 +903,7 @@ func stepExecAfter(req *ExecutionRequest) bool { "ot_username": req.AuthenticatedUser.Username, } - finalParsedCommand, err := parseCommandForReplacements(req.Binding.Action.ShellAfterCompleted, args, req.Binding.Entity) + finalParsedCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.ShellAfterCompleted, req.Binding.Entity, args) if err != nil { msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n" diff --git a/service/internal/executor/executor_test.go b/service/internal/executor/executor_test.go index 89559bb..f9efefe 100644 --- a/service/internal/executor/executor_test.go +++ b/service/internal/executor/executor_test.go @@ -74,7 +74,8 @@ func TestExecNonExistant(t *testing.T) { } func TestArgumentNameCamelCase(t *testing.T) { - a1 := &config.Action{ + req := newExecRequest() + req.Binding.Action = &config.Action{ Title: "Do some tickles", Shell: "echo 'Tickling {{ personName }}'", Arguments: []config.ActionArgument{ @@ -85,18 +86,19 @@ func TestArgumentNameCamelCase(t *testing.T) { }, } - values := map[string]string{ + req.Arguments = map[string]string{ "personName": "Fred", } - out, err := parseActionArguments(values, a1, nil) + out, err := parseActionArguments(req) assert.Equal(t, "echo 'Tickling Fred'", out) assert.Nil(t, err) } func TestArgumentNameSnakeCase(t *testing.T) { - a1 := &config.Action{ + req := newExecRequest() + req.Binding.Action = &config.Action{ Title: "Do some tickles", Shell: "echo 'Tickling {{ person_name }}'", Arguments: []config.ActionArgument{ @@ -107,11 +109,11 @@ func TestArgumentNameSnakeCase(t *testing.T) { }, } - values := map[string]string{ + req.Arguments = map[string]string{ "person_name": "Fred", } - out, err := parseActionArguments(values, a1, nil) + out, err := parseActionArguments(req) assert.Equal(t, "echo 'Tickling Fred'", out) assert.Nil(t, err) @@ -205,7 +207,8 @@ func TestGetPagingIndexes(t *testing.T) { } func TestUnsetRequiredArgument(t *testing.T) { - a1 := &config.Action{ + req := newExecRequest() + req.Binding.Action = &config.Action{ Title: "Print your name", Shell: "echo 'Your name is: {{ name }}'", Arguments: []config.ActionArgument{ @@ -216,16 +219,17 @@ func TestUnsetRequiredArgument(t *testing.T) { }, } - values := map[string]string{} + req.Arguments = map[string]string{} - out, err := parseActionArguments(values, a1, nil) + out, err := parseActionArguments(req) assert.Equal(t, "", out) assert.NotNil(t, err) } func TestUnusedArgumentStillPassesTypeSafetyCheck(t *testing.T) { - a1 := &config.Action{ + req := newExecRequest() + req.Binding.Action = &config.Action{ Title: "Print your name", Shell: "echo 'Your name is: {{ name }}'", Arguments: []config.ActionArgument{ @@ -240,12 +244,12 @@ func TestUnusedArgumentStillPassesTypeSafetyCheck(t *testing.T) { }, } - values := map[string]string{ + req.Arguments = map[string]string{ "name": "Fred", "age": "Not an integer", } - out, err := parseActionArguments(values, a1, nil) + out, err := parseActionArguments(req) assert.Equal(t, "", out) assert.NotNil(t, err) diff --git a/service/internal/tpl/templates.go b/service/internal/tpl/templates.go index a0aea20..26f9925 100644 --- a/service/internal/tpl/templates.go +++ b/service/internal/tpl/templates.go @@ -19,8 +19,8 @@ type olivetinInfo struct { Runtime *installationinfo.RuntimeInfo } -var legacyArgumentRegex = regexp.MustCompile(`{{ ([a-zA-Z0-9_]+) }}`) -var legacyEntityPropertiesRegex = regexp.MustCompile(`{{ ([a-zA-Z0-9_]+)\.([a-zA-Z0-9_\.]+) }}`) +var legacyArgumentRegex = regexp.MustCompile(`{{\s*([a-zA-Z0-9_]+)\s*}}`) +var legacyEntityPropertiesRegex = regexp.MustCompile(`{{\s*([a-zA-Z0-9_]+)\.([a-zA-Z0-9_\.]+)\s*}}`) type generalTemplateContext struct { OliveTin olivetinInfo @@ -106,40 +106,33 @@ func migrateLegacyEntityProperties(rawShellCommand string) string { } func migrateLegacyArgumentNames(rawShellCommand string) string { - foundArgumentNames := legacyArgumentRegex.FindAllStringSubmatch(rawShellCommand, -1) + matches := legacyArgumentRegex.FindAllStringSubmatchIndex(rawShellCommand, -1) - for _, match := range foundArgumentNames { - argName := match[1] + for i := len(matches) - 1; i >= 0; i-- { + match := matches[i] + fullMatchStart := match[0] + fullMatchEnd := match[1] + argNameStart := match[2] + argNameEnd := match[3] - if !strings.HasPrefix(argName, ".Arguments.") { - log.WithFields(log.Fields{ - "old": argName, - "new": ".Arguments." + argName, - }).Debugf("Legacy variable name found, changing to Argument") + argName := rawShellCommand[argNameStart:argNameEnd] - rawShellCommand = strings.ReplaceAll(rawShellCommand, argName, ".Arguments."+argName) - } + log.WithFields(log.Fields{ + "old": argName, + "new": ".Arguments." + argName, + }).Debugf("Legacy variable name found, changing to Argument") + + replacement := "{{ .Arguments." + argName + " }}" + rawShellCommand = rawShellCommand[:fullMatchStart] + replacement + rawShellCommand[fullMatchEnd:] } return rawShellCommand } -func ParseTemplateWithArgs(source string, ent *entities.Entity, args map[string]string) string { +func ParseTemplateWithActionContext(source string, ent *entities.Entity, args map[string]string) (string, error) { source = migrateLegacyArgumentNames(source) source = migrateLegacyEntityProperties(source) - ret := "" - - t, err := tpl.Parse(source) - - if err != nil { - log.WithFields(log.Fields{ - "source": source, - "err": err, - }).Error("Error parsing template") - return fmt.Sprintf("tpl parse error: %v", err.Error()) - } - var entdata any if ent != nil { @@ -154,31 +147,73 @@ func ParseTemplateWithArgs(source string, ent *entities.Entity, args map[string] CurrentEntity: entdata, } + result, err := parseTemplate(source, templateVariables) + + if isMissingArgumentError, argName := checkMissingArgumentError(err); isMissingArgumentError { + return "", fmt.Errorf("required arg not provided: %s", argName) + } + + if err != nil { + return "", err + } + + return result, nil +} + +func checkMissingArgumentError(err error) (bool, string) { + if err == nil { + return false, "" + } + + if strings.Contains(err.Error(), "map has no entry for key") { + re := regexp.MustCompile(`\.Arguments\.(\w+)`) + match := re.FindStringSubmatch(err.Error()) + if len(match) > 1 { + return true, match[1] + } + } + + return false, "" +} + +func parseTemplate(source string, data any) (string, error) { + t, err := tpl.Parse(source) + + if err != nil { + return "", err + } + + t = t.Option("missingkey=error") + var sb strings.Builder - err = t.Execute(&sb, &templateVariables) + err = t.Execute(&sb, data) if err != nil { log.WithFields(log.Fields{ - "source": source, - "err": err, - "currentEntity": ent, + "source": source, + "err": err, }).Errorf("Error executing template") - ret = fmt.Sprintf("tpl exec error: %v", err.Error()) + + return "", err } else { - ret = sb.String() + return sb.String(), nil } - - return ret } -func ParseTemplateWith(source string, ent *entities.Entity) string { - return ParseTemplateWithArgs(source, ent, nil) +func ParseTemplateOfActionBeforeExec(source string, ent *entities.Entity) string { + result, err := ParseTemplateWithActionContext(source, ent, nil) + if err != nil { + return "" + } + return result } +/* func ParseTemplateBoolWith(source string, ent *entities.Entity) bool { source = strings.TrimSpace(source) - tplBool := ParseTemplateWith(source, ent) + tplBool := ParseTemplateOfActionBeforeExec(source, ent) return tplBool == "true" } +*/ From 086a5ffd22b41766f5cbe2654a53b2771fe004b7 Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Sat, 7 Feb 2026 00:31:24 +0000 Subject: [PATCH 11/20] chore: fix tpl coderabbit suggestions --- service/internal/tpl/templates.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/service/internal/tpl/templates.go b/service/internal/tpl/templates.go index 26f9925..bf0a242 100644 --- a/service/internal/tpl/templates.go +++ b/service/internal/tpl/templates.go @@ -12,7 +12,8 @@ import ( log "github.com/sirupsen/logrus" ) -var tpl = template.New("tpl") +var tpl = template.New("tpl"). + Option("missingkey=error") type olivetinInfo struct { Build *installationinfo.BuildInfo @@ -183,8 +184,6 @@ func parseTemplate(source string, data any) (string, error) { return "", err } - t = t.Option("missingkey=error") - var sb strings.Builder err = t.Execute(&sb, data) @@ -203,6 +202,10 @@ func parseTemplate(source string, data any) (string, error) { func ParseTemplateOfActionBeforeExec(source string, ent *entities.Entity) string { result, err := ParseTemplateWithActionContext(source, ent, nil) if err != nil { + log.WithFields(log.Fields{ + "source": source, + "err": err, + }).Errorf("Error parsing template of action before exec") return "" } return result From e2be4a03c1a4b15a7ce7f97a6bb75c4a69ac24fe Mon Sep 17 00:00:00 2001 From: Yohany Flores Suarez <yohanyflores@gmail.com> Date: Sun, 1 Feb 2026 10:23:51 -0500 Subject: [PATCH 12/20] Update theme style import fallback in App.vue --- frontend/resources/vue/App.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/resources/vue/App.vue b/frontend/resources/vue/App.vue index 063890d..8c61c1a 100644 --- a/frontend/resources/vue/App.vue +++ b/frontend/resources/vue/App.vue @@ -364,7 +364,7 @@ function applyTheme() { if (themePreference.value && themePreference.value !== '') { themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);` } else { - themeStyle.textContent = '' + themeStyle.textContent = '@import url('/theme.css') layer(theme);' } } From 8ffed8757eb5e02e0ada4b6df1fa2ae5cee3aa0c Mon Sep 17 00:00:00 2001 From: Yohany Flores Suarez <yohanyflores@gmail.com> Date: Sun, 1 Feb 2026 10:48:44 -0500 Subject: [PATCH 13/20] Syntax error: Mismatched quote characters will break the string literal. --- frontend/resources/vue/App.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/resources/vue/App.vue b/frontend/resources/vue/App.vue index 8c61c1a..3acbe21 100644 --- a/frontend/resources/vue/App.vue +++ b/frontend/resources/vue/App.vue @@ -364,7 +364,7 @@ function applyTheme() { if (themePreference.value && themePreference.value !== '') { themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);` } else { - themeStyle.textContent = '@import url('/theme.css') layer(theme);' + themeStyle.textContent = `@import url('/theme.css') layer(theme);` } } From 3afd7c26e53c50e125408e58103c843706fb367e Mon Sep 17 00:00:00 2001 From: jla <jla@angerops.com> Date: Sat, 7 Feb 2026 22:28:51 -0500 Subject: [PATCH 14/20] Fix: Remove JSON quotes from webhook JSONPath string extraction --- service/internal/webhooks/jsonpath.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/service/internal/webhooks/jsonpath.go b/service/internal/webhooks/jsonpath.go index 8e51b70..bfeac08 100644 --- a/service/internal/webhooks/jsonpath.go +++ b/service/internal/webhooks/jsonpath.go @@ -46,7 +46,12 @@ func (m *JSONMatcher) ExtractValue(pathExpr string) (string, error) { return "", err } - // Marshal to JSON for consistent string representation + // For string values, return directly without marshaling to avoid adding JSON quotes + if strValue, ok := value.(string); ok { + return strValue, nil + } + + // For non-string values, marshal to JSON for consistent string representation jsonBytes, err := json.Marshal(value) if err != nil { return "", fmt.Errorf("failed to marshal extracted value: %w", err) From c1062fb4fa2675b5b25c2e616bbefcc992280869 Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Fri, 13 Feb 2026 14:44:14 +0000 Subject: [PATCH 15/20] chore: Picocrank upgrade for vue router 5 --- frontend/package-lock.json | 422 ++++++++---------- frontend/package.json | 10 +- .../resources/vue/components/Breadcrumbs.vue | 1 + 3 files changed, 196 insertions(+), 237 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 40612b6..51cfad8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,21 +13,21 @@ "@connectrpc/connect-web": "^2.1.1", "@hugeicons/core-free-icons": "^3.1.1", "@hugeicons/vue": "^1.0.4", - "@vitejs/plugin-vue": "^6.0.3", + "@vitejs/plugin-vue": "^6.0.4", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.13.1", + "picocrank": "^1.14.0", "standard": "^17.1.2", "unplugin-vue-components": "^31.0.0", "vite": "^7.3.1", - "vue": "^3.5.27", + "vue": "^3.5.28", "vue-i18n": "^11.2.8", - "vue-router": "^5.0.1" + "vue-router": "^5.0.2" }, "devDependencies": { "process": "^0.11.10", - "stylelint": "^17.0.0", + "stylelint": "^17.3.0", "stylelint-config-standard": "^40.0.0" } }, @@ -196,39 +196,39 @@ "peer": true }, "node_modules/@cacheable/memory": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.5.tgz", - "integrity": "sha512-fkiAxCvssEyJZ5fxX4tcdZFRmW9JehSTGvvqmXn6rTzG5cH6V/3C4ad8yb01vOjp2xBydHkHrgpW0qeGtzt6VQ==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.7.tgz", + "integrity": "sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==", "dev": true, "license": "MIT", "dependencies": { - "@cacheable/utils": "^2.3.0", - "@keyv/bigmap": "^1.1.0", - "hookified": "^1.12.2", - "keyv": "^5.5.4" + "@cacheable/utils": "^2.3.3", + "@keyv/bigmap": "^1.3.0", + "hookified": "^1.14.0", + "keyv": "^5.5.5" } }, "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.0.tgz", - "integrity": "sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", "dev": true, "license": "MIT", "dependencies": { - "hashery": "^1.2.0", - "hookified": "^1.13.0" + "hashery": "^1.4.0", + "hookified": "^1.15.0" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "keyv": "^5.5.4" + "keyv": "^5.6.0" } }, "node_modules/@cacheable/memory/node_modules/keyv": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.4.tgz", - "integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", "dependencies": { @@ -236,20 +236,20 @@ } }, "node_modules/@cacheable/utils": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.1.tgz", - "integrity": "sha512-38NJXjIr4W1Sghun8ju+uYWD8h2c61B4dKwfnQHVDFpAJ9oS28RpfqZQJ6Dgd3RceGkILDY9YT+72HJR3LoeSQ==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.4.tgz", + "integrity": "sha512-knwKUJEYgIfwShABS1BX6JyJJTglAFcEU7EXqzTdiGCXur4voqkiJkdgZIQtWNFhynzDWERcTYv/sETMu3uJWA==", "dev": true, "license": "MIT", "dependencies": { - "hashery": "^1.2.0", - "keyv": "^5.5.4" + "hashery": "^1.3.0", + "keyv": "^5.6.0" } }, "node_modules/@cacheable/utils/node_modules/keyv": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.4.tgz", - "integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", "dependencies": { @@ -275,6 +275,30 @@ "@connectrpc/connect": "2.1.1" } }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, "node_modules/@csstools/css-parser-algorithms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", @@ -299,9 +323,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.25.tgz", - "integrity": "sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q==", + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.27.tgz", + "integrity": "sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==", "dev": true, "funding": [ { @@ -313,10 +337,7 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } + "license": "MIT-0" }, "node_modules/@csstools/css-tokenizer": { "version": "4.0.0", @@ -1068,9 +1089,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -1371,12 +1392,12 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", - "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", + "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==", "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.53" + "@rolldown/pluginutils": "1.0.0-rc.2" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -1414,39 +1435,39 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", - "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz", + "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.27", - "entities": "^7.0.0", + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.28", + "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", - "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", + "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-core": "3.5.28", + "@vue/shared": "3.5.28" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", - "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", + "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/compiler-core": "3.5.27", - "@vue/compiler-dom": "3.5.27", - "@vue/compiler-ssr": "3.5.27", - "@vue/shared": "3.5.27", + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.28", + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", @@ -1454,13 +1475,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", - "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", + "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-dom": "3.5.28", + "@vue/shared": "3.5.28" } }, "node_modules/@vue/devtools-api": { @@ -1494,53 +1515,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", - "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz", + "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.27" + "@vue/shared": "3.5.28" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", - "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz", + "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/reactivity": "3.5.28", + "@vue/shared": "3.5.28" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", - "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", + "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.27", - "@vue/runtime-core": "3.5.27", - "@vue/shared": "3.5.27", + "@vue/reactivity": "3.5.28", + "@vue/runtime-core": "3.5.28", + "@vue/shared": "3.5.28", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", - "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz", + "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28" }, "peerDependencies": { - "vue": "3.5.27" + "vue": "3.5.28" } }, "node_modules/@vue/shared": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", - "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", + "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", "license": "MIT" }, "node_modules/@xterm/addon-fit": { @@ -1900,23 +1921,23 @@ } }, "node_modules/cacheable": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.2.0.tgz", - "integrity": "sha512-LEJxRqfeomiiRd2t0uON6hxAtgOoWDfY3fugebbz+J3vDLO+SkdfFChQcOHTZhj9SYa9iwE9MGYNX72dKiOE4w==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.2.tgz", + "integrity": "sha512-w+ZuRNmex9c1TR9RcsxbfTKCjSL0rh1WA5SABbrWprIHeNBdmyQLSYonlDy9gpD+63XT8DgZ/wNh1Smvc9WnJA==", "dev": true, "license": "MIT", "dependencies": { - "@cacheable/memory": "^2.0.5", - "@cacheable/utils": "^2.3.0", - "hookified": "^1.13.0", - "keyv": "^5.5.4", - "qified": "^0.5.2" + "@cacheable/memory": "^2.0.7", + "@cacheable/utils": "^2.3.3", + "hookified": "^1.15.0", + "keyv": "^5.5.5", + "qified": "^0.6.0" } }, "node_modules/cacheable/node_modules/keyv": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.4.tgz", - "integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", "dependencies": { @@ -2283,9 +2304,9 @@ "dev": true }, "node_modules/entities": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", - "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -3569,13 +3590,13 @@ } }, "node_modules/hashery": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.2.0.tgz", - "integrity": "sha512-43XJKpwle72Ik5Zpam7MuzRWyNdwwdf6XHlh8wCj2PggvWf+v/Dm5B0dxGZOmddidgeO6Ofu9As/o231Ti/9PA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.4.0.tgz", + "integrity": "sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==", "dev": true, "license": "MIT", "dependencies": { - "hookified": "^1.13.0" + "hookified": "^1.14.0" }, "engines": { "node": ">=20" @@ -3600,9 +3621,9 @@ "license": "MIT" }, "node_modules/hookified": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.13.0.tgz", - "integrity": "sha512-6sPYUY8olshgM/1LDNW4QZQN0IqgKhtl/1C8koNZBJrKLBk3AZl6chQtNwpNztvfiApHMEwMHek5rv993PRbWw==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", "dev": true, "license": "MIT" }, @@ -4854,97 +4875,19 @@ "license": "ISC" }, "node_modules/picocrank": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.13.1.tgz", - "integrity": "sha512-gzrDNpmnMPcmsD7glFrqlqCVGSiX0wJhLMPCTCs2wLe52pjFJc0duHqHCUXNQ81OEhFwkG7N2c+1yF6i6yEMlw==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.14.0.tgz", + "integrity": "sha512-ksjqPHFMFE6ENaIXjhund50wocFmaLy22jYgWlWikugHBdd/0YlHfOOuoIMn0wKV8bSrJhcM3pQug/qz45Bc4g==", "license": "ISC", "dependencies": { "@hugeicons/core-free-icons": "^3.1.1", "@hugeicons/vue": "^1.0.4", - "@vitejs/plugin-vue": "^6.0.3", + "@vitejs/plugin-vue": "^6.0.4", "femtocrank": "^2.5.0", - "unplugin-vue-components": "^30.0.0", + "unplugin-vue-components": "^31.0.0", "vite": "^7.3.1", - "vue": "^3.5.26", - "vue-router": "^4.6.4" - } - }, - "node_modules/picocrank/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/picocrank/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/picocrank/node_modules/unplugin-vue-components": { - "version": "30.0.0", - "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-30.0.0.tgz", - "integrity": "sha512-4qVE/lwCgmdPTp6h0qsRN2u642tt4boBQtcpn4wQcWZAsr8TQwq+SPT3NDu/6kBFxzo/sSEK4ioXhOOBrXc3iw==", - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.3", - "debug": "^4.4.3", - "local-pkg": "^1.1.2", - "magic-string": "^0.30.19", - "mlly": "^1.8.0", - "tinyglobby": "^0.2.15", - "unplugin": "^2.3.10", - "unplugin-utils": "^0.3.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@babel/parser": "^7.15.8", - "@nuxt/kit": "^3.2.2 || ^4.0.0", - "vue": "2 || 3" - }, - "peerDependenciesMeta": { - "@babel/parser": { - "optional": true - }, - "@nuxt/kit": { - "optional": true - } - } - }, - "node_modules/picocrank/node_modules/vue-router": { - "version": "4.6.4", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", - "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", - "license": "MIT", - "dependencies": { - "@vue/devtools-api": "^6.6.4" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "vue": "^3.5.0" + "vue": "^3.5.28", + "vue-router": "^5.0.2" } }, "node_modules/picomatch": { @@ -5176,13 +5119,13 @@ } }, "node_modules/qified": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/qified/-/qified-0.5.2.tgz", - "integrity": "sha512-7gJ6mxcQb9vUBOtbKm5mDevbe2uRcOEVp1g4gb/Q+oLntB3HY8eBhOYRxFI2mlDFlY1e4DOSCptzxarXRvzxCA==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.6.0.tgz", + "integrity": "sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==", "dev": true, "license": "MIT", "dependencies": { - "hookified": "^1.13.0" + "hookified": "^1.14.0" }, "engines": { "node": ">=20" @@ -5911,9 +5854,9 @@ } }, "node_modules/stylelint": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.0.0.tgz", - "integrity": "sha512-saMZ2mqdQre4AfouxcbTdpVglDRcROb4MIucKHvgsDb/0IX7ODhcaz+EOIyfxAsm8Zjl/7j4hJj6MgIYYM8Xwg==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.3.0.tgz", + "integrity": "sha512-1POV91lcEMhj6SLVaOeA0KlS9yattS+qq+cyWqP/nYzWco7K5jznpGH1ExngvPlTM9QF1Kjd2bmuzJu9TH2OcA==", "dev": true, "funding": [ { @@ -5927,8 +5870,9 @@ ], "license": "MIT", "dependencies": { + "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.0.25", + "@csstools/css-syntax-patches-for-csstree": "^1.0.26", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", @@ -5941,7 +5885,7 @@ "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^11.1.1", + "file-entry-cache": "^11.1.2", "global-modules": "^2.0.0", "globby": "^16.1.0", "globjoin": "^0.1.4", @@ -5960,7 +5904,7 @@ "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0", - "string-width": "^8.1.0", + "string-width": "^8.1.1", "supports-hyperlinks": "^4.4.0", "svg-tags": "^1.0.0", "table": "^6.9.0", @@ -6046,25 +5990,25 @@ } }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.1.tgz", - "integrity": "sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.2.tgz", + "integrity": "sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^6.1.19" + "flat-cache": "^6.1.20" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.19", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.19.tgz", - "integrity": "sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A==", + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.20.tgz", + "integrity": "sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==", "dev": true, "license": "MIT", "dependencies": { - "cacheable": "^2.2.0", + "cacheable": "^2.3.2", "flatted": "^3.3.3", - "hookified": "^1.13.0" + "hookified": "^1.15.0" } }, "node_modules/stylelint/node_modules/ignore": { @@ -6078,9 +6022,9 @@ } }, "node_modules/stylelint/node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", + "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", "dev": true, "license": "MIT", "dependencies": { @@ -6671,16 +6615,16 @@ } }, "node_modules/vue": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", - "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", + "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.27", - "@vue/compiler-sfc": "3.5.27", - "@vue/runtime-dom": "3.5.27", - "@vue/server-renderer": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-sfc": "3.5.28", + "@vue/runtime-dom": "3.5.28", + "@vue/server-renderer": "3.5.28", + "@vue/shared": "3.5.28" }, "peerDependencies": { "typescript": "*" @@ -6712,9 +6656,9 @@ } }, "node_modules/vue-router": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.1.tgz", - "integrity": "sha512-t+lFugGXMdaq8lbn+vXG4j2H9UlsP205Tszz1wcDk9FyxqItBzcdJQ06IhpkQ2mHOfiTOHZeBshkskzPzHJkCw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.2.tgz", + "integrity": "sha512-YFhwaE5c5JcJpNB1arpkl4/GnO32wiUWRB+OEj1T0DlDxEZoOfbltl2xEwktNU/9o1sGcGburIXSpbLpPFe/6w==", "license": "MIT", "dependencies": { "@babel/generator": "^7.28.6", @@ -6731,7 +6675,7 @@ "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", - "unplugin": "^2.3.11", + "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, @@ -6739,7 +6683,7 @@ "url": "https://github.com/sponsors/posva" }, "peerDependencies": { - "@pinia/colada": "^0.18.1", + "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" @@ -6789,6 +6733,20 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vue-router/node_modules/unplugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", + "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index d0cb079..a56e2bb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "source": "index.html", "devDependencies": { "process": "^0.11.10", - "stylelint": "^17.0.0", + "stylelint": "^17.3.0", "stylelint-config-standard": "^40.0.0" }, "scripts": { @@ -26,16 +26,16 @@ "@connectrpc/connect-web": "^2.1.1", "@hugeicons/core-free-icons": "^3.1.1", "@hugeicons/vue": "^1.0.4", - "@vitejs/plugin-vue": "^6.0.3", + "@vitejs/plugin-vue": "^6.0.4", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.13.1", + "picocrank": "^1.14.0", "standard": "^17.1.2", "unplugin-vue-components": "^31.0.0", "vite": "^7.3.1", - "vue": "^3.5.27", + "vue": "^3.5.28", "vue-i18n": "^11.2.8", - "vue-router": "^5.0.1" + "vue-router": "^5.0.2" } } diff --git a/frontend/resources/vue/components/Breadcrumbs.vue b/frontend/resources/vue/components/Breadcrumbs.vue index 77c14da..5041292 100644 --- a/frontend/resources/vue/components/Breadcrumbs.vue +++ b/frontend/resources/vue/components/Breadcrumbs.vue @@ -40,6 +40,7 @@ a:hover { links.value = []; matched.forEach((record) => { + if (!record) return; if (record.meta && record.meta.breadcrumb) { record.meta.breadcrumb.forEach((item) => { links.value.push({ From cc8d8652f1d9befb1164755fc83bf934565a28dc Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Fri, 13 Feb 2026 18:38:17 +0000 Subject: [PATCH 16/20] chore: dep update Feb 2026 --- service/go.mod | 58 ++++++++++++++++++++++++-------------------------- service/go.sum | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 30 deletions(-) diff --git a/service/go.mod b/service/go.mod index b3ba5fb..659b3ab 100644 --- a/service/go.mod +++ b/service/go.mod @@ -1,18 +1,16 @@ module github.com/OliveTin/OliveTin -go 1.24.0 - -toolchain go1.24.9 +go 1.25.0 exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884 require ( connectrpc.com/connect v1.19.1 github.com/Masterminds/semver v1.5.0 - github.com/MicahParks/keyfunc/v3 v3.7.0 + github.com/MicahParks/keyfunc/v3 v3.8.0 github.com/PaesslerAG/jsonpath v0.1.1 github.com/alexedwards/argon2id v1.0.0 - github.com/bufbuild/buf v1.64.0 + github.com/bufbuild/buf v1.65.0 github.com/fsnotify/fsnotify v1.9.0 github.com/fzipp/gocyclo v0.6.0 github.com/go-critic/go-critic v0.14.3 @@ -29,9 +27,9 @@ require ( github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 go.akshayshah.org/connectproto v0.6.0 - golang.org/x/exp v0.0.0-20260112195511-716be5621a96 - golang.org/x/oauth2 v0.34.0 - golang.org/x/sys v0.40.0 + golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a + golang.org/x/oauth2 v0.35.0 + golang.org/x/sys v0.41.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) @@ -39,15 +37,15 @@ require ( require ( buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 // indirect buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 // indirect - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect - buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260122161138-ab4e39a3c3bc.2 // indirect - buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260122161138-ab4e39a3c3bc.1 // indirect + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 // indirect + buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2 // indirect + buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1 // indirect buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 // indirect buf.build/go/app v0.2.0 // indirect buf.build/go/bufplugin v0.9.0 // indirect buf.build/go/bufprivateusage v0.1.0 // indirect buf.build/go/interrupt v1.1.0 // indirect - buf.build/go/protovalidate v1.1.0 // indirect + buf.build/go/protovalidate v1.1.2 // indirect buf.build/go/protoyaml v0.6.0 // indirect buf.build/go/spdx v0.2.0 // indirect buf.build/go/standard v0.1.0 // indirect @@ -59,7 +57,7 @@ require ( github.com/PaesslerAG/gval v1.2.4 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608 // indirect + github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e // indirect github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cli/browser v1.3.0 // indirect @@ -70,14 +68,14 @@ require ( github.com/cristalhq/acmd v0.12.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v29.1.5+incompatible // indirect + github.com/docker/cli v29.2.1+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v28.5.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-chi/chi/v5 v5.2.4 // indirect + github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect @@ -90,13 +88,13 @@ require ( github.com/go-toolsmith/typep v1.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gofrs/flock v0.13.0 // indirect - github.com/google/cel-go v0.26.1 // indirect + github.com/google/cel-go v0.27.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-containerregistry v0.20.7 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jdx/go-netrc v1.0.0 // indirect - github.com/klauspost/compress v1.18.3 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -139,26 +137,26 @@ require ( go.lsp.dev/protocol v0.12.0 // indirect go.lsp.dev/uri v0.3.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect - go.opentelemetry.io/otel v1.39.0 // indirect - go.opentelemetry.io/otel/metric v1.39.0 // indirect - go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96 // indirect - golang.org/x/mod v0.32.0 // indirect - golang.org/x/net v0.49.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20260212183809-81e46e3db34a // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.50.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/term v0.39.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.41.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect + golang.org/x/tools v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/grpc v1.75.1 // indirect mvdan.cc/xurls/v2 v2.6.0 // indirect pluginrpc.com/pluginrpc v0.5.0 // indirect diff --git a/service/go.sum b/service/go.sum index f026f26..5ef6552 100644 --- a/service/go.sum +++ b/service/go.sum @@ -4,14 +4,20 @@ buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-2025010916 buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1/go.mod h1:8PRKXhgNes29Tjrnv8KdZzg3I1QceOkzibW1QK7EXv0= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 h1:PMmTMyvHScV9Mn8wc6ASge9uRcHy0jtqPd+fM35LmsQ= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251202164234-62b14f0b533c.2 h1:eQ6XRVUaYYZFOZvBsyrOYLWbw6464s5dVnHscxa0b8w= buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251202164234-62b14f0b533c.2/go.mod h1:omxVRch3jEPMINnUipLsuRWoEhND6LPXELKBG7xzyDw= buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260122161138-ab4e39a3c3bc.2 h1:cMzWbIukJ5uk1M58CtqmBE7Ojacg/t2nAg4AbS78uX8= buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260122161138-ab4e39a3c3bc.2/go.mod h1:GL3rFhQQsaI3PCBa0y5X71UHs6q5E/Xf9Q8WXBxE7a8= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2 h1:XPrWCd9ydEo5Ofv1aNJVJaxndMXLQjRO9vVzsJG3jL8= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2/go.mod h1:mpsjeEaxOYPIJV2cz4IagLghZufRvx+NPVtInjEeoQ8= buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1 h1:PdfIJUbUVKdajMVYuMdvr2Wvo+wmzGnlPEYA4bhFaWI= buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40= buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260122161138-ab4e39a3c3bc.1 h1:yWmrELGX6l1GphG9kPVcrMQLjWfXGI5bLDxwE+SfbDw= buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260122161138-ab4e39a3c3bc.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1 h1:Yreby6Ypa58wdQUEm9Fnc5g8n/jP487Dq3aK5yBYwfk= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40= buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 h1:iGPvEJltOXUMANWf0zajcRcbiOXLD90ZwPUFvbcuv6Q= buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1/go.mod h1:nWVKKRA29zdt4uvkjka3i/y4mkrswyWwiu0TbdX0zts= buf.build/go/app v0.2.0 h1:NYaH13A+RzPb7M5vO8uZYZ2maBZI5+MS9A9tQm66fy8= @@ -24,6 +30,8 @@ buf.build/go/interrupt v1.1.0 h1:olBuhgv9Sav4/9pkSLoxgiOsZDgM5VhRhvRpn3DL0lE= buf.build/go/interrupt v1.1.0/go.mod h1:ql56nXPG1oHlvZa6efNC7SKAQ/tUjS6z0mhJl0gyeRM= buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY= buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss= +buf.build/go/protovalidate v1.1.2 h1:83vYHoY8f34hB8MeitGaYE3CGVPFxwdEUuskh5qQpA0= +buf.build/go/protovalidate v1.1.2/go.mod h1:Ez3z+w4c+wG+EpW8ovgZaZPnPl2XVF6kaxgcv1NG/QE= buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w= buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q= buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw= @@ -46,6 +54,8 @@ github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOh github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0= github.com/MicahParks/keyfunc/v3 v3.7.0 h1:pdafUNyq+p3ZlvjJX1HWFP7MA3+cLpDtg69U3kITJGM= github.com/MicahParks/keyfunc/v3 v3.7.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0= +github.com/MicahParks/keyfunc/v3 v3.8.0 h1:Hx2dgIjAXGk9slakM6rV9BOeaWDPEXXZ4Us8guNBfds= +github.com/MicahParks/keyfunc/v3 v3.8.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8= @@ -63,6 +73,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/bufbuild/buf v1.61.0 h1:JPaK/RM2eoheyzznW+1LxaFgN6xjBCi8s25q2kUbH9A= @@ -71,12 +82,16 @@ github.com/bufbuild/buf v1.63.0 h1:vMIRozWqYcOU992FqcGgAp8LjoWVTzr52qEolUT+xi4= github.com/bufbuild/buf v1.63.0/go.mod h1:IWF+TIxwmk4DeyDmguN8WhxKFKQitcX2WnP+RlJlDiY= github.com/bufbuild/buf v1.64.0 h1:puHWFcVKmZFSu4KuaN0kZiQ32n7VVc3un1FeLU77XUs= github.com/bufbuild/buf v1.64.0/go.mod h1:U4ISwkjZXRLMaCkPG9zp1xY3xHEIwhCFwyNAaA56SGw= +github.com/bufbuild/buf v1.65.0 h1:f2BzeCY9rRh9P5KD340ZoPAaFLTkssoUTHx7lpqozgg= +github.com/bufbuild/buf v1.65.0/go.mod h1:7SAs2YqGpPXHqBBXBeYQbCzY0OQq4Jbg6XCqirEiYvQ= github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e h1:LQA+1MyiPkolGHJGC2GMDC5Xu+0RDVH6jGMKech7Exs= github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI= github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 h1:cQYwUyAzyMmYr7AyJU1C6pVCpUrJJBkmx7UunZosxxs= github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI= github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608 h1:3aRREBMDRbAajlaYTtD4uC9f2UYbqqyhaveDqJ35G/w= github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI= +github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e h1:emH16Bf1w4C0cJ3ge4QtBAl4sIYJe23EfpWH0SpA9co= +github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE= github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU= github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -111,6 +126,8 @@ github.com/docker/cli v29.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOo github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao= github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg= +github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= @@ -133,6 +150,8 @@ github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-critic/go-critic v0.14.2 h1:PMvP5f+LdR8p6B29npvChUXbD1vrNlKDf60NJtgMBOo= github.com/go-critic/go-critic v0.14.2/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= @@ -173,6 +192,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63Y github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= +github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -197,6 +218,8 @@ github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uq github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= @@ -343,8 +366,12 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= @@ -352,12 +379,18 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfg go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= @@ -380,22 +413,30 @@ golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o= +golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20251219203646-944ab1f22d93 h1:PbC785RGO6yPO051ItgbG/adwoKRWC0VS7kXXeD/iqk= golang.org/x/exp/typeparams v0.0.0-20251219203646-944ab1f22d93/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96 h1:RMc8anw0hCPcg5CZYN2PEQ8nMwosk461R6vFwPrCFVg= golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= +golang.org/x/exp/typeparams v0.0.0-20260212183809-81e46e3db34a h1:n3SZDk8iNpMasCwQD7/0dIaCVf3gJiGZ9Rqa094jUN0= +golang.org/x/exp/typeparams v0.0.0-20260212183809-81e46e3db34a/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -405,8 +446,12 @@ golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -427,6 +472,8 @@ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -436,6 +483,8 @@ golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -446,6 +495,8 @@ golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -456,6 +507,8 @@ golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E= @@ -464,12 +517,16 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1: google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d h1:tUKoKfdZnSjTf5LW7xpG4c6SZ3Ozisn5eumcoTuMEN4= google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q= google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= From a1e5a0ff4eb100f3fca483791888193f8fd3dd9f Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Fri, 13 Feb 2026 20:58:02 +0000 Subject: [PATCH 17/20] fix: cssClass - again. #805 --- frontend/resources/vue/App.vue | 6 +- integration-tests/tests/cssClass/config.yaml | 24 +++++++ integration-tests/tests/cssClass/cssClass.mjs | 66 +++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 integration-tests/tests/cssClass/config.yaml create mode 100644 integration-tests/tests/cssClass/cssClass.mjs diff --git a/frontend/resources/vue/App.vue b/frontend/resources/vue/App.vue index 3acbe21..27bb425 100644 --- a/frontend/resources/vue/App.vue +++ b/frontend/resources/vue/App.vue @@ -361,10 +361,12 @@ function applyTheme() { document.head.appendChild(themeStyle) } + // Load theme into @layer theme so it takes precedence over @layer components (theme is + // last in style.css layer order). Fixes #804 regression after beta.2. if (themePreference.value && themePreference.value !== '') { - themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);` + themeStyle.textContent = `@layer theme { @import url('/custom-webui/themes/${themePreference.value}/theme.css'); }` } else { - themeStyle.textContent = `@import url('/theme.css') layer(theme);` + themeStyle.textContent = `@layer theme { @import url('/theme.css'); }` } } diff --git a/integration-tests/tests/cssClass/config.yaml b/integration-tests/tests/cssClass/config.yaml new file mode 100644 index 0000000..3e8cfc6 --- /dev/null +++ b/integration-tests/tests/cssClass/config.yaml @@ -0,0 +1,24 @@ +# +# Integration Test Config: cssClass on dashboard components (#804) +# + +listenAddressSingleHTTPFrontend: 0.0.0.0:1337 + +logLevel: "DEBUG" +checkForUpdates: false + +actions: [] + +dashboards: + - title: CssClass Dashboard + contents: + - title: Button with custom class + type: link + cssClass: test-custom-class + inlineAction: + shell: echo ok + icon: ping + - title: Display with custom class + type: display + cssClass: test-display-class + contents: [] diff --git a/integration-tests/tests/cssClass/cssClass.mjs b/integration-tests/tests/cssClass/cssClass.mjs new file mode 100644 index 0000000..444a6cb --- /dev/null +++ b/integration-tests/tests/cssClass/cssClass.mjs @@ -0,0 +1,66 @@ +import { describe, it, before, after, afterEach } from 'mocha' +import { expect } from 'chai' +import { By } from 'selenium-webdriver' +import { + getRootAndWait, + takeScreenshotOnFailure, +} from '../../lib/elements.js' + +describe('config: cssClass', function () { + before(async function () { + await runner.start('cssClass') + }) + + after(async () => { + await runner.stop() + }) + + afterEach(function () { + takeScreenshotOnFailure(this.currentTest, webdriver) + }) + + it('cssClass is applied to action button (link component)', async function () { + await getRootAndWait() + + const buttonWithClass = await webdriver.findElements(By.css('.action-button button.test-custom-class')) + expect(buttonWithClass).to.have.length.at.least(1, 'Action button should have cssClass test-custom-class on the button') + + const classAttr = await buttonWithClass[0].getAttribute('class') + expect(classAttr).to.include('test-custom-class') + }) + + it('cssClass override: style rule targeting custom class wins over component styles', async function () { + await getRootAndWait() + + const buttonWithClass = await webdriver.findElements(By.css('.action-button button.test-custom-class')) + expect(buttonWithClass).to.have.length.at.least(1) + + const beforePx = await buttonWithClass[0].getCssValue('border-top-width') + await webdriver.executeScript(` + var style = document.getElementById('cssclass-test-override-style'); + if (!style) { + style = document.createElement('style'); + style.id = 'cssclass-test-override-style'; + style.textContent = '.test-custom-class { border-top-width: 31px !important; }'; + document.head.appendChild(style); + } else { + style.textContent = '.test-custom-class { border-top-width: 31px !important; }'; + } + `) + await webdriver.sleep(150) + + const afterPx = await buttonWithClass[0].getCssValue('border-top-width') + const afterNum = parseInt(afterPx, 10) + expect(afterNum).to.be.greaterThan(10, 'Override targeting cssClass should win over component 1px (before=' + beforePx + ' after=' + afterPx + ') (#804)') + }) + + it('cssClass is applied to display component', async function () { + await getRootAndWait() + + const displayElements = await webdriver.findElements(By.css('.display.test-display-class')) + expect(displayElements).to.have.length.at.least(1, 'Display component with cssClass test-display-class should be in DOM') + + const classAttr = await displayElements[0].getAttribute('class') + expect(classAttr).to.include('test-display-class') + }) +}) From ea7a1fb3e379ceeebb8383d30eb63507ac235e9d Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Fri, 13 Feb 2026 23:18:35 +0000 Subject: [PATCH 18/20] fix: cssClass again again #804 --- frontend/resources/vue/App.vue | 7 +++---- integration-tests/tests/cssClass/config.yaml | 3 +++ integration-tests/tests/cssClass/cssClass.mjs | 11 +++++++++++ .../custom-webui/themes/cssclass-theme/theme.css | 4 ++++ 4 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 integration-tests/tests/cssClass/custom-webui/themes/cssclass-theme/theme.css diff --git a/frontend/resources/vue/App.vue b/frontend/resources/vue/App.vue index 27bb425..5c1fa52 100644 --- a/frontend/resources/vue/App.vue +++ b/frontend/resources/vue/App.vue @@ -361,12 +361,11 @@ function applyTheme() { document.head.appendChild(themeStyle) } - // Load theme into @layer theme so it takes precedence over @layer components (theme is - // last in style.css layer order). Fixes #804 regression after beta.2. + // Load theme into @layer theme so it takes precedence over @layer components if (themePreference.value && themePreference.value !== '') { - themeStyle.textContent = `@layer theme { @import url('/custom-webui/themes/${themePreference.value}/theme.css'); }` + themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);` } else { - themeStyle.textContent = `@layer theme { @import url('/theme.css'); }` + themeStyle.textContent = `@import url('/theme.css') layer(theme);` } } diff --git a/integration-tests/tests/cssClass/config.yaml b/integration-tests/tests/cssClass/config.yaml index 3e8cfc6..8819541 100644 --- a/integration-tests/tests/cssClass/config.yaml +++ b/integration-tests/tests/cssClass/config.yaml @@ -7,6 +7,9 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337 logLevel: "DEBUG" checkForUpdates: false +# Custom theme used to verify theme CSS applies to cssClass (e.g. action button background) +themeName: cssclass-theme + actions: [] dashboards: diff --git a/integration-tests/tests/cssClass/cssClass.mjs b/integration-tests/tests/cssClass/cssClass.mjs index 444a6cb..f6ee292 100644 --- a/integration-tests/tests/cssClass/cssClass.mjs +++ b/integration-tests/tests/cssClass/cssClass.mjs @@ -29,6 +29,17 @@ describe('config: cssClass', function () { expect(classAttr).to.include('test-custom-class') }) + it('custom theme applies background color to action button via cssClass', async function () { + await getRootAndWait() + + const buttonWithClass = await webdriver.findElements(By.css('.action-button button.test-custom-class')) + expect(buttonWithClass).to.have.length.at.least(1, 'Action button with test-custom-class should exist') + + const bgColor = await buttonWithClass[0].getCssValue('background-color') + expect(bgColor, 'Theme theme.css should set .action-button button.test-custom-class background to rgb(32, 64, 128)') + .to.match(/rgba?\(\s*32\s*,\s*64\s*,\s*128\s*(,\s*1)?\s*\)/) + }) + it('cssClass override: style rule targeting custom class wins over component styles', async function () { await getRootAndWait() diff --git a/integration-tests/tests/cssClass/custom-webui/themes/cssclass-theme/theme.css b/integration-tests/tests/cssClass/custom-webui/themes/cssclass-theme/theme.css new file mode 100644 index 0000000..5373d91 --- /dev/null +++ b/integration-tests/tests/cssClass/custom-webui/themes/cssclass-theme/theme.css @@ -0,0 +1,4 @@ +/* Theme for cssClass integration test: set a distinct background on the action button */ +.action-button button.test-custom-class { + background-color: rgb(32, 64, 128); +} From 1248ee8765f7b25e764719422b0b3c841c36ae05 Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Fri, 13 Feb 2026 23:36:16 +0000 Subject: [PATCH 19/20] chore: remove extranious comments --- service/internal/api/api.go | 6 +++++- service/internal/executor/loadlogs.go | 13 +------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/service/internal/api/api.go b/service/internal/api/api.go index c3f9caa..faf58d1 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -710,7 +710,11 @@ func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.Dum return connect.NewResponse(res), nil } - jsonstring, _ := json.MarshalIndent(tpl.GetNewGeneralTemplateContext(), "", " ") + jsonstring, err := json.MarshalIndent(tpl.GetNewGeneralTemplateContext(), "", " ") + if err != nil { + log.WithError(err).Error("DumpVars: failed to marshal template context from GetNewGeneralTemplateContext") + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("dump vars: marshal template context: %w", err)) + } fmt.Printf("%s", jsonstring) res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore" diff --git a/service/internal/executor/loadlogs.go b/service/internal/executor/loadlogs.go index 6a03ae9..2741fba 100644 --- a/service/internal/executor/loadlogs.go +++ b/service/internal/executor/loadlogs.go @@ -37,7 +37,6 @@ func (e *Executor) LoadLogsFromDisk() { }).Info("Finished loading persisted logs from disk") } -// readLogDirectory reads the log directory and returns entries, or nil if the directory doesn't exist or can't be read. func (e *Executor) readLogDirectory(resultsDir string) ([]os.DirEntry, int) { if _, err := os.Stat(resultsDir); os.IsNotExist(err) { log.WithFields(log.Fields{ @@ -62,7 +61,6 @@ func (e *Executor) readLogDirectory(resultsDir string) ([]os.DirEntry, int) { return entries, 0 } -// parseLogFiles parses YAML log files from the directory entries. func (e *Executor) parseLogFiles(resultsDir string, entries []os.DirEntry, skippedCount int) ([]*InternalLogEntry, int) { loadedLogs := make([]*InternalLogEntry, 0) @@ -81,12 +79,10 @@ func (e *Executor) parseLogFiles(resultsDir string, entries []os.DirEntry, skipp return loadedLogs, skippedCount } -// shouldProcessLogEntry checks if a directory entry should be processed as a log file. func (e *Executor) shouldProcessLogEntry(entry os.DirEntry) bool { return !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yaml") } -// processLogFileEntry processes a single log file entry and returns the log entry or nil if it should be skipped. func (e *Executor) processLogFileEntry(resultsDir, filename string) (*InternalLogEntry, int) { logEntry, ok := e.loadLogFileFromPath(resultsDir, filename) if !ok { @@ -104,7 +100,6 @@ func (e *Executor) processLogFileEntry(resultsDir, filename string) (*InternalLo return logEntry, 0 } -// loadLogFileFromPath loads and unmarshals a single log file. func (e *Executor) loadLogFileFromPath(resultsDir, filename string) (*InternalLogEntry, bool) { filepath := filepath.Join(resultsDir, filename) data, err := os.ReadFile(filepath) @@ -128,7 +123,7 @@ func (e *Executor) loadLogFileFromPath(resultsDir, filename string) (*InternalLo return &logEntry, true } -// restoreBindingForLogEntry attempts to restore the binding for a log entry if it's missing or invalid. +// Skipped when the entry already has a valid binding or has no ActionConfigTitle (e.g. action/entity removed from config). func (e *Executor) restoreBindingForLogEntry(logEntry *InternalLogEntry, filepath string) { if e.hasValidBinding(logEntry) || logEntry.ActionConfigTitle == "" { return @@ -144,12 +139,10 @@ func (e *Executor) restoreBindingForLogEntry(logEntry *InternalLogEntry, filepat logEntry.Binding = nil } -// hasValidBinding checks if a log entry has a valid binding. func (e *Executor) hasValidBinding(logEntry *InternalLogEntry) bool { return logEntry.Binding != nil && logEntry.Binding.Action != nil } -// logBindingNotFound logs a debug message when a binding cannot be found for a log entry. func (e *Executor) logBindingNotFound(logEntry *InternalLogEntry, filepath string) { log.WithFields(log.Fields{ "file": filepath, @@ -159,7 +152,6 @@ func (e *Executor) logBindingNotFound(logEntry *InternalLogEntry, filepath strin }).Debug("Could not find binding for log entry, loading without binding") } -// restoreLogsToExecutor restores loaded logs to the executor's internal structures. func (e *Executor) restoreLogsToExecutor(loadedLogs []*InternalLogEntry, skippedCount int) int { e.logmutex.Lock() defer e.logmutex.Unlock() @@ -185,7 +177,6 @@ func (e *Executor) restoreLogsToExecutor(loadedLogs []*InternalLogEntry, skipped return skippedCount } -// addLogToBindingMap adds a log entry to the LogsByBindingId map. func (e *Executor) addLogToBindingMap(logEntry *InternalLogEntry) { if _, containsKey := e.LogsByBindingId[logEntry.Binding.ID]; !containsKey { e.LogsByBindingId[logEntry.Binding.ID] = make([]*InternalLogEntry, 0) @@ -193,7 +184,6 @@ func (e *Executor) addLogToBindingMap(logEntry *InternalLogEntry) { e.LogsByBindingId[logEntry.Binding.ID] = append(e.LogsByBindingId[logEntry.Binding.ID], logEntry) } -// findBindingByActionTitle attempts to find a binding by matching the action config title and entity prefix. func (e *Executor) findBindingByActionTitle(actionConfigTitle string, entityPrefix string) *ActionBinding { e.MapActionBindingsLock.RLock() defer e.MapActionBindingsLock.RUnlock() @@ -207,7 +197,6 @@ func (e *Executor) findBindingByActionTitle(actionConfigTitle string, entityPref return nil } -// matchesEntityPrefix checks if a binding matches the given entity prefix. func (e *Executor) matchesEntityPrefix(binding *ActionBinding, entityPrefix string) bool { if entityPrefix == "" { return binding.Entity == nil From 011ee866dfd98b18fcefb969828e36ec02edeb5f Mon Sep 17 00:00:00 2001 From: jamesread <contact@jread.com> Date: Fri, 13 Feb 2026 23:53:43 +0000 Subject: [PATCH 20/20] feat: template parsing for env in password fields --- service/internal/config/config.go | 3 +-- service/internal/config/sanitize.go | 31 +++++++++++++++++------------ service/internal/env/env.go | 17 ++++++++++++++++ service/internal/tpl/templates.go | 16 ++------------- 4 files changed, 38 insertions(+), 29 deletions(-) create mode 100644 service/internal/env/env.go diff --git a/service/internal/config/config.go b/service/internal/config/config.go index 79a92d5..de5bdcc 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -171,8 +171,7 @@ type Config struct { BannerCSS string `koanf:"bannerCss"` Include string `koanf:"include"` - sourceFiles []string - passwordTemplateParser func(string, interface{}) string + sourceFiles []string } type AuthLocalUsersConfig struct { diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index 779cfe1..43364f5 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -2,7 +2,9 @@ package config import ( "strings" + "text/template" + "github.com/OliveTin/OliveTin/internal/env" "github.com/google/uuid" log "github.com/sirupsen/logrus" ) @@ -173,26 +175,29 @@ func (cfg *Config) sanitizeLogHistoryPageSize() { } } -// SetPasswordTemplateParser sets the function to use for parsing password templates. -// This is called from main.go to avoid import cycles (config can't import entities). -func (cfg *Config) SetPasswordTemplateParser(parser func(string, interface{}) string) { - cfg.passwordTemplateParser = parser -} - func (cfg *Config) sanitizeLocalUserPasswords() { - if cfg.passwordTemplateParser == nil { - return - } - for _, user := range cfg.AuthLocalUsers.Users { if user.Password != "" { - // Parse password as template to support environment variables and other template values - // Note: .CurrentEntity is nil in this context as local users are not entity-bound - user.Password = cfg.passwordTemplateParser(user.Password, nil) + user.Password = parsePasswordTemplate(user.Password) } } } +// parsePasswordTemplate expands {{ .Env.VAR }} in local user password fields using the process environment. +func parsePasswordTemplate(source string) string { + t, err := template.New("password").Option("missingkey=error").Parse(source) + if err != nil { + log.WithFields(log.Fields{"error": err}).Debug("Password template parse failed, using literal") + return source + } + var b strings.Builder + if err := t.Execute(&b, map[string]interface{}{"Env": env.BuildEnvMap()}); err != nil { + log.WithFields(log.Fields{"error": err}).Debug("Password template execute failed, using literal") + return source + } + return b.String() +} + func getActionID(action *Action) string { if action.ID == "" { return uuid.NewString() diff --git a/service/internal/env/env.go b/service/internal/env/env.go new file mode 100644 index 0000000..995da66 --- /dev/null +++ b/service/internal/env/env.go @@ -0,0 +1,17 @@ +package env + +import ( + "os" + "strings" +) + +func BuildEnvMap() map[string]string { + envMap := make(map[string]string) + for _, e := range os.Environ() { + parts := strings.SplitN(e, "=", 2) + if len(parts) == 2 { + envMap[parts[0]] = parts[1] + } + } + return envMap +} diff --git a/service/internal/tpl/templates.go b/service/internal/tpl/templates.go index bf0a242..580b907 100644 --- a/service/internal/tpl/templates.go +++ b/service/internal/tpl/templates.go @@ -2,12 +2,12 @@ package tpl import ( "fmt" - "os" "regexp" "strings" "text/template" "github.com/OliveTin/OliveTin/internal/entities" + "github.com/OliveTin/OliveTin/internal/env" "github.com/OliveTin/OliveTin/internal/installationinfo" log "github.com/sirupsen/logrus" ) @@ -49,7 +49,7 @@ func init() { Runtime: installationinfo.Runtime, } - cachedEnvMap = buildEnvMap() + cachedEnvMap = env.BuildEnvMap() } func GetNewGeneralTemplateContext() *generalTemplateContext { @@ -59,18 +59,6 @@ func GetNewGeneralTemplateContext() *generalTemplateContext { } } -func buildEnvMap() map[string]string { - envMap := make(map[string]string) - for _, env := range os.Environ() { - parts := strings.SplitN(env, "=", 2) - if len(parts) == 2 { - envMap[parts[0]] = parts[1] - } - } - - return envMap -} - func migrateLegacyEntityProperties(rawShellCommand string) string { foundArgumentNames := legacyEntityPropertiesRegex.FindAllStringSubmatch(rawShellCommand, -1)