Просмотр исходного кода

Merge branch 'next' into dependabot/npm_and_yarn/frontend/minimatch-3.1.5

James Read 4 месяцев назад
Родитель
Сommit
e8e62d22f1

+ 1 - 0
README.md

@@ -10,6 +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/)
 
 [OliveTin 2k to 3k upgrade guide](https://docs.olivetin.app/upgrade/2k3k.html)
 </div>

+ 13 - 1
frontend/js/OutputTerminal.js

@@ -1,5 +1,6 @@
 import { Terminal } from '@xterm/xterm'
 import { FitAddon } from '@xterm/addon-fit'
+import { WebLinksAddon } from '@xterm/addon-web-links'
 import { Mutex } from './Mutex.js'
 
 /**
@@ -18,13 +19,24 @@ export class OutputTerminal {
   constructor (executionTrackingId) {
     this.executionTrackingId = executionTrackingId
     this.writeMutex = new Mutex()
+    const linkHandler = {
+      activate (event, text, _range) {
+        event.preventDefault()
+        window.open(text, '_blank')
+      }
+    }
+
     this.terminal = new Terminal({
-      convertEol: true
+      convertEol: true,
+      linkHandler
     })
 
     const fitAddon = new FitAddon()
     this.terminal.loadAddon(fitAddon)
     this.terminal.fit = fitAddon
+
+    this.terminal.loadAddon(new WebLinksAddon((event, uri) => linkHandler.activate(event, uri)))
+    this.linkHandlerConfigured = true
   }
 
   async write (out, then) {

+ 7 - 0
frontend/package-lock.json

@@ -15,6 +15,7 @@
 				"@hugeicons/vue": "^1.0.4",
 				"@vitejs/plugin-vue": "^6.0.4",
 				"@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.14.0",
@@ -1570,6 +1571,12 @@
 			"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
 			"license": "MIT"
 		},
+		"node_modules/@xterm/addon-web-links": {
+			"version": "0.12.0",
+			"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz",
+			"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
+			"license": "MIT"
+		},
 		"node_modules/@xterm/xterm": {
 			"version": "6.0.0",
 			"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",

+ 2 - 1
frontend/package.json

@@ -28,13 +28,14 @@
 		"@hugeicons/vue": "^1.0.4",
 		"@vitejs/plugin-vue": "^6.0.4",
 		"@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.14.0",
 		"standard": "^17.1.2",
 		"unplugin-vue-components": "^31.0.0",
 		"vite": "^7.3.1",
-    "vue": "^3.5.29",
+		"vue": "^3.5.29",
 		"vue-i18n": "^11.2.8",
 		"vue-router": "^5.0.3"
 	}

+ 5 - 1
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts

@@ -210,6 +210,11 @@ export declare type EffectivePolicy = Message<"olivetin.api.v1.EffectivePolicy">
    * @generated from field: bool show_log_list = 2;
    */
   showLogList: boolean;
+
+  /**
+   * @generated from field: bool show_version_number = 3;
+   */
+  showVersionNumber: boolean;
 };
 
 /**
@@ -1853,4 +1858,3 @@ export declare const OliveTinApiService: GenService<{
     output: typeof EntitySchema;
   },
 }>;
-

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js


+ 11 - 9
frontend/resources/vue/App.vue

@@ -31,7 +31,7 @@
             <footer title="footer" v-if="showFooter">
                 <p>
                     <img title="application icon" :src="logoUrl" alt="OliveTin logo" style="height: 1em;" class="logo" />
-                    OliveTin {{ currentVersion }}
+                    OliveTin <span v-if="showVersionNumber">{{ currentVersion }}</span>
                 </p>
                 <p>
                     <span>
@@ -52,7 +52,7 @@
 
                     <span>{{ t('connected') }}</span>
                 </p>
-                <p>
+                <p v-if="showVersionNumber">
                     <a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
                 </p>
             </footer>
@@ -68,7 +68,7 @@
                 </option>
             </select>
             <p class="browser-languages">
-                {{ t('language-dialog.browser-languages') }}: 
+                {{ t('language-dialog.browser-languages') }}:
                 <span v-if="browserLanguages.length > 0">{{ browserLanguages.join(', ') }}</span>
                 <span v-else>{{ t('language-dialog.not-available') }}</span>
             </p>
@@ -126,6 +126,7 @@ const showFooter = ref(true)
 const showNavigation = ref(true)
 const showLogs = ref(true)
 const showDiagnostics = ref(true)
+const showVersionNumber = ref(true)
 const showLoginLink = ref(true)
 const sectionNavigationStyle = ref('sidebar')
 
@@ -184,7 +185,7 @@ function normalizeBrowserLanguage() {
     if (navigator.languages && navigator.languages.length > 0) {
         for (const candidate of navigator.languages) {
             const lowerCandidate = candidate.toLowerCase()
-            
+
             // Try exact match (case-insensitive)
             const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
             if (exact) {
@@ -223,6 +224,7 @@ function updateHeaderFromInit() {
     showNavigation.value = window.initResponse.showNavigation
     showLogs.value = window.initResponse.showLogList
     showDiagnostics.value = window.initResponse.showDiagnostics
+    showVersionNumber.value = window.initResponse.effectivePolicy?.showVersionNumber ?? true
     sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
     availableThemes.value = window.initResponse.availableThemes || []
 
@@ -277,7 +279,7 @@ function renderNavigation() {
 
 function openLanguageDialog() {
     selectedLanguage.value = languagePreference.value
-    
+
     if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
         browserLanguages.value = navigator.languages
     } else {
@@ -327,7 +329,7 @@ function handleLanguageDialogClick(event) {
 
 function openThemeDialog() {
     selectedTheme.value = themePreference.value || ''
-    
+
     if (themeDialog.value) {
         themeDialog.value.showModal()
     }
@@ -354,7 +356,7 @@ function changeTheme() {
 
 function applyTheme() {
     let themeStyle = document.getElementById('theme-style')
-    
+
     if (!themeStyle) {
         themeStyle = document.createElement('style')
         themeStyle.id = 'theme-style'
@@ -404,10 +406,10 @@ window.updateHeaderFromInit = updateHeaderFromInit
 onMounted(() => {
     serverConnection.value = true;
     updateHeaderFromInit()
-    
+
     // Initialize selected language from stored preference
     selectedLanguage.value = languagePreference.value
-    
+
     // Initialize selected theme from stored preference
     selectedTheme.value = themePreference.value || ''
 

+ 5 - 2
frontend/resources/vue/views/DiagnosticsView.vue

@@ -162,7 +162,10 @@ async function generateBrowserInfo() {
       userAgentData: userAgentData
     }
 
-    const olivetinVersion = window.initResponse?.currentVersion || t('diagnostics.unknown')
+    const showVersionNumber = window.initResponse?.effectivePolicy?.showVersionNumber ?? true
+    const olivetinVersion = showVersionNumber
+      ? (window.initResponse?.currentVersion || t('diagnostics.unknown'))
+      : '[hidden]'
     const currentLanguage = locale.value || t('diagnostics.unknown')
 
     let output = '';
@@ -300,4 +303,4 @@ onMounted(() => {
   flex-direction: column;
   gap: 1em;
 }
-</style>
+</style>

+ 13 - 0
integration-tests/tests/xtermLinkHandling/config.yaml

@@ -0,0 +1,13 @@
+#
+# Integration Test Config: xterm link handling
+#
+
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
+
+logLevel: "DEBUG"
+checkForUpdates: false
+
+actions:
+  - title: Echo URL
+    shell: echo "See https://example.com for more info"
+    popupOnStart: execution-dialog-stdout-only

+ 69 - 0
integration-tests/tests/xtermLinkHandling/xtermLinkHandling.mjs

@@ -0,0 +1,69 @@
+import { describe, it, before, after } from 'mocha'
+import { expect } from 'chai'
+import { By, Condition } from 'selenium-webdriver'
+import {
+  getRootAndWait,
+  takeScreenshotOnFailure,
+  getTerminalBuffer,
+} from '../../lib/elements.js'
+
+describe('config: xtermLinkHandling', function () {
+  before(async function () {
+    await runner.start('xtermLinkHandling')
+  })
+
+  after(async () => {
+    await runner.stop()
+  })
+
+  afterEach(function () {
+    takeScreenshotOnFailure(this.currentTest, webdriver)
+  })
+
+  it('xterm output shows URL and link handling is configured', async function () {
+    await getRootAndWait()
+
+    await webdriver.wait(new Condition('wait for Echo URL button', async () => {
+      const btns = await webdriver.findElements(By.css('[title="Echo URL"]'))
+      return btns.length === 1
+    }), 10000)
+
+    const echoUrlButton = await webdriver.findElement(By.css('[title="Echo URL"]'))
+    await echoUrlButton.click()
+
+    await webdriver.wait(new Condition('wait for execution view', async () => {
+      const url = await webdriver.getCurrentUrl()
+      return url.includes('/logs/') && !url.endsWith('/logs')
+    }), 10000)
+
+    await webdriver.wait(new Condition('wait for execution status', async () => {
+      const statusElements = await webdriver.findElements(By.id('execution-dialog-status'))
+      return statusElements.length > 0
+    }), 5000)
+
+    await webdriver.wait(new Condition('wait for execution to finish', async () => {
+      try {
+        const statusElement = await webdriver.findElement(By.id('execution-dialog-status'))
+        const statusText = await statusElement.getText()
+        return !statusText.includes('Executing')
+      } catch (e) {
+        return false
+      }
+    }), 5000)
+
+    await webdriver.sleep(500)
+
+    const bufferText = await getTerminalBuffer()
+    expect(bufferText).to.not.be.null
+    expect(bufferText).to.include('https://example.com')
+
+    const linkHandlerSet = await webdriver.executeScript(`
+      try {
+        return !!(window.terminal && window.terminal.linkHandlerConfigured === true)
+      } catch (e) {
+        return false
+      }
+    `)
+    expect(linkHandlerSet).to.equal(true)
+  })
+})

+ 1 - 0
proto/olivetin/api/v1/olivetin.proto

@@ -51,6 +51,7 @@ message GetDashboardResponse {
 message EffectivePolicy {
 	bool show_diagnostics = 1;
 	bool show_log_list = 2;
+	bool show_version_number = 3;
 }
 
 message GetDashboardRequest {

+ 16 - 7
service/gen/olivetin/api/v1/olivetin.pb.go

@@ -410,11 +410,12 @@ func (x *GetDashboardResponse) GetDashboard() *Dashboard {
 }
 
 type EffectivePolicy struct {
-	state           protoimpl.MessageState `protogen:"open.v1"`
-	ShowDiagnostics bool                   `protobuf:"varint,1,opt,name=show_diagnostics,json=showDiagnostics,proto3" json:"show_diagnostics,omitempty"`
-	ShowLogList     bool                   `protobuf:"varint,2,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"`
-	unknownFields   protoimpl.UnknownFields
-	sizeCache       protoimpl.SizeCache
+	state             protoimpl.MessageState `protogen:"open.v1"`
+	ShowDiagnostics   bool                   `protobuf:"varint,1,opt,name=show_diagnostics,json=showDiagnostics,proto3" json:"show_diagnostics,omitempty"`
+	ShowLogList       bool                   `protobuf:"varint,2,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"`
+	ShowVersionNumber bool                   `protobuf:"varint,3,opt,name=show_version_number,json=showVersionNumber,proto3" json:"show_version_number,omitempty"`
+	unknownFields     protoimpl.UnknownFields
+	sizeCache         protoimpl.SizeCache
 }
 
 func (x *EffectivePolicy) Reset() {
@@ -461,6 +462,13 @@ func (x *EffectivePolicy) GetShowLogList() bool {
 	return false
 }
 
+func (x *EffectivePolicy) GetShowVersionNumber() bool {
+	if x != nil {
+		return x.ShowVersionNumber
+	}
+	return false
+}
+
 type GetDashboardRequest struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
 	Title         string                 `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
@@ -3934,10 +3942,11 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
 	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"f\n" +
 	"\x14GetDashboardResponse\x12\x14\n" +
 	"\x05title\x18\x01 \x01(\tR\x05title\x128\n" +
-	"\tdashboard\x18\x04 \x01(\v2\x1a.olivetin.api.v1.DashboardR\tdashboard\"`\n" +
+	"\tdashboard\x18\x04 \x01(\v2\x1a.olivetin.api.v1.DashboardR\tdashboard\"\x90\x01\n" +
 	"\x0fEffectivePolicy\x12)\n" +
 	"\x10show_diagnostics\x18\x01 \x01(\bR\x0fshowDiagnostics\x12\"\n" +
-	"\rshow_log_list\x18\x02 \x01(\bR\vshowLogList\"k\n" +
+	"\rshow_log_list\x18\x02 \x01(\bR\vshowLogList\x12.\n" +
+	"\x13show_version_number\x18\x03 \x01(\bR\x11showVersionNumber\"k\n" +
 	"\x13GetDashboardRequest\x12\x14\n" +
 	"\x05title\x18\x01 \x01(\tR\x05title\x12\x1f\n" +
 	"\ventity_type\x18\x02 \x01(\tR\n" +

+ 8 - 8
service/go.mod

@@ -1,6 +1,6 @@
 module github.com/OliveTin/OliveTin
 
-go 1.25.0
+go 1.25.6
 
 exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
 
@@ -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.65.0
+	github.com/bufbuild/buf v1.66.0
 	github.com/fsnotify/fsnotify v1.9.0
 	github.com/fzipp/gocyclo v0.6.0
 	github.com/go-critic/go-critic v0.14.3
@@ -27,7 +27,7 @@ require (
 	github.com/sirupsen/logrus v1.9.4
 	github.com/stretchr/testify v1.11.1
 	go.akshayshah.org/connectproto v0.6.0
-	golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
+	golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
 	golang.org/x/oauth2 v0.35.0
 	golang.org/x/sys v0.41.0
 	google.golang.org/protobuf v1.36.11
@@ -45,7 +45,7 @@ require (
 	buf.build/go/bufplugin v0.9.0 // indirect
 	buf.build/go/bufprivateusage v0.1.0 // indirect
 	buf.build/go/interrupt v1.1.0 // indirect
-	buf.build/go/protovalidate v1.1.2 // indirect
+	buf.build/go/protovalidate v1.1.3 // indirect
 	buf.build/go/protoyaml v0.6.0 // indirect
 	buf.build/go/spdx v0.2.0 // indirect
 	buf.build/go/standard v0.1.0 // indirect
@@ -57,7 +57,7 @@ require (
 	github.com/PaesslerAG/gval v1.2.4 // indirect
 	github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e // indirect
+	github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113 // indirect
 	github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cli/browser v1.3.0 // indirect
@@ -90,7 +90,7 @@ require (
 	github.com/gofrs/flock v0.13.0 // indirect
 	github.com/google/cel-go v0.27.0 // indirect
 	github.com/google/go-cmp v0.7.0 // indirect
-	github.com/google/go-containerregistry v0.20.7 // indirect
+	github.com/google/go-containerregistry v0.21.0 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jdx/go-netrc v1.0.0 // indirect
@@ -155,8 +155,8 @@ require (
 	golang.org/x/text v0.34.0 // indirect
 	golang.org/x/time v0.14.0 // indirect
 	golang.org/x/tools v0.42.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
 	google.golang.org/grpc v1.75.1 // indirect
 	mvdan.cc/xurls/v2 v2.6.0 // indirect
 	pluginrpc.com/pluginrpc v0.5.0 // indirect

+ 15 - 0
service/go.sum

@@ -32,6 +32,8 @@ buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY
 buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss=
 buf.build/go/protovalidate v1.1.2 h1:83vYHoY8f34hB8MeitGaYE3CGVPFxwdEUuskh5qQpA0=
 buf.build/go/protovalidate v1.1.2/go.mod h1:Ez3z+w4c+wG+EpW8ovgZaZPnPl2XVF6kaxgcv1NG/QE=
+buf.build/go/protovalidate v1.1.3 h1:m2GVEgQWd7rk+vIoAZ+f0ygGjvQTuqPQapBBdcpWVPE=
+buf.build/go/protovalidate v1.1.3/go.mod h1:9XIuohWz+kj+9JVn3WQneHA5LZP50mjvneZMnbLkiIE=
 buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
 buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
 buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
@@ -84,6 +86,8 @@ github.com/bufbuild/buf v1.64.0 h1:puHWFcVKmZFSu4KuaN0kZiQ32n7VVc3un1FeLU77XUs=
 github.com/bufbuild/buf v1.64.0/go.mod h1:U4ISwkjZXRLMaCkPG9zp1xY3xHEIwhCFwyNAaA56SGw=
 github.com/bufbuild/buf v1.65.0 h1:f2BzeCY9rRh9P5KD340ZoPAaFLTkssoUTHx7lpqozgg=
 github.com/bufbuild/buf v1.65.0/go.mod h1:7SAs2YqGpPXHqBBXBeYQbCzY0OQq4Jbg6XCqirEiYvQ=
+github.com/bufbuild/buf v1.66.0 h1:6kksYJpu6r45bvPJSTwNSwRqiAjrwB9YyU7skjNzFVo=
+github.com/bufbuild/buf v1.66.0/go.mod h1:tWVlwtIPZ7kzlCB9D0hbbfrroT0GNCybPdPQXq1i1Ac=
 github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e h1:LQA+1MyiPkolGHJGC2GMDC5Xu+0RDVH6jGMKech7Exs=
 github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
 github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 h1:cQYwUyAzyMmYr7AyJU1C6pVCpUrJJBkmx7UunZosxxs=
@@ -92,6 +96,8 @@ github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608 h1:3aRREB
 github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
 github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e h1:emH16Bf1w4C0cJ3ge4QtBAl4sIYJe23EfpWH0SpA9co=
 github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
+github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113 h1:nxt1QhP9rMQNFhHTdcNFwJ9wKCSdBjd28gz+qGDv4kM=
+github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
 github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
 github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@@ -200,6 +206,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.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
 github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
+github.com/google/go-containerregistry v0.21.0 h1:ocqxUOczFwAZQBMNE7kuzfqvDe0VWoZxQMOesXreCDI=
+github.com/google/go-containerregistry v0.21.0/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0=
 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/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
@@ -375,6 +383,7 @@ go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIl
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
 go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
@@ -421,6 +430,8 @@ golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7
 golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
 golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
 golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
+golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
+golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
 golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20251219203646-944ab1f22d93 h1:PbC785RGO6yPO051ItgbG/adwoKRWC0VS7kXXeD/iqk=
@@ -519,6 +530,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d h1:
 google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
 google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
 google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
+google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
+google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
@@ -527,6 +540,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
 google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
 google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

+ 13 - 4
service/internal/api/api.go

@@ -710,7 +710,9 @@ func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAm
 }
 
 func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *connect.Request[apiv1.SosReportRequest]) (*connect.Response[apiv1.SosReportResponse], error) {
-	sos := installationinfo.GetSosReport()
+	user := auth.UserFromApiCall(ctx, req, api.cfg)
+	redactVersion := !user.EffectivePolicy.ShowVersionNumber
+	sos := installationinfo.GetSosReport(redactVersion)
 
 	if !api.cfg.InsecureAllowDumpSos {
 		log.Info(sos)
@@ -914,12 +916,19 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq
 
 	loginRequired := user.IsGuest() && api.cfg.AuthRequireGuestsToLogin
 
+	showVersion := user.EffectivePolicy.ShowVersionNumber
+	currentVersion := ""
+	availableVersion := ""
+	if showVersion {
+		currentVersion = installationinfo.Build.Version
+		availableVersion = installationinfo.Runtime.AvailableVersion
+	}
 	res := &apiv1.InitResponse{
 		ShowFooter:                api.cfg.ShowFooter,
 		ShowNavigation:            api.cfg.ShowNavigation,
-		ShowNewVersions:           api.cfg.ShowNewVersions,
-		AvailableVersion:          installationinfo.Runtime.AvailableVersion,
-		CurrentVersion:            installationinfo.Build.Version,
+		ShowNewVersions:           showVersion && api.cfg.ShowNewVersions,
+		AvailableVersion:          availableVersion,
+		CurrentVersion:            currentVersion,
 		PageTitle:                 api.cfg.PageTitle,
 		SectionNavigationStyle:    api.cfg.SectionNavigationStyle,
 		DefaultIconForBack:        api.cfg.DefaultIconForBack,

+ 3 - 2
service/internal/api/apiActions.go

@@ -55,8 +55,9 @@ func matchesEntity(binding *executor.ActionBinding, entity *entities.Entity) boo
 
 func buildEffectivePolicy(policy *config.ConfigurationPolicy) *apiv1.EffectivePolicy {
 	ret := &apiv1.EffectivePolicy{
-		ShowDiagnostics: policy.ShowDiagnostics,
-		ShowLogList:     policy.ShowLogList,
+		ShowDiagnostics:   policy.ShowDiagnostics,
+		ShowLogList:       policy.ShowLogList,
+		ShowVersionNumber: policy.ShowVersionNumber,
 	}
 
 	return ret

+ 7 - 2
service/internal/auth/authpublic/authenticateduser.go

@@ -76,8 +76,9 @@ func (u *AuthenticatedUser) BuildUserAcls(cfg *config.Config) {
 
 func getEffectivePolicy(cfg *config.Config, u *AuthenticatedUser) *config.ConfigurationPolicy {
 	ret := &config.ConfigurationPolicy{
-		ShowDiagnostics: cfg.DefaultPolicy.ShowDiagnostics,
-		ShowLogList:     cfg.DefaultPolicy.ShowLogList,
+		ShowDiagnostics:   cfg.DefaultPolicy.ShowDiagnostics,
+		ShowLogList:       cfg.DefaultPolicy.ShowLogList,
+		ShowVersionNumber: cfg.DefaultPolicy.ShowVersionNumber,
 	}
 
 	for _, acl := range cfg.AccessControlLists {
@@ -98,5 +99,9 @@ func buildConfigurationPolicy(ret *config.ConfigurationPolicy, policy config.Con
 		ret.ShowLogList = policy.ShowLogList
 	}
 
+	if policy.ShowVersionNumber {
+		ret.ShowVersionNumber = policy.ShowVersionNumber
+	}
+
 	return ret
 }

+ 27 - 17
service/internal/auth/otoauth2/restapi_auth_oauth2.go

@@ -11,6 +11,7 @@ import (
 	"io"
 	"net/http"
 	"os"
+	"sync"
 	"time"
 
 	authTypes "github.com/OliveTin/OliveTin/internal/auth/authpublic"
@@ -21,6 +22,7 @@ import (
 
 type OAuth2Handler struct {
 	cfg                 *config.Config
+	mu                  sync.RWMutex
 	registeredStates    map[string]*oauth2State
 	registeredProviders map[string]*oauth2.Config
 }
@@ -144,11 +146,13 @@ func (h *OAuth2Handler) HandleOAuthLogin(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
+	h.mu.Lock()
 	h.registeredStates[state] = &oauth2State{
 		providerConfig: provider,
 		providerName:   providerName,
 		Username:       "",
 	}
+	h.mu.Unlock()
 
 	h.setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state)
 
@@ -177,7 +181,9 @@ func (h *OAuth2Handler) checkOAuthCallbackCookie(w http.ResponseWriter, r *http.
 		return nil, state, false
 	}
 
+	h.mu.RLock()
 	registeredState, ok := h.registeredStates[state]
+	h.mu.RUnlock()
 	if !ok {
 		log.Errorf("State not found in server: %v", state)
 		http.Error(w, "State not found in server", http.StatusBadRequest)
@@ -287,8 +293,10 @@ func (h *OAuth2Handler) HandleOAuthCallback(w http.ResponseWriter, r *http.Reque
 	userInfoClient := h.createUserInfoClient(ctx, registeredState.providerConfig, tok, clientSettings)
 	userinfo := getUserInfo(h.cfg, userInfoClient, providerConfig)
 
+	h.mu.Lock()
 	h.registeredStates[state].Username = userinfo.Username
 	h.registeredStates[state].Usergroup = h.computeUsergroup(userinfo, providerConfig)
+	h.mu.Unlock()
 
 	http.Redirect(w, r, "/", http.StatusFound)
 }
@@ -366,34 +374,36 @@ func getDataField(data map[string]any, field string) string {
 	return stringVal
 }
 
-func (h *OAuth2Handler) CheckUserFromOAuth2Cookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
-	cookie, err := context.Request.Cookie("olivetin-sid-oauth")
-
-	user := &authTypes.AuthenticatedUser{}
-
-	if err != nil {
-		return nil
+func (h *OAuth2Handler) lookupOAuth2UserByState(state string) (*authTypes.AuthenticatedUser, bool) {
+	h.mu.RLock()
+	serverState, found := h.registeredStates[state]
+	if !found {
+		h.mu.RUnlock()
+		return nil, false
+	}
+	user := &authTypes.AuthenticatedUser{
+		Username:      serverState.Username,
+		UsergroupLine: serverState.Usergroup,
+		Provider:      "oauth2",
+		SID:           state,
 	}
+	h.mu.RUnlock()
+	return user, true
+}
 
-	if cookie.Value == "" {
+func (h *OAuth2Handler) CheckUserFromOAuth2Cookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
+	cookie, err := context.Request.Cookie("olivetin-sid-oauth")
+	if err != nil || cookie.Value == "" {
 		return nil
 	}
 
-	serverState, found := h.registeredStates[cookie.Value]
-
+	user, found := h.lookupOAuth2UserByState(cookie.Value)
 	if !found {
 		log.WithFields(log.Fields{
 			"sid":      cookie.Value,
 			"provider": "oauth2",
 		}).Warnf("Stale session")
-
 		return nil
 	}
-
-	user.Username = serverState.Username
-	user.UsergroupLine = serverState.Usergroup
-	user.Provider = "oauth2"
-	user.SID = cookie.Value
-
 	return user
 }

+ 4 - 2
service/internal/config/config.go

@@ -98,8 +98,9 @@ type AccessControlList struct {
 
 // ConfigurationPolicy defines global settings which are overridden with an ACL.
 type ConfigurationPolicy struct {
-	ShowDiagnostics bool `koanf:"showDiagnostics"`
-	ShowLogList     bool `koanf:"showLogList"`
+	ShowDiagnostics   bool `koanf:"showDiagnostics"`
+	ShowLogList       bool `koanf:"showLogList"`
+	ShowVersionNumber bool `koanf:"showVersionNumber"`
 }
 
 type PrometheusConfig struct {
@@ -297,6 +298,7 @@ func DefaultConfigWithBasePort(basePort int) *Config {
 
 	config.DefaultPolicy.ShowDiagnostics = true
 	config.DefaultPolicy.ShowLogList = true
+	config.DefaultPolicy.ShowVersionNumber = true
 
 	return &config
 }

+ 1 - 0
service/internal/config/sanitize.go

@@ -164,6 +164,7 @@ func (cfg *Config) sanitizeAuthRequireGuestsToLogin() {
 		cfg.DefaultPermissions.View = false
 		cfg.DefaultPermissions.Exec = false
 		cfg.DefaultPermissions.Logs = false
+		cfg.DefaultPermissions.Kill = false
 	}
 }
 

+ 11 - 3
service/internal/installationinfo/sosreport.go

@@ -40,15 +40,23 @@ func configToSosreport(cfg *config.Config) *sosReportConfig {
 	}
 }
 
-func GetSosReport() string {
+func GetSosReport(redactVersion bool) string {
 	ret := ""
 
 	ret += "### SOSREPORT START (copy all text to SOSREPORT END)\n"
 
-	out, _ := yaml.Marshal(Build)
+	buildForReport := *Build
+	if redactVersion {
+		buildForReport.Version = "[redacted]"
+	}
+	out, _ := yaml.Marshal(&buildForReport)
 	ret += fmt.Sprintf("# Build: \n%+v\n", string(out))
 
-	out, _ = yaml.Marshal(Runtime)
+	runtimeForReport := *Runtime
+	if redactVersion {
+		runtimeForReport.AvailableVersion = "[redacted]"
+	}
+	out, _ = yaml.Marshal(&runtimeForReport)
 	ret += fmt.Sprintf("# Runtime:\n%+v\n", string(out))
 
 	out, _ = yaml.Marshal(configToSosreport(Config))

Некоторые файлы не были показаны из-за большого количества измененных файлов