From 1eb1d6113455fb6e2cc081c2c1241bdd0df6f030 Mon Sep 17 00:00:00 2001 From: Oliver Tzeng Date: Fri, 24 Jul 2026 12:22:00 +0800 Subject: [PATCH 01/17] translated to zh-Hant-TW --- lang/zh-Hant-TW.yaml | 79 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 lang/zh-Hant-TW.yaml diff --git a/lang/zh-Hant-TW.yaml b/lang/zh-Hant-TW.yaml new file mode 100644 index 0000000..7f20e2a --- /dev/null +++ b/lang/zh-Hant-TW.yaml @@ -0,0 +1,79 @@ +schemaVersion: 1 +translations: + welcome: 歡迎使用 OliveTin + docs: 官方文件 + raise-issue: 在 GitHub 上建立 Issue + nav.actions: 動作 + nav.logs: 日誌 + nav.entities: 實體 + nav.diagnostics: 診斷 + connected: 已連線 + disconnected: 已斷線 + reconnecting: 重新連線中… + disconnected-banner-announcement: 系統事件 WebSocket 已斷線。 + disconnected-banner-link-text: "系統事件 WebSocket 已斷線" + disconnected-banner-suffix: " 自 {disconnectedSince} 起。將在 {reconnectIn} 後嘗試重新連線。" + disconnected-banner-suffix-reconnecting: " 自 {disconnectedSince} 起。嘗試重新連線中…" + login-button: 登入 + logs.title: 日誌 + logs.page-description: 這是已執行動作的日誌清單。請使用過濾條件欄位來搜尋關鍵字與運算式。 + logs.filter-placeholder: 過濾日誌 (例如 !Update 或 Status != Completed) + logs.filter-help-title: 過濾語法 + logs.filter-help-intro: 過濾器會在伺服器端針對您有權限檢視的項目進行篩選。您可以使用 and / or 來組合搜尋條件。 + logs.filter-help-fields: "欄位:Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode。運算子:==, !=, contains。在單一單字前加上 ! 前綴可排除相符的項目。" + logs.filter-help-examples: "範例:backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed" + logs.filter-error: 無法套用過濾運算式。 + logs.no-logs-for-filter: 沒有符合目前過濾條件的日誌。 + logs.timestamp: 時間戳記 + logs.action: 動作 + logs.metadata: 中繼資料 + logs.status: 狀態 + logs.no-logs-to-display: 目前沒有可顯示的日誌。 + logs.timed-out: 執行逾時 + logs.blocked: 已阻擋 + logs.exit-code: 結束代碼 + logs.completed: 已完成 + logs.clear-filter: 清除搜尋過濾條件 + logs.clear-date-filter: 清除日期過濾條件 + logs.calendar: 日曆 + logs.calendar-title: 日誌日曆 + logs.back-to-list: 返回清單 + logs.queue: 佇列 + logs.queue-title: 執行佇列 + logs.queue-page-description: 依動作群組分類的使用中與等待中執行項目。您無權檢視的項目將會被隱藏。 + logs.queue-empty: 目前沒有使用中或等待中的執行項目。 + logs.queue-default-group: 預設 + logs.action-group-limits: "並行數:{concurrent},佇列大小:{queueSize}" + logs.queue-group-active: "{active} 個使用中 (最大值 {max})" + logs.queue-group-active-unlimited: "{active} 個使用中" + logs.queue-waiting: 等待中 + logs.queue-running: 執行中 + logs.queue-position: "#{position}" + logs.queue-entity: 實體 + logs.queue-action-details: 動作詳細資訊 + diagnostics.get-support: 尋求技術支援 + diagnostics.get-support-description: 如果您在使用 OliveTin 時遇到問題並希望提出支援請求,提供此頁面的「伺服器診斷資訊」將會有很大的幫助。 + diagnostics.where-to-find-help: 哪裡可以尋求協助 + diagnostics.ssh: SSH + diagnostics.found-key: 找到金鑰 (Key) + diagnostics.found-config: 找到設定檔 (Config) + diagnostics.server-diagnostics: 伺服器診斷 + diagnostics.server-diagnostics-description: 此區塊可讓您產生詳細的設定與環境報告。建議在提出支援請求時附上此報告。 + diagnostics.server-diagnostics-docs: 伺服器診斷官方文件 + diagnostics.generate-server-diagnostics: 產生伺服器診斷資訊 + diagnostics.browser-info: 瀏覽器資訊 + diagnostics.browser-info-description: 此區塊可讓您產生瀏覽器資訊的詳細報告。這在排解特定瀏覽器問題時相當有幫助。 + diagnostics.generate-browser-info: 產生瀏覽器資訊 + diagnostics.copy-to-clipboard: 複製到剪貼簿 + diagnostics.copied: 已複製! + diagnostics.unknown: 未知 + diagnostics.useragent-data-error: 取得 userAgentData 時發生錯誤 + return-to-index: 返回首頁 + search-filter: 過濾目前頁面 + language-dialog.title: 選擇語言 + language-dialog.browser-languages: 瀏覽器語言 + language-dialog.not-available: 無法使用 + language-dialog.close: 關閉 + theme-dialog.title: 選擇佈景主題 + theme-dialog.default: 預設佈景主題 + theme-dialog.close: 關閉 From c4531b344e3dd8de3bc389e21d08c1685f6d10f9 Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 24 Jul 2026 07:12:03 +0100 Subject: [PATCH 02/17] fix(executor): sanitize unsafe chars in log filenames Co-authored-by: Cursor --- docs/modules/ROOT/pages/logs/saving.adoc | 2 + service/internal/executor/executor.go | 20 ++++- service/internal/executor/executor_test.go | 90 ++++++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/logs/saving.adoc b/docs/modules/ROOT/pages/logs/saving.adoc index d69d181..be2b751 100644 --- a/docs/modules/ROOT/pages/logs/saving.adoc +++ b/docs/modules/ROOT/pages/logs/saving.adoc @@ -29,6 +29,8 @@ actions: From the above example, you can see there There are two types of logs - **results (.yaml)** and **output (.log)** +Filenames are built from the action title, a unix timestamp, and the execution tracking ID. Characters that are unsafe in filenames (for example `/` and `\`) are replaced with `_` so titles like `Create/update Monthly Report` still write a single file. The original title is unchanged inside the results YAML. + * **Results (.yaml)** - this captures almost everything that OliveTin knows about the action and looks like this. + [source,yaml] diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 6be518d..08dca61 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -1391,7 +1391,7 @@ func triggerLoop(req *ExecutionRequest) { } func stepSaveLog(req *ExecutionRequest) bool { - filename := fmt.Sprintf("%v.%v.%v", req.logEntry.ActionTitle, req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID) + filename := fmt.Sprintf("%v.%v.%v", sanitizeLogFilename(req.logEntry.ActionTitle), req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID) saveLogResults(req, filename) saveLogOutput(req, filename) @@ -1399,6 +1399,24 @@ func stepSaveLog(req *ExecutionRequest) bool { return true } +// sanitizeLogFilename replaces characters that are unsafe in filenames so action +// titles like "Create/update Report" do not create nested paths or fail to write. +func sanitizeLogFilename(title string) string { + replacer := strings.NewReplacer( + "/", "_", + "\\", "_", + ":", "_", + "*", "_", + "?", "_", + "\"", "_", + "<", "_", + ">", "_", + "|", "_", + ) + + return replacer.Replace(title) +} + func firstNonEmpty(one, two string) string { if one != "" { return one diff --git a/service/internal/executor/executor_test.go b/service/internal/executor/executor_test.go index 8b84c33..b2fed8c 100644 --- a/service/internal/executor/executor_test.go +++ b/service/internal/executor/executor_test.go @@ -780,3 +780,93 @@ func (c *executionFinishedCollector) OnExecutionFinished(entry *InternalLogEntry func (c *executionFinishedCollector) OnOutputChunk(_ []byte, _ string) {} func (c *executionFinishedCollector) OnActionMapRebuilt() {} + +func TestSanitizeLogFilename(t *testing.T) { + tests := []struct { + title string + want string + }{ + {"Echo Test", "Echo Test"}, + {"Create/update Monthly Report", "Create_update Monthly Report"}, + {`path\with\backslashes`, "path_with_backslashes"}, + {`a:b*c?d"eg|h`, "a_b_c_d_e_f_g_h"}, + } + + for _, tt := range tests { + assert.Equal(t, tt.want, sanitizeLogFilename(tt.title), "title=%q", tt.title) + } +} + +func TestStepSaveLogSanitizesSlashInTitle(t *testing.T) { + resultsDir := t.TempDir() + outputDir := t.TempDir() + started := time.Unix(1714333384, 0) + trackingID := "5e2dc9e5-b6b3-445b-bff9-c2082b0bbbb2" + title := "Create/update Monthly Report" + + req := &ExecutionRequest{ + Cfg: &config.Config{ + SaveLogs: config.SaveLogsConfig{ + ResultsDirectory: resultsDir, + OutputDirectory: outputDir, + }, + }, + Binding: &ActionBinding{ + Action: &config.Action{}, + }, + logEntry: &InternalLogEntry{ + ActionTitle: title, + DatetimeStarted: started, + ExecutionTrackingID: trackingID, + Output: "report ok", + }, + } + + assert.True(t, stepSaveLog(req)) + + expectedBase := "Create_update Monthly Report.1714333384." + trackingID + resultsPath := filepath.Join(resultsDir, expectedBase+".yaml") + outputPath := filepath.Join(outputDir, expectedBase+".log") + + assert.FileExists(t, resultsPath) + assert.FileExists(t, outputPath) + + resultsEntries, err := os.ReadDir(resultsDir) + assert.NoError(t, err) + assert.Len(t, resultsEntries, 1, "results file must be flat under resultsDirectory, not a subdirectory") + + data, err := os.ReadFile(resultsPath) + assert.NoError(t, err) + assert.Contains(t, string(data), title, "YAML content keeps the original action title") + + output, err := os.ReadFile(outputPath) + assert.NoError(t, err) + assert.Equal(t, "report ok", string(output)) +} + +func TestStepSaveLogKeepsSafeTitleFilename(t *testing.T) { + resultsDir := t.TempDir() + started := time.Unix(1714333384, 0) + trackingID := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + + req := &ExecutionRequest{ + Cfg: &config.Config{ + SaveLogs: config.SaveLogsConfig{ + ResultsDirectory: resultsDir, + }, + }, + Binding: &ActionBinding{ + Action: &config.Action{}, + }, + logEntry: &InternalLogEntry{ + ActionTitle: "Echo Test", + DatetimeStarted: started, + ExecutionTrackingID: trackingID, + }, + } + + assert.True(t, stepSaveLog(req)) + + expectedPath := filepath.Join(resultsDir, "Echo Test.1714333384."+trackingID+".yaml") + assert.FileExists(t, expectedPath) +} From 58b1c0080d72e5ae0151cfdbfdc23c92b82656e0 Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 24 Jul 2026 07:12:16 +0100 Subject: [PATCH 03/17] docs(macos): note notarized release binaries Co-authored-by: Cursor --- docs/modules/ROOT/pages/install/macos.adoc | 6 +++--- docs/modules/ROOT/pages/install/macos_service.adoc | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/install/macos.adoc b/docs/modules/ROOT/pages/install/macos.adoc index 6c35f66..fe67e95 100644 --- a/docs/modules/ROOT/pages/install/macos.adoc +++ b/docs/modules/ROOT/pages/install/macos.adoc @@ -34,11 +34,11 @@ tar -xzf OliveTin-darwin-arm64.tar.gz cd OliveTin-darwin-arm64 ---- -== Remove the Gatekeeper quarantine +== Gatekeeper and notarization -The binary is downloaded from the internet and is not notarized by Apple, so on first run Gatekeeper blocks it with a message like _"OliveTin can't be opened because Apple cannot check it for malicious software."_ +Current release binaries are **Developer ID signed and notarized** by Apple. After extract, you should be able to run `./OliveTin` normally. -Clear the quarantine attribute so it will run: +If Gatekeeper still blocks an older (unsigned) build, or you see a prompt that Apple cannot check the binary for malicious software, clear the quarantine attribute: [source,shell] ---- diff --git a/docs/modules/ROOT/pages/install/macos_service.adoc b/docs/modules/ROOT/pages/install/macos_service.adoc index 4b3134c..86e9157 100644 --- a/docs/modules/ROOT/pages/install/macos_service.adoc +++ b/docs/modules/ROOT/pages/install/macos_service.adoc @@ -2,7 +2,7 @@ This option installs OliveTin as a launchd service, so it runs in the background and starts automatically. This is the macOS equivalent of running OliveTin as a Linux systemd service or a xref:install/windows_service.adoc[Windows service]. If you just want to run OliveTin as a regular application, follow the xref:install/macos.adoc[macOS install] instructions instead. -Before continuing, complete the xref:install/macos.adoc[macOS install] steps (download, extract, and clear the Gatekeeper quarantine) and confirm OliveTin starts correctly by running `./OliveTin`. +Before continuing, complete the xref:install/macos.adoc[macOS install] steps (download, extract, and confirm Gatekeeper allows the binary) and confirm OliveTin starts correctly by running `./OliveTin`. == Choose LaunchAgent or LaunchDaemon From 926b02bcbad28cce35d7b182f661151a445c3d1d Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 24 Jul 2026 07:20:25 +0100 Subject: [PATCH 04/17] fix: updated translations for de-DE, es-ES, it-IT and zh-Hans-CN --- lang/combined_output.json | 119 ++++++++++++++++++++++++++++++++++++-- lang/de-DE.yaml | 10 +++- lang/es-ES.yaml | 10 +++- lang/it-IT.yaml | 10 +++- lang/zh-Hans-CN.yaml | 10 +++- 5 files changed, 151 insertions(+), 8 deletions(-) diff --git a/lang/combined_output.json b/lang/combined_output.json index 1a166f8..cffc185 100644 --- a/lang/combined_output.json +++ b/lang/combined_output.json @@ -11,7 +11,7 @@ "diagnostics.found-key": "Schlüssel gefunden", "diagnostics.generate-browser-info": "Browser-Informationen erstellen", "diagnostics.generate-server-diagnostics": "Server-Diagnostik erstellen", - "diagnostics.get-support": "Unterstützung erhalten", + "diagnostics.get-support": "Unterstützung erhalten Unterstützung erhalten", "diagnostics.get-support-description": "Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, Server-Diagnostik von dieser Seite einzufügen.", "diagnostics.server-diagnostics": "Server-Diagnostik", "diagnostics.server-diagnostics-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Konfiguration und Umgebung zu erstellen. Es ist eine gute Idee, dies bei einer Support-Anfrage einzufügen.", @@ -41,10 +41,18 @@ "logs.clear-filter": "Suchfilter löschen", "logs.completed": "Abgeschlossen", "logs.exit-code": "Ausführungscode", + "logs.filter-error": "Filterausdruck konnte nicht angewendet werden.", + "logs.filter-help-examples": "Beispiele: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed", + "logs.filter-help-fields": "Felder: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operatoren: ==, !=, contains. Ein !-Präfix vor einem einzelnen Wort schließt passende Einträge aus.", + "logs.filter-help-intro": "Filter laufen auf dem Server über Einträge, die Sie anzeigen dürfen. Kombinieren Sie Begriffe mit and / or.", + "logs.filter-help-title": "Filtersyntax", + "logs.filter-placeholder": "Protokolle filtern (z. B. !Update oder Status != Completed)", "logs.metadata": "Metadaten", + "logs.no-logs-for-filter": "Keine Protokolle entsprechen dem aktuellen Filter.", "logs.no-logs-to-display": "Es gibt keine Protokolle zu anzeigen.", "logs.page-description": "Dies ist eine Liste von Protokollen von Aktionen, die ausgeführt wurden. Sie können die Liste nach Aktionstitel filtern.", "logs.queue": "Warteschlange", + "logs.queue-action-details": "Aktionsdetails", "logs.queue-default-group": "Standard", "logs.queue-empty": "Derzeit gibt es keine aktiven oder wartenden Ausführungen.", "logs.queue-entity": "Entität", @@ -161,7 +169,7 @@ "diagnostics.found-key": "Clave encontrada", "diagnostics.generate-browser-info": "Generar información del navegador", "diagnostics.generate-server-diagnostics": "Generar diagnóstico del servidor", - "diagnostics.get-support": "Obtener soporte", + "diagnostics.get-support": "Obtener soporte Obtener soporte", "diagnostics.get-support-description": "Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir diagnósticos del servidor desde esta página.", "diagnostics.server-diagnostics": "Diagnóstico del servidor", "diagnostics.server-diagnostics-description": "Esta sección le permite generar un informe detallado de su configuración y entorno. Es una buena idea incluir esto al presentar una solicitud de soporte.", @@ -191,10 +199,18 @@ "logs.clear-filter": "Limpiar filtro de búsqueda", "logs.completed": "Completado", "logs.exit-code": "Código de salida", + "logs.filter-error": "No se pudo aplicar la expresión del filtro.", + "logs.filter-help-examples": "Ejemplos: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed", + "logs.filter-help-fields": "Campos: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operadores: ==, !=, contains. Un prefijo ! en una sola palabra excluye las entradas coincidentes.", + "logs.filter-help-intro": "Los filtros se ejecutan en el servidor sobre las entradas que puede ver. Combine términos con and / or.", + "logs.filter-help-title": "Sintaxis del filtro", + "logs.filter-placeholder": "Filtrar registros (p. ej. !Update o Status != Completed)", "logs.metadata": "Metadatos", + "logs.no-logs-for-filter": "Ningún registro coincide con el filtro actual.", "logs.no-logs-to-display": "No hay registros para mostrar.", "logs.page-description": "Esta es una lista de registros de acciones que han sido ejecutadas. Puede filtrar la lista por título de acción.", "logs.queue": "Cola", + "logs.queue-action-details": "Detalles de la acción", "logs.queue-default-group": "Predeterminado", "logs.queue-empty": "No hay ejecuciones activas o en espera en este momento.", "logs.queue-entity": "Entidad", @@ -232,7 +248,7 @@ "diagnostics.found-key": "Chiave trovata", "diagnostics.generate-browser-info": "Genera informazioni del browser", "diagnostics.generate-server-diagnostics": "Genera diagnostica del server", - "diagnostics.get-support": "Ottenere supporto", + "diagnostics.get-support": "Ottenere supporto Ottenere supporto", "diagnostics.get-support-description": "Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere la diagnostica del server da questa pagina.", "diagnostics.server-diagnostics": "Diagnostica del server", "diagnostics.server-diagnostics-description": "Questa sezione ti consente di generare un rapporto dettagliato della tua configurazione e ambiente. È una buona idea includere questo quando si presenta una richiesta di supporto.", @@ -262,10 +278,18 @@ "logs.clear-filter": "Cancella filtro di ricerca", "logs.completed": "Completato", "logs.exit-code": "Codice di uscita", + "logs.filter-error": "Impossibile applicare l'espressione di filtro.", + "logs.filter-help-examples": "Esempi: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed", + "logs.filter-help-fields": "Campi: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operatori: ==, !=, contains. Un prefisso ! su una singola parola esclude le voci corrispondenti.", + "logs.filter-help-intro": "I filtri vengono eseguiti sul server sulle voci che sei autorizzato a visualizzare. Combina i termini con and / or.", + "logs.filter-help-title": "Sintassi del filtro", + "logs.filter-placeholder": "Filtra i registri (es. !Update o Status != Completed)", "logs.metadata": "Metadati", + "logs.no-logs-for-filter": "Nessun registro corrisponde al filtro corrente.", "logs.no-logs-to-display": "Non ci sono registri da mostrare.", "logs.page-description": "Questa è una lista di registri delle azioni che sono state eseguite. Puoi filtrare la lista per titolo dell'azione.", "logs.queue": "Coda", + "logs.queue-action-details": "Dettagli azione", "logs.queue-default-group": "Predefinito", "logs.queue-empty": "Non ci sono esecuzioni attive o in attesa al momento.", "logs.queue-entity": "Entità", @@ -303,7 +327,7 @@ "diagnostics.found-key": "找到密钥", "diagnostics.generate-browser-info": "生成浏览器信息", "diagnostics.generate-server-diagnostics": "生成服务器诊断", - "diagnostics.get-support": "获取支持", + "diagnostics.get-support": "获取支持 获取支持", "diagnostics.get-support-description": "如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。", "diagnostics.server-diagnostics": "服务器诊断", "diagnostics.server-diagnostics-description": "此部分允许您生成配置和环境的详细报告。在提交支持请求时包含此信息是个好主意。", @@ -333,10 +357,18 @@ "logs.clear-filter": "清除搜索筛选器", "logs.completed": "完成", "logs.exit-code": "退出代码", + "logs.filter-error": "无法应用过滤表达式。", + "logs.filter-help-examples": "示例:backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed", + "logs.filter-help-fields": "字段:Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode。运算符:==, !=, contains。在单个单词前加上 ! 前缀可排除匹配的条目。", + "logs.filter-help-intro": "过滤器会在服务器端针对您有权限查看的条目进行筛选。您可以使用 and / or 来组合搜索条件。", + "logs.filter-help-title": "过滤语法", + "logs.filter-placeholder": "过滤日志(例如 !Update 或 Status != Completed)", "logs.metadata": "元数据", + "logs.no-logs-for-filter": "没有符合当前过滤条件的日志。", "logs.no-logs-to-display": "没有日志可显示。", "logs.page-description": "这是一个动作执行日志列表。您可以按动作标题过滤列表。", "logs.queue": "队列", + "logs.queue-action-details": "动作详细信息", "logs.queue-default-group": "默认", "logs.queue-empty": "当前没有正在运行或等待中的执行。", "logs.queue-entity": "实体", @@ -363,6 +395,85 @@ "theme-dialog.default": "默认主题", "theme-dialog.title": "选择主题", "welcome": "欢迎使用 OliveTin" + }, + "zh-Hant-TW": { + "connected": "已連線", + "diagnostics.browser-info": "瀏覽器資訊", + "diagnostics.browser-info-description": "此區塊可讓您產生瀏覽器資訊的詳細報告。這在排解特定瀏覽器問題時相當有幫助。", + "diagnostics.copied": "已複製!", + "diagnostics.copy-to-clipboard": "複製到剪貼簿", + "diagnostics.found-config": "找到設定檔 (Config)", + "diagnostics.found-key": "找到金鑰 (Key)", + "diagnostics.generate-browser-info": "產生瀏覽器資訊", + "diagnostics.generate-server-diagnostics": "產生伺服器診斷資訊", + "diagnostics.get-support": "尋求技術支援", + "diagnostics.get-support-description": "如果您在使用 OliveTin 時遇到問題並希望提出支援請求,提供此頁面的「伺服器診斷資訊」將會有很大的幫助。", + "diagnostics.server-diagnostics": "伺服器診斷", + "diagnostics.server-diagnostics-description": "此區塊可讓您產生詳細的設定與環境報告。建議在提出支援請求時附上此報告。", + "diagnostics.server-diagnostics-docs": "伺服器診斷官方文件", + "diagnostics.ssh": "SSH", + "diagnostics.unknown": "未知", + "diagnostics.useragent-data-error": "取得 userAgentData 時發生錯誤", + "diagnostics.where-to-find-help": "哪裡可以尋求協助", + "disconnected": "已斷線", + "disconnected-banner-announcement": "系統事件 WebSocket 已斷線。", + "disconnected-banner-link-text": "系統事件 WebSocket 已斷線", + "disconnected-banner-suffix": " 自 {disconnectedSince} 起。將在 {reconnectIn} 後嘗試重新連線。", + "disconnected-banner-suffix-reconnecting": " 自 {disconnectedSince} 起。嘗試重新連線中…", + "docs": "官方文件", + "language-dialog.browser-languages": "瀏覽器語言", + "language-dialog.close": "關閉", + "language-dialog.not-available": "無法使用", + "language-dialog.title": "選擇語言", + "login-button": "登入", + "logs.action": "動作", + "logs.action-group-limits": "並行數:{concurrent},佇列大小:{queueSize}", + "logs.back-to-list": "返回清單", + "logs.blocked": "已阻擋", + "logs.calendar": "日曆", + "logs.calendar-title": "日誌日曆", + "logs.clear-date-filter": "清除日期過濾條件", + "logs.clear-filter": "清除搜尋過濾條件", + "logs.completed": "已完成", + "logs.exit-code": "結束代碼", + "logs.filter-error": "無法套用過濾運算式。", + "logs.filter-help-examples": "範例:backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed", + "logs.filter-help-fields": "欄位:Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode。運算子:==, !=, contains。在單一單字前加上 ! 前綴可排除相符的項目。", + "logs.filter-help-intro": "過濾器會在伺服器端針對您有權限檢視的項目進行篩選。您可以使用 and / or 來組合搜尋條件。", + "logs.filter-help-title": "過濾語法", + "logs.filter-placeholder": "過濾日誌 (例如 !Update 或 Status != Completed)", + "logs.metadata": "中繼資料", + "logs.no-logs-for-filter": "沒有符合目前過濾條件的日誌。", + "logs.no-logs-to-display": "目前沒有可顯示的日誌。", + "logs.page-description": "這是已執行動作的日誌清單。請使用過濾條件欄位來搜尋關鍵字與運算式。", + "logs.queue": "佇列", + "logs.queue-action-details": "動作詳細資訊", + "logs.queue-default-group": "預設", + "logs.queue-empty": "目前沒有使用中或等待中的執行項目。", + "logs.queue-entity": "實體", + "logs.queue-group-active": "{active} 個使用中 (最大值 {max})", + "logs.queue-group-active-unlimited": "{active} 個使用中", + "logs.queue-page-description": "依動作群組分類的使用中與等待中執行項目。您無權檢視的項目將會被隱藏。", + "logs.queue-position": "#{position}", + "logs.queue-running": "執行中", + "logs.queue-title": "執行佇列", + "logs.queue-waiting": "等待中", + "logs.status": "狀態", + "logs.timed-out": "執行逾時", + "logs.timestamp": "時間戳記", + "logs.title": "日誌", + "nav.actions": "動作", + "nav.diagnostics": "診斷", + "nav.entities": "實體", + "nav.logs": "日誌", + "raise-issue": "在 GitHub 上建立 Issue", + "reconnecting": "重新連線中…", + "return-to-index": "返回首頁", + "search-filter": "過濾目前頁面", + "theme-dialog.close": "關閉", + "theme-dialog.default": "預設佈景主題", + "theme-dialog.title": "選擇佈景主題", + "welcome": "歡迎使用 OliveTin" } } } diff --git a/lang/de-DE.yaml b/lang/de-DE.yaml index c585fc3..813e533 100644 --- a/lang/de-DE.yaml +++ b/lang/de-DE.yaml @@ -17,6 +17,13 @@ translations: docs: Dokumentation logs.title: Protokolle logs.page-description: Dies ist eine Liste von Protokollen von Aktionen, die ausgeführt wurden. Sie können die Liste nach Aktionstitel filtern. + logs.filter-placeholder: Protokolle filtern (z. B. !Update oder Status != Completed) + logs.filter-help-title: Filtersyntax + logs.filter-help-intro: Filter laufen auf dem Server über Einträge, die Sie anzeigen dürfen. Kombinieren Sie Begriffe mit and / or. + logs.filter-help-fields: "Felder: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operatoren: ==, !=, contains. Ein !-Präfix vor einem einzelnen Wort schließt passende Einträge aus." + logs.filter-help-examples: "Beispiele: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed" + logs.filter-error: Filterausdruck konnte nicht angewendet werden. + logs.no-logs-for-filter: Keine Protokolle entsprechen dem aktuellen Filter. logs.timestamp: Zeitstempel logs.action: Aktion logs.metadata: Metadaten @@ -43,7 +50,8 @@ translations: logs.queue-running: Läuft logs.queue-position: "#{position}" logs.queue-entity: Entität - diagnostics.get-support: Unterstützung erhalten + logs.queue-action-details: Aktionsdetails + diagnostics.get-support: Unterstützung erhalten Unterstützung erhalten diagnostics.get-support-description: Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, Server-Diagnostik von dieser Seite einzufügen. diagnostics.where-to-find-help: Wo Sie Hilfe finden diagnostics.ssh: SSH diff --git a/lang/es-ES.yaml b/lang/es-ES.yaml index d8ef226..bb834aa 100644 --- a/lang/es-ES.yaml +++ b/lang/es-ES.yaml @@ -17,6 +17,13 @@ translations: docs: Documentación logs.title: Registros logs.page-description: Esta es una lista de registros de acciones que han sido ejecutadas. Puede filtrar la lista por título de acción. + logs.filter-placeholder: Filtrar registros (p. ej. !Update o Status != Completed) + logs.filter-help-title: Sintaxis del filtro + logs.filter-help-intro: Los filtros se ejecutan en el servidor sobre las entradas que puede ver. Combine términos con and / or. + logs.filter-help-fields: "Campos: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operadores: ==, !=, contains. Un prefijo ! en una sola palabra excluye las entradas coincidentes." + logs.filter-help-examples: "Ejemplos: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed" + logs.filter-error: No se pudo aplicar la expresión del filtro. + logs.no-logs-for-filter: Ningún registro coincide con el filtro actual. logs.timestamp: Marca de tiempo logs.action: Acción logs.metadata: Metadatos @@ -43,7 +50,8 @@ translations: logs.queue-running: En ejecución logs.queue-position: "#{position}" logs.queue-entity: Entidad - diagnostics.get-support: Obtener soporte + logs.queue-action-details: Detalles de la acción + diagnostics.get-support: Obtener soporte Obtener soporte diagnostics.get-support-description: Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir diagnósticos del servidor desde esta página. diagnostics.where-to-find-help: Dónde encontrar ayuda diagnostics.ssh: SSH diff --git a/lang/it-IT.yaml b/lang/it-IT.yaml index 37e87cf..d7f54cf 100644 --- a/lang/it-IT.yaml +++ b/lang/it-IT.yaml @@ -17,6 +17,13 @@ translations: raise-issue: Segnala un problema su GitHub logs.title: Registri logs.page-description: Questa è una lista di registri delle azioni che sono state eseguite. Puoi filtrare la lista per titolo dell'azione. + logs.filter-placeholder: Filtra i registri (es. !Update o Status != Completed) + logs.filter-help-title: Sintassi del filtro + logs.filter-help-intro: I filtri vengono eseguiti sul server sulle voci che sei autorizzato a visualizzare. Combina i termini con and / or. + logs.filter-help-fields: "Campi: Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode. Operatori: ==, !=, contains. Un prefisso ! su una singola parola esclude le voci corrispondenti." + logs.filter-help-examples: "Esempi: backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed" + logs.filter-error: Impossibile applicare l'espressione di filtro. + logs.no-logs-for-filter: Nessun registro corrisponde al filtro corrente. logs.timestamp: Date e ora logs.action: Azione logs.metadata: Metadati @@ -43,7 +50,8 @@ translations: logs.queue-running: In esecuzione logs.queue-position: "#{position}" logs.queue-entity: Entità - diagnostics.get-support: Ottenere supporto + logs.queue-action-details: Dettagli azione + diagnostics.get-support: Ottenere supporto Ottenere supporto diagnostics.get-support-description: Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere la diagnostica del server da questa pagina. diagnostics.where-to-find-help: Dove trovare aiuto diagnostics.ssh: SSH diff --git a/lang/zh-Hans-CN.yaml b/lang/zh-Hans-CN.yaml index b65fa18..23ac07d 100644 --- a/lang/zh-Hans-CN.yaml +++ b/lang/zh-Hans-CN.yaml @@ -17,6 +17,13 @@ translations: docs: 文档 logs.title: 日志 logs.page-description: 这是一个动作执行日志列表。您可以按动作标题过滤列表。 + logs.filter-placeholder: 过滤日志(例如 !Update 或 Status != Completed) + logs.filter-help-title: 过滤语法 + logs.filter-help-intro: 过滤器会在服务器端针对您有权限查看的条目进行筛选。您可以使用 and / or 来组合搜索条件。 + logs.filter-help-fields: "字段:Status, Action, User, Output, Blocked, TimedOut, Running, ExitCode。运算符:==, !=, contains。在单个单词前加上 ! 前缀可排除匹配的条目。" + logs.filter-help-examples: "示例:backup · !Update · Status != Completed · Status == Blocked · Action contains backup and Status == Completed" + logs.filter-error: 无法应用过滤表达式。 + logs.no-logs-for-filter: 没有符合当前过滤条件的日志。 logs.timestamp: 时间戳 logs.action: 动作 logs.metadata: 元数据 @@ -52,7 +59,8 @@ translations: logs.queue-running: 运行中 logs.queue-position: "第 {position} 位" logs.queue-entity: 实体 - diagnostics.get-support: 获取支持 + logs.queue-action-details: 动作详细信息 + diagnostics.get-support: 获取支持 获取支持 diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。 diagnostics.where-to-find-help: 在哪里找到帮助 diagnostics.ssh: SSH From fe1ac20935665ee645858ced90d399c2c41ef1c9 Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 24 Jul 2026 15:27:02 +0100 Subject: [PATCH 05/17] feat: Show config issues in diagnostics --- .../advanced_configuration/diagnostics.adoc | 20 +- frontend/js/websocket.js | 11 +- frontend/package-lock.json | 519 ++++++++++-------- frontend/package.json | 18 +- .../gen/olivetin/api/v1/olivetin_pb.d.ts | 63 ++- .../gen/olivetin/api/v1/olivetin_pb.js | 37 +- frontend/resources/vue/App.vue | 9 +- .../resources/vue/views/DiagnosticsView.vue | 130 ++--- lang/combined_output.json | 66 +++ lang/de-DE.yaml | 11 + lang/en.yaml | 11 + lang/es-ES.yaml | 11 + lang/it-IT.yaml | 11 + lang/zh-Hans-CN.yaml | 11 + lang/zh-Hant-TW.yaml | 11 + proto/olivetin/api/v1/olivetin.proto | 13 + service/gen/olivetin/api/v1/olivetin.pb.go | 406 +++++++++----- service/internal/api/api.go | 2 + .../internal/api/api_config_issues_test.go | 197 +++++++ service/internal/api/config_issues.go | 75 +++ service/internal/config/config.go | 4 + service/internal/config/config_reloader.go | 41 +- service/internal/config/sanitize.go | 83 +-- service/internal/config/source_file.go | 39 ++ service/internal/config/source_file_test.go | 51 ++ service/internal/configcheck/rebuild.go | 469 ++++++++++++++++ service/internal/configcheck/rebuild_test.go | 201 +++++++ .../internal/configcheck/watcher_path_test.go | 46 ++ service/internal/configissues/issue.go | 33 ++ service/internal/configissues/store.go | 125 +++++ service/internal/configissues/store_test.go | 39 ++ service/internal/entities/entities.go | 38 +- service/internal/entities/load_state.go | 33 ++ .../executor/argument_defaults_test.go | 45 ++ service/internal/executor/executor_actions.go | 52 +- .../internal/filehelper/file_change_notify.go | 120 ++-- .../internal/filehelper/watcher_path_test.go | 63 +++ service/internal/oncalendarfile/calendar.go | 6 +- service/internal/onfileindir/fileindir.go | 12 +- service/internal/tpl/check_template_test.go | 17 + service/internal/tpl/templates.go | 19 + 41 files changed, 2534 insertions(+), 634 deletions(-) create mode 100644 service/internal/api/api_config_issues_test.go create mode 100644 service/internal/api/config_issues.go create mode 100644 service/internal/config/source_file.go create mode 100644 service/internal/config/source_file_test.go create mode 100644 service/internal/configcheck/rebuild.go create mode 100644 service/internal/configcheck/rebuild_test.go create mode 100644 service/internal/configcheck/watcher_path_test.go create mode 100644 service/internal/configissues/issue.go create mode 100644 service/internal/configissues/store.go create mode 100644 service/internal/configissues/store_test.go create mode 100644 service/internal/entities/load_state.go create mode 100644 service/internal/executor/argument_defaults_test.go create mode 100644 service/internal/filehelper/watcher_path_test.go create mode 100644 service/internal/tpl/check_template_test.go diff --git a/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc b/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc index 854ec96..b0861c8 100644 --- a/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc +++ b/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc @@ -1,11 +1,29 @@ = Diagnostics -OliveTin provides a built-in diagnostics page that can be used to help check how OliveTin is running and help to troubleshoot issues. It's mainly designed for checking SSH configuration at the moment. +OliveTin provides a built-in diagnostics page that can be used to help check how OliveTin is running and help to troubleshoot issues. This is a screenshot of the diagnostics page, which can be accessed by clicking the "Diagnostics" link in the navigation bar: image::advanced_configuration/diagnostics/diagnostics.png[] +== Configuration issues + +The Diagnostics page includes a **Configuration issues** section. OliveTin checks the loaded configuration for problems such as: + +* Filesystem watch paths that cannot be created (for example missing directories used by `execOnFileCreatedInDir`) +* Argument templates that fail to parse +* Missing entity files or empty entity data for entity-bound actions +* Invalid cron schedules +* Entity-bound actions that also use `execOnCron` (cron cannot bind an entity) +* Unset environment variables referenced from configuration +* Other sanitize-time warnings (for example unknown action groups) + +When one or more issues exist, the Diagnostics navigation link shows a count badge with the number of issues so operators notice without opening the page first. The list and badge refresh after configuration reload and after entity file changes. + +Issues that refer to a specific action are only shown to users who are allowed to view that action. The badge count uses the same per-user filtering. Issues that are not tied to an action (for example unset environment variables) remain visible to anyone who can open Diagnostics. + +Where possible, each issue includes the configuration source file that defined the related action or entity (for example a file under an `include` directory). + == Disabling Diagnostics The diagnostics page is enabled by default, but you can disable it by using the OliveTin xref::security/acl.adoc#_acls_and_policies_global[security policy configuration], using the defaults, or via an ACL. Examples are shown below for each of these methods. diff --git a/frontend/js/websocket.js b/frontend/js/websocket.js index 01397c6..f44c010 100644 --- a/frontend/js/websocket.js +++ b/frontend/js/websocket.js @@ -199,6 +199,11 @@ async function handleConfigChangedEvent (j) { window.dispatchEvent(j) } +async function handleEntityChangedEvent (j) { + await refreshInitAfterConfigChange() + window.dispatchEvent(j) +} + const eventCaseToTypeName = { entityChanged: 'EventEntityChanged', configChanged: 'EventConfigChanged', @@ -230,9 +235,13 @@ function handleEvent (msg) { case 'EventHeartbeat': break case 'EventOutputChunk': - case 'EventEntityChanged': window.dispatchEvent(j) break + case 'EventEntityChanged': + handleEntityChangedEvent(j).catch((err) => { + console.error('EventEntityChanged handler failed:', err) + }) + break case 'EventExecutionFinished': case 'EventExecutionStarted': window.dispatchEvent(j) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4916cf4..b4f3e44 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,25 +11,25 @@ "dependencies": { "@connectrpc/connect": "^2.1.2", "@connectrpc/connect-web": "^2.1.2", - "@hugeicons/core-free-icons": "^4.2.2", + "@hugeicons/core-free-icons": "^4.2.3", "@hugeicons/vue": "^1.0.7", - "@vitejs/plugin-vue": "^6.0.7", + "@vitejs/plugin-vue": "^6.0.8", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.18.2", + "picocrank": "^1.21.0", "standard": "^17.1.2", "unplugin-vue-components": "^32.1.0", - "vite": "^8.1.3", - "vue": "^3.5.39", - "vue-i18n": "^11.4.6", - "vue-router": "^5.1.0" + "vite": "^8.1.5", + "vue": "^3.5.40", + "vue-i18n": "^11.4.7", + "vue-router": "^5.2.0" }, "devDependencies": { - "eslint-plugin-vue": "^10.9.2", + "eslint-plugin-vue": "^10.10.0", "process": "^0.11.10", - "stylelint": "^17.14.0", + "stylelint": "^17.14.1", "stylelint-config-standard": "^40.0.0" }, "engines": { @@ -52,13 +52,13 @@ } }, "node_modules/@babel/generator": { - "version": "8.0.0-rc.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.5.tgz", - "integrity": "sha512-nFZPWz3FHIS7y6rMIVoa/WBwjdutfIaRJIBQjzn+t3RnecZoRNlGmGcyR2wb0T/IgSd50Kz/6dG8/LvMCRunjg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", + "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", "license": "MIT", "dependencies": { - "@babel/parser": "^8.0.0-rc.5", - "@babel/types": "^8.0.0-rc.5", + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", @@ -69,30 +69,30 @@ } }, "node_modules/@babel/generator/node_modules/@babel/helper-string-parser": { - "version": "8.0.0-rc.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.5.tgz", - "integrity": "sha512-sN7R8rBvDurfaziNfDEIjIntlazmlkCDGO4SNl2RJ3wRCn+QxspLV7hzYAE8WWVd2joVuT8sUxeePdLp2idI1A==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", "license": "MIT", "engines": { "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.0-rc.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.5.tgz", - "integrity": "sha512-ehJDxHvtbZ85RtX/L2fi0h9AGsBNqB5Euv1EB8RMAvGYvD+2X+QbpzzOpbklnNXO+WSZJNOaetw2BBj27xsWVg==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", "license": "MIT", "engines": { "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/generator/node_modules/@babel/parser": { - "version": "8.0.0-rc.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.5.tgz", - "integrity": "sha512-/Mfg83rK3+jsRbl4Vbd0jqxc6M1A1/WNFtgrowRM1unEsD3XcNnrBdMM0JWakd0/RN9lseQKwPduW1TiEwKOlQ==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz", + "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==", "license": "MIT", "dependencies": { - "@babel/types": "^8.0.0-rc.5" + "@babel/types": "^8.0.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -102,13 +102,13 @@ } }, "node_modules/@babel/generator/node_modules/@babel/types": { - "version": "8.0.0-rc.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.5.tgz", - "integrity": "sha512-JeSVu/m8x/zpp4CLjYHVNXuhEyOkhPXuxM8YOXjh6L4LlvQNKuUNOTo5KdBuKAcTDHw8DquToTaEkhsBqPXOaA==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^8.0.0-rc.5", - "@babel/helper-validator-identifier": "^8.0.0-rc.5" + "@babel/helper-string-parser": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4" }, "engines": { "node": "^22.18.0 || >=24.11.0" @@ -168,13 +168,13 @@ "peer": true }, "node_modules/@cacheable/memory": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz", - "integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", "dev": true, "license": "MIT", "dependencies": { - "@cacheable/utils": "^2.4.1", + "@cacheable/utils": "^2.5.0", "@keyv/bigmap": "^1.3.1", "hookified": "^1.15.1", "keyv": "^5.6.0" @@ -208,9 +208,9 @@ } }, "node_modules/@cacheable/utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", - "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", "dev": true, "license": "MIT", "dependencies": { @@ -295,9 +295,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", - "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", "dev": true, "funding": [ { @@ -883,9 +883,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -942,9 +942,9 @@ } }, "node_modules/@hugeicons/core-free-icons": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.2.tgz", - "integrity": "sha512-fjQW3p6cwoJmwK3oCnV8Z3PC5sHihDvr2jkAHax8cxqp62GaV/D7B/Rnao+JwicW8fmLZUQ6QrzWfoyMFOEQlQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.3.tgz", + "integrity": "sha512-fpiU0qFZkv4ABi23xJ2m4qNr0BBfuIaYjPboN8WDA6aj9D+OzXa7eykxKOpACZRuGbDz1U9S5DsNx1d5KNnsgA==", "license": "MIT" }, "node_modules/@hugeicons/vue": { @@ -998,14 +998,14 @@ "license": "MIT" }, "node_modules/@intlify/core-base": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.6.tgz", - "integrity": "sha512-EOeHO95XESK9IFHgHeZXunsM/WBAoCA0DlaWODvx14vKmetAuS97t+l6Xe9hTUqntPpF93vtVSjjUDafw3wXMw==", + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.7.tgz", + "integrity": "sha512-MSB/sBKwEWJTILvQIhg2rnIcwPpLayo3wGwvVA+dJTNeUBD9GoqQgAaSOLdI9iOPDHCm9YoVnLqpfzza98MpkQ==", "license": "MIT", "dependencies": { - "@intlify/devtools-types": "11.4.6", - "@intlify/message-compiler": "11.4.6", - "@intlify/shared": "11.4.6" + "@intlify/devtools-types": "11.4.7", + "@intlify/message-compiler": "11.4.7", + "@intlify/shared": "11.4.7" }, "engines": { "node": ">= 22" @@ -1015,13 +1015,13 @@ } }, "node_modules/@intlify/devtools-types": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.6.tgz", - "integrity": "sha512-wowQPpNem56b2d43IJmqbrzG2FeBKe5f/kUGlpNuBmXs6OSqncF8m1+1lxHuW8ISZJF0ma2RkW3iLkw0g0G4VA==", + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.7.tgz", + "integrity": "sha512-GSz+J+hqH+AEpAHIYya6fSufS30OaMnG39HiZX7DmGKi3+aaLvassCfsXENEc4Wr4m68q2YP0QdMdB3D9UeAXg==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.4.6", - "@intlify/shared": "11.4.6" + "@intlify/core-base": "11.4.7", + "@intlify/shared": "11.4.7" }, "engines": { "node": ">= 22" @@ -1031,12 +1031,12 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.6.tgz", - "integrity": "sha512-5nj3jULqeTAC1WovwMs1LQWgatTa2pM/rXN9T3XW8rdOtXW9ZF6/GLSNFTKDQmPLwclhPdgUWLJ/4w3fMeeC/Q==", + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.7.tgz", + "integrity": "sha512-bHxmh7n94N4N1evADeb7XTkc3jTw6Ki5biMFZVSX6Jmk+iehy8/maeH2XUsBI27rtKIK+Hzc6QnVAKggUwylKw==", "license": "MIT", "dependencies": { - "@intlify/shared": "11.4.6", + "@intlify/shared": "11.4.7", "source-map-js": "^1.0.2" }, "engines": { @@ -1047,9 +1047,9 @@ } }, "node_modules/@intlify/shared": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.6.tgz", - "integrity": "sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA==", + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.7.tgz", + "integrity": "sha512-OtjPZan3No2OZZFnMUiCVsXC6+j+XRwEywaFDk0AoayAbLuPesyDloXhJZLl9JUl5vHZeQUkYSbEA8VX+CWMjg==", "license": "MIT", "engines": { "node": ">= 22" @@ -1481,9 +1481,9 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", - "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", "license": "MIT", "dependencies": { "@rolldown/pluginutils": "^1.0.1" @@ -1497,9 +1497,9 @@ } }, "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==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.4.tgz", + "integrity": "sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==", "license": "MIT", "dependencies": { "@vue/compiler-sfc": "^3.5.22", @@ -1524,53 +1524,53 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", - "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", - "@vue/shared": "3.5.39", + "@vue/shared": "3.5.40", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", - "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", - "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", - "@vue/compiler-core": "3.5.39", - "@vue/compiler-dom": "3.5.39", - "@vue/compiler-ssr": "3.5.39", - "@vue/shared": "3.5.39", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.15", + "postcss": "^8.5.19", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", - "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" } }, "node_modules/@vue/devtools-api": { @@ -1580,71 +1580,69 @@ "license": "MIT" }, "node_modules/@vue/devtools-kit": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.2.tgz", - "integrity": "sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.5.tgz", + "integrity": "sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA==", "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^8.1.2", + "@vue/devtools-shared": "^8.1.5", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "node_modules/@vue/devtools-shared": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.2.tgz", - "integrity": "sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.5.tgz", + "integrity": "sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ==", "license": "MIT" }, "node_modules/@vue/reactivity": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", - "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.39" + "@vue/shared": "3.5.40" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", - "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", - "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.39", - "@vue/runtime-core": "3.5.39", - "@vue/shared": "3.5.39", + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", - "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.39", - "@vue/shared": "3.5.39" - }, - "peerDependencies": { - "vue": "3.5.39" + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" } }, "node_modules/@vue/shared": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", - "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", "license": "MIT" }, "node_modules/@xterm/addon-fit": { @@ -1974,9 +1972,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2018,14 +2016,14 @@ } }, "node_modules/cacheable": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz", - "integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", "dev": true, "license": "MIT", "dependencies": { - "@cacheable/memory": "^2.0.8", - "@cacheable/utils": "^2.4.1", + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", "hookified": "^1.15.0", "keyv": "^5.6.0", "qified": "^0.10.1" @@ -3015,18 +3013,18 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.9.2.tgz", - "integrity": "sha512-4g7ZP3pYcuqd7Zp0pzUKcos0W+RkjBz4EGdhJ92FcYk6v03Ti/GK5NwjgsjxHK+98eXDbHeK7VtX1az7/8doZA==", + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.10.0.tgz", + "integrity": "sha512-dL9x9rBHqqNcByWiLOHK6L0SB97V82/NC0cZRn9cXPjM7pCuWlpQQP9bFH4vjBv80ej1ZpzAkuD8zWH1o9bZbA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", + "@eslint-community/eslint-utils": "^4.9.1", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", - "postcss-selector-parser": "^7.1.0", - "semver": "^7.6.3", - "xml-name-validator": "^4.0.0" + "postcss-selector-parser": "^7.1.4", + "semver": "^7.8.5", + "xml-name-validator": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3232,9 +3230,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true, "funding": [ { @@ -3597,9 +3595,9 @@ } }, "node_modules/globby": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz", - "integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.2.tgz", + "integrity": "sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==", "dev": true, "license": "MIT", "dependencies": { @@ -3618,9 +3616,9 @@ } }, "node_modules/globby/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", "dev": true, "license": "MIT", "engines": { @@ -4311,9 +4309,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "funding": [ { "type": "github", @@ -4934,9 +4932,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -4966,6 +4964,12 @@ "node": ">=0.10.0" } }, + "node_modules/nostics": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz", + "integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==", + "license": "MIT" + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -5268,9 +5272,9 @@ "license": "ISC" }, "node_modules/picocrank": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.18.2.tgz", - "integrity": "sha512-YR1Y00KnEPjH6OCrijSxzTHoyHLhKrsPhkA7ubOkbvcUuD7wtVKbRxfPkjAkA838dGymjoG//iFca2oLnNT5tg==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.21.0.tgz", + "integrity": "sha512-KM2xyrHfgrKwAwFtyr/o4cuHYqSeTS5sNAOpSUBw/zFvTGPBUWP/K82cYXRU4Tuz7NoQD6D0VbwAs/Q+rvEjkA==", "license": "ISC", "dependencies": { "@hugeicons/core-free-icons": "^4.2.2", @@ -5400,9 +5404,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", "funding": [ { "type": "opencollective", @@ -5419,7 +5423,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6233,9 +6237,9 @@ } }, "node_modules/stylelint": { - "version": "17.14.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.0.tgz", - "integrity": "sha512-8xkHPpdqYryeIsOgfsYTmr6cIeC4nLYWk5S8BPxpodq8mIuepggkMljsHewWfuAjj/+qpRKou2QerhjMH3iasg==", + "version": "17.14.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.1.tgz", + "integrity": "sha512-xVQwyiuxALUBNB2fBe0tmNemg9KqLtdj3T64mioFDar79B2cU8LIyz+3KL6LdiHs9NkeNfwxpKSaIVOY8f112g==", "dev": true, "funding": [ { @@ -6251,7 +6255,7 @@ "dependencies": { "@csstools/css-calc": "^3.2.1", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.1.5", + "@csstools/css-syntax-patches-for-csstree": "^1.1.6", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", @@ -6263,9 +6267,9 @@ "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^11.1.3", + "file-entry-cache": "^11.1.5", "global-modules": "^2.0.0", - "globby": "^16.2.0", + "globby": "^16.2.1", "globjoin": "^0.1.4", "html-tags": "^5.1.0", "ignore": "^7.0.5", @@ -6275,12 +6279,12 @@ "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", - "postcss": "^8.5.15", + "postcss": "^8.5.16", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.4", "postcss-value-parser": "^4.2.0", "string-width": "^8.2.1", - "supports-hyperlinks": "^4.4.0", + "supports-hyperlinks": "^4.5.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^7.0.1" @@ -6355,23 +6359,23 @@ } }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.3.tgz", - "integrity": "sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^6.1.22" + "flat-cache": "^6.1.23" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.22", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.22.tgz", - "integrity": "sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==", + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", "dev": true, "license": "MIT", "dependencies": { - "cacheable": "^2.3.4", + "cacheable": "^2.5.0", "flatted": "^3.4.2", "hookified": "^1.15.0" } @@ -6431,9 +6435,9 @@ } }, "node_modules/supports-hyperlinks": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.4.0.tgz", - "integrity": "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz", + "integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==", "dev": true, "license": "MIT", "dependencies": { @@ -6750,27 +6754,67 @@ } }, "node_modules/unplugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", - "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz", + "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "picomatch": "^4.0.3", + "picomatch": "^4.0.4", "webpack-virtual-modules": "^0.6.2" }, "engines": { "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/unplugin-utils": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", - "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz", + "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==", "license": "MIT", "dependencies": { "pathe": "^2.0.3", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=20.19.0" @@ -6872,15 +6916,15 @@ } }, "node_modules/vite": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", - "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.16", - "rolldown": "~1.1.3", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "bin": { @@ -6949,9 +6993,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -6961,16 +7005,16 @@ } }, "node_modules/vue": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", - "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.39", - "@vue/compiler-sfc": "3.5.39", - "@vue/runtime-dom": "3.5.39", - "@vue/server-renderer": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" }, "peerDependencies": { "typescript": "*" @@ -7074,14 +7118,14 @@ } }, "node_modules/vue-i18n": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.6.tgz", - "integrity": "sha512-l0gE7Rfy0phCa5ChKYkOq543Wgd39BCK6hkktfr1Ed4D99oRkgPK9ffShASZdeC8OJxGfdWmpYoAaAH6iLEuIg==", + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.7.tgz", + "integrity": "sha512-j6RyshdPPzqLiMAUpnpvZGFPM+rRoWi14Sl5yTsquvoW0/56DWyvhAj2o9TO2YXGvb6teg8T0xrYO9jR3urvdw==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.4.6", - "@intlify/devtools-types": "11.4.6", - "@intlify/shared": "11.4.6", + "@intlify/core-base": "11.4.7", + "@intlify/devtools-types": "11.4.7", + "@intlify/shared": "11.4.7", "@vue/devtools-api": "^6.5.0" }, "engines": { @@ -7095,27 +7139,28 @@ } }, "node_modules/vue-router": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.1.0.tgz", - "integrity": "sha512-HAbiLzLEHQwxPgvsbOJDAwtavszEgLwri6XfyrsPECIFez8+59xc9LofWVdc/HEaSRT822lJ8H9Ns38VVond5g==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.2.0.tgz", + "integrity": "sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==", "license": "MIT", "dependencies": { - "@babel/generator": "^8.0.0-rc.4", - "@vue-macros/common": "^3.1.1", - "@vue/devtools-api": "^8.1.2", + "@babel/generator": "^8.0.0", + "@vue-macros/common": "^3.1.3", + "@vue/devtools-api": "^8.1.5", "ast-walker-scope": "^0.9.0", "chokidar": "^5.0.0", "json5": "^2.2.3", - "local-pkg": "^1.1.2", + "local-pkg": "^1.2.1", "magic-string": "^0.30.21", "mlly": "^1.8.2", "muggle-string": "^0.4.1", + "nostics": "^1.1.4", "pathe": "^2.0.3", - "picomatch": "^4.0.3", + "picomatch": "^4.0.5", "scule": "^1.3.0", - "tinyglobby": "^0.2.16", - "unplugin": "^3.0.0", - "unplugin-utils": "^0.3.1", + "tinyglobby": "^0.2.17", + "unplugin": "^3.3.0", + "unplugin-utils": "^0.3.2", "yaml": "^2.9.0" }, "funding": { @@ -7123,10 +7168,10 @@ }, "peerDependencies": { "@pinia/colada": ">=0.21.2", - "@vue/compiler-sfc": "^3.5.34", - "pinia": "^3.0.4", - "vite": "^7.0.0 || ^8.0.0", - "vue": "^3.5.34" + "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", + "pinia": "^3.0.4 || ^4.0.2", + "vite": "^7.3.0 || ^8.0.0", + "vue": "^3.5.34 || ^4.0.0" }, "peerDependenciesMeta": { "@pinia/colada": { @@ -7144,12 +7189,12 @@ } }, "node_modules/vue-router/node_modules/@vue/devtools-api": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.2.tgz", - "integrity": "sha512-vA0O112YqyDuNA1s7Yb2gCgToQ/OxOWiFDO5ThLCcDy0ldHnSd1dUTaSYhOldbqoNgumE4dxtGAoAaSUKUD1Zg==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.5.tgz", + "integrity": "sha512-YJipMVAKe5wT5CWf5kTYCaNV7NMNjFVxJkIkJaJ4W/nCxEBzlZzrOsYKeCymdCrFZmBS/+wTWFoUs3Jf/Q6XSQ==", "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^8.1.2" + "@vue/devtools-kit": "^8.1.5" } }, "node_modules/vue-router/node_modules/json5": { @@ -7165,9 +7210,9 @@ } }, "node_modules/vue-router/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -7320,13 +7365,13 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/yaml": { diff --git a/frontend/package.json b/frontend/package.json index e812077..e7cd836 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,9 +5,9 @@ "repository": "https://github.com/OliveTin/OliveTin", "source": "index.html", "devDependencies": { - "eslint-plugin-vue": "^10.9.2", + "eslint-plugin-vue": "^10.10.0", "process": "^0.11.10", - "stylelint": "^17.14.0", + "stylelint": "^17.14.1", "stylelint-config-standard": "^40.0.0" }, "scripts": { @@ -25,20 +25,20 @@ "dependencies": { "@connectrpc/connect": "^2.1.2", "@connectrpc/connect-web": "^2.1.2", - "@hugeicons/core-free-icons": "^4.2.2", + "@hugeicons/core-free-icons": "^4.2.3", "@hugeicons/vue": "^1.0.7", - "@vitejs/plugin-vue": "^6.0.7", + "@vitejs/plugin-vue": "^6.0.8", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.18.2", + "picocrank": "^1.21.0", "standard": "^17.1.2", "unplugin-vue-components": "^32.1.0", - "vite": "^8.1.3", - "vue": "^3.5.39", - "vue-i18n": "^11.4.6", - "vue-router": "^5.1.0" + "vite": "^8.1.5", + "vue": "^3.5.40", + "vue-i18n": "^11.4.7", + "vue-router": "^5.2.0" }, "engines": { "node": "^20.19.0 || >=22.12.0" 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 6450b63..0f482a3 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.12.1 +// @generated by protoc-gen-es v2.13.0 // @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3) /* eslint-disable */ @@ -1666,6 +1666,57 @@ export declare type GetDiagnosticsRequest = Message<"olivetin.api.v1.GetDiagnost */ export declare const GetDiagnosticsRequestSchema: GenMessage; +/** + * @generated from message olivetin.api.v1.ConfigIssue + */ +export declare type ConfigIssue = Message<"olivetin.api.v1.ConfigIssue"> & { + /** + * @generated from field: string severity = 1; + */ + severity: string; + + /** + * @generated from field: string code = 2; + */ + code: string; + + /** + * @generated from field: string message = 3; + */ + message: string; + + /** + * @generated from field: string action_id = 4; + */ + actionId: string; + + /** + * @generated from field: string action_title = 5; + */ + actionTitle: string; + + /** + * @generated from field: string argument_name = 6; + */ + argumentName: string; + + /** + * @generated from field: string source = 7; + */ + source: string; + + /** + * @generated from field: string config_file = 8; + */ + configFile: string; +}; + +/** + * Describes the message olivetin.api.v1.ConfigIssue. + * Use `create(ConfigIssueSchema)` to create a new message. + */ +export declare const ConfigIssueSchema: GenMessage; + /** * @generated from message olivetin.api.v1.GetDiagnosticsResponse */ @@ -1679,6 +1730,11 @@ export declare type GetDiagnosticsResponse = Message<"olivetin.api.v1.GetDiagnos * @generated from field: string SshFoundConfig = 2; */ SshFoundConfig: string; + + /** + * @generated from field: repeated olivetin.api.v1.ConfigIssue config_issues = 3; + */ + configIssues: ConfigIssue[]; }; /** @@ -1829,6 +1885,11 @@ export declare type InitResponse = Message<"olivetin.api.v1.InitResponse"> & { * @generated from field: bool show_navigate_on_start_icons = 25; */ showNavigateOnStartIcons: boolean; + + /** + * @generated from field: int32 config_issue_count = 26; + */ + configIssueCount: number; }; /** 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 825efe7..df4fcd9 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.12.1 +// @generated by protoc-gen-es v2.13.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("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLGBAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBQgASgJEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcEoECBAQESJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAki1AEKE0VudGl0eVJlbGF0ZWRBY3Rpb24SJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhJZChNwcmVmaWxsZWRfYXJndW1lbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkVudGl0eVJlbGF0ZWRBY3Rpb24uUHJlZmlsbGVkQXJndW1lbnRzRW50cnkaOQoXUHJlZmlsbGVkQXJndW1lbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASL/AQoGRW50aXR5Eg0KBXRpdGxlGAEgASgJEhIKCnVuaXF1ZV9rZXkYAiABKAkSDAoEdHlwZRgDIAEoCRITCgtkaXJlY3RvcmllcxgEIAMoCRIzCgZmaWVsZHMYBSADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5LkZpZWxkc0VudHJ5Ej0KD3JlbGF0ZWRfYWN0aW9ucxgGIAMoCzIkLm9saXZldGluLmFwaS52MS5FbnRpdHlSZWxhdGVkQWN0aW9uEgwKBGljb24YByABKAkaLQoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJUChRHZXREYXNoYm9hcmRSZXNwb25zZRINCgV0aXRsZRgBIAEoCRItCglkYXNoYm9hcmQYBCABKAsyGi5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkIl8KD0VmZmVjdGl2ZVBvbGljeRIYChBzaG93X2RpYWdub3N0aWNzGAEgASgIEhUKDXNob3dfbG9nX2xpc3QYAiABKAgSGwoTc2hvd192ZXJzaW9uX251bWJlchgDIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSKUAQoSU3RhcnRBY3Rpb25SZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSGgoSdW5pcXVlX3RyYWNraW5nX2lkGAMgASgJEhUKDWp1c3RpZmljYXRpb24YBCABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIn4KGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50EhUKDWp1c3RpZmljYXRpb24YAyABKAkiSgoaU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IiwKF1N0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSI5ChhTdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIjMKHlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiTwofU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiXgoOR2V0TG9nc1JlcXVlc3QSFAoMc3RhcnRfb2Zmc2V0GAEgASgDEhMKC2RhdGVfZmlsdGVyGAIgASgJEhEKCXBhZ2Vfc2l6ZRgDIAEoAxIOCgZmaWx0ZXIYBCABKAkilAQKCExvZ0VudHJ5EhgKEGRhdGV0aW1lX3N0YXJ0ZWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEg4KBm91dHB1dBgDIAEoCRIRCgl0aW1lZF9vdXQYBSABKAgSEQoJZXhpdF9jb2RlGAYgASgFEgwKBHVzZXIYByABKAkSEgoKdXNlcl9jbGFzcxgIIAEoCRITCgthY3Rpb25faWNvbhgJIAEoCRIMCgR0YWdzGAogAygJEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgLIAEoCRIZChFkYXRldGltZV9maW5pc2hlZBgMIAEoCRIZChFleGVjdXRpb25fc3RhcnRlZBgOIAEoCBIaChJleGVjdXRpb25fZmluaXNoZWQYDyABKAgSDwoHYmxvY2tlZBgQIAEoCBIWCg5kYXRldGltZV9pbmRleBgRIAEoAxIQCghjYW5fa2lsbBgSIAEoCBIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYEyABKAkSEgoKYmluZGluZ19pZBgUIAEoCRIOCgZxdWV1ZWQYFSABKAgSGAoQcXVldWVkX2Zvcl9ncm91cBgWIAEoCRIVCg1qdXN0aWZpY2F0aW9uGBcgASgJEjcKCWFyZ3VtZW50cxgYIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IpEBCg9HZXRMb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyI/ChRHZXRBY3Rpb25Mb2dzUmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSFAoMc3RhcnRfb2Zmc2V0GAIgASgDIpcBChVHZXRBY3Rpb25Mb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyIaChhHZXRFeGVjdXRpb25RdWV1ZVJlcXVlc3QixgEKFEV4ZWN1dGlvblF1ZXVlQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEhMKC2FjdGlvbl9pY29uGAMgASgJEhYKDm1heF9jb25jdXJyZW50GAQgASgFEhQKDGFjdGl2ZV9jb3VudBgFIAEoBRIVCg1lbnRpdHlfcHJlZml4GAYgASgJEioKB2VudHJpZXMYByADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiwQEKE0V4ZWN1dGlvblF1ZXVlR3JvdXASDAoEbmFtZRgBIAEoCRIMCgRpY29uGAIgASgJEhYKDm1heF9jb25jdXJyZW50GAMgASgFEhQKDGFjdGl2ZV9jb3VudBgEIAEoBRI2CgdhY3Rpb25zGAUgAygLMiUub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlQWN0aW9uEhQKDHF1ZXVlZF9jb3VudBgGIAEoBRISCgpxdWV1ZV9zaXplGAcgASgFImcKGUdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2USNAoGZ3JvdXBzGAEgAygLMiQub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlR3JvdXASFAoMdG90YWxfYWN0aXZlGAIgASgFImUKG1ZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBINCgV2YWx1ZRgBIAEoCRIMCgR0eXBlGAIgASgJEhIKCmJpbmRpbmdfaWQYAyABKAkSFQoNYXJndW1lbnRfbmFtZRgEIAEoCSJCChxWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlEg0KBXZhbGlkGAEgASgIEhMKC2Rlc2NyaXB0aW9uGAIgASgJIjYKFVdhdGNoRXhlY3V0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkiJgoUV2F0Y2hFeGVjdXRpb25VcGRhdGUSDgoGdXBkYXRlGAEgASgJIkoKFkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEhEKCWFjdGlvbl9pZBgCIAEoCSJhChlEYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkSDAoEcGF0aBgEIAEoCSKPAQoXRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Ig8KDVdob0FtSVJlcXVlc3QibAoOV2hvQW1JUmVzcG9uc2USGgoSYXV0aGVudGljYXRlZF91c2VyGAEgASgJEhEKCXVzZXJncm91cBgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIMCgRhY2xzGAQgAygJEgsKA3NpZBgFIAEoCSIaChhTZXJ2ZXJEaWFnbm9zdGljc1JlcXVlc3QiKgoZU2VydmVyRGlhZ25vc3RpY3NSZXNwb25zZRINCgVhbGVydBgBIAEoCSIRCg9EdW1wVmFyc1JlcXVlc3QilQEKEER1bXBWYXJzUmVzcG9uc2USDQoFYWxlcnQYASABKAkSQQoIY29udGVudHMYAiADKAsyLy5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZS5Db250ZW50c0VudHJ5Gi8KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI7CgxEZWJ1Z0JpbmRpbmcSFAoMYWN0aW9uX3RpdGxlGAEgASgJEhUKDWVudGl0eV9wcmVmaXgYAiABKAkiHgocRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdCLOAQodRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2USDQoFYWxlcnQYASABKAkSTgoIY29udGVudHMYAiADKAsyPC5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UuQ29udGVudHNFbnRyeRpOCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRIsCgV2YWx1ZRgCIAEoCzIdLm9saXZldGluLmFwaS52MS5EZWJ1Z0JpbmRpbmc6AjgBIhIKEEdldFJlYWR5elJlcXVlc3QiIwoRR2V0UmVhZHl6UmVzcG9uc2USDgoGc3RhdHVzGAEgASgJIhQKEkV2ZW50U3RyZWFtUmVxdWVzdCKZAwoTRXZlbnRTdHJlYW1SZXNwb25zZRI9Cg5lbnRpdHlfY2hhbmdlZBgCIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudEVudGl0eUNoYW5nZWRIABI9Cg5jb25maWdfY2hhbmdlZBgDIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudENvbmZpZ0NoYW5nZWRIABJFChJleGVjdXRpb25fZmluaXNoZWQYBCABKAsyJy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25GaW5pc2hlZEgAEkMKEWV4ZWN1dGlvbl9zdGFydGVkGAUgASgLMiYub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uU3RhcnRlZEgAEjkKDG91dHB1dF9jaHVuaxgGIAEoCzIhLm9saXZldGluLmFwaS52MS5FdmVudE91dHB1dENodW5rSAASNAoJaGVhcnRiZWF0GAcgASgLMh8ub2xpdmV0aW4uYXBpLnYxLkV2ZW50SGVhcnRiZWF0SABCBwoFZXZlbnQiQQoQRXZlbnRPdXRwdXRDaHVuaxIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGb3V0cHV0GAIgASgJIhQKEkV2ZW50RW50aXR5Q2hhbmdlZCIUChJFdmVudENvbmZpZ0NoYW5nZWQiEAoORXZlbnRIZWFydGJlYXQiRgoWRXZlbnRFeGVjdXRpb25GaW5pc2hlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiRQoVRXZlbnRFeGVjdXRpb25TdGFydGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIyChFLaWxsQWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkibQoSS2lsbEFjdGlvblJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZraWxsZWQYAiABKAgSGQoRYWxyZWFkeV9jb21wbGV0ZWQYAyABKAgSDQoFZm91bmQYBCABKAgiOwoVTG9jYWxVc2VyTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIikKFkxvY2FsVXNlckxvZ2luUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCInChNQYXNzd29yZEhhc2hSZXF1ZXN0EhAKCHBhc3N3b3JkGAEgASgJIiQKFFBhc3N3b3JkSGFzaFJlc3BvbnNlEgwKBGhhc2gYASABKAkiDwoNTG9nb3V0UmVxdWVzdCIQCg5Mb2dvdXRSZXNwb25zZSIXChVHZXREaWFnbm9zdGljc1JlcXVlc3QiRQoWR2V0RGlhZ25vc3RpY3NSZXNwb25zZRITCgtTc2hGb3VuZEtleRgBIAEoCRIWCg5Tc2hGb3VuZENvbmZpZxgCIAEoCSINCgtJbml0UmVxdWVzdCLrBQoMSW5pdFJlc3BvbnNlEhIKCnNob3dGb290ZXIYASABKAgSFgoOc2hvd05hdmlnYXRpb24YAiABKAgSFwoPc2hvd05ld1ZlcnNpb25zGAMgASgIEhgKEGF2YWlsYWJsZVZlcnNpb24YBCABKAkSFgoOY3VycmVudFZlcnNpb24YBSABKAkSEQoJcGFnZVRpdGxlGAYgASgJEh4KFnNlY3Rpb25OYXZpZ2F0aW9uU3R5bGUYByABKAkSGgoSZGVmYXVsdEljb25Gb3JCYWNrGAggASgJEhYKDmVuYWJsZUN1c3RvbUpzGAkgASgIEhQKDGF1dGhMb2dpblVybBgKIAEoCRIWCg5hdXRoTG9jYWxMb2dpbhgLIAEoCBIRCglzdHlsZU1vZHMYDCADKAkSOAoPb0F1dGgyUHJvdmlkZXJzGA0gAygLMh8ub2xpdmV0aW4uYXBpLnYxLk9BdXRoMlByb3ZpZGVyEjgKD2FkZGl0aW9uYWxMaW5rcxgOIAMoCzIfLm9saXZldGluLmFwaS52MS5BZGRpdGlvbmFsTGluaxIWCg5yb290RGFzaGJvYXJkcxgPIAMoCRIaChJhdXRoZW50aWNhdGVkX3VzZXIYECABKAkSIwobYXV0aGVudGljYXRlZF91c2VyX3Byb3ZpZGVyGBEgASgJEjoKEGVmZmVjdGl2ZV9wb2xpY3kYEiABKAsyIC5vbGl2ZXRpbi5hcGkudjEuRWZmZWN0aXZlUG9saWN5EhYKDmJhbm5lcl9tZXNzYWdlGBMgASgJEhIKCmJhbm5lcl9jc3MYFCABKAkSGAoQc2hvd19kaWFnbm9zdGljcxgVIAEoCBIVCg1zaG93X2xvZ19saXN0GBYgASgIEhYKDmxvZ2luX3JlcXVpcmVkGBcgASgIEhgKEGF2YWlsYWJsZV90aGVtZXMYGCADKAkSJAocc2hvd19uYXZpZ2F0ZV9vbl9zdGFydF9pY29ucxgZIAEoCCIsCg5BZGRpdGlvbmFsTGluaxINCgV0aXRsZRgBIAEoCRILCgN1cmwYAiABKAkiOgoOT0F1dGgyUHJvdmlkZXISDQoFdGl0bGUYASABKAkSDAoEaWNvbhgDIAEoCRILCgNrZXkYBCABKAkiLQoXR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCSKLAQoYR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlEicKBmFjdGlvbhgBIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SRgoSYmFja190b19kYXNoYm9hcmRzGAIgAygLMioub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQiWgoSR2V0RW50aXRpZXNSZXF1ZXN0EhMKC2VudGl0eV90eXBlGAEgASgJEg4KBmZpbHRlchgCIAEoCRIMCgRwYWdlGAMgASgFEhEKCXBhZ2Vfc2l6ZRgEIAEoBSJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uIsUBChBFbnRpdHlEZWZpbml0aW9uEg0KBXRpdGxlGAEgASgJEioKCWluc3RhbmNlcxgCIAMoCzIXLm9saXZldGluLmFwaS52MS5FbnRpdHkSGgoSdXNlZF9vbl9kYXNoYm9hcmRzGAMgAygJEgwKBGljb24YBCABKAkSMwoKcHJvcGVydGllcxgFIAMoCzIfLm9saXZldGluLmFwaS52MS5FbnRpdHlQcm9wZXJ0eRIXCg90b3RhbF9pbnN0YW5jZXMYBiABKAUiLQoORW50aXR5UHJvcGVydHkSDAoEbmFtZRgBIAEoCRINCgV0aXRsZRgCIAEoCSI0ChBHZXRFbnRpdHlSZXF1ZXN0EhIKCnVuaXF1ZV9rZXkYASABKAkSDAoEdHlwZRgCIAEoCSI1ChRSZXN0YXJ0QWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAky7hMKEk9saXZlVGluQXBpU2VydmljZRJdCgxHZXREYXNoYm9hcmQSJC5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVxdWVzdBolLm9saXZldGluLmFwaS52MS5HZXREYXNoYm9hcmRSZXNwb25zZSIAEloKC1N0YXJ0QWN0aW9uEiMub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlc3BvbnNlIgASbwoSU3RhcnRBY3Rpb25BbmRXYWl0Eioub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QaKy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2UiABJpChBTdGFydEFjdGlvbkJ5R2V0Eigub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0Gikub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRSZXNwb25zZSIAEn4KF1N0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0Ei8ub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBowLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlc3BvbnNlIgASXgoNUmVzdGFydEFjdGlvbhIlLm9saXZldGluLmFwaS52MS5SZXN0YXJ0QWN0aW9uUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlc3BvbnNlIgASVwoKS2lsbEFjdGlvbhIiLm9saXZldGluLmFwaS52MS5LaWxsQWN0aW9uUmVxdWVzdBojLm9saXZldGluLmFwaS52MS5LaWxsQWN0aW9uUmVzcG9uc2UiABJmCg9FeGVjdXRpb25TdGF0dXMSJy5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBooLm9saXZldGluLmFwaS52MS5FeGVjdXRpb25TdGF0dXNSZXNwb25zZSIAEk4KB0dldExvZ3MSHy5vbGl2ZXRpbi5hcGkudjEuR2V0TG9nc1JlcXVlc3QaIC5vbGl2ZXRpbi5hcGkudjEuR2V0TG9nc1Jlc3BvbnNlIgASYAoNR2V0QWN0aW9uTG9ncxIlLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25Mb2dzUmVxdWVzdBomLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25Mb2dzUmVzcG9uc2UiABJsChFHZXRFeGVjdXRpb25RdWV1ZRIpLm9saXZldGluLmFwaS52MS5HZXRFeGVjdXRpb25RdWV1ZVJlcXVlc3QaKi5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXNwb25zZSIAEnUKFFZhbGlkYXRlQXJndW1lbnRUeXBlEiwub2xpdmV0aW4uYXBpLnYxLlZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBotLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlIgASSwoGV2hvQW1JEh4ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuV2hvQW1JUmVzcG9uc2UiABJsChFTZXJ2ZXJEaWFnbm9zdGljcxIpLm9saXZldGluLmFwaS52MS5TZXJ2ZXJEaWFnbm9zdGljc1JlcXVlc3QaKi5vbGl2ZXRpbi5hcGkudjEuU2VydmVyRGlhZ25vc3RpY3NSZXNwb25zZSIAElEKCER1bXBWYXJzEiAub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVxdWVzdBohLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1Jlc3BvbnNlIgASeAoVRHVtcFB1YmxpY0lkQWN0aW9uTWFwEi0ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QaLi5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UiABJUCglHZXRSZWFkeXoSIS5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVxdWVzdBoiLm9saXZldGluLmFwaS52MS5HZXRSZWFkeXpSZXNwb25zZSIAEmMKDkxvY2FsVXNlckxvZ2luEiYub2xpdmV0aW4uYXBpLnYxLkxvY2FsVXNlckxvZ2luUmVxdWVzdBonLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlc3BvbnNlIgASXQoMUGFzc3dvcmRIYXNoEiQub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuUGFzc3dvcmRIYXNoUmVzcG9uc2UiABJLCgZMb2dvdXQSHi5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVxdWVzdBofLm9saXZldGluLmFwaS52MS5Mb2dvdXRSZXNwb25zZSIAElwKC0V2ZW50U3RyZWFtEiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50U3RyZWFtUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlc3BvbnNlIgAwARJjCg5HZXREaWFnbm9zdGljcxImLm9saXZldGluLmFwaS52MS5HZXREaWFnbm9zdGljc1JlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXNwb25zZSIAEkUKBEluaXQSHC5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlcXVlc3QaHS5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlc3BvbnNlIgASaQoQR2V0QWN0aW9uQmluZGluZxIoLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVxdWVzdBopLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2UiABJaCgtHZXRFbnRpdGllcxIjLm9saXZldGluLmFwaS52MS5HZXRFbnRpdGllc1JlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXNwb25zZSIAEkkKCUdldEVudGl0eRIhLm9saXZldGluLmFwaS52MS5HZXRFbnRpdHlSZXF1ZXN0Ghcub2xpdmV0aW4uYXBpLnYxLkVudGl0eSIAQjhaNmdpdGh1Yi5jb20vT2xpdmVUaW4vT2xpdmVUaW4vZ2VuL29saXZldGluL2FwaS92MTthcGl2MWIGcHJvdG8z"); + fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLGBAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBQgASgJEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcEoECBAQESJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAki1AEKE0VudGl0eVJlbGF0ZWRBY3Rpb24SJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhJZChNwcmVmaWxsZWRfYXJndW1lbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkVudGl0eVJlbGF0ZWRBY3Rpb24uUHJlZmlsbGVkQXJndW1lbnRzRW50cnkaOQoXUHJlZmlsbGVkQXJndW1lbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASL/AQoGRW50aXR5Eg0KBXRpdGxlGAEgASgJEhIKCnVuaXF1ZV9rZXkYAiABKAkSDAoEdHlwZRgDIAEoCRITCgtkaXJlY3RvcmllcxgEIAMoCRIzCgZmaWVsZHMYBSADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5LkZpZWxkc0VudHJ5Ej0KD3JlbGF0ZWRfYWN0aW9ucxgGIAMoCzIkLm9saXZldGluLmFwaS52MS5FbnRpdHlSZWxhdGVkQWN0aW9uEgwKBGljb24YByABKAkaLQoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJUChRHZXREYXNoYm9hcmRSZXNwb25zZRINCgV0aXRsZRgBIAEoCRItCglkYXNoYm9hcmQYBCABKAsyGi5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkIl8KD0VmZmVjdGl2ZVBvbGljeRIYChBzaG93X2RpYWdub3N0aWNzGAEgASgIEhUKDXNob3dfbG9nX2xpc3QYAiABKAgSGwoTc2hvd192ZXJzaW9uX251bWJlchgDIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSKUAQoSU3RhcnRBY3Rpb25SZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSGgoSdW5pcXVlX3RyYWNraW5nX2lkGAMgASgJEhUKDWp1c3RpZmljYXRpb24YBCABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIn4KGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50EhUKDWp1c3RpZmljYXRpb24YAyABKAkiSgoaU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IiwKF1N0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSI5ChhTdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIjMKHlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiTwofU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiXgoOR2V0TG9nc1JlcXVlc3QSFAoMc3RhcnRfb2Zmc2V0GAEgASgDEhMKC2RhdGVfZmlsdGVyGAIgASgJEhEKCXBhZ2Vfc2l6ZRgDIAEoAxIOCgZmaWx0ZXIYBCABKAkilAQKCExvZ0VudHJ5EhgKEGRhdGV0aW1lX3N0YXJ0ZWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEg4KBm91dHB1dBgDIAEoCRIRCgl0aW1lZF9vdXQYBSABKAgSEQoJZXhpdF9jb2RlGAYgASgFEgwKBHVzZXIYByABKAkSEgoKdXNlcl9jbGFzcxgIIAEoCRITCgthY3Rpb25faWNvbhgJIAEoCRIMCgR0YWdzGAogAygJEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgLIAEoCRIZChFkYXRldGltZV9maW5pc2hlZBgMIAEoCRIZChFleGVjdXRpb25fc3RhcnRlZBgOIAEoCBIaChJleGVjdXRpb25fZmluaXNoZWQYDyABKAgSDwoHYmxvY2tlZBgQIAEoCBIWCg5kYXRldGltZV9pbmRleBgRIAEoAxIQCghjYW5fa2lsbBgSIAEoCBIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYEyABKAkSEgoKYmluZGluZ19pZBgUIAEoCRIOCgZxdWV1ZWQYFSABKAgSGAoQcXVldWVkX2Zvcl9ncm91cBgWIAEoCRIVCg1qdXN0aWZpY2F0aW9uGBcgASgJEjcKCWFyZ3VtZW50cxgYIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IpEBCg9HZXRMb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyI/ChRHZXRBY3Rpb25Mb2dzUmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSFAoMc3RhcnRfb2Zmc2V0GAIgASgDIpcBChVHZXRBY3Rpb25Mb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyIaChhHZXRFeGVjdXRpb25RdWV1ZVJlcXVlc3QixgEKFEV4ZWN1dGlvblF1ZXVlQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEhMKC2FjdGlvbl9pY29uGAMgASgJEhYKDm1heF9jb25jdXJyZW50GAQgASgFEhQKDGFjdGl2ZV9jb3VudBgFIAEoBRIVCg1lbnRpdHlfcHJlZml4GAYgASgJEioKB2VudHJpZXMYByADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiwQEKE0V4ZWN1dGlvblF1ZXVlR3JvdXASDAoEbmFtZRgBIAEoCRIMCgRpY29uGAIgASgJEhYKDm1heF9jb25jdXJyZW50GAMgASgFEhQKDGFjdGl2ZV9jb3VudBgEIAEoBRI2CgdhY3Rpb25zGAUgAygLMiUub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlQWN0aW9uEhQKDHF1ZXVlZF9jb3VudBgGIAEoBRISCgpxdWV1ZV9zaXplGAcgASgFImcKGUdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2USNAoGZ3JvdXBzGAEgAygLMiQub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlR3JvdXASFAoMdG90YWxfYWN0aXZlGAIgASgFImUKG1ZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBINCgV2YWx1ZRgBIAEoCRIMCgR0eXBlGAIgASgJEhIKCmJpbmRpbmdfaWQYAyABKAkSFQoNYXJndW1lbnRfbmFtZRgEIAEoCSJCChxWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlEg0KBXZhbGlkGAEgASgIEhMKC2Rlc2NyaXB0aW9uGAIgASgJIjYKFVdhdGNoRXhlY3V0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkiJgoUV2F0Y2hFeGVjdXRpb25VcGRhdGUSDgoGdXBkYXRlGAEgASgJIkoKFkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEhEKCWFjdGlvbl9pZBgCIAEoCSJhChlEYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkSDAoEcGF0aBgEIAEoCSKPAQoXRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Ig8KDVdob0FtSVJlcXVlc3QibAoOV2hvQW1JUmVzcG9uc2USGgoSYXV0aGVudGljYXRlZF91c2VyGAEgASgJEhEKCXVzZXJncm91cBgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIMCgRhY2xzGAQgAygJEgsKA3NpZBgFIAEoCSIaChhTZXJ2ZXJEaWFnbm9zdGljc1JlcXVlc3QiKgoZU2VydmVyRGlhZ25vc3RpY3NSZXNwb25zZRINCgVhbGVydBgBIAEoCSIRCg9EdW1wVmFyc1JlcXVlc3QilQEKEER1bXBWYXJzUmVzcG9uc2USDQoFYWxlcnQYASABKAkSQQoIY29udGVudHMYAiADKAsyLy5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZS5Db250ZW50c0VudHJ5Gi8KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI7CgxEZWJ1Z0JpbmRpbmcSFAoMYWN0aW9uX3RpdGxlGAEgASgJEhUKDWVudGl0eV9wcmVmaXgYAiABKAkiHgocRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdCLOAQodRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2USDQoFYWxlcnQYASABKAkSTgoIY29udGVudHMYAiADKAsyPC5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UuQ29udGVudHNFbnRyeRpOCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRIsCgV2YWx1ZRgCIAEoCzIdLm9saXZldGluLmFwaS52MS5EZWJ1Z0JpbmRpbmc6AjgBIhIKEEdldFJlYWR5elJlcXVlc3QiIwoRR2V0UmVhZHl6UmVzcG9uc2USDgoGc3RhdHVzGAEgASgJIhQKEkV2ZW50U3RyZWFtUmVxdWVzdCKZAwoTRXZlbnRTdHJlYW1SZXNwb25zZRI9Cg5lbnRpdHlfY2hhbmdlZBgCIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudEVudGl0eUNoYW5nZWRIABI9Cg5jb25maWdfY2hhbmdlZBgDIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudENvbmZpZ0NoYW5nZWRIABJFChJleGVjdXRpb25fZmluaXNoZWQYBCABKAsyJy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25GaW5pc2hlZEgAEkMKEWV4ZWN1dGlvbl9zdGFydGVkGAUgASgLMiYub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uU3RhcnRlZEgAEjkKDG91dHB1dF9jaHVuaxgGIAEoCzIhLm9saXZldGluLmFwaS52MS5FdmVudE91dHB1dENodW5rSAASNAoJaGVhcnRiZWF0GAcgASgLMh8ub2xpdmV0aW4uYXBpLnYxLkV2ZW50SGVhcnRiZWF0SABCBwoFZXZlbnQiQQoQRXZlbnRPdXRwdXRDaHVuaxIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGb3V0cHV0GAIgASgJIhQKEkV2ZW50RW50aXR5Q2hhbmdlZCIUChJFdmVudENvbmZpZ0NoYW5nZWQiEAoORXZlbnRIZWFydGJlYXQiRgoWRXZlbnRFeGVjdXRpb25GaW5pc2hlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiRQoVRXZlbnRFeGVjdXRpb25TdGFydGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIyChFLaWxsQWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkibQoSS2lsbEFjdGlvblJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZraWxsZWQYAiABKAgSGQoRYWxyZWFkeV9jb21wbGV0ZWQYAyABKAgSDQoFZm91bmQYBCABKAgiOwoVTG9jYWxVc2VyTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIikKFkxvY2FsVXNlckxvZ2luUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCInChNQYXNzd29yZEhhc2hSZXF1ZXN0EhAKCHBhc3N3b3JkGAEgASgJIiQKFFBhc3N3b3JkSGFzaFJlc3BvbnNlEgwKBGhhc2gYASABKAkiDwoNTG9nb3V0UmVxdWVzdCIQCg5Mb2dvdXRSZXNwb25zZSIXChVHZXREaWFnbm9zdGljc1JlcXVlc3QiowEKC0NvbmZpZ0lzc3VlEhAKCHNldmVyaXR5GAEgASgJEgwKBGNvZGUYAiABKAkSDwoHbWVzc2FnZRgDIAEoCRIRCglhY3Rpb25faWQYBCABKAkSFAoMYWN0aW9uX3RpdGxlGAUgASgJEhUKDWFyZ3VtZW50X25hbWUYBiABKAkSDgoGc291cmNlGAcgASgJEhMKC2NvbmZpZ19maWxlGAggASgJInoKFkdldERpYWdub3N0aWNzUmVzcG9uc2USEwoLU3NoRm91bmRLZXkYASABKAkSFgoOU3NoRm91bmRDb25maWcYAiABKAkSMwoNY29uZmlnX2lzc3VlcxgDIAMoCzIcLm9saXZldGluLmFwaS52MS5Db25maWdJc3N1ZSINCgtJbml0UmVxdWVzdCKHBgoMSW5pdFJlc3BvbnNlEhIKCnNob3dGb290ZXIYASABKAgSFgoOc2hvd05hdmlnYXRpb24YAiABKAgSFwoPc2hvd05ld1ZlcnNpb25zGAMgASgIEhgKEGF2YWlsYWJsZVZlcnNpb24YBCABKAkSFgoOY3VycmVudFZlcnNpb24YBSABKAkSEQoJcGFnZVRpdGxlGAYgASgJEh4KFnNlY3Rpb25OYXZpZ2F0aW9uU3R5bGUYByABKAkSGgoSZGVmYXVsdEljb25Gb3JCYWNrGAggASgJEhYKDmVuYWJsZUN1c3RvbUpzGAkgASgIEhQKDGF1dGhMb2dpblVybBgKIAEoCRIWCg5hdXRoTG9jYWxMb2dpbhgLIAEoCBIRCglzdHlsZU1vZHMYDCADKAkSOAoPb0F1dGgyUHJvdmlkZXJzGA0gAygLMh8ub2xpdmV0aW4uYXBpLnYxLk9BdXRoMlByb3ZpZGVyEjgKD2FkZGl0aW9uYWxMaW5rcxgOIAMoCzIfLm9saXZldGluLmFwaS52MS5BZGRpdGlvbmFsTGluaxIWCg5yb290RGFzaGJvYXJkcxgPIAMoCRIaChJhdXRoZW50aWNhdGVkX3VzZXIYECABKAkSIwobYXV0aGVudGljYXRlZF91c2VyX3Byb3ZpZGVyGBEgASgJEjoKEGVmZmVjdGl2ZV9wb2xpY3kYEiABKAsyIC5vbGl2ZXRpbi5hcGkudjEuRWZmZWN0aXZlUG9saWN5EhYKDmJhbm5lcl9tZXNzYWdlGBMgASgJEhIKCmJhbm5lcl9jc3MYFCABKAkSGAoQc2hvd19kaWFnbm9zdGljcxgVIAEoCBIVCg1zaG93X2xvZ19saXN0GBYgASgIEhYKDmxvZ2luX3JlcXVpcmVkGBcgASgIEhgKEGF2YWlsYWJsZV90aGVtZXMYGCADKAkSJAocc2hvd19uYXZpZ2F0ZV9vbl9zdGFydF9pY29ucxgZIAEoCBIaChJjb25maWdfaXNzdWVfY291bnQYGiABKAUiLAoOQWRkaXRpb25hbExpbmsSDQoFdGl0bGUYASABKAkSCwoDdXJsGAIgASgJIjoKDk9BdXRoMlByb3ZpZGVyEg0KBXRpdGxlGAEgASgJEgwKBGljb24YAyABKAkSCwoDa2V5GAQgASgJIi0KF0dldEFjdGlvbkJpbmRpbmdSZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkiiwEKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0IloKEkdldEVudGl0aWVzUmVxdWVzdBITCgtlbnRpdHlfdHlwZRgBIAEoCRIOCgZmaWx0ZXIYAiABKAkSDAoEcGFnZRgDIAEoBRIRCglwYWdlX3NpemUYBCABKAUiVAoTR2V0RW50aXRpZXNSZXNwb25zZRI9ChJlbnRpdHlfZGVmaW5pdGlvbnMYASADKAsyIS5vbGl2ZXRpbi5hcGkudjEuRW50aXR5RGVmaW5pdGlvbiLFAQoQRW50aXR5RGVmaW5pdGlvbhINCgV0aXRsZRgBIAEoCRIqCglpbnN0YW5jZXMYAiADKAsyFy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5EhoKEnVzZWRfb25fZGFzaGJvYXJkcxgDIAMoCRIMCgRpY29uGAQgASgJEjMKCnByb3BlcnRpZXMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5UHJvcGVydHkSFwoPdG90YWxfaW5zdGFuY2VzGAYgASgFIi0KDkVudGl0eVByb3BlcnR5EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMu4TChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASbAoRR2V0RXhlY3V0aW9uUXVldWUSKS5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2UiABJ1ChRWYWxpZGF0ZUFyZ3VtZW50VHlwZRIsLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QaLS5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZSIAEksKBldob0FtSRIeLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlc3BvbnNlIgASbAoRU2VydmVyRGlhZ25vc3RpY3MSKS5vbGl2ZXRpbi5hcGkudjEuU2VydmVyRGlhZ25vc3RpY3NSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLlNlcnZlckRpYWdub3N0aWNzUmVzcG9uc2UiABJRCghEdW1wVmFycxIgLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1JlcXVlc3QaIS5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZSIAEngKFUR1bXBQdWJsaWNJZEFjdGlvbk1hcBItLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Gi4ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlIgASVAoJR2V0UmVhZHl6EiEub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVzcG9uc2UiABJjCg5Mb2NhbFVzZXJMb2dpbhImLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXNwb25zZSIAEl0KDFBhc3N3b3JkSGFzaBIkLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlc3BvbnNlIgASSwoGTG9nb3V0Eh4ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVzcG9uc2UiABJcCgtFdmVudFN0cmVhbRIjLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXNwb25zZSIAMAESYwoOR2V0RGlhZ25vc3RpY3MSJi5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVzcG9uc2UiABJFCgRJbml0Ehwub2xpdmV0aW4uYXBpLnYxLkluaXRSZXF1ZXN0Gh0ub2xpdmV0aW4uYXBpLnYxLkluaXRSZXNwb25zZSIAEmkKEEdldEFjdGlvbkJpbmRpbmcSKC5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlIgASWgoLR2V0RW50aXRpZXMSIy5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVzcG9uc2UiABJJCglHZXRFbnRpdHkSIS5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXR5UmVxdWVzdBoXLm9saXZldGluLmFwaS52MS5FbnRpdHkiAEI4WjZnaXRodWIuY29tL09saXZlVGluL09saXZlVGluL2dlbi9vbGl2ZXRpbi9hcGkvdjE7YXBpdjFiBnByb3RvMw"); /** * Describes the message olivetin.api.v1.Action. @@ -465,96 +465,103 @@ export const LogoutResponseSchema = /*@__PURE__*/ export const GetDiagnosticsRequestSchema = /*@__PURE__*/ messageDesc(file_olivetin_api_v1_olivetin, 64); +/** + * Describes the message olivetin.api.v1.ConfigIssue. + * Use `create(ConfigIssueSchema)` to create a new message. + */ +export const ConfigIssueSchema = /*@__PURE__*/ + messageDesc(file_olivetin_api_v1_olivetin, 65); + /** * Describes the message olivetin.api.v1.GetDiagnosticsResponse. * Use `create(GetDiagnosticsResponseSchema)` to create a new message. */ export const GetDiagnosticsResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 65); + messageDesc(file_olivetin_api_v1_olivetin, 66); /** * Describes the message olivetin.api.v1.InitRequest. * Use `create(InitRequestSchema)` to create a new message. */ export const InitRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 66); + messageDesc(file_olivetin_api_v1_olivetin, 67); /** * Describes the message olivetin.api.v1.InitResponse. * Use `create(InitResponseSchema)` to create a new message. */ export const InitResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 67); + messageDesc(file_olivetin_api_v1_olivetin, 68); /** * Describes the message olivetin.api.v1.AdditionalLink. * Use `create(AdditionalLinkSchema)` to create a new message. */ export const AdditionalLinkSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 68); + messageDesc(file_olivetin_api_v1_olivetin, 69); /** * Describes the message olivetin.api.v1.OAuth2Provider. * Use `create(OAuth2ProviderSchema)` to create a new message. */ export const OAuth2ProviderSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 69); + messageDesc(file_olivetin_api_v1_olivetin, 70); /** * Describes the message olivetin.api.v1.GetActionBindingRequest. * Use `create(GetActionBindingRequestSchema)` to create a new message. */ export const GetActionBindingRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 70); + messageDesc(file_olivetin_api_v1_olivetin, 71); /** * Describes the message olivetin.api.v1.GetActionBindingResponse. * Use `create(GetActionBindingResponseSchema)` to create a new message. */ export const GetActionBindingResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 71); + messageDesc(file_olivetin_api_v1_olivetin, 72); /** * Describes the message olivetin.api.v1.GetEntitiesRequest. * Use `create(GetEntitiesRequestSchema)` to create a new message. */ export const GetEntitiesRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 72); + messageDesc(file_olivetin_api_v1_olivetin, 73); /** * Describes the message olivetin.api.v1.GetEntitiesResponse. * Use `create(GetEntitiesResponseSchema)` to create a new message. */ export const GetEntitiesResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 73); + messageDesc(file_olivetin_api_v1_olivetin, 74); /** * Describes the message olivetin.api.v1.EntityDefinition. * Use `create(EntityDefinitionSchema)` to create a new message. */ export const EntityDefinitionSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 74); + messageDesc(file_olivetin_api_v1_olivetin, 75); /** * Describes the message olivetin.api.v1.EntityProperty. * Use `create(EntityPropertySchema)` to create a new message. */ export const EntityPropertySchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 75); + messageDesc(file_olivetin_api_v1_olivetin, 76); /** * Describes the message olivetin.api.v1.GetEntityRequest. * Use `create(GetEntityRequestSchema)` to create a new message. */ export const GetEntityRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 76); + messageDesc(file_olivetin_api_v1_olivetin, 77); /** * Describes the message olivetin.api.v1.RestartActionRequest. * Use `create(RestartActionRequestSchema)` to create a new message. */ export const RestartActionRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 77); + messageDesc(file_olivetin_api_v1_olivetin, 78); /** * @generated from service olivetin.api.v1.OliveTinApiService diff --git a/frontend/resources/vue/App.vue b/frontend/resources/vue/App.vue index 709f27c..cc3a388 100644 --- a/frontend/resources/vue/App.vue +++ b/frontend/resources/vue/App.vue @@ -345,8 +345,8 @@ function renderNavigation () { const rootDashboards = window.initResponse?.rootDashboards || [] - if (typeof navigation.value.clear === 'function') { - navigation.value.clear() + if (typeof navigation.value.clearNavigationLinks === 'function') { + navigation.value.clearNavigationLinks() } for (const rootDashboard of rootDashboards) { @@ -367,7 +367,10 @@ function renderNavigation () { } if (showDiagnostics.value) { - navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics')) + const issueCount = window.initResponse?.configIssueCount || 0 + navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'), { + count: issueCount + }) } } diff --git a/frontend/resources/vue/views/DiagnosticsView.vue b/frontend/resources/vue/views/DiagnosticsView.vue index ab80c7b..e66f78e 100644 --- a/frontend/resources/vue/views/DiagnosticsView.vue +++ b/frontend/resources/vue/views/DiagnosticsView.vue @@ -1,4 +1,28 @@ - - diff --git a/lang/combined_output.json b/lang/combined_output.json index cffc185..ede7c13 100644 --- a/lang/combined_output.json +++ b/lang/combined_output.json @@ -5,6 +5,17 @@ "connected": "Verbunden", "diagnostics.browser-info": "Browser-Informationen", "diagnostics.browser-info-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Browser-Informationen zu erstellen. Dies kann bei der Fehlerbehebung von browser-spezifischen Problemen hilfreich sein.", + "diagnostics.config-issue-action": "Action", + "diagnostics.config-issue-argument": "Argument", + "diagnostics.config-issue-code": "Code", + "diagnostics.config-issue-config-file": "Config file", + "diagnostics.config-issue-detail": "Detail", + "diagnostics.config-issue-message": "Message", + "diagnostics.config-issue-severity": "Severity", + "diagnostics.config-issue-source": "Source", + "diagnostics.config-issues": "Configuration issues", + "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.", + "diagnostics.config-issues-none": "No configuration issues detected.", "diagnostics.copied": "Kopiert!", "diagnostics.copy-to-clipboard": "In Zwischenablage kopieren", "diagnostics.found-config": "Konfiguration gefunden", @@ -84,6 +95,17 @@ "connected": "Connected", "diagnostics.browser-info": "Browser Info", "diagnostics.browser-info-description": "This section allows you to generate a detailed report of your browser information. This can be helpful when troubleshooting browser-specific issues.", + "diagnostics.config-issue-action": "Action", + "diagnostics.config-issue-argument": "Argument", + "diagnostics.config-issue-code": "Code", + "diagnostics.config-issue-config-file": "Config file", + "diagnostics.config-issue-detail": "Detail", + "diagnostics.config-issue-message": "Message", + "diagnostics.config-issue-severity": "Severity", + "diagnostics.config-issue-source": "Source", + "diagnostics.config-issues": "Configuration issues", + "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.", + "diagnostics.config-issues-none": "No configuration issues detected.", "diagnostics.copied": "Copied!", "diagnostics.copy-to-clipboard": "Copy to Clipboard", "diagnostics.found-config": "Found Config", @@ -163,6 +185,17 @@ "connected": "Conectado", "diagnostics.browser-info": "Información del navegador", "diagnostics.browser-info-description": "Esta sección le permite generar un informe detallado de su información del navegador. Esto puede ser útil al solucionar problemas específicos del navegador.", + "diagnostics.config-issue-action": "Action", + "diagnostics.config-issue-argument": "Argument", + "diagnostics.config-issue-code": "Code", + "diagnostics.config-issue-config-file": "Config file", + "diagnostics.config-issue-detail": "Detail", + "diagnostics.config-issue-message": "Message", + "diagnostics.config-issue-severity": "Severity", + "diagnostics.config-issue-source": "Source", + "diagnostics.config-issues": "Configuration issues", + "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.", + "diagnostics.config-issues-none": "No configuration issues detected.", "diagnostics.copied": "¡Copiado!", "diagnostics.copy-to-clipboard": "Copiar al portapapeles", "diagnostics.found-config": "Configuración encontrada", @@ -242,6 +275,17 @@ "connected": "Connesso", "diagnostics.browser-info": "Informazioni del browser", "diagnostics.browser-info-description": "Questa sezione ti consente di generare un rapporto dettagliato delle informazioni del tuo browser. Questo può essere utile durante la risoluzione dei problemi specifici del browser.", + "diagnostics.config-issue-action": "Action", + "diagnostics.config-issue-argument": "Argument", + "diagnostics.config-issue-code": "Code", + "diagnostics.config-issue-config-file": "Config file", + "diagnostics.config-issue-detail": "Detail", + "diagnostics.config-issue-message": "Message", + "diagnostics.config-issue-severity": "Severity", + "diagnostics.config-issue-source": "Source", + "diagnostics.config-issues": "Configuration issues", + "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.", + "diagnostics.config-issues-none": "No configuration issues detected.", "diagnostics.copied": "Copiato!", "diagnostics.copy-to-clipboard": "Copia negli appunti", "diagnostics.found-config": "Configurazione trovata", @@ -321,6 +365,17 @@ "connected": "已连接", "diagnostics.browser-info": "浏览器信息", "diagnostics.browser-info-description": "此部分允许您生成浏览器信息的详细报告。这在排查浏览器特定问题时很有帮助。", + "diagnostics.config-issue-action": "Action", + "diagnostics.config-issue-argument": "Argument", + "diagnostics.config-issue-code": "Code", + "diagnostics.config-issue-config-file": "Config file", + "diagnostics.config-issue-detail": "Detail", + "diagnostics.config-issue-message": "Message", + "diagnostics.config-issue-severity": "Severity", + "diagnostics.config-issue-source": "Source", + "diagnostics.config-issues": "Configuration issues", + "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.", + "diagnostics.config-issues-none": "No configuration issues detected.", "diagnostics.copied": "已复制!", "diagnostics.copy-to-clipboard": "复制到剪贴板", "diagnostics.found-config": "找到配置", @@ -400,6 +455,17 @@ "connected": "已連線", "diagnostics.browser-info": "瀏覽器資訊", "diagnostics.browser-info-description": "此區塊可讓您產生瀏覽器資訊的詳細報告。這在排解特定瀏覽器問題時相當有幫助。", + "diagnostics.config-issue-action": "Action", + "diagnostics.config-issue-argument": "Argument", + "diagnostics.config-issue-code": "Code", + "diagnostics.config-issue-config-file": "Config file", + "diagnostics.config-issue-detail": "Detail", + "diagnostics.config-issue-message": "Message", + "diagnostics.config-issue-severity": "Severity", + "diagnostics.config-issue-source": "Source", + "diagnostics.config-issues": "Configuration issues", + "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.", + "diagnostics.config-issues-none": "No configuration issues detected.", "diagnostics.copied": "已複製!", "diagnostics.copy-to-clipboard": "複製到剪貼簿", "diagnostics.found-config": "找到設定檔 (Config)", diff --git a/lang/de-DE.yaml b/lang/de-DE.yaml index 813e533..789c2cf 100644 --- a/lang/de-DE.yaml +++ b/lang/de-DE.yaml @@ -54,6 +54,17 @@ translations: diagnostics.get-support: Unterstützung erhalten Unterstützung erhalten diagnostics.get-support-description: Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, Server-Diagnostik von dieser Seite einzufügen. diagnostics.where-to-find-help: Wo Sie Hilfe finden + diagnostics.config-issues: Configuration issues + diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin. + diagnostics.config-issues-none: No configuration issues detected. + diagnostics.config-issue-severity: Severity + diagnostics.config-issue-code: Code + diagnostics.config-issue-message: Message + diagnostics.config-issue-action: Action + diagnostics.config-issue-argument: Argument + diagnostics.config-issue-config-file: Config file + diagnostics.config-issue-detail: Detail + diagnostics.config-issue-source: Source diagnostics.ssh: SSH diagnostics.found-key: Schlüssel gefunden diagnostics.found-config: Konfiguration gefunden diff --git a/lang/en.yaml b/lang/en.yaml index a334965..9d1cc4c 100644 --- a/lang/en.yaml +++ b/lang/en.yaml @@ -54,6 +54,17 @@ translations: diagnostics.get-support: Get support diagnostics.get-support-description: If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include Server Diagnostics from this page. diagnostics.where-to-find-help: Where to find help + diagnostics.config-issues: Configuration issues + diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin. + diagnostics.config-issues-none: No configuration issues detected. + diagnostics.config-issue-severity: Severity + diagnostics.config-issue-code: Code + diagnostics.config-issue-message: Message + diagnostics.config-issue-action: Action + diagnostics.config-issue-argument: Argument + diagnostics.config-issue-config-file: Config file + diagnostics.config-issue-detail: Detail + diagnostics.config-issue-source: Source diagnostics.ssh: SSH diagnostics.found-key: Found Key diagnostics.found-config: Found Config diff --git a/lang/es-ES.yaml b/lang/es-ES.yaml index bb834aa..6f8a6a5 100644 --- a/lang/es-ES.yaml +++ b/lang/es-ES.yaml @@ -54,6 +54,17 @@ translations: diagnostics.get-support: Obtener soporte Obtener soporte diagnostics.get-support-description: Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir diagnósticos del servidor desde esta página. diagnostics.where-to-find-help: Dónde encontrar ayuda + diagnostics.config-issues: Configuration issues + diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin. + diagnostics.config-issues-none: No configuration issues detected. + diagnostics.config-issue-severity: Severity + diagnostics.config-issue-code: Code + diagnostics.config-issue-message: Message + diagnostics.config-issue-action: Action + diagnostics.config-issue-argument: Argument + diagnostics.config-issue-config-file: Config file + diagnostics.config-issue-detail: Detail + diagnostics.config-issue-source: Source diagnostics.ssh: SSH diagnostics.found-key: Clave encontrada diagnostics.found-config: Configuración encontrada diff --git a/lang/it-IT.yaml b/lang/it-IT.yaml index d7f54cf..0543b6e 100644 --- a/lang/it-IT.yaml +++ b/lang/it-IT.yaml @@ -54,6 +54,17 @@ translations: diagnostics.get-support: Ottenere supporto Ottenere supporto diagnostics.get-support-description: Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere la diagnostica del server da questa pagina. diagnostics.where-to-find-help: Dove trovare aiuto + diagnostics.config-issues: Configuration issues + diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin. + diagnostics.config-issues-none: No configuration issues detected. + diagnostics.config-issue-severity: Severity + diagnostics.config-issue-code: Code + diagnostics.config-issue-message: Message + diagnostics.config-issue-action: Action + diagnostics.config-issue-argument: Argument + diagnostics.config-issue-config-file: Config file + diagnostics.config-issue-detail: Detail + diagnostics.config-issue-source: Source diagnostics.ssh: SSH diagnostics.found-key: Chiave trovata diagnostics.found-config: Configurazione trovata diff --git a/lang/zh-Hans-CN.yaml b/lang/zh-Hans-CN.yaml index 23ac07d..07a8c94 100644 --- a/lang/zh-Hans-CN.yaml +++ b/lang/zh-Hans-CN.yaml @@ -63,6 +63,17 @@ translations: diagnostics.get-support: 获取支持 获取支持 diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。 diagnostics.where-to-find-help: 在哪里找到帮助 + diagnostics.config-issues: Configuration issues + diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin. + diagnostics.config-issues-none: No configuration issues detected. + diagnostics.config-issue-severity: Severity + diagnostics.config-issue-code: Code + diagnostics.config-issue-message: Message + diagnostics.config-issue-action: Action + diagnostics.config-issue-argument: Argument + diagnostics.config-issue-config-file: Config file + diagnostics.config-issue-detail: Detail + diagnostics.config-issue-source: Source diagnostics.ssh: SSH diagnostics.found-key: 找到密钥 diagnostics.found-config: 找到配置 diff --git a/lang/zh-Hant-TW.yaml b/lang/zh-Hant-TW.yaml index 7f20e2a..65f2cf7 100644 --- a/lang/zh-Hant-TW.yaml +++ b/lang/zh-Hant-TW.yaml @@ -54,6 +54,17 @@ translations: diagnostics.get-support: 尋求技術支援 diagnostics.get-support-description: 如果您在使用 OliveTin 時遇到問題並希望提出支援請求,提供此頁面的「伺服器診斷資訊」將會有很大的幫助。 diagnostics.where-to-find-help: 哪裡可以尋求協助 + diagnostics.config-issues: Configuration issues + diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin. + diagnostics.config-issues-none: No configuration issues detected. + diagnostics.config-issue-severity: Severity + diagnostics.config-issue-code: Code + diagnostics.config-issue-message: Message + diagnostics.config-issue-action: Action + diagnostics.config-issue-argument: Argument + diagnostics.config-issue-config-file: Config file + diagnostics.config-issue-detail: Detail + diagnostics.config-issue-source: Source diagnostics.ssh: SSH diagnostics.found-key: 找到金鑰 (Key) diagnostics.found-config: 找到設定檔 (Config) diff --git a/proto/olivetin/api/v1/olivetin.proto b/proto/olivetin/api/v1/olivetin.proto index 1b16b5f..0d49b28 100644 --- a/proto/olivetin/api/v1/olivetin.proto +++ b/proto/olivetin/api/v1/olivetin.proto @@ -379,9 +379,21 @@ message LogoutResponse { message GetDiagnosticsRequest { } +message ConfigIssue { + string severity = 1; + string code = 2; + string message = 3; + string action_id = 4; + string action_title = 5; + string argument_name = 6; + string source = 7; + string config_file = 8; +} + message GetDiagnosticsResponse { string SshFoundKey = 1; string SshFoundConfig = 2; + repeated ConfigIssue config_issues = 3; } message InitRequest {} @@ -412,6 +424,7 @@ message InitResponse { bool login_required = 23; repeated string available_themes = 24; // List of available theme names bool show_navigate_on_start_icons = 25; + int32 config_issue_count = 26; } message AdditionalLink { diff --git a/service/gen/olivetin/api/v1/olivetin.pb.go b/service/gen/olivetin/api/v1/olivetin.pb.go index e0a0c96..ba38dd5 100644 --- a/service/gen/olivetin/api/v1/olivetin.pb.go +++ b/service/gen/olivetin/api/v1/olivetin.pb.go @@ -3871,17 +3871,118 @@ func (*GetDiagnosticsRequest) Descriptor() ([]byte, []int) { return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{64} } +type ConfigIssue struct { + state protoimpl.MessageState `protogen:"open.v1"` + Severity string `protobuf:"bytes,1,opt,name=severity,proto3" json:"severity,omitempty"` + Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + ActionId string `protobuf:"bytes,4,opt,name=action_id,json=actionId,proto3" json:"action_id,omitempty"` + ActionTitle string `protobuf:"bytes,5,opt,name=action_title,json=actionTitle,proto3" json:"action_title,omitempty"` + ArgumentName string `protobuf:"bytes,6,opt,name=argument_name,json=argumentName,proto3" json:"argument_name,omitempty"` + Source string `protobuf:"bytes,7,opt,name=source,proto3" json:"source,omitempty"` + ConfigFile string `protobuf:"bytes,8,opt,name=config_file,json=configFile,proto3" json:"config_file,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigIssue) Reset() { + *x = ConfigIssue{} + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigIssue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigIssue) ProtoMessage() {} + +func (x *ConfigIssue) ProtoReflect() protoreflect.Message { + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfigIssue.ProtoReflect.Descriptor instead. +func (*ConfigIssue) Descriptor() ([]byte, []int) { + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{65} +} + +func (x *ConfigIssue) GetSeverity() string { + if x != nil { + return x.Severity + } + return "" +} + +func (x *ConfigIssue) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +func (x *ConfigIssue) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ConfigIssue) GetActionId() string { + if x != nil { + return x.ActionId + } + return "" +} + +func (x *ConfigIssue) GetActionTitle() string { + if x != nil { + return x.ActionTitle + } + return "" +} + +func (x *ConfigIssue) GetArgumentName() string { + if x != nil { + return x.ArgumentName + } + return "" +} + +func (x *ConfigIssue) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *ConfigIssue) GetConfigFile() string { + if x != nil { + return x.ConfigFile + } + return "" +} + type GetDiagnosticsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` SshFoundKey string `protobuf:"bytes,1,opt,name=SshFoundKey,proto3" json:"SshFoundKey,omitempty"` SshFoundConfig string `protobuf:"bytes,2,opt,name=SshFoundConfig,proto3" json:"SshFoundConfig,omitempty"` + ConfigIssues []*ConfigIssue `protobuf:"bytes,3,rep,name=config_issues,json=configIssues,proto3" json:"config_issues,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetDiagnosticsResponse) Reset() { *x = GetDiagnosticsResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3893,7 +3994,7 @@ func (x *GetDiagnosticsResponse) String() string { func (*GetDiagnosticsResponse) ProtoMessage() {} func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3906,7 +4007,7 @@ func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDiagnosticsResponse.ProtoReflect.Descriptor instead. func (*GetDiagnosticsResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{65} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{66} } func (x *GetDiagnosticsResponse) GetSshFoundKey() string { @@ -3923,6 +4024,13 @@ func (x *GetDiagnosticsResponse) GetSshFoundConfig() string { return "" } +func (x *GetDiagnosticsResponse) GetConfigIssues() []*ConfigIssue { + if x != nil { + return x.ConfigIssues + } + return nil +} + type InitRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -3931,7 +4039,7 @@ type InitRequest struct { func (x *InitRequest) Reset() { *x = InitRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3943,7 +4051,7 @@ func (x *InitRequest) String() string { func (*InitRequest) ProtoMessage() {} func (x *InitRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3956,7 +4064,7 @@ func (x *InitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InitRequest.ProtoReflect.Descriptor instead. func (*InitRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{66} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{67} } type InitResponse struct { @@ -3986,13 +4094,14 @@ type InitResponse struct { 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"` + ConfigIssueCount int32 `protobuf:"varint,26,opt,name=config_issue_count,json=configIssueCount,proto3" json:"config_issue_count,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InitResponse) Reset() { *x = InitResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4004,7 +4113,7 @@ func (x *InitResponse) String() string { func (*InitResponse) ProtoMessage() {} func (x *InitResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4017,7 +4126,7 @@ func (x *InitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InitResponse.ProtoReflect.Descriptor instead. func (*InitResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{67} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{68} } func (x *InitResponse) GetShowFooter() bool { @@ -4195,6 +4304,13 @@ func (x *InitResponse) GetShowNavigateOnStartIcons() bool { return false } +func (x *InitResponse) GetConfigIssueCount() int32 { + if x != nil { + return x.ConfigIssueCount + } + return 0 +} + type AdditionalLink struct { state protoimpl.MessageState `protogen:"open.v1"` Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` @@ -4205,7 +4321,7 @@ type AdditionalLink struct { func (x *AdditionalLink) Reset() { *x = AdditionalLink{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4217,7 +4333,7 @@ func (x *AdditionalLink) String() string { func (*AdditionalLink) ProtoMessage() {} func (x *AdditionalLink) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4230,7 +4346,7 @@ func (x *AdditionalLink) ProtoReflect() protoreflect.Message { // Deprecated: Use AdditionalLink.ProtoReflect.Descriptor instead. func (*AdditionalLink) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{68} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{69} } func (x *AdditionalLink) GetTitle() string { @@ -4258,7 +4374,7 @@ type OAuth2Provider struct { func (x *OAuth2Provider) Reset() { *x = OAuth2Provider{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4270,7 +4386,7 @@ func (x *OAuth2Provider) String() string { func (*OAuth2Provider) ProtoMessage() {} func (x *OAuth2Provider) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4283,7 +4399,7 @@ func (x *OAuth2Provider) ProtoReflect() protoreflect.Message { // Deprecated: Use OAuth2Provider.ProtoReflect.Descriptor instead. func (*OAuth2Provider) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{69} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{70} } func (x *OAuth2Provider) GetTitle() string { @@ -4316,7 +4432,7 @@ type GetActionBindingRequest struct { func (x *GetActionBindingRequest) Reset() { *x = GetActionBindingRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4328,7 +4444,7 @@ func (x *GetActionBindingRequest) String() string { func (*GetActionBindingRequest) ProtoMessage() {} func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4341,7 +4457,7 @@ func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActionBindingRequest.ProtoReflect.Descriptor instead. func (*GetActionBindingRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{70} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{71} } func (x *GetActionBindingRequest) GetBindingId() string { @@ -4361,7 +4477,7 @@ type GetActionBindingResponse struct { func (x *GetActionBindingResponse) Reset() { *x = GetActionBindingResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4373,7 +4489,7 @@ func (x *GetActionBindingResponse) String() string { func (*GetActionBindingResponse) ProtoMessage() {} func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4386,7 +4502,7 @@ func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActionBindingResponse.ProtoReflect.Descriptor instead. func (*GetActionBindingResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{71} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{72} } func (x *GetActionBindingResponse) GetAction() *Action { @@ -4415,7 +4531,7 @@ type GetEntitiesRequest struct { func (x *GetEntitiesRequest) Reset() { *x = GetEntitiesRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4427,7 +4543,7 @@ func (x *GetEntitiesRequest) String() string { func (*GetEntitiesRequest) ProtoMessage() {} func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4440,7 +4556,7 @@ func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEntitiesRequest.ProtoReflect.Descriptor instead. func (*GetEntitiesRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{72} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{73} } func (x *GetEntitiesRequest) GetEntityType() string { @@ -4480,7 +4596,7 @@ type GetEntitiesResponse struct { func (x *GetEntitiesResponse) Reset() { *x = GetEntitiesResponse{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4492,7 +4608,7 @@ func (x *GetEntitiesResponse) String() string { func (*GetEntitiesResponse) ProtoMessage() {} func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4505,7 +4621,7 @@ func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEntitiesResponse.ProtoReflect.Descriptor instead. func (*GetEntitiesResponse) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{73} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{74} } func (x *GetEntitiesResponse) GetEntityDefinitions() []*EntityDefinition { @@ -4529,7 +4645,7 @@ type EntityDefinition struct { func (x *EntityDefinition) Reset() { *x = EntityDefinition{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4541,7 +4657,7 @@ func (x *EntityDefinition) String() string { func (*EntityDefinition) ProtoMessage() {} func (x *EntityDefinition) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4554,7 +4670,7 @@ func (x *EntityDefinition) ProtoReflect() protoreflect.Message { // Deprecated: Use EntityDefinition.ProtoReflect.Descriptor instead. func (*EntityDefinition) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{74} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{75} } func (x *EntityDefinition) GetTitle() string { @@ -4609,7 +4725,7 @@ type EntityProperty struct { func (x *EntityProperty) Reset() { *x = EntityProperty{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4621,7 +4737,7 @@ func (x *EntityProperty) String() string { func (*EntityProperty) ProtoMessage() {} func (x *EntityProperty) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4634,7 +4750,7 @@ func (x *EntityProperty) ProtoReflect() protoreflect.Message { // Deprecated: Use EntityProperty.ProtoReflect.Descriptor instead. func (*EntityProperty) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{75} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{76} } func (x *EntityProperty) GetName() string { @@ -4661,7 +4777,7 @@ type GetEntityRequest struct { func (x *GetEntityRequest) Reset() { *x = GetEntityRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4673,7 +4789,7 @@ func (x *GetEntityRequest) String() string { func (*GetEntityRequest) ProtoMessage() {} func (x *GetEntityRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4686,7 +4802,7 @@ func (x *GetEntityRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetEntityRequest.ProtoReflect.Descriptor instead. func (*GetEntityRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{76} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{77} } func (x *GetEntityRequest) GetUniqueKey() string { @@ -4712,7 +4828,7 @@ type RestartActionRequest struct { func (x *RestartActionRequest) Reset() { *x = RestartActionRequest{} - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4724,7 +4840,7 @@ func (x *RestartActionRequest) String() string { func (*RestartActionRequest) ProtoMessage() {} func (x *RestartActionRequest) ProtoReflect() protoreflect.Message { - mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77] + mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4737,7 +4853,7 @@ func (x *RestartActionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestartActionRequest.ProtoReflect.Descriptor instead. func (*RestartActionRequest) Descriptor() ([]byte, []int) { - return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{77} + return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{78} } func (x *RestartActionRequest) GetExecutionTrackingId() string { @@ -5044,11 +5160,22 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\x04hash\x18\x01 \x01(\tR\x04hash\"\x0f\n" + "\rLogoutRequest\"\x10\n" + "\x0eLogoutResponse\"\x17\n" + - "\x15GetDiagnosticsRequest\"b\n" + + "\x15GetDiagnosticsRequest\"\xf5\x01\n" + + "\vConfigIssue\x12\x1a\n" + + "\bseverity\x18\x01 \x01(\tR\bseverity\x12\x12\n" + + "\x04code\x18\x02 \x01(\tR\x04code\x12\x18\n" + + "\amessage\x18\x03 \x01(\tR\amessage\x12\x1b\n" + + "\taction_id\x18\x04 \x01(\tR\bactionId\x12!\n" + + "\faction_title\x18\x05 \x01(\tR\vactionTitle\x12#\n" + + "\rargument_name\x18\x06 \x01(\tR\fargumentName\x12\x16\n" + + "\x06source\x18\a \x01(\tR\x06source\x12\x1f\n" + + "\vconfig_file\x18\b \x01(\tR\n" + + "configFile\"\xa5\x01\n" + "\x16GetDiagnosticsResponse\x12 \n" + "\vSshFoundKey\x18\x01 \x01(\tR\vSshFoundKey\x12&\n" + - "\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\"\r\n" + - "\vInitRequest\"\x8d\t\n" + + "\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\x12A\n" + + "\rconfig_issues\x18\x03 \x03(\v2\x1c.olivetin.api.v1.ConfigIssueR\fconfigIssues\"\r\n" + + "\vInitRequest\"\xbb\t\n" + "\fInitResponse\x12\x1e\n" + "\n" + "showFooter\x18\x01 \x01(\bR\n" + @@ -5078,7 +5205,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\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\x12>\n" + - "\x1cshow_navigate_on_start_icons\x18\x19 \x01(\bR\x18showNavigateOnStartIcons\"8\n" + + "\x1cshow_navigate_on_start_icons\x18\x19 \x01(\bR\x18showNavigateOnStartIcons\x12,\n" + + "\x12config_issue_count\x18\x1a \x01(\x05R\x10configIssueCount\"8\n" + "\x0eAdditionalLink\x12\x14\n" + "\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" + "\x03url\x18\x02 \x01(\tR\x03url\"L\n" + @@ -5159,7 +5287,7 @@ func file_olivetin_api_v1_olivetin_proto_rawDescGZIP() []byte { return file_olivetin_api_v1_olivetin_proto_rawDescData } -var file_olivetin_api_v1_olivetin_proto_msgTypes = make([]protoimpl.MessageInfo, 85) +var file_olivetin_api_v1_olivetin_proto_msgTypes = make([]protoimpl.MessageInfo, 86) var file_olivetin_api_v1_olivetin_proto_goTypes = []any{ (*Action)(nil), // 0: olivetin.api.v1.Action (*ActionGroupMembership)(nil), // 1: olivetin.api.v1.ActionGroupMembership @@ -5226,38 +5354,39 @@ var file_olivetin_api_v1_olivetin_proto_goTypes = []any{ (*LogoutRequest)(nil), // 62: olivetin.api.v1.LogoutRequest (*LogoutResponse)(nil), // 63: olivetin.api.v1.LogoutResponse (*GetDiagnosticsRequest)(nil), // 64: olivetin.api.v1.GetDiagnosticsRequest - (*GetDiagnosticsResponse)(nil), // 65: olivetin.api.v1.GetDiagnosticsResponse - (*InitRequest)(nil), // 66: olivetin.api.v1.InitRequest - (*InitResponse)(nil), // 67: olivetin.api.v1.InitResponse - (*AdditionalLink)(nil), // 68: olivetin.api.v1.AdditionalLink - (*OAuth2Provider)(nil), // 69: olivetin.api.v1.OAuth2Provider - (*GetActionBindingRequest)(nil), // 70: olivetin.api.v1.GetActionBindingRequest - (*GetActionBindingResponse)(nil), // 71: olivetin.api.v1.GetActionBindingResponse - (*GetEntitiesRequest)(nil), // 72: olivetin.api.v1.GetEntitiesRequest - (*GetEntitiesResponse)(nil), // 73: olivetin.api.v1.GetEntitiesResponse - (*EntityDefinition)(nil), // 74: olivetin.api.v1.EntityDefinition - (*EntityProperty)(nil), // 75: olivetin.api.v1.EntityProperty - (*GetEntityRequest)(nil), // 76: olivetin.api.v1.GetEntityRequest - (*RestartActionRequest)(nil), // 77: olivetin.api.v1.RestartActionRequest - nil, // 78: olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry - nil, // 79: olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry - nil, // 80: olivetin.api.v1.ActionArgument.SuggestionsEntry - nil, // 81: olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry - nil, // 82: olivetin.api.v1.Entity.FieldsEntry - nil, // 83: olivetin.api.v1.DumpVarsResponse.ContentsEntry - nil, // 84: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry + (*ConfigIssue)(nil), // 65: olivetin.api.v1.ConfigIssue + (*GetDiagnosticsResponse)(nil), // 66: olivetin.api.v1.GetDiagnosticsResponse + (*InitRequest)(nil), // 67: olivetin.api.v1.InitRequest + (*InitResponse)(nil), // 68: olivetin.api.v1.InitResponse + (*AdditionalLink)(nil), // 69: olivetin.api.v1.AdditionalLink + (*OAuth2Provider)(nil), // 70: olivetin.api.v1.OAuth2Provider + (*GetActionBindingRequest)(nil), // 71: olivetin.api.v1.GetActionBindingRequest + (*GetActionBindingResponse)(nil), // 72: olivetin.api.v1.GetActionBindingResponse + (*GetEntitiesRequest)(nil), // 73: olivetin.api.v1.GetEntitiesRequest + (*GetEntitiesResponse)(nil), // 74: olivetin.api.v1.GetEntitiesResponse + (*EntityDefinition)(nil), // 75: olivetin.api.v1.EntityDefinition + (*EntityProperty)(nil), // 76: olivetin.api.v1.EntityProperty + (*GetEntityRequest)(nil), // 77: olivetin.api.v1.GetEntityRequest + (*RestartActionRequest)(nil), // 78: olivetin.api.v1.RestartActionRequest + nil, // 79: olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry + nil, // 80: olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry + nil, // 81: olivetin.api.v1.ActionArgument.SuggestionsEntry + nil, // 82: olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry + nil, // 83: olivetin.api.v1.Entity.FieldsEntry + nil, // 84: olivetin.api.v1.DumpVarsResponse.ContentsEntry + nil, // 85: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry } var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{ 3, // 0: olivetin.api.v1.Action.arguments:type_name -> olivetin.api.v1.ActionArgument 2, // 1: olivetin.api.v1.Action.exec_on_webhooks:type_name -> olivetin.api.v1.ActionWebhookExecHint 1, // 2: olivetin.api.v1.Action.groups:type_name -> olivetin.api.v1.ActionGroupMembership - 78, // 3: olivetin.api.v1.ActionWebhookExecHint.match_headers:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry - 79, // 4: olivetin.api.v1.ActionWebhookExecHint.match_query:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry + 79, // 3: olivetin.api.v1.ActionWebhookExecHint.match_headers:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry + 80, // 4: olivetin.api.v1.ActionWebhookExecHint.match_query:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry 4, // 5: olivetin.api.v1.ActionArgument.choices:type_name -> olivetin.api.v1.ActionArgumentChoice - 80, // 6: olivetin.api.v1.ActionArgument.suggestions:type_name -> olivetin.api.v1.ActionArgument.SuggestionsEntry + 81, // 6: olivetin.api.v1.ActionArgument.suggestions:type_name -> olivetin.api.v1.ActionArgument.SuggestionsEntry 0, // 7: olivetin.api.v1.EntityRelatedAction.action:type_name -> olivetin.api.v1.Action - 81, // 8: olivetin.api.v1.EntityRelatedAction.prefilled_arguments:type_name -> olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry - 82, // 9: olivetin.api.v1.Entity.fields:type_name -> olivetin.api.v1.Entity.FieldsEntry + 82, // 8: olivetin.api.v1.EntityRelatedAction.prefilled_arguments:type_name -> olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry + 83, // 9: olivetin.api.v1.Entity.fields:type_name -> olivetin.api.v1.Entity.FieldsEntry 5, // 10: olivetin.api.v1.Entity.related_actions:type_name -> olivetin.api.v1.EntityRelatedAction 10, // 11: olivetin.api.v1.GetDashboardResponse.dashboard:type_name -> olivetin.api.v1.Dashboard 11, // 12: olivetin.api.v1.Dashboard.contents:type_name -> olivetin.api.v1.DashboardComponent @@ -5275,8 +5404,8 @@ var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{ 28, // 24: olivetin.api.v1.GetExecutionQueueResponse.groups:type_name -> olivetin.api.v1.ExecutionQueueGroup 22, // 25: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry 35, // 26: olivetin.api.v1.ExecutionStatusResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget - 83, // 27: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry - 84, // 28: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry + 84, // 27: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry + 85, // 28: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry 51, // 29: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged 52, // 30: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged 54, // 31: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished @@ -5285,72 +5414,73 @@ var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{ 53, // 34: olivetin.api.v1.EventStreamResponse.heartbeat:type_name -> olivetin.api.v1.EventHeartbeat 22, // 35: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry 22, // 36: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry - 69, // 37: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider - 68, // 38: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink - 8, // 39: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy - 0, // 40: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action - 35, // 41: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget - 74, // 42: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition - 6, // 43: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity - 75, // 44: olivetin.api.v1.EntityDefinition.properties:type_name -> olivetin.api.v1.EntityProperty - 43, // 45: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding - 9, // 46: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest - 12, // 47: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest - 15, // 48: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest - 17, // 49: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest - 19, // 50: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest - 77, // 51: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest - 56, // 52: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest - 34, // 53: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest - 21, // 54: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest - 24, // 55: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest - 26, // 56: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest - 30, // 57: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest - 37, // 58: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest - 39, // 59: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:input_type -> olivetin.api.v1.ServerDiagnosticsRequest - 41, // 60: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest - 44, // 61: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest - 46, // 62: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest - 58, // 63: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest - 60, // 64: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest - 62, // 65: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest - 48, // 66: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest - 64, // 67: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest - 66, // 68: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest - 70, // 69: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest - 72, // 70: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest - 76, // 71: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest - 7, // 72: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse - 14, // 73: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse - 16, // 74: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse - 18, // 75: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse - 20, // 76: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse - 14, // 77: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse - 57, // 78: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse - 36, // 79: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse - 23, // 80: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse - 25, // 81: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse - 29, // 82: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse - 31, // 83: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse - 38, // 84: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse - 40, // 85: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:output_type -> olivetin.api.v1.ServerDiagnosticsResponse - 42, // 86: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse - 45, // 87: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse - 47, // 88: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse - 59, // 89: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse - 61, // 90: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse - 63, // 91: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse - 49, // 92: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse - 65, // 93: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse - 67, // 94: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse - 71, // 95: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse - 73, // 96: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse - 6, // 97: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity - 72, // [72:98] is the sub-list for method output_type - 46, // [46:72] is the sub-list for method input_type - 46, // [46:46] is the sub-list for extension type_name - 46, // [46:46] is the sub-list for extension extendee - 0, // [0:46] is the sub-list for field type_name + 65, // 37: olivetin.api.v1.GetDiagnosticsResponse.config_issues:type_name -> olivetin.api.v1.ConfigIssue + 70, // 38: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider + 69, // 39: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink + 8, // 40: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy + 0, // 41: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action + 35, // 42: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget + 75, // 43: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition + 6, // 44: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity + 76, // 45: olivetin.api.v1.EntityDefinition.properties:type_name -> olivetin.api.v1.EntityProperty + 43, // 46: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding + 9, // 47: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest + 12, // 48: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest + 15, // 49: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest + 17, // 50: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest + 19, // 51: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest + 78, // 52: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest + 56, // 53: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest + 34, // 54: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest + 21, // 55: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest + 24, // 56: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest + 26, // 57: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest + 30, // 58: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest + 37, // 59: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest + 39, // 60: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:input_type -> olivetin.api.v1.ServerDiagnosticsRequest + 41, // 61: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest + 44, // 62: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest + 46, // 63: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest + 58, // 64: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest + 60, // 65: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest + 62, // 66: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest + 48, // 67: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest + 64, // 68: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest + 67, // 69: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest + 71, // 70: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest + 73, // 71: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest + 77, // 72: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest + 7, // 73: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse + 14, // 74: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse + 16, // 75: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse + 18, // 76: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse + 20, // 77: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse + 14, // 78: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse + 57, // 79: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse + 36, // 80: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse + 23, // 81: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse + 25, // 82: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse + 29, // 83: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse + 31, // 84: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse + 38, // 85: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse + 40, // 86: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:output_type -> olivetin.api.v1.ServerDiagnosticsResponse + 42, // 87: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse + 45, // 88: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse + 47, // 89: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse + 59, // 90: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse + 61, // 91: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse + 63, // 92: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse + 49, // 93: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse + 66, // 94: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse + 68, // 95: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse + 72, // 96: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse + 74, // 97: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse + 6, // 98: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity + 73, // [73:99] is the sub-list for method output_type + 47, // [47:73] is the sub-list for method input_type + 47, // [47:47] is the sub-list for extension type_name + 47, // [47:47] is the sub-list for extension extendee + 0, // [0:47] is the sub-list for field type_name } func init() { file_olivetin_api_v1_olivetin_proto_init() } @@ -5372,7 +5502,7 @@ func file_olivetin_api_v1_olivetin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_olivetin_api_v1_olivetin_proto_rawDesc), len(file_olivetin_api_v1_olivetin_proto_rawDesc)), NumEnums: 0, - NumMessages: 85, + NumMessages: 86, NumExtensions: 0, NumServices: 1, }, diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 9f9432e..81b83ee 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -1200,6 +1200,7 @@ func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[api res := &apiv1.GetDiagnosticsResponse{ SshFoundKey: installationinfo.Runtime.SshFoundKey, SshFoundConfig: installationinfo.Runtime.SshFoundConfig, + ConfigIssues: api.buildConfigIssuesForUser(user), } return connect.NewResponse(res), nil } @@ -1242,6 +1243,7 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq LoginRequired: loginRequired, AvailableThemes: discoverAvailableThemes(api.cfg), ShowNavigateOnStartIcons: api.cfg.ShowNavigateOnStartIcons, + ConfigIssueCount: configIssueCountForUser(api, user), } return connect.NewResponse(res), nil diff --git a/service/internal/api/api_config_issues_test.go b/service/internal/api/api_config_issues_test.go new file mode 100644 index 0000000..c83a265 --- /dev/null +++ b/service/internal/api/api_config_issues_test.go @@ -0,0 +1,197 @@ +package api + +import ( + "context" + "testing" + + "connectrpc.com/connect" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" + config "github.com/OliveTin/OliveTin/internal/config" + "github.com/OliveTin/OliveTin/internal/configissues" +) + +func TestGetDiagnosticsReturnsConfigIssues(t *testing.T) { + cfg := config.DefaultConfig() + cfg.Actions = []*config.Action{ + { + Title: "Deploy", + ID: "deploy", + Entity: "host", + Arguments: []config.ActionArgument{ + { + Name: "ansible_host", + Type: "ascii_identifier", + Default: `{{ host.name | default "All" }}`, + }, + }, + ExecOnCron: []string{"0 1 * * *"}, + }, + } + + ts, client := getNewTestServerAndClient(cfg) + defer ts.Close() + + res, err := client.GetDiagnostics(context.Background(), connect.NewRequest(&apiv1.GetDiagnosticsRequest{})) + require.NoError(t, err) + require.NotNil(t, res.Msg) + require.NotEmpty(t, res.Msg.ConfigIssues) + + codes := map[string]bool{} + for _, issue := range res.Msg.ConfigIssues { + codes[issue.Code] = true + } + assert.True(t, codes[configissues.CodeTemplateParse]) + assert.True(t, codes[configissues.CodeCronEntityBinding]) + + initRes, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{})) + require.NoError(t, err) + assert.Equal(t, int32(len(res.Msg.ConfigIssues)), initRes.Msg.ConfigIssueCount) +} + +func TestInitHidesConfigIssueCountWithoutDiagnostics(t *testing.T) { + cfg := config.DefaultConfig() + cfg.DefaultPolicy.ShowDiagnostics = false + cfg.Actions = []*config.Action{ + { + Title: "Deploy", + ID: "deploy", + Entity: "host", + Arguments: []config.ActionArgument{ + { + Name: "ansible_host", + Type: "ascii_identifier", + Default: `{{ host.name | default "All" }}`, + }, + }, + }, + } + + ts, client := getNewTestServerAndClient(cfg) + defer ts.Close() + + initRes, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{})) + require.NoError(t, err) + assert.Equal(t, int32(0), initRes.Msg.ConfigIssueCount) + + _, err = client.GetDiagnostics(context.Background(), connect.NewRequest(&apiv1.GetDiagnosticsRequest{})) + require.Error(t, err) + assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err)) +} + +func attachBrokenTemplateArgToSecretAction(cfg *config.Config) { + for i := range cfg.Actions { + if cfg.Actions[i].ID != "secret_action" { + continue + } + cfg.Actions[i].Arguments = []config.ActionArgument{ + { + Name: "target", + Type: "ascii_identifier", + Default: `{{ host.name | default "All" }}`, + }, + } + return + } +} + +func assertNoActionScopedConfigIssues(t *testing.T, issues []*apiv1.ConfigIssue) { + t.Helper() + for _, issue := range issues { + assert.Empty(t, issue.ActionId, + "user with view:false must not see action-scoped config issues") + } +} + +func configIssuesContainActionID(issues []*apiv1.ConfigIssue, actionID string) bool { + for _, issue := range issues { + if issue.ActionId == actionID { + return true + } + } + return false +} + +func TestConfigIssuesHideActionsWithoutViewPermission(t *testing.T) { + cfg, _, _ := buildViewPermissionTestConfig(t) + cfg.AuthHttpHeaderUsername = "X-Ot-User" + attachBrokenTemplateArgToSecretAction(cfg) + + ts, client := getNewTestServerAndClient(cfg) + defer ts.Close() + + configissues.Report(configissues.Issue{ + Severity: configissues.SeverityWarning, + Code: configissues.CodeEnvUnset, + Message: "TEST_GLOBAL_ENV is not set", + Source: "TEST_GLOBAL_ENV", + }) + + lowReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{}) + lowReq.Header().Set("X-Ot-User", "low") + lowRes, err := client.GetDiagnostics(context.Background(), lowReq) + require.NoError(t, err) + + assertNoActionScopedConfigIssues(t, lowRes.Msg.ConfigIssues) + require.NotEmpty(t, lowRes.Msg.ConfigIssues, "low user should still see global config issues") + + lowInit := connect.NewRequest(&apiv1.InitRequest{}) + lowInit.Header().Set("X-Ot-User", "low") + lowInitRes, err := client.Init(context.Background(), lowInit) + require.NoError(t, err) + assert.Equal(t, int32(len(lowRes.Msg.ConfigIssues)), lowInitRes.Msg.ConfigIssueCount) + + adminReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{}) + adminReq.Header().Set("X-Ot-User", "admin") + adminRes, err := client.GetDiagnostics(context.Background(), adminReq) + require.NoError(t, err) + + assert.True(t, configIssuesContainActionID(adminRes.Msg.ConfigIssues, "secret_action"), + "admin must still see config issues for secret_action") + assert.Greater(t, len(adminRes.Msg.ConfigIssues), len(lowRes.Msg.ConfigIssues)) +} + +func TestConfigIssuesHideRuntimeWatcherFailuresWithoutViewPermission(t *testing.T) { + cfg, _, _ := buildViewPermissionTestConfig(t) + cfg.AuthHttpHeaderUsername = "X-Ot-User" + + ts, client := getNewTestServerAndClient(cfg) + defer ts.Close() + + configissues.Report(configissues.Issue{ + Severity: configissues.SeverityError, + Code: configissues.CodeWatcherPath, + Message: `Could not create watcher for "/secret/path": permission denied`, + ActionID: "secret_action", + ActionTitle: "Secret Action", + Source: "/secret/path", + ConfigFile: "/etc/OliveTin/secret.yaml", + }) + + lowReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{}) + lowReq.Header().Set("X-Ot-User", "low") + lowRes, err := client.GetDiagnostics(context.Background(), lowReq) + require.NoError(t, err) + + for _, issue := range lowRes.Msg.ConfigIssues { + assert.NotEqual(t, configissues.CodeWatcherPath, issue.Code, + "user with view:false must not see action-scoped watcher_path issues") + assert.NotContains(t, issue.Source, "/secret/path") + } + + adminReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{}) + adminReq.Header().Set("X-Ot-User", "admin") + adminRes, err := client.GetDiagnostics(context.Background(), adminReq) + require.NoError(t, err) + + foundWatcher := false + for _, issue := range adminRes.Msg.ConfigIssues { + if issue.Code == configissues.CodeWatcherPath && issue.ActionId == "secret_action" { + foundWatcher = true + break + } + } + assert.True(t, foundWatcher, "admin must see action-scoped watcher_path issues") +} diff --git a/service/internal/api/config_issues.go b/service/internal/api/config_issues.go new file mode 100644 index 0000000..306fd0b --- /dev/null +++ b/service/internal/api/config_issues.go @@ -0,0 +1,75 @@ +package api + +import ( + apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" + authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic" + "github.com/OliveTin/OliveTin/internal/config" + "github.com/OliveTin/OliveTin/internal/configissues" +) + +func configIssueCountForUser(api *oliveTinAPI, user *authpublic.AuthenticatedUser) int32 { + return int32(len(api.buildConfigIssuesForUser(user))) +} + +func (api *oliveTinAPI) buildConfigIssuesForUser(user *authpublic.AuthenticatedUser) []*apiv1.ConfigIssue { + if !userCanSeeDiagnostics(user) { + return []*apiv1.ConfigIssue{} + } + + return filterConfigIssuesForUser(api, user, configissues.List()) +} + +func userCanSeeDiagnostics(user *authpublic.AuthenticatedUser) bool { + return user != nil && user.EffectivePolicy != nil && user.EffectivePolicy.ShowDiagnostics +} + +func filterConfigIssuesForUser(api *oliveTinAPI, user *authpublic.AuthenticatedUser, src []configissues.Issue) []*apiv1.ConfigIssue { + out := make([]*apiv1.ConfigIssue, 0, len(src)) + for _, issue := range src { + if api.includeConfigIssueForUser(user, issue) { + out = append(out, toProtoConfigIssue(issue)) + } + } + return out +} + +func (api *oliveTinAPI) includeConfigIssueForUser(user *authpublic.AuthenticatedUser, issue configissues.Issue) bool { + if issue.ActionID == "" { + return true + } + return api.canViewActionID(user, issue.ActionID) +} + +func (api *oliveTinAPI) canViewActionID(user *authpublic.AuthenticatedUser, actionID string) bool { + action := findActionInList(api.cfg.Actions, actionID) + if action == nil { + return false + } + return api.userCanViewAction(user, action) +} + +func findActionInList(actions []*config.Action, actionID string) *config.Action { + for _, action := range actions { + if actionHasID(action, actionID) { + return action + } + } + return nil +} + +func actionHasID(action *config.Action, actionID string) bool { + return action != nil && action.ID == actionID +} + +func toProtoConfigIssue(issue configissues.Issue) *apiv1.ConfigIssue { + return &apiv1.ConfigIssue{ + Severity: issue.Severity, + Code: issue.Code, + Message: issue.Message, + ActionId: issue.ActionID, + ActionTitle: issue.ActionTitle, + ArgumentName: issue.ArgumentName, + Source: issue.Source, + ConfigFile: issue.ConfigFile, + } +} diff --git a/service/internal/config/config.go b/service/internal/config/config.go index de85262..4b15077 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -39,6 +39,8 @@ type Action struct { EnabledExpression string `koanf:"enabledExpression"` Groups []string `koanf:"groups"` Justification string `koanf:"justification"` + // SourceFile is set by OliveTin when loading config (not user YAML). + SourceFile string `koanf:"x-olivetin-source-file"` } func (action *Action) RequiresJustification() bool { @@ -110,6 +112,8 @@ type EntityFile struct { Name string `koanf:"name"` Icon string `koanf:"icon"` Properties []EntityProperty `koanf:"properties"` + // SourceFile is set by OliveTin when loading config (not user YAML). + SourceFile string `koanf:"x-olivetin-source-file"` } // EntityProperty defines a column shown when listing entity instances in the UI. diff --git a/service/internal/config/config_reloader.go b/service/internal/config/config_reloader.go index bc8631d..39a2484 100644 --- a/service/internal/config/config_reloader.go +++ b/service/internal/config/config_reloader.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "os" "path/filepath" "reflect" @@ -8,6 +9,7 @@ import ( "sort" "strings" + "github.com/OliveTin/OliveTin/internal/configissues" "github.com/go-viper/mapstructure/v2" "github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/providers/file" @@ -40,6 +42,8 @@ func AppendSource(cfg *Config, k *koanf.Koanf, configPath string) { "configPath": configPath, }).Info("Appending cfg source") + configissues.BeginConfigLoad() + stampLoadedConfigSources(k, configPath) loadIncludedConfigsFromDir(k, configPath) if !unmarshalRoot(k, cfg) { @@ -172,7 +176,7 @@ func listYamlFiles(includePath string) ([]string, bool) { func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) { filePath := filepath.Join(includePath, filename) - if err := k.Load(file.Provider(filePath), yaml.Parser(), koanf.WithMergeFunc(mergeFunc)); err != nil { + if err := k.Load(file.Provider(filePath), yaml.Parser(), koanf.WithMergeFunc(mergeFuncForSource(filePath))); err != nil { log.Errorf("Error loading included config file %s: %v", filePath, err) return } @@ -182,8 +186,15 @@ func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) { }).Info("Successfully loaded included config file") } +func mergeFuncForSource(sourceFile string) func(src, dest map[string]interface{}) error { + return func(src map[string]interface{}, dest map[string]interface{}) error { + return mergeFunc(src, dest, sourceFile) + } +} + // mergeActionsWhenBothExist merges actions when both src and dest have actions. -func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{}, dest map[string]interface{}) { +func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{}, dest map[string]interface{}, sourceFile string) { + stampSourceOnMaps(srcActions, sourceFile) srcSlice, ok1 := srcActions.([]interface{}) destSlice, ok2 := destActions.([]interface{}) if ok1 && ok2 { @@ -194,10 +205,11 @@ func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{}, } // mergeActionsFromSource merges actions from source into destination. -func mergeActionsFromSource(srcActions interface{}, dest map[string]interface{}) { +func mergeActionsFromSource(srcActions interface{}, dest map[string]interface{}, sourceFile string) { if destActions, ok := dest["actions"]; ok { - mergeActionsWhenBothExist(srcActions, destActions, dest) + mergeActionsWhenBothExist(srcActions, destActions, dest, sourceFile) } else { + stampSourceOnMaps(srcActions, sourceFile) dest["actions"] = srcActions } } @@ -223,7 +235,8 @@ func mergeDashboardsFromSource(srcDashboards interface{}, dest map[string]interf } // mergeEntitiesWhenBothExist merges entities when both src and dest have entities. -func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{}, dest map[string]interface{}) { +func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{}, dest map[string]interface{}, sourceFile string) { + stampSourceOnMaps(srcEntities, sourceFile) srcSlice, ok1 := srcEntities.([]interface{}) destSlice, ok2 := destEntities.([]interface{}) if ok1 && ok2 { @@ -234,17 +247,18 @@ func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{ } // mergeEntitiesFromSource merges entities from source into destination. -func mergeEntitiesFromSource(srcEntities interface{}, dest map[string]interface{}) { +func mergeEntitiesFromSource(srcEntities interface{}, dest map[string]interface{}, sourceFile string) { if destEntities, ok := dest["entities"]; ok { - mergeEntitiesWhenBothExist(srcEntities, destEntities, dest) + mergeEntitiesWhenBothExist(srcEntities, destEntities, dest, sourceFile) } else { + stampSourceOnMaps(srcEntities, sourceFile) dest["entities"] = srcEntities } } -func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error { +func mergeFunc(src map[string]interface{}, dest map[string]interface{}, sourceFile string) error { if srcActions, ok := src["actions"]; ok { - mergeActionsFromSource(srcActions, dest) + mergeActionsFromSource(srcActions, dest, sourceFile) } if srcDashboards, ok := src["dashboards"]; ok { @@ -252,7 +266,7 @@ func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error { } if srcEntities, ok := src["entities"]; ok { - mergeEntitiesFromSource(srcEntities, dest) + mergeEntitiesFromSource(srcEntities, dest, sourceFile) } return nil @@ -285,7 +299,12 @@ func envDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error val, set := os.LookupEnv(key) log.Debugf("Environment variable %q: set=%v, value=%q", key, set, val) if !set { - log.Warnf("Config file references unset environment variable: \"%s\"", key) + configissues.ReportSticky(configissues.Issue{ + Severity: configissues.SeverityWarning, + Code: configissues.CodeEnvUnset, + Message: fmt.Sprintf("Config file references unset environment variable %q", key), + Source: key, + }) } return val }) diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index c085f23..9b9e3fb 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -5,6 +5,7 @@ import ( "strings" "text/template" + "github.com/OliveTin/OliveTin/internal/configissues" "github.com/OliveTin/OliveTin/internal/env" "github.com/google/uuid" log "github.com/sirupsen/logrus" @@ -29,7 +30,6 @@ func (cfg *Config) Sanitize() { cfg.sanitizeDashboardsForInlineActions() cfg.sanitizeActionGroups() - cfg.sanitizeActionGroupReferences() cfg.sanitizeEntities() if err := cfg.validateReservedActionArgumentNames(); err != nil { @@ -238,7 +238,7 @@ func (action *Action) sanitize(cfg *Config) { action.Groups = dedupeStrings(action.Groups) for idx := range action.Arguments { - action.Arguments[idx].sanitize() + action.Arguments[idx].sanitize(action) } } @@ -283,14 +283,6 @@ func (cfg *Config) sanitizeActionGroups() { } } -func (cfg *Config) sanitizeActionGroupReferences() { - for _, action := range cfg.Actions { - for _, groupName := range action.Groups { - cfg.warnInvalidActionGroupReference(action, groupName) - } - } -} - func (cfg *Config) sanitizeEntities() { for _, entityFile := range cfg.Entities { if entityFile == nil { @@ -310,24 +302,6 @@ func sanitizeEntityProperties(entityFile *EntityFile) { } } -func (cfg *Config) warnInvalidActionGroupReference(action *Action, groupName string) { - group, found := cfg.ActionGroups[groupName] - if !found { - log.WithFields(log.Fields{ - "actionTitle": action.Title, - "groupName": groupName, - }).Warn("Action references unknown action group") - return - } - - if group == nil || group.MaxConcurrent < 1 { - log.WithFields(log.Fields{ - "actionTitle": action.Title, - "groupName": groupName, - }).Warn("Action references action group that will not be enforced at runtime") - } -} - func (cfg *Config) sanitizeAuthRequireGuestsToLogin() { if cfg.AuthRequireGuestsToLogin { log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false") @@ -505,7 +479,7 @@ func (cfg *Config) sanitizeOnClickDefaults() { cfg.DefaultPopupOnStart = cfg.DefaultOnClick } -func (arg *ActionArgument) sanitize() { +func (arg *ActionArgument) sanitize(action *Action) { if arg.Title == "" { arg.Title = arg.Name } @@ -516,45 +490,24 @@ func (arg *ActionArgument) sanitize() { } } - arg.sanitizeNoType() - arg.sanitizeChecklist() - - // Default value validation runs in executor at config load (validateArgumentDefaults). + arg.sanitizeNoType(action) + // Checklist configuration problems are reported by configcheck. } -func (arg *ActionArgument) sanitizeChecklist() { - if arg.Type != "checklist" { - return - } - - arg.warnMissingChecklistChoices() - arg.warnInvalidChecklistEntityTemplate() -} - -func (arg *ActionArgument) warnMissingChecklistChoices() { - if len(arg.Choices) == 0 { - log.WithFields(log.Fields{ - "arg": arg.Name, - }).Warn("Checklist argument has no choices defined") - } -} - -func (arg *ActionArgument) warnInvalidChecklistEntityTemplate() { - if arg.Entity == "" || len(arg.Choices) == 1 { - return - } - - log.WithFields(log.Fields{ - "arg": arg.Name, - "entity": arg.Entity, - }).Warn("Checklist argument with entity should define exactly one choice template") -} - -func (arg *ActionArgument) sanitizeNoType() { +func (arg *ActionArgument) sanitizeNoType(action *Action) { if len(arg.Choices) == 0 && arg.Type == "" { - log.WithFields(log.Fields{ - "arg": arg.Name, - }).Warn("Argument type isn't set, will default to 'ascii' but this may not be safe. You should set a type specifically.") + issue := configissues.Issue{ + Severity: configissues.SeverityWarning, + Code: configissues.CodeArgTypeUnset, + Message: "Argument type isn't set, will default to 'ascii' but this may not be safe", + ArgumentName: arg.Name, + } + if action != nil { + issue.ActionID = action.ID + issue.ActionTitle = action.Title + issue.ConfigFile = action.SourceFile + } + configissues.ReportSticky(issue) arg.Type = "ascii" } } diff --git a/service/internal/config/source_file.go b/service/internal/config/source_file.go new file mode 100644 index 0000000..2f9cc09 --- /dev/null +++ b/service/internal/config/source_file.go @@ -0,0 +1,39 @@ +package config + +import "github.com/knadh/koanf/v2" + +const sourceFileKey = "x-olivetin-source-file" + +// stampSourceOnMaps sets the OliveTin source-file marker on each map in a +// koanf slice value (actions or entities). +func stampSourceOnMaps(raw any, sourceFile string) any { + if sourceFile == "" { + return raw + } + + items, ok := raw.([]interface{}) + if !ok { + return raw + } + + for _, item := range items { + stampSourceOnMap(item, sourceFile) + } + return items +} + +func stampSourceOnMap(item any, sourceFile string) { + m, ok := item.(map[string]interface{}) + if !ok { + return + } + if existing, ok := m[sourceFileKey].(string); ok && existing != "" { + return + } + m[sourceFileKey] = sourceFile +} + +func stampLoadedConfigSources(k *koanf.Koanf, configPath string) { + _ = k.Set("actions", stampSourceOnMaps(k.Get("actions"), configPath)) + _ = k.Set("entities", stampSourceOnMaps(k.Get("entities"), configPath)) +} diff --git a/service/internal/config/source_file_test.go b/service/internal/config/source_file_test.go new file mode 100644 index 0000000..a237c55 --- /dev/null +++ b/service/internal/config/source_file_test.go @@ -0,0 +1,51 @@ +package config_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/OliveTin/OliveTin/internal/config" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAppendSourceStampsActionSourceFiles(t *testing.T) { + dir := t.TempDir() + includeDir := filepath.Join(dir, "config.d") + require.NoError(t, os.Mkdir(includeDir, 0o755)) + + basePath := filepath.Join(dir, "config.yaml") + require.NoError(t, os.WriteFile(basePath, []byte(` +include: config.d +actions: + - title: From base + shell: echo base +`), 0o644)) + + includePath := filepath.Join(includeDir, "10-extra.yaml") + require.NoError(t, os.WriteFile(includePath, []byte(` +actions: + - title: From include + shell: echo include +entities: + - name: host + file: hosts.yaml +`), 0o644)) + + k := koanf.New(".") + require.NoError(t, k.Load(file.Provider(basePath), yaml.Parser())) + + cfg := config.DefaultConfig() + config.AppendSource(cfg, k, basePath) + + require.Len(t, cfg.Actions, 2) + assert.Equal(t, basePath, cfg.Actions[0].SourceFile) + assert.Equal(t, includePath, cfg.Actions[1].SourceFile) + + require.Len(t, cfg.Entities, 1) + assert.Equal(t, includePath, cfg.Entities[0].SourceFile) +} diff --git a/service/internal/configcheck/rebuild.go b/service/internal/configcheck/rebuild.go new file mode 100644 index 0000000..92c3d82 --- /dev/null +++ b/service/internal/configcheck/rebuild.go @@ -0,0 +1,469 @@ +package configcheck + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/OliveTin/OliveTin/internal/config" + "github.com/OliveTin/OliveTin/internal/configissues" + "github.com/OliveTin/OliveTin/internal/entities" + "github.com/OliveTin/OliveTin/internal/tpl" + "github.com/robfig/cron/v3" + "gopkg.in/yaml.v3" +) + +// Rebuild clears and rebuilds the issue list from the current config, entity +// state, sticky load-time reports, and any extra issues provided by callers +// (for example literal argument default validation from the executor). +func Rebuild(cfg *config.Config, extra ...configissues.Issue) { + if cfg == nil { + configissues.Replace(extra) + return + } + + collected := make([]configissues.Issue, 0) + collected = append(collected, configissues.CopySticky()...) + collected = append(collected, collectActionGroupIssues(cfg)...) + collected = append(collected, collectArgumentIssues(cfg)...) + collected = append(collected, collectIncludeIssues(cfg)...) + collected = append(collected, collectTemplateParseIssues(cfg)...) + collected = append(collected, collectEntityFileIssues(cfg)...) + collected = append(collected, collectEntityEmptyIssues(cfg)...) + collected = append(collected, collectCronIssues(cfg)...) + collected = append(collected, collectWatcherPathIssues(cfg)...) + collected = append(collected, extra...) + configissues.Replace(collected) +} + +func collectActionGroupIssues(cfg *config.Config) []configissues.Issue { + out := make([]configissues.Issue, 0) + for _, action := range cfg.Actions { + if action == nil { + continue + } + for _, groupName := range action.Groups { + out = append(out, actionGroupIssue(cfg, action, groupName)...) + } + } + return out +} + +func actionGroupIssue(cfg *config.Config, action *config.Action, groupName string) []configissues.Issue { + group, found := cfg.ActionGroups[groupName] + if !found { + return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeActionGroupUnknown, + fmt.Sprintf("Action references unknown action group %q", groupName), groupName, "")} + } + + if group == nil || group.MaxConcurrent < 1 { + return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeActionGroupUnenforced, + fmt.Sprintf("Action references action group %q that will not be enforced at runtime", groupName), groupName, "")} + } + + return nil +} + +func actionIssue(action *config.Action, severity, code, message, source, argName string) configissues.Issue { + return configissues.Issue{ + Severity: severity, + Code: code, + Message: message, + ActionID: action.ID, + ActionTitle: action.Title, + ArgumentName: argName, + Source: source, + ConfigFile: action.SourceFile, + } +} + +func collectArgumentIssues(cfg *config.Config) []configissues.Issue { + out := make([]configissues.Issue, 0) + for _, action := range cfg.Actions { + if action == nil { + continue + } + for i := range action.Arguments { + out = append(out, argumentIssues(action, &action.Arguments[i])...) + } + } + return out +} + +func argumentIssues(action *config.Action, arg *config.ActionArgument) []configissues.Issue { + if arg.Type != "checklist" { + return nil + } + + out := make([]configissues.Issue, 0) + out = append(out, checklistNoChoicesIssue(action, arg)...) + out = append(out, checklistEntityChoicesIssue(action, arg)...) + return out +} + +func checklistNoChoicesIssue(action *config.Action, arg *config.ActionArgument) []configissues.Issue { + if len(arg.Choices) > 0 { + return nil + } + + return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeChecklistNoChoices, + "Checklist argument has no choices defined", "", arg.Name)} +} + +func checklistEntityChoicesIssue(action *config.Action, arg *config.ActionArgument) []configissues.Issue { + if arg.Entity == "" || len(arg.Choices) == 1 { + return nil + } + + return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeChecklistEntityChoices, + "Checklist argument with entity should define exactly one choice template", arg.Entity, arg.Name)} +} + +func collectIncludeIssues(cfg *config.Config) []configissues.Issue { + if cfg.Include == "" { + return nil + } + + includePath := filepath.Join(cfg.GetDir(), cfg.Include) + info, err := os.Stat(includePath) + if err != nil { + return []configissues.Issue{{ + Severity: configissues.SeverityWarning, + Code: configissues.CodeIncludeMissing, + Message: fmt.Sprintf("Include directory not found: %s", includePath), + Source: includePath, + ConfigFile: includePath, + }} + } + + if !info.IsDir() { + return []configissues.Issue{{ + Severity: configissues.SeverityWarning, + Code: configissues.CodeIncludeMissing, + Message: fmt.Sprintf("Include path is not a directory: %s", includePath), + Source: includePath, + ConfigFile: includePath, + }} + } + + return nil +} + +func collectTemplateParseIssues(cfg *config.Config) []configissues.Issue { + out := make([]configissues.Issue, 0) + for _, action := range cfg.Actions { + if action == nil { + continue + } + for i := range action.Arguments { + arg := &action.Arguments[i] + out = append(out, templateIssuesForArg(action, arg)...) + } + } + return out +} + +func templateIssuesForArg(action *config.Action, arg *config.ActionArgument) []configissues.Issue { + out := make([]configissues.Issue, 0) + + if looksLikeTemplate(arg.Default) { + if err := tpl.CheckActionTemplateParses(arg.Default); err != nil { + out = append(out, actionIssue(action, configissues.SeverityError, configissues.CodeTemplateParse, + fmt.Sprintf("Argument default template failed to parse: %v", err), arg.Default, arg.Name)) + } + } + + for _, choice := range arg.Choices { + out = append(out, templateIssueForChoice(action, arg, choice.Title, "title")...) + out = append(out, templateIssueForChoice(action, arg, choice.Value, "value")...) + } + + return out +} + +func templateIssueForChoice(action *config.Action, arg *config.ActionArgument, value, field string) []configissues.Issue { + if !looksLikeTemplate(value) { + return nil + } + + if err := tpl.CheckActionTemplateParses(value); err != nil { + return []configissues.Issue{actionIssue(action, configissues.SeverityError, configissues.CodeTemplateParse, + fmt.Sprintf("Argument choice %s template failed to parse: %v", field, err), value, arg.Name)} + } + + return nil +} + +func looksLikeTemplate(value string) bool { + return strings.Contains(value, "{{") +} + +func collectEntityFileIssues(cfg *config.Config) []configissues.Issue { + out := make([]configissues.Issue, 0) + baseDir := entities.ResolveEntitiesBaseDir(cfg.GetDir()) + + for _, ef := range cfg.Entities { + out = append(out, entityFileIssuesFor(ef, baseDir)...) + } + + return out +} + +func entityFileIssuesFor(ef *config.EntityFile, baseDir string) []configissues.Issue { + if ef == nil || ef.File == "" { + return nil + } + + path := resolveEntityPath(ef.File, baseDir) + issue := entityFileIssue(ef, path) + if issue == nil { + return nil + } + return []configissues.Issue{*issue} +} + +func resolveEntityPath(file, baseDir string) string { + if filepath.IsAbs(file) { + return file + } + return filepath.Join(baseDir, file) +} + +func entityFileIssue(ef *config.EntityFile, path string) *configissues.Issue { + data, err := os.ReadFile(path) + if err != nil { + return entityReadIssue(ef, path, err) + } + + if len(strings.TrimSpace(string(data))) == 0 { + return &configissues.Issue{ + Severity: configissues.SeverityWarning, + Code: configissues.CodeEntityFile, + Message: fmt.Sprintf("Entity %q file is empty", ef.Name), + Source: path, + ConfigFile: ef.SourceFile, + } + } + + if strings.HasSuffix(path, ".json") { + return entityJSONParseIssue(ef, path, data) + } + + return entityYAMLParseIssue(ef, path, data) +} + +func entityReadIssue(ef *config.EntityFile, path string, err error) *configissues.Issue { + return &configissues.Issue{ + Severity: configissues.SeverityError, + Code: configissues.CodeEntityFile, + Message: fmt.Sprintf("Entity %q file could not be read: %v", ef.Name, err), + Source: path, + ConfigFile: ef.SourceFile, + } +} + +func entityJSONParseIssue(ef *config.EntityFile, path string, data []byte) *configissues.Issue { + decoder := json.NewDecoder(bytes.NewReader(data)) + for decoder.More() { + d := make(map[string]any) + if err := decoder.Decode(&d); err != nil { + return &configissues.Issue{ + Severity: configissues.SeverityError, + Code: configissues.CodeEntityFile, + Message: fmt.Sprintf("Entity %q file could not be parsed as JSON: %v", ef.Name, err), + Source: path, + ConfigFile: ef.SourceFile, + } + } + } + return nil +} + +func entityYAMLParseIssue(ef *config.EntityFile, path string, data []byte) *configissues.Issue { + var parsed []map[string]any + if err := yaml.Unmarshal(data, &parsed); err != nil { + return &configissues.Issue{ + Severity: configissues.SeverityError, + Code: configissues.CodeEntityFile, + Message: fmt.Sprintf("Entity %q file could not be parsed as YAML: %v", ef.Name, err), + Source: path, + ConfigFile: ef.SourceFile, + } + } + return nil +} + +func collectEntityEmptyIssues(cfg *config.Config) []configissues.Issue { + out := make([]configissues.Issue, 0) + for _, action := range cfg.Actions { + out = append(out, entityEmptyIssueFor(cfg, action)...) + } + return out +} + +func entityEmptyIssueFor(cfg *config.Config, action *config.Action) []configissues.Issue { + if !shouldReportEntityEmpty(cfg, action) { + return nil + } + + return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeEntityEmpty, + fmt.Sprintf("Entity-bound action has no instances of entity %q", action.Entity), action.Entity, "")} +} + +func shouldReportEntityEmpty(cfg *config.Config, action *config.Action) bool { + if action == nil || action.Entity == "" { + return false + } + if len(entities.GetEntityInstances(action.Entity)) > 0 { + return false + } + return entityEmptyLoadGatePassed(cfg, action.Entity) +} + +func entityEmptyLoadGatePassed(cfg *config.Config, entityName string) bool { + // Avoid startup false positives: entity files load after the first action-map rebuild. + if entityTypeConfigured(cfg, entityName) && !entities.HasEntityLoadAttempted(entityName) { + return false + } + return true +} + +func entityTypeConfigured(cfg *config.Config, entityName string) bool { + for _, ef := range cfg.Entities { + if ef != nil && ef.Name == entityName { + return true + } + } + return false +} + +func collectCronIssues(cfg *config.Config) []configissues.Issue { + out := make([]configissues.Issue, 0) + parser := cronParser(cfg) + + for _, action := range cfg.Actions { + out = append(out, cronIssuesForAction(action, parser)...) + } + + return out +} + +func cronIssuesForAction(action *config.Action, parser cron.Parser) []configissues.Issue { + if action == nil { + return nil + } + + out := make([]configissues.Issue, 0) + out = append(out, cronEntityBindingIssue(action)...) + out = append(out, cronScheduleIssues(action, parser)...) + return out +} + +func cronEntityBindingIssue(action *config.Action) []configissues.Issue { + if action.Entity == "" || len(action.ExecOnCron) == 0 { + return nil + } + + return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeCronEntityBinding, + "Entity-bound action uses execOnCron; cron runs without an entity binding and will not execute", action.Entity, "")} +} + +func cronScheduleIssues(action *config.Action, parser cron.Parser) []configissues.Issue { + out := make([]configissues.Issue, 0) + for _, cronline := range action.ExecOnCron { + out = append(out, cronScheduleIssue(action, parser, cronline)...) + } + return out +} + +func cronScheduleIssue(action *config.Action, parser cron.Parser, cronline string) []configissues.Issue { + _, err := parser.Parse(cronline) + if err == nil { + return nil + } + + return []configissues.Issue{actionIssue(action, configissues.SeverityError, configissues.CodeCronInvalid, + fmt.Sprintf("Invalid cron schedule %q: %v", cronline, err), cronline, "")} +} + +func cronParser(cfg *config.Config) cron.Parser { + if cfg.CronSupportForSeconds { + return cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) + } + return cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) +} + +func collectWatcherPathIssues(cfg *config.Config) []configissues.Issue { + out := make([]configissues.Issue, 0) + for _, action := range cfg.Actions { + out = append(out, watcherPathIssuesForAction(action)...) + } + out = append(out, watcherPathIssuesForEntities(cfg)...) + return out +} + +func watcherPathIssuesForAction(action *config.Action) []configissues.Issue { + if action == nil { + return nil + } + + out := make([]configissues.Issue, 0) + for _, dir := range action.ExecOnFileCreatedInDir { + out = append(out, watcherDirIssue(action, dir)...) + } + for _, dir := range action.ExecOnFileChangedInDir { + out = append(out, watcherDirIssue(action, dir)...) + } + if action.ExecOnCalendarFile != "" { + out = append(out, watcherFileIssue(action, action.ExecOnCalendarFile)...) + } + return out +} + +func watcherPathIssuesForEntities(cfg *config.Config) []configissues.Issue { + out := make([]configissues.Issue, 0) + baseDir := entities.ResolveEntitiesBaseDir(cfg.GetDir()) + + for _, ef := range cfg.Entities { + if ef == nil || ef.File == "" { + continue + } + path := resolveEntityPath(ef.File, baseDir) + out = append(out, missingWatchDirIssue(path, filepath.Dir(path), ef.SourceFile, "", "")...) + } + return out +} + +func watcherDirIssue(action *config.Action, dir string) []configissues.Issue { + if dir == "" { + return nil + } + return missingWatchDirIssue(dir, dir, action.SourceFile, action.ID, action.Title) +} + +func watcherFileIssue(action *config.Action, filePath string) []configissues.Issue { + if filePath == "" { + return nil + } + return missingWatchDirIssue(filePath, filepath.Dir(filePath), action.SourceFile, action.ID, action.Title) +} + +func missingWatchDirIssue(displayPath, dirToStat, configFile, actionID, actionTitle string) []configissues.Issue { + _, err := os.Stat(dirToStat) + if err == nil { + return nil + } + + return []configissues.Issue{{ + Severity: configissues.SeverityError, + Code: configissues.CodeWatcherPath, + Message: fmt.Sprintf("Could not create watcher for %q: %v", displayPath, err), + ActionID: actionID, + ActionTitle: actionTitle, + Source: displayPath, + ConfigFile: configFile, + }} +} diff --git a/service/internal/configcheck/rebuild_test.go b/service/internal/configcheck/rebuild_test.go new file mode 100644 index 0000000..339484f --- /dev/null +++ b/service/internal/configcheck/rebuild_test.go @@ -0,0 +1,201 @@ +package configcheck_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/OliveTin/OliveTin/internal/config" + "github.com/OliveTin/OliveTin/internal/configcheck" + "github.com/OliveTin/OliveTin/internal/configissues" + "github.com/OliveTin/OliveTin/internal/entities" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRebuildTemplateParseDefault(t *testing.T) { + configissues.BeginConfigLoad() + cfg := config.DefaultConfig() + cfg.Actions = []*config.Action{ + { + Title: "Deploy", + ID: "deploy", + Entity: "host", + Arguments: []config.ActionArgument{ + { + Name: "ansible_host", + Type: "ascii_identifier", + Default: `{{ host.name | default "All" }}`, + }, + }, + }, + } + + configcheck.Rebuild(cfg) + + issues := configissues.List() + require.NotEmpty(t, issues) + assert.True(t, hasCode(issues, configissues.CodeTemplateParse)) + assert.True(t, hasCode(issues, configissues.CodeEntityEmpty)) + assert.False(t, hasCode(issues, configissues.CodeCronEntityBinding)) +} + +func TestRebuildValidTemplateDefault(t *testing.T) { + configissues.BeginConfigLoad() + cfg := config.DefaultConfig() + cfg.Actions = []*config.Action{ + { + Title: "Sleep", + ID: "sleep", + Arguments: []config.ActionArgument{ + { + Name: "ssh_host", + Type: "ascii_identifier", + Default: `{{ host.hostname }}`, + }, + }, + }, + } + + configcheck.Rebuild(cfg) + + assert.False(t, hasCode(configissues.List(), configissues.CodeTemplateParse)) +} + +func TestRebuildCronEntityBinding(t *testing.T) { + configissues.BeginConfigLoad() + cfg := config.DefaultConfig() + cfg.Actions = []*config.Action{ + { + Title: "Nightly", + ID: "nightly", + Entity: "host", + ExecOnCron: []string{"0 1 * * *"}, + }, + } + + configcheck.Rebuild(cfg) + + assert.True(t, hasCode(configissues.List(), configissues.CodeCronEntityBinding)) +} + +func TestRebuildInvalidCron(t *testing.T) { + configissues.BeginConfigLoad() + cfg := config.DefaultConfig() + cfg.Actions = []*config.Action{ + { + Title: "Broken cron", + ID: "broken", + ExecOnCron: []string{"not a cron line"}, + }, + } + + configcheck.Rebuild(cfg) + + assert.True(t, hasCode(configissues.List(), configissues.CodeCronInvalid)) +} + +func TestRebuildMissingEntityFile(t *testing.T) { + configissues.BeginConfigLoad() + cfg := config.DefaultConfig() + dir := t.TempDir() + cfg.SetDir(dir) + cfg.Entities = []*config.EntityFile{ + {Name: "host", File: "missing-hosts.yaml"}, + } + + configcheck.Rebuild(cfg) + + assert.True(t, hasCode(configissues.List(), configissues.CodeEntityFile)) +} + +func TestRebuildEntityFileLoadsAndClearsEmpty(t *testing.T) { + configissues.BeginConfigLoad() + cfg := config.DefaultConfig() + dir := t.TempDir() + cfg.SetDir(dir) + + entityPath := filepath.Join(dir, "hosts.yaml") + require.NoError(t, os.WriteFile(entityPath, []byte("- name: cradle\n"), 0o644)) + + cfg.Entities = []*config.EntityFile{ + {Name: "host", File: "hosts.yaml"}, + } + cfg.Actions = []*config.Action{ + {Title: "Wake", ID: "wake", Entity: "host"}, + } + + entities.AddEntity("host", "0", map[string]any{"name": "cradle"}) + t.Cleanup(func() { entities.ClearEntitiesOfType("host") }) + + configcheck.Rebuild(cfg) + + issues := configissues.List() + assert.False(t, hasCode(issues, configissues.CodeEntityFile)) + assert.False(t, hasCode(issues, configissues.CodeEntityEmpty)) +} + +func TestRebuildIncludesActionConfigFile(t *testing.T) { + configissues.BeginConfigLoad() + cfg := config.DefaultConfig() + cfg.Actions = []*config.Action{ + { + Title: "Nightly", + ID: "nightly", + Entity: "host", + SourceFile: "/etc/OliveTin/config.d/16-gitops-sr-soe.yaml", + ExecOnCron: []string{"0 1 * * *"}, + }, + } + + configcheck.Rebuild(cfg) + + var found bool + for _, issue := range configissues.List() { + if issue.Code == configissues.CodeCronEntityBinding { + assert.Equal(t, "/etc/OliveTin/config.d/16-gitops-sr-soe.yaml", issue.ConfigFile) + found = true + } + } + assert.True(t, found) +} + +func TestRebuildSkipsEntityEmptyBeforeLoadAttempt(t *testing.T) { + entities.ResetEntityLoadAttempts() + configissues.BeginConfigLoad() + cfg := config.DefaultConfig() + cfg.Entities = []*config.EntityFile{{Name: "container", File: "containers.json"}} + cfg.Actions = []*config.Action{ + {Title: "Start container", ID: "start", Entity: "container"}, + } + + configcheck.Rebuild(cfg) + assert.False(t, hasCode(configissues.List(), configissues.CodeEntityEmpty)) + + entities.MarkEntityLoadAttempted("container") + configcheck.Rebuild(cfg) + assert.True(t, hasCode(configissues.List(), configissues.CodeEntityEmpty)) +} + +func TestStickyEnvUnsetSurvivesRebuild(t *testing.T) { + configissues.BeginConfigLoad() + configissues.ReportSticky(configissues.Issue{ + Severity: configissues.SeverityWarning, + Code: configissues.CodeEnvUnset, + Message: "unset", + Source: "MISSING_VAR", + }) + + configcheck.Rebuild(config.DefaultConfig()) + + assert.True(t, hasCode(configissues.List(), configissues.CodeEnvUnset)) +} + +func hasCode(issues []configissues.Issue, code string) bool { + for _, issue := range issues { + if issue.Code == code { + return true + } + } + return false +} diff --git a/service/internal/configcheck/watcher_path_test.go b/service/internal/configcheck/watcher_path_test.go new file mode 100644 index 0000000..2827cca --- /dev/null +++ b/service/internal/configcheck/watcher_path_test.go @@ -0,0 +1,46 @@ +package configcheck_test + +import ( + "path/filepath" + "testing" + + "github.com/OliveTin/OliveTin/internal/config" + "github.com/OliveTin/OliveTin/internal/configcheck" + "github.com/OliveTin/OliveTin/internal/configissues" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRebuildWatcherPathMissingDir(t *testing.T) { + configissues.BeginConfigLoad() + configissues.Replace(nil) + + missing := filepath.Join(t.TempDir(), "does-not-exist") + cfg := config.DefaultConfig() + cfg.Actions = []*config.Action{ + { + Title: "On create", + ID: "on_create", + SourceFile: "/etc/OliveTin/config.yaml", + ExecOnFileCreatedInDir: []string{missing}, + }, + } + + configcheck.Rebuild(cfg) + + issues := configissues.List() + require.True(t, hasCode(issues, configissues.CodeWatcherPath)) + + var found *configissues.Issue + for i := range issues { + if issues[i].Code == configissues.CodeWatcherPath { + found = &issues[i] + break + } + } + require.NotNil(t, found) + assert.Equal(t, missing, found.Source) + assert.Equal(t, "/etc/OliveTin/config.yaml", found.ConfigFile) + assert.Contains(t, found.Message, missing) + assert.Contains(t, found.Message, "Could not create watcher") +} diff --git a/service/internal/configissues/issue.go b/service/internal/configissues/issue.go new file mode 100644 index 0000000..63489f1 --- /dev/null +++ b/service/internal/configissues/issue.go @@ -0,0 +1,33 @@ +package configissues + +const ( + SeverityWarning = "warning" + SeverityError = "error" + + CodeActionGroupUnknown = "action_group_unknown" + CodeActionGroupUnenforced = "action_group_unenforced" + CodeArgTypeUnset = "arg_type_unset" + CodeChecklistNoChoices = "checklist_no_choices" + CodeChecklistEntityChoices = "checklist_entity_choices" + CodeEnvUnset = "env_unset" + CodeIncludeMissing = "include_missing" + CodeTemplateParse = "template_parse" + CodeArgDefaultInvalid = "arg_default_invalid" + CodeEntityFile = "entity_file" + CodeEntityEmpty = "entity_empty" + CodeCronInvalid = "cron_invalid" + CodeCronEntityBinding = "cron_entity_binding" + CodeWatcherPath = "watcher_path" +) + +// Issue is a configuration warning or error surfaced on Diagnostics. +type Issue struct { + Severity string + Code string + Message string + ActionID string + ActionTitle string + ArgumentName string + Source string + ConfigFile string +} diff --git a/service/internal/configissues/store.go b/service/internal/configissues/store.go new file mode 100644 index 0000000..6d112c6 --- /dev/null +++ b/service/internal/configissues/store.go @@ -0,0 +1,125 @@ +package configissues + +import ( + "sync" + + log "github.com/sirupsen/logrus" +) + +var ( + storeMu sync.RWMutex + issues []Issue + + stickyMu sync.Mutex + sticky []Issue +) + +// BeginConfigLoad clears sticky issues captured during config decode/include load. +func BeginConfigLoad() { + stickyMu.Lock() + defer stickyMu.Unlock() + sticky = nil +} + +// ReportSticky records an issue that cannot be re-derived after config decode +// (for example unset environment variables that were already expanded). +func ReportSticky(issue Issue) { + stickyMu.Lock() + defer stickyMu.Unlock() + sticky = append(sticky, issue) +} + +func copySticky() []Issue { + stickyMu.Lock() + defer stickyMu.Unlock() + out := make([]Issue, len(sticky)) + copy(out, sticky) + return out +} + +// CopySticky returns a copy of sticky load-time issues. +func CopySticky() []Issue { + return copySticky() +} + +// Replace sets the current issue list and logs only newly appeared issues. +func Replace(next []Issue) { + storeMu.Lock() + defer storeMu.Unlock() + + previous := make(map[string]struct{}, len(issues)) + for _, issue := range issues { + previous[issueFingerprint(issue)] = struct{}{} + } + + issues = make([]Issue, len(next)) + copy(issues, next) + + for _, issue := range issues { + if _, exists := previous[issueFingerprint(issue)]; exists { + continue + } + logNewIssue(issue) + } +} + +func issueFingerprint(issue Issue) string { + return issue.Severity + "\x00" + + issue.Code + "\x00" + + issue.ActionID + "\x00" + + issue.ActionTitle + "\x00" + + issue.ArgumentName + "\x00" + + issue.Source + "\x00" + + issue.ConfigFile + "\x00" + + issue.Message +} + +func logNewIssue(issue Issue) { + fields := log.Fields{ + "code": issue.Code, + "actionTitle": issue.ActionTitle, + "argumentName": issue.ArgumentName, + "configFile": issue.ConfigFile, + "source": issue.Source, + } + + if issue.Severity == SeverityError { + log.WithFields(fields).Error(issue.Message) + return + } + log.WithFields(fields).Warn(issue.Message) +} + +// Report adds a live issue if it is not already present and logs it when new. +// Use this for runtime discoveries that happen outside Rebuild (for example +// filesystem watcher setup failures). +func Report(issue Issue) { + storeMu.Lock() + defer storeMu.Unlock() + + fp := issueFingerprint(issue) + for _, existing := range issues { + if issueFingerprint(existing) == fp { + return + } + } + + issues = append(issues, issue) + logNewIssue(issue) +} + +// List returns a copy of the current issues. +func List() []Issue { + storeMu.RLock() + defer storeMu.RUnlock() + out := make([]Issue, len(issues)) + copy(out, issues) + return out +} + +// Count returns the number of current issues. +func Count() int { + storeMu.RLock() + defer storeMu.RUnlock() + return len(issues) +} diff --git a/service/internal/configissues/store_test.go b/service/internal/configissues/store_test.go new file mode 100644 index 0000000..b33c3d2 --- /dev/null +++ b/service/internal/configissues/store_test.go @@ -0,0 +1,39 @@ +package configissues_test + +import ( + "testing" + + "github.com/OliveTin/OliveTin/internal/configissues" + "github.com/stretchr/testify/assert" +) + +func TestStoreReplaceListCount(t *testing.T) { + configissues.BeginConfigLoad() + configissues.Replace([]configissues.Issue{{ + Severity: configissues.SeverityError, + Code: configissues.CodeTemplateParse, + Message: "broken", + }}) + + assert.Equal(t, 1, configissues.Count()) + assert.Equal(t, configissues.CodeTemplateParse, configissues.List()[0].Code) + + configissues.Replace(nil) + assert.Equal(t, 0, configissues.Count()) +} + +func TestStickyCopy(t *testing.T) { + configissues.BeginConfigLoad() + configissues.ReportSticky(configissues.Issue{ + Code: configissues.CodeEnvUnset, + Message: "missing", + Source: "FOO", + }) + + copied := configissues.CopySticky() + assert.Len(t, copied, 1) + assert.Equal(t, configissues.CodeEnvUnset, copied[0].Code) + + configissues.BeginConfigLoad() + assert.Empty(t, configissues.CopySticky()) +} diff --git a/service/internal/entities/entities.go b/service/internal/entities/entities.go index 386fb20..f00b313 100644 --- a/service/internal/entities/entities.go +++ b/service/internal/entities/entities.go @@ -30,13 +30,18 @@ func AddListener(l func()) { } func SetupEntityFileWatchers(cfg *config.Config) { - baseDir := resolveEntitiesBaseDir(cfg.GetDir()) + baseDir := ResolveEntitiesBaseDir(cfg.GetDir()) for i := range cfg.Entities { // #337 - iterate by key, not by value ef := cfg.Entities[i] watchAndLoadEntity(baseDir, ef) } } +// ResolveEntitiesBaseDir returns the directory used to resolve relative entity file paths. +func ResolveEntitiesBaseDir(configDir string) string { + return resolveEntitiesBaseDir(configDir) +} + //gocyclo:ignore func resolveEntitiesBaseDir(configDir string) string { absConfigDir, err := filepath.Abs(configDir) @@ -64,15 +69,26 @@ func watchAndLoadEntity(baseDir string, ef *config.EntityFile) { p = filepath.Join(baseDir, p) log.WithFields(log.Fields{"entityFile": p}).Debugf("Adding config dir to entity file path") } - go filehelper.WatchFileWrite(p, func(filename string) { loadEntityFile(p, ef.Name) }) + go filehelper.WatchFileWrite(p, func(filename string) { loadEntityFile(p, ef.Name) }, filehelper.WatchMeta{}) loadEntityFile(p, ef.Name) } func loadEntityFile(filename string, entityname string) { + defer func() { + MarkEntityLoadAttempted(entityname) + notifyEntityListeners() + }() + if strings.HasSuffix(filename, ".json") { loadEntityFileJson(filename, entityname) - } else { - loadEntityFileYaml(filename, entityname) + return + } + loadEntityFileYaml(filename, entityname) +} + +func notifyEntityListeners() { + for _, l := range listeners { + l() } } @@ -86,6 +102,7 @@ func loadEntityFileJson(filename string, entityname string) { if err != nil { log.Errorf("ReadIn: %v", err) + ClearEntitiesOfType(entityname) return } @@ -100,13 +117,14 @@ func loadEntityFileJson(filename string, entityname string) { if err != nil { log.Errorf("%v", err) + ClearEntitiesOfType(entityname) return } data = append(data, d) } - updateSvFromFile(entityname, data) + replaceEntitiesFromFile(entityname, data) } func loadEntityFileYaml(filename string, entityname string) { @@ -119,6 +137,7 @@ func loadEntityFileYaml(filename string, entityname string) { if err != nil { log.Errorf("ReadIn: %v", err) + ClearEntitiesOfType(entityname) return } @@ -128,22 +147,19 @@ func loadEntityFileYaml(filename string, entityname string) { if err != nil { log.Errorf("Unmarshal: %v", err) + ClearEntitiesOfType(entityname) return } - updateSvFromFile(entityname, data) + replaceEntitiesFromFile(entityname, data) } -func updateSvFromFile(entityname string, data []map[string]any) { +func replaceEntitiesFromFile(entityname string, data []map[string]any) { ClearEntitiesOfType(entityname) for i, mapp := range data { AddEntity(entityname, fmt.Sprintf("%d", i), mapp) } - - for _, l := range listeners { - l() - } } /* diff --git a/service/internal/entities/load_state.go b/service/internal/entities/load_state.go new file mode 100644 index 0000000..979bc75 --- /dev/null +++ b/service/internal/entities/load_state.go @@ -0,0 +1,33 @@ +package entities + +import "sync" + +var ( + loadAttemptedMu sync.RWMutex + loadAttempted = map[string]bool{} +) + +// MarkEntityLoadAttempted records that OliveTin has tried to load this entity +// type from its entity file (successfully or not). +func MarkEntityLoadAttempted(entityName string) { + if entityName == "" { + return + } + loadAttemptedMu.Lock() + defer loadAttemptedMu.Unlock() + loadAttempted[entityName] = true +} + +// HasEntityLoadAttempted reports whether a load has been attempted for the entity type. +func HasEntityLoadAttempted(entityName string) bool { + loadAttemptedMu.RLock() + defer loadAttemptedMu.RUnlock() + return loadAttempted[entityName] +} + +// ResetEntityLoadAttempts clears load-attempt tracking (used by tests). +func ResetEntityLoadAttempts() { + loadAttemptedMu.Lock() + defer loadAttemptedMu.Unlock() + loadAttempted = map[string]bool{} +} diff --git a/service/internal/executor/argument_defaults_test.go b/service/internal/executor/argument_defaults_test.go new file mode 100644 index 0000000..b846c36 --- /dev/null +++ b/service/internal/executor/argument_defaults_test.go @@ -0,0 +1,45 @@ +package executor + +import ( + "testing" + + config "github.com/OliveTin/OliveTin/internal/config" + "github.com/OliveTin/OliveTin/internal/configissues" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateArgumentDefaultSkipsTemplates(t *testing.T) { + action := &config.Action{Title: "Deploy", ID: "deploy"} + arg := &config.ActionArgument{ + Name: "host", + Type: "ascii_identifier", + Default: `{{ host.name }}`, + } + + assert.Nil(t, validateArgumentDefault(action, arg)) +} + +func TestValidateArgumentDefaultLiteralInvalid(t *testing.T) { + action := &config.Action{Title: "Deploy", ID: "deploy"} + arg := &config.ActionArgument{ + Name: "host", + Type: "ascii_identifier", + Default: "not a valid id!!", + } + + issue := validateArgumentDefault(action, arg) + require.NotNil(t, issue) + assert.Equal(t, configissues.CodeArgDefaultInvalid, issue.Code) +} + +func TestValidateArgumentDefaultLiteralValid(t *testing.T) { + action := &config.Action{Title: "Deploy", ID: "deploy"} + arg := &config.ActionArgument{ + Name: "host", + Type: "ascii_identifier", + Default: "example.com", + } + + assert.Nil(t, validateArgumentDefault(action, arg)) +} diff --git a/service/internal/executor/executor_actions.go b/service/internal/executor/executor_actions.go index 75017d3..08a129e 100644 --- a/service/internal/executor/executor_actions.go +++ b/service/internal/executor/executor_actions.go @@ -3,10 +3,12 @@ package executor import ( "crypto/sha256" "fmt" + "strings" - config "github.com/OliveTin/OliveTin/internal/config" + "github.com/OliveTin/OliveTin/internal/config" + "github.com/OliveTin/OliveTin/internal/configcheck" + "github.com/OliveTin/OliveTin/internal/configissues" "github.com/OliveTin/OliveTin/internal/entities" - log "github.com/sirupsen/logrus" ) func (e *Executor) FindBindingByID(id string) *ActionBinding { @@ -40,40 +42,54 @@ type RebuildActionMapRequest struct { dashboardTargets *dashboardTargetIndex } -func validateArgumentDefaults(cfg *config.Config) { +func validateArgumentDefaults(cfg *config.Config) []configissues.Issue { if cfg == nil { - return + return nil } + out := make([]configissues.Issue, 0) for _, action := range cfg.Actions { - validateActionArgumentDefaults(action) + out = append(out, validateActionArgumentDefaults(action)...) } + return out } -func validateActionArgumentDefaults(action *config.Action) { +func validateActionArgumentDefaults(action *config.Action) []configissues.Issue { if action == nil { - return + return nil } + out := make([]configissues.Issue, 0) for i := range action.Arguments { - validateArgumentDefault(action, &action.Arguments[i]) + if issue := validateArgumentDefault(action, &action.Arguments[i]); issue != nil { + out = append(out, *issue) + } } + return out } -func validateArgumentDefault(action *config.Action, arg *config.ActionArgument) { +func validateArgumentDefault(action *config.Action, arg *config.ActionArgument) *configissues.Issue { if arg.Default == "" { - return + return nil + } + if strings.Contains(arg.Default, "{{") { + return nil } if err := ValidateArgument(arg, arg.Default, action); err != nil { - log.WithFields(log.Fields{ - "actionTitle": action.Title, - "argName": arg.Name, - "default": arg.Default, - "error": err, - }).Warn("Argument default value failed validation") + return &configissues.Issue{ + Severity: configissues.SeverityWarning, + Code: configissues.CodeArgDefaultInvalid, + Message: fmt.Sprintf("Argument default value failed validation: %v", err), + ActionID: action.ID, + ActionTitle: action.Title, + ArgumentName: arg.Name, + Source: arg.Default, + ConfigFile: action.SourceFile, + } } + return nil } func (e *Executor) RebuildActionMap() { - validateArgumentDefaults(e.Cfg) + defaultIssues := validateArgumentDefaults(e.Cfg) e.MapActionBindingsLock.Lock() @@ -94,6 +110,8 @@ func (e *Executor) RebuildActionMap() { e.MapActionBindingsLock.Unlock() + configcheck.Rebuild(e.Cfg, defaultIssues...) + for _, l := range e.copyListeners() { l.OnActionMapRebuilt() } diff --git a/service/internal/filehelper/file_change_notify.go b/service/internal/filehelper/file_change_notify.go index a39eaa1..5173903 100644 --- a/service/internal/filehelper/file_change_notify.go +++ b/service/internal/filehelper/file_change_notify.go @@ -1,12 +1,14 @@ package filehelper import ( - "github.com/fsnotify/fsnotify" - log "github.com/sirupsen/logrus" + "fmt" "path/filepath" + "sync" "time" - "sync" + "github.com/OliveTin/OliveTin/internal/configissues" + "github.com/fsnotify/fsnotify" + log "github.com/sirupsen/logrus" ) var ( @@ -28,33 +30,44 @@ const ( debounceDelay = 300 * time.Millisecond ) +// WatchMeta attaches action context to watcher setup failures so Diagnostics +// can apply the same action view ACL as other configuration issues. +type WatchMeta struct { + ActionID string + ActionTitle string + ConfigFile string +} + type watchContext struct { filename string filedir string callback func(filename string) interestedEvent fsnotify.Op event *fsnotify.Event + meta WatchMeta } -func WatchDirectoryCreate(fullpath string, callback func(filename string)) { +func WatchDirectoryCreate(fullpath string, callback func(filename string), meta WatchMeta) { watchPath(&watchContext{ filedir: fullpath, filename: "", callback: callback, interestedEvent: fsnotify.Create, + meta: meta, }) } -func WatchDirectoryWrite(fullpath string, callback func(filename string)) { +func WatchDirectoryWrite(fullpath string, callback func(filename string), meta WatchMeta) { watchPath(&watchContext{ filedir: fullpath, filename: "", callback: callback, interestedEvent: fsnotify.Write, + meta: meta, }) } -func WatchFileWrite(fullpath string, callback func(filename string)) { +func WatchFileWrite(fullpath string, callback func(filename string), meta WatchMeta) { filename := filepath.Base(fullpath) filedir := filepath.Dir(fullpath) @@ -63,69 +76,94 @@ func WatchFileWrite(fullpath string, callback func(filename string)) { filename: filename, callback: callback, interestedEvent: fsnotify.Write, + meta: meta, }) } func watchPath(ctx *watchContext) { watcher, err := fsnotify.NewWatcher() - if err != nil { - log.Errorf("Could not watch for files being created: %v", err) + reportWatcherFailure(ctx, err) return } - defer func() { - if err := watcher.Close(); err != nil { - log.Errorf("Failed to close file watcher: %v", err) - } - }() + defer closeWatcher(watcher) - done := make(chan bool) - - go func() { - for { - processEvent(ctx, watcher) - } - }() - - err = watcher.Add(ctx.filedir) - - if err != nil { - log.Errorf("Could not create watcher: %v", err) + if err := watcher.Add(ctx.filedir); err != nil { + reportWatcherFailure(ctx, err) + return } - <-done + for processEvent(ctx, watcher) { + } } -func processEvent(ctx *watchContext, watcher *fsnotify.Watcher) { +func closeWatcher(watcher *fsnotify.Watcher) { + if err := watcher.Close(); err != nil { + log.Errorf("Failed to close file watcher: %v", err) + } +} + +func watchTarget(ctx *watchContext) string { + if ctx.filename == "" { + return ctx.filedir + } + return filepath.Join(ctx.filedir, ctx.filename) +} + +func reportWatcherFailure(ctx *watchContext, err error) { + path := watchTarget(ctx) + message := fmt.Sprintf("Could not create watcher for %q: %v", path, err) + configissues.Report(configissues.Issue{ + Severity: configissues.SeverityError, + Code: configissues.CodeWatcherPath, + Message: message, + ActionID: ctx.meta.ActionID, + ActionTitle: ctx.meta.ActionTitle, + Source: path, + ConfigFile: ctx.meta.ConfigFile, + }) +} + +// processEvent waits for one watcher event. It returns false when the watcher +// channels are closed so the caller can stop looping. +func processEvent(ctx *watchContext, watcher *fsnotify.Watcher) bool { select { case event, ok := <-watcher.Events: - ctx.event = &event - - if !consumeEvent(ok, ctx) { - return - } - - break - case err := <-watcher.Errors: - log.Errorf("Error in fsnotify: %v", err) - return + return handleWatcherEvent(ctx, event, ok) + case err, ok := <-watcher.Errors: + return handleWatcherError(ctx, err, ok) } } -func consumeEvent(ok bool, ctx *watchContext) bool { +func handleWatcherEvent(ctx *watchContext, event fsnotify.Event, ok bool) bool { if !ok { return false } + ctx.event = &event + consumeEvent(ctx) + return true +} +func handleWatcherError(ctx *watchContext, err error, ok bool) bool { + if !ok { + return false + } + if err != nil { + log.WithFields(log.Fields{ + "path": watchTarget(ctx), + }).Errorf("Error in fsnotify: %v", err) + } + return true +} + +func consumeEvent(ctx *watchContext) { if ctx.filename != "" && filepath.Base(ctx.event.Name) != ctx.filename { log.Tracef("fsnotify irreleventa event different file %+v", ctx.event) - return true + return } consumeRelevantEvents(ctx) - - return true } func consumeRelevantEvents(ctx *watchContext) { diff --git a/service/internal/filehelper/watcher_path_test.go b/service/internal/filehelper/watcher_path_test.go new file mode 100644 index 0000000..a79af2a --- /dev/null +++ b/service/internal/filehelper/watcher_path_test.go @@ -0,0 +1,63 @@ +package filehelper_test + +import ( + "path/filepath" + "testing" + "time" + + "github.com/OliveTin/OliveTin/internal/configissues" + "github.com/OliveTin/OliveTin/internal/filehelper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWatchDirectoryCreateReportsMissingPath(t *testing.T) { + configissues.Replace(nil) + + missing := filepath.Join(t.TempDir(), "missing-watch-dir") + done := make(chan struct{}) + go func() { + filehelper.WatchDirectoryCreate(missing, func(string) {}, filehelper.WatchMeta{}) + close(done) + }() + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("WatchDirectoryCreate did not return after watcher failure") + } + + issues := configissues.List() + require.Len(t, issues, 1) + assert.Equal(t, configissues.CodeWatcherPath, issues[0].Code) + assert.Equal(t, missing, issues[0].Source) + assert.Contains(t, issues[0].Message, missing) + assert.Empty(t, issues[0].ActionID) +} + +func TestWatchDirectoryCreateReportsActionMeta(t *testing.T) { + configissues.Replace(nil) + + missing := filepath.Join(t.TempDir(), "missing-secret-watch") + done := make(chan struct{}) + go func() { + filehelper.WatchDirectoryCreate(missing, func(string) {}, filehelper.WatchMeta{ + ActionID: "secret_action", + ActionTitle: "Secret Action", + ConfigFile: "/etc/OliveTin/secret.yaml", + }) + close(done) + }() + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("WatchDirectoryCreate did not return after watcher failure") + } + + issues := configissues.List() + require.Len(t, issues, 1) + assert.Equal(t, "secret_action", issues[0].ActionID) + assert.Equal(t, "Secret Action", issues[0].ActionTitle) + assert.Equal(t, "/etc/OliveTin/secret.yaml", issues[0].ConfigFile) +} diff --git a/service/internal/oncalendarfile/calendar.go b/service/internal/oncalendarfile/calendar.go index 21d8ba0..32bbc6d 100644 --- a/service/internal/oncalendarfile/calendar.go +++ b/service/internal/oncalendarfile/calendar.go @@ -37,7 +37,11 @@ func Schedule(cfg *config.Config, ex *executor.Executor) { parseCalendarFile(captured, cfg, ex, filename) } - go filehelper.WatchFileWrite(action.ExecOnCalendarFile, x) + go filehelper.WatchFileWrite(action.ExecOnCalendarFile, x, filehelper.WatchMeta{ + ActionID: action.ID, + ActionTitle: action.Title, + ConfigFile: action.SourceFile, + }) x(action.ExecOnCalendarFile) } diff --git a/service/internal/onfileindir/fileindir.go b/service/internal/onfileindir/fileindir.go index b6085d1..ca786d6 100644 --- a/service/internal/onfileindir/fileindir.go +++ b/service/internal/onfileindir/fileindir.go @@ -17,7 +17,7 @@ func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) { go func(act *config.Action, dir string) { filehelper.WatchDirectoryCreate(dir, func(filename string) { scheduleExec(act, cfg, ex, filename) - }) + }, watchMetaForAction(act)) }(action, dirname) } for _, dirname := range action.ExecOnFileChangedInDir { @@ -27,12 +27,20 @@ func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) { go func(act *config.Action, dir string) { filehelper.WatchDirectoryWrite(dir, func(filename string) { scheduleExec(act, cfg, ex, filename) - }) + }, watchMetaForAction(act)) }(action, dirname) } } } +func watchMetaForAction(action *config.Action) filehelper.WatchMeta { + return filehelper.WatchMeta{ + ActionID: action.ID, + ActionTitle: action.Title, + ConfigFile: action.SourceFile, + } +} + func scheduleExec(action *config.Action, cfg *config.Config, ex *executor.Executor, path string) { args := map[string]string{ "filepath": path, diff --git a/service/internal/tpl/check_template_test.go b/service/internal/tpl/check_template_test.go new file mode 100644 index 0000000..1ed8208 --- /dev/null +++ b/service/internal/tpl/check_template_test.go @@ -0,0 +1,17 @@ +package tpl_test + +import ( + "testing" + + "github.com/OliveTin/OliveTin/internal/tpl" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCheckActionTemplateParses(t *testing.T) { + assert.NoError(t, tpl.CheckActionTemplateParses(`{{ host.name }}`)) + assert.NoError(t, tpl.CheckActionTemplateParses(`{{ host.hostname }}`)) + + err := tpl.CheckActionTemplateParses(`{{ host.name | default "All" }}`) + require.Error(t, err) +} diff --git a/service/internal/tpl/templates.go b/service/internal/tpl/templates.go index e2e23d3..f2ca256 100644 --- a/service/internal/tpl/templates.go +++ b/service/internal/tpl/templates.go @@ -219,6 +219,25 @@ func ParseTemplateOfActionBeforeExec(source string, ent *entities.Entity) string return result } +// CheckActionTemplateParses migrates legacy template syntax and reports parse +// errors only (execution against entity/argument data is not attempted). +func CheckActionTemplateParses(source string) error { + if source == "" { + return nil + } + + source = migrateLegacyArgumentNames(source) + source = migrateLegacyEntityProperties(source) + + clone, err := tpl.Clone() + if err != nil { + return err + } + + _, err = clone.Parse(source) + return err +} + /* func ParseTemplateBoolWith(source string, ent *entities.Entity) bool { source = strings.TrimSpace(source) From e1f8c01fe551bccaea512b68704ce91196c06ad3 Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 24 Jul 2026 16:26:13 +0100 Subject: [PATCH 06/17] chore: coderabbit suggestions --- .../resources/vue/views/DiagnosticsView.vue | 11 +- lang/combined_output.json | 104 +++++++++--------- lang/de-DE.yaml | 16 +-- lang/es-ES.yaml | 22 ++-- lang/it-IT.yaml | 22 ++-- lang/zh-Hans-CN.yaml | 22 ++-- lang/zh-Hant-TW.yaml | 22 ++-- .../internal/api/api_config_issues_test.go | 16 +-- service/internal/config/source_file.go | 18 ++- service/internal/configcheck/rebuild.go | 27 ++++- service/internal/configissues/store.go | 4 +- service/internal/entities/entities.go | 21 +++- 12 files changed, 178 insertions(+), 127 deletions(-) diff --git a/frontend/resources/vue/views/DiagnosticsView.vue b/frontend/resources/vue/views/DiagnosticsView.vue index e66f78e..4c68e44 100644 --- a/frontend/resources/vue/views/DiagnosticsView.vue +++ b/frontend/resources/vue/views/DiagnosticsView.vue @@ -108,7 +108,7 @@ diff --git a/lang/combined_output.json b/lang/combined_output.json index ede7c13..f47b5df 100644 --- a/lang/combined_output.json +++ b/lang/combined_output.json @@ -5,17 +5,17 @@ "connected": "Verbunden", "diagnostics.browser-info": "Browser-Informationen", "diagnostics.browser-info-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Browser-Informationen zu erstellen. Dies kann bei der Fehlerbehebung von browser-spezifischen Problemen hilfreich sein.", - "diagnostics.config-issue-action": "Action", + "diagnostics.config-issue-action": "Aktion", "diagnostics.config-issue-argument": "Argument", "diagnostics.config-issue-code": "Code", - "diagnostics.config-issue-config-file": "Config file", + "diagnostics.config-issue-config-file": "Konfigurationsdatei", "diagnostics.config-issue-detail": "Detail", - "diagnostics.config-issue-message": "Message", - "diagnostics.config-issue-severity": "Severity", - "diagnostics.config-issue-source": "Source", - "diagnostics.config-issues": "Configuration issues", - "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.", - "diagnostics.config-issues-none": "No configuration issues detected.", + "diagnostics.config-issue-message": "Meldung", + "diagnostics.config-issue-severity": "Schweregrad", + "diagnostics.config-issue-source": "Quelle", + "diagnostics.config-issues": "Konfigurationsprobleme", + "diagnostics.config-issues-description": "In der aktuellen OliveTin-Konfiguration erkannte Probleme. Beheben Sie diese in Ihren Konfigurationsdateien und laden Sie OliveTin neu.", + "diagnostics.config-issues-none": "Keine Konfigurationsprobleme erkannt.", "diagnostics.copied": "Kopiert!", "diagnostics.copy-to-clipboard": "In Zwischenablage kopieren", "diagnostics.found-config": "Konfiguration gefunden", @@ -185,17 +185,17 @@ "connected": "Conectado", "diagnostics.browser-info": "Información del navegador", "diagnostics.browser-info-description": "Esta sección le permite generar un informe detallado de su información del navegador. Esto puede ser útil al solucionar problemas específicos del navegador.", - "diagnostics.config-issue-action": "Action", - "diagnostics.config-issue-argument": "Argument", - "diagnostics.config-issue-code": "Code", - "diagnostics.config-issue-config-file": "Config file", - "diagnostics.config-issue-detail": "Detail", - "diagnostics.config-issue-message": "Message", - "diagnostics.config-issue-severity": "Severity", - "diagnostics.config-issue-source": "Source", - "diagnostics.config-issues": "Configuration issues", - "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.", - "diagnostics.config-issues-none": "No configuration issues detected.", + "diagnostics.config-issue-action": "Acción", + "diagnostics.config-issue-argument": "Argumento", + "diagnostics.config-issue-code": "Código", + "diagnostics.config-issue-config-file": "Archivo de configuración", + "diagnostics.config-issue-detail": "Detalle", + "diagnostics.config-issue-message": "Mensaje", + "diagnostics.config-issue-severity": "Severidad", + "diagnostics.config-issue-source": "Origen", + "diagnostics.config-issues": "Problemas de configuración", + "diagnostics.config-issues-description": "Problemas detectados en la configuración actual de OliveTin. Corríjalos en sus archivos de configuración y vuelva a cargar OliveTin.", + "diagnostics.config-issues-none": "No se detectaron problemas de configuración.", "diagnostics.copied": "¡Copiado!", "diagnostics.copy-to-clipboard": "Copiar al portapapeles", "diagnostics.found-config": "Configuración encontrada", @@ -275,17 +275,17 @@ "connected": "Connesso", "diagnostics.browser-info": "Informazioni del browser", "diagnostics.browser-info-description": "Questa sezione ti consente di generare un rapporto dettagliato delle informazioni del tuo browser. Questo può essere utile durante la risoluzione dei problemi specifici del browser.", - "diagnostics.config-issue-action": "Action", - "diagnostics.config-issue-argument": "Argument", - "diagnostics.config-issue-code": "Code", - "diagnostics.config-issue-config-file": "Config file", - "diagnostics.config-issue-detail": "Detail", - "diagnostics.config-issue-message": "Message", - "diagnostics.config-issue-severity": "Severity", - "diagnostics.config-issue-source": "Source", - "diagnostics.config-issues": "Configuration issues", - "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.", - "diagnostics.config-issues-none": "No configuration issues detected.", + "diagnostics.config-issue-action": "Azione", + "diagnostics.config-issue-argument": "Argomento", + "diagnostics.config-issue-code": "Codice", + "diagnostics.config-issue-config-file": "File di configurazione", + "diagnostics.config-issue-detail": "Dettaglio", + "diagnostics.config-issue-message": "Messaggio", + "diagnostics.config-issue-severity": "Gravità", + "diagnostics.config-issue-source": "Origine", + "diagnostics.config-issues": "Problemi di configurazione", + "diagnostics.config-issues-description": "Problemi rilevati nella configurazione corrente di OliveTin. Correggili nei file di configurazione e ricarica OliveTin.", + "diagnostics.config-issues-none": "Nessun problema di configurazione rilevato.", "diagnostics.copied": "Copiato!", "diagnostics.copy-to-clipboard": "Copia negli appunti", "diagnostics.found-config": "Configurazione trovata", @@ -365,17 +365,17 @@ "connected": "已连接", "diagnostics.browser-info": "浏览器信息", "diagnostics.browser-info-description": "此部分允许您生成浏览器信息的详细报告。这在排查浏览器特定问题时很有帮助。", - "diagnostics.config-issue-action": "Action", - "diagnostics.config-issue-argument": "Argument", - "diagnostics.config-issue-code": "Code", - "diagnostics.config-issue-config-file": "Config file", - "diagnostics.config-issue-detail": "Detail", - "diagnostics.config-issue-message": "Message", - "diagnostics.config-issue-severity": "Severity", - "diagnostics.config-issue-source": "Source", - "diagnostics.config-issues": "Configuration issues", - "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.", - "diagnostics.config-issues-none": "No configuration issues detected.", + "diagnostics.config-issue-action": "操作", + "diagnostics.config-issue-argument": "参数", + "diagnostics.config-issue-code": "代码", + "diagnostics.config-issue-config-file": "配置文件", + "diagnostics.config-issue-detail": "详情", + "diagnostics.config-issue-message": "消息", + "diagnostics.config-issue-severity": "严重程度", + "diagnostics.config-issue-source": "来源", + "diagnostics.config-issues": "配置问题", + "diagnostics.config-issues-description": "当前 OliveTin 配置中检测到的问题。请在配置文件中修复这些问题并重新加载 OliveTin。", + "diagnostics.config-issues-none": "未检测到配置问题。", "diagnostics.copied": "已复制!", "diagnostics.copy-to-clipboard": "复制到剪贴板", "diagnostics.found-config": "找到配置", @@ -455,17 +455,17 @@ "connected": "已連線", "diagnostics.browser-info": "瀏覽器資訊", "diagnostics.browser-info-description": "此區塊可讓您產生瀏覽器資訊的詳細報告。這在排解特定瀏覽器問題時相當有幫助。", - "diagnostics.config-issue-action": "Action", - "diagnostics.config-issue-argument": "Argument", - "diagnostics.config-issue-code": "Code", - "diagnostics.config-issue-config-file": "Config file", - "diagnostics.config-issue-detail": "Detail", - "diagnostics.config-issue-message": "Message", - "diagnostics.config-issue-severity": "Severity", - "diagnostics.config-issue-source": "Source", - "diagnostics.config-issues": "Configuration issues", - "diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.", - "diagnostics.config-issues-none": "No configuration issues detected.", + "diagnostics.config-issue-action": "動作", + "diagnostics.config-issue-argument": "參數", + "diagnostics.config-issue-code": "代碼", + "diagnostics.config-issue-config-file": "設定檔", + "diagnostics.config-issue-detail": "詳細資料", + "diagnostics.config-issue-message": "訊息", + "diagnostics.config-issue-severity": "嚴重程度", + "diagnostics.config-issue-source": "來源", + "diagnostics.config-issues": "設定問題", + "diagnostics.config-issues-description": "目前 OliveTin 設定中偵測到的問題。請在設定檔中修正這些問題並重新載入 OliveTin。", + "diagnostics.config-issues-none": "未偵測到設定問題。", "diagnostics.copied": "已複製!", "diagnostics.copy-to-clipboard": "複製到剪貼簿", "diagnostics.found-config": "找到設定檔 (Config)", diff --git a/lang/de-DE.yaml b/lang/de-DE.yaml index 789c2cf..b8db0d3 100644 --- a/lang/de-DE.yaml +++ b/lang/de-DE.yaml @@ -54,17 +54,17 @@ translations: diagnostics.get-support: Unterstützung erhalten Unterstützung erhalten diagnostics.get-support-description: Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, Server-Diagnostik von dieser Seite einzufügen. diagnostics.where-to-find-help: Wo Sie Hilfe finden - diagnostics.config-issues: Configuration issues - diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin. - diagnostics.config-issues-none: No configuration issues detected. - diagnostics.config-issue-severity: Severity + diagnostics.config-issues: Konfigurationsprobleme + diagnostics.config-issues-description: In der aktuellen OliveTin-Konfiguration erkannte Probleme. Beheben Sie diese in Ihren Konfigurationsdateien und laden Sie OliveTin neu. + diagnostics.config-issues-none: Keine Konfigurationsprobleme erkannt. + diagnostics.config-issue-severity: Schweregrad diagnostics.config-issue-code: Code - diagnostics.config-issue-message: Message - diagnostics.config-issue-action: Action + diagnostics.config-issue-message: Meldung + diagnostics.config-issue-action: Aktion diagnostics.config-issue-argument: Argument - diagnostics.config-issue-config-file: Config file + diagnostics.config-issue-config-file: Konfigurationsdatei diagnostics.config-issue-detail: Detail - diagnostics.config-issue-source: Source + diagnostics.config-issue-source: Quelle diagnostics.ssh: SSH diagnostics.found-key: Schlüssel gefunden diagnostics.found-config: Konfiguration gefunden diff --git a/lang/es-ES.yaml b/lang/es-ES.yaml index 6f8a6a5..a442417 100644 --- a/lang/es-ES.yaml +++ b/lang/es-ES.yaml @@ -54,17 +54,17 @@ translations: diagnostics.get-support: Obtener soporte Obtener soporte diagnostics.get-support-description: Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir diagnósticos del servidor desde esta página. diagnostics.where-to-find-help: Dónde encontrar ayuda - diagnostics.config-issues: Configuration issues - diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin. - diagnostics.config-issues-none: No configuration issues detected. - diagnostics.config-issue-severity: Severity - diagnostics.config-issue-code: Code - diagnostics.config-issue-message: Message - diagnostics.config-issue-action: Action - diagnostics.config-issue-argument: Argument - diagnostics.config-issue-config-file: Config file - diagnostics.config-issue-detail: Detail - diagnostics.config-issue-source: Source + diagnostics.config-issues: Problemas de configuración + diagnostics.config-issues-description: Problemas detectados en la configuración actual de OliveTin. Corríjalos en sus archivos de configuración y vuelva a cargar OliveTin. + diagnostics.config-issues-none: No se detectaron problemas de configuración. + diagnostics.config-issue-severity: Severidad + diagnostics.config-issue-code: Código + diagnostics.config-issue-message: Mensaje + diagnostics.config-issue-action: Acción + diagnostics.config-issue-argument: Argumento + diagnostics.config-issue-config-file: Archivo de configuración + diagnostics.config-issue-detail: Detalle + diagnostics.config-issue-source: Origen diagnostics.ssh: SSH diagnostics.found-key: Clave encontrada diagnostics.found-config: Configuración encontrada diff --git a/lang/it-IT.yaml b/lang/it-IT.yaml index 0543b6e..bb2f1ee 100644 --- a/lang/it-IT.yaml +++ b/lang/it-IT.yaml @@ -54,17 +54,17 @@ translations: diagnostics.get-support: Ottenere supporto Ottenere supporto diagnostics.get-support-description: Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere la diagnostica del server da questa pagina. diagnostics.where-to-find-help: Dove trovare aiuto - diagnostics.config-issues: Configuration issues - diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin. - diagnostics.config-issues-none: No configuration issues detected. - diagnostics.config-issue-severity: Severity - diagnostics.config-issue-code: Code - diagnostics.config-issue-message: Message - diagnostics.config-issue-action: Action - diagnostics.config-issue-argument: Argument - diagnostics.config-issue-config-file: Config file - diagnostics.config-issue-detail: Detail - diagnostics.config-issue-source: Source + diagnostics.config-issues: Problemi di configurazione + diagnostics.config-issues-description: Problemi rilevati nella configurazione corrente di OliveTin. Correggili nei file di configurazione e ricarica OliveTin. + diagnostics.config-issues-none: Nessun problema di configurazione rilevato. + diagnostics.config-issue-severity: Gravità + diagnostics.config-issue-code: Codice + diagnostics.config-issue-message: Messaggio + diagnostics.config-issue-action: Azione + diagnostics.config-issue-argument: Argomento + diagnostics.config-issue-config-file: File di configurazione + diagnostics.config-issue-detail: Dettaglio + diagnostics.config-issue-source: Origine diagnostics.ssh: SSH diagnostics.found-key: Chiave trovata diagnostics.found-config: Configurazione trovata diff --git a/lang/zh-Hans-CN.yaml b/lang/zh-Hans-CN.yaml index 07a8c94..6937946 100644 --- a/lang/zh-Hans-CN.yaml +++ b/lang/zh-Hans-CN.yaml @@ -63,17 +63,17 @@ translations: diagnostics.get-support: 获取支持 获取支持 diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。 diagnostics.where-to-find-help: 在哪里找到帮助 - diagnostics.config-issues: Configuration issues - diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin. - diagnostics.config-issues-none: No configuration issues detected. - diagnostics.config-issue-severity: Severity - diagnostics.config-issue-code: Code - diagnostics.config-issue-message: Message - diagnostics.config-issue-action: Action - diagnostics.config-issue-argument: Argument - diagnostics.config-issue-config-file: Config file - diagnostics.config-issue-detail: Detail - diagnostics.config-issue-source: Source + diagnostics.config-issues: 配置问题 + diagnostics.config-issues-description: 当前 OliveTin 配置中检测到的问题。请在配置文件中修复这些问题并重新加载 OliveTin。 + diagnostics.config-issues-none: 未检测到配置问题。 + diagnostics.config-issue-severity: 严重程度 + diagnostics.config-issue-code: 代码 + diagnostics.config-issue-message: 消息 + diagnostics.config-issue-action: 操作 + diagnostics.config-issue-argument: 参数 + diagnostics.config-issue-config-file: 配置文件 + diagnostics.config-issue-detail: 详情 + diagnostics.config-issue-source: 来源 diagnostics.ssh: SSH diagnostics.found-key: 找到密钥 diagnostics.found-config: 找到配置 diff --git a/lang/zh-Hant-TW.yaml b/lang/zh-Hant-TW.yaml index 65f2cf7..462ffd7 100644 --- a/lang/zh-Hant-TW.yaml +++ b/lang/zh-Hant-TW.yaml @@ -54,17 +54,17 @@ translations: diagnostics.get-support: 尋求技術支援 diagnostics.get-support-description: 如果您在使用 OliveTin 時遇到問題並希望提出支援請求,提供此頁面的「伺服器診斷資訊」將會有很大的幫助。 diagnostics.where-to-find-help: 哪裡可以尋求協助 - diagnostics.config-issues: Configuration issues - diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin. - diagnostics.config-issues-none: No configuration issues detected. - diagnostics.config-issue-severity: Severity - diagnostics.config-issue-code: Code - diagnostics.config-issue-message: Message - diagnostics.config-issue-action: Action - diagnostics.config-issue-argument: Argument - diagnostics.config-issue-config-file: Config file - diagnostics.config-issue-detail: Detail - diagnostics.config-issue-source: Source + diagnostics.config-issues: 設定問題 + diagnostics.config-issues-description: 目前 OliveTin 設定中偵測到的問題。請在設定檔中修正這些問題並重新載入 OliveTin。 + diagnostics.config-issues-none: 未偵測到設定問題。 + diagnostics.config-issue-severity: 嚴重程度 + diagnostics.config-issue-code: 代碼 + diagnostics.config-issue-message: 訊息 + diagnostics.config-issue-action: 動作 + diagnostics.config-issue-argument: 參數 + diagnostics.config-issue-config-file: 設定檔 + diagnostics.config-issue-detail: 詳細資料 + diagnostics.config-issue-source: 來源 diagnostics.ssh: SSH diagnostics.found-key: 找到金鑰 (Key) diagnostics.found-config: 找到設定檔 (Config) diff --git a/service/internal/api/api_config_issues_test.go b/service/internal/api/api_config_issues_test.go index c83a265..f142593 100644 --- a/service/internal/api/api_config_issues_test.go +++ b/service/internal/api/api_config_issues_test.go @@ -31,8 +31,8 @@ func TestGetDiagnosticsReturnsConfigIssues(t *testing.T) { }, } - ts, client := getNewTestServerAndClient(cfg) - defer ts.Close() + testServer, client := getNewTestServerAndClient(cfg) + defer testServer.Close() res, err := client.GetDiagnostics(context.Background(), connect.NewRequest(&apiv1.GetDiagnosticsRequest{})) require.NoError(t, err) @@ -69,8 +69,8 @@ func TestInitHidesConfigIssueCountWithoutDiagnostics(t *testing.T) { }, } - ts, client := getNewTestServerAndClient(cfg) - defer ts.Close() + testServer, client := getNewTestServerAndClient(cfg) + defer testServer.Close() initRes, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{})) require.NoError(t, err) @@ -119,8 +119,8 @@ func TestConfigIssuesHideActionsWithoutViewPermission(t *testing.T) { cfg.AuthHttpHeaderUsername = "X-Ot-User" attachBrokenTemplateArgToSecretAction(cfg) - ts, client := getNewTestServerAndClient(cfg) - defer ts.Close() + testServer, client := getNewTestServerAndClient(cfg) + defer testServer.Close() configissues.Report(configissues.Issue{ Severity: configissues.SeverityWarning, @@ -157,8 +157,8 @@ func TestConfigIssuesHideRuntimeWatcherFailuresWithoutViewPermission(t *testing. cfg, _, _ := buildViewPermissionTestConfig(t) cfg.AuthHttpHeaderUsername = "X-Ot-User" - ts, client := getNewTestServerAndClient(cfg) - defer ts.Close() + testServer, client := getNewTestServerAndClient(cfg) + defer testServer.Close() configissues.Report(configissues.Issue{ Severity: configissues.SeverityError, diff --git a/service/internal/config/source_file.go b/service/internal/config/source_file.go index 2f9cc09..817ec52 100644 --- a/service/internal/config/source_file.go +++ b/service/internal/config/source_file.go @@ -1,6 +1,9 @@ package config -import "github.com/knadh/koanf/v2" +import ( + "github.com/knadh/koanf/v2" + log "github.com/sirupsen/logrus" +) const sourceFileKey = "x-olivetin-source-file" @@ -34,6 +37,15 @@ func stampSourceOnMap(item any, sourceFile string) { } func stampLoadedConfigSources(k *koanf.Koanf, configPath string) { - _ = k.Set("actions", stampSourceOnMaps(k.Get("actions"), configPath)) - _ = k.Set("entities", stampSourceOnMaps(k.Get("entities"), configPath)) + stampConfigKey(k, "actions", configPath) + stampConfigKey(k, "entities", configPath) +} + +func stampConfigKey(k *koanf.Koanf, key, configPath string) { + if err := k.Set(key, stampSourceOnMaps(k.Get(key), configPath)); err != nil { + log.WithFields(log.Fields{ + "key": key, + "configPath": configPath, + }).Errorf("Failed to persist source stamps: %v", err) + } } diff --git a/service/internal/configcheck/rebuild.go b/service/internal/configcheck/rebuild.go index 92c3d82..9063fee 100644 --- a/service/internal/configcheck/rebuild.go +++ b/service/internal/configcheck/rebuild.go @@ -114,7 +114,7 @@ func checklistNoChoicesIssue(action *config.Action, arg *config.ActionArgument) } func checklistEntityChoicesIssue(action *config.Action, arg *config.ActionArgument) []configissues.Issue { - if arg.Entity == "" || len(arg.Choices) == 1 { + if arg.Entity == "" || len(arg.Choices) <= 1 { return nil } @@ -127,7 +127,10 @@ func collectIncludeIssues(cfg *config.Config) []configissues.Issue { return nil } - includePath := filepath.Join(cfg.GetDir(), cfg.Include) + includePath := cfg.Include + if !filepath.IsAbs(includePath) { + includePath = filepath.Join(cfg.GetDir(), cfg.Include) + } info, err := os.Stat(includePath) if err != nil { return []configissues.Issue{{ @@ -410,19 +413,33 @@ func watcherPathIssuesForAction(action *config.Action) []configissues.Issue { return nil } + out := make([]configissues.Issue, 0) + out = append(out, watcherCreatedDirIssues(action)...) + out = append(out, watcherChangedDirIssues(action)...) + out = append(out, watcherCalendarFileIssues(action)...) + return out +} + +func watcherCreatedDirIssues(action *config.Action) []configissues.Issue { out := make([]configissues.Issue, 0) for _, dir := range action.ExecOnFileCreatedInDir { out = append(out, watcherDirIssue(action, dir)...) } + return out +} + +func watcherChangedDirIssues(action *config.Action) []configissues.Issue { + out := make([]configissues.Issue, 0) for _, dir := range action.ExecOnFileChangedInDir { out = append(out, watcherDirIssue(action, dir)...) } - if action.ExecOnCalendarFile != "" { - out = append(out, watcherFileIssue(action, action.ExecOnCalendarFile)...) - } return out } +func watcherCalendarFileIssues(action *config.Action) []configissues.Issue { + return watcherFileIssue(action, action.ExecOnCalendarFile) +} + func watcherPathIssuesForEntities(cfg *config.Config) []configissues.Issue { out := make([]configissues.Issue, 0) baseDir := entities.ResolveEntitiesBaseDir(cfg.GetDir()) diff --git a/service/internal/configissues/store.go b/service/internal/configissues/store.go index 6d112c6..e83871e 100644 --- a/service/internal/configissues/store.go +++ b/service/internal/configissues/store.go @@ -97,9 +97,9 @@ func Report(issue Issue) { storeMu.Lock() defer storeMu.Unlock() - fp := issueFingerprint(issue) + issueFingerprintValue := issueFingerprint(issue) for _, existing := range issues { - if issueFingerprint(existing) == fp { + if issueFingerprint(existing) == issueFingerprintValue { return } } diff --git a/service/internal/entities/entities.go b/service/internal/entities/entities.go index f00b313..0cbd7d9 100644 --- a/service/internal/entities/entities.go +++ b/service/internal/entities/entities.go @@ -69,7 +69,9 @@ func watchAndLoadEntity(baseDir string, ef *config.EntityFile) { p = filepath.Join(baseDir, p) log.WithFields(log.Fields{"entityFile": p}).Debugf("Adding config dir to entity file path") } - go filehelper.WatchFileWrite(p, func(filename string) { loadEntityFile(p, ef.Name) }, filehelper.WatchMeta{}) + go filehelper.WatchFileWrite(p, func(filename string) { loadEntityFile(p, ef.Name) }, filehelper.WatchMeta{ + ConfigFile: ef.SourceFile, + }) loadEntityFile(p, ef.Name) } @@ -155,10 +157,23 @@ func loadEntityFileYaml(filename string, entityname string) { } func replaceEntitiesFromFile(entityname string, data []map[string]any) { - ClearEntitiesOfType(entityname) + rwmutex.Lock() + defer rwmutex.Unlock() + delete(entities, entityname) + + if len(data) == 0 { + return + } + + entities[entityname] = make(entityInstancesByKey, 0) for i, mapp := range data { - AddEntity(entityname, fmt.Sprintf("%d", i), mapp) + entityKey := fmt.Sprintf("%d", i) + entities[entityname][entityKey] = &Entity{ + Data: mapp, + UniqueKey: entityKey, + Title: findEntityTitle(mapp), + } } } From 9702b42712693719ab9b420a6429cfb844d971a9 Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 24 Jul 2026 16:57:46 +0100 Subject: [PATCH 07/17] chore: fix tests after picocrank update --- .../dashboardsWithBasicFieldsets.js | 6 +++--- .../emptyDashboardsAreHidden/emptyDashboardsAreHidden.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/integration-tests/tests/dashboardsWithBasicFieldsets/dashboardsWithBasicFieldsets.js b/integration-tests/tests/dashboardsWithBasicFieldsets/dashboardsWithBasicFieldsets.js index b67be0b..64b690c 100644 --- a/integration-tests/tests/dashboardsWithBasicFieldsets/dashboardsWithBasicFieldsets.js +++ b/integration-tests/tests/dashboardsWithBasicFieldsets/dashboardsWithBasicFieldsets.js @@ -32,7 +32,7 @@ describe('config: dashboards with basic fieldsets', function () { await openSidebar() const navigationLinks = await getNavigationLinks() - assert.equal(navigationLinks.length, 5, 'Expected the nav to only have 5 links') // test dashboard + logs + diagnostics + entities + separator + assert.equal(navigationLinks.length, 4, 'Expected the nav to only have 4 links') // test dashboard + entities + logs + diagnostics const firstLink = await navigationLinks[0] @@ -44,7 +44,7 @@ describe('config: dashboards with basic fieldsets', function () { // Check that we have the expected number of fieldsets const dashboardRows = await webdriver.findElements(By.css('.dashboard-row')) expect(dashboardRows).to.have.length(3, 'Expected 3 dashboard rows total') - + // Check that we have fieldsets with the expected titles const fieldsetTitles = [] for (let i = 0; i < dashboardRows.length; i++) { @@ -54,7 +54,7 @@ describe('config: dashboards with basic fieldsets', function () { fieldsetTitles.push(title) } } - + // We should have fieldsets for: Fieldset 1, Fieldset 2, and Actions fieldsets expect(fieldsetTitles).to.include('Fieldset 1') expect(fieldsetTitles).to.include('Fieldset 2') diff --git a/integration-tests/tests/emptyDashboardsAreHidden/emptyDashboardsAreHidden.js b/integration-tests/tests/emptyDashboardsAreHidden/emptyDashboardsAreHidden.js index e5d498b..6cc133d 100644 --- a/integration-tests/tests/emptyDashboardsAreHidden/emptyDashboardsAreHidden.js +++ b/integration-tests/tests/emptyDashboardsAreHidden/emptyDashboardsAreHidden.js @@ -32,6 +32,6 @@ describe('config: empty dashboards are hidden', function () { const navigationLinks = await getNavigationLinks() expect(navigationLinks).to.not.be.empty - expect(navigationLinks.length).to.be.equal(4, 'Expected the nav to only have 4 links') + expect(navigationLinks.length).to.be.equal(3, 'Expected the nav to only have 3 links') // entities + logs + diagnostics }) }) From 8e68166a792ae910948431794506e58c2187d0f6 Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 24 Jul 2026 17:21:50 +0100 Subject: [PATCH 08/17] docs: SignPath sponsorship note --- README.md | 8 +++++++- .../ROOT/pages/reference/donations_and_sponsorship.adoc | 7 +++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a1c8a9d..8abeb0e 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ OliveTin follows these principles: * **Open Source & Free Software**: following the [Open Source Definition](https://opensource.org/osd) and the [Free Software Definition](https://www.gnu.org/philosophy/free-sw.html). All code and assets are available under the [AGPL-3.0 License](LICENSE). * **Independent**: No company owns the code or is responsible for the projects' governance. * **Inclusive**: No "core", "pro", "premium" or "enterprise" version. The only version is the one you can download and run, and it has all the features. -* **Invisible**: No usage tracking, no user tracking, no ads, and no telemetry. +* **Invisible**: No usage tracking, no user tracking, no ads, and no telemetry. * **Internal**: No internet connection required for any functionality. ## Documentation @@ -88,3 +88,9 @@ OliveTin follows these principles: All documentation can be found at [docs.olivetin.app](https://docs.olivetin.app). This includes installation and usage guide, etc. You can find instructions in the docs on how to install as a **Linux package**, **Linux Container**, on **FreeBSD**, **Windows**, **MacOS** and other platforms, too! + +## Donations & Sponsorship + +OliveTin does not accept monetary donations or sponsorship. See [Donations & Sponsorship](https://docs.olivetin.app/reference/donations_and_sponsorship.html) for the full explanation. + +* Free code signing provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/). diff --git a/docs/modules/ROOT/pages/reference/donations_and_sponsorship.adoc b/docs/modules/ROOT/pages/reference/donations_and_sponsorship.adoc index ba51e6d..b7151d3 100644 --- a/docs/modules/ROOT/pages/reference/donations_and_sponsorship.adoc +++ b/docs/modules/ROOT/pages/reference/donations_and_sponsorship.adoc @@ -3,9 +3,8 @@ Sometimes I (James Read) get asked if people can donate money, or sponsor me for OliveTin. If you are reading this page, maybe you are thinking the same. -**I do not accept donations or sponsorship** for OliveTin, but I want to **thank you very much indeed** for thinking about the potential. +**I do not accept monetary donations or sponsorship** for OliveTin, but I want to **thank you very much indeed** for thinking about the potential. -I have a job that pays me, where I don't write code as part of my day job (very often) - I enjoy coding as a hobby in my spare time. If I get money for that, it somehow takes the pleasure or fun out of it, or makes it feel like a job. There are concerns that I might "prefer" to work on one user's feature request if I am sponsored, or even feel compelled that I have to work on something because someone gave me money. I don't want OliveTin development to go that way. There are also other little considerations, like being paid on the side out of my job then affects my job, and tax, and so on and so on. This is why I don't take any form of money, donations or sponsorship. - -Another way that you can show your appreciation for OliveTin, that actually means a lot more than money, is to xref:reference/contribute.adoc[contribute to OliveTin]. There are little ways and big ways to contribute - depends on if you have 2 minutes or 2 weeks to give! +I have a job that pays me, where I don't write code as part of my day job (very often) - I enjoy coding as a hobby in my spare time. If I get money for that, it somehow takes the pleasure or fun out of it, or makes it feel like a job. There are concerns that I might "prefer" to work on one user's feature request if I am sponsored, or even feel compelled that I have to work on something because someone gave me money. I don't want OliveTin development to go that way. There are also other little considerations, like being paid on the side out of my job then affects my job, and tax, and so on and so on. This is why I don't take any form of money, donations or sponsorship. +Another way that you can show your appreciation for OliveTin, that actually means a lot more than money, is to xref:reference/contribute.adoc[contribute to OliveTin]. There are little ways and big ways to contribute - depends on if you have 2 minutes or 2 weeks to give! From 6eb3827dddd02ef02dfce19cd613e1db3ea5d580 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 25 Jul 2026 22:00:02 +0100 Subject: [PATCH 09/17] feat: dnsname argument type --- docs/modules/ROOT/pages/args/types.adoc | 1 + frontend/resources/vue/views/ArgumentForm.vue | 2 +- service/internal/executor/arguments.go | 32 ++++++++++++++++ service/internal/executor/arguments_test.go | 38 +++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/args/types.adoc b/docs/modules/ROOT/pages/args/types.adoc index 6d6f687..272db8e 100644 --- a/docs/modules/ROOT/pages/args/types.adoc +++ b/docs/modules/ROOT/pages/args/types.adoc @@ -10,6 +10,7 @@ A full list of argument types are below; | (default) | xref:args/input.adoc[Textbox] | If a `type:` is not set, and `choices:` is empty, then ascii will be used, and a warning will be logged. It is recommended that you set the type explicitly, rather than relying on defaults. | ascii | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, but no spaces or punctuation | ascii_identifier | xref:args/input.adoc[Textbox] | Like a DNS name, a-Z (case insensitive), 0-9, `-`, `.`, and `_`. +| dnsname | xref:args/input.adoc[Textbox] | A DNS hostname (RFC 1123). Short names (e.g. `webserver`) and FQDNs (e.g. `webserver.example.com`). Letters/digits/hyphens only, no underscores. Optional trailing dot allowed. | shell_safe_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but also allows `@` and `+`. Useful for shell-safe usernames and email-style identifiers. | ascii_sentence | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, with spaces, `.` and `,`. | unicode_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but allows unicode characters. This is useful for languages that use non-ascii characters, such as Chinese, Japanese, etc. diff --git a/frontend/resources/vue/views/ArgumentForm.vue b/frontend/resources/vue/views/ArgumentForm.vue index 0588358..9da632c 100644 --- a/frontend/resources/vue/views/ArgumentForm.vue +++ b/frontend/resources/vue/views/ArgumentForm.vue @@ -291,7 +291,7 @@ function getInputType (arg) { return 'checkbox' } - if (arg.type === 'ascii_identifier' || arg.type === 'shell_safe_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') { + if (arg.type === 'ascii_identifier' || arg.type === 'dnsname' || arg.type === 'shell_safe_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') { return 'text' } diff --git a/service/internal/executor/arguments.go b/service/internal/executor/arguments.go index 6810e6c..e9ff778 100644 --- a/service/internal/executor/arguments.go +++ b/service/internal/executor/arguments.go @@ -24,6 +24,9 @@ var ( "shell_safe_identifier": `^[a-zA-Z0-9@\.\_\+\-]+$`, "ascii_sentence": `^[a-zA-Z0-9\-\._, ]+$`, } + + dnsNameLabelPattern = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`) + dnsNameAllNumericPattern = regexp.MustCompile(`^[0-9]+$`) ) // parseExecArray parses all exec arguments in the action. @@ -223,6 +226,8 @@ func TypeSafetyCheck(name string, value string, argumentType string) error { return typeSafetyCheckUrl(value) case "datetime": return typeSafetyCheckDatetime(value) + case "dnsname": + return typeSafetyCheckDnsName(value) } return typeSafetyCheckRegex(name, value, argumentType) @@ -304,6 +309,33 @@ func typeSafetyCheckEmail(value string) error { return nil } +// typeSafetyCheckDnsName validates a DNS hostname (RFC 1123 LDH labels). +// Accepts short names (e.g. webserver) and FQDNs (e.g. webserver.example.com). +// An optional trailing dot is allowed. +func typeSafetyCheckDnsName(value string) error { + hostname := strings.TrimSuffix(value, ".") + if hostname == "" || len(hostname) > 253 { + return fmt.Errorf("invalid dnsname length") + } + + return typeSafetyCheckDnsNameLabels(strings.Split(hostname, ".")) +} + +func typeSafetyCheckDnsNameLabels(labels []string) error { + for _, label := range labels { + if !dnsNameLabelPattern.MatchString(label) { + return fmt.Errorf("invalid dnsname label %q", label) + } + } + + tld := labels[len(labels)-1] + if dnsNameAllNumericPattern.MatchString(tld) { + return fmt.Errorf("dnsname top-level label must not be all-numeric") + } + + return nil +} + func typeSafetyCheckDatetime(value string) error { _, err := time.Parse("2006-01-02T15:04:05", value) diff --git a/service/internal/executor/arguments_test.go b/service/internal/executor/arguments_test.go index d6caffe..6d28626 100644 --- a/service/internal/executor/arguments_test.go +++ b/service/internal/executor/arguments_test.go @@ -812,6 +812,44 @@ func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) { } } +func TestTypeSafetyCheckDnsName(t *testing.T) { + tests := []struct { + name string + value string + hasError bool + }{ + {"Short name", "webserver", false}, + {"Localhost", "localhost", false}, + {"Simple domain", "example.com", false}, + {"Host with subdomain", "webserver.example.com", false}, + {"Deep subdomain", "a.b.c.example.co.uk", false}, + {"Label starting with digit", "1host.example.com", false}, + {"Trailing dot", "example.com.", false}, + {"Punycode IDN", "xn--bcher-kva.example", false}, + {"Underscore", "my_host.example.com", true}, + {"Space", "example .com", true}, + {"Leading hyphen label", "-host.example.com", true}, + {"Trailing hyphen label", "host-.example.com", true}, + {"Empty label", "example..com", true}, + {"IP address", "192.168.1.1", true}, + {"All numeric TLD", "example.123", true}, + {"All numeric short name", "12345", true}, + {"Special chars", "exam!ple.com", true}, + {"Unicode label", "bücher.example.com", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := TypeSafetyCheck("host", tt.value, "dnsname") + if tt.hasError { + assert.NotNil(t, err, "Expected error for value '%s'", tt.value) + } else { + assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err) + } + }) + } +} + func TestTypeSafetyCheckShellSafeIdentifier(t *testing.T) { tests := []struct { name string From 6604cf44a1f0b6e18712f45d6c83d798c19b1c8a Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 25 Jul 2026 22:17:27 +0100 Subject: [PATCH 10/17] docs: "macOS" title in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8abeb0e..2223621 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ OliveTin follows these principles: All documentation can be found at [docs.olivetin.app](https://docs.olivetin.app). This includes installation and usage guide, etc. -You can find instructions in the docs on how to install as a **Linux package**, **Linux Container**, on **FreeBSD**, **Windows**, **MacOS** and other platforms, too! +You can find instructions in the docs on how to install as a **Linux package**, **Linux Container**, on **FreeBSD**, **Windows**, **macOS** and other platforms, too! ## Donations & Sponsorship From 8b8b422c194623b127e0f43c66c507ab65b95179 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 25 Jul 2026 22:18:58 +0100 Subject: [PATCH 11/17] fix: several duplicate translation fixes --- lang/combined_output.json | 8 ++++---- lang/de-DE.yaml | 2 +- lang/es-ES.yaml | 2 +- lang/it-IT.yaml | 2 +- lang/zh-Hans-CN.yaml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lang/combined_output.json b/lang/combined_output.json index f47b5df..4db2148 100644 --- a/lang/combined_output.json +++ b/lang/combined_output.json @@ -22,7 +22,7 @@ "diagnostics.found-key": "Schlüssel gefunden", "diagnostics.generate-browser-info": "Browser-Informationen erstellen", "diagnostics.generate-server-diagnostics": "Server-Diagnostik erstellen", - "diagnostics.get-support": "Unterstützung erhalten Unterstützung erhalten", + "diagnostics.get-support": "Unterstützung erhalten", "diagnostics.get-support-description": "Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, Server-Diagnostik von dieser Seite einzufügen.", "diagnostics.server-diagnostics": "Server-Diagnostik", "diagnostics.server-diagnostics-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Konfiguration und Umgebung zu erstellen. Es ist eine gute Idee, dies bei einer Support-Anfrage einzufügen.", @@ -202,7 +202,7 @@ "diagnostics.found-key": "Clave encontrada", "diagnostics.generate-browser-info": "Generar información del navegador", "diagnostics.generate-server-diagnostics": "Generar diagnóstico del servidor", - "diagnostics.get-support": "Obtener soporte Obtener soporte", + "diagnostics.get-support": "Obtener soporte", "diagnostics.get-support-description": "Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir diagnósticos del servidor desde esta página.", "diagnostics.server-diagnostics": "Diagnóstico del servidor", "diagnostics.server-diagnostics-description": "Esta sección le permite generar un informe detallado de su configuración y entorno. Es una buena idea incluir esto al presentar una solicitud de soporte.", @@ -292,7 +292,7 @@ "diagnostics.found-key": "Chiave trovata", "diagnostics.generate-browser-info": "Genera informazioni del browser", "diagnostics.generate-server-diagnostics": "Genera diagnostica del server", - "diagnostics.get-support": "Ottenere supporto Ottenere supporto", + "diagnostics.get-support": "Ottenere supporto", "diagnostics.get-support-description": "Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere la diagnostica del server da questa pagina.", "diagnostics.server-diagnostics": "Diagnostica del server", "diagnostics.server-diagnostics-description": "Questa sezione ti consente di generare un rapporto dettagliato della tua configurazione e ambiente. È una buona idea includere questo quando si presenta una richiesta di supporto.", @@ -382,7 +382,7 @@ "diagnostics.found-key": "找到密钥", "diagnostics.generate-browser-info": "生成浏览器信息", "diagnostics.generate-server-diagnostics": "生成服务器诊断", - "diagnostics.get-support": "获取支持 获取支持", + "diagnostics.get-support": "获取支持", "diagnostics.get-support-description": "如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。", "diagnostics.server-diagnostics": "服务器诊断", "diagnostics.server-diagnostics-description": "此部分允许您生成配置和环境的详细报告。在提交支持请求时包含此信息是个好主意。", diff --git a/lang/de-DE.yaml b/lang/de-DE.yaml index b8db0d3..6f00faf 100644 --- a/lang/de-DE.yaml +++ b/lang/de-DE.yaml @@ -51,7 +51,7 @@ translations: logs.queue-position: "#{position}" logs.queue-entity: Entität logs.queue-action-details: Aktionsdetails - diagnostics.get-support: Unterstützung erhalten Unterstützung erhalten + diagnostics.get-support: Unterstützung erhalten diagnostics.get-support-description: Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, Server-Diagnostik von dieser Seite einzufügen. diagnostics.where-to-find-help: Wo Sie Hilfe finden diagnostics.config-issues: Konfigurationsprobleme diff --git a/lang/es-ES.yaml b/lang/es-ES.yaml index a442417..1aa6752 100644 --- a/lang/es-ES.yaml +++ b/lang/es-ES.yaml @@ -51,7 +51,7 @@ translations: logs.queue-position: "#{position}" logs.queue-entity: Entidad logs.queue-action-details: Detalles de la acción - diagnostics.get-support: Obtener soporte Obtener soporte + diagnostics.get-support: Obtener soporte diagnostics.get-support-description: Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir diagnósticos del servidor desde esta página. diagnostics.where-to-find-help: Dónde encontrar ayuda diagnostics.config-issues: Problemas de configuración diff --git a/lang/it-IT.yaml b/lang/it-IT.yaml index bb2f1ee..739552c 100644 --- a/lang/it-IT.yaml +++ b/lang/it-IT.yaml @@ -51,7 +51,7 @@ translations: logs.queue-position: "#{position}" logs.queue-entity: Entità logs.queue-action-details: Dettagli azione - diagnostics.get-support: Ottenere supporto Ottenere supporto + diagnostics.get-support: Ottenere supporto diagnostics.get-support-description: Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere la diagnostica del server da questa pagina. diagnostics.where-to-find-help: Dove trovare aiuto diagnostics.config-issues: Problemi di configurazione diff --git a/lang/zh-Hans-CN.yaml b/lang/zh-Hans-CN.yaml index 6937946..a2560e2 100644 --- a/lang/zh-Hans-CN.yaml +++ b/lang/zh-Hans-CN.yaml @@ -60,7 +60,7 @@ translations: logs.queue-position: "第 {position} 位" logs.queue-entity: 实体 logs.queue-action-details: 动作详细信息 - diagnostics.get-support: 获取支持 获取支持 + diagnostics.get-support: 获取支持 diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。 diagnostics.where-to-find-help: 在哪里找到帮助 diagnostics.config-issues: 配置问题 From b000e82238bb4218d7178a25522321e328a69510 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 25 Jul 2026 22:20:14 +0100 Subject: [PATCH 12/17] chore: Prevent users overriding configsource --- service/internal/config/config.go | 4 +- service/internal/config/config_reloader.go | 1 + service/internal/config/source_file.go | 53 +++++++++++++++++++-- service/internal/config/source_file_test.go | 29 +++++++++++ 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/service/internal/config/config.go b/service/internal/config/config.go index 4b15077..5abc54e 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -40,7 +40,7 @@ type Action struct { Groups []string `koanf:"groups"` Justification string `koanf:"justification"` // SourceFile is set by OliveTin when loading config (not user YAML). - SourceFile string `koanf:"x-olivetin-source-file"` + SourceFile string `koanf:"-"` } func (action *Action) RequiresJustification() bool { @@ -113,7 +113,7 @@ type EntityFile struct { Icon string `koanf:"icon"` Properties []EntityProperty `koanf:"properties"` // SourceFile is set by OliveTin when loading config (not user YAML). - SourceFile string `koanf:"x-olivetin-source-file"` + SourceFile string `koanf:"-"` } // EntityProperty defines a column shown when listing entity instances in the UI. diff --git a/service/internal/config/config_reloader.go b/service/internal/config/config_reloader.go index 39a2484..0927387 100644 --- a/service/internal/config/config_reloader.go +++ b/service/internal/config/config_reloader.go @@ -50,6 +50,7 @@ func AppendSource(cfg *Config, k *koanf.Koanf, configPath string) { return } + applyStampedSourceFiles(k, cfg) afterLoadFinalize(cfg, configPath) } diff --git a/service/internal/config/source_file.go b/service/internal/config/source_file.go index 817ec52..37f1880 100644 --- a/service/internal/config/source_file.go +++ b/service/internal/config/source_file.go @@ -8,7 +8,8 @@ import ( const sourceFileKey = "x-olivetin-source-file" // stampSourceOnMaps sets the OliveTin source-file marker on each map in a -// koanf slice value (actions or entities). +// koanf slice value (actions or entities). Always overwrites so user-provided +// x-olivetin-source-file values cannot spoof the real config path. func stampSourceOnMaps(raw any, sourceFile string) any { if sourceFile == "" { return raw @@ -30,9 +31,6 @@ func stampSourceOnMap(item any, sourceFile string) { if !ok { return } - if existing, ok := m[sourceFileKey].(string); ok && existing != "" { - return - } m[sourceFileKey] = sourceFile } @@ -49,3 +47,50 @@ func stampConfigKey(k *koanf.Koanf, key, configPath string) { }).Errorf("Failed to persist source stamps: %v", err) } } + +// applyStampedSourceFiles copies stamped source paths from koanf maps onto +// unmarshaled actions/entities. SourceFile uses koanf:"-" so YAML cannot set it. +func applyStampedSourceFiles(k *koanf.Koanf, cfg *Config) { + actionPaths := stampedSourcePaths(k.Get("actions")) + for i, action := range cfg.Actions { + if action != nil { + action.SourceFile = sourcePathAt(actionPaths, i) + } + } + + entityPaths := stampedSourcePaths(k.Get("entities")) + for i, entity := range cfg.Entities { + if entity != nil { + entity.SourceFile = sourcePathAt(entityPaths, i) + } + } +} + +func sourcePathAt(paths []string, index int) string { + if index >= len(paths) { + return "" + } + return paths[index] +} + +func stampedSourcePaths(raw any) []string { + items, ok := raw.([]interface{}) + if !ok { + return nil + } + + paths := make([]string, len(items)) + for i, item := range items { + paths[i] = stampedSourceFromMap(item) + } + return paths +} + +func stampedSourceFromMap(item any) string { + m, ok := item.(map[string]interface{}) + if !ok { + return "" + } + path, _ := m[sourceFileKey].(string) + return path +} diff --git a/service/internal/config/source_file_test.go b/service/internal/config/source_file_test.go index a237c55..25e954b 100644 --- a/service/internal/config/source_file_test.go +++ b/service/internal/config/source_file_test.go @@ -49,3 +49,32 @@ entities: require.Len(t, cfg.Entities, 1) assert.Equal(t, includePath, cfg.Entities[0].SourceFile) } + +func TestAppendSourceIgnoresUserProvidedSourceFile(t *testing.T) { + dir := t.TempDir() + basePath := filepath.Join(dir, "config.yaml") + require.NoError(t, os.WriteFile(basePath, []byte(` +actions: + - title: Spoofed + shell: echo hi + x-olivetin-source-file: /tmp/fake-user-path.yaml +entities: + - name: host + file: hosts.yaml + x-olivetin-source-file: /tmp/fake-entity-path.yaml +`), 0o644)) + + k := koanf.New(".") + require.NoError(t, k.Load(file.Provider(basePath), yaml.Parser())) + + cfg := config.DefaultConfig() + config.AppendSource(cfg, k, basePath) + + require.Len(t, cfg.Actions, 1) + assert.Equal(t, basePath, cfg.Actions[0].SourceFile) + assert.NotEqual(t, "/tmp/fake-user-path.yaml", cfg.Actions[0].SourceFile) + + require.Len(t, cfg.Entities, 1) + assert.Equal(t, basePath, cfg.Entities[0].SourceFile) + assert.NotEqual(t, "/tmp/fake-entity-path.yaml", cfg.Entities[0].SourceFile) +} From 18903853fb682f33aa9ffbb8c1eb1df6fd0a1b41 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 25 Jul 2026 22:20:52 +0100 Subject: [PATCH 13/17] fix: A single entities read failure wont clear all entities --- service/internal/entities/entities.go | 4 -- service/internal/entities/entities_test.go | 33 ++++++++++ service/internal/executor/executor.go | 21 +++++- service/internal/executor/executor_test.go | 77 ++++++++++++++++++++++ 4 files changed, 128 insertions(+), 7 deletions(-) diff --git a/service/internal/entities/entities.go b/service/internal/entities/entities.go index 0cbd7d9..6be2b7d 100644 --- a/service/internal/entities/entities.go +++ b/service/internal/entities/entities.go @@ -104,7 +104,6 @@ func loadEntityFileJson(filename string, entityname string) { if err != nil { log.Errorf("ReadIn: %v", err) - ClearEntitiesOfType(entityname) return } @@ -119,7 +118,6 @@ func loadEntityFileJson(filename string, entityname string) { if err != nil { log.Errorf("%v", err) - ClearEntitiesOfType(entityname) return } @@ -139,7 +137,6 @@ func loadEntityFileYaml(filename string, entityname string) { if err != nil { log.Errorf("ReadIn: %v", err) - ClearEntitiesOfType(entityname) return } @@ -149,7 +146,6 @@ func loadEntityFileYaml(filename string, entityname string) { if err != nil { log.Errorf("Unmarshal: %v", err) - ClearEntitiesOfType(entityname) return } diff --git a/service/internal/entities/entities_test.go b/service/internal/entities/entities_test.go index b13fd08..deb411b 100644 --- a/service/internal/entities/entities_test.go +++ b/service/internal/entities/entities_test.go @@ -1,6 +1,8 @@ package entities import ( + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -59,3 +61,34 @@ func TestGetEntityInstancesOrdered_emptyOrMissing(t *testing.T) { ordered = GetEntityInstancesOrdered("empty_test") assert.Nil(t, ordered) } + +func TestLoadEntityFile_preservesEntitiesOnTransientFailure(t *testing.T) { + const entityName = "preserve_on_fail" + ClearEntitiesOfType(entityName) + defer ClearEntitiesOfType(entityName) + + dir := t.TempDir() + yamlPath := filepath.Join(dir, "hosts.yaml") + require.NoError(t, os.WriteFile(yamlPath, []byte("- title: kept\n"), 0o600)) + + loadEntityFile(yamlPath, entityName) + require.Len(t, GetEntityInstancesOrdered(entityName), 1) + + loadEntityFile(filepath.Join(dir, "missing.yaml"), entityName) + require.Len(t, GetEntityInstancesOrdered(entityName), 1, "read failure should keep last good entities") + + require.NoError(t, os.WriteFile(yamlPath, []byte("not: valid: yaml: ["), 0o600)) + loadEntityFile(yamlPath, entityName) + require.Len(t, GetEntityInstancesOrdered(entityName), 1, "parse failure should keep last good entities") + + jsonPath := filepath.Join(dir, "hosts.json") + require.NoError(t, os.WriteFile(jsonPath, []byte("{\"title\":\"json-kept\"}\n"), 0o600)) + loadEntityFile(jsonPath, entityName) + require.Len(t, GetEntityInstancesOrdered(entityName), 1) + + require.NoError(t, os.WriteFile(jsonPath, []byte("{bad json"), 0o600)) + loadEntityFile(jsonPath, entityName) + ordered := GetEntityInstancesOrdered(entityName) + require.Len(t, ordered, 1, "JSON parse failure should keep last good entities") + assert.Equal(t, "json-kept", ordered[0].Title) +} diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 08dca61..08b9328 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -1391,6 +1391,11 @@ func triggerLoop(req *ExecutionRequest) { } func stepSaveLog(req *ExecutionRequest) bool { + if !canSaveExecutionLog(req) { + log.Warnf("Cannot save execution log; missing request, log entry, binding/action, or config") + return false + } + filename := fmt.Sprintf("%v.%v.%v", sanitizeLogFilename(req.logEntry.ActionTitle), req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID) saveLogResults(req, filename) @@ -1399,10 +1404,14 @@ func stepSaveLog(req *ExecutionRequest) bool { return true } +func canSaveExecutionLog(req *ExecutionRequest) bool { + return req != nil && req.logEntry != nil && req.Binding != nil && req.Binding.Action != nil && req.Cfg != nil +} + // sanitizeLogFilename replaces characters that are unsafe in filenames so action // titles like "Create/update Report" do not create nested paths or fail to write. func sanitizeLogFilename(title string) string { - replacer := strings.NewReplacer( + oldnew := []string{ "/", "_", "\\", "_", ":", "_", @@ -1412,9 +1421,15 @@ func sanitizeLogFilename(title string) string { "<", "_", ">", "_", "|", "_", - ) + } - return replacer.Replace(title) + // NUL and other C0 controls plus DEL are invalid or problematic in filenames. + for i := 0; i < 32; i++ { + oldnew = append(oldnew, string(rune(i)), "_") + } + oldnew = append(oldnew, "\x7f", "_") + + return strings.NewReplacer(oldnew...).Replace(title) } func firstNonEmpty(one, two string) string { diff --git a/service/internal/executor/executor_test.go b/service/internal/executor/executor_test.go index b2fed8c..1869a83 100644 --- a/service/internal/executor/executor_test.go +++ b/service/internal/executor/executor_test.go @@ -790,6 +790,8 @@ func TestSanitizeLogFilename(t *testing.T) { {"Create/update Monthly Report", "Create_update Monthly Report"}, {`path\with\backslashes`, "path_with_backslashes"}, {`a:b*c?d"eg|h`, "a_b_c_d_e_f_g_h"}, + {"has\x00nul", "has_nul"}, + {"tab\there\nand\rreturn", "tab_here_and_return"}, } for _, tt := range tests { @@ -870,3 +872,78 @@ func TestStepSaveLogKeepsSafeTitleFilename(t *testing.T) { expectedPath := filepath.Join(resultsDir, "Echo Test.1714333384."+trackingID+".yaml") assert.FileExists(t, expectedPath) } + +func TestStepSaveLogSanitizesNULInTitle(t *testing.T) { + resultsDir := t.TempDir() + outputDir := t.TempDir() + started := time.Unix(1714333384, 0) + trackingID := "bbbbbbbb-cccc-dddd-eeee-ffffffffffff" + title := "Bad\x00Title" + + req := &ExecutionRequest{ + Cfg: &config.Config{ + SaveLogs: config.SaveLogsConfig{ + ResultsDirectory: resultsDir, + OutputDirectory: outputDir, + }, + }, + Binding: &ActionBinding{ + Action: &config.Action{}, + }, + logEntry: &InternalLogEntry{ + ActionTitle: title, + DatetimeStarted: started, + ExecutionTrackingID: trackingID, + Output: "nul ok", + }, + } + + assert.True(t, stepSaveLog(req)) + + expectedBase := "Bad_Title.1714333384." + trackingID + resultsPath := filepath.Join(resultsDir, expectedBase+".yaml") + outputPath := filepath.Join(outputDir, expectedBase+".log") + + assert.FileExists(t, resultsPath) + assert.FileExists(t, outputPath) + assert.NotContains(t, resultsPath, "\x00") + assert.NotContains(t, outputPath, "\x00") + + output, err := os.ReadFile(outputPath) + assert.NoError(t, err) + assert.Equal(t, "nul ok", string(output)) +} + +func TestStepSaveLogReturnsFalseWhenDependenciesMissing(t *testing.T) { + started := time.Unix(1714333384, 0) + valid := &ExecutionRequest{ + Cfg: &config.Config{}, + Binding: &ActionBinding{ + Action: &config.Action{}, + }, + logEntry: &InternalLogEntry{ + ActionTitle: "Echo", + DatetimeStarted: started, + ExecutionTrackingID: "cccccccc-dddd-eeee-ffff-000000000000", + }, + } + + assert.False(t, stepSaveLog(nil)) + assert.False(t, stepSaveLog(&ExecutionRequest{})) + + missingLog := *valid + missingLog.logEntry = nil + assert.False(t, stepSaveLog(&missingLog)) + + missingBinding := *valid + missingBinding.Binding = nil + assert.False(t, stepSaveLog(&missingBinding)) + + missingAction := *valid + missingAction.Binding = &ActionBinding{} + assert.False(t, stepSaveLog(&missingAction)) + + missingCfg := *valid + missingCfg.Cfg = nil + assert.False(t, stepSaveLog(&missingCfg)) +} From b6218e9538ea41735ec1995cfc90b02abfc3f852 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 25 Jul 2026 23:05:00 +0100 Subject: [PATCH 14/17] docs: Improve installation intro page --- docs/modules/ROOT/pages/install/intro.adoc | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/modules/ROOT/pages/install/intro.adoc b/docs/modules/ROOT/pages/install/intro.adoc index baddb14..c8eae3e 100644 --- a/docs/modules/ROOT/pages/install/intro.adoc +++ b/docs/modules/ROOT/pages/install/intro.adoc @@ -6,10 +6,6 @@ You will need approximately **10 minutes** to install OliveTin on almost every p You probably will need about **10-20 minutes** to understand how OliveTin configuration works, and to be able to write your first action to make it do something useful. -== Where to find help - -include::partial$support.adoc[] - == First step: where are you installing? * Linux @@ -27,9 +23,17 @@ include::partial$support.adoc[] *** xref:install/docker_compose.adoc[Docker Compose] *** xref:install/helm.adoc[Kubernetes with Helm] *** xref:install/k8s.adoc[Kubernetes with Manifests] -* xref:install/bsd.adoc[FreeBSD] -* xref:install/windows.adoc[Windows] -* xref:install/macos.adoc[MacOS] +* xref:install/bsd.adoc[BSD] +* Windows +** xref:install/windows.adoc[Windows] +** xref:install/windows.adoc[Windows Service] +* macOS +** xref:install/macos.adoc[macOS Desktop] +** xref:install/macos.adoc[macOS Service] + +== Where to find help + +include::partial$support.adoc[] == What's Next? From 8f059778ce7d8f47e683e6fa46985acf31816c48 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 26 Jul 2026 00:11:47 +0100 Subject: [PATCH 15/17] docs: OliveTin now uses up to Level 2 of 5 AI autonomy. --- AI.md | 10 +++++++--- README.md | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/AI.md b/AI.md index 2248078..6d6cd2c 100644 --- a/AI.md +++ b/AI.md @@ -5,14 +5,18 @@ - [x] The project does not include any AI functionality at runtime. - [x] No data, usage, or similar is sent to, or analyized by AI. +## Development - Autonomy level + +OliveTin development uses up to **Level 2 of 5 (pair programmer)** on the [AI Levels of Autonomy in Software Engineering](https://blog.jread.com/posts/ai-levels-of-autonomy-in-software-engineering/) scale: AI may generate suggestions and non-trivial changes, but humans review and decide what lands in the project. + ## Development - Contributions - [x] The project **does accept** contributions that were written with AI help. **However**: - - The contribution must be attributed to a human username who takes responsibility for the code as if they wrote it themselves. - - AI often generates very unmaintainable code as it gets longer - loads of duplication, very little function re-use amd very poor at following style guides / idiomatic design. All code contributions (AI or not) are scrutinized hard for **maintainability** and **clean merging**. Please follow the CONTRIBUTORS guide. + - The contribution must be attributed to a human username who takes responsibility for the code as if they wrote it themselves. + - AI often generates very unmaintainable code as it gets longer - loads of duplication, very little function re-use amd very poor at following style guides / idiomatic design. All code contributions (AI or not) are scrutinized hard for **maintainability** and **clean merging**. Please follow the CONTRIBUTORS guide. - AI that helps with short tab completion is generally fine. - AI that writes lots of new code across lots of files, or makes lots of superfluous changes is generally less likely to be accepted. - - Vibe coding is not a suitable way to contribute to this project. + - Vibe coding is generally not a suitable way to contribute to this project. - [x] Contributors should declare when AI has been used to help write contributions in the pull request body message. - [x] The project uses AI as an **optional** part of the PR process (coderabbitai). Please raise any concerns about usage within the PR. - [x] Suggestions from coderabbitai can be accepted verbaitem, but ideally it should be the PR author that uses coderabbitai as a guide, who then re-writes the contribution. diff --git a/README.md b/README.md index 2223621..1c7c510 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5050/badge)](https://bestpractices.coreinfrastructure.org/projects/5050) [![Go Report Card](https://goreportcard.com/badge/github.com/Olivetin/OliveTin)](https://goreportcard.com/report/github.com/OliveTin/OliveTin) -[![AI Autonomy Level](https://img.shields.io/badge/AI%20Autonomy-Level%201%20of%205%20(assistance--only)-blue)](https://blog.jread.com/posts/ai-levels-of-autonomy-in-software-engineering/) +[![AI Autonomy Level](https://img.shields.io/badge/AI%20Autonomy-Level%202%20of%205%20(pair--programmer)-blue)](https://blog.jread.com/posts/ai-levels-of-autonomy-in-software-engineering/) [OliveTin 2k to 3k upgrade guide](https://docs.olivetin.app/upgrade/2k3k.html) From e226ec53dd974b07d1756f8918018ab6b03f8a5d Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 26 Jul 2026 00:55:26 +0100 Subject: [PATCH 16/17] chore: Dependency update --- frontend/package-lock.json | 8 +- frontend/package.json | 2 +- integration-tests/package-lock.json | 116 +++++++----- integration-tests/package.json | 6 +- service/go.mod | 132 +++++++------ service/go.sum | 277 ++++++++++++++-------------- 6 files changed, 275 insertions(+), 266 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b4f3e44..58f11e9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,7 +18,7 @@ "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.21.0", + "picocrank": "^1.21.1", "standard": "^17.1.2", "unplugin-vue-components": "^32.1.0", "vite": "^8.1.5", @@ -5272,9 +5272,9 @@ "license": "ISC" }, "node_modules/picocrank": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.21.0.tgz", - "integrity": "sha512-KM2xyrHfgrKwAwFtyr/o4cuHYqSeTS5sNAOpSUBw/zFvTGPBUWP/K82cYXRU4Tuz7NoQD6D0VbwAs/Q+rvEjkA==", + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.21.1.tgz", + "integrity": "sha512-VnfNFok7BkpLKymAIbD2zUA0bf/AiNBS63Xk2mfi8uXOkdAmf06pQaI0ssGBv2/S9p1jbCATWj9OFz1uqa39NQ==", "license": "ISC", "dependencies": { "@hugeicons/core-free-icons": "^4.2.2", diff --git a/frontend/package.json b/frontend/package.json index e7cd836..5762813 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,7 +32,7 @@ "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.21.0", + "picocrank": "^1.21.1", "standard": "^17.1.2", "unplugin-vue-components": "^32.1.0", "vite": "^8.1.5", diff --git a/integration-tests/package-lock.json b/integration-tests/package-lock.json index 24ce67d..7adefd5 100644 --- a/integration-tests/package-lock.json +++ b/integration-tests/package-lock.json @@ -9,13 +9,13 @@ "version": "1.0.0", "license": "AGPL-3.0-only", "dependencies": { - "wait-on": "^9.0.10" + "wait-on": "^9.1.0" }, "devDependencies": { "chai": "^6.2.2", - "eslint": "^10.6.0", + "eslint": "^10.8.0", "mocha": "^11.7.6", - "selenium-webdriver": "^4.45.0" + "selenium-webdriver": "^4.46.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -92,9 +92,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -172,9 +172,9 @@ "license": "BSD-3-Clause" }, "node_modules/@hapi/tlds": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", - "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.7.tgz", + "integrity": "sha512-MgNjRwy9Ti92yVAixLmDc8dd1bJIKwO9qlWCfFQRwRmUEDPQHYn4G6hwPFvFGUTzAa0FsS+inMjLin7GnyBRhA==", "license": "BSD-3-Clause", "engines": { "node": ">=14.0.0" @@ -333,6 +333,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -391,13 +403,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, @@ -408,16 +421,16 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/brace-expansion/node_modules/balanced-match": { @@ -636,7 +649,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -734,9 +746,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -783,9 +795,9 @@ } }, "node_modules/eslint": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", - "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "dev": true, "license": "MIT", "workspaces": [ @@ -795,7 +807,7 @@ "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.6.0", + "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", @@ -819,7 +831,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -1159,9 +1171,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -1253,6 +1265,19 @@ "he": "bin/he" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1375,9 +1400,9 @@ } }, "node_modules/joi": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz", - "integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.3.tgz", + "integrity": "sha512-N5A3KTWQpPWT4ExxxPlUx7WmykGXRzhNidWhV41d6Abu9YfI2NyWCJuxdPnslJCPWtbRpSVOWSnSS6GakLM/Rg==", "license": "BSD-3-Clause", "dependencies": { "@hapi/address": "^5.1.1", @@ -1393,9 +1418,9 @@ } }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "funding": [ { @@ -1625,9 +1650,9 @@ } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -1669,7 +1694,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -1879,9 +1903,9 @@ "dev": true }, "node_modules/selenium-webdriver": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.45.0.tgz", - "integrity": "sha512-Cb2nqvJiwXVOtRTCYHX9D1FJR5+Ls7aL3Nev0t6n4CpXsQ//YGiiUmSCbvTDDeLtbV85SZ46qmLab4SIYKXWRw==", + "version": "4.46.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.46.0.tgz", + "integrity": "sha512-UlTkgnx9y+bf3QxFsrgUyMqC2oy1Yf+qcOs7xIC/XfsUPv/ow7Sicx6SJ7AeOn2/Z+xF1M8SLqyn983wipI82w==", "dev": true, "funding": [ { @@ -2136,13 +2160,13 @@ "dev": true }, "node_modules/wait-on": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.10.tgz", - "integrity": "sha512-rCoJEhvMr0X6alHmwc9abbrA5ZrLZFKpFQVKPNFwl2h7DapXOGdmimIHDtLOWhT4PjhZhxFEtZoQgEXbkDWdZw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.1.0.tgz", + "integrity": "sha512-PymrLXHLBM1Ju/Xspb2ADUhbPSMvbnuNvy/mN2hWtpbJ3da0h3Ky1LqwKPG5QSVR57liyO0iUpfipYl/s5qNvA==", "license": "MIT", "dependencies": { - "axios": "^1.16.0", - "joi": "^18.2.1", + "axios": "^1.18.1", + "joi": "^18.2.3", "lodash": "^4.18.1", "minimist": "^1.2.8", "rxjs": "^7.8.2" diff --git a/integration-tests/package.json b/integration-tests/package.json index eca81fa..c3c7db1 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -12,11 +12,11 @@ "license": "AGPL-3.0-only", "devDependencies": { "chai": "^6.2.2", - "eslint": "^10.6.0", + "eslint": "^10.8.0", "mocha": "^11.7.6", - "selenium-webdriver": "^4.45.0" + "selenium-webdriver": "^4.46.0" }, "dependencies": { - "wait-on": "^9.0.10" + "wait-on": "^9.1.0" } } diff --git a/service/go.mod b/service/go.mod index 0f29f87..5d15066 100644 --- a/service/go.mod +++ b/service/go.mod @@ -10,7 +10,7 @@ require ( 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.71.0 + github.com/bufbuild/buf v1.72.0 github.com/expr-lang/expr v1.17.8 github.com/fsnotify/fsnotify v1.10.1 github.com/go-viper/mapstructure/v2 v2.5.0 @@ -23,12 +23,12 @@ require ( 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.5 - github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/client_golang v1.24.1 github.com/robfig/cron/v3 v3.0.1 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-20260611194520-c48552f49976 + golang.org/x/exp v0.0.0-20260718201538-764159d718ef golang.org/x/oauth2 v0.36.0 golang.org/x/sys v0.47.0 google.golang.org/protobuf v1.36.11 @@ -36,15 +36,15 @@ require ( ) require ( - 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect + 4d63.com/gocheckcompilerdirectives v1.4.0 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect - buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260626152828-968bf0468096.1 // indirect + buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260722160903-4d94f3df3a7b.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-20260415201107-50325440f8f2.1 // indirect - buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260629211719-be09125d9193.1 // indirect - buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260629211719-be09125d9193.1 // indirect + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1 // indirect + buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260713175918-10d915f5b43b.1 // indirect + buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260713175918-10d915f5b43b.1 // indirect buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 // indirect - buf.build/go/app v0.2.1-0.20260407195847-833f8f978cda // indirect + buf.build/go/app v0.2.1-0.20260626143626-be153867abea // indirect buf.build/go/bufplugin v0.10.0 // indirect buf.build/go/bufprivateusage v0.1.0 // indirect buf.build/go/interrupt v1.1.0 // indirect @@ -53,21 +53,21 @@ require ( buf.build/go/spdx v0.2.0 // indirect buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect cel.dev/expr v0.25.2 // indirect - charm.land/lipgloss/v2 v2.0.3 // indirect - codeberg.org/chavacava/garif v0.2.0 // indirect + charm.land/lipgloss/v2 v2.0.5 // indirect + codeberg.org/chavacava/garif v0.2.1 // indirect codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect connectrpc.com/otelconnect v0.9.0 // indirect dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect - dev.gaijin.team/go/golib v0.6.0 // indirect + dev.gaijin.team/go/golib v0.8.1 // indirect github.com/4meepo/tagalign v1.4.3 // indirect - github.com/Abirdcfly/dupword v0.1.7 // indirect + github.com/Abirdcfly/dupword v0.1.8 // indirect github.com/AdminBenni/iota-mixing v1.0.0 // indirect github.com/AlwxSin/noinlineerr v1.0.5 // indirect - github.com/Antonboom/errname v1.1.1 // indirect - github.com/Antonboom/nilnil v1.1.1 // indirect + github.com/Antonboom/errname v1.1.2 // indirect + github.com/Antonboom/nilnil v1.1.2 // indirect github.com/Antonboom/testifylint v1.6.4 // indirect github.com/BurntSushi/toml v1.6.0 // indirect - github.com/ClickHouse/clickhouse-go-linter v1.2.0 // indirect + github.com/ClickHouse/clickhouse-go-linter v1.2.1 // indirect github.com/Djarvur/go-err113 v0.1.1 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/MicahParks/jwkset v0.11.0 // indirect @@ -75,7 +75,7 @@ require ( github.com/MirrexOne/unqueryvet v1.5.4 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect github.com/PaesslerAG/gval v1.2.4 // indirect - github.com/alecthomas/chroma/v2 v2.24.1 // indirect + github.com/alecthomas/chroma/v2 v2.27.0 // indirect github.com/alecthomas/go-check-sumtype v0.3.1 // indirect github.com/alexkohler/nakedret/v2 v2.0.6 // indirect github.com/alexkohler/prealloc v1.1.0 // indirect @@ -92,16 +92,16 @@ require ( github.com/bombsimon/wsl/v5 v5.8.0 // indirect github.com/breml/bidichk v0.3.3 // indirect github.com/breml/errchkjson v0.4.1 // indirect - github.com/bufbuild/protocompile v0.14.2-0.20260605203730-cd7c3c124e10 // indirect + github.com/bufbuild/protocompile v0.14.2-0.20260716165721-bb5762d29672 // indirect github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b // indirect github.com/butuzov/ireturn v0.4.1 // indirect - github.com/butuzov/mirror v1.3.0 // indirect + github.com/butuzov/mirror v1.3.3 // indirect github.com/catenacyber/perfsprint v0.10.1 // indirect github.com/ccojocar/zxcvbn-go v1.0.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charithe/durationcheck v0.0.11 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect - github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260720091822-7cc6674724ac // indirect github.com/charmbracelet/x/ansi v0.11.7 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect @@ -114,26 +114,26 @@ require ( github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect - github.com/daixiang0/gci v0.13.7 // indirect - github.com/dave/dst v0.27.3 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/denis-tingaikin/go-header v0.5.0 // indirect + github.com/daixiang0/gci v0.14.0 // indirect + github.com/dave/dst v0.27.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/denis-tingaikin/go-header v1.0.0 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/dlclark/regexp2 v1.12.0 // indirect - github.com/docker/cli v29.6.1+incompatible // indirect + github.com/dlclark/regexp2/v2 v2.5.2 // indirect + github.com/docker/cli v29.6.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.8 // indirect - github.com/docker/go-connections v0.7.0 // indirect + github.com/docker/go-connections v0.8.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/color v1.19.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect - github.com/firefart/nonamedreturns v1.0.6 // indirect + github.com/firefart/nonamedreturns v1.0.8 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect - github.com/ghostiam/protogetter v0.3.20 // indirect + github.com/ghostiam/protogetter v0.3.21 // indirect github.com/go-critic/go-critic v0.14.4 // indirect github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect @@ -154,7 +154,7 @@ require ( github.com/golangci/misspell v0.8.0 // indirect github.com/golangci/plugin-module-register v0.1.2 // indirect github.com/golangci/revgrep v0.8.0 // indirect - github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba // indirect + github.com/golangci/rowserrcheck v0.0.0-20260701145345-399602f983c8 // indirect github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect github.com/google/cel-go v0.29.2 // indirect @@ -168,24 +168,23 @@ require ( github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect github.com/hashicorp/go-version v1.9.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jdx/go-netrc v1.0.0 // indirect - github.com/jgautheron/goconst v1.10.0 // indirect + github.com/jgautheron/goconst v1.10.2 // indirect github.com/jjti/go-spancheck v0.6.5 // indirect github.com/julz/importas v0.2.0 // indirect github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect - github.com/kisielk/errcheck v1.10.0 // indirect + github.com/kisielk/errcheck v1.20.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect - github.com/klauspost/compress v1.19.0 // indirect + github.com/klauspost/compress v1.19.1 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/kulti/thelper v0.7.1 // indirect github.com/kunwardeep/paralleltest v1.0.15 // indirect github.com/lasiar/canonicalheader v1.1.2 // indirect github.com/ldez/exptostd v0.4.5 // indirect - github.com/ldez/gomoddirectives v0.8.0 // indirect + github.com/ldez/gomoddirectives v0.9.0 // indirect github.com/ldez/grignotin v0.10.1 // indirect github.com/ldez/structtags v0.6.1 // indirect github.com/ldez/tagliatelle v0.7.2 // indirect @@ -193,19 +192,17 @@ require ( github.com/leonklingele/grouper v1.1.2 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/macabu/inamedparam v0.2.0 // indirect - github.com/magiconair/properties v1.8.6 // indirect github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect github.com/manuelarte/funcorder v0.6.0 // indirect github.com/maratori/testableexamples v1.0.1 // indirect github.com/maratori/testpackage v1.1.2 // indirect github.com/matoous/godox v1.1.0 // indirect github.com/mattn/go-colorable v0.1.15 // indirect - github.com/mattn/go-isatty v0.0.22 // indirect - github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/mattn/go-runewidth v0.0.27 // indirect github.com/mgechev/revive v1.15.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/moby/api v1.55.0 // indirect @@ -216,15 +213,14 @@ require ( github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect - github.com/nunnatsa/ginkgolinter v0.23.0 // indirect + github.com/nunnatsa/ginkgolinter v0.23.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.3.1 // indirect - github.com/petermattis/goid v0.0.0-20260701081913-4f67fd55d3b4 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect + github.com/petermattis/goid v0.0.0-20260725062400-500c67a39b75 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.69.0 // indirect + github.com/prometheus/common v0.70.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect github.com/quasilyte/go-ruleguard v0.4.5 // indirect github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect @@ -232,45 +228,45 @@ require ( github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.60.0 // indirect - github.com/raeperd/recvcheck v0.2.0 // indirect + github.com/quic-go/quic-go v0.61.0 // indirect + github.com/raeperd/recvcheck v0.3.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rogpeppe/go-internal v1.15.0 // indirect github.com/rs/cors v1.11.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryancurrah/gomodguard v1.4.1 // indirect github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect - github.com/securego/gosec/v2 v2.26.1 // indirect + github.com/securego/gosec/v2 v2.28.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sivchari/containedctx v1.0.3 // indirect github.com/sonatard/noctx v0.5.1 // indirect github.com/sourcegraph/go-diff v0.8.0 // indirect github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cobra v1.10.2 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.12.0 // indirect + github.com/spf13/viper v1.21.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/subosito/gotenv v1.4.1 // indirect + github.com/stretchr/objx v0.5.3 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/tetafro/godot v1.5.6 // indirect github.com/tetratelabs/wazero v1.12.0 // indirect github.com/tidwall/btree v1.8.1 // indirect - github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 // indirect + github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c // indirect github.com/timonwong/loggercheck v0.11.0 // indirect github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/ultraware/funlen v0.2.0 // indirect github.com/ultraware/whitespace v0.2.0 // indirect github.com/uudashr/gocognit v1.2.1 // indirect - github.com/uudashr/iface v1.4.2 // indirect + github.com/uudashr/iface v1.5.0 // indirect github.com/xen0n/gosmopolitan v1.3.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yagipy/maintidx v1.0.0 // indirect @@ -280,7 +276,7 @@ require ( go-simpler.org/musttag v0.14.0 // indirect go-simpler.org/sloglint v0.12.0 // indirect go.augendre.info/arangolint v0.4.0 // indirect - go.augendre.info/fatcontext v0.9.0 // indirect + go.augendre.info/fatcontext v0.10.0 // indirect go.lsp.dev/jsonrpc2 v1.0.1 // indirect go.lsp.dev/protocol v1.0.1 // indirect go.lsp.dev/uri v1.0.1 // indirect @@ -293,19 +289,17 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.53.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.56.0 // indirect + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20260718201538-764159d718ef // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/net v0.57.0 // indirect golang.org/x/sync v0.22.0 // indirect - golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.39.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.47.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + golang.org/x/tools v0.48.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df // indirect honnef.co/go/tools v0.7.0 // indirect mvdan.cc/gofumpt v0.10.0 // indirect mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect diff --git a/service/go.sum b/service/go.sum index 0711bd7..b898710 100644 --- a/service/go.sum +++ b/service/go.sum @@ -1,21 +1,21 @@ -4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A= -4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= +4d63.com/gocheckcompilerdirectives v1.4.0 h1:ZLq62rbGWVmQhiZ8kuNVIT/M09xCSTdJz9K3xOdT/CY= +4d63.com/gocheckcompilerdirectives v1.4.0/go.mod h1:9ZOAiMOjqC/nRwci2fcUXVHUNLG/cH6r6rhUh+jTFtQ= 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= -buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260626152828-968bf0468096.1 h1:bHi5/cwz7IPRhFcrRA4iytTjDI3WwFkv1m5K7y+YOTc= -buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260626152828-968bf0468096.1/go.mod h1:1Znr6gmYBhbxWUPRrrVnSLXQsz8bvFVw1HHJq2bI3VQ= +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260722160903-4d94f3df3a7b.1 h1:IbnjTSFlFUyPCRF50eY1u9EyPpU1wAyAN5neSgI5jyE= +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260722160903-4d94f3df3a7b.1/go.mod h1:1Znr6gmYBhbxWUPRrrVnSLXQsz8bvFVw1HHJq2bI3VQ= buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 h1:HwzzCRS4ZrEm1++rzSDxHnO0DOjiT1b8I/24e8a4exY= 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-20260415201107-50325440f8f2.1 h1:s6hzCXtND/ICdGPTMGk7C+/BFlr2Jg5GyH0NKf4XGXg= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= -buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260629211719-be09125d9193.1 h1:szEsAEWp6MqAX3NHUFq3ZH3oGKyajcte99KrCz0MpQ0= -buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260629211719-be09125d9193.1/go.mod h1:VKtlgFOV7s6j5/wPr+d6c0Q80c8MF4OfZev4e0d9pNo= -buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260629211719-be09125d9193.1 h1:JYYze3qnJZxo4PWTSZGKH+NJs0Vi2DYFo/NzUpa8Em4= -buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260629211719-be09125d9193.1/go.mod h1:5QgvXU4BhJqHgkO27sFJrRQHABWN4X6QnGvL8hWCJKk= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1 h1:fXh8CsdNpjRr8R5vFdqtIxPt/Lno2IIJlYOdZBIZn0w= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260713175918-10d915f5b43b.1 h1:c4w6fQ6wAhZLgVI+HZozlWX9I2aVPN34vrl/8cuovVc= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260713175918-10d915f5b43b.1/go.mod h1:mbcuFKpnCQsF8H2fBs1lN1dQVAiASPQKwFgTkXqYscI= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260713175918-10d915f5b43b.1 h1:qJUlFKLFdb3OFCpmA+r5AQbysgBD7WLRapE9lOs08r8= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260713175918-10d915f5b43b.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.1-0.20260407195847-833f8f978cda h1:eysSyjrJtkxU1A/9+Kv+1Mwq9K6BYBw+STIOVsZ256Y= -buf.build/go/app v0.2.1-0.20260407195847-833f8f978cda/go.mod h1:V32mBaPWsfq6REAeZvvs/rQl7ZCl9Dn7eW1BBrmH0GQ= +buf.build/go/app v0.2.1-0.20260626143626-be153867abea h1:krHaIyUJSTnurb0fbYQtsXkZWiWQ/ydOgfm61TPSODc= +buf.build/go/app v0.2.1-0.20260626143626-be153867abea/go.mod h1:V32mBaPWsfq6REAeZvvs/rQl7ZCl9Dn7eW1BBrmH0GQ= buf.build/go/bufplugin v0.10.0 h1:vZBX0mq9as5UIBug8U+/DkGRaHNlM/HVOw59O8fvOIU= buf.build/go/bufplugin v0.10.0/go.mod h1:ax7obVurKDH1I2nR4pFTS+TE6K3kZhTmwDCN2YgdV8I= buf.build/go/bufprivateusage v0.1.0 h1:SzCoCcmzS3zyXHEXHeSQhGI7OTkgtljoknLzsUz9Gg4= @@ -32,10 +32,10 @@ buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 h1:njYKSWoLiq2i5O7y2b buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5/go.mod h1:DQmodNT9EHX94WzUaWiZK+/4EaFa/xZTc1gzfCxZVXU= cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= -charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU= -charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA= -codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= -codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= +charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= +charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc= +codeberg.org/chavacava/garif v0.2.1 h1:K9oYxSlvlXrHXyW26Z4q61bTpJDo1wbvXcYKar/F/LM= +codeberg.org/chavacava/garif v0.2.1/go.mod h1:oHnDSmc0f9K1MeE+MQD/yjkiIB5Xsn5y3S9Dg96Xk84= codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI= codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8= connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= @@ -46,26 +46,26 @@ connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc= dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y= dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= -dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= -dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE= +dev.gaijin.team/go/golib v0.8.1 h1:JYju4x9BSo+QD/AYeHULVDcvEhiFg8wOi6pT0IaZF5E= +dev.gaijin.team/go/golib v0.8.1/go.mod h1:c5fu7t1RSGMxSQgcUYO1sODbzsYnOCXJLmHeNG1Eb+0= github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8= github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= -github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ= -github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4= +github.com/Abirdcfly/dupword v0.1.8 h1:SrhcUuGsROBuChFxHALRYzyyPODWn9zwghmzPvD9Cd8= +github.com/Abirdcfly/dupword v0.1.8/go.mod h1:XZrhVnI7YGpsTiWZANSQaBJ4QpL/Tq5vIEdKJJAs9WI= github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo= github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY= github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY= github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc= -github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q= -github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ= -github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ= -github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II= +github.com/Antonboom/errname v1.1.2 h1:dxwONZJua3VB8Xh/VaCjqAcqF645sWWv7xj26zy7tdQ= +github.com/Antonboom/errname v1.1.2/go.mod h1:YeZIpgLMxT+SNkruGgYkLhzq/9vs3fsolTZegKaKDZI= +github.com/Antonboom/nilnil v1.1.2 h1:aNlFuJhaEseXe4fHO3xbjXlSeEiQVYa2lEkWD2s2hAY= +github.com/Antonboom/nilnil v1.1.2/go.mod h1:0ynwvphOLmAuMwTNDyBnDZmSwZoDpcFXmUHmzoHH2WA= github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ= github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/ClickHouse/clickhouse-go-linter v1.2.0 h1:zbm174up3hTKjp0wKZVnTzRiG7tSF5XZF0FJG/MuCBI= -github.com/ClickHouse/clickhouse-go-linter v1.2.0/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4= +github.com/ClickHouse/clickhouse-go-linter v1.2.1 h1:zGEKIyd5YL08ieWG/LOUmlau2DxbxPVOfAeo+4Jz3ck= +github.com/ClickHouse/clickhouse-go-linter v1.2.1/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4= github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= @@ -90,8 +90,8 @@ github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEs github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM= -github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI= +github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs= +github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8= github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= @@ -132,16 +132,16 @@ github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDw github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s= 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.71.0 h1:/XLs8mFKjlM/LVbBBBQ3uSNcyEClyAV8lJiO7QR04d8= -github.com/bufbuild/buf v1.71.0/go.mod h1:fbHMfu0nEbIPQT3IG7rAfPZXoFYEfZJuqFbAXu1wnRU= -github.com/bufbuild/protocompile v0.14.2-0.20260605203730-cd7c3c124e10 h1:N0ZOKMwHpKsFhMGSxnB6JiVkM7SbH8IAKlGGO0SpQBA= -github.com/bufbuild/protocompile v0.14.2-0.20260605203730-cd7c3c124e10/go.mod h1:jPUiZUFWc8E3Kc2Y4SRlGAdjde4amGkHY0BUACNS43E= +github.com/bufbuild/buf v1.72.0 h1:VMmGFtCLrxyS2wkpghExmhhiqJDdmc8DcwAvsGJGJ94= +github.com/bufbuild/buf v1.72.0/go.mod h1:bhtIlPDo3q/PDw4yaTdx2+jxkc33Aq+ygFIi6Diz2yU= +github.com/bufbuild/protocompile v0.14.2-0.20260716165721-bb5762d29672 h1:6xykiXQPoF/hhOjAhwiHQ0kgcAvrDZw0Tjl1VQkpu5c= +github.com/bufbuild/protocompile v0.14.2-0.20260716165721-bb5762d29672/go.mod h1:jPUiZUFWc8E3Kc2Y4SRlGAdjde4amGkHY0BUACNS43E= github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b h1:b7wvo9ZhjLzCp7tGbOUMvgtYTnd33zGSAmMxcdxMnhQ= github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ= github.com/butuzov/ireturn v0.4.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I= github.com/butuzov/ireturn v0.4.1/go.mod h1:q+DXKzTDV5guNuXLnIab9fKXizTn2miZHLhxH7V/GB4= -github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= -github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= +github.com/butuzov/mirror v1.3.3 h1:v0RsWBhfFc1RQqE/f3sHpSttKDtodFn0gFmtYyD4/hA= +github.com/butuzov/mirror v1.3.3/go.mod h1:h9BzzwYnTiHO0GzgvaTqIg7VSsOUhdIv51cHFFBmX1w= github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ= github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= @@ -152,8 +152,8 @@ github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1Di github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= -github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI= -github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI= +github.com/charmbracelet/ultraviolet v0.0.0-20260720091822-7cc6674724ac h1:BP8qMDGjmOejoVTklEXXTHI0OwMIt8JHPSPHxscFFwA= +github.com/charmbracelet/ultraviolet v0.0.0-20260720091822-7cc6674724ac/go.mod h1:psnCZIfwwxVs6v6DhUc6NJ8AQ3ejvs2ejKwoOMeVmUk= github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= @@ -179,27 +179,30 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3 github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= -github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ= -github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ= -github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= -github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= +github.com/daixiang0/gci v0.14.0 h1:h6AcLqmjIOBgojhtzY2CvBnA6RawPTkBHgtMvYD5YZ8= +github.com/daixiang0/gci v0.14.0/go.mod h1:w9E+SWQ4aPQ+xYPUdqitGDoXpT4mayqOfk7Szz2U6wQ= +github.com/dave/dst v0.27.4 h1:d+EVnOZmphH+lUEXq9rit4GjsFSKJ3AhfRWf7eobTps= +github.com/dave/dst v0.27.4/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= -github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denis-tingaikin/go-header v1.0.0 h1:QIwZWb3jLC6pOp9NEFldiD8raqRmCE/n0VUdZKW32x8= +github.com/denis-tingaikin/go-header v1.0.0/go.mod h1:NT3qKwqsXQYp8WHVgkwxL49qB5jsRmdr9dGQCDfpmZ0= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v29.6.1+incompatible h1:oO7F4nn3Ovr/5TlfTUWFbMwBSS/B7Xs6Epv26gBrUP8= -github.com/docker/cli v29.6.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/dlclark/regexp2/v2 v2.5.2 h1:HAsucWRhsqcDzl6Ua9aR8JwYOTzrZyPrF0/FNxJVAI0= +github.com/dlclark/regexp2/v2 v2.5.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= +github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn6LwTVOcqw= +github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q= github.com/docker/docker-credential-helpers v0.9.8/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= -github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= -github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-connections v0.8.0 h1:T9UlP76qPLA/HaLrcC+s4Doqqv5XsWMMUGPF5Aih/k0= +github.com/docker/go-connections v0.8.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= @@ -212,23 +215,23 @@ github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= -github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E= -github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/firefart/nonamedreturns v1.0.8 h1:iB32Dl17zJl1zlVEj/WlUWgx0HiRyQ85OUw1WHa4/II= +github.com/firefart/nonamedreturns v1.0.8/go.mod h1:vxFNvm5AfP/8rgAKFzYmnqx0yp1HjrYsErZ9pHPTznA= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= -github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= -github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= +github.com/ghostiam/protogetter v0.3.21 h1:EeWTGvL/Eyosp653hiWb6Byx4b69iJC4/E+za7vQHoI= +github.com/ghostiam/protogetter v0.3.21/go.mod h1:iAKSpyoHwYzay+OpjoWgwzRtPFthEfuUvmlomTThck0= github.com/go-critic/go-critic v0.14.4 h1:dSX4C3pWSeuMVxvQh6yG8U0ReSf3YOmKi4nwX5q7n/8= github.com/go-critic/go-critic v0.14.4/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg= github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474= @@ -284,8 +287,8 @@ github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3H github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= -github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba h1:lqtcnSMDuuJdu/LrKWi5RJzpSNLOJXYe/nzQutTI5kg= -github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba/go.mod h1:sCBNcpRmhJCtbFGz49+IM3ETTFf7QdJ30AeYCd43NKk= +github.com/golangci/rowserrcheck v0.0.0-20260701145345-399602f983c8 h1:OtbxoLqMgJUWEHXhlNkvTlxJMQNSddWMZP/e6FaNkBI= +github.com/golangci/rowserrcheck v0.0.0-20260701145345-399602f983c8/go.mod h1:RLgEC6y0EaFAJT8kkHrRxwsGFwYamOB6BYzUZOpfTVs= github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM= github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= @@ -299,8 +302,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= -github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= -github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/pprof v0.0.0-20260709232956-b9395ee17fa0 h1:du0WGc8xSKq/++e0cglxhS/mXVqsR7+c7jLEi5Vqduw= +github.com/google/pprof v0.0.0-20260709232956-b9395ee17fa0/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs= @@ -326,8 +329,6 @@ github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaX github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -336,8 +337,8 @@ github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da h1:hYsJqujd3A4Xtp github.com/jamesread/golure v0.0.0-20260510214136-6ef80e0ce8da/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE= github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ= github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8= -github.com/jgautheron/goconst v1.10.0 h1:Ptt+OoE4NaEWKhLrWrrN3IpZdGLiqaf7WLnEX/iv4Jw= -github.com/jgautheron/goconst v1.10.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc= +github.com/jgautheron/goconst v1.10.2 h1:0wFg/DbpYB0ChoP94qUM+SuQpqj5oCzPEqAil5dV8Pg= +github.com/jgautheron/goconst v1.10.2/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc= github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88= github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM= github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= @@ -346,12 +347,12 @@ github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0= github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY= -github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw= -github.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= +github.com/kisielk/errcheck v1.20.0 h1:9rwHBNKzd4wkDWcROy3DvFGNqEPlkxBg305rvk7HabI= +github.com/kisielk/errcheck v1.20.0/go.mod h1:O+f80MKNwX8Oor2jwgpeQ9An7uJm+hRSgT+h22knRJU= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= -github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= -github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= +github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= 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= @@ -380,8 +381,8 @@ github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0 github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ= github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM= -github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk= -github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q= +github.com/ldez/gomoddirectives v0.9.0 h1:2YV/EX7nVlWL4jySusYTzBKHuE3D2fgcRsQuMa3yIoo= +github.com/ldez/gomoddirectives v0.9.0/go.mod h1:DdZzfm9MdXCjn2/UGYXCFfo+tzrp2Ib4iD2Q0kIJkwE= github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o= github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas= github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk= @@ -396,8 +397,6 @@ github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= github.com/manuelarte/funcorder v0.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0= @@ -412,18 +411,16 @@ github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= -github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0= +github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q= github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -444,12 +441,12 @@ github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhK github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8= -github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4= -github.com/onsi/ginkgo/v2 v2.28.2 h1:DTrMfpqxiNUyQ3Y0zhn1n3cOO2euFgQPYIpkWwxVFps= -github.com/onsi/ginkgo/v2 v2.28.2/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/nunnatsa/ginkgolinter v0.23.1 h1:1JOE+iDubBmH2YvP485w9AHBI8osHFBdVeMucRRvkDM= +github.com/nunnatsa/ginkgolinter v0.23.1/go.mod h1:jh1sHyjC5TzL24rFUJvxtXV0iD2/9S7C/8N4aQd/lag= +github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E= +github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= +github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -461,20 +458,19 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= -github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/petermattis/goid v0.0.0-20260701081913-4f67fd55d3b4 h1:YCvN6o1QTbVkeHMlTjnzfP02SvsmOZLlJdFzw/f9BTk= -github.com/petermattis/goid v0.0.0-20260701081913-4f67fd55d3b4/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/petermattis/goid v0.0.0-20260725062400-500c67a39b75 h1:VmZ6mKVkxavKEhEy4ZYyV7BwBYBFBP0TwIqmLk84fpU= +github.com/petermattis/goid v0.0.0-20260725062400-500c67a39b75/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= +github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk= -github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= +github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= +github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4= @@ -493,18 +489,18 @@ github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4 github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= -github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= -github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI= -github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= +github.com/quic-go/quic-go v0.61.0 h1:ui88A53s8MSVYLC56en0KQ17HARk+9986Dn0SBfKNvA= +github.com/quic-go/quic-go v0.61.0/go.mod h1:9So2anK4Tp22URSQq00k+Vo2PNkle96ycDPDHL4s9vs= +github.com/raeperd/recvcheck v0.3.0 h1:PM+XYvyxIj3bo+kobJfFTdTuU3Lmfu96mKDbyHDbRt8= +github.com/raeperd/recvcheck v0.3.0/go.mod h1:PZNwG+HztFYMH2ZPq0Hu3QgkV2yiA6VrtNz9c1fXWJo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= +github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -515,6 +511,8 @@ github.com/ryancurrah/gomodguard/v2 v2.1.3 h1:E7sz3PJwE9Ba1reVxSpF6XLCPJZ74Kfw/L github.com/ryancurrah/gomodguard/v2 v2.1.3/go.mod h1:CQicdLGatWMxLX53JzoBjYlsNZhHbmLv2AVa0s2aivU= github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg= github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= @@ -523,8 +521,8 @@ github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tM github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ= github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8= -github.com/securego/gosec/v2 v2.26.1 h1:gdkttGhQFVehqRJ8grKH4DrpqM/QlPKNHBnl8QgcEC4= -github.com/securego/gosec/v2 v2.26.1/go.mod h1:57UW4p0uoP3kxoTkhoo3axLdVAi+OWrLg/Ax/kdqtPE= +github.com/securego/gosec/v2 v2.28.0 h1:ZsSdiDb0AtTpLFVol5z91gbMei9ZiLEPG/pZjZujp7c= +github.com/securego/gosec/v2 v2.28.0/go.mod h1:lb4/9AHe+lJy/kjWmWRWWsEipvbwGKuxf+tY1Pmjdnk= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -540,32 +538,29 @@ github.com/sourcegraph/go-diff v0.8.0 h1:ipIyu4cTsLbIrln4l0qtHA3r0a7gyK4ntKjtQyt github.com/sourcegraph/go-diff v0.8.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= -github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= @@ -576,8 +571,8 @@ github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZ github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0= github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA= github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A= -github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 h1:SiHe5XLTn9sFWJ5pBwJ5FN/4j34q9ZlOAD//kMoMYp0= -github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M= +github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c h1:d/FUP8GD4QiY/rdFzZmBu3KqL9eBNVzMu7W/5pGkaMM= +github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M= github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M= github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is= @@ -590,8 +585,8 @@ github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSW github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4= github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q= -github.com/uudashr/iface v1.4.2 h1:06Vq5RKVYThBsj0Bnw4oasMjD1r+7CE/bcKOA8dVSvg= -github.com/uudashr/iface v1.4.2/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= +github.com/uudashr/iface v1.5.0 h1:PgdMt4uAettGG8K/Kbamc4B9FABgUgnS3TLbl6fnjEk= +github.com/uudashr/iface v1.5.0/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= @@ -621,8 +616,8 @@ go.akshayshah.org/connectproto v0.6.0 h1:tqmysQF2AfvUeYS03mRAAZTFpiQeXqhGIDnH1GO go.akshayshah.org/connectproto v0.6.0/go.mod h1:uA9TR/6MhBlLn0fh8VXRyL26EKTJlimWao4jbz7JHbA= go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50= go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA= -go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE= -go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= +go.augendre.info/fatcontext v0.10.0 h1:HhFopmivh8U1+AU7f0kuwUeg2eiIns7YsGQOMHwSJ90= +go.augendre.info/fatcontext v0.10.0/go.mod h1:pqpGvA9GlrXy+aXkp8L2dKz12Zp4g2FhzcAtwToU+2w= go.lsp.dev/jsonrpc2 v1.0.1 h1:JZAchpm6RcOuwEOnZ7kF3kA1EpEjdJZ/6rMSfOF3+F4= go.lsp.dev/jsonrpc2 v1.0.1/go.mod h1:WN+TesqrPumBmUdsuWyn9DHq3zl4i85L/qG0NLdj8qg= go.lsp.dev/protocol v1.0.1 h1:kLWUIk1v/deXxo1D0VO4avjZnfeuZ82mouJgJd/NbdU= @@ -660,22 +655,22 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A= +golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= 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-20260611194520-c48552f49976 h1:GTD/WuaexTazIG/SxLOz4rEKZPDVilmVVC2nz4xhwfE= -golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo= +golang.org/x/exp/typeparams v0.0.0-20260718201538-764159d718ef h1:HbgfnigKyfbszJsaFQE4IMPTcjdKZMNxyYlF00yVkf4= +golang.org/x/exp/typeparams v0.0.0-20260718201538-764159d718ef/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -686,8 +681,8 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -721,8 +716,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -731,8 +726,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= -golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -744,8 +739,8 @@ golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0t golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= @@ -754,20 +749,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 h1:admdQBe8jR3VWhBsUrAOaF2Qw6K/+p5pSm1GN8+6Fw4= -google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800/go.mod h1:FPk7EXUKMtImne7AmknoYjT4QXqKIzzRbeQIXzLk6fQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df h1:NsJx+hCSwIBI6+C4BuJIkb8xOG1M+nfQDsqIrQHT92k= +google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df h1:O3ig1i5WDDzsVzRp+cCdgelT9vXnlnOFdlEeFtL4HCc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= From 839d244e51ddd3c80c09bfbbe0fd68ceb07dc83a Mon Sep 17 00:00:00 2001 From: jamesread Date: Mon, 27 Jul 2026 23:17:42 +0100 Subject: [PATCH 17/17] fix: intermediate output and comfirmation --- .../ROOT/pages/args/input_confirmation.adoc | 2 + docs/modules/ROOT/pages/args/types.adoc | 2 +- service/internal/executor/arguments.go | 26 +++++++--- service/internal/executor/arguments_test.go | 37 +++++++++++++-- service/internal/executor/executor.go | 27 ++++++++++- service/internal/executor/executor_test.go | 47 +++++++++++++++++++ service/internal/executor/log_arguments.go | 3 +- 7 files changed, 130 insertions(+), 14 deletions(-) diff --git a/docs/modules/ROOT/pages/args/input_confirmation.adoc b/docs/modules/ROOT/pages/args/input_confirmation.adoc index 1209f22..edeb3da 100644 --- a/docs/modules/ROOT/pages/args/input_confirmation.adoc +++ b/docs/modules/ROOT/pages/args/input_confirmation.adoc @@ -3,6 +3,8 @@ The `confirmation` type argument is a special argument type, which simply disables the "Start" button until a checkbox is ticked. This can be useful if you have an action with no other arguments, but you want to prevent accidental button-clicks starting the action. +Confirmation arguments are usually left unnamed, so nothing is substituted into the command. If you do give the argument a `name`, the only allowed values are `0` (unchecked) and `1` (checked), so confirmation is safe to use with `shell:` actions. + [source,yaml] ---- actions: diff --git a/docs/modules/ROOT/pages/args/types.adoc b/docs/modules/ROOT/pages/args/types.adoc index 272db8e..e9e4cb5 100644 --- a/docs/modules/ROOT/pages/args/types.adoc +++ b/docs/modules/ROOT/pages/args/types.adoc @@ -20,7 +20,7 @@ A full list of argument types are below; | regex:... | xref:args/input.adoc[Textbox] | Version 2024.03.081 and above support custom regex patterns. See xref:args/regex.adoc[Custom regex arguments]. | int | xref:args/input.adoc[Textbox] | Any number, made up of the characters 0 to 9. Negative numbers are not supported. | url | xref:args/input.adoc[Textbox] | A URL (e.g. https://example.com). Accepts any scheme, including `file://` and `ftp://`. See warning below. -| confirmation | xref:args/input_confirmation.adoc[Confirmation] | A "hidden" argument that makes the action require a confirmation before launching. +| confirmation | xref:args/input_confirmation.adoc[Confirmation] | A UI gate that requires a checkbox before starting. Usually unnamed (nothing is substituted). If named, the value is only `0` or `1`. | checklist | xref:args/input_checklist.adoc[Checklist] | Multiple checkboxes from predefined choices. Selected values are passed as a comma-separated string. | n/a, but `choices` used | xref:args/input_dropdown.adoc[Dropdown] | A "hidden" argument that makes the action require a confirmation before launching. | raw_string_multiline | xref:args/input_textarea.adoc[Textarea] | Anything. This is **dangerous**, as effectively people can type anything they like diff --git a/service/internal/executor/arguments.go b/service/internal/executor/arguments.go index e9ff778..53ce43f 100644 --- a/service/internal/executor/arguments.go +++ b/service/internal/executor/arguments.go @@ -150,12 +150,7 @@ func redactExecArgs(execArgs []string, arguments []config.ActionArgument, argume } func argumentSkipsValidation(arg *config.ActionArgument) bool { - switch arg.Type { - case "confirmation", "html": - return true - default: - return false - } + return arg.Type == "html" } func typecheckActionArgument(arg *config.ActionArgument, value string, action *config.Action) error { @@ -163,6 +158,10 @@ func typecheckActionArgument(arg *config.ActionArgument, value string, action *c return nil } + if arg.Type == "confirmation" { + return typecheckConfirmation(arg, value) + } + if arg.Name == "" { return fmt.Errorf("argument name cannot be empty") } @@ -170,6 +169,20 @@ func typecheckActionArgument(arg *config.ActionArgument, value string, action *c return typecheckActionArgumentFound(value, arg) } +// typecheckConfirmation allows unnamed confirmation args as UI-only gates. +// Named confirmation values are only ever "0" or "1", matching the web UI. +func typecheckConfirmation(arg *config.ActionArgument, value string) error { + if arg.Name == "" { + return nil + } + + if value == "0" || value == "1" { + return nil + } + + return fmt.Errorf("argument %q of type confirmation must be \"0\" or \"1\"", arg.Name) +} + // ValidateArgument validates a single argument value using the same logic as the executor. // It applies mangling transformations and performs full validation including null checks, // choice validation, and type safety checks. @@ -403,7 +416,6 @@ var shellUnsafeArgumentTypes = map[string]struct{}{ "very_dangerous_raw_string": {}, "password": {}, "html": {}, - "confirmation": {}, } func isUnsafeShellArgumentType(arg *config.ActionArgument) bool { diff --git a/service/internal/executor/arguments_test.go b/service/internal/executor/arguments_test.go index 6d28626..23ae6f5 100644 --- a/service/internal/executor/arguments_test.go +++ b/service/internal/executor/arguments_test.go @@ -523,8 +523,20 @@ func TestCheckShellArgumentSafetyWithConfirmation(t *testing.T) { } err := checkShellArgumentSafety(&a1) - assert.NotNil(t, err) - assert.Contains(t, err.Error(), "unsafe argument type 'confirmation'") + assert.Nil(t, err, "confirmation is constrained to 0/1 and is safe with shell") +} + +func TestCheckShellArgumentSafetyWithUnnamedConfirmation(t *testing.T) { + a1 := config.Action{ + Title: "Confirm shell unnamed", + Shell: "echo ok", + Arguments: []config.ActionArgument{ + {Type: "confirmation", Title: "Are you sure?!"}, + }, + } + + err := checkShellArgumentSafety(&a1) + assert.Nil(t, err) } func TestCheckShellArgumentSafetyWithChoicelessCheckbox(t *testing.T) { @@ -930,8 +942,27 @@ func TestTypecheckActionArgumentConfirmation(t *testing.T) { } action := config.Action{Title: "Test"} + assert.Nil(t, typecheckActionArgument(&arg, "0", &action)) + assert.Nil(t, typecheckActionArgument(&arg, "1", &action)) + err := typecheckActionArgument(&arg, "any_value", &action) - assert.Nil(t, err, "Confirmation type should always pass validation") + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "must be \"0\" or \"1\"") + + err = typecheckActionArgument(&arg, "", &action) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "must be \"0\" or \"1\"") +} + +func TestTypecheckActionArgumentUnnamedConfirmation(t *testing.T) { + arg := config.ActionArgument{ + Type: "confirmation", + Title: "Are you sure?!", + } + action := config.Action{Title: "Test"} + + assert.Nil(t, typecheckActionArgument(&arg, "", &action)) + assert.Nil(t, typecheckActionArgument(&arg, "ignored", &action)) } func TestTypecheckActionArgumentHtmlWithoutName(t *testing.T) { diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 08b9328..bbe421c 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -1097,6 +1097,7 @@ func appendErrorToStderr(req *ExecutionRequest, err error) { type OutputStreamer struct { Req *ExecutionRequest + mu sync.Mutex output bytes.Buffer } @@ -1105,10 +1106,31 @@ func (ost *OutputStreamer) Write(o []byte) (n int, err error) { listener.OnOutputChunk(o, ost.Req.TrackingID) } - return ost.output.Write(o) + ost.mu.Lock() + n, err = ost.output.Write(o) + outputSoFar := "" + if err == nil { + outputSoFar = ost.output.String() + } + ost.mu.Unlock() + + if err != nil { + return n, err + } + + // Keep the log entry's Output in sync while the command is still running so + // ExecutionStatus / mid-run result views can show output produced so far. + ost.Req.mutateLogEntry(func(entry *InternalLogEntry) { + entry.Output = outputSoFar + }) + + return n, nil } func (ost *OutputStreamer) String() string { + ost.mu.Lock() + defer ost.mu.Unlock() + return ost.output.String() } @@ -1155,9 +1177,10 @@ func stepExec(req *ExecutionRequest) bool { }) ctx.setProcess(cmd.Process) waiterr := cmd.Wait() + finalOutput := streamer.String() req.mutateLogEntry(func(entry *InternalLogEntry) { entry.ExitCode = int32(commandExitCode(cmd)) - entry.Output = streamer.String() + entry.Output = finalOutput }) appendErrorToStderr(req, runerr) diff --git a/service/internal/executor/executor_test.go b/service/internal/executor/executor_test.go index 1869a83..bb40aa7 100644 --- a/service/internal/executor/executor_test.go +++ b/service/internal/executor/executor_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/OliveTin/OliveTin/internal/auth" authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic" @@ -947,3 +948,49 @@ func TestStepSaveLogReturnsFalseWhenDependenciesMissing(t *testing.T) { missingCfg.Cfg = nil assert.False(t, stepSaveLog(&missingCfg)) } + +func TestLogEntryOutputAvailableWhileRunning(t *testing.T) { + cfg := config.DefaultConfig() + e := DefaultExecutor(cfg) + action := &config.Action{ + Title: "Slow output", + Shell: "echo hello-mid-run; sleep 2", + } + cfg.Actions = append(cfg.Actions, action) + cfg.Sanitize() + e.RebuildActionMap() + + binding := e.FindBindingWithNoEntity(action) + require.NotNil(t, binding) + + wg, trackingID := e.ExecRequest(&ExecutionRequest{ + Binding: binding, + Cfg: cfg, + AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"), + }) + + var sawOutputWhileRunning bool + require.Eventually(t, func() bool { + snapshot, ok := e.SnapshotLog(trackingID) + if !ok { + return false + } + if snapshot.ExecutionFinished { + return false + } + if strings.Contains(snapshot.Output, "hello-mid-run") { + sawOutputWhileRunning = true + return true + } + return false + }, 2*time.Second, 10*time.Millisecond) + + wg.Wait() + + require.True(t, sawOutputWhileRunning, "expected Output to contain printed text before ExecutionFinished") + + snapshot, ok := e.SnapshotLog(trackingID) + require.True(t, ok) + assert.True(t, snapshot.ExecutionFinished) + assert.Contains(t, snapshot.Output, "hello-mid-run") +} diff --git a/service/internal/executor/log_arguments.go b/service/internal/executor/log_arguments.go index c207d0b..1e21fb2 100644 --- a/service/internal/executor/log_arguments.go +++ b/service/internal/executor/log_arguments.go @@ -128,7 +128,8 @@ func RestartArgumentsIncomplete(action *config.Action, entity *entities.Entity, } func restartArgumentRequired(arg *config.ActionArgument, entity *entities.Entity) bool { - if argumentSkipsValidation(arg) { + // confirmation is a UI gate; html skips validation. Neither needs a stored value to restart. + if argumentSkipsValidation(arg) || arg.Type == "confirmation" { return false }