James Read пре 5 месеци
родитељ
комит
3dd82a6f4d
50 измењених фајлова са 6960 додато и 290 уклоњено
  1. 1 1
      config.yaml
  2. 19 1
      frontend/js/websocket.js
  3. 1 1
      frontend/main.js
  4. 36 36
      frontend/package-lock.json
  5. 5 4
      frontend/package.json
  6. 40 12
      frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts
  7. 0 0
      frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js
  8. 121 7
      frontend/resources/vue/ActionButton.vue
  9. 14 3
      frontend/resources/vue/App.vue
  10. 1 1
      frontend/resources/vue/Dashboard.vue
  11. 1 1
      frontend/resources/vue/components/DashboardComponent.vue
  12. 1 1
      frontend/resources/vue/components/DashboardComponentDirectory.vue
  13. 1 1
      frontend/resources/vue/components/DashboardComponentDisplay.vue
  14. 45 31
      frontend/resources/vue/components/DashboardComponentMostRecentExecution.vue
  15. 12 0
      frontend/resources/vue/router.js
  16. 5 0
      frontend/resources/vue/stores/rateLimits.js
  17. 62 2
      frontend/resources/vue/views/ArgumentForm.vue
  18. 168 0
      frontend/resources/vue/views/LogsCalendarView.vue
  19. 119 5
      frontend/resources/vue/views/LogsListView.vue
  20. 39 0
      integration-tests/tests/enabledExpression/config.yaml
  21. 150 0
      integration-tests/tests/enabledExpression/enabledExpression.mjs
  22. 5 0
      integration-tests/tests/enabledExpression/entities/lights.yaml
  23. 2 0
      integration-tests/tests/stdoutMostRecentExecution/config.yaml
  24. 3 4
      integration-tests/tests/stdoutMostRecentExecution/stdoutMostRecentExecution.mjs
  25. 19 0
      integration-tests/tests/suggestionsBrowserKey/config.yaml
  26. 325 0
      integration-tests/tests/suggestionsBrowserKey/suggestionsBrowserKey.mjs
  27. 20 0
      lang/combined_output.json
  28. 4 0
      lang/de-DE.yaml
  29. 4 0
      lang/en.yaml
  30. 4 0
      lang/es-ES.yaml
  31. 4 0
      lang/it-IT.yaml
  32. 4 0
      lang/zh-Hans-CN.yaml
  33. 7 3
      proto/olivetin/api/v1/olivetin.proto
  34. 116 78
      service/gen/olivetin/api/v1/olivetin.pb.go
  35. 4378 0
      service/gen/olivetin/api/v1/olivetin.pb.go.orig
  36. 8 5
      service/go.mod
  37. 24 0
      service/go.sum
  38. 61 29
      service/internal/api/api.go
  39. 79 17
      service/internal/api/apiActions.go
  40. 165 3
      service/internal/api/api_test.go
  41. 1 1
      service/internal/api/dashboards.go
  42. 24 9
      service/internal/config/config.go
  43. 159 19
      service/internal/executor/executor.go
  44. 15 15
      service/internal/executor/executor_actions.go
  45. 5 0
      service/internal/httpservers/frontend.go
  46. 169 0
      service/internal/webhooks/auth.go
  47. 112 0
      service/internal/webhooks/github.go
  48. 162 0
      service/internal/webhooks/handler.go
  49. 55 0
      service/internal/webhooks/jsonpath.go
  50. 185 0
      service/internal/webhooks/matcher.go

+ 1 - 1
config.yaml

@@ -50,7 +50,7 @@ actions:
     popupOnStart: execution-button
     maxRate:
       - limit: 3
-        duration: 5m
+        duration: 1m
 
   # You are not limited to operating system commands, and of course you can run
   # your own scripts. Here `maxConcurrent` stops the script running multiple

+ 19 - 1
frontend/js/websocket.js

@@ -1,7 +1,10 @@
 import { buttonResults } from '../resources/vue/stores/buttonResults.js'
+import { rateLimits } from '../resources/vue/stores/rateLimits.js'
 
 export function initWebsocket () {
   window.addEventListener('EventOutputChunk', onOutputChunk)
+  window.addEventListener('EventExecutionStarted', onExecutionChanged)
+  window.addEventListener('EventExecutionFinished', onExecutionChanged)
 
   reconnectWebsocket()
 }
@@ -40,7 +43,6 @@ function handleEvent (msg) {
       break
     case 'EventExecutionFinished':
     case 'EventExecutionStarted':
-      buttonResults[msg.event.value.logEntry.executionTrackingId] = msg.event.value.logEntry
       window.dispatchEvent(j)
       break
     default:
@@ -59,3 +61,19 @@ function onOutputChunk (evt) {
     }
   }
 }
+
+function onExecutionChanged (evt) {
+  buttonResults[evt.payload.logEntry.executionTrackingId] = evt.payload.logEntry
+
+  const logEntry = evt.payload.logEntry
+
+  // Update rate limit store from logEntry if rate limit expiry datetime is provided
+  if (logEntry && logEntry.datetimeRateLimitExpires && logEntry.bindingId) {
+    // Parse datetime string "2006-01-02 15:04:05" and convert to Unix timestamp
+    const date = new Date(logEntry.datetimeRateLimitExpires.replace(' ', 'T'))
+    rateLimits[logEntry.bindingId] = date.getTime() / 1000
+  } else if (logEntry && logEntry.bindingId) {
+    // Clear rate limit if not set
+    rateLimits[logEntry.bindingId] = 0
+  }
+}

+ 1 - 1
frontend/main.js

@@ -93,7 +93,7 @@ function setupVue (i18nSettings) {
 
 function setupErrorDisplay (errorMessage) {
   const ErrorApp = {
-    render() {
+    render () {
       return h('section', { class: 'bad', style: 'padding: 2em; text-align: center; margin: 2em auto;' }, [
         h('h2', 'OliveTin Init Failed'),
         h('p', errorMessage),

+ 36 - 36
frontend/package-lock.json

@@ -11,17 +11,17 @@
 			"dependencies": {
 				"@connectrpc/connect": "^2.1.1",
 				"@connectrpc/connect-web": "^2.1.1",
-				"@hugeicons/core-free-icons": "^3.1.0",
+				"@hugeicons/core-free-icons": "^3.1.1",
 				"@hugeicons/vue": "^1.0.4",
 				"@vitejs/plugin-vue": "^6.0.3",
 				"@xterm/addon-fit": "^0.11.0",
 				"@xterm/xterm": "^6.0.0",
 				"iconify-icon": "^3.0.2",
-				"picocrank": "^1.10.0",
+				"picocrank": "^1.13.0",
 				"standard": "^17.1.2",
 				"unplugin-vue-components": "^30.0.0",
-				"vite": "^7.3.0",
-				"vue-i18n": "^11.2.7",
+				"vite": "^7.3.1",
+				"vue-i18n": "^11.2.8",
 				"vue-router": "^4.6.4"
 			},
 			"devDependencies": {
@@ -855,9 +855,9 @@
 			}
 		},
 		"node_modules/@hugeicons/core-free-icons": {
-			"version": "3.1.0",
-			"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-3.1.0.tgz",
-			"integrity": "sha512-DVIdHcPxJ8MyaXaGe3appbaB5z4DWit5RLn8vTy3hsKTrpFEm4QftixfbcHXRqLpOMAoMa2+UXk35qNJ4ZWIsg==",
+			"version": "3.1.1",
+			"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-3.1.1.tgz",
+			"integrity": "sha512-UpS2lUQFi5sKyJSWwM6rO+BnPLvVz1gsyCpPHeZyVuZqi89YH8ksliza4cwaODqKOZyeXmG8juo1ty4QtQofkg==",
 			"license": "MIT"
 		},
 		"node_modules/@hugeicons/vue": {
@@ -911,13 +911,13 @@
 			"license": "MIT"
 		},
 		"node_modules/@intlify/core-base": {
-			"version": "11.2.7",
-			"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.7.tgz",
-			"integrity": "sha512-+Ra9I/LAzXDnmv/IrTO03WMCiLya7pHRmGJvNl9fKwx/W4REJ0xaMk2PxCRqnxcBsX443amEMdebQ3R1geiuIw==",
+			"version": "11.2.8",
+			"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.8.tgz",
+			"integrity": "sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==",
 			"license": "MIT",
 			"dependencies": {
-				"@intlify/message-compiler": "11.2.7",
-				"@intlify/shared": "11.2.7"
+				"@intlify/message-compiler": "11.2.8",
+				"@intlify/shared": "11.2.8"
 			},
 			"engines": {
 				"node": ">= 16"
@@ -927,12 +927,12 @@
 			}
 		},
 		"node_modules/@intlify/message-compiler": {
-			"version": "11.2.7",
-			"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.7.tgz",
-			"integrity": "sha512-TFamC+GzJAotAFwUNvbtRVBgvuSn2nCwKNresmPUHv3IIVMmXJt7QQJj/DORI1h8hs46ZF6L0Fs2xBohSOE4iQ==",
+			"version": "11.2.8",
+			"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.8.tgz",
+			"integrity": "sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==",
 			"license": "MIT",
 			"dependencies": {
-				"@intlify/shared": "11.2.7",
+				"@intlify/shared": "11.2.8",
 				"source-map-js": "^1.0.2"
 			},
 			"engines": {
@@ -943,9 +943,9 @@
 			}
 		},
 		"node_modules/@intlify/shared": {
-			"version": "11.2.7",
-			"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.7.tgz",
-			"integrity": "sha512-uvlkvc/0uQ4FDlHQZccpUnmcOwNcaI3i+69ck2YJ+GqM35AoVbuS63b+YfirV4G0SZh64Ij2UMcFRMmB4nr95w==",
+			"version": "11.2.8",
+			"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.8.tgz",
+			"integrity": "sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==",
 			"license": "MIT",
 			"engines": {
 				"node": ">= 16"
@@ -2961,9 +2961,9 @@
 			}
 		},
 		"node_modules/femtocrank": {
-			"version": "2.4.12",
-			"resolved": "https://registry.npmjs.org/femtocrank/-/femtocrank-2.4.12.tgz",
-			"integrity": "sha512-X2a4WVG1ADGjQcULUyH2FJ4njJNZobfP+iPO1MpAEtWRyVEDxps6dmktbOb3igoyCxNObF0T0uKwUC7zV21C0A==",
+			"version": "2.5.0",
+			"resolved": "https://registry.npmjs.org/femtocrank/-/femtocrank-2.5.0.tgz",
+			"integrity": "sha512-plV1HNS/fUzohWJ349kuCBZ3TCfXz7V4F/sY2lVbVWtGXUV+aHxLG6IddAMEf64k2LJ8j0KVrj+nIIKepFaKvg==",
 			"license": "AGPL-3.0"
 		},
 		"node_modules/file-entry-cache": {
@@ -4607,17 +4607,17 @@
 			"license": "ISC"
 		},
 		"node_modules/picocrank": {
-			"version": "1.10.0",
-			"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.10.0.tgz",
-			"integrity": "sha512-rpuFopcko5jKuQpnqpvN0Umg9M2qmDI6Z/tVzbnT9PSplwJmtw9wTjgDHyQXujoBBULyurRqtIeQKDPNFLlHOQ==",
+			"version": "1.13.0",
+			"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.13.0.tgz",
+			"integrity": "sha512-kfUU2KVZnHyZ3/s+vM2tw25G+rK//eMjhF7cuXNQm0Ar/o5YvPiLBx8uxCrNNNvhGqb+jQK1Jbn41KO6rAR8Lw==",
 			"license": "ISC",
 			"dependencies": {
-				"@hugeicons/core-free-icons": "^3.1.0",
+				"@hugeicons/core-free-icons": "^3.1.1",
 				"@hugeicons/vue": "^1.0.4",
 				"@vitejs/plugin-vue": "^6.0.3",
-				"femtocrank": "^2.4.12",
+				"femtocrank": "^2.5.0",
 				"unplugin-vue-components": "^30.0.0",
-				"vite": "^7.3.0",
+				"vite": "^7.3.1",
 				"vue": "^3.5.26",
 				"vue-router": "^4.6.4"
 			}
@@ -6122,9 +6122,9 @@
 			}
 		},
 		"node_modules/vite": {
-			"version": "7.3.0",
-			"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
-			"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
+			"version": "7.3.1",
+			"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+			"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
 			"license": "MIT",
 			"dependencies": {
 				"esbuild": "^0.27.0",
@@ -6246,13 +6246,13 @@
 			}
 		},
 		"node_modules/vue-i18n": {
-			"version": "11.2.7",
-			"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.7.tgz",
-			"integrity": "sha512-LPv8bAY5OA0UvFEXl4vBQOBqJzRrlExy92tWgRuwW7tbykHf7CH71G2Y4TM2OwGcIS4+hyqKHS2EVBqaYwPY9Q==",
+			"version": "11.2.8",
+			"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.8.tgz",
+			"integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==",
 			"license": "MIT",
 			"dependencies": {
-				"@intlify/core-base": "11.2.7",
-				"@intlify/shared": "11.2.7",
+				"@intlify/core-base": "11.2.8",
+				"@intlify/shared": "11.2.8",
 				"@vue/devtools-api": "^6.5.0"
 			},
 			"engines": {

+ 5 - 4
frontend/package.json

@@ -24,17 +24,18 @@
 	"dependencies": {
 		"@connectrpc/connect": "^2.1.1",
 		"@connectrpc/connect-web": "^2.1.1",
-		"@hugeicons/core-free-icons": "^3.1.0",
+		"@hugeicons/core-free-icons": "^3.1.1",
 		"@hugeicons/vue": "^1.0.4",
 		"@vitejs/plugin-vue": "^6.0.3",
 		"@xterm/addon-fit": "^0.11.0",
 		"@xterm/xterm": "^6.0.0",
 		"iconify-icon": "^3.0.2",
-		"picocrank": "^1.10.0",
+		"picocrank": "^1.13.0",
 		"standard": "^17.1.2",
 		"unplugin-vue-components": "^30.0.0",
-		"vite": "^7.3.0",
-		"vue-i18n": "^11.2.7",
+		"vite": "^7.3.1",
+    "vue": "^3.5.26",
+		"vue-i18n": "^11.2.8",
 		"vue-router": "^4.6.4"
 	}
 }

+ 40 - 12
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts

@@ -53,6 +53,13 @@ export declare type Action = Message<"olivetin.api.v1.Action"> & {
    * @generated from field: int32 timeout = 8;
    */
   timeout: number;
+
+  /**
+   * Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
+   *
+   * @generated from field: string datetime_rate_limit_expires = 9;
+   */
+  datetimeRateLimitExpires: string;
 };
 
 /**
@@ -99,6 +106,11 @@ export declare type ActionArgument = Message<"olivetin.api.v1.ActionArgument"> &
    * @generated from field: map<string, string> suggestions = 7;
    */
   suggestions: { [key: string]: string };
+
+  /**
+   * @generated from field: string suggestions_browser_key = 8;
+   */
+  suggestionsBrowserKey: string;
 };
 
 /**
@@ -476,6 +488,13 @@ export declare type GetLogsRequest = Message<"olivetin.api.v1.GetLogsRequest"> &
    * @generated from field: int64 start_offset = 1;
    */
   startOffset: bigint;
+
+  /**
+   * Optional date filter in YYYY-MM-DD format
+   *
+   * @generated from field: string date_filter = 2;
+   */
+  dateFilter: string;
 };
 
 /**
@@ -543,11 +562,6 @@ export declare type LogEntry = Message<"olivetin.api.v1.LogEntry"> & {
    */
   datetimeFinished: string;
 
-  /**
-   * @generated from field: string action_id = 13;
-   */
-  actionId: string;
-
   /**
    * @generated from field: bool execution_started = 14;
    */
@@ -572,6 +586,20 @@ export declare type LogEntry = Message<"olivetin.api.v1.LogEntry"> & {
    * @generated from field: bool can_kill = 18;
    */
   canKill: boolean;
+
+  /**
+   * Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
+   *
+   * @generated from field: string datetime_rate_limit_expires = 19;
+   */
+  datetimeRateLimitExpires: string;
+
+  /**
+   * Binding ID for matching rate limits to action buttons
+   *
+   * @generated from field: string binding_id = 20;
+   */
+  bindingId: string;
 };
 
 /**
@@ -904,9 +932,9 @@ export declare type DumpVarsResponse = Message<"olivetin.api.v1.DumpVarsResponse
 export declare const DumpVarsResponseSchema: GenMessage<DumpVarsResponse>;
 
 /**
- * @generated from message olivetin.api.v1.ActionEntityPair
+ * @generated from message olivetin.api.v1.DebugBinding
  */
-export declare type ActionEntityPair = Message<"olivetin.api.v1.ActionEntityPair"> & {
+export declare type DebugBinding = Message<"olivetin.api.v1.DebugBinding"> & {
   /**
    * @generated from field: string action_title = 1;
    */
@@ -919,10 +947,10 @@ export declare type ActionEntityPair = Message<"olivetin.api.v1.ActionEntityPair
 };
 
 /**
- * Describes the message olivetin.api.v1.ActionEntityPair.
- * Use `create(ActionEntityPairSchema)` to create a new message.
+ * Describes the message olivetin.api.v1.DebugBinding.
+ * Use `create(DebugBindingSchema)` to create a new message.
  */
-export declare const ActionEntityPairSchema: GenMessage<ActionEntityPair>;
+export declare const DebugBindingSchema: GenMessage<DebugBinding>;
 
 /**
  * @generated from message olivetin.api.v1.DumpPublicIdActionMapRequest
@@ -946,9 +974,9 @@ export declare type DumpPublicIdActionMapResponse = Message<"olivetin.api.v1.Dum
   alert: string;
 
   /**
-   * @generated from field: map<string, olivetin.api.v1.ActionEntityPair> contents = 2;
+   * @generated from field: map<string, olivetin.api.v1.DebugBinding> contents = 2;
    */
-  contents: { [key: string]: ActionEntityPair };
+  contents: { [key: string]: DebugBinding };
 };
 
 /**

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js


+ 121 - 7
frontend/resources/vue/ActionButton.vue

@@ -1,7 +1,7 @@
 <template>
-	<div :id="`actionButton-${actionId}`" role="none" class="action-button">
-		<button :id="`actionButtonInner-${actionId}`" :title="title" :disabled="!canExec || isDisabled"
-													  :class="buttonClasses" @click="handleClick">
+	<div :id="`actionButton-${bindingId}`" role="none" class="action-button">
+		<button :id="`actionButtonInner-${bindingId}`" :title="title" :disabled="!canExec || isDisabled"
+													  :class="combinedClasses" @click="handleClick">
 
 			<div class="navigate-on-start-container">
 				<div v-if="navigateOnStart == 'pop'" class="navigate-on-start" title="Opens a popup dialog on start">
@@ -18,18 +18,19 @@
 			<span class="icon" v-html="unicodeIcon"></span>
 			<span class="title" aria-live="polite">{{ displayTitle }}
 			</span>
+			<span v-if="rateLimitMessage" class="rate-limit-message">{{ rateLimitMessage }}</span>
 		</button>
 	</div>
 </template>
 
 <script setup>
-import ArgumentForm from './views/ArgumentForm.vue'
 import { buttonResults } from './stores/buttonResults'
+import { rateLimits } from './stores/rateLimits'
 import { useRouter } from 'vue-router'
 import { HugeiconsIcon } from '@hugeicons/vue'
 import { WorkoutRunIcon, TypeCursorIcon, ComputerTerminal01Icon } from '@hugeicons/core-free-icons'
 
-import { ref, watch, onMounted, inject } from 'vue'
+import { ref, watch, onMounted, onUnmounted, inject, computed } from 'vue'
 
 const router = useRouter()
 const navigateOnStart = ref('')
@@ -38,10 +39,15 @@ const props = defineProps({
   actionData: {
 	type: Object,
 	required: true
+  },
+  cssClass: {
+	type: String,
+	required: false,
+	default: ''
   }
 })
 
-const actionId = ref('')
+const bindingId = ref('')
 const title = ref('')
 const canExec = ref(true)
 const popupOnStart = ref('')
@@ -54,9 +60,24 @@ const displayTitle = ref('')
 const isDisabled = ref(false)
 const showArgumentForm = ref(false)
 
+// Rate limiting
+const rateLimitExpires = ref(0)
+const isRateLimited = ref(false)
+const rateLimitMessage = ref('')
+let rateLimitInterval = null
+
 // Animation classes
 const buttonClasses = ref([])
 
+// Combined classes including custom cssClass
+const combinedClasses = computed(() => {
+	const classes = [...buttonClasses.value]
+	if (props.cssClass) {
+		classes.push(props.cssClass)
+	}
+	return classes
+})
+
 // Timestamps
 const updateIterationTimestamp = ref(0)
 
@@ -75,7 +96,7 @@ function constructFromJson(json) {
 
   updateFromJson(json)
 
-  actionId.value = json.bindingId
+  bindingId.value = json.bindingId
   title.value = json.title
   canExec.value = json.canExec
   popupOnStart.value = json.popupOnStart
@@ -89,6 +110,19 @@ function constructFromJson(json) {
   isDisabled.value = !json.canExec
   displayTitle.value = title.value
   unicodeIcon.value = getUnicodeIcon(json.icon)
+  
+  // Initialize rate limit from action data (parse datetime string)
+  if (json.datetimeRateLimitExpires) {
+	const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
+	rateLimitExpires.value = date.getTime() / 1000
+  } else {
+	rateLimitExpires.value = 0
+  }
+  // Also initialize the store so the watch picks it up
+  if (bindingId.value) {
+	rateLimits[bindingId.value] = rateLimitExpires.value
+  }
+  updateRateLimitStatus()
 }
 
 function updateFromJson(json) {
@@ -96,6 +130,55 @@ function updateFromJson(json) {
   // title - as the callback URL relies on it
 
   unicodeIcon.value = getUnicodeIcon(json.icon)
+  
+  // Update rate limiting if changed (parse datetime string)
+  if (json.datetimeRateLimitExpires) {
+	const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
+	rateLimitExpires.value = date.getTime() / 1000
+	updateRateLimitStatus()
+  } else if (json.datetimeRateLimitExpires === '') {
+	// Explicitly clear if empty string
+	rateLimitExpires.value = 0
+	updateRateLimitStatus()
+  }
+}
+
+function updateRateLimitStatus() {
+  if (rateLimitExpires.value === 0) {
+	isRateLimited.value = false
+	rateLimitMessage.value = ''
+	if (rateLimitInterval) {
+	  clearInterval(rateLimitInterval)
+	  rateLimitInterval = null
+	}
+	return
+  }
+
+  const now = Math.floor(Date.now() / 1000)
+  const expires = rateLimitExpires.value
+
+  if (now >= expires) {
+	// Rate limit has expired
+	isRateLimited.value = false
+	rateLimitMessage.value = ''
+	rateLimitExpires.value = 0
+	if (rateLimitInterval) {
+	  clearInterval(rateLimitInterval)
+	  rateLimitInterval = null
+	}
+  } else {
+	// Still rate limited
+	isRateLimited.value = true
+	const secondsRemaining = expires - now
+	rateLimitMessage.value = `Rate limited, available in ${secondsRemaining} second${secondsRemaining !== 1 ? 's' : ''}`
+	
+	// Set up interval to update every second
+	if (!rateLimitInterval) {
+	  rateLimitInterval = setInterval(() => {
+		updateRateLimitStatus()
+	  }, 1000)
+	}
+  }
 }
 
 async function handleClick() {
@@ -199,6 +282,30 @@ function onExecStatusChanged() {
 
 onMounted(() => {
   constructFromJson(props.actionData)
+  
+  // Watch the central rate limit store for updates to this button's bindingId
+  // Watch the entire rateLimits object to ensure reactivity with dynamic keys
+  watch(
+	rateLimits,
+	() => {
+	  const id = bindingId.value
+	  if (id && rateLimits[id] !== undefined) {
+		const newExpires = rateLimits[id]
+		if (newExpires !== rateLimitExpires.value) {
+		  rateLimitExpires.value = newExpires
+		  updateRateLimitStatus()
+		}
+	  }
+	},
+	{ deep: true }
+  )
+})
+
+onUnmounted(() => {
+  if (rateLimitInterval) {
+	clearInterval(rateLimitInterval)
+	rateLimitInterval = null
+  }
 })
 
 watch(
@@ -256,6 +363,13 @@ watch(
 	padding: 0.2em;
 }
 
+.action-button button .rate-limit-message {
+	font-size: 0.75em;
+	color: #856404;
+	padding: 0.2em;
+	font-weight: normal;
+}
+
 /* Animation classes */
 .action-button button.action-timeout {
 	background: #fff3cd;

+ 14 - 3
frontend/resources/vue/App.vue

@@ -1,5 +1,5 @@
 <template>
-    <Header :title="pageTitle" :logoUrl="logoUrl" @toggleSidebar="toggleSidebar" :sidebarEnabled="showNavigation">
+    <Header :title="pageTitle" :logoUrl="logoUrl" @toggleSidebar="toggleSidebar" :sidebarEnabled="sidebarEnabled" :topBarEnabled="topbarEnabled" :navigation="navigation">
         <template #toolbar>
             <div id="banner" v-if="bannerMessage" :style="bannerCss">
                 <p>{{ bannerMessage }}</p>
@@ -19,8 +19,8 @@
     </Header>
 
     <div id="layout">
-        <Navigation ref="navigation" v-if="showNavigation">
-            <Sidebar ref="sidebar" id = "mainnav" v-if="showNavigation" />
+        <Navigation ref="navigation">
+            <Sidebar ref="sidebar" id = "mainnav" v-if="sidebarEnabled && showNavigation"/>
         </Navigation>
 
 		<div id="content" initial-martial-complete="{{ hasLoaded }}">
@@ -108,6 +108,7 @@ const showNavigation = ref(true)
 const showLogs = ref(true)
 const showDiagnostics = ref(true)
 const showLoginLink = ref(true)
+const sectionNavigationStyle = ref('sidebar')
 
 const languageDialog = ref(null)
 const browserLanguages = ref([])
@@ -135,6 +136,15 @@ const currentLanguageName = computed(() => {
     return availableLanguages[languagePreference.value] || languagePreference.value
 })
 
+// Computed properties for navigation style
+const topbarEnabled = computed(() => {
+    return sectionNavigationStyle.value === 'topbar'
+})
+
+const sidebarEnabled = computed(() => {
+    return sectionNavigationStyle.value !== 'topbar' && showNavigation.value
+})
+
 function normalizeBrowserLanguage() {
     const available = Object.keys(combinedTranslations.messages || {})
 
@@ -180,6 +190,7 @@ function updateHeaderFromInit() {
     showNavigation.value = window.initResponse.showNavigation
     showLogs.value = window.initResponse.showLogList
     showDiagnostics.value = window.initResponse.showDiagnostics
+    sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
 
     if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
         showLoginLink.value = false

+ 1 - 1
frontend/resources/vue/Dashboard.vue

@@ -45,7 +45,7 @@
                     <span v-else>{{ component.title }}</span>
                 </h2>
 
-                <fieldset>
+                <fieldset :class="component.cssClass">
                     <template v-for="subcomponent in component.contents">
                         <DashboardComponent :component="subcomponent" />
                     </template>

+ 1 - 1
frontend/resources/vue/components/DashboardComponent.vue

@@ -1,5 +1,5 @@
 <template>
-    <ActionButton v-if="component.type == 'link'" :actionData="component.action" :key="component.title" />
+    <ActionButton v-if="component.type == 'link'" :actionData="component.action" :cssClass="component.cssClass" :key="component.title" />
 
     <DashboardComponentDirectory v-else-if="component.type == 'directory'" :component="component" />
 

+ 1 - 1
frontend/resources/vue/components/DashboardComponentDirectory.vue

@@ -1,5 +1,5 @@
 <template>
-    <button @click="navigateToDirectory">
+    <button @click="navigateToDirectory" :class="component.cssClass">
         <span class="icon" v-html="unicodeIcon"></span>
         <span class="title">{{ component.title }}</span>
     </button>

+ 1 - 1
frontend/resources/vue/components/DashboardComponentDisplay.vue

@@ -1,5 +1,5 @@
 <template>
-    <div class="display">
+    <div class="display" :class="component.cssClass">
         <div v-html="component.title" />
     </div>
 </template>

+ 45 - 31
frontend/resources/vue/components/DashboardComponentMostRecentExecution.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="mre-container">   
+  <div class="mre-container" :class="component.cssClass">   
     <router-link 
         v-if="executionTrackingId" 
         :to="`/logs/${executionTrackingId}`" 
@@ -12,7 +12,8 @@
 </template>
 
 <script setup>
-import { ref, onMounted, onBeforeUnmount } from 'vue'
+import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
+import { buttonResults } from '../stores/buttonResults'
 
 const props = defineProps({
   component: {
@@ -23,7 +24,20 @@ const props = defineProps({
 
 const output = ref('Waiting...')
 const executionTrackingId = ref(null)
-let eventListener = null
+let unwatchButtonResults = null
+
+function updateFromLogEntry(logEntry) {
+  if (logEntry) {
+    if (logEntry.output !== undefined) {
+      output.value = logEntry.output
+    } else {
+      output.value = 'No output available'
+    }
+    if (logEntry.executionTrackingId) {
+      executionTrackingId.value = logEntry.executionTrackingId
+    }
+  }
+}
 
 async function fetchMostRecentExecution() {
   if (!props.component.title) {
@@ -46,14 +60,7 @@ async function fetchMostRecentExecution() {
     const result = await window.client.executionStatus(executionStatusArgs)
     
     if (result.logEntry) {
-      if (result.logEntry.output !== undefined) {
-        output.value = result.logEntry.output
-      } else {
-        output.value = 'No output available'
-      }
-      if (result.logEntry.executionTrackingId) {
-        executionTrackingId.value = result.logEntry.executionTrackingId
-      }
+      updateFromLogEntry(result.logEntry)
     } else {
       output.value = 'No output available'
       executionTrackingId.value = null
@@ -70,32 +77,39 @@ async function fetchMostRecentExecution() {
   }
 }
 
-function handleExecutionFinished(event) {
-  // The dashboard component "title" field is used for lots of things
-  // and in this context for MreOutput it's just to refer to an actionId.
-  //
-  // So this is not a typo.
-  const logEntry = event.payload.logEntry
-  if (logEntry && logEntry.actionId === props.component.title) {
-    if (logEntry.output !== undefined) {
-      output.value = logEntry.output
-    }
-    if (logEntry.executionTrackingId) {
-      executionTrackingId.value = logEntry.executionTrackingId
-    }
-  }
-}
-
 onMounted(() => {
   fetchMostRecentExecution()
   
-  eventListener = (event) => handleExecutionFinished(event)
-  window.addEventListener('EventExecutionFinished', eventListener)
+  unwatchButtonResults = watch(
+    buttonResults,
+    () => {
+      // Find the most recent finished execution for this bindingId
+      const bindingId = props.component.title
+      let mostRecent = null
+      let mostRecentTime = null
+      
+      for (const trackingId in buttonResults) {
+        const logEntry = buttonResults[trackingId]
+        if (logEntry && logEntry.bindingId === bindingId && logEntry.executionFinished) {
+          const finishedTime = new Date(logEntry.datetimeFinished)
+          if (!mostRecent || finishedTime > mostRecentTime) {
+            mostRecent = logEntry
+            mostRecentTime = finishedTime
+          }
+        }
+      }
+      
+      if (mostRecent) {
+        updateFromLogEntry(mostRecent)
+      }
+    },
+    { deep: true }
+  )
 })
 
 onBeforeUnmount(() => {
-  if (eventListener) {
-    window.removeEventListener('EventExecutionFinished', eventListener)
+  if (unwatchButtonResults) {
+    unwatchButtonResults()
   }
 })
 </script>

+ 12 - 0
frontend/resources/vue/router.js

@@ -35,6 +35,18 @@ const routes = [
       icon: LeftToRightListDashIcon
     }
   },
+  {
+    path: '/logs/calendar',
+    name: 'LogsCalendar',
+    component: () => import('./views/LogsCalendarView.vue'),
+    meta: { 
+      title: 'Logs Calendar',
+      breadcrumb: [
+        { name: "Logs", href: "/logs" },
+        { name: "Calendar" },
+      ]
+    }
+  },
   {
     path: '/entities',
     name: 'Entities',

+ 5 - 0
frontend/resources/vue/stores/rateLimits.js

@@ -0,0 +1,5 @@
+import { reactive } from 'vue'
+
+// Store rate limit expiry times by bindingId
+// This allows all ActionButton components to reactively update when rate limits change
+export const rateLimits = reactive({})

+ 62 - 2
frontend/resources/vue/views/ArgumentForm.vue

@@ -12,10 +12,13 @@
                 {{ formatLabel(arg.title) }}
               </label>
 
-              <datalist v-if="arg.suggestions && Object.keys(arg.suggestions).length > 0" :id="`${arg.name}-choices`">
+              <datalist v-if="(arg.suggestions && Object.keys(arg.suggestions).length > 0) || getBrowserSuggestions(arg).length > 0" :id="`${arg.name}-choices`">
                 <option v-for="(suggestion, key) in arg.suggestions" :key="key" :value="key">
                   {{ suggestion }}
                 </option>
+                <option v-for="(suggestion, index) in getBrowserSuggestions(arg)" :key="`browser-${index}`" :value="suggestion">
+                  {{ suggestion }}
+                </option>
               </datalist>
 
               <select v-if="getInputComponent(arg) === 'select'" :id="arg.name" :name="arg.name" :value="getArgumentValue(arg)"
@@ -28,7 +31,7 @@
               <component v-else :is="getInputComponent(arg)" :id="arg.name" :name="arg.name" 
                 :value="(arg.type === 'checkbox' || arg.type === 'confirmation') ? undefined : getArgumentValue(arg)"
                 :checked="(arg.type === 'checkbox' || arg.type === 'confirmation') ? getArgumentValue(arg) : undefined"
-                :list="arg.suggestions ? `${arg.name}-choices` : undefined" 
+                :list="(arg.suggestions || getBrowserSuggestions(arg).length > 0) ? `${arg.name}-choices` : undefined" 
                 :type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
                 :rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
                 :step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)"
@@ -313,6 +316,60 @@ function getUniqueId() {
   }
 }
 
+function getBrowserSuggestions(arg) {
+  if (!arg.suggestionsBrowserKey) {
+    return []
+  }
+  
+  try {
+    const stored = localStorage.getItem(`olivetin-suggestions-${arg.suggestionsBrowserKey}`)
+    if (stored) {
+      const suggestions = JSON.parse(stored)
+      return Array.isArray(suggestions) ? suggestions : []
+    }
+  } catch (err) {
+    console.warn('Failed to load browser suggestions:', err)
+  }
+  
+  return []
+}
+
+function saveBrowserSuggestions() {
+  for (const arg of actionArguments.value) {
+    if (arg.suggestionsBrowserKey) {
+      const value = argValues.value[arg.name]
+      
+      // Only save non-empty values for non-checkbox/confirmation/password types
+      if (value && value !== '' && arg.type !== 'checkbox' && arg.type !== 'confirmation' && arg.type !== 'password') {
+        try {
+          const key = `olivetin-suggestions-${arg.suggestionsBrowserKey}`
+          const stored = localStorage.getItem(key)
+          let suggestions = []
+          
+          if (stored) {
+            suggestions = JSON.parse(stored)
+            if (!Array.isArray(suggestions)) {
+              suggestions = []
+            }
+          }
+          
+          // Add value if not already present
+          if (!suggestions.includes(value)) {
+            suggestions.unshift(value) // Add to beginning
+            // Keep only the most recent 50 suggestions
+            if (suggestions.length > 50) {
+              suggestions = suggestions.slice(0, 50)
+            }
+            localStorage.setItem(key, JSON.stringify(suggestions))
+          }
+        } catch (err) {
+          console.warn('Failed to save browser suggestions:', err)
+        }
+      }
+    }
+  }
+}
+
 async function startAction(actionArgs) {
   const startActionArgs = {
     bindingId: props.bindingId,
@@ -356,6 +413,9 @@ async function handleSubmit(event) {
   const argvs = getArgumentValues()
   console.log('argument form has elements that passed validation')
   
+  // Save values to localStorage for arguments with suggestionsBrowserKey
+  saveBrowserSuggestions()
+  
   try {
     const response = await startAction(argvs)
     router.push(`/logs/${response.executionTrackingId}`)

+ 168 - 0
frontend/resources/vue/views/LogsCalendarView.vue

@@ -0,0 +1,168 @@
+<template>
+  <Section :title="t('logs.calendar-title')" :padding="false">
+    <template #toolbar>
+      <router-link to="/logs" class="button neutral">
+        <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
+          <path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"/>
+        </svg>
+        {{ t('logs.back-to-list') }}
+      </router-link>
+    </template>
+
+    <div class="padding">
+      <Calendar
+        :events="calendarEvents"
+        :loading="loading"
+        :error="error"
+        :current-month="currentMonthIndex"
+        :current-year="currentYear"
+        @event-click="handleEventClick"
+        @date-click="handleDayClick"
+        @month-change="handleMonthChange"
+      />
+    </div>
+  </Section>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+import Calendar from 'picocrank/vue/components/Calendar.vue'
+import Section from 'picocrank/vue/components/Section.vue'
+
+const router = useRouter()
+const { t } = useI18n()
+
+const logs = ref([])
+const loading = ref(false)
+const error = ref(null)
+const currentMonthIndex = ref(new Date().getMonth())
+const currentYear = ref(new Date().getFullYear())
+
+// Convert logs to calendar events format
+const calendarEvents = computed(() => {
+  return logs.value
+    .filter(log => {
+      // Only include logs with valid start dates
+      if (!log.datetimeStarted) return false
+      const startDate = new Date(log.datetimeStarted)
+      return !isNaN(startDate.getTime())
+    })
+    .map(log => {
+      const startDate = new Date(log.datetimeStarted)
+      let endDate = log.datetimeFinished ? new Date(log.datetimeFinished) : null
+      
+      // Validate end date
+      if (endDate && isNaN(endDate.getTime())) {
+        endDate = null
+      }
+
+      return {
+        id: log.executionTrackingId,
+        title: log.actionTitle || 'Untitled Action',
+        date: startDate,
+        startDate: startDate,
+        endDate: endDate,
+        actionIcon: log.actionIcon,
+        user: log.user,
+        tags: log.tags,
+        logEntry: log
+      }
+    })
+})
+
+async function fetchLogs() {
+  loading.value = true
+  error.value = null
+  
+  try {
+    // Currently fetches only the default page (startOffset: 0)
+    // Multi-page fetching: loop through pages until no more logs or limit reached
+    const allLogs = []
+    let startOffset = BigInt(0)
+    const maxLogs = 10000 // Reasonable limit to prevent excessive API calls
+    const pageSize = 100 // Typical page size, will be updated from API response
+    
+    while (allLogs.length < maxLogs) {
+      const args = {
+        "startOffset": startOffset,
+      }
+
+      const response = await window.client.getLogs(args)
+      const pageLogs = response.logs || []
+      
+      // If no logs returned, we've reached the end
+      if (pageLogs.length === 0) {
+        break
+      }
+      
+      // Append logs from this page
+      allLogs.push(...pageLogs)
+      
+      // Update offset for next page
+      const currentPageSize = Number(response.pageSize) || pageLogs.length
+      startOffset += BigInt(currentPageSize)
+      
+      // If we got fewer logs than the page size, we've reached the end
+      if (pageLogs.length < currentPageSize) {
+        break
+      }
+    }
+    
+    logs.value = allLogs
+  } catch (err) {
+    console.error('Failed to fetch logs:', err)
+    error.value = 'Failed to load logs'
+    window.showBigError('fetch-logs-calendar', 'getting logs for calendar', err, false)
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleEventClick(event) {
+  // Navigate to the execution view when clicking on a calendar event
+  if (event.id) {
+    router.push(`/logs/${event.id}`)
+  }
+}
+
+function handleDayClick(date) {
+  // Navigate to logs list filtered by the selected date
+  // Format date as YYYY-MM-DD using local date components to avoid timezone issues
+  const year = date.getFullYear()
+  const month = String(date.getMonth() + 1).padStart(2, '0')
+  const day = String(date.getDate()).padStart(2, '0')
+  const dateString = `${year}-${month}-${day}`
+  router.push({ path: '/logs', query: { date: dateString } })
+}
+
+function handleMonthChange(month, year) {
+  currentMonthIndex.value = month
+  currentYear.value = year
+  // Optionally fetch logs for the new month if needed
+  // For now, we'll keep all logs loaded
+}
+
+onMounted(() => {
+  fetchLogs()
+})
+</script>
+
+<style scoped>
+.padding {
+  padding: 1rem;
+}
+
+@media (prefers-color-scheme: dark) {
+  :deep(div.calendar-header-nav) {
+    background-color: var(--bg, #111);
+    color: var(--text-color, #fff);
+    border-color: var(--border-color, #333);
+  }
+
+  :deep(div.calendar-header-nav h2.calendar-title) {
+    color: #fff !important;
+  }
+}
+</style>

+ 119 - 5
frontend/resources/vue/views/LogsListView.vue

@@ -1,6 +1,9 @@
 <template>
   <Section :title="t('logs.title')" :padding="false">
       <template #toolbar>
+        <router-link to="/logs/calendar" class="button neutral">
+          {{ t('logs.calendar') }}
+        </router-link>
         <label class="input-with-icons">
           <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
             <path fill="currentColor"
@@ -21,7 +24,20 @@
         <table class="logs-table">
           <thead>
             <tr>
-              <th>{{ t('logs.timestamp') }}</th>
+              <th>
+                <div class="timestamp-header">
+                  <span>{{ t('logs.timestamp') }}</span>
+                  <span v-if="selectedDate" class="date-filter-indicator">
+                    <span class="date-filter-text">{{ formatDateFilter(selectedDate) }}</span>
+                    <button :title="t('logs.clear-date-filter')" @click="clearDateFilter" class="clear-date-button">
+                      <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
+                        <path fill="currentColor"
+                          d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
+                      </svg>
+                    </button>
+                  </span>
+                </div>
+              </th>
               <th>{{ t('logs.action') }}</th>
               <th>{{ t('logs.metadata') }}</th>
               <th>{{ t('logs.status') }}</th>
@@ -56,7 +72,14 @@
           @page-size-change="handlePageSizeChange" itemTitle="execution logs" />
       </div>
 
-      <div v-show="logs.length === 0" class="empty-state">
+      <div v-show="selectedDate && filteredLogs.length === 0" class="empty-state">
+        <p>{{ t('logs.no-logs-to-display') }} {{ formatDateFilter(selectedDate) }}.</p>
+        <button @click="clearDateFilter" class="button neutral">
+          {{ t('logs.clear-date-filter') }}
+        </button>
+      </div>
+
+      <div v-show="logs.length === 0 && !selectedDate" class="empty-state">
         <p>{{ t('logs.no-logs-to-display') }}</p>
         <router-link to="/">{{ t('return-to-index') }}</router-link>
       </div>
@@ -64,27 +87,50 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue'
+import { ref, computed, onMounted, watch } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
 import Pagination from 'picocrank/vue/components/Pagination.vue'
 import Section from 'picocrank/vue/components/Section.vue'
 import { useI18n } from 'vue-i18n'
 import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
 
+const route = useRoute()
+const router = useRouter()
+
 const logs = ref([])
 const searchText = ref('')
 const pageSize = ref(10)
 const currentPage = ref(1)
 const loading = ref(false)
 const totalCount = ref(0)
+const selectedDate = ref(null)
 
 const { t } = useI18n()
 
+// Read date query parameter from route
+function updateDateFromRoute() {
+  const dateParam = route.query.date
+  if (dateParam) {
+    selectedDate.value = dateParam
+  } else {
+    selectedDate.value = null
+  }
+  // Re-fetch logs when date changes
+  fetchLogs()
+}
+
+// Watch for route changes to update date filter
+watch(() => route.query.date, () => {
+  updateDateFromRoute()
+})
+
 const filteredLogs = computed(() => {
   let result = logs.value
   
+  // Date filtering is now done server-side, so we only need to filter by search text
   if (searchText.value) {
     const searchLower = searchText.value.toLowerCase()
-    result = logs.value.filter(log =>
+    result = result.filter(log =>
       log.actionTitle.toLowerCase().includes(searchLower)
     )
   }
@@ -106,6 +152,11 @@ async function fetchLogs() {
       "startOffset": BigInt(startOffset),
     }
 
+    // Add date filter if selected
+    if (selectedDate.value) {
+      args.dateFilter = selectedDate.value
+    }
+
     const response = await window.client.getLogs(args)
 
     logs.value = response.logs
@@ -123,6 +174,24 @@ function clearSearch() {
   searchText.value = ''
 }
 
+function clearDateFilter() {
+  selectedDate.value = null
+  // Remove date query parameter from URL
+  const query = { ...route.query }
+  delete query.date
+  router.push({ path: route.path, query })
+}
+
+function formatDateFilter(dateString) {
+  // Format YYYY-MM-DD to a short format (e.g., "Jan 15, 2024")
+  try {
+    const date = new Date(dateString + 'T00:00:00')
+    return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
+  } catch (err) {
+    return dateString
+  }
+}
+
 function formatTimestamp(timestamp) {
   if (!timestamp) return 'Unknown'
   try {
@@ -141,9 +210,11 @@ function handlePageChange(page) {
 function handlePageSizeChange(newPageSize) {
   pageSize.value = newPageSize
   currentPage.value = 1 // Reset to first page
+  fetchLogs()
 }
 
 onMounted(() => {
+  updateDateFromRoute()
   fetchLogs()
 })
 </script>
@@ -227,4 +298,47 @@ onMounted(() => {
   text-decoration: underline;
 }
 
-</style>
+.timestamp-header {
+  display: flex;
+  flex-direction: column;
+  gap: 0.25rem;
+}
+
+.date-filter-indicator {
+  display: flex;
+  align-items: center;
+  gap: 0.25rem;
+  font-size: 0.75rem;
+  font-weight: normal;
+  color: var(--text-secondary, #666);
+  white-space: nowrap;
+}
+
+.date-filter-text {
+  font-style: italic;
+}
+
+.timestamp-header .clear-date-button {
+  background: none;
+  border: none;
+  cursor: pointer;
+  padding: 0.125rem;
+  border-radius: 3px;
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+  opacity: 0.7;
+  transition: opacity 0.2s;
+}
+
+.timestamp-header .clear-date-button:hover {
+  opacity: 1;
+  background: var(--hover-background, rgba(0, 0, 0, 0.05));
+}
+
+.timestamp-header .clear-date-button svg {
+  width: 0.75rem;
+  height: 0.75rem;
+}
+
+</style>

+ 39 - 0
integration-tests/tests/enabledExpression/config.yaml

@@ -0,0 +1,39 @@
+#
+# Integration Test Config: EnabledExpression
+#
+
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
+
+logLevel: "DEBUG"
+checkForUpdates: false
+
+actions:
+  - title: Turn On Light
+    shell: echo "Turning on light"
+    icon: light
+    entity: light
+    enabledExpression: "{{ eq .CurrentEntity.powered_on false }}"
+
+  - title: Turn Off Light
+    shell: echo "Turning off light"
+    icon: light
+    entity: light
+    enabledExpression: "{{ eq .CurrentEntity.powered_on true }}"
+
+  - title: Always Enabled Action
+    shell: echo "Always enabled"
+    icon: check
+
+entities:
+  - file: entities/lights.yaml
+    name: light
+
+dashboards:
+  - title: LightDashboard
+    contents:
+      - title: Light Controls
+        type: fieldset
+        entity: light
+        contents:
+          - title: Turn On Light
+          - title: Turn Off Light

+ 150 - 0
integration-tests/tests/enabledExpression/enabledExpression.mjs

@@ -0,0 +1,150 @@
+import { describe, it, before, after } from 'mocha'
+import { expect } from 'chai'
+import { By, until, Condition } from 'selenium-webdriver'
+import {
+  getRootAndWait,
+  takeScreenshotOnFailure,
+} from '../../lib/elements.js'
+
+describe('config: enabledExpression', function () {
+  this.timeout(30000) // Increase timeout for async operations
+
+  before(async function () {
+    await runner.start('enabledExpression')
+  })
+
+  after(async () => {
+    await runner.stop()
+  })
+
+  afterEach(function () {
+    takeScreenshotOnFailure(this.currentTest, webdriver);
+  });
+
+  it('Action with enabledExpression for lights enable the correct action', async function() {
+    await getRootAndWait()
+
+    // Navigate to the Lights Dashboard
+    // Use the path with space encoded as %20 - Vue Router should decode it
+    await webdriver.get(runner.baseUrl() + '/dashboards/LightDashboard')
+
+    // Wait for the URL to change and the route to be processed
+    await webdriver.wait(new Condition('wait for URL to contain dashboards', async function() {
+      const url = await webdriver.getCurrentUrl()
+      return url.includes('/dashboards/')
+    }), 5000)
+
+    // Wait for dashboard to load by checking the loaded-dashboard attribute
+    // The attribute should be set to the decoded title "LightDashboard"
+    await webdriver.wait(new Condition('wait for loaded-dashboard', async function() {
+      const body = await webdriver.findElement(By.tagName('body'))
+      const attr = await body.getAttribute('loaded-dashboard')
+      if (attr) {
+        console.log('Current loaded-dashboard attribute:', attr)
+      }
+      // Accept either decoded or encoded version (component should decode, but handle both)
+      return attr === 'LightDashboard'
+    }), 10000)
+
+    // Verify we got the correct dashboard (prefer decoded, but accept encoded)
+    const body = await webdriver.findElement(By.tagName('body'))
+    const attr = await body.getAttribute('loaded-dashboard')
+    if (attr !== 'LightDashboard') {
+      const currentUrl = await webdriver.getCurrentUrl()
+      throw new Error(`Dashboard not loaded correctly. Expected "LightDashboard", got "${attr}". Current URL: ${currentUrl}`)
+    }
+
+    // Wait for dashboard content to appear - check for dashboard rows first
+    await webdriver.wait(until.elementsLocated(By.css('.dashboard-row')), 5000)
+
+    // Debug: Check what's on the page
+    const dashboardRows = await webdriver.findElements(By.css('.dashboard-row'))
+    console.log(`Found ${dashboardRows.length} dashboard rows`)
+    
+    for (let i = 0; i < dashboardRows.length; i++) {
+      const row = dashboardRows[i]
+      const h2Elements = await row.findElements(By.css('h2'))
+      if (h2Elements.length > 0) {
+        const h2Text = await h2Elements[0].getText()
+        console.log(`Row ${i} h2: "${h2Text}"`)
+      }
+      const fieldsets = await row.findElements(By.css('fieldset'))
+      console.log(`Row ${i} has ${fieldsets.length} fieldsets`)
+      if (fieldsets.length > 0) {
+        const buttons = await fieldsets[0].findElements(By.css('.action-button button'))
+        console.log(`Row ${i} fieldset has ${buttons.length} buttons`)
+      }
+    }
+
+    // Find buttons by looking within entity fieldsets
+    // Both rows have h2 title "Light Controls", so we identify them by which buttons are enabled
+    // Living Room Light (powered_on: false) - Turn On should be enabled, Turn Off disabled
+    // Bedroom Light (powered_on: true) - Turn Off should be enabled, Turn On disabled
+    let turnOnButton = null
+    let turnOffButton = null
+    
+    for (const row of dashboardRows) {
+      // Get the fieldset in this row
+      const fieldsets = await row.findElements(By.css('fieldset'))
+      if (fieldsets.length === 0) continue
+      
+      const buttons = await fieldsets[0].findElements(By.css('.action-button button'))
+      
+      // Check each button to identify which entity this row represents
+      for (const btn of buttons) {
+        const title = await btn.getAttribute('title')
+        const disabled = await btn.getAttribute('disabled')
+        const isEnabled = disabled === null
+        
+        if (title === 'Turn On Light' && isEnabled) {
+          // This is the Living Room Light row (Turn On is enabled because powered_on: false)
+          turnOnButton = btn
+        }
+        
+        if (title === 'Turn Off Light' && isEnabled) {
+          // This is the Bedroom Light row (Turn Off is enabled because powered_on: true)
+          turnOffButton = btn
+        }
+      }
+    }
+
+    expect(turnOnButton).to.not.be.null
+    expect(turnOffButton).to.not.be.null
+
+    // Check that Turn On button is enabled (light is off)
+    const turnOnDisabled = await turnOnButton.getAttribute('disabled')
+    expect(turnOnDisabled).to.be.null
+
+    // Check that Turn Off button is enabled (light is on)
+    const turnOffDisabled = await turnOffButton.getAttribute('disabled')
+    expect(turnOffDisabled).to.be.null
+  })
+
+  it('Action without enabledExpression is always enabled', async function() {
+    await getRootAndWait()
+
+    // Navigate to actions view
+    await webdriver.get(runner.baseUrl())
+
+    // Wait for action buttons
+    await webdriver.wait(until.elementLocated(By.css('.action-button')), 10000)
+
+    // Find "Always Enabled Action" button
+    const actionButtons = await webdriver.findElements(By.css('.action-button button'))
+    let alwaysEnabledButton = null
+
+    for (const btn of actionButtons) {
+      const title = await btn.getAttribute('title')
+      if (title === 'Always Enabled Action') {
+        alwaysEnabledButton = btn
+        break
+      }
+    }
+
+    expect(alwaysEnabledButton).to.not.be.null
+
+    // Check that it's enabled
+    const disabled = await alwaysEnabledButton.getAttribute('disabled')
+    expect(disabled).to.be.null
+  })
+})

+ 5 - 0
integration-tests/tests/enabledExpression/entities/lights.yaml

@@ -0,0 +1,5 @@
+- name: "Living Room Light"
+  powered_on: false
+
+- name: "Bedroom Light"
+  powered_on: true

+ 2 - 0
integration-tests/tests/stdoutMostRecentExecution/config.yaml

@@ -1,5 +1,7 @@
 logLevel: debug
 
+insecureAllowDumpActionMap: true
+
 actions:
   - title: Check status
     id: status_command

+ 3 - 4
integration-tests/tests/stdoutMostRecentExecution/stdoutMostRecentExecution.mjs

@@ -59,8 +59,7 @@ describe('config: stdout-most-recent-execution', function () {
   })
 
   it('stdout-most-recent-execution updates after action execution', async function () {
-    this.timeout(30000) // Increase timeout for this test
-
+    this.timeout(45000)
     await getRootAndWait()
 
     // Wait for the mre-output element
@@ -105,7 +104,7 @@ describe('config: stdout-most-recent-execution', function () {
     await statusButton.click()
 
     // Wait a moment for the action to start
-    await webdriver.sleep(500)
+    await webdriver.sleep(2000)
 
     // Wait for the output to update (the component listens to EventExecutionFinished events)
     // We'll wait for the output to change from the initial state
@@ -138,4 +137,4 @@ describe('config: stdout-most-recent-execution', function () {
     expect(updatedText).to.not.include('No execution found')
     expect(updatedText.trim().length).to.be.greaterThan(0)
   })
-})
+})

+ 19 - 0
integration-tests/tests/suggestionsBrowserKey/config.yaml

@@ -0,0 +1,19 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
+
+logLevel: "DEBUG"
+checkForUpdates: false
+
+actions:
+  - title: Test suggestionsBrowserKey
+    shell: "echo 'Input value: {{ testInput }}, Second input: {{ testInput2 }}'"
+    icon: ping
+    arguments:
+      - name: testInput
+        title: Test Input
+        description: "This input uses suggestionsBrowserKey"
+        suggestionsBrowserKey: test-suggestions-key
+      - name: testInput2
+        title: Test Input 2
+        description: "This input shares the same suggestionsBrowserKey"
+        suggestionsBrowserKey: test-suggestions-key

+ 325 - 0
integration-tests/tests/suggestionsBrowserKey/suggestionsBrowserKey.mjs

@@ -0,0 +1,325 @@
+import { describe, it, before, after } from 'mocha'
+import { expect } from 'chai'
+import { By, Condition } from 'selenium-webdriver'
+import {
+  getRootAndWait,
+  getActionButton,
+  takeScreenshotOnFailure,
+  getTerminalBuffer,
+} from '../../lib/elements.js'
+
+async function openArgumentForm() {
+  await getRootAndWait()
+  const btn = await getActionButton(webdriver, 'Test suggestionsBrowserKey')
+  await btn.click()
+
+  await webdriver.wait(
+    new Condition('wait for argument form page', async () => {
+      const url = await webdriver.getCurrentUrl()
+      return url.includes('/actionBinding/') && url.includes('/argumentForm')
+    }),
+    5000
+  )
+}
+
+async function getTestInput() {
+  return await webdriver.findElement(By.id('testInput'))
+}
+
+async function getTestInput2() {
+  return await webdriver.findElement(By.id('testInput2'))
+}
+
+async function getDatalistOptions(inputName = 'testInput') {
+  return await webdriver.findElements(By.css(`datalist#${inputName}-choices option`))
+}
+
+async function submitForm() {
+  const submitButton = await webdriver.findElement(By.css('button[name="start"]'))
+  await submitButton.click()
+}
+
+async function waitForLogsPage() {
+  await webdriver.wait(
+    new Condition('wait for logs page', async () => {
+      const url = await webdriver.getCurrentUrl()
+      return url.includes('/logs/') && !url.endsWith('/logs')
+    }),
+    5000
+  )
+}
+
+async function waitForExecutionComplete() {
+  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)
+}
+
+async function getLocalStorageItem(key) {
+  return await webdriver.executeScript(`return localStorage.getItem('${key}')`)
+}
+
+async function clearLocalStorage() {
+  await webdriver.executeScript('return localStorage.clear()')
+}
+
+describe('config: suggestionsBrowserKey', function () {
+  before(async function () {
+    await runner.start('suggestionsBrowserKey')
+  })
+
+  after(async () => {
+    await runner.stop()
+  })
+
+  afterEach(function () {
+    takeScreenshotOnFailure(this.currentTest, webdriver)
+  })
+
+  it('Input fields with suggestionsBrowserKey are rendered', async function () {
+    await openArgumentForm()
+
+    const input1 = await getTestInput()
+    expect(await input1.getTagName()).to.equal('input')
+    expect(await input1.getAttribute('type')).to.equal('text')
+
+    const label1 = await webdriver.findElement(By.css('label[for="testInput"]'))
+    expect(await label1.getText()).to.contain('Test Input')
+
+    const input2 = await getTestInput2()
+    expect(await input2.getTagName()).to.equal('input')
+    expect(await input2.getAttribute('type')).to.equal('text')
+
+    const label2 = await webdriver.findElement(By.css('label[for="testInput2"]'))
+    expect(await label2.getText()).to.contain('Test Input 2')
+  })
+
+  it('Submitting form saves value to localStorage', async function () {
+    this.timeout(15000)
+    
+    // Clear localStorage first
+    await clearLocalStorage()
+    
+    await openArgumentForm()
+
+    const input = await getTestInput()
+    const testValue = 'test-value-123'
+    await input.clear()
+    await input.sendKeys(testValue)
+
+    await submitForm()
+    await waitForLogsPage()
+    await waitForExecutionComplete()
+
+    // Verify value was saved to localStorage
+    const stored = await getLocalStorageItem('olivetin-suggestions-test-suggestions-key')
+    expect(stored).to.not.be.null
+    
+    const suggestions = JSON.parse(stored)
+    expect(suggestions).to.be.an('array')
+    expect(suggestions).to.include(testValue)
+  })
+
+  it('Previously saved values appear in datalist', async function () {
+    this.timeout(15000)
+    
+    // First, save a value to localStorage
+    const testValue = 'saved-suggestion-456'
+    await webdriver.executeScript(`
+      const key = 'olivetin-suggestions-test-suggestions-key';
+      localStorage.setItem(key, JSON.stringify(['${testValue}']));
+    `)
+
+    // Open the form
+    await openArgumentForm()
+
+    // Check that datalist exists and contains the saved value
+    const datalist = await webdriver.findElement(By.id('testInput-choices'))
+    expect(datalist).to.not.be.null
+
+    const options = await getDatalistOptions()
+    expect(options.length).to.be.greaterThan(0)
+
+    // Check if the saved value appears in the datalist
+    let foundValue = false
+    for (const option of options) {
+      const value = await option.getAttribute('value')
+      if (value === testValue) {
+        foundValue = true
+        break
+      }
+    }
+    expect(foundValue).to.be.true
+  })
+
+  it('Multiple submissions accumulate suggestions', async function () {
+    this.timeout(20000)
+    
+    // Clear localStorage first
+    await clearLocalStorage()
+
+    // Submit first value
+    await openArgumentForm()
+    const input1 = await getTestInput()
+    await input1.clear()
+    await input1.sendKeys('first-value')
+    await submitForm()
+    await waitForLogsPage()
+    await waitForExecutionComplete()
+
+    // Submit second value
+    await openArgumentForm()
+    const input2 = await getTestInput()
+    await input2.clear()
+    await input2.sendKeys('second-value')
+    await submitForm()
+    await waitForLogsPage()
+    await waitForExecutionComplete()
+
+    // Verify both values are in localStorage
+    const stored = await getLocalStorageItem('olivetin-suggestions-test-suggestions-key')
+    expect(stored).to.not.be.null
+    
+    const suggestions = JSON.parse(stored)
+    expect(suggestions).to.be.an('array')
+    expect(suggestions).to.include('first-value')
+    expect(suggestions).to.include('second-value')
+    expect(suggestions[0]).to.equal('second-value') // Most recent should be first
+  })
+
+  it('Empty values are not saved to localStorage', async function () {
+    this.timeout(15000)
+    
+    // Clear localStorage first
+    await clearLocalStorage()
+
+    await openArgumentForm()
+
+    const input = await getTestInput()
+    // Leave input empty (or clear it if it has a default)
+    await input.clear()
+
+    await submitForm()
+    await waitForLogsPage()
+    await waitForExecutionComplete()
+
+    // Verify empty value was not saved - localStorage should be null or empty-equivalent
+    const stored = await getLocalStorageItem('olivetin-suggestions-test-suggestions-key')
+    // Should be null OR empty JSON array string ("[]") OR parse to empty array
+    if (stored !== null) {
+      const suggestions = JSON.parse(stored)
+      expect(suggestions).to.be.an('array')
+      expect(suggestions).to.have.length(0)
+    }
+    // If stored is null, that's also acceptable - no assertion needed
+  })
+
+  it('Suggestions are shared across inputs with the same suggestionsBrowserKey', async function () {
+    this.timeout(20000)
+    
+    // Clear localStorage first
+    await clearLocalStorage()
+
+    // Submit a value using the first input
+    await openArgumentForm()
+    const input1 = await getTestInput()
+    await input1.clear()
+    await input1.sendKeys('shared-value-from-input1')
+    await submitForm()
+    await waitForLogsPage()
+    await waitForExecutionComplete()
+
+    // Open the form again and verify the value appears in both datalists
+    await openArgumentForm()
+    
+    // Check first input's datalist
+    const datalist1 = await webdriver.findElement(By.id('testInput-choices'))
+    expect(datalist1).to.not.be.null
+    const options1 = await getDatalistOptions('testInput')
+    let foundInInput1 = false
+    for (const option of options1) {
+      const value = await option.getAttribute('value')
+      if (value === 'shared-value-from-input1') {
+        foundInInput1 = true
+        break
+      }
+    }
+    expect(foundInInput1).to.be.true
+
+    // Check second input's datalist
+    const datalist2 = await webdriver.findElement(By.id('testInput2-choices'))
+    expect(datalist2).to.not.be.null
+    const options2 = await getDatalistOptions('testInput2')
+    let foundInInput2 = false
+    for (const option of options2) {
+      const value = await option.getAttribute('value')
+      if (value === 'shared-value-from-input1') {
+        foundInInput2 = true
+        break
+      }
+    }
+    expect(foundInInput2).to.be.true
+
+    // Now submit a value using the second input
+    const input2 = await getTestInput2()
+    await input2.clear()
+    await input2.sendKeys('shared-value-from-input2')
+    await submitForm()
+    await waitForLogsPage()
+    await waitForExecutionComplete()
+
+    // Verify both values appear in both datalists
+    await openArgumentForm()
+    
+    // Check that both values are in the first input's datalist
+    const options1After = await getDatalistOptions('testInput')
+    let foundValue1 = false
+    let foundValue2 = false
+    for (const option of options1After) {
+      const value = await option.getAttribute('value')
+      if (value === 'shared-value-from-input1') {
+        foundValue1 = true
+      }
+      if (value === 'shared-value-from-input2') {
+        foundValue2 = true
+      }
+    }
+    expect(foundValue1).to.be.true
+    expect(foundValue2).to.be.true
+
+    // Check that both values are in the second input's datalist
+    const options2After = await getDatalistOptions('testInput2')
+    foundValue1 = false
+    foundValue2 = false
+    for (const option of options2After) {
+      const value = await option.getAttribute('value')
+      if (value === 'shared-value-from-input1') {
+        foundValue1 = true
+      }
+      if (value === 'shared-value-from-input2') {
+        foundValue2 = true
+      }
+    }
+    expect(foundValue1).to.be.true
+    expect(foundValue2).to.be.true
+  })
+})

+ 20 - 0
lang/combined_output.json

@@ -27,7 +27,11 @@
             "language-dialog.title": "Sprache auswählen",
             "login-button": "Login",
             "logs.action": "Aktion",
+            "logs.back-to-list": "Zurück zur Liste",
             "logs.blocked": "Blockiert",
+            "logs.calendar": "Kalender",
+            "logs.calendar-title": "Protokoll-Kalender",
+            "logs.clear-date-filter": "Datumsfilter löschen",
             "logs.clear-filter": "Suchfilter löschen",
             "logs.completed": "Abgeschlossen",
             "logs.exit-code": "Ausführungscode",
@@ -73,7 +77,11 @@
             "language-dialog.title": "Select Language",
             "login-button": "Login",
             "logs.action": "Action",
+            "logs.back-to-list": "Back to List",
             "logs.blocked": "Blocked",
+            "logs.calendar": "Calendar",
+            "logs.calendar-title": "Logs Calendar",
+            "logs.clear-date-filter": "Clear date filter",
             "logs.clear-filter": "Clear search filter",
             "logs.completed": "Completed",
             "logs.exit-code": "Exit code",
@@ -119,7 +127,11 @@
             "language-dialog.title": "Seleccionar idioma",
             "login-button": "Iniciar sesión",
             "logs.action": "Acción",
+            "logs.back-to-list": "Volver a la Lista",
             "logs.blocked": "Bloqueado",
+            "logs.calendar": "Calendario",
+            "logs.calendar-title": "Calendario de Registros",
+            "logs.clear-date-filter": "Limpiar filtro de fecha",
             "logs.clear-filter": "Limpiar filtro de búsqueda",
             "logs.completed": "Completado",
             "logs.exit-code": "Código de salida",
@@ -165,7 +177,11 @@
             "language-dialog.title": "Seleziona lingua",
             "login-button": "Login",
             "logs.action": "Azione",
+            "logs.back-to-list": "Torna all'Elenco",
             "logs.blocked": "Bloccato",
+            "logs.calendar": "Calendario",
+            "logs.calendar-title": "Calendario dei Registri",
+            "logs.clear-date-filter": "Cancella filtro data",
             "logs.clear-filter": "Cancella filtro di ricerca",
             "logs.completed": "Completato",
             "logs.exit-code": "Codice di uscita",
@@ -211,7 +227,11 @@
             "language-dialog.title": "选择语言",
             "login-button": "登录",
             "logs.action": "动作",
+            "logs.back-to-list": "返回列表",
             "logs.blocked": "阻塞",
+            "logs.calendar": "日历",
+            "logs.calendar-title": "日志日历",
+            "logs.clear-date-filter": "清除日期筛选器",
             "logs.clear-filter": "清除搜索筛选器",
             "logs.completed": "完成",
             "logs.exit-code": "退出代码",

+ 4 - 0
lang/de-DE.yaml

@@ -21,6 +21,10 @@ translations:
   logs.exit-code: Ausführungscode
   logs.completed: Abgeschlossen
   logs.clear-filter: Suchfilter löschen
+  logs.clear-date-filter: Datumsfilter löschen
+  logs.calendar: Kalender
+  logs.calendar-title: Protokoll-Kalender
+  logs.back-to-list: Zurück zur Liste
   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, einen sosreport von dieser Seite einzufügen.
   diagnostics.where-to-find-help: Wo Sie Hilfe finden

+ 4 - 0
lang/en.yaml

@@ -21,6 +21,10 @@ translations:
   logs.exit-code: Exit code
   logs.completed: Completed
   logs.clear-filter: Clear search filter
+  logs.clear-date-filter: Clear date filter
+  logs.calendar: Calendar
+  logs.calendar-title: Logs Calendar
+  logs.back-to-list: Back to List
   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 a sosreport from this page.
   diagnostics.where-to-find-help: Where to find help

+ 4 - 0
lang/es-ES.yaml

@@ -21,6 +21,10 @@ translations:
   logs.exit-code: Código de salida
   logs.completed: Completado
   logs.clear-filter: Limpiar filtro de búsqueda
+  logs.clear-date-filter: Limpiar filtro de fecha
+  logs.calendar: Calendario
+  logs.calendar-title: Calendario de Registros
+  logs.back-to-list: Volver a la Lista
   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 un sosreport de esta página.
   diagnostics.where-to-find-help: Dónde encontrar ayuda

+ 4 - 0
lang/it-IT.yaml

@@ -21,6 +21,10 @@ translations:
   logs.exit-code: Codice di uscita
   logs.completed: Completato
   logs.clear-filter: Cancella filtro di ricerca
+  logs.clear-date-filter: Cancella filtro data
+  logs.calendar: Calendario
+  logs.calendar-title: Calendario dei Registri
+  logs.back-to-list: Torna all'Elenco
   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 un sosreport da questa pagina.
   diagnostics.where-to-find-help: Dove trovare aiuto

+ 4 - 0
lang/zh-Hans-CN.yaml

@@ -27,6 +27,10 @@ translations:
   logs.exit-code: 退出代码
   logs.completed: 完成
   logs.clear-filter: 清除搜索筛选器
+  logs.clear-date-filter: 清除日期筛选器
+  logs.calendar: 日历
+  logs.calendar-title: 日志日历
+  logs.back-to-list: 返回列表
   diagnostics.get-support: 获取支持
   diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含 sosreport 将非常有帮助。
   diagnostics.where-to-find-help: 在哪里找到帮助

+ 7 - 3
proto/olivetin/api/v1/olivetin.proto

@@ -13,6 +13,7 @@ message Action {
 	string popup_on_start = 6;
 	int32 order = 7;
 	int32 timeout = 8;
+	string datetime_rate_limit_expires = 9; // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
 }
 
 message ActionArgument {
@@ -25,6 +26,7 @@ message ActionArgument {
 
 	string description = 6;
 	map<string, string> suggestions = 7;
+	string suggestions_browser_key = 8;
 }
 
 message ActionArgumentChoice {
@@ -118,6 +120,7 @@ message StartActionByGetAndWaitResponse {
 
 message GetLogsRequest{
   int64 start_offset = 1;
+  string date_filter = 2; // Optional date filter in YYYY-MM-DD format
 };
 
 message LogEntry {
@@ -132,12 +135,13 @@ message LogEntry {
 	repeated string tags = 10;
 	string execution_tracking_id = 11;
 	string datetime_finished = 12;
-	string action_id = 13;
 	bool execution_started = 14;
 	bool execution_finished = 15;
 	bool blocked = 16;
 	int64 datetime_index = 17;
 	bool can_kill = 18;
+	string datetime_rate_limit_expires = 19; // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
+	string binding_id = 20; // Binding ID for matching rate limits to action buttons
 }
 
 message GetLogsResponse {
@@ -215,7 +219,7 @@ message DumpVarsResponse {
 	map<string, string> contents = 2;
 }
 
-message ActionEntityPair {
+message DebugBinding {
 	string action_title = 1;
 	string entity_prefix = 2;
 }
@@ -223,7 +227,7 @@ message ActionEntityPair {
 message DumpPublicIdActionMapRequest {}
 message DumpPublicIdActionMapResponse {
 	string alert = 1;
-	map<string, ActionEntityPair> contents = 2;
+	map<string, DebugBinding> contents = 2;
 }
 
 message GetReadyzRequest {}

+ 116 - 78
service/gen/olivetin/api/v1/olivetin.pb.go

@@ -22,17 +22,18 @@ const (
 )
 
 type Action struct {
-	state         protoimpl.MessageState `protogen:"open.v1"`
-	BindingId     string                 `protobuf:"bytes,1,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"`
-	Title         string                 `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
-	Icon          string                 `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"`
-	CanExec       bool                   `protobuf:"varint,4,opt,name=can_exec,json=canExec,proto3" json:"can_exec,omitempty"`
-	Arguments     []*ActionArgument      `protobuf:"bytes,5,rep,name=arguments,proto3" json:"arguments,omitempty"`
-	PopupOnStart  string                 `protobuf:"bytes,6,opt,name=popup_on_start,json=popupOnStart,proto3" json:"popup_on_start,omitempty"`
-	Order         int32                  `protobuf:"varint,7,opt,name=order,proto3" json:"order,omitempty"`
-	Timeout       int32                  `protobuf:"varint,8,opt,name=timeout,proto3" json:"timeout,omitempty"`
-	unknownFields protoimpl.UnknownFields
-	sizeCache     protoimpl.SizeCache
+	state                    protoimpl.MessageState `protogen:"open.v1"`
+	BindingId                string                 `protobuf:"bytes,1,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"`
+	Title                    string                 `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
+	Icon                     string                 `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"`
+	CanExec                  bool                   `protobuf:"varint,4,opt,name=can_exec,json=canExec,proto3" json:"can_exec,omitempty"`
+	Arguments                []*ActionArgument      `protobuf:"bytes,5,rep,name=arguments,proto3" json:"arguments,omitempty"`
+	PopupOnStart             string                 `protobuf:"bytes,6,opt,name=popup_on_start,json=popupOnStart,proto3" json:"popup_on_start,omitempty"`
+	Order                    int32                  `protobuf:"varint,7,opt,name=order,proto3" json:"order,omitempty"`
+	Timeout                  int32                  `protobuf:"varint,8,opt,name=timeout,proto3" json:"timeout,omitempty"`
+	DatetimeRateLimitExpires string                 `protobuf:"bytes,9,opt,name=datetime_rate_limit_expires,json=datetimeRateLimitExpires,proto3" json:"datetime_rate_limit_expires,omitempty"` // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
+	unknownFields            protoimpl.UnknownFields
+	sizeCache                protoimpl.SizeCache
 }
 
 func (x *Action) Reset() {
@@ -121,17 +122,25 @@ func (x *Action) GetTimeout() int32 {
 	return 0
 }
 
+func (x *Action) GetDatetimeRateLimitExpires() string {
+	if x != nil {
+		return x.DatetimeRateLimitExpires
+	}
+	return ""
+}
+
 type ActionArgument struct {
-	state         protoimpl.MessageState  `protogen:"open.v1"`
-	Name          string                  `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
-	Title         string                  `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
-	Type          string                  `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
-	DefaultValue  string                  `protobuf:"bytes,4,opt,name=default_value,json=defaultValue,proto3" json:"default_value,omitempty"`
-	Choices       []*ActionArgumentChoice `protobuf:"bytes,5,rep,name=choices,proto3" json:"choices,omitempty"`
-	Description   string                  `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"`
-	Suggestions   map[string]string       `protobuf:"bytes,7,rep,name=suggestions,proto3" json:"suggestions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
-	unknownFields protoimpl.UnknownFields
-	sizeCache     protoimpl.SizeCache
+	state                 protoimpl.MessageState  `protogen:"open.v1"`
+	Name                  string                  `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Title                 string                  `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
+	Type                  string                  `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
+	DefaultValue          string                  `protobuf:"bytes,4,opt,name=default_value,json=defaultValue,proto3" json:"default_value,omitempty"`
+	Choices               []*ActionArgumentChoice `protobuf:"bytes,5,rep,name=choices,proto3" json:"choices,omitempty"`
+	Description           string                  `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"`
+	Suggestions           map[string]string       `protobuf:"bytes,7,rep,name=suggestions,proto3" json:"suggestions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	SuggestionsBrowserKey string                  `protobuf:"bytes,8,opt,name=suggestions_browser_key,json=suggestionsBrowserKey,proto3" json:"suggestions_browser_key,omitempty"`
+	unknownFields         protoimpl.UnknownFields
+	sizeCache             protoimpl.SizeCache
 }
 
 func (x *ActionArgument) Reset() {
@@ -213,6 +222,13 @@ func (x *ActionArgument) GetSuggestions() map[string]string {
 	return nil
 }
 
+func (x *ActionArgument) GetSuggestionsBrowserKey() string {
+	if x != nil {
+		return x.SuggestionsBrowserKey
+	}
+	return ""
+}
+
 type ActionArgumentChoice struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
 	Value         string                 `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
@@ -1088,6 +1104,7 @@ func (x *StartActionByGetAndWaitResponse) GetLogEntry() *LogEntry {
 type GetLogsRequest struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
 	StartOffset   int64                  `protobuf:"varint,1,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"`
+	DateFilter    string                 `protobuf:"bytes,2,opt,name=date_filter,json=dateFilter,proto3" json:"date_filter,omitempty"` // Optional date filter in YYYY-MM-DD format
 	unknownFields protoimpl.UnknownFields
 	sizeCache     protoimpl.SizeCache
 }
@@ -1129,27 +1146,35 @@ func (x *GetLogsRequest) GetStartOffset() int64 {
 	return 0
 }
 
+func (x *GetLogsRequest) GetDateFilter() string {
+	if x != nil {
+		return x.DateFilter
+	}
+	return ""
+}
+
 type LogEntry struct {
-	state               protoimpl.MessageState `protogen:"open.v1"`
-	DatetimeStarted     string                 `protobuf:"bytes,1,opt,name=datetime_started,json=datetimeStarted,proto3" json:"datetime_started,omitempty"`
-	ActionTitle         string                 `protobuf:"bytes,2,opt,name=action_title,json=actionTitle,proto3" json:"action_title,omitempty"`
-	Output              string                 `protobuf:"bytes,3,opt,name=output,proto3" json:"output,omitempty"`
-	TimedOut            bool                   `protobuf:"varint,5,opt,name=timed_out,json=timedOut,proto3" json:"timed_out,omitempty"`
-	ExitCode            int32                  `protobuf:"varint,6,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"`
-	User                string                 `protobuf:"bytes,7,opt,name=user,proto3" json:"user,omitempty"`
-	UserClass           string                 `protobuf:"bytes,8,opt,name=user_class,json=userClass,proto3" json:"user_class,omitempty"`
-	ActionIcon          string                 `protobuf:"bytes,9,opt,name=action_icon,json=actionIcon,proto3" json:"action_icon,omitempty"`
-	Tags                []string               `protobuf:"bytes,10,rep,name=tags,proto3" json:"tags,omitempty"`
-	ExecutionTrackingId string                 `protobuf:"bytes,11,opt,name=execution_tracking_id,json=executionTrackingId,proto3" json:"execution_tracking_id,omitempty"`
-	DatetimeFinished    string                 `protobuf:"bytes,12,opt,name=datetime_finished,json=datetimeFinished,proto3" json:"datetime_finished,omitempty"`
-	ActionId            string                 `protobuf:"bytes,13,opt,name=action_id,json=actionId,proto3" json:"action_id,omitempty"`
-	ExecutionStarted    bool                   `protobuf:"varint,14,opt,name=execution_started,json=executionStarted,proto3" json:"execution_started,omitempty"`
-	ExecutionFinished   bool                   `protobuf:"varint,15,opt,name=execution_finished,json=executionFinished,proto3" json:"execution_finished,omitempty"`
-	Blocked             bool                   `protobuf:"varint,16,opt,name=blocked,proto3" json:"blocked,omitempty"`
-	DatetimeIndex       int64                  `protobuf:"varint,17,opt,name=datetime_index,json=datetimeIndex,proto3" json:"datetime_index,omitempty"`
-	CanKill             bool                   `protobuf:"varint,18,opt,name=can_kill,json=canKill,proto3" json:"can_kill,omitempty"`
-	unknownFields       protoimpl.UnknownFields
-	sizeCache           protoimpl.SizeCache
+	state                    protoimpl.MessageState `protogen:"open.v1"`
+	DatetimeStarted          string                 `protobuf:"bytes,1,opt,name=datetime_started,json=datetimeStarted,proto3" json:"datetime_started,omitempty"`
+	ActionTitle              string                 `protobuf:"bytes,2,opt,name=action_title,json=actionTitle,proto3" json:"action_title,omitempty"`
+	Output                   string                 `protobuf:"bytes,3,opt,name=output,proto3" json:"output,omitempty"`
+	TimedOut                 bool                   `protobuf:"varint,5,opt,name=timed_out,json=timedOut,proto3" json:"timed_out,omitempty"`
+	ExitCode                 int32                  `protobuf:"varint,6,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"`
+	User                     string                 `protobuf:"bytes,7,opt,name=user,proto3" json:"user,omitempty"`
+	UserClass                string                 `protobuf:"bytes,8,opt,name=user_class,json=userClass,proto3" json:"user_class,omitempty"`
+	ActionIcon               string                 `protobuf:"bytes,9,opt,name=action_icon,json=actionIcon,proto3" json:"action_icon,omitempty"`
+	Tags                     []string               `protobuf:"bytes,10,rep,name=tags,proto3" json:"tags,omitempty"`
+	ExecutionTrackingId      string                 `protobuf:"bytes,11,opt,name=execution_tracking_id,json=executionTrackingId,proto3" json:"execution_tracking_id,omitempty"`
+	DatetimeFinished         string                 `protobuf:"bytes,12,opt,name=datetime_finished,json=datetimeFinished,proto3" json:"datetime_finished,omitempty"`
+	ExecutionStarted         bool                   `protobuf:"varint,14,opt,name=execution_started,json=executionStarted,proto3" json:"execution_started,omitempty"`
+	ExecutionFinished        bool                   `protobuf:"varint,15,opt,name=execution_finished,json=executionFinished,proto3" json:"execution_finished,omitempty"`
+	Blocked                  bool                   `protobuf:"varint,16,opt,name=blocked,proto3" json:"blocked,omitempty"`
+	DatetimeIndex            int64                  `protobuf:"varint,17,opt,name=datetime_index,json=datetimeIndex,proto3" json:"datetime_index,omitempty"`
+	CanKill                  bool                   `protobuf:"varint,18,opt,name=can_kill,json=canKill,proto3" json:"can_kill,omitempty"`
+	DatetimeRateLimitExpires string                 `protobuf:"bytes,19,opt,name=datetime_rate_limit_expires,json=datetimeRateLimitExpires,proto3" json:"datetime_rate_limit_expires,omitempty"` // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
+	BindingId                string                 `protobuf:"bytes,20,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"`                                                  // Binding ID for matching rate limits to action buttons
+	unknownFields            protoimpl.UnknownFields
+	sizeCache                protoimpl.SizeCache
 }
 
 func (x *LogEntry) Reset() {
@@ -1259,13 +1284,6 @@ func (x *LogEntry) GetDatetimeFinished() string {
 	return ""
 }
 
-func (x *LogEntry) GetActionId() string {
-	if x != nil {
-		return x.ActionId
-	}
-	return ""
-}
-
 func (x *LogEntry) GetExecutionStarted() bool {
 	if x != nil {
 		return x.ExecutionStarted
@@ -1301,6 +1319,20 @@ func (x *LogEntry) GetCanKill() bool {
 	return false
 }
 
+func (x *LogEntry) GetDatetimeRateLimitExpires() string {
+	if x != nil {
+		return x.DatetimeRateLimitExpires
+	}
+	return ""
+}
+
+func (x *LogEntry) GetBindingId() string {
+	if x != nil {
+		return x.BindingId
+	}
+	return ""
+}
+
 type GetLogsResponse struct {
 	state          protoimpl.MessageState `protogen:"open.v1"`
 	Logs           []*LogEntry            `protobuf:"bytes,1,rep,name=logs,proto3" json:"logs,omitempty"`
@@ -2089,7 +2121,7 @@ func (x *DumpVarsResponse) GetContents() map[string]string {
 	return nil
 }
 
-type ActionEntityPair struct {
+type DebugBinding struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
 	ActionTitle   string                 `protobuf:"bytes,1,opt,name=action_title,json=actionTitle,proto3" json:"action_title,omitempty"`
 	EntityPrefix  string                 `protobuf:"bytes,2,opt,name=entity_prefix,json=entityPrefix,proto3" json:"entity_prefix,omitempty"`
@@ -2097,20 +2129,20 @@ type ActionEntityPair struct {
 	sizeCache     protoimpl.SizeCache
 }
 
-func (x *ActionEntityPair) Reset() {
-	*x = ActionEntityPair{}
+func (x *DebugBinding) Reset() {
+	*x = DebugBinding{}
 	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[35]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
 
-func (x *ActionEntityPair) String() string {
+func (x *DebugBinding) String() string {
 	return protoimpl.X.MessageStringOf(x)
 }
 
-func (*ActionEntityPair) ProtoMessage() {}
+func (*DebugBinding) ProtoMessage() {}
 
-func (x *ActionEntityPair) ProtoReflect() protoreflect.Message {
+func (x *DebugBinding) ProtoReflect() protoreflect.Message {
 	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[35]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -2122,19 +2154,19 @@ func (x *ActionEntityPair) ProtoReflect() protoreflect.Message {
 	return mi.MessageOf(x)
 }
 
-// Deprecated: Use ActionEntityPair.ProtoReflect.Descriptor instead.
-func (*ActionEntityPair) Descriptor() ([]byte, []int) {
+// Deprecated: Use DebugBinding.ProtoReflect.Descriptor instead.
+func (*DebugBinding) Descriptor() ([]byte, []int) {
 	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{35}
 }
 
-func (x *ActionEntityPair) GetActionTitle() string {
+func (x *DebugBinding) GetActionTitle() string {
 	if x != nil {
 		return x.ActionTitle
 	}
 	return ""
 }
 
-func (x *ActionEntityPair) GetEntityPrefix() string {
+func (x *DebugBinding) GetEntityPrefix() string {
 	if x != nil {
 		return x.EntityPrefix
 	}
@@ -2178,9 +2210,9 @@ func (*DumpPublicIdActionMapRequest) Descriptor() ([]byte, []int) {
 }
 
 type DumpPublicIdActionMapResponse struct {
-	state         protoimpl.MessageState       `protogen:"open.v1"`
-	Alert         string                       `protobuf:"bytes,1,opt,name=alert,proto3" json:"alert,omitempty"`
-	Contents      map[string]*ActionEntityPair `protobuf:"bytes,2,rep,name=contents,proto3" json:"contents,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	state         protoimpl.MessageState   `protogen:"open.v1"`
+	Alert         string                   `protobuf:"bytes,1,opt,name=alert,proto3" json:"alert,omitempty"`
+	Contents      map[string]*DebugBinding `protobuf:"bytes,2,rep,name=contents,proto3" json:"contents,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
 	unknownFields protoimpl.UnknownFields
 	sizeCache     protoimpl.SizeCache
 }
@@ -2222,7 +2254,7 @@ func (x *DumpPublicIdActionMapResponse) GetAlert() string {
 	return ""
 }
 
-func (x *DumpPublicIdActionMapResponse) GetContents() map[string]*ActionEntityPair {
+func (x *DumpPublicIdActionMapResponse) GetContents() map[string]*DebugBinding {
 	if x != nil {
 		return x.Contents
 	}
@@ -3839,7 +3871,7 @@ var File_olivetin_api_v1_olivetin_proto protoreflect.FileDescriptor
 
 const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
 	"\n" +
-	"\x1eolivetin/api/v1/olivetin.proto\x12\x0folivetin.api.v1\"\x81\x02\n" +
+	"\x1eolivetin/api/v1/olivetin.proto\x12\x0folivetin.api.v1\"\xc0\x02\n" +
 	"\x06Action\x12\x1d\n" +
 	"\n" +
 	"binding_id\x18\x01 \x01(\tR\tbindingId\x12\x14\n" +
@@ -3849,7 +3881,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
 	"\targuments\x18\x05 \x03(\v2\x1f.olivetin.api.v1.ActionArgumentR\targuments\x12$\n" +
 	"\x0epopup_on_start\x18\x06 \x01(\tR\fpopupOnStart\x12\x14\n" +
 	"\x05order\x18\a \x01(\x05R\x05order\x12\x18\n" +
-	"\atimeout\x18\b \x01(\x05R\atimeout\"\xea\x02\n" +
+	"\atimeout\x18\b \x01(\x05R\atimeout\x12=\n" +
+	"\x1bdatetime_rate_limit_expires\x18\t \x01(\tR\x18datetimeRateLimitExpires\"\xa2\x03\n" +
 	"\x0eActionArgument\x12\x12\n" +
 	"\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
 	"\x05title\x18\x02 \x01(\tR\x05title\x12\x12\n" +
@@ -3857,7 +3890,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
 	"\rdefault_value\x18\x04 \x01(\tR\fdefaultValue\x12?\n" +
 	"\achoices\x18\x05 \x03(\v2%.olivetin.api.v1.ActionArgumentChoiceR\achoices\x12 \n" +
 	"\vdescription\x18\x06 \x01(\tR\vdescription\x12R\n" +
-	"\vsuggestions\x18\a \x03(\v20.olivetin.api.v1.ActionArgument.SuggestionsEntryR\vsuggestions\x1a>\n" +
+	"\vsuggestions\x18\a \x03(\v20.olivetin.api.v1.ActionArgument.SuggestionsEntryR\vsuggestions\x126\n" +
+	"\x17suggestions_browser_key\x18\b \x01(\tR\x15suggestionsBrowserKey\x1a>\n" +
 	"\x10SuggestionsEntry\x12\x10\n" +
 	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
 	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"B\n" +
@@ -3922,9 +3956,11 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
 	"\x1eStartActionByGetAndWaitRequest\x12\x1b\n" +
 	"\taction_id\x18\x01 \x01(\tR\bactionId\"Y\n" +
 	"\x1fStartActionByGetAndWaitResponse\x126\n" +
-	"\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"3\n" +
+	"\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"T\n" +
 	"\x0eGetLogsRequest\x12!\n" +
-	"\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\"\xc8\x04\n" +
+	"\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\x12\x1f\n" +
+	"\vdate_filter\x18\x02 \x01(\tR\n" +
+	"dateFilter\"\x89\x05\n" +
 	"\bLogEntry\x12)\n" +
 	"\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" +
 	"\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\n" +
@@ -3939,13 +3975,15 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
 	"\x04tags\x18\n" +
 	" \x03(\tR\x04tags\x122\n" +
 	"\x15execution_tracking_id\x18\v \x01(\tR\x13executionTrackingId\x12+\n" +
-	"\x11datetime_finished\x18\f \x01(\tR\x10datetimeFinished\x12\x1b\n" +
-	"\taction_id\x18\r \x01(\tR\bactionId\x12+\n" +
+	"\x11datetime_finished\x18\f \x01(\tR\x10datetimeFinished\x12+\n" +
 	"\x11execution_started\x18\x0e \x01(\bR\x10executionStarted\x12-\n" +
 	"\x12execution_finished\x18\x0f \x01(\bR\x11executionFinished\x12\x18\n" +
 	"\ablocked\x18\x10 \x01(\bR\ablocked\x12%\n" +
 	"\x0edatetime_index\x18\x11 \x01(\x03R\rdatetimeIndex\x12\x19\n" +
-	"\bcan_kill\x18\x12 \x01(\bR\acanKill\"\xca\x01\n" +
+	"\bcan_kill\x18\x12 \x01(\bR\acanKill\x12=\n" +
+	"\x1bdatetime_rate_limit_expires\x18\x13 \x01(\tR\x18datetimeRateLimitExpires\x12\x1d\n" +
+	"\n" +
+	"binding_id\x18\x14 \x01(\tR\tbindingId\"\xca\x01\n" +
 	"\x0fGetLogsResponse\x12-\n" +
 	"\x04logs\x18\x01 \x03(\v2\x19.olivetin.api.v1.LogEntryR\x04logs\x12'\n" +
 	"\x0fcount_remaining\x18\x02 \x01(\x03R\x0ecountRemaining\x12\x1b\n" +
@@ -3997,17 +4035,17 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
 	"\bcontents\x18\x02 \x03(\v2/.olivetin.api.v1.DumpVarsResponse.ContentsEntryR\bcontents\x1a;\n" +
 	"\rContentsEntry\x12\x10\n" +
 	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
-	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"Z\n" +
-	"\x10ActionEntityPair\x12!\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"V\n" +
+	"\fDebugBinding\x12!\n" +
 	"\faction_title\x18\x01 \x01(\tR\vactionTitle\x12#\n" +
 	"\rentity_prefix\x18\x02 \x01(\tR\fentityPrefix\"\x1e\n" +
-	"\x1cDumpPublicIdActionMapRequest\"\xef\x01\n" +
+	"\x1cDumpPublicIdActionMapRequest\"\xeb\x01\n" +
 	"\x1dDumpPublicIdActionMapResponse\x12\x14\n" +
 	"\x05alert\x18\x01 \x01(\tR\x05alert\x12X\n" +
-	"\bcontents\x18\x02 \x03(\v2<.olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntryR\bcontents\x1a^\n" +
+	"\bcontents\x18\x02 \x03(\v2<.olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntryR\bcontents\x1aZ\n" +
 	"\rContentsEntry\x12\x10\n" +
-	"\x03key\x18\x01 \x01(\tR\x03key\x127\n" +
-	"\x05value\x18\x02 \x01(\v2!.olivetin.api.v1.ActionEntityPairR\x05value:\x028\x01\"\x12\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x123\n" +
+	"\x05value\x18\x02 \x01(\v2\x1d.olivetin.api.v1.DebugBindingR\x05value:\x028\x01\"\x12\n" +
 	"\x10GetReadyzRequest\"+\n" +
 	"\x11GetReadyzResponse\x12\x16\n" +
 	"\x06status\x18\x01 \x01(\tR\x06status\"\x14\n" +
@@ -4181,7 +4219,7 @@ var file_olivetin_api_v1_olivetin_proto_goTypes = []any{
 	(*SosReportResponse)(nil),               // 32: olivetin.api.v1.SosReportResponse
 	(*DumpVarsRequest)(nil),                 // 33: olivetin.api.v1.DumpVarsRequest
 	(*DumpVarsResponse)(nil),                // 34: olivetin.api.v1.DumpVarsResponse
-	(*ActionEntityPair)(nil),                // 35: olivetin.api.v1.ActionEntityPair
+	(*DebugBinding)(nil),                    // 35: olivetin.api.v1.DebugBinding
 	(*DumpPublicIdActionMapRequest)(nil),    // 36: olivetin.api.v1.DumpPublicIdActionMapRequest
 	(*DumpPublicIdActionMapResponse)(nil),   // 37: olivetin.api.v1.DumpPublicIdActionMapResponse
 	(*GetReadyzRequest)(nil),                // 38: olivetin.api.v1.GetReadyzRequest
@@ -4250,7 +4288,7 @@ var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
 	0,  // 27: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action
 	65, // 28: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition
 	3,  // 29: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity
-	35, // 30: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.ActionEntityPair
+	35, // 30: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding
 	6,  // 31: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest
 	9,  // 32: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest
 	12, // 33: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest

+ 4378 - 0
service/gen/olivetin/api/v1/olivetin.pb.go.orig

@@ -0,0 +1,4378 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.11
+// 	protoc        (unknown)
+// source: olivetin/api/v1/olivetin.proto
+
+package apiv1
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type Action struct {
+	state                    protoimpl.MessageState `protogen:"open.v1"`
+	BindingId                string                 `protobuf:"bytes,1,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"`
+	Title                    string                 `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
+	Icon                     string                 `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"`
+	CanExec                  bool                   `protobuf:"varint,4,opt,name=can_exec,json=canExec,proto3" json:"can_exec,omitempty"`
+	Arguments                []*ActionArgument      `protobuf:"bytes,5,rep,name=arguments,proto3" json:"arguments,omitempty"`
+	PopupOnStart             string                 `protobuf:"bytes,6,opt,name=popup_on_start,json=popupOnStart,proto3" json:"popup_on_start,omitempty"`
+	Order                    int32                  `protobuf:"varint,7,opt,name=order,proto3" json:"order,omitempty"`
+	Timeout                  int32                  `protobuf:"varint,8,opt,name=timeout,proto3" json:"timeout,omitempty"`
+	DatetimeRateLimitExpires string                 `protobuf:"bytes,9,opt,name=datetime_rate_limit_expires,json=datetimeRateLimitExpires,proto3" json:"datetime_rate_limit_expires,omitempty"` // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
+	unknownFields            protoimpl.UnknownFields
+	sizeCache                protoimpl.SizeCache
+}
+
+func (x *Action) Reset() {
+	*x = Action{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Action) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Action) ProtoMessage() {}
+
+func (x *Action) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[0]
+	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 Action.ProtoReflect.Descriptor instead.
+func (*Action) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Action) GetBindingId() string {
+	if x != nil {
+		return x.BindingId
+	}
+	return ""
+}
+
+func (x *Action) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+func (x *Action) GetIcon() string {
+	if x != nil {
+		return x.Icon
+	}
+	return ""
+}
+
+func (x *Action) GetCanExec() bool {
+	if x != nil {
+		return x.CanExec
+	}
+	return false
+}
+
+func (x *Action) GetArguments() []*ActionArgument {
+	if x != nil {
+		return x.Arguments
+	}
+	return nil
+}
+
+func (x *Action) GetPopupOnStart() string {
+	if x != nil {
+		return x.PopupOnStart
+	}
+	return ""
+}
+
+func (x *Action) GetOrder() int32 {
+	if x != nil {
+		return x.Order
+	}
+	return 0
+}
+
+func (x *Action) GetTimeout() int32 {
+	if x != nil {
+		return x.Timeout
+	}
+	return 0
+}
+
+func (x *Action) GetDatetimeRateLimitExpires() string {
+	if x != nil {
+		return x.DatetimeRateLimitExpires
+	}
+	return ""
+}
+
+type ActionArgument struct {
+	state                 protoimpl.MessageState  `protogen:"open.v1"`
+	Name                  string                  `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Title                 string                  `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
+	Type                  string                  `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
+	DefaultValue          string                  `protobuf:"bytes,4,opt,name=default_value,json=defaultValue,proto3" json:"default_value,omitempty"`
+	Choices               []*ActionArgumentChoice `protobuf:"bytes,5,rep,name=choices,proto3" json:"choices,omitempty"`
+	Description           string                  `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"`
+	Suggestions           map[string]string       `protobuf:"bytes,7,rep,name=suggestions,proto3" json:"suggestions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	SuggestionsBrowserKey string                  `protobuf:"bytes,8,opt,name=suggestions_browser_key,json=suggestionsBrowserKey,proto3" json:"suggestions_browser_key,omitempty"`
+	unknownFields         protoimpl.UnknownFields
+	sizeCache             protoimpl.SizeCache
+}
+
+func (x *ActionArgument) Reset() {
+	*x = ActionArgument{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[1]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ActionArgument) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ActionArgument) ProtoMessage() {}
+
+func (x *ActionArgument) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[1]
+	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 ActionArgument.ProtoReflect.Descriptor instead.
+func (*ActionArgument) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *ActionArgument) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *ActionArgument) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+func (x *ActionArgument) GetType() string {
+	if x != nil {
+		return x.Type
+	}
+	return ""
+}
+
+func (x *ActionArgument) GetDefaultValue() string {
+	if x != nil {
+		return x.DefaultValue
+	}
+	return ""
+}
+
+func (x *ActionArgument) GetChoices() []*ActionArgumentChoice {
+	if x != nil {
+		return x.Choices
+	}
+	return nil
+}
+
+func (x *ActionArgument) GetDescription() string {
+	if x != nil {
+		return x.Description
+	}
+	return ""
+}
+
+func (x *ActionArgument) GetSuggestions() map[string]string {
+	if x != nil {
+		return x.Suggestions
+	}
+	return nil
+}
+
+func (x *ActionArgument) GetSuggestionsBrowserKey() string {
+	if x != nil {
+		return x.SuggestionsBrowserKey
+	}
+	return ""
+}
+
+type ActionArgumentChoice struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Value         string                 `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
+	Title         string                 `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *ActionArgumentChoice) Reset() {
+	*x = ActionArgumentChoice{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[2]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ActionArgumentChoice) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ActionArgumentChoice) ProtoMessage() {}
+
+func (x *ActionArgumentChoice) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[2]
+	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 ActionArgumentChoice.ProtoReflect.Descriptor instead.
+func (*ActionArgumentChoice) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *ActionArgumentChoice) GetValue() string {
+	if x != nil {
+		return x.Value
+	}
+	return ""
+}
+
+func (x *ActionArgumentChoice) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+type Entity struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Title         string                 `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
+	UniqueKey     string                 `protobuf:"bytes,2,opt,name=unique_key,json=uniqueKey,proto3" json:"unique_key,omitempty"`
+	Type          string                 `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
+	Directories   []string               `protobuf:"bytes,4,rep,name=directories,proto3" json:"directories,omitempty"`
+	Fields        map[string]string      `protobuf:"bytes,5,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Entity) Reset() {
+	*x = Entity{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[3]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Entity) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Entity) ProtoMessage() {}
+
+func (x *Entity) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[3]
+	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 Entity.ProtoReflect.Descriptor instead.
+func (*Entity) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *Entity) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+func (x *Entity) GetUniqueKey() string {
+	if x != nil {
+		return x.UniqueKey
+	}
+	return ""
+}
+
+func (x *Entity) GetType() string {
+	if x != nil {
+		return x.Type
+	}
+	return ""
+}
+
+func (x *Entity) GetDirectories() []string {
+	if x != nil {
+		return x.Directories
+	}
+	return nil
+}
+
+func (x *Entity) GetFields() map[string]string {
+	if x != nil {
+		return x.Fields
+	}
+	return nil
+}
+
+type GetDashboardResponse struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Title         string                 `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
+	Dashboard     *Dashboard             `protobuf:"bytes,4,opt,name=dashboard,proto3" json:"dashboard,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GetDashboardResponse) Reset() {
+	*x = GetDashboardResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[4]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetDashboardResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetDashboardResponse) ProtoMessage() {}
+
+func (x *GetDashboardResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[4]
+	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 GetDashboardResponse.ProtoReflect.Descriptor instead.
+func (*GetDashboardResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *GetDashboardResponse) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+func (x *GetDashboardResponse) GetDashboard() *Dashboard {
+	if x != nil {
+		return x.Dashboard
+	}
+	return nil
+}
+
+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
+}
+
+func (x *EffectivePolicy) Reset() {
+	*x = EffectivePolicy{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[5]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *EffectivePolicy) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EffectivePolicy) ProtoMessage() {}
+
+func (x *EffectivePolicy) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[5]
+	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 EffectivePolicy.ProtoReflect.Descriptor instead.
+func (*EffectivePolicy) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *EffectivePolicy) GetShowDiagnostics() bool {
+	if x != nil {
+		return x.ShowDiagnostics
+	}
+	return false
+}
+
+func (x *EffectivePolicy) GetShowLogList() bool {
+	if x != nil {
+		return x.ShowLogList
+	}
+	return false
+}
+
+type GetDashboardRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Title         string                 `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
+	EntityType    string                 `protobuf:"bytes,2,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"`
+	EntityKey     string                 `protobuf:"bytes,3,opt,name=entity_key,json=entityKey,proto3" json:"entity_key,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GetDashboardRequest) Reset() {
+	*x = GetDashboardRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[6]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetDashboardRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetDashboardRequest) ProtoMessage() {}
+
+func (x *GetDashboardRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[6]
+	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 GetDashboardRequest.ProtoReflect.Descriptor instead.
+func (*GetDashboardRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *GetDashboardRequest) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+func (x *GetDashboardRequest) GetEntityType() string {
+	if x != nil {
+		return x.EntityType
+	}
+	return ""
+}
+
+func (x *GetDashboardRequest) GetEntityKey() string {
+	if x != nil {
+		return x.EntityKey
+	}
+	return ""
+}
+
+type Dashboard struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Title         string                 `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
+	Contents      []*DashboardComponent  `protobuf:"bytes,2,rep,name=contents,proto3" json:"contents,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Dashboard) Reset() {
+	*x = Dashboard{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[7]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Dashboard) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Dashboard) ProtoMessage() {}
+
+func (x *Dashboard) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[7]
+	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 Dashboard.ProtoReflect.Descriptor instead.
+func (*Dashboard) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *Dashboard) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+func (x *Dashboard) GetContents() []*DashboardComponent {
+	if x != nil {
+		return x.Contents
+	}
+	return nil
+}
+
+type DashboardComponent struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Title         string                 `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
+	Type          string                 `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
+	Contents      []*DashboardComponent  `protobuf:"bytes,3,rep,name=contents,proto3" json:"contents,omitempty"`
+	Icon          string                 `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"`
+	CssClass      string                 `protobuf:"bytes,5,opt,name=css_class,json=cssClass,proto3" json:"css_class,omitempty"`
+	Action        *Action                `protobuf:"bytes,6,opt,name=action,proto3" json:"action,omitempty"`
+	EntityType    string                 `protobuf:"bytes,7,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"`
+	EntityKey     string                 `protobuf:"bytes,8,opt,name=entity_key,json=entityKey,proto3" json:"entity_key,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *DashboardComponent) Reset() {
+	*x = DashboardComponent{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[8]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *DashboardComponent) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DashboardComponent) ProtoMessage() {}
+
+func (x *DashboardComponent) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[8]
+	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 DashboardComponent.ProtoReflect.Descriptor instead.
+func (*DashboardComponent) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *DashboardComponent) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+func (x *DashboardComponent) GetType() string {
+	if x != nil {
+		return x.Type
+	}
+	return ""
+}
+
+func (x *DashboardComponent) GetContents() []*DashboardComponent {
+	if x != nil {
+		return x.Contents
+	}
+	return nil
+}
+
+func (x *DashboardComponent) GetIcon() string {
+	if x != nil {
+		return x.Icon
+	}
+	return ""
+}
+
+func (x *DashboardComponent) GetCssClass() string {
+	if x != nil {
+		return x.CssClass
+	}
+	return ""
+}
+
+func (x *DashboardComponent) GetAction() *Action {
+	if x != nil {
+		return x.Action
+	}
+	return nil
+}
+
+func (x *DashboardComponent) GetEntityType() string {
+	if x != nil {
+		return x.EntityType
+	}
+	return ""
+}
+
+func (x *DashboardComponent) GetEntityKey() string {
+	if x != nil {
+		return x.EntityKey
+	}
+	return ""
+}
+
+type StartActionRequest struct {
+	state            protoimpl.MessageState `protogen:"open.v1"`
+	BindingId        string                 `protobuf:"bytes,1,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"`
+	Arguments        []*StartActionArgument `protobuf:"bytes,2,rep,name=arguments,proto3" json:"arguments,omitempty"`
+	UniqueTrackingId string                 `protobuf:"bytes,3,opt,name=unique_tracking_id,json=uniqueTrackingId,proto3" json:"unique_tracking_id,omitempty"`
+	unknownFields    protoimpl.UnknownFields
+	sizeCache        protoimpl.SizeCache
+}
+
+func (x *StartActionRequest) Reset() {
+	*x = StartActionRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[9]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *StartActionRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StartActionRequest) ProtoMessage() {}
+
+func (x *StartActionRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[9]
+	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 StartActionRequest.ProtoReflect.Descriptor instead.
+func (*StartActionRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *StartActionRequest) GetBindingId() string {
+	if x != nil {
+		return x.BindingId
+	}
+	return ""
+}
+
+func (x *StartActionRequest) GetArguments() []*StartActionArgument {
+	if x != nil {
+		return x.Arguments
+	}
+	return nil
+}
+
+func (x *StartActionRequest) GetUniqueTrackingId() string {
+	if x != nil {
+		return x.UniqueTrackingId
+	}
+	return ""
+}
+
+type StartActionArgument struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Name          string                 `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Value         string                 `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *StartActionArgument) Reset() {
+	*x = StartActionArgument{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[10]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *StartActionArgument) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StartActionArgument) ProtoMessage() {}
+
+func (x *StartActionArgument) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[10]
+	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 StartActionArgument.ProtoReflect.Descriptor instead.
+func (*StartActionArgument) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *StartActionArgument) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *StartActionArgument) GetValue() string {
+	if x != nil {
+		return x.Value
+	}
+	return ""
+}
+
+type StartActionResponse struct {
+	state               protoimpl.MessageState `protogen:"open.v1"`
+	ExecutionTrackingId string                 `protobuf:"bytes,2,opt,name=execution_tracking_id,json=executionTrackingId,proto3" json:"execution_tracking_id,omitempty"`
+	unknownFields       protoimpl.UnknownFields
+	sizeCache           protoimpl.SizeCache
+}
+
+func (x *StartActionResponse) Reset() {
+	*x = StartActionResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[11]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *StartActionResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StartActionResponse) ProtoMessage() {}
+
+func (x *StartActionResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[11]
+	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 StartActionResponse.ProtoReflect.Descriptor instead.
+func (*StartActionResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *StartActionResponse) GetExecutionTrackingId() string {
+	if x != nil {
+		return x.ExecutionTrackingId
+	}
+	return ""
+}
+
+type StartActionAndWaitRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	ActionId      string                 `protobuf:"bytes,1,opt,name=action_id,json=actionId,proto3" json:"action_id,omitempty"`
+	Arguments     []*StartActionArgument `protobuf:"bytes,2,rep,name=arguments,proto3" json:"arguments,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *StartActionAndWaitRequest) Reset() {
+	*x = StartActionAndWaitRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[12]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *StartActionAndWaitRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StartActionAndWaitRequest) ProtoMessage() {}
+
+func (x *StartActionAndWaitRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[12]
+	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 StartActionAndWaitRequest.ProtoReflect.Descriptor instead.
+func (*StartActionAndWaitRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *StartActionAndWaitRequest) GetActionId() string {
+	if x != nil {
+		return x.ActionId
+	}
+	return ""
+}
+
+func (x *StartActionAndWaitRequest) GetArguments() []*StartActionArgument {
+	if x != nil {
+		return x.Arguments
+	}
+	return nil
+}
+
+type StartActionAndWaitResponse struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	LogEntry      *LogEntry              `protobuf:"bytes,1,opt,name=log_entry,json=logEntry,proto3" json:"log_entry,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *StartActionAndWaitResponse) Reset() {
+	*x = StartActionAndWaitResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[13]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *StartActionAndWaitResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StartActionAndWaitResponse) ProtoMessage() {}
+
+func (x *StartActionAndWaitResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[13]
+	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 StartActionAndWaitResponse.ProtoReflect.Descriptor instead.
+func (*StartActionAndWaitResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{13}
+}
+
+func (x *StartActionAndWaitResponse) GetLogEntry() *LogEntry {
+	if x != nil {
+		return x.LogEntry
+	}
+	return nil
+}
+
+type StartActionByGetRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	ActionId      string                 `protobuf:"bytes,1,opt,name=action_id,json=actionId,proto3" json:"action_id,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *StartActionByGetRequest) Reset() {
+	*x = StartActionByGetRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[14]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *StartActionByGetRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StartActionByGetRequest) ProtoMessage() {}
+
+func (x *StartActionByGetRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[14]
+	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 StartActionByGetRequest.ProtoReflect.Descriptor instead.
+func (*StartActionByGetRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{14}
+}
+
+func (x *StartActionByGetRequest) GetActionId() string {
+	if x != nil {
+		return x.ActionId
+	}
+	return ""
+}
+
+type StartActionByGetResponse struct {
+	state               protoimpl.MessageState `protogen:"open.v1"`
+	ExecutionTrackingId string                 `protobuf:"bytes,2,opt,name=execution_tracking_id,json=executionTrackingId,proto3" json:"execution_tracking_id,omitempty"`
+	unknownFields       protoimpl.UnknownFields
+	sizeCache           protoimpl.SizeCache
+}
+
+func (x *StartActionByGetResponse) Reset() {
+	*x = StartActionByGetResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[15]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *StartActionByGetResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StartActionByGetResponse) ProtoMessage() {}
+
+func (x *StartActionByGetResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[15]
+	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 StartActionByGetResponse.ProtoReflect.Descriptor instead.
+func (*StartActionByGetResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{15}
+}
+
+func (x *StartActionByGetResponse) GetExecutionTrackingId() string {
+	if x != nil {
+		return x.ExecutionTrackingId
+	}
+	return ""
+}
+
+type StartActionByGetAndWaitRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	ActionId      string                 `protobuf:"bytes,1,opt,name=action_id,json=actionId,proto3" json:"action_id,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *StartActionByGetAndWaitRequest) Reset() {
+	*x = StartActionByGetAndWaitRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[16]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *StartActionByGetAndWaitRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StartActionByGetAndWaitRequest) ProtoMessage() {}
+
+func (x *StartActionByGetAndWaitRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[16]
+	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 StartActionByGetAndWaitRequest.ProtoReflect.Descriptor instead.
+func (*StartActionByGetAndWaitRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{16}
+}
+
+func (x *StartActionByGetAndWaitRequest) GetActionId() string {
+	if x != nil {
+		return x.ActionId
+	}
+	return ""
+}
+
+type StartActionByGetAndWaitResponse struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	LogEntry      *LogEntry              `protobuf:"bytes,1,opt,name=log_entry,json=logEntry,proto3" json:"log_entry,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *StartActionByGetAndWaitResponse) Reset() {
+	*x = StartActionByGetAndWaitResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[17]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *StartActionByGetAndWaitResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StartActionByGetAndWaitResponse) ProtoMessage() {}
+
+func (x *StartActionByGetAndWaitResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[17]
+	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 StartActionByGetAndWaitResponse.ProtoReflect.Descriptor instead.
+func (*StartActionByGetAndWaitResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{17}
+}
+
+func (x *StartActionByGetAndWaitResponse) GetLogEntry() *LogEntry {
+	if x != nil {
+		return x.LogEntry
+	}
+	return nil
+}
+
+type GetLogsRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	StartOffset   int64                  `protobuf:"varint,1,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"`
+	DateFilter    string                 `protobuf:"bytes,2,opt,name=date_filter,json=dateFilter,proto3" json:"date_filter,omitempty"` // Optional date filter in YYYY-MM-DD format
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GetLogsRequest) Reset() {
+	*x = GetLogsRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[18]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetLogsRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetLogsRequest) ProtoMessage() {}
+
+func (x *GetLogsRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[18]
+	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 GetLogsRequest.ProtoReflect.Descriptor instead.
+func (*GetLogsRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{18}
+}
+
+func (x *GetLogsRequest) GetStartOffset() int64 {
+	if x != nil {
+		return x.StartOffset
+	}
+	return 0
+}
+
+func (x *GetLogsRequest) GetDateFilter() string {
+	if x != nil {
+		return x.DateFilter
+	}
+	return ""
+}
+
+type LogEntry struct {
+	state                    protoimpl.MessageState `protogen:"open.v1"`
+	DatetimeStarted          string                 `protobuf:"bytes,1,opt,name=datetime_started,json=datetimeStarted,proto3" json:"datetime_started,omitempty"`
+	ActionTitle              string                 `protobuf:"bytes,2,opt,name=action_title,json=actionTitle,proto3" json:"action_title,omitempty"`
+	Output                   string                 `protobuf:"bytes,3,opt,name=output,proto3" json:"output,omitempty"`
+	TimedOut                 bool                   `protobuf:"varint,5,opt,name=timed_out,json=timedOut,proto3" json:"timed_out,omitempty"`
+	ExitCode                 int32                  `protobuf:"varint,6,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"`
+	User                     string                 `protobuf:"bytes,7,opt,name=user,proto3" json:"user,omitempty"`
+	UserClass                string                 `protobuf:"bytes,8,opt,name=user_class,json=userClass,proto3" json:"user_class,omitempty"`
+	ActionIcon               string                 `protobuf:"bytes,9,opt,name=action_icon,json=actionIcon,proto3" json:"action_icon,omitempty"`
+	Tags                     []string               `protobuf:"bytes,10,rep,name=tags,proto3" json:"tags,omitempty"`
+	ExecutionTrackingId      string                 `protobuf:"bytes,11,opt,name=execution_tracking_id,json=executionTrackingId,proto3" json:"execution_tracking_id,omitempty"`
+	DatetimeFinished         string                 `protobuf:"bytes,12,opt,name=datetime_finished,json=datetimeFinished,proto3" json:"datetime_finished,omitempty"`
+	ExecutionStarted         bool                   `protobuf:"varint,14,opt,name=execution_started,json=executionStarted,proto3" json:"execution_started,omitempty"`
+	ExecutionFinished        bool                   `protobuf:"varint,15,opt,name=execution_finished,json=executionFinished,proto3" json:"execution_finished,omitempty"`
+	Blocked                  bool                   `protobuf:"varint,16,opt,name=blocked,proto3" json:"blocked,omitempty"`
+	DatetimeIndex            int64                  `protobuf:"varint,17,opt,name=datetime_index,json=datetimeIndex,proto3" json:"datetime_index,omitempty"`
+	CanKill                  bool                   `protobuf:"varint,18,opt,name=can_kill,json=canKill,proto3" json:"can_kill,omitempty"`
+	DatetimeRateLimitExpires string                 `protobuf:"bytes,19,opt,name=datetime_rate_limit_expires,json=datetimeRateLimitExpires,proto3" json:"datetime_rate_limit_expires,omitempty"` // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
+	BindingId                string                 `protobuf:"bytes,20,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"`                                                  // Binding ID for matching rate limits to action buttons
+	unknownFields            protoimpl.UnknownFields
+	sizeCache                protoimpl.SizeCache
+}
+
+func (x *LogEntry) Reset() {
+	*x = LogEntry{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[19]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *LogEntry) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LogEntry) ProtoMessage() {}
+
+func (x *LogEntry) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[19]
+	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 LogEntry.ProtoReflect.Descriptor instead.
+func (*LogEntry) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{19}
+}
+
+func (x *LogEntry) GetDatetimeStarted() string {
+	if x != nil {
+		return x.DatetimeStarted
+	}
+	return ""
+}
+
+func (x *LogEntry) GetActionTitle() string {
+	if x != nil {
+		return x.ActionTitle
+	}
+	return ""
+}
+
+func (x *LogEntry) GetOutput() string {
+	if x != nil {
+		return x.Output
+	}
+	return ""
+}
+
+func (x *LogEntry) GetTimedOut() bool {
+	if x != nil {
+		return x.TimedOut
+	}
+	return false
+}
+
+func (x *LogEntry) GetExitCode() int32 {
+	if x != nil {
+		return x.ExitCode
+	}
+	return 0
+}
+
+func (x *LogEntry) GetUser() string {
+	if x != nil {
+		return x.User
+	}
+	return ""
+}
+
+func (x *LogEntry) GetUserClass() string {
+	if x != nil {
+		return x.UserClass
+	}
+	return ""
+}
+
+func (x *LogEntry) GetActionIcon() string {
+	if x != nil {
+		return x.ActionIcon
+	}
+	return ""
+}
+
+func (x *LogEntry) GetTags() []string {
+	if x != nil {
+		return x.Tags
+	}
+	return nil
+}
+
+func (x *LogEntry) GetExecutionTrackingId() string {
+	if x != nil {
+		return x.ExecutionTrackingId
+	}
+	return ""
+}
+
+func (x *LogEntry) GetDatetimeFinished() string {
+	if x != nil {
+		return x.DatetimeFinished
+	}
+	return ""
+}
+
+func (x *LogEntry) GetExecutionStarted() bool {
+	if x != nil {
+		return x.ExecutionStarted
+	}
+	return false
+}
+
+func (x *LogEntry) GetExecutionFinished() bool {
+	if x != nil {
+		return x.ExecutionFinished
+	}
+	return false
+}
+
+func (x *LogEntry) GetBlocked() bool {
+	if x != nil {
+		return x.Blocked
+	}
+	return false
+}
+
+func (x *LogEntry) GetDatetimeIndex() int64 {
+	if x != nil {
+		return x.DatetimeIndex
+	}
+	return 0
+}
+
+func (x *LogEntry) GetCanKill() bool {
+	if x != nil {
+		return x.CanKill
+	}
+	return false
+}
+
+func (x *LogEntry) GetDatetimeRateLimitExpires() string {
+	if x != nil {
+		return x.DatetimeRateLimitExpires
+	}
+	return ""
+}
+
+func (x *LogEntry) GetBindingId() string {
+	if x != nil {
+		return x.BindingId
+	}
+	return ""
+}
+
+type GetLogsResponse struct {
+	state          protoimpl.MessageState `protogen:"open.v1"`
+	Logs           []*LogEntry            `protobuf:"bytes,1,rep,name=logs,proto3" json:"logs,omitempty"`
+	CountRemaining int64                  `protobuf:"varint,2,opt,name=count_remaining,json=countRemaining,proto3" json:"count_remaining,omitempty"`
+	PageSize       int64                  `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
+	TotalCount     int64                  `protobuf:"varint,4,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"`
+	StartOffset    int64                  `protobuf:"varint,5,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"`
+	unknownFields  protoimpl.UnknownFields
+	sizeCache      protoimpl.SizeCache
+}
+
+func (x *GetLogsResponse) Reset() {
+	*x = GetLogsResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[20]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetLogsResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetLogsResponse) ProtoMessage() {}
+
+func (x *GetLogsResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[20]
+	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 GetLogsResponse.ProtoReflect.Descriptor instead.
+func (*GetLogsResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{20}
+}
+
+func (x *GetLogsResponse) GetLogs() []*LogEntry {
+	if x != nil {
+		return x.Logs
+	}
+	return nil
+}
+
+func (x *GetLogsResponse) GetCountRemaining() int64 {
+	if x != nil {
+		return x.CountRemaining
+	}
+	return 0
+}
+
+func (x *GetLogsResponse) GetPageSize() int64 {
+	if x != nil {
+		return x.PageSize
+	}
+	return 0
+}
+
+func (x *GetLogsResponse) GetTotalCount() int64 {
+	if x != nil {
+		return x.TotalCount
+	}
+	return 0
+}
+
+func (x *GetLogsResponse) GetStartOffset() int64 {
+	if x != nil {
+		return x.StartOffset
+	}
+	return 0
+}
+
+type GetActionLogsRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	ActionId      string                 `protobuf:"bytes,1,opt,name=action_id,json=actionId,proto3" json:"action_id,omitempty"`
+	StartOffset   int64                  `protobuf:"varint,2,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GetActionLogsRequest) Reset() {
+	*x = GetActionLogsRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[21]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetActionLogsRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetActionLogsRequest) ProtoMessage() {}
+
+func (x *GetActionLogsRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[21]
+	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 GetActionLogsRequest.ProtoReflect.Descriptor instead.
+func (*GetActionLogsRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{21}
+}
+
+func (x *GetActionLogsRequest) GetActionId() string {
+	if x != nil {
+		return x.ActionId
+	}
+	return ""
+}
+
+func (x *GetActionLogsRequest) GetStartOffset() int64 {
+	if x != nil {
+		return x.StartOffset
+	}
+	return 0
+}
+
+type GetActionLogsResponse struct {
+	state          protoimpl.MessageState `protogen:"open.v1"`
+	Logs           []*LogEntry            `protobuf:"bytes,1,rep,name=logs,proto3" json:"logs,omitempty"`
+	CountRemaining int64                  `protobuf:"varint,2,opt,name=count_remaining,json=countRemaining,proto3" json:"count_remaining,omitempty"`
+	PageSize       int64                  `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
+	TotalCount     int64                  `protobuf:"varint,4,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"`
+	StartOffset    int64                  `protobuf:"varint,5,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"`
+	unknownFields  protoimpl.UnknownFields
+	sizeCache      protoimpl.SizeCache
+}
+
+func (x *GetActionLogsResponse) Reset() {
+	*x = GetActionLogsResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[22]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetActionLogsResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetActionLogsResponse) ProtoMessage() {}
+
+func (x *GetActionLogsResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[22]
+	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 GetActionLogsResponse.ProtoReflect.Descriptor instead.
+func (*GetActionLogsResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{22}
+}
+
+func (x *GetActionLogsResponse) GetLogs() []*LogEntry {
+	if x != nil {
+		return x.Logs
+	}
+	return nil
+}
+
+func (x *GetActionLogsResponse) GetCountRemaining() int64 {
+	if x != nil {
+		return x.CountRemaining
+	}
+	return 0
+}
+
+func (x *GetActionLogsResponse) GetPageSize() int64 {
+	if x != nil {
+		return x.PageSize
+	}
+	return 0
+}
+
+func (x *GetActionLogsResponse) GetTotalCount() int64 {
+	if x != nil {
+		return x.TotalCount
+	}
+	return 0
+}
+
+func (x *GetActionLogsResponse) GetStartOffset() int64 {
+	if x != nil {
+		return x.StartOffset
+	}
+	return 0
+}
+
+type ValidateArgumentTypeRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Value         string                 `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
+	Type          string                 `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
+	BindingId     string                 `protobuf:"bytes,3,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"`
+	ArgumentName  string                 `protobuf:"bytes,4,opt,name=argument_name,json=argumentName,proto3" json:"argument_name,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *ValidateArgumentTypeRequest) Reset() {
+	*x = ValidateArgumentTypeRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[23]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ValidateArgumentTypeRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ValidateArgumentTypeRequest) ProtoMessage() {}
+
+func (x *ValidateArgumentTypeRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[23]
+	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 ValidateArgumentTypeRequest.ProtoReflect.Descriptor instead.
+func (*ValidateArgumentTypeRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{23}
+}
+
+func (x *ValidateArgumentTypeRequest) GetValue() string {
+	if x != nil {
+		return x.Value
+	}
+	return ""
+}
+
+func (x *ValidateArgumentTypeRequest) GetType() string {
+	if x != nil {
+		return x.Type
+	}
+	return ""
+}
+
+func (x *ValidateArgumentTypeRequest) GetBindingId() string {
+	if x != nil {
+		return x.BindingId
+	}
+	return ""
+}
+
+func (x *ValidateArgumentTypeRequest) GetArgumentName() string {
+	if x != nil {
+		return x.ArgumentName
+	}
+	return ""
+}
+
+type ValidateArgumentTypeResponse struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Valid         bool                   `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"`
+	Description   string                 `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *ValidateArgumentTypeResponse) Reset() {
+	*x = ValidateArgumentTypeResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[24]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ValidateArgumentTypeResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ValidateArgumentTypeResponse) ProtoMessage() {}
+
+func (x *ValidateArgumentTypeResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[24]
+	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 ValidateArgumentTypeResponse.ProtoReflect.Descriptor instead.
+func (*ValidateArgumentTypeResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{24}
+}
+
+func (x *ValidateArgumentTypeResponse) GetValid() bool {
+	if x != nil {
+		return x.Valid
+	}
+	return false
+}
+
+func (x *ValidateArgumentTypeResponse) GetDescription() string {
+	if x != nil {
+		return x.Description
+	}
+	return ""
+}
+
+type WatchExecutionRequest struct {
+	state               protoimpl.MessageState `protogen:"open.v1"`
+	ExecutionTrackingId string                 `protobuf:"bytes,1,opt,name=execution_tracking_id,json=executionTrackingId,proto3" json:"execution_tracking_id,omitempty"`
+	unknownFields       protoimpl.UnknownFields
+	sizeCache           protoimpl.SizeCache
+}
+
+func (x *WatchExecutionRequest) Reset() {
+	*x = WatchExecutionRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[25]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *WatchExecutionRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*WatchExecutionRequest) ProtoMessage() {}
+
+func (x *WatchExecutionRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[25]
+	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 WatchExecutionRequest.ProtoReflect.Descriptor instead.
+func (*WatchExecutionRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{25}
+}
+
+func (x *WatchExecutionRequest) GetExecutionTrackingId() string {
+	if x != nil {
+		return x.ExecutionTrackingId
+	}
+	return ""
+}
+
+type WatchExecutionUpdate struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Update        string                 `protobuf:"bytes,1,opt,name=update,proto3" json:"update,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *WatchExecutionUpdate) Reset() {
+	*x = WatchExecutionUpdate{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[26]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *WatchExecutionUpdate) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*WatchExecutionUpdate) ProtoMessage() {}
+
+func (x *WatchExecutionUpdate) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[26]
+	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 WatchExecutionUpdate.ProtoReflect.Descriptor instead.
+func (*WatchExecutionUpdate) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{26}
+}
+
+func (x *WatchExecutionUpdate) GetUpdate() string {
+	if x != nil {
+		return x.Update
+	}
+	return ""
+}
+
+type ExecutionStatusRequest struct {
+	state               protoimpl.MessageState `protogen:"open.v1"`
+	ExecutionTrackingId string                 `protobuf:"bytes,1,opt,name=execution_tracking_id,json=executionTrackingId,proto3" json:"execution_tracking_id,omitempty"`
+	ActionId            string                 `protobuf:"bytes,2,opt,name=action_id,json=actionId,proto3" json:"action_id,omitempty"`
+	unknownFields       protoimpl.UnknownFields
+	sizeCache           protoimpl.SizeCache
+}
+
+func (x *ExecutionStatusRequest) Reset() {
+	*x = ExecutionStatusRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[27]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ExecutionStatusRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ExecutionStatusRequest) ProtoMessage() {}
+
+func (x *ExecutionStatusRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[27]
+	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 ExecutionStatusRequest.ProtoReflect.Descriptor instead.
+func (*ExecutionStatusRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{27}
+}
+
+func (x *ExecutionStatusRequest) GetExecutionTrackingId() string {
+	if x != nil {
+		return x.ExecutionTrackingId
+	}
+	return ""
+}
+
+func (x *ExecutionStatusRequest) GetActionId() string {
+	if x != nil {
+		return x.ActionId
+	}
+	return ""
+}
+
+type ExecutionStatusResponse struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	LogEntry      *LogEntry              `protobuf:"bytes,1,opt,name=log_entry,json=logEntry,proto3" json:"log_entry,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *ExecutionStatusResponse) Reset() {
+	*x = ExecutionStatusResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[28]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ExecutionStatusResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ExecutionStatusResponse) ProtoMessage() {}
+
+func (x *ExecutionStatusResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[28]
+	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 ExecutionStatusResponse.ProtoReflect.Descriptor instead.
+func (*ExecutionStatusResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{28}
+}
+
+func (x *ExecutionStatusResponse) GetLogEntry() *LogEntry {
+	if x != nil {
+		return x.LogEntry
+	}
+	return nil
+}
+
+type WhoAmIRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *WhoAmIRequest) Reset() {
+	*x = WhoAmIRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[29]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *WhoAmIRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*WhoAmIRequest) ProtoMessage() {}
+
+func (x *WhoAmIRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[29]
+	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 WhoAmIRequest.ProtoReflect.Descriptor instead.
+func (*WhoAmIRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{29}
+}
+
+type WhoAmIResponse struct {
+	state             protoimpl.MessageState `protogen:"open.v1"`
+	AuthenticatedUser string                 `protobuf:"bytes,1,opt,name=authenticated_user,json=authenticatedUser,proto3" json:"authenticated_user,omitempty"`
+	Usergroup         string                 `protobuf:"bytes,2,opt,name=usergroup,proto3" json:"usergroup,omitempty"`
+	Provider          string                 `protobuf:"bytes,3,opt,name=provider,proto3" json:"provider,omitempty"`
+	Acls              []string               `protobuf:"bytes,4,rep,name=acls,proto3" json:"acls,omitempty"`
+	Sid               string                 `protobuf:"bytes,5,opt,name=sid,proto3" json:"sid,omitempty"`
+	unknownFields     protoimpl.UnknownFields
+	sizeCache         protoimpl.SizeCache
+}
+
+func (x *WhoAmIResponse) Reset() {
+	*x = WhoAmIResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[30]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *WhoAmIResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*WhoAmIResponse) ProtoMessage() {}
+
+func (x *WhoAmIResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[30]
+	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 WhoAmIResponse.ProtoReflect.Descriptor instead.
+func (*WhoAmIResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{30}
+}
+
+func (x *WhoAmIResponse) GetAuthenticatedUser() string {
+	if x != nil {
+		return x.AuthenticatedUser
+	}
+	return ""
+}
+
+func (x *WhoAmIResponse) GetUsergroup() string {
+	if x != nil {
+		return x.Usergroup
+	}
+	return ""
+}
+
+func (x *WhoAmIResponse) GetProvider() string {
+	if x != nil {
+		return x.Provider
+	}
+	return ""
+}
+
+func (x *WhoAmIResponse) GetAcls() []string {
+	if x != nil {
+		return x.Acls
+	}
+	return nil
+}
+
+func (x *WhoAmIResponse) GetSid() string {
+	if x != nil {
+		return x.Sid
+	}
+	return ""
+}
+
+type SosReportRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *SosReportRequest) Reset() {
+	*x = SosReportRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[31]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *SosReportRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SosReportRequest) ProtoMessage() {}
+
+func (x *SosReportRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[31]
+	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 SosReportRequest.ProtoReflect.Descriptor instead.
+func (*SosReportRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{31}
+}
+
+type SosReportResponse struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Alert         string                 `protobuf:"bytes,1,opt,name=alert,proto3" json:"alert,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *SosReportResponse) Reset() {
+	*x = SosReportResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[32]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *SosReportResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SosReportResponse) ProtoMessage() {}
+
+func (x *SosReportResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[32]
+	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 SosReportResponse.ProtoReflect.Descriptor instead.
+func (*SosReportResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{32}
+}
+
+func (x *SosReportResponse) GetAlert() string {
+	if x != nil {
+		return x.Alert
+	}
+	return ""
+}
+
+type DumpVarsRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *DumpVarsRequest) Reset() {
+	*x = DumpVarsRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[33]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *DumpVarsRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DumpVarsRequest) ProtoMessage() {}
+
+func (x *DumpVarsRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[33]
+	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 DumpVarsRequest.ProtoReflect.Descriptor instead.
+func (*DumpVarsRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{33}
+}
+
+type DumpVarsResponse struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Alert         string                 `protobuf:"bytes,1,opt,name=alert,proto3" json:"alert,omitempty"`
+	Contents      map[string]string      `protobuf:"bytes,2,rep,name=contents,proto3" json:"contents,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *DumpVarsResponse) Reset() {
+	*x = DumpVarsResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[34]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *DumpVarsResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DumpVarsResponse) ProtoMessage() {}
+
+func (x *DumpVarsResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[34]
+	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 DumpVarsResponse.ProtoReflect.Descriptor instead.
+func (*DumpVarsResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{34}
+}
+
+func (x *DumpVarsResponse) GetAlert() string {
+	if x != nil {
+		return x.Alert
+	}
+	return ""
+}
+
+func (x *DumpVarsResponse) GetContents() map[string]string {
+	if x != nil {
+		return x.Contents
+	}
+	return nil
+}
+
+type DebugBinding struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	ActionTitle   string                 `protobuf:"bytes,1,opt,name=action_title,json=actionTitle,proto3" json:"action_title,omitempty"`
+	EntityPrefix  string                 `protobuf:"bytes,2,opt,name=entity_prefix,json=entityPrefix,proto3" json:"entity_prefix,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *DebugBinding) Reset() {
+	*x = DebugBinding{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[35]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *DebugBinding) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DebugBinding) ProtoMessage() {}
+
+func (x *DebugBinding) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[35]
+	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 DebugBinding.ProtoReflect.Descriptor instead.
+func (*DebugBinding) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{35}
+}
+
+func (x *DebugBinding) GetActionTitle() string {
+	if x != nil {
+		return x.ActionTitle
+	}
+	return ""
+}
+
+func (x *DebugBinding) GetEntityPrefix() string {
+	if x != nil {
+		return x.EntityPrefix
+	}
+	return ""
+}
+
+type DumpPublicIdActionMapRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *DumpPublicIdActionMapRequest) Reset() {
+	*x = DumpPublicIdActionMapRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[36]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *DumpPublicIdActionMapRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DumpPublicIdActionMapRequest) ProtoMessage() {}
+
+func (x *DumpPublicIdActionMapRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[36]
+	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 DumpPublicIdActionMapRequest.ProtoReflect.Descriptor instead.
+func (*DumpPublicIdActionMapRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{36}
+}
+
+type DumpPublicIdActionMapResponse struct {
+	state         protoimpl.MessageState   `protogen:"open.v1"`
+	Alert         string                   `protobuf:"bytes,1,opt,name=alert,proto3" json:"alert,omitempty"`
+	Contents      map[string]*DebugBinding `protobuf:"bytes,2,rep,name=contents,proto3" json:"contents,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *DumpPublicIdActionMapResponse) Reset() {
+	*x = DumpPublicIdActionMapResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[37]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *DumpPublicIdActionMapResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DumpPublicIdActionMapResponse) ProtoMessage() {}
+
+func (x *DumpPublicIdActionMapResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[37]
+	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 DumpPublicIdActionMapResponse.ProtoReflect.Descriptor instead.
+func (*DumpPublicIdActionMapResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{37}
+}
+
+func (x *DumpPublicIdActionMapResponse) GetAlert() string {
+	if x != nil {
+		return x.Alert
+	}
+	return ""
+}
+
+func (x *DumpPublicIdActionMapResponse) GetContents() map[string]*DebugBinding {
+	if x != nil {
+		return x.Contents
+	}
+	return nil
+}
+
+type GetReadyzRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GetReadyzRequest) Reset() {
+	*x = GetReadyzRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[38]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetReadyzRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetReadyzRequest) ProtoMessage() {}
+
+func (x *GetReadyzRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[38]
+	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 GetReadyzRequest.ProtoReflect.Descriptor instead.
+func (*GetReadyzRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{38}
+}
+
+type GetReadyzResponse struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Status        string                 `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GetReadyzResponse) Reset() {
+	*x = GetReadyzResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[39]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetReadyzResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetReadyzResponse) ProtoMessage() {}
+
+func (x *GetReadyzResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[39]
+	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 GetReadyzResponse.ProtoReflect.Descriptor instead.
+func (*GetReadyzResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{39}
+}
+
+func (x *GetReadyzResponse) GetStatus() string {
+	if x != nil {
+		return x.Status
+	}
+	return ""
+}
+
+type EventStreamRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *EventStreamRequest) Reset() {
+	*x = EventStreamRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[40]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *EventStreamRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EventStreamRequest) ProtoMessage() {}
+
+func (x *EventStreamRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[40]
+	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 EventStreamRequest.ProtoReflect.Descriptor instead.
+func (*EventStreamRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{40}
+}
+
+type EventStreamResponse struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	// Types that are valid to be assigned to Event:
+	//
+	//	*EventStreamResponse_EntityChanged
+	//	*EventStreamResponse_ConfigChanged
+	//	*EventStreamResponse_ExecutionFinished
+	//	*EventStreamResponse_ExecutionStarted
+	//	*EventStreamResponse_OutputChunk
+	Event         isEventStreamResponse_Event `protobuf_oneof:"event"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *EventStreamResponse) Reset() {
+	*x = EventStreamResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[41]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *EventStreamResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EventStreamResponse) ProtoMessage() {}
+
+func (x *EventStreamResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[41]
+	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 EventStreamResponse.ProtoReflect.Descriptor instead.
+func (*EventStreamResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{41}
+}
+
+func (x *EventStreamResponse) GetEvent() isEventStreamResponse_Event {
+	if x != nil {
+		return x.Event
+	}
+	return nil
+}
+
+func (x *EventStreamResponse) GetEntityChanged() *EventEntityChanged {
+	if x != nil {
+		if x, ok := x.Event.(*EventStreamResponse_EntityChanged); ok {
+			return x.EntityChanged
+		}
+	}
+	return nil
+}
+
+func (x *EventStreamResponse) GetConfigChanged() *EventConfigChanged {
+	if x != nil {
+		if x, ok := x.Event.(*EventStreamResponse_ConfigChanged); ok {
+			return x.ConfigChanged
+		}
+	}
+	return nil
+}
+
+func (x *EventStreamResponse) GetExecutionFinished() *EventExecutionFinished {
+	if x != nil {
+		if x, ok := x.Event.(*EventStreamResponse_ExecutionFinished); ok {
+			return x.ExecutionFinished
+		}
+	}
+	return nil
+}
+
+func (x *EventStreamResponse) GetExecutionStarted() *EventExecutionStarted {
+	if x != nil {
+		if x, ok := x.Event.(*EventStreamResponse_ExecutionStarted); ok {
+			return x.ExecutionStarted
+		}
+	}
+	return nil
+}
+
+func (x *EventStreamResponse) GetOutputChunk() *EventOutputChunk {
+	if x != nil {
+		if x, ok := x.Event.(*EventStreamResponse_OutputChunk); ok {
+			return x.OutputChunk
+		}
+	}
+	return nil
+}
+
+type isEventStreamResponse_Event interface {
+	isEventStreamResponse_Event()
+}
+
+type EventStreamResponse_EntityChanged struct {
+	EntityChanged *EventEntityChanged `protobuf:"bytes,2,opt,name=entity_changed,json=entityChanged,proto3,oneof"`
+}
+
+type EventStreamResponse_ConfigChanged struct {
+	ConfigChanged *EventConfigChanged `protobuf:"bytes,3,opt,name=config_changed,json=configChanged,proto3,oneof"`
+}
+
+type EventStreamResponse_ExecutionFinished struct {
+	ExecutionFinished *EventExecutionFinished `protobuf:"bytes,4,opt,name=execution_finished,json=executionFinished,proto3,oneof"`
+}
+
+type EventStreamResponse_ExecutionStarted struct {
+	ExecutionStarted *EventExecutionStarted `protobuf:"bytes,5,opt,name=execution_started,json=executionStarted,proto3,oneof"`
+}
+
+type EventStreamResponse_OutputChunk struct {
+	OutputChunk *EventOutputChunk `protobuf:"bytes,6,opt,name=output_chunk,json=outputChunk,proto3,oneof"`
+}
+
+func (*EventStreamResponse_EntityChanged) isEventStreamResponse_Event() {}
+
+func (*EventStreamResponse_ConfigChanged) isEventStreamResponse_Event() {}
+
+func (*EventStreamResponse_ExecutionFinished) isEventStreamResponse_Event() {}
+
+func (*EventStreamResponse_ExecutionStarted) isEventStreamResponse_Event() {}
+
+func (*EventStreamResponse_OutputChunk) isEventStreamResponse_Event() {}
+
+type EventOutputChunk struct {
+	state               protoimpl.MessageState `protogen:"open.v1"`
+	ExecutionTrackingId string                 `protobuf:"bytes,1,opt,name=execution_tracking_id,json=executionTrackingId,proto3" json:"execution_tracking_id,omitempty"`
+	Output              string                 `protobuf:"bytes,2,opt,name=output,proto3" json:"output,omitempty"`
+	unknownFields       protoimpl.UnknownFields
+	sizeCache           protoimpl.SizeCache
+}
+
+func (x *EventOutputChunk) Reset() {
+	*x = EventOutputChunk{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[42]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *EventOutputChunk) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EventOutputChunk) ProtoMessage() {}
+
+func (x *EventOutputChunk) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[42]
+	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 EventOutputChunk.ProtoReflect.Descriptor instead.
+func (*EventOutputChunk) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{42}
+}
+
+func (x *EventOutputChunk) GetExecutionTrackingId() string {
+	if x != nil {
+		return x.ExecutionTrackingId
+	}
+	return ""
+}
+
+func (x *EventOutputChunk) GetOutput() string {
+	if x != nil {
+		return x.Output
+	}
+	return ""
+}
+
+type EventEntityChanged struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *EventEntityChanged) Reset() {
+	*x = EventEntityChanged{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[43]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *EventEntityChanged) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EventEntityChanged) ProtoMessage() {}
+
+func (x *EventEntityChanged) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[43]
+	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 EventEntityChanged.ProtoReflect.Descriptor instead.
+func (*EventEntityChanged) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{43}
+}
+
+type EventConfigChanged struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *EventConfigChanged) Reset() {
+	*x = EventConfigChanged{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[44]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *EventConfigChanged) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EventConfigChanged) ProtoMessage() {}
+
+func (x *EventConfigChanged) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[44]
+	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 EventConfigChanged.ProtoReflect.Descriptor instead.
+func (*EventConfigChanged) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{44}
+}
+
+type EventExecutionFinished struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	LogEntry      *LogEntry              `protobuf:"bytes,1,opt,name=log_entry,json=logEntry,proto3" json:"log_entry,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *EventExecutionFinished) Reset() {
+	*x = EventExecutionFinished{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[45]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *EventExecutionFinished) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EventExecutionFinished) ProtoMessage() {}
+
+func (x *EventExecutionFinished) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[45]
+	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 EventExecutionFinished.ProtoReflect.Descriptor instead.
+func (*EventExecutionFinished) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{45}
+}
+
+func (x *EventExecutionFinished) GetLogEntry() *LogEntry {
+	if x != nil {
+		return x.LogEntry
+	}
+	return nil
+}
+
+type EventExecutionStarted struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	LogEntry      *LogEntry              `protobuf:"bytes,1,opt,name=log_entry,json=logEntry,proto3" json:"log_entry,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *EventExecutionStarted) Reset() {
+	*x = EventExecutionStarted{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[46]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *EventExecutionStarted) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EventExecutionStarted) ProtoMessage() {}
+
+func (x *EventExecutionStarted) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[46]
+	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 EventExecutionStarted.ProtoReflect.Descriptor instead.
+func (*EventExecutionStarted) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{46}
+}
+
+func (x *EventExecutionStarted) GetLogEntry() *LogEntry {
+	if x != nil {
+		return x.LogEntry
+	}
+	return nil
+}
+
+type KillActionRequest struct {
+	state               protoimpl.MessageState `protogen:"open.v1"`
+	ExecutionTrackingId string                 `protobuf:"bytes,1,opt,name=execution_tracking_id,json=executionTrackingId,proto3" json:"execution_tracking_id,omitempty"`
+	unknownFields       protoimpl.UnknownFields
+	sizeCache           protoimpl.SizeCache
+}
+
+func (x *KillActionRequest) Reset() {
+	*x = KillActionRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[47]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *KillActionRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*KillActionRequest) ProtoMessage() {}
+
+func (x *KillActionRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[47]
+	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 KillActionRequest.ProtoReflect.Descriptor instead.
+func (*KillActionRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{47}
+}
+
+func (x *KillActionRequest) GetExecutionTrackingId() string {
+	if x != nil {
+		return x.ExecutionTrackingId
+	}
+	return ""
+}
+
+type KillActionResponse struct {
+	state               protoimpl.MessageState `protogen:"open.v1"`
+	ExecutionTrackingId string                 `protobuf:"bytes,1,opt,name=execution_tracking_id,json=executionTrackingId,proto3" json:"execution_tracking_id,omitempty"`
+	Killed              bool                   `protobuf:"varint,2,opt,name=killed,proto3" json:"killed,omitempty"`
+	AlreadyCompleted    bool                   `protobuf:"varint,3,opt,name=already_completed,json=alreadyCompleted,proto3" json:"already_completed,omitempty"`
+	Found               bool                   `protobuf:"varint,4,opt,name=found,proto3" json:"found,omitempty"`
+	unknownFields       protoimpl.UnknownFields
+	sizeCache           protoimpl.SizeCache
+}
+
+func (x *KillActionResponse) Reset() {
+	*x = KillActionResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[48]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *KillActionResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*KillActionResponse) ProtoMessage() {}
+
+func (x *KillActionResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[48]
+	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 KillActionResponse.ProtoReflect.Descriptor instead.
+func (*KillActionResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{48}
+}
+
+func (x *KillActionResponse) GetExecutionTrackingId() string {
+	if x != nil {
+		return x.ExecutionTrackingId
+	}
+	return ""
+}
+
+func (x *KillActionResponse) GetKilled() bool {
+	if x != nil {
+		return x.Killed
+	}
+	return false
+}
+
+func (x *KillActionResponse) GetAlreadyCompleted() bool {
+	if x != nil {
+		return x.AlreadyCompleted
+	}
+	return false
+}
+
+func (x *KillActionResponse) GetFound() bool {
+	if x != nil {
+		return x.Found
+	}
+	return false
+}
+
+type LocalUserLoginRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Username      string                 `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
+	Password      string                 `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *LocalUserLoginRequest) Reset() {
+	*x = LocalUserLoginRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[49]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *LocalUserLoginRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LocalUserLoginRequest) ProtoMessage() {}
+
+func (x *LocalUserLoginRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[49]
+	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 LocalUserLoginRequest.ProtoReflect.Descriptor instead.
+func (*LocalUserLoginRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{49}
+}
+
+func (x *LocalUserLoginRequest) GetUsername() string {
+	if x != nil {
+		return x.Username
+	}
+	return ""
+}
+
+func (x *LocalUserLoginRequest) GetPassword() string {
+	if x != nil {
+		return x.Password
+	}
+	return ""
+}
+
+type LocalUserLoginResponse struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Success       bool                   `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *LocalUserLoginResponse) Reset() {
+	*x = LocalUserLoginResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[50]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *LocalUserLoginResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LocalUserLoginResponse) ProtoMessage() {}
+
+func (x *LocalUserLoginResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[50]
+	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 LocalUserLoginResponse.ProtoReflect.Descriptor instead.
+func (*LocalUserLoginResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{50}
+}
+
+func (x *LocalUserLoginResponse) GetSuccess() bool {
+	if x != nil {
+		return x.Success
+	}
+	return false
+}
+
+type PasswordHashRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Password      string                 `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *PasswordHashRequest) Reset() {
+	*x = PasswordHashRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[51]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *PasswordHashRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PasswordHashRequest) ProtoMessage() {}
+
+func (x *PasswordHashRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[51]
+	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 PasswordHashRequest.ProtoReflect.Descriptor instead.
+func (*PasswordHashRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{51}
+}
+
+func (x *PasswordHashRequest) GetPassword() string {
+	if x != nil {
+		return x.Password
+	}
+	return ""
+}
+
+type PasswordHashResponse struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Hash          string                 `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *PasswordHashResponse) Reset() {
+	*x = PasswordHashResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[52]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *PasswordHashResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PasswordHashResponse) ProtoMessage() {}
+
+func (x *PasswordHashResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[52]
+	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 PasswordHashResponse.ProtoReflect.Descriptor instead.
+func (*PasswordHashResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{52}
+}
+
+func (x *PasswordHashResponse) GetHash() string {
+	if x != nil {
+		return x.Hash
+	}
+	return ""
+}
+
+type LogoutRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *LogoutRequest) Reset() {
+	*x = LogoutRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[53]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *LogoutRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LogoutRequest) ProtoMessage() {}
+
+func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[53]
+	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 LogoutRequest.ProtoReflect.Descriptor instead.
+func (*LogoutRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{53}
+}
+
+type LogoutResponse struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *LogoutResponse) Reset() {
+	*x = LogoutResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[54]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *LogoutResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LogoutResponse) ProtoMessage() {}
+
+func (x *LogoutResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[54]
+	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 LogoutResponse.ProtoReflect.Descriptor instead.
+func (*LogoutResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{54}
+}
+
+type GetDiagnosticsRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GetDiagnosticsRequest) Reset() {
+	*x = GetDiagnosticsRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[55]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetDiagnosticsRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetDiagnosticsRequest) ProtoMessage() {}
+
+func (x *GetDiagnosticsRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[55]
+	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 GetDiagnosticsRequest.ProtoReflect.Descriptor instead.
+func (*GetDiagnosticsRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{55}
+}
+
+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"`
+	unknownFields  protoimpl.UnknownFields
+	sizeCache      protoimpl.SizeCache
+}
+
+func (x *GetDiagnosticsResponse) Reset() {
+	*x = GetDiagnosticsResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[56]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetDiagnosticsResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetDiagnosticsResponse) ProtoMessage() {}
+
+func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[56]
+	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 GetDiagnosticsResponse.ProtoReflect.Descriptor instead.
+func (*GetDiagnosticsResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{56}
+}
+
+func (x *GetDiagnosticsResponse) GetSshFoundKey() string {
+	if x != nil {
+		return x.SshFoundKey
+	}
+	return ""
+}
+
+func (x *GetDiagnosticsResponse) GetSshFoundConfig() string {
+	if x != nil {
+		return x.SshFoundConfig
+	}
+	return ""
+}
+
+type InitRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *InitRequest) Reset() {
+	*x = InitRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[57]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *InitRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*InitRequest) ProtoMessage() {}
+
+func (x *InitRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[57]
+	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 InitRequest.ProtoReflect.Descriptor instead.
+func (*InitRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{57}
+}
+
+type InitResponse struct {
+	state                     protoimpl.MessageState `protogen:"open.v1"`
+	ShowFooter                bool                   `protobuf:"varint,1,opt,name=showFooter,proto3" json:"showFooter,omitempty"`
+	ShowNavigation            bool                   `protobuf:"varint,2,opt,name=showNavigation,proto3" json:"showNavigation,omitempty"`
+	ShowNewVersions           bool                   `protobuf:"varint,3,opt,name=showNewVersions,proto3" json:"showNewVersions,omitempty"`
+	AvailableVersion          string                 `protobuf:"bytes,4,opt,name=availableVersion,proto3" json:"availableVersion,omitempty"`
+	CurrentVersion            string                 `protobuf:"bytes,5,opt,name=currentVersion,proto3" json:"currentVersion,omitempty"`
+	PageTitle                 string                 `protobuf:"bytes,6,opt,name=pageTitle,proto3" json:"pageTitle,omitempty"`
+	SectionNavigationStyle    string                 `protobuf:"bytes,7,opt,name=sectionNavigationStyle,proto3" json:"sectionNavigationStyle,omitempty"`
+	DefaultIconForBack        string                 `protobuf:"bytes,8,opt,name=defaultIconForBack,proto3" json:"defaultIconForBack,omitempty"`
+	EnableCustomJs            bool                   `protobuf:"varint,9,opt,name=enableCustomJs,proto3" json:"enableCustomJs,omitempty"`
+	AuthLoginUrl              string                 `protobuf:"bytes,10,opt,name=authLoginUrl,proto3" json:"authLoginUrl,omitempty"`
+	AuthLocalLogin            bool                   `protobuf:"varint,11,opt,name=authLocalLogin,proto3" json:"authLocalLogin,omitempty"`
+	StyleMods                 []string               `protobuf:"bytes,12,rep,name=styleMods,proto3" json:"styleMods,omitempty"`
+	OAuth2Providers           []*OAuth2Provider      `protobuf:"bytes,13,rep,name=oAuth2Providers,proto3" json:"oAuth2Providers,omitempty"`
+	AdditionalLinks           []*AdditionalLink      `protobuf:"bytes,14,rep,name=additionalLinks,proto3" json:"additionalLinks,omitempty"`
+	RootDashboards            []string               `protobuf:"bytes,15,rep,name=rootDashboards,proto3" json:"rootDashboards,omitempty"`
+	AuthenticatedUser         string                 `protobuf:"bytes,16,opt,name=authenticated_user,json=authenticatedUser,proto3" json:"authenticated_user,omitempty"`
+	AuthenticatedUserProvider string                 `protobuf:"bytes,17,opt,name=authenticated_user_provider,json=authenticatedUserProvider,proto3" json:"authenticated_user_provider,omitempty"`
+	EffectivePolicy           *EffectivePolicy       `protobuf:"bytes,18,opt,name=effective_policy,json=effectivePolicy,proto3" json:"effective_policy,omitempty"`
+	BannerMessage             string                 `protobuf:"bytes,19,opt,name=banner_message,json=bannerMessage,proto3" json:"banner_message,omitempty"`
+	BannerCss                 string                 `protobuf:"bytes,20,opt,name=banner_css,json=bannerCss,proto3" json:"banner_css,omitempty"`
+	ShowDiagnostics           bool                   `protobuf:"varint,21,opt,name=show_diagnostics,json=showDiagnostics,proto3" json:"show_diagnostics,omitempty"`
+	ShowLogList               bool                   `protobuf:"varint,22,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"`
+	LoginRequired             bool                   `protobuf:"varint,23,opt,name=login_required,json=loginRequired,proto3" json:"login_required,omitempty"`
+	unknownFields             protoimpl.UnknownFields
+	sizeCache                 protoimpl.SizeCache
+}
+
+func (x *InitResponse) Reset() {
+	*x = InitResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[58]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *InitResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*InitResponse) ProtoMessage() {}
+
+func (x *InitResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[58]
+	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 InitResponse.ProtoReflect.Descriptor instead.
+func (*InitResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{58}
+}
+
+func (x *InitResponse) GetShowFooter() bool {
+	if x != nil {
+		return x.ShowFooter
+	}
+	return false
+}
+
+func (x *InitResponse) GetShowNavigation() bool {
+	if x != nil {
+		return x.ShowNavigation
+	}
+	return false
+}
+
+func (x *InitResponse) GetShowNewVersions() bool {
+	if x != nil {
+		return x.ShowNewVersions
+	}
+	return false
+}
+
+func (x *InitResponse) GetAvailableVersion() string {
+	if x != nil {
+		return x.AvailableVersion
+	}
+	return ""
+}
+
+func (x *InitResponse) GetCurrentVersion() string {
+	if x != nil {
+		return x.CurrentVersion
+	}
+	return ""
+}
+
+func (x *InitResponse) GetPageTitle() string {
+	if x != nil {
+		return x.PageTitle
+	}
+	return ""
+}
+
+func (x *InitResponse) GetSectionNavigationStyle() string {
+	if x != nil {
+		return x.SectionNavigationStyle
+	}
+	return ""
+}
+
+func (x *InitResponse) GetDefaultIconForBack() string {
+	if x != nil {
+		return x.DefaultIconForBack
+	}
+	return ""
+}
+
+func (x *InitResponse) GetEnableCustomJs() bool {
+	if x != nil {
+		return x.EnableCustomJs
+	}
+	return false
+}
+
+func (x *InitResponse) GetAuthLoginUrl() string {
+	if x != nil {
+		return x.AuthLoginUrl
+	}
+	return ""
+}
+
+func (x *InitResponse) GetAuthLocalLogin() bool {
+	if x != nil {
+		return x.AuthLocalLogin
+	}
+	return false
+}
+
+func (x *InitResponse) GetStyleMods() []string {
+	if x != nil {
+		return x.StyleMods
+	}
+	return nil
+}
+
+func (x *InitResponse) GetOAuth2Providers() []*OAuth2Provider {
+	if x != nil {
+		return x.OAuth2Providers
+	}
+	return nil
+}
+
+func (x *InitResponse) GetAdditionalLinks() []*AdditionalLink {
+	if x != nil {
+		return x.AdditionalLinks
+	}
+	return nil
+}
+
+func (x *InitResponse) GetRootDashboards() []string {
+	if x != nil {
+		return x.RootDashboards
+	}
+	return nil
+}
+
+func (x *InitResponse) GetAuthenticatedUser() string {
+	if x != nil {
+		return x.AuthenticatedUser
+	}
+	return ""
+}
+
+func (x *InitResponse) GetAuthenticatedUserProvider() string {
+	if x != nil {
+		return x.AuthenticatedUserProvider
+	}
+	return ""
+}
+
+func (x *InitResponse) GetEffectivePolicy() *EffectivePolicy {
+	if x != nil {
+		return x.EffectivePolicy
+	}
+	return nil
+}
+
+func (x *InitResponse) GetBannerMessage() string {
+	if x != nil {
+		return x.BannerMessage
+	}
+	return ""
+}
+
+func (x *InitResponse) GetBannerCss() string {
+	if x != nil {
+		return x.BannerCss
+	}
+	return ""
+}
+
+func (x *InitResponse) GetShowDiagnostics() bool {
+	if x != nil {
+		return x.ShowDiagnostics
+	}
+	return false
+}
+
+func (x *InitResponse) GetShowLogList() bool {
+	if x != nil {
+		return x.ShowLogList
+	}
+	return false
+}
+
+func (x *InitResponse) GetLoginRequired() bool {
+	if x != nil {
+		return x.LoginRequired
+	}
+	return false
+}
+
+type AdditionalLink struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Title         string                 `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
+	Url           string                 `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *AdditionalLink) Reset() {
+	*x = AdditionalLink{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[59]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *AdditionalLink) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AdditionalLink) ProtoMessage() {}
+
+func (x *AdditionalLink) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[59]
+	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 AdditionalLink.ProtoReflect.Descriptor instead.
+func (*AdditionalLink) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{59}
+}
+
+func (x *AdditionalLink) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+func (x *AdditionalLink) GetUrl() string {
+	if x != nil {
+		return x.Url
+	}
+	return ""
+}
+
+type OAuth2Provider struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Title         string                 `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
+	Icon          string                 `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"`
+	Key           string                 `protobuf:"bytes,4,opt,name=key,proto3" json:"key,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *OAuth2Provider) Reset() {
+	*x = OAuth2Provider{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[60]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *OAuth2Provider) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*OAuth2Provider) ProtoMessage() {}
+
+func (x *OAuth2Provider) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[60]
+	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 OAuth2Provider.ProtoReflect.Descriptor instead.
+func (*OAuth2Provider) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{60}
+}
+
+func (x *OAuth2Provider) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+func (x *OAuth2Provider) GetIcon() string {
+	if x != nil {
+		return x.Icon
+	}
+	return ""
+}
+
+func (x *OAuth2Provider) GetKey() string {
+	if x != nil {
+		return x.Key
+	}
+	return ""
+}
+
+type GetActionBindingRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	BindingId     string                 `protobuf:"bytes,1,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GetActionBindingRequest) Reset() {
+	*x = GetActionBindingRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[61]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetActionBindingRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetActionBindingRequest) ProtoMessage() {}
+
+func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[61]
+	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 GetActionBindingRequest.ProtoReflect.Descriptor instead.
+func (*GetActionBindingRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{61}
+}
+
+func (x *GetActionBindingRequest) GetBindingId() string {
+	if x != nil {
+		return x.BindingId
+	}
+	return ""
+}
+
+type GetActionBindingResponse struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Action        *Action                `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GetActionBindingResponse) Reset() {
+	*x = GetActionBindingResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[62]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetActionBindingResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetActionBindingResponse) ProtoMessage() {}
+
+func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[62]
+	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 GetActionBindingResponse.ProtoReflect.Descriptor instead.
+func (*GetActionBindingResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{62}
+}
+
+func (x *GetActionBindingResponse) GetAction() *Action {
+	if x != nil {
+		return x.Action
+	}
+	return nil
+}
+
+type GetEntitiesRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GetEntitiesRequest) Reset() {
+	*x = GetEntitiesRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[63]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetEntitiesRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetEntitiesRequest) ProtoMessage() {}
+
+func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[63]
+	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 GetEntitiesRequest.ProtoReflect.Descriptor instead.
+func (*GetEntitiesRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{63}
+}
+
+type GetEntitiesResponse struct {
+	state             protoimpl.MessageState `protogen:"open.v1"`
+	EntityDefinitions []*EntityDefinition    `protobuf:"bytes,1,rep,name=entity_definitions,json=entityDefinitions,proto3" json:"entity_definitions,omitempty"`
+	unknownFields     protoimpl.UnknownFields
+	sizeCache         protoimpl.SizeCache
+}
+
+func (x *GetEntitiesResponse) Reset() {
+	*x = GetEntitiesResponse{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[64]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetEntitiesResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetEntitiesResponse) ProtoMessage() {}
+
+func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[64]
+	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 GetEntitiesResponse.ProtoReflect.Descriptor instead.
+func (*GetEntitiesResponse) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{64}
+}
+
+func (x *GetEntitiesResponse) GetEntityDefinitions() []*EntityDefinition {
+	if x != nil {
+		return x.EntityDefinitions
+	}
+	return nil
+}
+
+type EntityDefinition struct {
+	state            protoimpl.MessageState `protogen:"open.v1"`
+	Title            string                 `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
+	Instances        []*Entity              `protobuf:"bytes,2,rep,name=instances,proto3" json:"instances,omitempty"`
+	UsedOnDashboards []string               `protobuf:"bytes,3,rep,name=used_on_dashboards,json=usedOnDashboards,proto3" json:"used_on_dashboards,omitempty"`
+	unknownFields    protoimpl.UnknownFields
+	sizeCache        protoimpl.SizeCache
+}
+
+func (x *EntityDefinition) Reset() {
+	*x = EntityDefinition{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *EntityDefinition) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EntityDefinition) ProtoMessage() {}
+
+func (x *EntityDefinition) 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 EntityDefinition.ProtoReflect.Descriptor instead.
+func (*EntityDefinition) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{65}
+}
+
+func (x *EntityDefinition) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+func (x *EntityDefinition) GetInstances() []*Entity {
+	if x != nil {
+		return x.Instances
+	}
+	return nil
+}
+
+func (x *EntityDefinition) GetUsedOnDashboards() []string {
+	if x != nil {
+		return x.UsedOnDashboards
+	}
+	return nil
+}
+
+type GetEntityRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	UniqueKey     string                 `protobuf:"bytes,1,opt,name=unique_key,json=uniqueKey,proto3" json:"unique_key,omitempty"`
+	Type          string                 `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GetEntityRequest) Reset() {
+	*x = GetEntityRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GetEntityRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetEntityRequest) ProtoMessage() {}
+
+func (x *GetEntityRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66]
+	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 GetEntityRequest.ProtoReflect.Descriptor instead.
+func (*GetEntityRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{66}
+}
+
+func (x *GetEntityRequest) GetUniqueKey() string {
+	if x != nil {
+		return x.UniqueKey
+	}
+	return ""
+}
+
+func (x *GetEntityRequest) GetType() string {
+	if x != nil {
+		return x.Type
+	}
+	return ""
+}
+
+type RestartActionRequest struct {
+	state               protoimpl.MessageState `protogen:"open.v1"`
+	ExecutionTrackingId string                 `protobuf:"bytes,1,opt,name=execution_tracking_id,json=executionTrackingId,proto3" json:"execution_tracking_id,omitempty"`
+	unknownFields       protoimpl.UnknownFields
+	sizeCache           protoimpl.SizeCache
+}
+
+func (x *RestartActionRequest) Reset() {
+	*x = RestartActionRequest{}
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *RestartActionRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RestartActionRequest) ProtoMessage() {}
+
+func (x *RestartActionRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67]
+	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 RestartActionRequest.ProtoReflect.Descriptor instead.
+func (*RestartActionRequest) Descriptor() ([]byte, []int) {
+	return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{67}
+}
+
+func (x *RestartActionRequest) GetExecutionTrackingId() string {
+	if x != nil {
+		return x.ExecutionTrackingId
+	}
+	return ""
+}
+
+var File_olivetin_api_v1_olivetin_proto protoreflect.FileDescriptor
+
+const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
+	"\n" +
+	"\x1eolivetin/api/v1/olivetin.proto\x12\x0folivetin.api.v1\"\xc0\x02\n" +
+	"\x06Action\x12\x1d\n" +
+	"\n" +
+	"binding_id\x18\x01 \x01(\tR\tbindingId\x12\x14\n" +
+	"\x05title\x18\x02 \x01(\tR\x05title\x12\x12\n" +
+	"\x04icon\x18\x03 \x01(\tR\x04icon\x12\x19\n" +
+	"\bcan_exec\x18\x04 \x01(\bR\acanExec\x12=\n" +
+	"\targuments\x18\x05 \x03(\v2\x1f.olivetin.api.v1.ActionArgumentR\targuments\x12$\n" +
+	"\x0epopup_on_start\x18\x06 \x01(\tR\fpopupOnStart\x12\x14\n" +
+	"\x05order\x18\a \x01(\x05R\x05order\x12\x18\n" +
+	"\atimeout\x18\b \x01(\x05R\atimeout\x12=\n" +
+	"\x1bdatetime_rate_limit_expires\x18\t \x01(\tR\x18datetimeRateLimitExpires\"\xa2\x03\n" +
+	"\x0eActionArgument\x12\x12\n" +
+	"\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
+	"\x05title\x18\x02 \x01(\tR\x05title\x12\x12\n" +
+	"\x04type\x18\x03 \x01(\tR\x04type\x12#\n" +
+	"\rdefault_value\x18\x04 \x01(\tR\fdefaultValue\x12?\n" +
+	"\achoices\x18\x05 \x03(\v2%.olivetin.api.v1.ActionArgumentChoiceR\achoices\x12 \n" +
+	"\vdescription\x18\x06 \x01(\tR\vdescription\x12R\n" +
+	"\vsuggestions\x18\a \x03(\v20.olivetin.api.v1.ActionArgument.SuggestionsEntryR\vsuggestions\x126\n" +
+	"\x17suggestions_browser_key\x18\b \x01(\tR\x15suggestionsBrowserKey\x1a>\n" +
+	"\x10SuggestionsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"B\n" +
+	"\x14ActionArgumentChoice\x12\x14\n" +
+	"\x05value\x18\x01 \x01(\tR\x05value\x12\x14\n" +
+	"\x05title\x18\x02 \x01(\tR\x05title\"\xeb\x01\n" +
+	"\x06Entity\x12\x14\n" +
+	"\x05title\x18\x01 \x01(\tR\x05title\x12\x1d\n" +
+	"\n" +
+	"unique_key\x18\x02 \x01(\tR\tuniqueKey\x12\x12\n" +
+	"\x04type\x18\x03 \x01(\tR\x04type\x12 \n" +
+	"\vdirectories\x18\x04 \x03(\tR\vdirectories\x12;\n" +
+	"\x06fields\x18\x05 \x03(\v2#.olivetin.api.v1.Entity.FieldsEntryR\x06fields\x1a9\n" +
+	"\vFieldsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\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" +
+	"\x0fEffectivePolicy\x12)\n" +
+	"\x10show_diagnostics\x18\x01 \x01(\bR\x0fshowDiagnostics\x12\"\n" +
+	"\rshow_log_list\x18\x02 \x01(\bR\vshowLogList\"k\n" +
+	"\x13GetDashboardRequest\x12\x14\n" +
+	"\x05title\x18\x01 \x01(\tR\x05title\x12\x1f\n" +
+	"\ventity_type\x18\x02 \x01(\tR\n" +
+	"entityType\x12\x1d\n" +
+	"\n" +
+	"entity_key\x18\x03 \x01(\tR\tentityKey\"b\n" +
+	"\tDashboard\x12\x14\n" +
+	"\x05title\x18\x01 \x01(\tR\x05title\x12?\n" +
+	"\bcontents\x18\x02 \x03(\v2#.olivetin.api.v1.DashboardComponentR\bcontents\"\xa1\x02\n" +
+	"\x12DashboardComponent\x12\x14\n" +
+	"\x05title\x18\x01 \x01(\tR\x05title\x12\x12\n" +
+	"\x04type\x18\x02 \x01(\tR\x04type\x12?\n" +
+	"\bcontents\x18\x03 \x03(\v2#.olivetin.api.v1.DashboardComponentR\bcontents\x12\x12\n" +
+	"\x04icon\x18\x04 \x01(\tR\x04icon\x12\x1b\n" +
+	"\tcss_class\x18\x05 \x01(\tR\bcssClass\x12/\n" +
+	"\x06action\x18\x06 \x01(\v2\x17.olivetin.api.v1.ActionR\x06action\x12\x1f\n" +
+	"\ventity_type\x18\a \x01(\tR\n" +
+	"entityType\x12\x1d\n" +
+	"\n" +
+	"entity_key\x18\b \x01(\tR\tentityKey\"\xa5\x01\n" +
+	"\x12StartActionRequest\x12\x1d\n" +
+	"\n" +
+	"binding_id\x18\x01 \x01(\tR\tbindingId\x12B\n" +
+	"\targuments\x18\x02 \x03(\v2$.olivetin.api.v1.StartActionArgumentR\targuments\x12,\n" +
+	"\x12unique_tracking_id\x18\x03 \x01(\tR\x10uniqueTrackingId\"?\n" +
+	"\x13StartActionArgument\x12\x12\n" +
+	"\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value\"I\n" +
+	"\x13StartActionResponse\x122\n" +
+	"\x15execution_tracking_id\x18\x02 \x01(\tR\x13executionTrackingId\"|\n" +
+	"\x19StartActionAndWaitRequest\x12\x1b\n" +
+	"\taction_id\x18\x01 \x01(\tR\bactionId\x12B\n" +
+	"\targuments\x18\x02 \x03(\v2$.olivetin.api.v1.StartActionArgumentR\targuments\"T\n" +
+	"\x1aStartActionAndWaitResponse\x126\n" +
+	"\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"6\n" +
+	"\x17StartActionByGetRequest\x12\x1b\n" +
+	"\taction_id\x18\x01 \x01(\tR\bactionId\"N\n" +
+	"\x18StartActionByGetResponse\x122\n" +
+	"\x15execution_tracking_id\x18\x02 \x01(\tR\x13executionTrackingId\"=\n" +
+	"\x1eStartActionByGetAndWaitRequest\x12\x1b\n" +
+	"\taction_id\x18\x01 \x01(\tR\bactionId\"Y\n" +
+	"\x1fStartActionByGetAndWaitResponse\x126\n" +
+	"\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"T\n" +
+	"\x0eGetLogsRequest\x12!\n" +
+	"\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\x12\x1f\n" +
+	"\vdate_filter\x18\x02 \x01(\tR\n" +
+	"dateFilter\"\x89\x05\n" +
+	"\bLogEntry\x12)\n" +
+	"\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" +
+	"\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\n" +
+	"\x06output\x18\x03 \x01(\tR\x06output\x12\x1b\n" +
+	"\ttimed_out\x18\x05 \x01(\bR\btimedOut\x12\x1b\n" +
+	"\texit_code\x18\x06 \x01(\x05R\bexitCode\x12\x12\n" +
+	"\x04user\x18\a \x01(\tR\x04user\x12\x1d\n" +
+	"\n" +
+	"user_class\x18\b \x01(\tR\tuserClass\x12\x1f\n" +
+	"\vaction_icon\x18\t \x01(\tR\n" +
+	"actionIcon\x12\x12\n" +
+	"\x04tags\x18\n" +
+	" \x03(\tR\x04tags\x122\n" +
+	"\x15execution_tracking_id\x18\v \x01(\tR\x13executionTrackingId\x12+\n" +
+	"\x11datetime_finished\x18\f \x01(\tR\x10datetimeFinished\x12+\n" +
+	"\x11execution_started\x18\x0e \x01(\bR\x10executionStarted\x12-\n" +
+	"\x12execution_finished\x18\x0f \x01(\bR\x11executionFinished\x12\x18\n" +
+	"\ablocked\x18\x10 \x01(\bR\ablocked\x12%\n" +
+	"\x0edatetime_index\x18\x11 \x01(\x03R\rdatetimeIndex\x12\x19\n" +
+	"\bcan_kill\x18\x12 \x01(\bR\acanKill\x12=\n" +
+	"\x1bdatetime_rate_limit_expires\x18\x13 \x01(\tR\x18datetimeRateLimitExpires\x12\x1d\n" +
+	"\n" +
+	"binding_id\x18\x14 \x01(\tR\tbindingId\"\xca\x01\n" +
+	"\x0fGetLogsResponse\x12-\n" +
+	"\x04logs\x18\x01 \x03(\v2\x19.olivetin.api.v1.LogEntryR\x04logs\x12'\n" +
+	"\x0fcount_remaining\x18\x02 \x01(\x03R\x0ecountRemaining\x12\x1b\n" +
+	"\tpage_size\x18\x03 \x01(\x03R\bpageSize\x12\x1f\n" +
+	"\vtotal_count\x18\x04 \x01(\x03R\n" +
+	"totalCount\x12!\n" +
+	"\fstart_offset\x18\x05 \x01(\x03R\vstartOffset\"V\n" +
+	"\x14GetActionLogsRequest\x12\x1b\n" +
+	"\taction_id\x18\x01 \x01(\tR\bactionId\x12!\n" +
+	"\fstart_offset\x18\x02 \x01(\x03R\vstartOffset\"\xd0\x01\n" +
+	"\x15GetActionLogsResponse\x12-\n" +
+	"\x04logs\x18\x01 \x03(\v2\x19.olivetin.api.v1.LogEntryR\x04logs\x12'\n" +
+	"\x0fcount_remaining\x18\x02 \x01(\x03R\x0ecountRemaining\x12\x1b\n" +
+	"\tpage_size\x18\x03 \x01(\x03R\bpageSize\x12\x1f\n" +
+	"\vtotal_count\x18\x04 \x01(\x03R\n" +
+	"totalCount\x12!\n" +
+	"\fstart_offset\x18\x05 \x01(\x03R\vstartOffset\"\x8b\x01\n" +
+	"\x1bValidateArgumentTypeRequest\x12\x14\n" +
+	"\x05value\x18\x01 \x01(\tR\x05value\x12\x12\n" +
+	"\x04type\x18\x02 \x01(\tR\x04type\x12\x1d\n" +
+	"\n" +
+	"binding_id\x18\x03 \x01(\tR\tbindingId\x12#\n" +
+	"\rargument_name\x18\x04 \x01(\tR\fargumentName\"V\n" +
+	"\x1cValidateArgumentTypeResponse\x12\x14\n" +
+	"\x05valid\x18\x01 \x01(\bR\x05valid\x12 \n" +
+	"\vdescription\x18\x02 \x01(\tR\vdescription\"K\n" +
+	"\x15WatchExecutionRequest\x122\n" +
+	"\x15execution_tracking_id\x18\x01 \x01(\tR\x13executionTrackingId\".\n" +
+	"\x14WatchExecutionUpdate\x12\x16\n" +
+	"\x06update\x18\x01 \x01(\tR\x06update\"i\n" +
+	"\x16ExecutionStatusRequest\x122\n" +
+	"\x15execution_tracking_id\x18\x01 \x01(\tR\x13executionTrackingId\x12\x1b\n" +
+	"\taction_id\x18\x02 \x01(\tR\bactionId\"Q\n" +
+	"\x17ExecutionStatusResponse\x126\n" +
+	"\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"\x0f\n" +
+	"\rWhoAmIRequest\"\x9f\x01\n" +
+	"\x0eWhoAmIResponse\x12-\n" +
+	"\x12authenticated_user\x18\x01 \x01(\tR\x11authenticatedUser\x12\x1c\n" +
+	"\tusergroup\x18\x02 \x01(\tR\tusergroup\x12\x1a\n" +
+	"\bprovider\x18\x03 \x01(\tR\bprovider\x12\x12\n" +
+	"\x04acls\x18\x04 \x03(\tR\x04acls\x12\x10\n" +
+	"\x03sid\x18\x05 \x01(\tR\x03sid\"\x12\n" +
+	"\x10SosReportRequest\")\n" +
+	"\x11SosReportResponse\x12\x14\n" +
+	"\x05alert\x18\x01 \x01(\tR\x05alert\"\x11\n" +
+	"\x0fDumpVarsRequest\"\xb2\x01\n" +
+	"\x10DumpVarsResponse\x12\x14\n" +
+	"\x05alert\x18\x01 \x01(\tR\x05alert\x12K\n" +
+	"\bcontents\x18\x02 \x03(\v2/.olivetin.api.v1.DumpVarsResponse.ContentsEntryR\bcontents\x1a;\n" +
+	"\rContentsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"V\n" +
+	"\fDebugBinding\x12!\n" +
+	"\faction_title\x18\x01 \x01(\tR\vactionTitle\x12#\n" +
+	"\rentity_prefix\x18\x02 \x01(\tR\fentityPrefix\"\x1e\n" +
+	"\x1cDumpPublicIdActionMapRequest\"\xeb\x01\n" +
+	"\x1dDumpPublicIdActionMapResponse\x12\x14\n" +
+	"\x05alert\x18\x01 \x01(\tR\x05alert\x12X\n" +
+	"\bcontents\x18\x02 \x03(\v2<.olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntryR\bcontents\x1aZ\n" +
+	"\rContentsEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x123\n" +
+	"\x05value\x18\x02 \x01(\v2\x1d.olivetin.api.v1.DebugBindingR\x05value:\x028\x01\"\x12\n" +
+	"\x10GetReadyzRequest\"+\n" +
+	"\x11GetReadyzResponse\x12\x16\n" +
+	"\x06status\x18\x01 \x01(\tR\x06status\"\x14\n" +
+	"\x12EventStreamRequest\"\xb3\x03\n" +
+	"\x13EventStreamResponse\x12L\n" +
+	"\x0eentity_changed\x18\x02 \x01(\v2#.olivetin.api.v1.EventEntityChangedH\x00R\rentityChanged\x12L\n" +
+	"\x0econfig_changed\x18\x03 \x01(\v2#.olivetin.api.v1.EventConfigChangedH\x00R\rconfigChanged\x12X\n" +
+	"\x12execution_finished\x18\x04 \x01(\v2'.olivetin.api.v1.EventExecutionFinishedH\x00R\x11executionFinished\x12U\n" +
+	"\x11execution_started\x18\x05 \x01(\v2&.olivetin.api.v1.EventExecutionStartedH\x00R\x10executionStarted\x12F\n" +
+	"\foutput_chunk\x18\x06 \x01(\v2!.olivetin.api.v1.EventOutputChunkH\x00R\voutputChunkB\a\n" +
+	"\x05event\"^\n" +
+	"\x10EventOutputChunk\x122\n" +
+	"\x15execution_tracking_id\x18\x01 \x01(\tR\x13executionTrackingId\x12\x16\n" +
+	"\x06output\x18\x02 \x01(\tR\x06output\"\x14\n" +
+	"\x12EventEntityChanged\"\x14\n" +
+	"\x12EventConfigChanged\"P\n" +
+	"\x16EventExecutionFinished\x126\n" +
+	"\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"O\n" +
+	"\x15EventExecutionStarted\x126\n" +
+	"\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"G\n" +
+	"\x11KillActionRequest\x122\n" +
+	"\x15execution_tracking_id\x18\x01 \x01(\tR\x13executionTrackingId\"\xa3\x01\n" +
+	"\x12KillActionResponse\x122\n" +
+	"\x15execution_tracking_id\x18\x01 \x01(\tR\x13executionTrackingId\x12\x16\n" +
+	"\x06killed\x18\x02 \x01(\bR\x06killed\x12+\n" +
+	"\x11already_completed\x18\x03 \x01(\bR\x10alreadyCompleted\x12\x14\n" +
+	"\x05found\x18\x04 \x01(\bR\x05found\"O\n" +
+	"\x15LocalUserLoginRequest\x12\x1a\n" +
+	"\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" +
+	"\bpassword\x18\x02 \x01(\tR\bpassword\"2\n" +
+	"\x16LocalUserLoginResponse\x12\x18\n" +
+	"\asuccess\x18\x01 \x01(\bR\asuccess\"1\n" +
+	"\x13PasswordHashRequest\x12\x1a\n" +
+	"\bpassword\x18\x01 \x01(\tR\bpassword\"*\n" +
+	"\x14PasswordHashResponse\x12\x12\n" +
+	"\x04hash\x18\x01 \x01(\tR\x04hash\"\x0f\n" +
+	"\rLogoutRequest\"\x10\n" +
+	"\x0eLogoutResponse\"\x17\n" +
+	"\x15GetDiagnosticsRequest\"b\n" +
+	"\x16GetDiagnosticsResponse\x12 \n" +
+	"\vSshFoundKey\x18\x01 \x01(\tR\vSshFoundKey\x12&\n" +
+	"\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\"\r\n" +
+	"\vInitRequest\"\xa2\b\n" +
+	"\fInitResponse\x12\x1e\n" +
+	"\n" +
+	"showFooter\x18\x01 \x01(\bR\n" +
+	"showFooter\x12&\n" +
+	"\x0eshowNavigation\x18\x02 \x01(\bR\x0eshowNavigation\x12(\n" +
+	"\x0fshowNewVersions\x18\x03 \x01(\bR\x0fshowNewVersions\x12*\n" +
+	"\x10availableVersion\x18\x04 \x01(\tR\x10availableVersion\x12&\n" +
+	"\x0ecurrentVersion\x18\x05 \x01(\tR\x0ecurrentVersion\x12\x1c\n" +
+	"\tpageTitle\x18\x06 \x01(\tR\tpageTitle\x126\n" +
+	"\x16sectionNavigationStyle\x18\a \x01(\tR\x16sectionNavigationStyle\x12.\n" +
+	"\x12defaultIconForBack\x18\b \x01(\tR\x12defaultIconForBack\x12&\n" +
+	"\x0eenableCustomJs\x18\t \x01(\bR\x0eenableCustomJs\x12\"\n" +
+	"\fauthLoginUrl\x18\n" +
+	" \x01(\tR\fauthLoginUrl\x12&\n" +
+	"\x0eauthLocalLogin\x18\v \x01(\bR\x0eauthLocalLogin\x12\x1c\n" +
+	"\tstyleMods\x18\f \x03(\tR\tstyleMods\x12I\n" +
+	"\x0foAuth2Providers\x18\r \x03(\v2\x1f.olivetin.api.v1.OAuth2ProviderR\x0foAuth2Providers\x12I\n" +
+	"\x0fadditionalLinks\x18\x0e \x03(\v2\x1f.olivetin.api.v1.AdditionalLinkR\x0fadditionalLinks\x12&\n" +
+	"\x0erootDashboards\x18\x0f \x03(\tR\x0erootDashboards\x12-\n" +
+	"\x12authenticated_user\x18\x10 \x01(\tR\x11authenticatedUser\x12>\n" +
+	"\x1bauthenticated_user_provider\x18\x11 \x01(\tR\x19authenticatedUserProvider\x12K\n" +
+	"\x10effective_policy\x18\x12 \x01(\v2 .olivetin.api.v1.EffectivePolicyR\x0feffectivePolicy\x12%\n" +
+	"\x0ebanner_message\x18\x13 \x01(\tR\rbannerMessage\x12\x1d\n" +
+	"\n" +
+	"banner_css\x18\x14 \x01(\tR\tbannerCss\x12)\n" +
+	"\x10show_diagnostics\x18\x15 \x01(\bR\x0fshowDiagnostics\x12\"\n" +
+	"\rshow_log_list\x18\x16 \x01(\bR\vshowLogList\x12%\n" +
+	"\x0elogin_required\x18\x17 \x01(\bR\rloginRequired\"8\n" +
+	"\x0eAdditionalLink\x12\x14\n" +
+	"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
+	"\x03url\x18\x02 \x01(\tR\x03url\"L\n" +
+	"\x0eOAuth2Provider\x12\x14\n" +
+	"\x05title\x18\x01 \x01(\tR\x05title\x12\x12\n" +
+	"\x04icon\x18\x03 \x01(\tR\x04icon\x12\x10\n" +
+	"\x03key\x18\x04 \x01(\tR\x03key\"8\n" +
+	"\x17GetActionBindingRequest\x12\x1d\n" +
+	"\n" +
+	"binding_id\x18\x01 \x01(\tR\tbindingId\"K\n" +
+	"\x18GetActionBindingResponse\x12/\n" +
+	"\x06action\x18\x01 \x01(\v2\x17.olivetin.api.v1.ActionR\x06action\"\x14\n" +
+	"\x12GetEntitiesRequest\"g\n" +
+	"\x13GetEntitiesResponse\x12P\n" +
+	"\x12entity_definitions\x18\x01 \x03(\v2!.olivetin.api.v1.EntityDefinitionR\x11entityDefinitions\"\x8d\x01\n" +
+	"\x10EntityDefinition\x12\x14\n" +
+	"\x05title\x18\x01 \x01(\tR\x05title\x125\n" +
+	"\tinstances\x18\x02 \x03(\v2\x17.olivetin.api.v1.EntityR\tinstances\x12,\n" +
+	"\x12used_on_dashboards\x18\x03 \x03(\tR\x10usedOnDashboards\"E\n" +
+	"\x10GetEntityRequest\x12\x1d\n" +
+	"\n" +
+	"unique_key\x18\x01 \x01(\tR\tuniqueKey\x12\x12\n" +
+	"\x04type\x18\x02 \x01(\tR\x04type\"J\n" +
+	"\x14RestartActionRequest\x122\n" +
+	"\x15execution_tracking_id\x18\x01 \x01(\tR\x13executionTrackingId2\xe8\x12\n" +
+	"\x12OliveTinApiService\x12]\n" +
+	"\fGetDashboard\x12$.olivetin.api.v1.GetDashboardRequest\x1a%.olivetin.api.v1.GetDashboardResponse\"\x00\x12Z\n" +
+	"\vStartAction\x12#.olivetin.api.v1.StartActionRequest\x1a$.olivetin.api.v1.StartActionResponse\"\x00\x12o\n" +
+	"\x12StartActionAndWait\x12*.olivetin.api.v1.StartActionAndWaitRequest\x1a+.olivetin.api.v1.StartActionAndWaitResponse\"\x00\x12i\n" +
+	"\x10StartActionByGet\x12(.olivetin.api.v1.StartActionByGetRequest\x1a).olivetin.api.v1.StartActionByGetResponse\"\x00\x12~\n" +
+	"\x17StartActionByGetAndWait\x12/.olivetin.api.v1.StartActionByGetAndWaitRequest\x1a0.olivetin.api.v1.StartActionByGetAndWaitResponse\"\x00\x12^\n" +
+	"\rRestartAction\x12%.olivetin.api.v1.RestartActionRequest\x1a$.olivetin.api.v1.StartActionResponse\"\x00\x12W\n" +
+	"\n" +
+	"KillAction\x12\".olivetin.api.v1.KillActionRequest\x1a#.olivetin.api.v1.KillActionResponse\"\x00\x12f\n" +
+	"\x0fExecutionStatus\x12'.olivetin.api.v1.ExecutionStatusRequest\x1a(.olivetin.api.v1.ExecutionStatusResponse\"\x00\x12N\n" +
+	"\aGetLogs\x12\x1f.olivetin.api.v1.GetLogsRequest\x1a .olivetin.api.v1.GetLogsResponse\"\x00\x12`\n" +
+	"\rGetActionLogs\x12%.olivetin.api.v1.GetActionLogsRequest\x1a&.olivetin.api.v1.GetActionLogsResponse\"\x00\x12u\n" +
+	"\x14ValidateArgumentType\x12,.olivetin.api.v1.ValidateArgumentTypeRequest\x1a-.olivetin.api.v1.ValidateArgumentTypeResponse\"\x00\x12K\n" +
+	"\x06WhoAmI\x12\x1e.olivetin.api.v1.WhoAmIRequest\x1a\x1f.olivetin.api.v1.WhoAmIResponse\"\x00\x12T\n" +
+	"\tSosReport\x12!.olivetin.api.v1.SosReportRequest\x1a\".olivetin.api.v1.SosReportResponse\"\x00\x12Q\n" +
+	"\bDumpVars\x12 .olivetin.api.v1.DumpVarsRequest\x1a!.olivetin.api.v1.DumpVarsResponse\"\x00\x12x\n" +
+	"\x15DumpPublicIdActionMap\x12-.olivetin.api.v1.DumpPublicIdActionMapRequest\x1a..olivetin.api.v1.DumpPublicIdActionMapResponse\"\x00\x12T\n" +
+	"\tGetReadyz\x12!.olivetin.api.v1.GetReadyzRequest\x1a\".olivetin.api.v1.GetReadyzResponse\"\x00\x12c\n" +
+	"\x0eLocalUserLogin\x12&.olivetin.api.v1.LocalUserLoginRequest\x1a'.olivetin.api.v1.LocalUserLoginResponse\"\x00\x12]\n" +
+	"\fPasswordHash\x12$.olivetin.api.v1.PasswordHashRequest\x1a%.olivetin.api.v1.PasswordHashResponse\"\x00\x12K\n" +
+	"\x06Logout\x12\x1e.olivetin.api.v1.LogoutRequest\x1a\x1f.olivetin.api.v1.LogoutResponse\"\x00\x12\\\n" +
+	"\vEventStream\x12#.olivetin.api.v1.EventStreamRequest\x1a$.olivetin.api.v1.EventStreamResponse\"\x000\x01\x12c\n" +
+	"\x0eGetDiagnostics\x12&.olivetin.api.v1.GetDiagnosticsRequest\x1a'.olivetin.api.v1.GetDiagnosticsResponse\"\x00\x12E\n" +
+	"\x04Init\x12\x1c.olivetin.api.v1.InitRequest\x1a\x1d.olivetin.api.v1.InitResponse\"\x00\x12i\n" +
+	"\x10GetActionBinding\x12(.olivetin.api.v1.GetActionBindingRequest\x1a).olivetin.api.v1.GetActionBindingResponse\"\x00\x12Z\n" +
+	"\vGetEntities\x12#.olivetin.api.v1.GetEntitiesRequest\x1a$.olivetin.api.v1.GetEntitiesResponse\"\x00\x12I\n" +
+	"\tGetEntity\x12!.olivetin.api.v1.GetEntityRequest\x1a\x17.olivetin.api.v1.Entity\"\x00B8Z6github.com/OliveTin/OliveTin/gen/olivetin/api/v1;apiv1b\x06proto3"
+
+var (
+	file_olivetin_api_v1_olivetin_proto_rawDescOnce sync.Once
+	file_olivetin_api_v1_olivetin_proto_rawDescData []byte
+)
+
+func file_olivetin_api_v1_olivetin_proto_rawDescGZIP() []byte {
+	file_olivetin_api_v1_olivetin_proto_rawDescOnce.Do(func() {
+		file_olivetin_api_v1_olivetin_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_olivetin_api_v1_olivetin_proto_rawDesc), len(file_olivetin_api_v1_olivetin_proto_rawDesc)))
+	})
+	return file_olivetin_api_v1_olivetin_proto_rawDescData
+}
+
+var file_olivetin_api_v1_olivetin_proto_msgTypes = make([]protoimpl.MessageInfo, 72)
+var file_olivetin_api_v1_olivetin_proto_goTypes = []any{
+	(*Action)(nil),                          // 0: olivetin.api.v1.Action
+	(*ActionArgument)(nil),                  // 1: olivetin.api.v1.ActionArgument
+	(*ActionArgumentChoice)(nil),            // 2: olivetin.api.v1.ActionArgumentChoice
+	(*Entity)(nil),                          // 3: olivetin.api.v1.Entity
+	(*GetDashboardResponse)(nil),            // 4: olivetin.api.v1.GetDashboardResponse
+	(*EffectivePolicy)(nil),                 // 5: olivetin.api.v1.EffectivePolicy
+	(*GetDashboardRequest)(nil),             // 6: olivetin.api.v1.GetDashboardRequest
+	(*Dashboard)(nil),                       // 7: olivetin.api.v1.Dashboard
+	(*DashboardComponent)(nil),              // 8: olivetin.api.v1.DashboardComponent
+	(*StartActionRequest)(nil),              // 9: olivetin.api.v1.StartActionRequest
+	(*StartActionArgument)(nil),             // 10: olivetin.api.v1.StartActionArgument
+	(*StartActionResponse)(nil),             // 11: olivetin.api.v1.StartActionResponse
+	(*StartActionAndWaitRequest)(nil),       // 12: olivetin.api.v1.StartActionAndWaitRequest
+	(*StartActionAndWaitResponse)(nil),      // 13: olivetin.api.v1.StartActionAndWaitResponse
+	(*StartActionByGetRequest)(nil),         // 14: olivetin.api.v1.StartActionByGetRequest
+	(*StartActionByGetResponse)(nil),        // 15: olivetin.api.v1.StartActionByGetResponse
+	(*StartActionByGetAndWaitRequest)(nil),  // 16: olivetin.api.v1.StartActionByGetAndWaitRequest
+	(*StartActionByGetAndWaitResponse)(nil), // 17: olivetin.api.v1.StartActionByGetAndWaitResponse
+	(*GetLogsRequest)(nil),                  // 18: olivetin.api.v1.GetLogsRequest
+	(*LogEntry)(nil),                        // 19: olivetin.api.v1.LogEntry
+	(*GetLogsResponse)(nil),                 // 20: olivetin.api.v1.GetLogsResponse
+	(*GetActionLogsRequest)(nil),            // 21: olivetin.api.v1.GetActionLogsRequest
+	(*GetActionLogsResponse)(nil),           // 22: olivetin.api.v1.GetActionLogsResponse
+	(*ValidateArgumentTypeRequest)(nil),     // 23: olivetin.api.v1.ValidateArgumentTypeRequest
+	(*ValidateArgumentTypeResponse)(nil),    // 24: olivetin.api.v1.ValidateArgumentTypeResponse
+	(*WatchExecutionRequest)(nil),           // 25: olivetin.api.v1.WatchExecutionRequest
+	(*WatchExecutionUpdate)(nil),            // 26: olivetin.api.v1.WatchExecutionUpdate
+	(*ExecutionStatusRequest)(nil),          // 27: olivetin.api.v1.ExecutionStatusRequest
+	(*ExecutionStatusResponse)(nil),         // 28: olivetin.api.v1.ExecutionStatusResponse
+	(*WhoAmIRequest)(nil),                   // 29: olivetin.api.v1.WhoAmIRequest
+	(*WhoAmIResponse)(nil),                  // 30: olivetin.api.v1.WhoAmIResponse
+	(*SosReportRequest)(nil),                // 31: olivetin.api.v1.SosReportRequest
+	(*SosReportResponse)(nil),               // 32: olivetin.api.v1.SosReportResponse
+	(*DumpVarsRequest)(nil),                 // 33: olivetin.api.v1.DumpVarsRequest
+	(*DumpVarsResponse)(nil),                // 34: olivetin.api.v1.DumpVarsResponse
+	(*DebugBinding)(nil),                    // 35: olivetin.api.v1.DebugBinding
+	(*DumpPublicIdActionMapRequest)(nil),    // 36: olivetin.api.v1.DumpPublicIdActionMapRequest
+	(*DumpPublicIdActionMapResponse)(nil),   // 37: olivetin.api.v1.DumpPublicIdActionMapResponse
+	(*GetReadyzRequest)(nil),                // 38: olivetin.api.v1.GetReadyzRequest
+	(*GetReadyzResponse)(nil),               // 39: olivetin.api.v1.GetReadyzResponse
+	(*EventStreamRequest)(nil),              // 40: olivetin.api.v1.EventStreamRequest
+	(*EventStreamResponse)(nil),             // 41: olivetin.api.v1.EventStreamResponse
+	(*EventOutputChunk)(nil),                // 42: olivetin.api.v1.EventOutputChunk
+	(*EventEntityChanged)(nil),              // 43: olivetin.api.v1.EventEntityChanged
+	(*EventConfigChanged)(nil),              // 44: olivetin.api.v1.EventConfigChanged
+	(*EventExecutionFinished)(nil),          // 45: olivetin.api.v1.EventExecutionFinished
+	(*EventExecutionStarted)(nil),           // 46: olivetin.api.v1.EventExecutionStarted
+	(*KillActionRequest)(nil),               // 47: olivetin.api.v1.KillActionRequest
+	(*KillActionResponse)(nil),              // 48: olivetin.api.v1.KillActionResponse
+	(*LocalUserLoginRequest)(nil),           // 49: olivetin.api.v1.LocalUserLoginRequest
+	(*LocalUserLoginResponse)(nil),          // 50: olivetin.api.v1.LocalUserLoginResponse
+	(*PasswordHashRequest)(nil),             // 51: olivetin.api.v1.PasswordHashRequest
+	(*PasswordHashResponse)(nil),            // 52: olivetin.api.v1.PasswordHashResponse
+	(*LogoutRequest)(nil),                   // 53: olivetin.api.v1.LogoutRequest
+	(*LogoutResponse)(nil),                  // 54: olivetin.api.v1.LogoutResponse
+	(*GetDiagnosticsRequest)(nil),           // 55: olivetin.api.v1.GetDiagnosticsRequest
+	(*GetDiagnosticsResponse)(nil),          // 56: olivetin.api.v1.GetDiagnosticsResponse
+	(*InitRequest)(nil),                     // 57: olivetin.api.v1.InitRequest
+	(*InitResponse)(nil),                    // 58: olivetin.api.v1.InitResponse
+	(*AdditionalLink)(nil),                  // 59: olivetin.api.v1.AdditionalLink
+	(*OAuth2Provider)(nil),                  // 60: olivetin.api.v1.OAuth2Provider
+	(*GetActionBindingRequest)(nil),         // 61: olivetin.api.v1.GetActionBindingRequest
+	(*GetActionBindingResponse)(nil),        // 62: olivetin.api.v1.GetActionBindingResponse
+	(*GetEntitiesRequest)(nil),              // 63: olivetin.api.v1.GetEntitiesRequest
+	(*GetEntitiesResponse)(nil),             // 64: olivetin.api.v1.GetEntitiesResponse
+	(*EntityDefinition)(nil),                // 65: olivetin.api.v1.EntityDefinition
+	(*GetEntityRequest)(nil),                // 66: olivetin.api.v1.GetEntityRequest
+	(*RestartActionRequest)(nil),            // 67: olivetin.api.v1.RestartActionRequest
+	nil,                                     // 68: olivetin.api.v1.ActionArgument.SuggestionsEntry
+	nil,                                     // 69: olivetin.api.v1.Entity.FieldsEntry
+	nil,                                     // 70: olivetin.api.v1.DumpVarsResponse.ContentsEntry
+	nil,                                     // 71: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
+}
+var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
+	1,  // 0: olivetin.api.v1.Action.arguments:type_name -> olivetin.api.v1.ActionArgument
+	2,  // 1: olivetin.api.v1.ActionArgument.choices:type_name -> olivetin.api.v1.ActionArgumentChoice
+	68, // 2: olivetin.api.v1.ActionArgument.suggestions:type_name -> olivetin.api.v1.ActionArgument.SuggestionsEntry
+	69, // 3: olivetin.api.v1.Entity.fields:type_name -> olivetin.api.v1.Entity.FieldsEntry
+	7,  // 4: olivetin.api.v1.GetDashboardResponse.dashboard:type_name -> olivetin.api.v1.Dashboard
+	8,  // 5: olivetin.api.v1.Dashboard.contents:type_name -> olivetin.api.v1.DashboardComponent
+	8,  // 6: olivetin.api.v1.DashboardComponent.contents:type_name -> olivetin.api.v1.DashboardComponent
+	0,  // 7: olivetin.api.v1.DashboardComponent.action:type_name -> olivetin.api.v1.Action
+	10, // 8: olivetin.api.v1.StartActionRequest.arguments:type_name -> olivetin.api.v1.StartActionArgument
+	10, // 9: olivetin.api.v1.StartActionAndWaitRequest.arguments:type_name -> olivetin.api.v1.StartActionArgument
+	19, // 10: olivetin.api.v1.StartActionAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
+	19, // 11: olivetin.api.v1.StartActionByGetAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
+	19, // 12: olivetin.api.v1.GetLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry
+	19, // 13: olivetin.api.v1.GetActionLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry
+	19, // 14: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
+	70, // 15: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry
+	71, // 16: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
+	43, // 17: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged
+	44, // 18: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged
+	45, // 19: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished
+	46, // 20: olivetin.api.v1.EventStreamResponse.execution_started:type_name -> olivetin.api.v1.EventExecutionStarted
+	42, // 21: olivetin.api.v1.EventStreamResponse.output_chunk:type_name -> olivetin.api.v1.EventOutputChunk
+	19, // 22: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry
+	19, // 23: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry
+	60, // 24: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider
+	59, // 25: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink
+	5,  // 26: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy
+	0,  // 27: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action
+	65, // 28: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition
+	3,  // 29: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity
+	35, // 30: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding
+	6,  // 31: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest
+	9,  // 32: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest
+	12, // 33: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest
+	14, // 34: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest
+	16, // 35: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest
+	67, // 36: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest
+	47, // 37: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest
+	27, // 38: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest
+	18, // 39: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest
+	21, // 40: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest
+	23, // 41: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest
+	29, // 42: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest
+	31, // 43: olivetin.api.v1.OliveTinApiService.SosReport:input_type -> olivetin.api.v1.SosReportRequest
+	33, // 44: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest
+	36, // 45: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest
+	38, // 46: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest
+	49, // 47: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest
+	51, // 48: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest
+	53, // 49: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest
+	40, // 50: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest
+	55, // 51: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest
+	57, // 52: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest
+	61, // 53: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest
+	63, // 54: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest
+	66, // 55: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest
+	4,  // 56: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse
+	11, // 57: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse
+	13, // 58: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse
+	15, // 59: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse
+	17, // 60: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse
+	11, // 61: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse
+	48, // 62: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse
+	28, // 63: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse
+	20, // 64: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse
+	22, // 65: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse
+	24, // 66: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse
+	30, // 67: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse
+	32, // 68: olivetin.api.v1.OliveTinApiService.SosReport:output_type -> olivetin.api.v1.SosReportResponse
+	34, // 69: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse
+	37, // 70: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse
+	39, // 71: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse
+	50, // 72: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse
+	52, // 73: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse
+	54, // 74: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse
+	41, // 75: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse
+	56, // 76: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse
+	58, // 77: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse
+	62, // 78: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse
+	64, // 79: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse
+	3,  // 80: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity
+	56, // [56:81] is the sub-list for method output_type
+	31, // [31:56] is the sub-list for method input_type
+	31, // [31:31] is the sub-list for extension type_name
+	31, // [31:31] is the sub-list for extension extendee
+	0,  // [0:31] is the sub-list for field type_name
+}
+
+func init() { file_olivetin_api_v1_olivetin_proto_init() }
+func file_olivetin_api_v1_olivetin_proto_init() {
+	if File_olivetin_api_v1_olivetin_proto != nil {
+		return
+	}
+	file_olivetin_api_v1_olivetin_proto_msgTypes[41].OneofWrappers = []any{
+		(*EventStreamResponse_EntityChanged)(nil),
+		(*EventStreamResponse_ConfigChanged)(nil),
+		(*EventStreamResponse_ExecutionFinished)(nil),
+		(*EventStreamResponse_ExecutionStarted)(nil),
+		(*EventStreamResponse_OutputChunk)(nil),
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			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:   72,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_olivetin_api_v1_olivetin_proto_goTypes,
+		DependencyIndexes: file_olivetin_api_v1_olivetin_proto_depIdxs,
+		MessageInfos:      file_olivetin_api_v1_olivetin_proto_msgTypes,
+	}.Build()
+	File_olivetin_api_v1_olivetin_proto = out.File
+	file_olivetin_api_v1_olivetin_proto_goTypes = nil
+	file_olivetin_api_v1_olivetin_proto_depIdxs = nil
+}

+ 8 - 5
service/go.mod

@@ -10,14 +10,15 @@ require (
 	connectrpc.com/connect v1.19.1
 	github.com/Masterminds/semver v1.5.0
 	github.com/MicahParks/keyfunc/v3 v3.7.0
+	github.com/PaesslerAG/jsonpath v0.1.1
 	github.com/alexedwards/argon2id v1.0.0
-	github.com/bufbuild/buf v1.61.0
+	github.com/bufbuild/buf v1.63.0
 	github.com/fsnotify/fsnotify v1.9.0
 	github.com/fzipp/gocyclo v0.6.0
 	github.com/go-critic/go-critic v0.14.2
 	github.com/golang-jwt/jwt/v5 v5.3.0
 	github.com/google/uuid v1.6.0
-	github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c
+	github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac
 	github.com/knadh/koanf/parsers/yaml v1.1.0
 	github.com/knadh/koanf/providers/env v1.1.0
 	github.com/knadh/koanf/providers/file v1.2.1
@@ -51,13 +52,14 @@ require (
 	buf.build/go/spdx v0.2.0 // indirect
 	buf.build/go/standard v0.1.0 // indirect
 	cel.dev/expr v0.25.1 // indirect
-	connectrpc.com/otelconnect v0.8.0 // indirect
+	connectrpc.com/otelconnect v0.9.0 // indirect
 	github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
 	github.com/MicahParks/jwkset v0.11.0 // indirect
 	github.com/Microsoft/go-winio v0.6.2 // indirect
+	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.20251223142729-db46c1b9d34e // indirect
+	github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 // 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
@@ -112,7 +114,7 @@ require (
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
-	github.com/prometheus/common v0.67.4 // indirect
+	github.com/prometheus/common v0.67.5 // indirect
 	github.com/prometheus/procfs v0.19.2 // indirect
 	github.com/quasilyte/go-ruleguard v0.4.5 // indirect
 	github.com/quasilyte/gogrep v0.5.0 // indirect
@@ -125,6 +127,7 @@ require (
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/segmentio/asm v1.2.1 // indirect
 	github.com/segmentio/encoding v0.5.3 // indirect
+	github.com/shopspring/decimal v1.4.0 // indirect
 	github.com/spf13/cobra v1.10.2 // indirect
 	github.com/spf13/pflag v1.0.10 // indirect
 	github.com/stoewer/go-strcase v1.3.1 // indirect

+ 24 - 0
service/go.sum

@@ -32,6 +32,8 @@ connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
 connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
 connectrpc.com/otelconnect v0.8.0 h1:a4qrN4H8aEE2jAoCxheZYYfEjXMgVPyL9OzPQLBEFXU=
 connectrpc.com/otelconnect v0.8.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
+connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA=
+connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
 github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
 github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
 github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
@@ -42,6 +44,13 @@ github.com/MicahParks/keyfunc/v3 v3.7.0 h1:pdafUNyq+p3ZlvjJX1HWFP7MA3+cLpDtg69U3
 github.com/MicahParks/keyfunc/v3 v3.7.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8=
+github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I=
+github.com/PaesslerAG/gval v1.2.4 h1:rhX7MpjJlcxYwL2eTTYIOBUyEKZ+A96T9vQySWkVUiU=
+github.com/PaesslerAG/gval v1.2.4/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
+github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
+github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
+github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
 github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
 github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
 github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
@@ -54,8 +63,12 @@ github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo
 github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
 github.com/bufbuild/buf v1.61.0 h1:JPaK/RM2eoheyzznW+1LxaFgN6xjBCi8s25q2kUbH9A=
 github.com/bufbuild/buf v1.61.0/go.mod h1:Xs3leBmxjL5tTnSVYfNwNXHXD1k5et3fR/tJyIyQl4s=
+github.com/bufbuild/buf v1.63.0 h1:vMIRozWqYcOU992FqcGgAp8LjoWVTzr52qEolUT+xi4=
+github.com/bufbuild/buf v1.63.0/go.mod h1:IWF+TIxwmk4DeyDmguN8WhxKFKQitcX2WnP+RlJlDiY=
 github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e h1:LQA+1MyiPkolGHJGC2GMDC5Xu+0RDVH6jGMKech7Exs=
 github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
+github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 h1:cQYwUyAzyMmYr7AyJU1C6pVCpUrJJBkmx7UunZosxxs=
+github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
 github.com/bufbuild/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=
@@ -152,6 +165,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c h1:v8gN2xXFQjkF0PsoGSqDviRNmPHcBsvl6rMSbvXz1sM=
 github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
+github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac h1:JQ6AC9tf2xhwTxMY9nuIeOPM7Cj0BDeCNgKPrNgQvtQ=
+github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac/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/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88=
@@ -216,6 +231,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
 github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
 github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
+github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
+github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
 github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
 github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
 github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4=
@@ -242,12 +259,17 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 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 v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
+github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
 github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
 github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
 github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
+github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
@@ -293,6 +315,7 @@ go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
 go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
 go.opentelemetry.io/otel/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/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=
@@ -305,6 +328,7 @@ go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6
 go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
 go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
 go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
+go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=

+ 61 - 29
service/internal/api/api.go

@@ -16,6 +16,7 @@ import (
 	"fmt"
 	"net/http"
 	"sync"
+	"time"
 
 	acl "github.com/OliveTin/OliveTin/internal/acl"
 	auth "github.com/OliveTin/OliveTin/internal/auth"
@@ -275,23 +276,37 @@ func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Re
 	}
 }
 
+func calculateRateLimitExpires(api *oliveTinAPI, logEntry *executor.InternalLogEntry) string {
+	if logEntry.Binding == nil || logEntry.Binding.Action == nil {
+		return ""
+	}
+
+	expiryUnix := api.executor.GetTimeUntilAvailable(logEntry.Binding)
+	if expiryUnix <= 0 {
+		return ""
+	}
+
+	return time.Unix(expiryUnix, 0).Format("2006-01-02 15:04:05")
+}
+
 func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *authpublic.AuthenticatedUser) *apiv1.LogEntry {
 	pble := &apiv1.LogEntry{
-		ActionTitle:         logEntry.ActionTitle,
-		ActionIcon:          logEntry.ActionIcon,
-		ActionId:            logEntry.ActionId,
-		DatetimeStarted:     logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
-		DatetimeFinished:    logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
-		DatetimeIndex:       logEntry.Index,
-		Output:              logEntry.Output,
-		TimedOut:            logEntry.TimedOut,
-		Blocked:             logEntry.Blocked,
-		ExitCode:            logEntry.ExitCode,
-		Tags:                logEntry.Tags,
-		ExecutionTrackingId: logEntry.ExecutionTrackingID,
-		ExecutionStarted:    logEntry.ExecutionStarted,
-		ExecutionFinished:   logEntry.ExecutionFinished,
-		User:                logEntry.Username,
+		ActionTitle:              logEntry.ActionTitle,
+		ActionIcon:               logEntry.ActionIcon,
+		DatetimeStarted:          logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
+		DatetimeFinished:         logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
+		DatetimeIndex:            logEntry.Index,
+		Output:                   logEntry.Output,
+		TimedOut:                 logEntry.TimedOut,
+		Blocked:                  logEntry.Blocked,
+		ExitCode:                 logEntry.ExitCode,
+		Tags:                     logEntry.Tags,
+		ExecutionTrackingId:      logEntry.ExecutionTrackingID,
+		ExecutionStarted:         logEntry.ExecutionStarted,
+		ExecutionFinished:        logEntry.ExecutionFinished,
+		User:                     logEntry.Username,
+		BindingId:                logEntry.Binding.ID,
+		DatetimeRateLimitExpires: calculateRateLimitExpires(api, logEntry),
 	}
 
 	if !pble.ExecutionFinished {
@@ -311,10 +326,20 @@ func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string
 	return logEntry
 }
 
-func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
+// This is the actual action ID, not the binding ID.
+func getMostRecentExecutionStatusByActionId(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
 	var ile *executor.InternalLogEntry
 
-	logs := api.executor.GetLogsByActionId(actionId)
+	binding := api.executor.FindBindingByID(actionId)
+	if binding == nil {
+		return nil
+	}
+
+	logs := api.executor.GetLogsByBindingId(binding.ID)
+
+	if len(logs) == 0 {
+		return nil
+	}
 
 	if len(logs) == 0 {
 		return nil
@@ -341,7 +366,7 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[ap
 		ile = getExecutionStatusByTrackingID(api, req.Msg.ExecutionTrackingId)
 
 	} else {
-		ile = getMostRecentExecutionStatusById(api, req.Msg.ActionId)
+		ile = getMostRecentExecutionStatusByActionId(api, req.Msg.ActionId)
 	}
 
 	if ile == nil {
@@ -474,7 +499,11 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetL
 	}
 
 	ret := &apiv1.GetLogsResponse{}
-	logEntries, paging := api.executor.GetLogTrackingIdsACL(api.cfg, user, req.Msg.StartOffset, api.cfg.LogHistoryPageSize)
+	dateFilter := ""
+	if req.Msg.DateFilter != "" {
+		dateFilter = req.Msg.DateFilter
+	}
+	logEntries, paging := api.executor.GetLogTrackingIdsACL(api.cfg, user, req.Msg.StartOffset, api.cfg.LogHistoryPageSize, dateFilter)
 	for _, le := range logEntries {
 		ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
 	}
@@ -539,7 +568,7 @@ func (api *oliveTinAPI) GetActionLogs(ctx ctx.Context, req *connect.Request[apiv
 		return nil, err
 	}
 
-	filtered := api.filterLogsByACL(api.executor.GetLogsByActionId(req.Msg.ActionId), user)
+	filtered := api.filterLogsByACL(api.executor.GetLogsByBindingId(req.Msg.ActionId), user)
 	page := paginate(int64(len(filtered)), api.cfg.LogHistoryPageSize, req.Msg.StartOffset)
 	if page.empty {
 		return connect.NewResponse(buildEmptyPageResponse(page)), nil
@@ -688,7 +717,7 @@ func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.Dum
 
 func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Request[apiv1.DumpPublicIdActionMapRequest]) (*connect.Response[apiv1.DumpPublicIdActionMapResponse], error) {
 	res := &apiv1.DumpPublicIdActionMapResponse{}
-	res.Contents = make(map[string]*apiv1.ActionEntityPair)
+	res.Contents = make(map[string]*apiv1.DebugBinding)
 
 	if !api.cfg.InsecureAllowDumpActionMap {
 		res.Alert = "Dumping Public IDs is disallowed."
@@ -696,16 +725,15 @@ func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Requ
 		return connect.NewResponse(res), nil
 	}
 
-	api.executor.MapActionIdToBindingLock.RLock()
+	api.executor.MapActionBindingsLock.RLock()
 
-	for k, v := range api.executor.MapActionIdToBinding {
-		res.Contents[k] = &apiv1.ActionEntityPair{
-			ActionTitle:  v.Action.Title,
-			EntityPrefix: "?",
+	for k, v := range api.executor.MapActionBindings {
+		res.Contents[k] = &apiv1.DebugBinding{
+			ActionTitle: v.Action.Title,
 		}
 	}
 
-	api.executor.MapActionIdToBindingLock.RUnlock()
+	api.executor.MapActionBindingsLock.RUnlock()
 
 	res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpActionMap = false again after you don't need it anymore"
 
@@ -723,6 +751,10 @@ func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *connect.Request[apiv1.Ge
 func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.EventStreamRequest], srv *connect.ServerStream[apiv1.EventStreamResponse]) error {
 	log.Debugf("EventStream: %v", req.Msg)
 
+	// Set X-Accel-Buffering header to disable nginx buffering for this stream
+	// https://github.com/OliveTin/OliveTin/issues/765
+	srv.ResponseHeader().Set("X-Accel-Buffering", "no")
+
 	user := auth.UserFromApiCall(ctx, req, api.cfg)
 
 	if err := api.checkDashboardAccess(user); err != nil {
@@ -809,7 +841,7 @@ func (api *oliveTinAPI) OnExecutionStarted(ex *executor.InternalLogEntry) {
 	}
 }
 
-func (api *oliveTinAPI) OnExecutionFinished(ex *executor.InternalLogEntry) {
+func (api *oliveTinAPI) OnExecutionFinished(ile *executor.InternalLogEntry) {
 	toRemove := []*streamingClient{}
 
 	for _, client := range api.copyOfStreamingClients() {
@@ -817,7 +849,7 @@ func (api *oliveTinAPI) OnExecutionFinished(ex *executor.InternalLogEntry) {
 		case client.channel <- &apiv1.EventStreamResponse{
 			Event: &apiv1.EventStreamResponse_ExecutionFinished{
 				ExecutionFinished: &apiv1.EventExecutionFinished{
-					LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
+					LogEntry: api.internalLogEntryToPb(ile, client.AuthenticatedUser),
 				},
 			},
 		}:

+ 79 - 17
service/internal/api/apiActions.go

@@ -1,6 +1,12 @@
 package api
 
 import (
+	"strconv"
+	"strings"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+
 	apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
 	acl "github.com/OliveTin/OliveTin/internal/acl"
 	authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
@@ -22,10 +28,10 @@ func (rr *DashboardRenderRequest) findAction(title string) *apiv1.Action {
 }
 
 func (rr *DashboardRenderRequest) findActionForEntity(title string, entity *entities.Entity) *apiv1.Action {
-	rr.ex.MapActionIdToBindingLock.RLock()
-	defer rr.ex.MapActionIdToBindingLock.RUnlock()
+	rr.ex.MapActionBindingsLock.RLock()
+	defer rr.ex.MapActionBindingsLock.RUnlock()
 
-	for _, binding := range rr.ex.MapActionIdToBinding {
+	for _, binding := range rr.ex.MapActionBindings {
 		if binding.Action.Title != title {
 			continue
 		}
@@ -55,28 +61,84 @@ func buildEffectivePolicy(policy *config.ConfigurationPolicy) *apiv1.EffectivePo
 	return ret
 }
 
+func evaluateEnabledExpression(action *config.Action, entity *entities.Entity) bool {
+	if action.EnabledExpression == "" {
+		return true
+	}
+
+	result := entities.ParseTemplateWith(action.EnabledExpression, entity)
+	result = strings.TrimSpace(result)
+
+	if result == "" {
+		return false
+	}
+
+	if isTemplateError(result, action) {
+		return false
+	}
+
+	return evaluateResultValue(result)
+}
+
+func isTemplateError(result string, action *config.Action) bool {
+	if !strings.HasPrefix(result, "tpl ") || !strings.Contains(result, "error") {
+		return false
+	}
+
+	log.WithFields(log.Fields{
+		"actionTitle":       action.Title,
+		"enabledExpression": action.EnabledExpression,
+		"result":            result,
+	}).Warn("enabledExpression template evaluation failed, treating as disabled")
+	return true
+}
+
+func evaluateResultValue(result string) bool {
+	if strings.EqualFold(result, "true") {
+		return true
+	}
+
+	if num, err := strconv.Atoi(result); err == nil {
+		return num != 0
+	}
+
+	return false
+}
+
 func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderRequest) *apiv1.Action {
 	action := actionBinding.Action
 
+	aclCanExec := acl.IsAllowedExec(rr.cfg, rr.AuthenticatedUser, action)
+	enabledExprCanExec := evaluateEnabledExpression(action, actionBinding.Entity)
+
+	// Calculate rate limit expiry time
+	expiryUnix := rr.ex.GetTimeUntilAvailable(actionBinding)
+	datetimeRateLimitExpires := ""
+	if expiryUnix > 0 {
+		datetimeRateLimitExpires = time.Unix(expiryUnix, 0).Format("2006-01-02 15:04:05")
+	}
+
 	btn := apiv1.Action{
-		BindingId:    actionBinding.ID,
-		Title:        entities.ParseTemplateWith(action.Title, actionBinding.Entity),
-		Icon:         entities.ParseTemplateWith(action.Icon, actionBinding.Entity),
-		CanExec:      acl.IsAllowedExec(rr.cfg, rr.AuthenticatedUser, action),
-		PopupOnStart: action.PopupOnStart,
-		Order:        int32(actionBinding.ConfigOrder),
-		Timeout:      int32(action.Timeout),
+		BindingId:                actionBinding.ID,
+		Title:                    entities.ParseTemplateWith(action.Title, actionBinding.Entity),
+		Icon:                     entities.ParseTemplateWith(action.Icon, actionBinding.Entity),
+		CanExec:                  aclCanExec && enabledExprCanExec,
+		PopupOnStart:             action.PopupOnStart,
+		Order:                    int32(actionBinding.ConfigOrder),
+		Timeout:                  int32(action.Timeout),
+		DatetimeRateLimitExpires: datetimeRateLimitExpires,
 	}
 
 	for _, cfgArg := range action.Arguments {
 		pbArg := apiv1.ActionArgument{
-			Name:         cfgArg.Name,
-			Title:        cfgArg.Title,
-			Type:         cfgArg.Type,
-			Description:  cfgArg.Description,
-			DefaultValue: cfgArg.Default,
-			Choices:      buildChoices(cfgArg),
-			Suggestions:  cfgArg.Suggestions,
+			Name:                  cfgArg.Name,
+			Title:                 cfgArg.Title,
+			Type:                  cfgArg.Type,
+			Description:           cfgArg.Description,
+			DefaultValue:          cfgArg.Default,
+			Choices:               buildChoices(cfgArg),
+			Suggestions:           cfgArg.Suggestions,
+			SuggestionsBrowserKey: cfgArg.SuggestionsBrowserKey,
 		}
 
 		btn.Arguments = append(btn.Arguments, &pbArg)

+ 165 - 3
service/internal/api/api_test.go

@@ -11,6 +11,7 @@ import (
 
 	apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
 	apiv1connect "github.com/OliveTin/OliveTin/gen/olivetin/api/v1/apiv1connect"
+	authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
 	config "github.com/OliveTin/OliveTin/internal/config"
 	"github.com/OliveTin/OliveTin/internal/entities"
 	"github.com/OliveTin/OliveTin/internal/executor"
@@ -20,7 +21,7 @@ import (
 	"path"
 )
 
-func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*httptest.Server, apiv1connect.OliveTinApiServiceClient) {
+func getNewTestServerAndClient(injectedConfig *config.Config) (*httptest.Server, apiv1connect.OliveTinApiServiceClient) {
 	ex := executor.DefaultExecutor(injectedConfig)
 	ex.RebuildActionMap()
 
@@ -62,7 +63,7 @@ func TestGetActionsAndStart(t *testing.T) {
 	ex := executor.DefaultExecutor(cfg)
 	ex.RebuildActionMap()
 
-	conn, client := getNewTestServerAndClient(t, cfg)
+	conn, client := getNewTestServerAndClient(cfg)
 
 	respInit, errInit := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
 	respGetReady, errReady := client.GetReadyz(context.Background(), connect.NewRequest(&apiv1.GetReadyzRequest{}))
@@ -98,7 +99,7 @@ func TestGetActionsAndStart(t *testing.T) {
 func TestGetEntities(t *testing.T) {
 	cfg := config.DefaultConfig()
 
-	ts, client := getNewTestServerAndClient(t, cfg)
+	ts, client := getNewTestServerAndClient(cfg)
 	defer ts.Close()
 
 	setupTestEntities()
@@ -173,3 +174,164 @@ func validateConsistency(t *testing.T, client apiv1connect.OliveTinApiServiceCli
 		}
 	}
 }
+
+func TestEvaluateEnabledExpression(t *testing.T) {
+	tests := []struct {
+		name           string
+		expression     string
+		entity         *entities.Entity
+		expectedResult bool
+	}{
+		{
+			name:           "empty expression returns true",
+			expression:     "",
+			entity:         nil,
+			expectedResult: true,
+		},
+		{
+			name:           "literal true returns true",
+			expression:     "true",
+			entity:         nil,
+			expectedResult: true,
+		},
+		{
+			name:           "literal True returns true (case insensitive)",
+			expression:     "True",
+			entity:         nil,
+			expectedResult: true,
+		},
+		{
+			name:           "literal 1 returns true",
+			expression:     "1",
+			entity:         nil,
+			expectedResult: true,
+		},
+		{
+			name:           "literal false returns false",
+			expression:     "false",
+			entity:         nil,
+			expectedResult: false,
+		},
+		{
+			name:           "literal 0 returns false",
+			expression:     "0",
+			entity:         nil,
+			expectedResult: false,
+		},
+		{
+			name:           "empty result returns false",
+			expression:     "{{ .NonExistent }}",
+			entity:         nil,
+			expectedResult: false,
+		},
+		{
+			name:           "expression with CurrentEntity true",
+			expression:     "{{ eq .CurrentEntity.powered_on true }}",
+			entity:         &entities.Entity{Data: map[string]any{"powered_on": true}},
+			expectedResult: true,
+		},
+		{
+			name:           "expression with CurrentEntity false",
+			expression:     "{{ eq .CurrentEntity.powered_on true }}",
+			entity:         &entities.Entity{Data: map[string]any{"powered_on": false}},
+			expectedResult: false,
+		},
+		{
+			name:           "expression with CurrentEntity integer 1",
+			expression:     "{{ .CurrentEntity.status }}",
+			entity:         &entities.Entity{Data: map[string]any{"status": 1}},
+			expectedResult: true,
+		},
+		{
+			name:           "expression with CurrentEntity integer 0",
+			expression:     "{{ .CurrentEntity.status }}",
+			entity:         &entities.Entity{Data: map[string]any{"status": 0}},
+			expectedResult: false,
+		},
+		{
+			name:           "template parse error returns false",
+			expression:     "{{ invalid syntax }}",
+			entity:         nil,
+			expectedResult: false,
+		},
+		{
+			name:           "template exec error returns false",
+			expression:     "{{ .CurrentEntity.nonexistent }}",
+			entity:         nil,
+			expectedResult: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			action := &config.Action{
+				EnabledExpression: tt.expression,
+			}
+			result := evaluateEnabledExpression(action, tt.entity)
+			assert.Equal(t, tt.expectedResult, result, "evaluateEnabledExpression should return expected result")
+		})
+	}
+}
+
+func TestBuildActionWithEnabledExpression(t *testing.T) {
+	cfg := config.DefaultConfig()
+	cfg.DefaultPermissions.Exec = true
+
+	action := &config.Action{
+		Title:             "Test Action",
+		Shell:             "echo test",
+		EnabledExpression: "{{ eq .CurrentEntity.enabled true }}",
+	}
+	cfg.Actions = append(cfg.Actions, action)
+
+	ex := executor.DefaultExecutor(cfg)
+	ex.RebuildActionMap()
+
+	binding := findBindingByTitle(ex, "Test Action")
+	assert.NotNil(t, binding, "Binding should be found")
+
+	rr := &DashboardRenderRequest{
+		AuthenticatedUser: &authpublic.AuthenticatedUser{Username: "testuser"},
+		cfg:               cfg,
+		ex:                ex,
+	}
+
+	testWithEntity(t, binding, rr, true, true, "Action should be executable when entity.enabled is true")
+	testWithEntity(t, binding, rr, false, false, "Action should not be executable when entity.enabled is false")
+
+	bindingNoExpr := findBindingByTitle(ex, "Test Action No Expression")
+	if bindingNoExpr == nil {
+		actionNoExpression := &config.Action{
+			Title: "Test Action No Expression",
+			Shell: "echo test",
+		}
+		cfg.Actions = append(cfg.Actions, actionNoExpression)
+		ex.RebuildActionMap()
+		bindingNoExpr = findBindingByTitle(ex, "Test Action No Expression")
+	}
+
+	actionResult := buildAction(bindingNoExpr, rr)
+	assert.True(t, actionResult.CanExec, "Action without enabledExpression should be executable")
+}
+
+func findBindingByTitle(ex *executor.Executor, title string) *executor.ActionBinding {
+	ex.MapActionBindingsLock.RLock()
+	defer ex.MapActionBindingsLock.RUnlock()
+
+	for _, b := range ex.MapActionBindings {
+		if b.Action.Title == title {
+			return b
+		}
+	}
+	return nil
+}
+
+func testWithEntity(t *testing.T, binding *executor.ActionBinding, rr *DashboardRenderRequest, enabled bool, expectedCanExec bool, message string) {
+	binding.Entity = &entities.Entity{
+		UniqueKey: "test-entity",
+		Data:      map[string]any{"enabled": enabled},
+	}
+
+	actionResult := buildAction(binding, rr)
+	assert.Equal(t, expectedCanExec, actionResult.CanExec, message)
+}

+ 1 - 1
service/internal/api/dashboards.go

@@ -128,7 +128,7 @@ func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard {
 		Contents: make([]*apiv1.DashboardComponent, 0),
 	}
 
-	for _, binding := range rr.ex.MapActionIdToBinding {
+	for _, binding := range rr.ex.MapActionBindings {
 		if binding.Action.Hidden {
 			continue
 		}

+ 24 - 9
service/internal/config/config.go

@@ -22,25 +22,28 @@ type Action struct {
 	ExecOnFileCreatedInDir []string         `koanf:"execOnFileCreatedInDir"`
 	ExecOnFileChangedInDir []string         `koanf:"execOnFileChangedInDir"`
 	ExecOnCalendarFile     string           `koanf:"execOnCalendarFile"`
+	ExecOnWebhook          []WebhookConfig  `koanf:"execOnWebhook"`
 	Triggers               []string         `koanf:"triggers"`
 	MaxConcurrent          int              `koanf:"maxConcurrent"`
 	MaxRate                []RateSpec       `koanf:"maxRate"`
 	Arguments              []ActionArgument `koanf:"arguments"`
 	PopupOnStart           string           `koanf:"popupOnStart"`
 	SaveLogs               SaveLogsConfig   `koanf:"saveLogs"`
+	EnabledExpression      string           `koanf:"enabledExpression"`
 }
 
 // ActionArgument objects appear on Actions.
 type ActionArgument struct {
-	Name        string                 `koanf:"name"`
-	Title       string                 `koanf:"title"`
-	Description string                 `koanf:"description"`
-	Type        string                 `koanf:"type"`
-	Default     string                 `koanf:"default"`
-	Choices     []ActionArgumentChoice `koanf:"choices"`
-	Entity      string                 `koanf:"entity"`
-	RejectNull  bool                   `koanf:"rejectNull"`
-	Suggestions map[string]string      `koanf:"suggestions"`
+	Name                  string                 `koanf:"name"`
+	Title                 string                 `koanf:"title"`
+	Description           string                 `koanf:"description"`
+	Type                  string                 `koanf:"type"`
+	Default               string                 `koanf:"default"`
+	Choices               []ActionArgumentChoice `koanf:"choices"`
+	Entity                string                 `koanf:"entity"`
+	RejectNull            bool                   `koanf:"rejectNull"`
+	Suggestions           map[string]string      `koanf:"suggestions"`
+	SuggestionsBrowserKey string                 `koanf:"suggestionsBrowserKey"`
 }
 
 // ActionArgumentChoice represents a predefined choice for an argument.
@@ -55,6 +58,18 @@ type RateSpec struct {
 	Duration string `koanf:"duration"`
 }
 
+// WebhookConfig defines configuration for generic webhook triggers.
+type WebhookConfig struct {
+	Secret       string            `koanf:"secret"`       // Optional: secret for signature verification
+	AuthType     string            `koanf:"authType"`     // Optional: "hmac-sha256", "hmac-sha1", "bearer", "basic", "none"
+	AuthHeader   string            `koanf:"authHeader"`   // Optional: custom header name for auth (default: "X-Webhook-Signature")
+	MatchHeaders map[string]string `koanf:"matchHeaders"` // Match HTTP headers
+	MatchPath    string            `koanf:"matchPath"`    // JSONPath expression to match in request body (format: "jsonpath=value" or just "jsonpath")
+	MatchQuery   map[string]string `koanf:"matchQuery"`   // Match URL query parameters
+	Extract      map[string]string `koanf:"extract"`      // Map action argument names to JSONPath expressions
+	Template     string            `koanf:"template"`     // Optional: template name (e.g., "github-push", "github-pr")
+}
+
 // Entity represents a "thing" that can have multiple actions associated with it.
 // for example, a media player with a start and stop action.
 type EntityFile struct {

+ 159 - 19
service/internal/executor/executor.go

@@ -49,12 +49,12 @@ type ActionBinding struct {
 type Executor struct {
 	logs                  map[string]*InternalLogEntry
 	logsTrackingIdsByDate []string
-	LogsByActionId        map[string][]*InternalLogEntry
+	LogsByBindingId       map[string][]*InternalLogEntry
 
 	logmutex sync.RWMutex
 
-	MapActionIdToBinding     map[string]*ActionBinding
-	MapActionIdToBindingLock sync.RWMutex
+	MapActionBindings     map[string]*ActionBinding
+	MapActionBindingsLock sync.RWMutex
 
 	Cfg *config.Config
 
@@ -86,7 +86,6 @@ type ExecutionRequest struct {
 // easily serializable.
 type InternalLogEntry struct {
 	Binding             *ActionBinding
-	BindingID           string
 	DatetimeStarted     time.Time
 	DatetimeFinished    time.Time
 	Output              string
@@ -110,7 +109,6 @@ type InternalLogEntry struct {
 	*/
 	ActionTitle string
 	ActionIcon  string
-	ActionId    string
 }
 
 type executorStepFunc func(*ExecutionRequest) bool
@@ -122,8 +120,8 @@ func DefaultExecutor(cfg *config.Config) *Executor {
 	e.Cfg = cfg
 	e.logs = make(map[string]*InternalLogEntry)
 	e.logsTrackingIdsByDate = make([]string, 0)
-	e.LogsByActionId = make(map[string][]*InternalLogEntry)
-	e.MapActionIdToBinding = make(map[string]*ActionBinding)
+	e.LogsByBindingId = make(map[string][]*InternalLogEntry)
+	e.MapActionBindings = make(map[string]*ActionBinding)
 
 	e.chainOfCommand = []executorStepFunc{
 		stepRequestAction,
@@ -236,12 +234,27 @@ func isLogEntryAllowedByACL(cfg *config.Config, user *authpublic.AuthenticatedUs
 	return acl.IsAllowedLogs(cfg, user, entry.Binding.Action)
 }
 
-func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.AuthenticatedUser) []*InternalLogEntry {
+func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.AuthenticatedUser, dateFilter string) []*InternalLogEntry {
 	e.logmutex.RLock()
 	defer e.logmutex.RUnlock()
 
 	filtered := make([]*InternalLogEntry, 0, len(e.logsTrackingIdsByDate))
 
+	var filterDate time.Time
+	var hasDateFilter bool
+	if dateFilter != "" {
+		parsedDate, err := time.Parse("2006-01-02", dateFilter)
+		if err != nil {
+			log.WithFields(log.Fields{
+				"dateFilter": dateFilter,
+				"error":      err,
+			}).Errorf("Failed to parse date filter, expected format YYYY-MM-DD")
+		} else {
+			filterDate = parsedDate
+			hasDateFilter = true
+		}
+	}
+
 	for _, trackingId := range e.logsTrackingIdsByDate {
 		entry := e.logs[trackingId]
 
@@ -249,6 +262,13 @@ func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.Authenti
 			continue
 		}
 		if isLogEntryAllowedByACL(cfg, user, entry) {
+			if hasDateFilter {
+				entryDate := entry.DatetimeStarted.UTC().Truncate(24 * time.Hour)
+				filterDateUTC := filterDate.UTC().Truncate(24 * time.Hour)
+				if !entryDate.Equal(filterDateUTC) {
+					continue
+				}
+			}
 			filtered = append(filtered, entry)
 		}
 	}
@@ -282,8 +302,9 @@ func paginateFilteredLogs(filtered []*InternalLogEntry, startOffset int64, pageC
 
 // GetLogTrackingIdsACL returns logs filtered by ACL visibility for the user and
 // paginated correctly based on the filtered set.
-func (e *Executor) GetLogTrackingIdsACL(cfg *config.Config, user *authpublic.AuthenticatedUser, startOffset int64, pageCount int64) ([]*InternalLogEntry, *PagingResult) {
-	filtered := e.filterLogsByACL(cfg, user)
+// dateFilter is optional and should be in YYYY-MM-DD format. If empty, no date filtering is applied.
+func (e *Executor) GetLogTrackingIdsACL(cfg *config.Config, user *authpublic.AuthenticatedUser, startOffset int64, pageCount int64, dateFilter string) ([]*InternalLogEntry, *PagingResult) {
+	filtered := e.filterLogsByACL(cfg, user, dateFilter)
 	return paginateFilteredLogs(filtered, startOffset, pageCount)
 }
 
@@ -297,10 +318,10 @@ func (e *Executor) GetLog(trackingID string) (*InternalLogEntry, bool) {
 	return entry, found
 }
 
-func (e *Executor) GetLogsByActionId(actionId string) []*InternalLogEntry {
+func (e *Executor) GetLogsByBindingId(bindingId string) []*InternalLogEntry {
 	e.logmutex.RLock()
 
-	logs, found := e.LogsByActionId[actionId]
+	logs, found := e.LogsByBindingId[bindingId]
 
 	e.logmutex.RUnlock()
 
@@ -311,6 +332,122 @@ func (e *Executor) GetLogsByActionId(actionId string) []*InternalLogEntry {
 	return logs
 }
 
+// shouldCountExecution checks if a log entry should be counted for rate limiting.
+func shouldCountExecution(logEntry *InternalLogEntry, windowStart time.Time) bool {
+	return !logEntry.Blocked && logEntry.DatetimeStarted.After(windowStart)
+}
+
+// updateOldestExecution updates the oldest execution time if this entry is older.
+func updateOldestExecution(oldestExecutionTime **time.Time, logEntry *InternalLogEntry) {
+	if *oldestExecutionTime == nil {
+		*oldestExecutionTime = &logEntry.DatetimeStarted
+	} else if logEntry.DatetimeStarted.Before(**oldestExecutionTime) {
+		*oldestExecutionTime = &logEntry.DatetimeStarted
+	}
+}
+
+// findOldestExecutionInWindow finds the oldest execution within the time window and counts executions.
+// Returns the count of executions and the oldest execution time, or nil if none found.
+func findOldestExecutionInWindow(logs []*InternalLogEntry, windowStart time.Time) (int, *time.Time) {
+	executions := 0
+	var oldestExecutionTime *time.Time
+
+	for _, logEntry := range logs {
+		if !shouldCountExecution(logEntry, windowStart) {
+			continue
+		}
+
+		executions++
+		updateOldestExecution(&oldestExecutionTime, logEntry)
+	}
+
+	return executions, oldestExecutionTime
+}
+
+// calculateExpiryTime calculates when the oldest execution will fall outside the rate limit window.
+func calculateExpiryTime(oldestExecutionTime time.Time, duration time.Duration, now time.Time) time.Time {
+	expiryTime := oldestExecutionTime.Add(duration)
+	if !expiryTime.After(now) {
+		return time.Time{}
+	}
+	return expiryTime
+}
+
+// updateMaxExpiryTime updates maxExpiryTime if expiryTime is later.
+func updateMaxExpiryTime(maxExpiryTime *time.Time, expiryTime time.Time) {
+	if expiryTime.IsZero() {
+		return
+	}
+
+	if maxExpiryTime.IsZero() || expiryTime.After(*maxExpiryTime) {
+		*maxExpiryTime = expiryTime
+	}
+}
+
+// calculateExpiryForRate calculates the expiry time for a single rate limit rule.
+// Returns the expiry time if the rate limit is exceeded, or zero time if not.
+func calculateExpiryForRate(rate config.RateSpec, logs []*InternalLogEntry, now time.Time) time.Time {
+	duration := parseDuration(rate)
+	if duration <= 0 {
+		return time.Time{}
+	}
+
+	windowStart := now.Add(-duration)
+	executions, oldestExecutionTime := findOldestExecutionInWindow(logs, windowStart)
+
+	if executions < rate.Limit || oldestExecutionTime == nil {
+		return time.Time{}
+	}
+
+	return calculateExpiryTime(*oldestExecutionTime, duration, now)
+}
+
+// getLogsForBinding retrieves logs for a binding ID.
+func (e *Executor) getLogsForBinding(bindingId string) []*InternalLogEntry {
+	e.logmutex.RLock()
+	logs, found := e.LogsByBindingId[bindingId]
+	e.logmutex.RUnlock()
+
+	if !found || len(logs) == 0 {
+		return nil
+	}
+
+	return logs
+}
+
+// calculateMaxExpiryTimeFromRates calculates the maximum expiry time across all rate limit rules.
+func calculateMaxExpiryTimeFromRates(rates []config.RateSpec, logs []*InternalLogEntry, now time.Time) time.Time {
+	var maxExpiryTime time.Time
+
+	for _, rate := range rates {
+		expiryTime := calculateExpiryForRate(rate, logs, now)
+		updateMaxExpiryTime(&maxExpiryTime, expiryTime)
+	}
+
+	return maxExpiryTime
+}
+
+// GetTimeUntilAvailable calculates when an action will be available again based on rate limits.
+// Returns the Unix timestamp in seconds when the rate limit expires, or 0 if the action is available now.
+func (e *Executor) GetTimeUntilAvailable(binding *ActionBinding) int64 {
+	if len(binding.Action.MaxRate) == 0 {
+		return 0
+	}
+
+	logs := e.getLogsForBinding(binding.ID)
+	if logs == nil {
+		return 0
+	}
+
+	maxExpiryTime := calculateMaxExpiryTimeFromRates(binding.Action.MaxRate, logs, time.Now())
+
+	if maxExpiryTime.IsZero() {
+		return 0
+	}
+
+	return maxExpiryTime.Unix()
+}
+
 func (e *Executor) SetLog(trackingID string, entry *InternalLogEntry) {
 	e.logmutex.Lock()
 
@@ -337,7 +474,6 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string)
 		ExitCode:            DefaultExitCodeNotExecuted,
 		ExecutionStarted:    false,
 		ExecutionFinished:   false,
-		ActionId:            "",
 		ActionTitle:         "notfound",
 		ActionIcon:          "&#x1f4a9;",
 		Username:            req.AuthenticatedUser.Username,
@@ -374,6 +510,11 @@ func (e *Executor) execChain(req *ExecutionRequest) {
 		}
 	}
 
+	// Ensure DatetimeFinished is set even if execution was blocked early
+	if req.logEntry.DatetimeFinished.IsZero() {
+		req.logEntry.DatetimeFinished = time.Now()
+	}
+
 	req.logEntry.ExecutionFinished = true
 
 	// This isn't a step, because we want to notify all listeners, irrespective
@@ -386,7 +527,7 @@ func getConcurrentCount(req *ExecutionRequest) int {
 
 	req.executor.logmutex.RLock()
 
-	for _, log := range req.executor.GetLogsByActionId(req.Binding.Action.ID) {
+	for _, log := range req.executor.GetLogsByBindingId(req.Binding.ID) {
 		if !log.ExecutionFinished {
 			concurrentCount += 1
 		}
@@ -436,7 +577,7 @@ func getExecutionsCount(rate config.RateSpec, req *ExecutionRequest) int {
 
 	then := time.Now().Add(-duration)
 
-	for _, logEntry := range req.executor.GetLogsByActionId(req.Binding.Action.ID) {
+	for _, logEntry := range req.executor.GetLogsByBindingId(req.Binding.ID) {
 		// FIXME
 		/*
 			if logEntry.EntityPrefix != req.EntityPrefix {
@@ -573,16 +714,15 @@ func stepRequestAction(req *ExecutionRequest) bool {
 	req.logEntry.ActionConfigTitle = req.Binding.Action.Title
 	req.logEntry.ActionTitle = entities.ParseTemplateWith(req.Binding.Action.Title, req.Binding.Entity)
 	req.logEntry.ActionIcon = req.Binding.Action.Icon
-	req.logEntry.ActionId = req.Binding.Action.ID
 	req.logEntry.Tags = req.Tags
 
 	req.executor.logmutex.Lock()
 
-	if _, containsKey := req.executor.LogsByActionId[req.Binding.Action.ID]; !containsKey {
-		req.executor.LogsByActionId[req.Binding.Action.ID] = make([]*InternalLogEntry, 0)
+	if _, containsKey := req.executor.LogsByBindingId[req.Binding.ID]; !containsKey {
+		req.executor.LogsByBindingId[req.Binding.ID] = make([]*InternalLogEntry, 0)
 	}
 
-	req.executor.LogsByActionId[req.Binding.Action.ID] = append(req.executor.LogsByActionId[req.Binding.Action.ID], req.logEntry)
+	req.executor.LogsByBindingId[req.Binding.ID] = append(req.executor.LogsByBindingId[req.Binding.ID], req.logEntry)
 
 	req.executor.logmutex.Unlock()
 

+ 15 - 15
service/internal/executor/executor_actions.go

@@ -11,9 +11,9 @@ import (
 )
 
 func (e *Executor) FindBindingByID(id string) *ActionBinding {
-	e.MapActionIdToBindingLock.RLock()
-	pair, found := e.MapActionIdToBinding[id]
-	e.MapActionIdToBindingLock.RUnlock()
+	e.MapActionBindingsLock.RLock()
+	pair, found := e.MapActionBindings[id]
+	e.MapActionBindingsLock.RUnlock()
 
 	if !found {
 		return nil
@@ -23,11 +23,11 @@ func (e *Executor) FindBindingByID(id string) *ActionBinding {
 }
 
 func (e *Executor) FindBindingWithNoEntity(action *config.Action) *ActionBinding {
-	e.MapActionIdToBindingLock.RLock()
+	e.MapActionBindingsLock.RLock()
 
-	defer e.MapActionIdToBindingLock.RUnlock()
+	defer e.MapActionBindingsLock.RUnlock()
 
-	for _, binding := range e.MapActionIdToBinding {
+	for _, binding := range e.MapActionBindings {
 		if binding.Action == action && binding.Entity == nil {
 			return binding
 		}
@@ -42,9 +42,9 @@ type RebuildActionMapRequest struct {
 }
 
 func (e *Executor) RebuildActionMap() {
-	e.MapActionIdToBindingLock.Lock()
+	e.MapActionBindingsLock.Lock()
 
-	clear(e.MapActionIdToBinding)
+	clear(e.MapActionBindings)
 
 	req := &RebuildActionMapRequest{
 		Cfg:                   e.Cfg,
@@ -65,7 +65,7 @@ func (e *Executor) RebuildActionMap() {
 		}
 	}
 
-	e.MapActionIdToBindingLock.Unlock()
+	e.MapActionBindingsLock.Unlock()
 
 	for _, l := range e.listeners {
 		l.OnActionMapRebuilt()
@@ -100,10 +100,10 @@ func recurseDashboardForActionTitles(component *config.DashboardComponent, req *
 }
 
 func registerAction(e *Executor, configOrder int, action *config.Action, req *RebuildActionMapRequest) {
-	actionId := hashActionToID(action, "")
+	bindingId := generateActionBindingId(action, "")
 
-	e.MapActionIdToBinding[actionId] = &ActionBinding{
-		ID:            actionId,
+	e.MapActionBindings[bindingId] = &ActionBinding{
+		ID:            bindingId,
 		Action:        action,
 		Entity:        nil,
 		ConfigOrder:   configOrder,
@@ -118,9 +118,9 @@ func registerActionsFromEntities(e *Executor, configOrder int, entityTitle strin
 }
 
 func registerActionFromEntity(e *Executor, configOrder int, tpl *config.Action, ent *entities.Entity, req *RebuildActionMapRequest) {
-	virtualActionId := hashActionToID(tpl, ent.UniqueKey)
+	virtualActionId := generateActionBindingId(tpl, ent.UniqueKey)
 
-	e.MapActionIdToBinding[virtualActionId] = &ActionBinding{
+	e.MapActionBindings[virtualActionId] = &ActionBinding{
 		ID:            virtualActionId,
 		Action:        tpl,
 		Entity:        ent,
@@ -129,7 +129,7 @@ func registerActionFromEntity(e *Executor, configOrder int, tpl *config.Action,
 	}
 }
 
-func hashActionToID(action *config.Action, entityPrefix string) string {
+func generateActionBindingId(action *config.Action, entityPrefix string) string {
 	if action.ID != "" && entityPrefix == "" {
 		return action.ID
 	}

+ 5 - 0
service/internal/httpservers/frontend.go

@@ -19,6 +19,7 @@ import (
 	"github.com/OliveTin/OliveTin/internal/auth/otoauth2"
 	config "github.com/OliveTin/OliveTin/internal/config"
 	"github.com/OliveTin/OliveTin/internal/executor"
+	"github.com/OliveTin/OliveTin/internal/webhooks"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -72,6 +73,10 @@ func StartFrontendMux(cfg *config.Config, ex *executor.Executor) {
 
 	mux.HandleFunc("/readyz", handleReadyz)
 
+	webhookHandler := webhooks.NewWebhookHandler(cfg, ex)
+	mux.HandleFunc("/webhooks", webhookHandler.HandleWebhook)
+	mux.HandleFunc("/webhooks/", webhookHandler.HandleWebhook)
+
 	webuiServer := NewWebUIServer(cfg)
 
 	mux.HandleFunc("/theme.css", webuiServer.generateThemeCss)

+ 169 - 0
service/internal/webhooks/auth.go

@@ -0,0 +1,169 @@
+package webhooks
+
+import (
+	"crypto/hmac"
+	"crypto/sha1"
+	"crypto/sha256"
+	"crypto/subtle"
+	"encoding/hex"
+	"net/http"
+	"strings"
+
+	"github.com/OliveTin/OliveTin/internal/config"
+	log "github.com/sirupsen/logrus"
+)
+
+type AuthVerifier struct {
+	config config.WebhookConfig
+}
+
+func NewAuthVerifier(cfg config.WebhookConfig) *AuthVerifier {
+	return &AuthVerifier{config: cfg}
+}
+
+func (v *AuthVerifier) Verify(r *http.Request, payload []byte) bool {
+	verifier := v.getVerifier()
+	if verifier == nil {
+		return v.handleUnknownAuthType()
+	}
+	return verifier(r, payload)
+}
+
+type authVerifierFunc func(*http.Request, []byte) bool
+
+func (v *AuthVerifier) getVerifier() authVerifierFunc {
+	if v.config.AuthType == "" || v.config.AuthType == "none" {
+		return func(_ *http.Request, _ []byte) bool {
+			return true
+		}
+	}
+
+	verifierMap := v.buildVerifierMap()
+	if verifier, ok := verifierMap[v.config.AuthType]; ok {
+		return verifier
+	}
+	return nil
+}
+
+func (v *AuthVerifier) buildVerifierMap() map[string]authVerifierFunc {
+	return map[string]authVerifierFunc{
+		"hmac-sha256": v.verifyHMAC256,
+		"hmac-sha1":   v.verifyHMAC1,
+		"bearer": func(r *http.Request, _ []byte) bool {
+			return v.verifyBearer(r)
+		},
+		"basic": func(r *http.Request, _ []byte) bool {
+			return v.verifyBasic(r)
+		},
+	}
+}
+
+func (v *AuthVerifier) handleUnknownAuthType() bool {
+	log.WithFields(log.Fields{
+		"authType": v.config.AuthType,
+	}).Warnf("Unknown auth type, rejecting")
+	return false
+}
+
+func (v *AuthVerifier) verifyHMAC256(r *http.Request, payload []byte) bool {
+	if v.config.Secret == "" {
+		log.Warnf("HMAC-SHA256 auth requires secret")
+		return false
+	}
+
+	headerName := v.config.AuthHeader
+	if headerName == "" {
+		headerName = "X-Webhook-Signature"
+	}
+
+	signature := r.Header.Get(headerName)
+	if signature == "" {
+		log.Debugf("Missing signature header: %s", headerName)
+		return false
+	}
+
+	expectedSig := strings.TrimPrefix(signature, "sha256=")
+
+	mac := hmac.New(sha256.New, []byte(v.config.Secret))
+	mac.Write(payload)
+	computedSig := hex.EncodeToString(mac.Sum(nil))
+
+	return hmac.Equal([]byte(expectedSig), []byte(computedSig))
+}
+
+func (v *AuthVerifier) verifyHMAC1(r *http.Request, payload []byte) bool {
+	if v.config.Secret == "" {
+		log.Warnf("HMAC-SHA1 auth requires secret")
+		return false
+	}
+
+	headerName := v.config.AuthHeader
+	if headerName == "" {
+		headerName = "X-Webhook-Signature"
+	}
+
+	signature := r.Header.Get(headerName)
+	if signature == "" {
+		log.Debugf("Missing signature header: %s", headerName)
+		return false
+	}
+
+	expectedSig := strings.TrimPrefix(signature, "sha1=")
+
+	mac := hmac.New(sha1.New, []byte(v.config.Secret))
+	mac.Write(payload)
+	computedSig := hex.EncodeToString(mac.Sum(nil))
+
+	return hmac.Equal([]byte(expectedSig), []byte(computedSig))
+}
+
+func (v *AuthVerifier) verifyBearer(r *http.Request) bool {
+	if v.config.Secret == "" {
+		log.Warnf("Bearer auth requires secret")
+		return false
+	}
+
+	authHeader := r.Header.Get("Authorization")
+	if !strings.HasPrefix(authHeader, "Bearer ") {
+		log.Debugf("Missing or invalid Bearer token")
+		return false
+	}
+
+	token := strings.TrimPrefix(authHeader, "Bearer ")
+	tokenBytes := []byte(token)
+	secretBytes := []byte(v.config.Secret)
+	return len(tokenBytes) == len(secretBytes) && subtle.ConstantTimeCompare(tokenBytes, secretBytes) == 1
+}
+
+func (v *AuthVerifier) verifyBasic(r *http.Request) bool {
+	if v.config.Secret == "" {
+		log.Warnf("Basic auth requires secret")
+		return false
+	}
+
+	username, password, ok := r.BasicAuth()
+	if !ok {
+		log.Debugf("Missing Basic auth header")
+		return false
+	}
+
+	return v.verifyBasicCredentials(username, password)
+}
+
+func (v *AuthVerifier) verifyBasicCredentials(username, password string) bool {
+	parts := strings.SplitN(v.config.Secret, ":", 2)
+	if len(parts) == 2 {
+		return v.verifyBasicWithUsername(username, password, parts[0], parts[1])
+	}
+	return v.verifyBasicPasswordOnly(password)
+}
+
+func (v *AuthVerifier) verifyBasicWithUsername(username, password, expectedUsername, expectedPassword string) bool {
+	usernameMatch := subtle.ConstantTimeCompare([]byte(username), []byte(expectedUsername))
+	passwordMatch := subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword))
+	return usernameMatch == 1 && passwordMatch == 1
+}
+
+func (v *AuthVerifier) verifyBasicPasswordOnly(password string) bool {
+	return subtle.ConstantTimeCompare([]byte(password), []byte(v.config.Secret)) == 1
+}

+ 112 - 0
service/internal/webhooks/github.go

@@ -0,0 +1,112 @@
+package webhooks
+
+import (
+	"github.com/OliveTin/OliveTin/internal/config"
+)
+
+// ApplyGitHubTemplate applies GitHub-specific template configurations
+// This allows users to use simple template names instead of configuring everything manually
+func ApplyGitHubTemplate(cfg *config.WebhookConfig, template string) {
+	applier := getTemplateApplier(template)
+	if applier != nil {
+		applier(cfg)
+	}
+}
+
+type templateApplier func(*config.WebhookConfig)
+
+func getTemplateApplier(template string) templateApplier {
+	templateMap := map[string]templateApplier{
+		"github-push":         applyGitHubPushTemplate,
+		"github-pr":           applyGitHubPRTemplate,
+		"github-pull-request": applyGitHubPRTemplate,
+		"github-release":      applyGitHubReleaseTemplate,
+		"github-workflow":     applyGitHubWorkflowTemplate,
+	}
+	return templateMap[template]
+}
+
+func setDefaultAuth(cfg *config.WebhookConfig) {
+	if cfg.AuthHeader == "" {
+		cfg.AuthHeader = "X-Hub-Signature-256"
+	}
+	if cfg.AuthType == "" {
+		cfg.AuthType = "hmac-sha256"
+	}
+}
+
+func ensureMatchHeaders(cfg *config.WebhookConfig) {
+	if len(cfg.MatchHeaders) == 0 {
+		cfg.MatchHeaders = make(map[string]string)
+	}
+}
+
+func ensureExtract(cfg *config.WebhookConfig) {
+	if len(cfg.Extract) == 0 {
+		cfg.Extract = make(map[string]string)
+	}
+}
+
+func setExtractIfMissing(cfg *config.WebhookConfig, key, value string) {
+	if _, exists := cfg.Extract[key]; !exists {
+		cfg.Extract[key] = value
+	}
+}
+
+func setMatchHeaderIfMissing(cfg *config.WebhookConfig, key, value string) {
+	if _, exists := cfg.MatchHeaders[key]; !exists {
+		cfg.MatchHeaders[key] = value
+	}
+}
+
+func applyGitHubPushTemplate(cfg *config.WebhookConfig) {
+	setDefaultAuth(cfg)
+	ensureMatchHeaders(cfg)
+	setMatchHeaderIfMissing(cfg, "X-GitHub-Event", "push")
+	ensureExtract(cfg)
+	setExtractIfMissing(cfg, "git_repository", "$.repository.full_name")
+	setExtractIfMissing(cfg, "git_ref", "$.ref")
+	setExtractIfMissing(cfg, "git_commit", "$.head_commit.id")
+	setExtractIfMissing(cfg, "git_branch", "$.ref")
+	setExtractIfMissing(cfg, "git_message", "$.head_commit.message")
+	setExtractIfMissing(cfg, "git_author", "$.head_commit.author.name")
+}
+
+func applyGitHubPRTemplate(cfg *config.WebhookConfig) {
+	setDefaultAuth(cfg)
+	ensureMatchHeaders(cfg)
+	setMatchHeaderIfMissing(cfg, "X-GitHub-Event", "pull_request")
+	ensureExtract(cfg)
+	setExtractIfMissing(cfg, "pr_number", "$.number")
+	setExtractIfMissing(cfg, "pr_title", "$.pull_request.title")
+	setExtractIfMissing(cfg, "pr_author", "$.pull_request.user.login")
+	setExtractIfMissing(cfg, "pr_action", "$.action")
+	setExtractIfMissing(cfg, "git_repository", "$.repository.full_name")
+	setExtractIfMissing(cfg, "pr_state", "$.pull_request.state")
+	setExtractIfMissing(cfg, "pr_head_sha", "$.pull_request.head.sha")
+}
+
+func applyGitHubReleaseTemplate(cfg *config.WebhookConfig) {
+	setDefaultAuth(cfg)
+	ensureMatchHeaders(cfg)
+	setMatchHeaderIfMissing(cfg, "X-GitHub-Event", "release")
+	ensureExtract(cfg)
+	setExtractIfMissing(cfg, "release_action", "$.action")
+	setExtractIfMissing(cfg, "release_tag", "$.release.tag_name")
+	setExtractIfMissing(cfg, "release_name", "$.release.name")
+	setExtractIfMissing(cfg, "git_repository", "$.repository.full_name")
+	setExtractIfMissing(cfg, "release_author", "$.release.author.login")
+}
+
+func applyGitHubWorkflowTemplate(cfg *config.WebhookConfig) {
+	setDefaultAuth(cfg)
+	ensureMatchHeaders(cfg)
+	setMatchHeaderIfMissing(cfg, "X-GitHub-Event", "workflow_run")
+	ensureExtract(cfg)
+	setExtractIfMissing(cfg, "workflow_name", "$.workflow_run.name")
+	setExtractIfMissing(cfg, "workflow_status", "$.workflow_run.status")
+	setExtractIfMissing(cfg, "workflow_conclusion", "$.workflow_run.conclusion")
+	setExtractIfMissing(cfg, "git_repository", "$.repository.full_name")
+	setExtractIfMissing(cfg, "git_commit", "$.workflow_run.head_sha")
+	setExtractIfMissing(cfg, "git_branch", "$.workflow_run.head_branch")
+}

+ 162 - 0
service/internal/webhooks/handler.go

@@ -0,0 +1,162 @@
+package webhooks
+
+import (
+	"io"
+	"net/http"
+
+	"github.com/OliveTin/OliveTin/internal/auth"
+	"github.com/OliveTin/OliveTin/internal/config"
+	"github.com/OliveTin/OliveTin/internal/executor"
+	log "github.com/sirupsen/logrus"
+)
+
+type ActionWebhookConfig struct {
+	Action *config.Action
+	Config config.WebhookConfig
+}
+
+type WebhookHandler struct {
+	cfg      *config.Config
+	executor *executor.Executor
+}
+
+func NewWebhookHandler(cfg *config.Config, ex *executor.Executor) *WebhookHandler {
+	return &WebhookHandler{
+		cfg:      cfg,
+		executor: ex,
+	}
+}
+
+func (h *WebhookHandler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodPost {
+		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	payload, err := h.readPayload(r)
+	if err != nil {
+		http.Error(w, "Failed to read payload", http.StatusBadRequest)
+		return
+	}
+
+	matchingActions := h.findMatchingActions(r, payload)
+	if len(matchingActions) == 0 {
+		h.writeOKResponse(w, "no matching webhook actions")
+		return
+	}
+
+	processed := h.processMatchingActions(matchingActions, r, payload)
+	log.WithFields(log.Fields{
+		"matched":   len(matchingActions),
+		"processed": processed,
+	}).Infof("Webhook processed")
+
+	h.writeOKResponse(w, "webhook actions")
+}
+
+func (h *WebhookHandler) readPayload(r *http.Request) ([]byte, error) {
+	maxSize := int64(1024 * 1024)
+	payload, err := io.ReadAll(io.LimitReader(r.Body, maxSize))
+	if err != nil {
+		log.WithFields(log.Fields{
+			"error": err,
+		}).Warnf("Failed to read webhook payload")
+		return nil, err
+	}
+
+	return payload, nil
+}
+
+func (h *WebhookHandler) writeOKResponse(w http.ResponseWriter, context string) {
+	w.WriteHeader(http.StatusOK)
+	if _, err := w.Write([]byte("OK")); err != nil {
+		log.WithError(err).Warnf("Failed to write response for %s", context)
+	}
+}
+
+func (h *WebhookHandler) processMatchingActions(matchingActions []ActionWebhookConfig, r *http.Request, payload []byte) int {
+	processed := 0
+	for _, actionConfig := range matchingActions {
+		if h.processWebhook(actionConfig, r, payload) {
+			processed++
+		}
+	}
+	return processed
+}
+
+func (h *WebhookHandler) findMatchingActions(r *http.Request, payload []byte) []ActionWebhookConfig {
+	var matches []ActionWebhookConfig
+
+	for _, action := range h.cfg.Actions {
+		matches = append(matches, h.findMatchingWebhooksForAction(action, r, payload)...)
+	}
+
+	return matches
+}
+
+func (h *WebhookHandler) findMatchingWebhooksForAction(action *config.Action, r *http.Request, payload []byte) []ActionWebhookConfig {
+	var matches []ActionWebhookConfig
+
+	for _, webhookConfig := range action.ExecOnWebhook {
+		webhookConfigCopy := webhookConfig
+
+		if webhookConfigCopy.Template != "" {
+			ApplyGitHubTemplate(&webhookConfigCopy, webhookConfigCopy.Template)
+		}
+
+		matcher := NewWebhookMatcher(webhookConfigCopy, r, payload)
+		if matcher.Matches() {
+			matches = append(matches, ActionWebhookConfig{
+				Action: action,
+				Config: webhookConfigCopy,
+			})
+		}
+	}
+
+	return matches
+}
+
+func (h *WebhookHandler) processWebhook(actionConfig ActionWebhookConfig, r *http.Request, payload []byte) bool {
+	verifier := NewAuthVerifier(actionConfig.Config)
+	if !verifier.Verify(r, payload) {
+		log.WithFields(log.Fields{
+			"actionTitle": actionConfig.Action.Title,
+			"authType":    actionConfig.Config.AuthType,
+		}).Warnf("Webhook authentication failed")
+		return false
+	}
+
+	matcher := NewWebhookMatcher(actionConfig.Config, r, payload)
+
+	args, err := matcher.ExtractArguments()
+	if err != nil {
+		log.WithFields(log.Fields{
+			"actionTitle": actionConfig.Action.Title,
+			"error":       err,
+		}).Warnf("Failed to extract webhook arguments")
+		return false
+	}
+
+	h.executeAction(actionConfig.Action, args)
+	return true
+}
+
+func (h *WebhookHandler) executeAction(action *config.Action, args map[string]string) {
+	binding := h.executor.FindBindingWithNoEntity(action)
+	if binding == nil {
+		log.WithFields(log.Fields{
+			"actionTitle": action.Title,
+		}).Warnf("Action binding not found, skipping execution")
+		return
+	}
+
+	req := &executor.ExecutionRequest{
+		Binding:           binding,
+		Cfg:               h.cfg,
+		Tags:              []string{"webhook"},
+		Arguments:         args,
+		AuthenticatedUser: auth.UserFromSystem(h.cfg, "webhook"),
+	}
+
+	h.executor.ExecRequest(req)
+}

+ 55 - 0
service/internal/webhooks/jsonpath.go

@@ -0,0 +1,55 @@
+package webhooks
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/PaesslerAG/jsonpath"
+)
+
+type JSONMatcher struct {
+	payload interface{}
+}
+
+func NewJSONMatcher(payload []byte) (*JSONMatcher, error) {
+	var data interface{}
+	if err := json.Unmarshal(payload, &data); err != nil {
+		return nil, err
+	}
+	return &JSONMatcher{payload: data}, nil
+}
+
+func (m *JSONMatcher) MatchPath(pathExpr string, expectedValue string) (bool, error) {
+	value, err := jsonpath.Get(pathExpr, m.payload)
+	if err != nil {
+		return false, err
+	}
+
+	// Marshal to JSON for consistent string representation
+	jsonBytes, err := json.Marshal(value)
+	if err != nil {
+		return false, fmt.Errorf("failed to marshal extracted value: %w", err)
+	}
+
+	valueStr := string(jsonBytes)
+	return valueStr == expectedValue, nil
+}
+
+func (m *JSONMatcher) ExtractValue(pathExpr string) (string, error) {
+	value, err := jsonpath.Get(pathExpr, m.payload)
+	if err != nil {
+		return "", err
+	}
+
+	// Marshal to JSON for consistent string representation
+	jsonBytes, err := json.Marshal(value)
+	if err != nil {
+		return "", fmt.Errorf("failed to marshal extracted value: %w", err)
+	}
+
+	return string(jsonBytes), nil
+}
+
+func (m *JSONMatcher) GetPayload() interface{} {
+	return m.payload
+}

+ 185 - 0
service/internal/webhooks/matcher.go

@@ -0,0 +1,185 @@
+package webhooks
+
+import (
+	"net/http"
+	"regexp"
+	"strings"
+
+	"github.com/OliveTin/OliveTin/internal/config"
+	log "github.com/sirupsen/logrus"
+)
+
+type WebhookMatcher struct {
+	config    config.WebhookConfig
+	req       *http.Request
+	bodyBytes []byte
+}
+
+func NewWebhookMatcher(cfg config.WebhookConfig, r *http.Request, bodyBytes []byte) *WebhookMatcher {
+	return &WebhookMatcher{
+		config:    cfg,
+		req:       r,
+		bodyBytes: bodyBytes,
+	}
+}
+
+func (m *WebhookMatcher) Matches() bool {
+	if !m.matchHeaders() {
+		return false
+	}
+
+	if !m.matchQuery() {
+		return false
+	}
+
+	if !m.matchPath() {
+		return false
+	}
+
+	return true
+}
+
+func (m *WebhookMatcher) matchHeaders() bool {
+	if len(m.config.MatchHeaders) == 0 {
+		return true
+	}
+
+	for key, expectedValue := range m.config.MatchHeaders {
+		actualValue := m.req.Header.Get(key)
+		if !m.compareValues(actualValue, expectedValue) {
+			log.WithFields(log.Fields{
+				"header":   key,
+				"expected": expectedValue,
+				"actual":   actualValue,
+			}).Debugf("Header mismatch")
+			return false
+		}
+	}
+	return true
+}
+
+func (m *WebhookMatcher) matchQuery() bool {
+	if len(m.config.MatchQuery) == 0 {
+		return true
+	}
+
+	query := m.req.URL.Query()
+	for key, expectedValue := range m.config.MatchQuery {
+		actualValue := query.Get(key)
+		if !m.compareValues(actualValue, expectedValue) {
+			log.WithFields(log.Fields{
+				"query":    key,
+				"expected": expectedValue,
+				"actual":   actualValue,
+			}).Debugf("Query parameter mismatch")
+			return false
+		}
+	}
+	return true
+}
+
+func (m *WebhookMatcher) matchPath() bool {
+	if m.config.MatchPath == "" {
+		return true
+	}
+
+	jsonPath, expectedValue := m.parseMatchPath()
+	matcher, err := NewJSONMatcher(m.bodyBytes)
+	if err != nil {
+		log.WithFields(log.Fields{
+			"error": err,
+		}).Debugf("Failed to create JSON matcher")
+		return false
+	}
+
+	return m.matchPathValue(matcher, jsonPath, expectedValue)
+}
+
+func (m *WebhookMatcher) parseMatchPath() (string, string) {
+	parts := strings.SplitN(m.config.MatchPath, "=", 2)
+	jsonPath := parts[0]
+	expectedValue := ""
+	if len(parts) == 2 {
+		expectedValue = parts[1]
+	}
+	return jsonPath, expectedValue
+}
+
+func (m *WebhookMatcher) matchPathValue(matcher *JSONMatcher, jsonPath, expectedValue string) bool {
+	if expectedValue == "" {
+		_, err := matcher.ExtractValue(jsonPath)
+		return err == nil
+	}
+
+	matches, err := matcher.MatchPath(jsonPath, expectedValue)
+	if err != nil {
+		log.WithFields(log.Fields{
+			"jsonPath": jsonPath,
+			"error":    err,
+		}).Debugf("Failed to match JSONPath")
+		return false
+	}
+	return matches
+}
+
+func (m *WebhookMatcher) compareValues(actual, expected string) bool {
+	if strings.HasPrefix(expected, "regex:") {
+		pattern := strings.TrimPrefix(expected, "regex:")
+		matched, err := regexp.MatchString(pattern, actual)
+		if err != nil {
+			log.WithFields(log.Fields{
+				"pattern": pattern,
+				"error":   err,
+			}).Warnf("Invalid regex pattern")
+			return false
+		}
+		return matched
+	}
+	return actual == expected
+}
+
+func (m *WebhookMatcher) ExtractArguments() (map[string]string, error) {
+	matcher, err := NewJSONMatcher(m.bodyBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	args := m.extractJSONPathValues(matcher)
+	m.addWebhookMetadata(args)
+	m.addWebhookHeaders(args)
+
+	return args, nil
+}
+
+func (m *WebhookMatcher) extractJSONPathValues(matcher *JSONMatcher) map[string]string {
+	args := make(map[string]string)
+
+	for argName, jsonPath := range m.config.Extract {
+		value, err := matcher.ExtractValue(jsonPath)
+		if err != nil {
+			log.WithFields(log.Fields{
+				"argName":  argName,
+				"jsonPath": jsonPath,
+				"error":    err,
+			}).Debugf("Failed to extract value")
+			continue
+		}
+		args[argName] = value
+	}
+
+	return args
+}
+
+func (m *WebhookMatcher) addWebhookMetadata(args map[string]string) {
+	args["webhook_method"] = m.req.Method
+	args["webhook_path"] = m.req.URL.Path
+	args["webhook_query"] = m.req.URL.RawQuery
+}
+
+func (m *WebhookMatcher) addWebhookHeaders(args map[string]string) {
+	for key, values := range m.req.Header {
+		if len(values) > 0 {
+			args["webhook_header_"+strings.ToLower(key)] = values[0]
+		}
+	}
+}

Неке датотеке нису приказане због велике количине промена