Selaa lähdekoodia

chore: OliveTin 3k progress

jamesread 11 kuukautta sitten
vanhempi
commit
a62d58f119
85 muutettua tiedostoa jossa 7233 lisäystä ja 6701 poistoa
  1. 5 4
      .gitignore
  2. 1 9
      Makefile
  3. 3 0
      config.yaml
  4. 0 0
      frontend/.eslintrc.json
  5. 1 0
      frontend/.npmrc
  6. 0 0
      frontend/.stylelintrc.json
  7. 21 0
      frontend/Makefile
  8. 0 0
      frontend/OliveTinLogo-120px.png
  9. 0 0
      frontend/OliveTinLogo-180px.png
  10. 0 0
      frontend/OliveTinLogo-57px.png
  11. 0 0
      frontend/OliveTinLogo.png
  12. 0 0
      frontend/OliveTinLogo.svg
  13. 9 90
      frontend/index.html
  14. 16 24
      frontend/js/ArgumentForm.js
  15. 0 0
      frontend/js/ExecutionFeedbackButton.js
  16. 0 0
      frontend/js/Mutex.js
  17. 4 1
      frontend/js/OutputTerminal.js
  18. 18 293
      frontend/js/marshaller.js
  19. 50 0
      frontend/js/websocket.js
  20. 0 0
      frontend/lib/iconify-icon-2.0.0.min.js
  21. 40 59
      frontend/main.js
  22. 588 1745
      frontend/package-lock.json
  23. 8 3
      frontend/package.json
  24. 1347 0
      frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts
  25. 10 0
      frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js
  26. 287 0
      frontend/resources/vue/ActionButton.vue
  27. 83 0
      frontend/resources/vue/App.vue
  28. 432 0
      frontend/resources/vue/ArgumentForm.vue
  29. 32 0
      frontend/resources/vue/Dashboard.vue
  30. 141 0
      frontend/resources/vue/ExecutionButton.vue
  31. 421 0
      frontend/resources/vue/ExecutionDialog.vue
  32. 15 33
      frontend/resources/vue/LoginForm.vue
  33. 111 0
      frontend/resources/vue/LogsList.vue
  34. 63 0
      frontend/resources/vue/components/ActionStatusDisplay.vue
  35. 254 0
      frontend/resources/vue/components/Sidebar.vue
  36. 179 0
      frontend/resources/vue/components/SidebarExample.vue
  37. 76 0
      frontend/resources/vue/router.js
  38. 23 0
      frontend/resources/vue/views/DashboardRoot.vue
  39. 215 0
      frontend/resources/vue/views/DiagnosticsView.vue
  40. 132 0
      frontend/resources/vue/views/LoginView.vue
  41. 318 0
      frontend/resources/vue/views/LogsView.vue
  42. 112 0
      frontend/resources/vue/views/NotFoundView.vue
  43. 41 0
      frontend/style.css
  44. 0 0
      frontend/themes/waffles/theme.css
  45. 29 0
      frontend/vite.config.js
  46. 6 7
      proto/buf.gen.yaml
  47. 69 121
      proto/olivetin/api/v1/olivetin.proto
  48. 0 1203
      service/gen/grpc/olivetin/api/v1/olivetin.pb.gw.go
  49. 0 728
      service/gen/grpc/olivetin/api/v1/olivetin_grpc.pb.go
  50. 631 0
      service/gen/olivetin/api/v1/apiv1connect/olivetin.connect.go
  51. 441 156
      service/gen/olivetin/api/v1/olivetin.pb.go
  52. 1 0
      service/go.mod
  53. 2 0
      service/go.sum
  54. 595 0
      service/internal/api/api.go
  55. 33 40
      service/internal/api/apiActions.go
  56. 1 3
      service/internal/api/api_test.go
  57. 2 2
      service/internal/api/dashboard_entities.go
  58. 54 15
      service/internal/api/dashboards.go
  59. 1 1
      service/internal/api/local_user_login.go
  60. 19 1
      service/internal/executor/executor.go
  61. 0 512
      service/internal/grpcapi/grpcApi.go
  62. 3 8
      service/internal/httpservers/httpServer.go
  63. 19 76
      service/internal/httpservers/restapi.go
  64. 10 10
      service/internal/httpservers/restapi_auth.go
  65. 19 17
      service/internal/httpservers/restapi_auth_jwt.go
  66. 5 4
      service/internal/httpservers/restapi_auth_local.go
  67. 45 35
      service/internal/httpservers/restapi_auth_oauth2.go
  68. 21 23
      service/internal/httpservers/singleFrontend.go
  69. 71 73
      service/internal/httpservers/webuiServer.go
  70. 1 1
      service/internal/websocket/websocket.go
  71. 1 4
      service/main.go
  72. 0 2
      service/tools.go
  73. 6 0
      var/entities/lights.yaml
  74. 92 0
      var/marketing/OliveTinLogoMonocrome.svg
  75. BIN
      var/marketing/mockup-laptop.png
  76. BIN
      var/marketing/mockup-laptop.xcf
  77. 0 4
      webui.dev/.parcelrc
  78. 0 6
      webui.dev/Makefile
  79. 0 174
      webui.dev/js/ActionButton.js
  80. 0 42
      webui.dev/js/ActionStatusDisplay.js
  81. 0 34
      webui.dev/js/ExecutionButton.js
  82. 0 237
      webui.dev/js/ExecutionDialog.js
  83. 0 37
      webui.dev/js/NavigationBar.js
  84. 0 82
      webui.dev/js/websocket.js
  85. 0 782
      webui.dev/style.css

+ 5 - 4
.gitignore

@@ -4,11 +4,12 @@ service/OliveTin
 service/OliveTin.armhf
 service/OliveTin.exe
 service/reports
+service/gen
 releases/
 dist/
 installation-id.txt
 tmp/
-webui/
-webui.dev/node_modules
-webui.dev/.parcel-cache
-custom-webui
+frontend/dist/
+frontend/node_modules
+custom-frontend
+integration-tests/screenshots/

+ 1 - 9
Makefile

@@ -47,16 +47,8 @@ devrun: compile
 
 devcontainer: compile podman-image podman-container
 
-webui-codestyle:
-	$(MAKE) -wC webui.dev codestyle
-
 webui-dist:
-	$(call delete-files,webui)
-	$(call delete-files,webui.dev/dist)
-	cd webui.dev && npm install
-	cd webui.dev && npx parcel build --public-url "."
-	python -c "import shutil;shutil.move('webui.dev/dist', 'webui')"
-	python -c "import shutil;import glob;[shutil.copy(f, 'webui') for f in glob.glob('webui.dev/*.png')]"
+	$(MAKE) -wC frontend dist
 
 clean:
 	$(call delete-files,dist)

+ 3 - 0
config.yaml

@@ -11,6 +11,9 @@ logLevel: "INFO"
 # Checking for updates https://docs.olivetin.app/reference/updateChecks.html
 checkForUpdates: false
 
+authLocalUsers:
+  enabled: true
+
 # Actions are commands that are executed by OliveTin, and normally show up as
 # buttons on the WebUI.
 #

+ 0 - 0
webui.dev/.eslintrc.json → frontend/.eslintrc.json


+ 1 - 0
frontend/.npmrc

@@ -0,0 +1 @@
+fund=false

+ 0 - 0
webui.dev/.stylelintrc.json → frontend/.stylelintrc.json


+ 21 - 0
frontend/Makefile

@@ -0,0 +1,21 @@
+define delete-files
+	python -c "import shutil;shutil.rmtree('$(1)', ignore_errors=True)"
+endef
+
+codestyle:
+	npm install
+	npx eslint --fix main.js js/*
+	npx stylelint style.css
+
+clean:
+	$(call delete-files,dist)
+
+deps:
+	npm install
+
+build:
+	npx vite build
+
+dist: deps clean build
+
+.PHONY: codestyle

+ 0 - 0
webui.dev/OliveTinLogo-120px.png → frontend/OliveTinLogo-120px.png


+ 0 - 0
webui.dev/OliveTinLogo-180px.png → frontend/OliveTinLogo-180px.png


+ 0 - 0
webui.dev/OliveTinLogo-57px.png → frontend/OliveTinLogo-57px.png


+ 0 - 0
webui.dev/OliveTinLogo.png → frontend/OliveTinLogo.png


+ 0 - 0
webui.dev/OliveTinLogo.svg → frontend/OliveTinLogo.svg


+ 9 - 90
webui.dev/index.html → frontend/index.html

@@ -22,79 +22,11 @@
 	</head>
 
 	<body>
-		<header>
-			<button id = "sidebar-toggler-button" aria-label = "Open sidebar navigation" aria-pressed = "false" aria-haspopup = "menu">&#9776;</button>
-
-			<h1 id = "page-title">OliveTin</h1>
-
-			<nav id = "mainnav" hidden>
-				<ul id = "navigation-links">
-					<li title = "Actions">
-						<a id = "showActions">Actions</a>
-					</li>
-				</ul>
-
-				<ul id = "supplemental-links">
-				</ul>
-			</nav>
-
-			<div class = "userinfo">
-				<span id = "link-login" hidden><a href = "/login">Login</a> |</span>
-				<span id = "link-logout" hidden><a href = "/api/Logout">Logout</a> |</span>
-				<span id = "username">&nbsp;</span>
-				<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 24 24"><g fill="none" fill-rule="evenodd"><path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10s10-4.477 10-10S17.523 2 12 2M8.5 9.5a3.5 3.5 0 1 1 7 0a3.5 3.5 0 0 1-7 0m9.758 7.484A7.99 7.99 0 0 1 12 20a7.99 7.99 0 0 1-6.258-3.016C7.363 15.821 9.575 15 12 15s4.637.821 6.258 1.984"/></g></svg>
-			</div>
-		</header>
+		<slot id = "app" />
 
 		<main title = "main content">
-			<section id = "contentLogs" title = "Logs" class = "box-shadow" hidden>
-				<div class = "toolbar">
-					<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" d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"/></svg>
-						<input placeholder = "Search for action name" id = "logSearchBox" />
-						<button id = "searchLogsClear" title = "Clear search filter" disabled>
-							<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>
-					</label>
-				</div>
-				<table id = "logsTable" title = "Logs" hidden>
-					<thead>
-						<tr title = "untitled">
-							<th>Timestamp</th>
-							<th>Action</th>
-							<th>Metadata</th>
-							<th>Status</th>
-						</tr>
-					</thead>
-					<tbody id = "logTableBody" />
-				</table>
-
-				<p id = "logsTableEmpty">There are no logs to display. <a href = "/">Return to index</a></p>
-
-				<p><strong>Note:</strong> The server is configured to only send <strong id = "logs-server-page-size">?</strong> log entries at a time. The search box at the top of this page only searches this current page of logs.</p>
-			</section>
 
-			<section id = "contentDiagnostics" title = "Diagnostics" class = "box-shadow" hidden>
-				<div id = "diagnostics" class = "ta-left">
-					<p><strong>Note:</strong> Diagnostics are only generated on OliveTin startup - they are not updated in real-time or when you refresh this page. They are intended as a "quick reference" to help you.</p>
-					<p>If you are having problems with OliveTin and want to raise a support request, please don't take a screenshot or copy text from this page, but instead it is highly recommended to include a <a href = "https://docs.olivetin.app/sosreport.html">sosreport</a> which is more detailed, and makes it easier to help you.</p>
-					<table>
-						<tbody>
-							<th colspan = "99">SSH</th>
-							<tr>
-								<td width = "10%">Found Key</td>
-								<td id = "diagnostics-sshfoundkey">?</td>
-							</tr>
-
-							<tr>
-								<td>Found Config</td>
-								<td id = "diagnostics-sshfoundconfig">?</td>
-						</tbody>
-					</table>
-				</div>
-			</section>
-
-			<section id = "contentActions" title = "Actions" hidden >
+			<section id = "contentActions" title = "Actions" class = "transparent" hidden >
 				<fieldset id = "root-group" title = "Actions">
 					<legend hidden>Actions</legend>
 				</fieldset>
@@ -105,22 +37,6 @@
 			</noscript>
 		</main>
 
-		<footer title = "footer">
-			<p><img title = "application icon" src = "OliveTinLogo.png" alt = "OliveTin logo" height = "1em" class = "logo" /> OliveTin</p>
-			<p>
-			<a href = "https://docs.olivetin.app" target = "_new">Documentation</a> |
-			<a href = "https://github.com/OliveTin/OliveTin/issues/new/choose" target = "_new">Raise an issue on GitHub</a> |
-			<span>Version: <span id = "currentVersion">?</span></span> |
-			<span>Server connection:
-				<span id = "serverConnectionRest">REST</span>,
-				<span id = "serverConnectionWebSocket">WebSocket</span>
-			</span>
-			</p>
-			<p>
-			<a id = "available-version" href = "http://olivetin.app" target = "_blank" hidden>?</a>
-			</p>
-		</footer>
-
 		<dialog title = "Big Error Message" id = "big-error" class = "error padded-content">
 
 		</dialog>
@@ -175,16 +91,16 @@
 							<h2>Local Login</h2>
 							<div class = "error"></div>
 							<div class = "arguments">
-								<label for = "username">
+								<label for = "in-username">
 									<span>Username:</span>
 								</label>
-								<input type = "text" name = "username" class = "username" />
+								<input type = "text" name = "username" id = "in-username" class = "username" autocomplete = "username"/>
 								<span></span>
 
-								<label for = "password">
+								<label for = "in-password">
 									<span>Password:</span>
 								</label>
-								<input type = "password" name = "password" class = "password" />
+								<input type = "password" name = "password" id = "in-password" class = "password" />
 								<span></span>
 
 								<button type = "submit">Login</button>
@@ -245,6 +161,9 @@
 			to at least display a helpful error message if we can't use OliveTin.
 			 */
 			window.showBigError = function (type, friendlyType, message, isFatal) {
+				console.error('Error ' + type + ': ', message)
+				return;
+
 				bigErrorDialog.innerHTML = '<h1>Error ' + friendlyType + '</h1><p>' + message + "</p><p><a href = 'http://docs.olivetin.app/troubleshooting/err-" + type + ".html' target = 'blank'/>" + type + " error in OliveTin Documentation</a></p>"
 
 				if (isFatal) {

+ 16 - 24
webui.dev/js/ArgumentForm.js → frontend/js/ArgumentForm.js

@@ -180,30 +180,7 @@ class ArgumentForm extends window.HTMLElement {
           }
 
           domEl.onchange = () => {
-            const validateArgumentTypeArgs = {
-              value: domEl.value,
-              type: arg.type
-            }
-
-            window.fetch(window.restBaseUrl + 'ValidateArgumentType', {
-              method: 'POST',
-              headers: {
-                'Content-Type': 'application/json'
-              },
-              body: JSON.stringify(validateArgumentTypeArgs)
-            }).then((res) => {
-              if (res.ok) {
-                return res.json()
-              } else {
-                throw new Error(res.statusText)
-              }
-            }).then((json) => {
-              if (json.valid) {
-                domEl.setCustomValidity('')
-              } else {
-                domEl.setCustomValidity(json.description)
-              }
-            })
+            formatValidation(domEl, arg)
           }
       }
     }
@@ -231,6 +208,21 @@ class ArgumentForm extends window.HTMLElement {
 
     return domEl
   }
+  
+  async formatValidation (domEl, arg) {
+    const validateArgumentTypeArgs = {
+      value: domEl.value,
+      type: arg.type
+    }
+
+    const validation = await window.validateArgumentType(validateArgumentTypeArgs)
+
+    if (validation.valid) {
+      domEl.setCustomValidity('')
+    } else {
+      domEl.setCustomValidity(validation.description)
+    }
+  }
 
   updateUrlWithArg (ev) {
     if (!ev.target.name) {

+ 0 - 0
webui.dev/js/ExecutionFeedbackButton.js → frontend/js/ExecutionFeedbackButton.js


+ 0 - 0
webui.dev/js/Mutex.js → frontend/js/Mutex.js


+ 4 - 1
webui.dev/js/OutputTerminal.js → frontend/js/OutputTerminal.js

@@ -37,7 +37,10 @@ export class OutputTerminal {
       })
     } finally {
       unlock()
-      then()
+
+      if (then != null && then !== undefined) {
+        then()
+      }
     }
   }
 

+ 18 - 293
webui.dev/js/marshaller.js → frontend/js/marshaller.js

@@ -1,12 +1,3 @@
-import './ActionButton.js' // To define action-button
-import { NavigationBar } from './NavigationBar.js'
-import { ExecutionDialog } from './ExecutionDialog.js'
-import { ActionStatusDisplay } from './ActionStatusDisplay.js'
-
-function getQueryParams () {
-  return new URLSearchParams(window.location.search.substring(1))
-}
-
 function checkAndTriggerActionFromQueryParam () {
   const params = getQueryParams()
 
@@ -77,18 +68,8 @@ function createAnnotation (key, val) {
  * This is a weird function that just sets some globals.
  */
 export function initMarshaller () {
-  window.navbar = new NavigationBar()
-
-  window.showSection = showSection
-  window.showSectionView = showSectionView
-
-  window.executionDialog = new ExecutionDialog()
-
   window.logEntries = new Map()
-  window.registeredPaths = new Map()
-  window.breadcrumbNavigation = []
 
-  window.currentPath = ''
 
   window.addEventListener('EventExecutionStarted', onExecutionStarted)
   window.addEventListener('EventExecutionFinished', onExecutionFinished)
@@ -125,49 +106,16 @@ export function marshalDashboardComponentsJsonToHtml (json) {
   } else {
     setUsername(json.authenticatedUser, json.authenticatedUserProvider)
 
-    marshalActionsJsonToHtml(json)
     marshalDashboardStructureToHtml(json)
-
-    window.navbar.refreshSectionPolicyLinks(json.effectivePolicy)
-
-    refreshDiagnostics(json)
   }
 
   document.body.setAttribute('initial-marshal-complete', 'true')
 }
 
-function marshalActionsJsonToHtml (json) {
-  const currentIterationTimestamp = Date.now()
-
-  window.actionButtons = {}
-  window.actionButtonsJson = {} // Store the JSON representation
-
-  for (const jsonButton of json.actions) {
-    let htmlButton = window.actionButtons[jsonButton.id]
-
-    if (typeof htmlButton === 'undefined') {
-      htmlButton = document.createElement('action-button')
-      htmlButton.constructFromJson(jsonButton)
-
-      window.actionButtons[jsonButton.title] = htmlButton
-      window.actionButtonsJson[jsonButton.title] = jsonButton // Store the JSON representation
-    }
-
-    htmlButton.updateFromJson(jsonButton)
-    htmlButton.updateIterationTimestamp = currentIterationTimestamp
-  }
-
-  // Remove existing, but stale buttons (that were not updated in this round)
-  for (const existingButton of document.querySelectorAll('action-button')) {
-    if (existingButton.updateIterationTimestamp !== currentIterationTimestamp) {
-      existingButton.remove()
-    }
-  }
-}
-
 function onOutputChunk (evt) {
   const chunk = evt.payload
 
+  return;
   if (chunk.executionTrackingId === window.executionDialog.executionTrackingId) {
     window.terminal.write(chunk.output)
   }
@@ -176,9 +124,9 @@ function onOutputChunk (evt) {
 function onExecutionStarted (evt) {
   const logEntry = evt.payload.logEntry
 
-  marshalLogsJsonToHtml({
-    logs: [logEntry]
-  })
+  // marshalLogsJsonToHtml({
+  //   logs: [logEntry]
+  // })
 }
 
 function onExecutionFinished (evt) {
@@ -186,11 +134,7 @@ function onExecutionFinished (evt) {
 
   window.logEntries.set(logEntry.executionTrackingId, logEntry)
 
-  const actionButton = window.actionButtons[logEntry.actionTitle]
-
-  if (actionButton === undefined) {
-    return
-  }
+  return;
 
   const executionButton = document.querySelector('execution-button#execution-' + logEntry.executionTrackingId)
   let feedbackButton = actionButton
@@ -216,9 +160,9 @@ function onExecutionFinished (evt) {
 
   feedbackButton.onExecutionFinished(logEntry)
 
-  marshalLogsJsonToHtml({
-    logs: [logEntry]
-  })
+  // marshalLogsJsonToHtml({
+  //   logs: [logEntry]
+  // })
 
   // If the current execution dialog is open, update that too
   if (window.executionDialog.dlg.open && window.executionDialog.executionUuid === logEntry.uuid) {
@@ -251,54 +195,6 @@ function showExecutionResult (pathName) {
   window.executionDialog.show()
 }
 
-function showSection (pathName) {
-  if (pathName.startsWith('/logs/')) {
-    showExecutionResult(pathName)
-    pushNewNavigationPath(pathName)
-    return
-  }
-
-  const path = window.registeredPaths.get(pathName)
-
-  if (path === undefined) {
-    console.warn('Section not found by path: ' + pathName)
-
-    showSection('/')
-    return
-  }
-
-  window.convertPathToBreadcrumb = convertPathToBreadcrumb
-  window.currentPath = pathName
-  window.breadcrumbNavigation = convertPathToBreadcrumb(pathName)
-
-  for (const section of document.querySelectorAll('section')) {
-    if (section.title === path.section) {
-      section.style.display = 'block'
-    } else {
-      section.style.display = 'none'
-    }
-  }
-
-  // Check for action parameter in query string
-  if (!checkAndTriggerActionFromQueryParam()) {
-    pushNewNavigationPath(pathName)
-  }
-
-  setSectionNavigationVisible(false)
-
-  showSectionView(path.view)
-}
-
-function pushNewNavigationPath (pathName) {
-  // Get the current query string
-  const queryString = window.location.search
-
-  // Push the new state with the path and preserve the query string
-  window.history.pushState({
-    path: pathName
-  }, null, pathName + queryString)
-}
-
 function setSectionNavigationVisible (visible) {
   const nav = document.querySelector('nav')
   const btn = document.getElementById('sidebar-toggler-button')
@@ -345,41 +241,6 @@ export function setupSectionNavigation (style) {
 
     document.body.classList.add('has-topbar')
   }
-
-  registerSection('/', 'Actions', null, document.getElementById('showActions'))
-  registerSection('/diagnostics', 'Diagnostics', null, null)
-  registerSection('/logs', 'Logs', null, null)
-  registerSection('/login', 'Login', null, null)
-}
-
-function registerSection (path, section, view, linkElement) {
-  window.registeredPaths.set(path, {
-    section: section,
-    view: view
-  })
-
-  if (linkElement != null) {
-    addLinkToSection(path, linkElement)
-  }
-}
-
-function addLinkToSection (pathName, element) {
-  const path = window.registeredPaths.get(pathName)
-
-  element.href = 'javascript:void(0)'
-  element.title = path.section
-  element.onclick = () => {
-    showSection(pathName)
-  }
-}
-
-function refreshDiagnostics (json) {
-  document.getElementById('diagnostics-sshfoundkey').innerHTML = json.diagnostics.SshFoundKey
-  document.getElementById('diagnostics-sshfoundconfig').innerHTML = json.diagnostics.SshFoundConfig
-}
-
-function getSystemTitle (title) {
-  return title.replaceAll(' ', '')
 }
 
 function marshalSingleDashboard (dashboard, nav) {
@@ -409,10 +270,10 @@ function marshalSingleDashboard (dashboard, nav) {
 
   window.navbar.createLink(dashboard.title, systemTitleUrl, false)
 
-  registerSection(systemTitleUrl, section.title, null, null)
 }
 
 function marshalDashboardStructureToHtml (json) {
+  return
   const nav = document.getElementById('navigation-links')
 
   for (const dashboard of json.dashboards) {
@@ -447,6 +308,7 @@ function marshalDashboardStructureToHtml (json) {
 }
 
 function marshalLink (item, fieldset) {
+  return
   let btn = window.actionButtons[item.title]
 
   if (typeof btn === 'undefined') {
@@ -467,27 +329,19 @@ function marshalMreOutput (dashboardComponent, fieldset) {
   pre.classList.add('mre-output')
   pre.innerHTML = 'Waiting...'
 
-  const executionStatus = {
+  const args = {
     actionId: dashboardComponent.title
   }
 
-  window.fetch(window.restBaseUrl + 'ExecutionStatus', {
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/json'
-    },
-    body: JSON.stringify(executionStatus)
-  }).then((res) => {
-    if (res.ok) {
-      return res.json()
-    } else {
-      pre.innerHTML = 'error'
+  try { 
+    const status = window.client.executionStatus(args)
+
+    updateMre(pre, status.logEntry)
+  } catch (err) {
+    pre.innerHTML = 'error'
 
       throw new Error(res.statusText)
-    }
-  }).then((json) => {
-    updateMre(pre, json.logEntry)
-  })
+  }
 
   const updateMre = (pre, json) => {
     pre.innerHTML = json.output
@@ -556,68 +410,6 @@ function marshalFieldset (item, section, parentDashboard) {
   section.appendChild(fs)
 }
 
-function showSectionView (selected) {
-  if (selected === '') {
-    selected = null
-  }
-
-  for (const fieldset of document.querySelectorAll('fieldset')) {
-    if (selected === null) {
-      if ((fieldset.id === 'root-group' || fieldset.getAttribute('parent-dashboard') !== '') && fieldset.children.length > 1) {
-        fieldset.style.display = 'grid'
-      } else {
-        fieldset.style.display = 'none'
-      }
-    } else {
-      if (fieldset.title === selected) {
-        fieldset.style.display = 'grid'
-      } else {
-        fieldset.style.display = 'none'
-      }
-    }
-  }
-
-  const current = window.registeredPaths.get(window.currentPath)
-
-  for (const navLink of document.querySelector('nav').querySelectorAll('a')) {
-    if (navLink.title === current.section) {
-      navLink.classList.add('selected')
-    } else {
-      navLink.classList.remove('selected')
-    }
-  }
-
-  rebuildH1BreadcrumbNavigation(selected)
-
-  pushNewNavigationPath(window.currentPath)
-}
-
-function rebuildH1BreadcrumbNavigation () {
-  const title = document.querySelector('h1')
-  title.innerHTML = ''
-
-  const rootLink = document.createElement('a')
-  rootLink.innerText = window.pageTitle
-  rootLink.href = 'javascript:void(0)'
-  rootLink.onclick = () => {
-    showSection('/')
-  }
-
-  title.appendChild(rootLink)
-
-  for (const pathName of window.breadcrumbNavigation) {
-    const sep = document.createElement('span')
-    sep.innerHTML = ' &raquo; '
-    title.append(sep)
-
-    const path = window.registeredPaths.get(pathName)
-
-    title.appendChild(createNavigationBreadcrumbDisplay(path))
-  }
-
-  document.title = title.innerText
-}
-
 function createNavigationBreadcrumbDisplay (path) {
   const a = document.createElement('a')
   a.href = 'javascript:void(0)'
@@ -678,76 +470,9 @@ function marshalDirectory (item, section) {
 
   const path = '/' + section.title + '/' + getSystemTitle(item.title)
 
-  registerSection(path, section.title, item.title, null)
-
   return path
 }
 
-export function marshalLogsJsonToHtml (json) {
-  // This function is called internally with a "fake" server response, that does
-  // not have pageSize set. So we need to check if it's set before trying to use it.
-  if (json.pageSize !== undefined) {
-    document.getElementById('logs-server-page-size').innerText = json.pageSize
-  }
-
-  if (json.logs != null && json.logs.length > 0) {
-    document.getElementById('logsTable').hidden = false
-    document.getElementById('logsTableEmpty').hidden = true
-  } else {
-    return
-  }
-
-  for (const logEntry of json.logs) {
-    let row = document.getElementById('log-' + logEntry.executionTrackingId)
-
-    if (row == null) {
-      const tpl = document.getElementById('tplLogRow')
-      row = tpl.content.querySelector('tr').cloneNode(true)
-      row.id = 'log-' + logEntry.executionTrackingId
-
-      row.querySelector('.content').onclick = () => {
-        window.executionDialog.reset()
-        window.executionDialog.show()
-        window.executionDialog.renderExecutionResult({
-          logEntry: window.logEntries.get(logEntry.executionTrackingId)
-        })
-        pushNewNavigationPath('/logs/' + logEntry.executionTrackingId)
-      }
-
-      row.exitCodeDisplay = new ActionStatusDisplay(row.querySelector('.exit-code'))
-
-      logEntry.dom = row
-
-      window.logEntries.set(logEntry.executionTrackingId, logEntry)
-
-      document.querySelector('#logTableBody').prepend(row)
-    }
-
-    row.querySelector('.timestamp').innerText = logEntry.datetimeStarted
-    row.querySelector('.content').innerText = logEntry.actionTitle
-    row.querySelector('.icon').innerHTML = logEntry.actionIcon
-    row.setAttribute('title', logEntry.actionTitle)
-
-    row.exitCodeDisplay.update(logEntry)
-
-    row.querySelector('.tags').innerHTML = ''
-
-    for (const tag of logEntry.tags) {
-      row.querySelector('.tags').append(createTag(tag))
-    }
-
-    row.querySelector('.tags').append(createAnnotation('user', logEntry.user))
-  }
-}
-
-window.addEventListener('popstate', (e) => {
-  e.preventDefault()
-
-  if (e.state != null && typeof e.state.path !== 'undefined') {
-    showSection(e.state.path)
-  }
-})
-
 export function refreshServerConnectionLabel () {
   if (window.restAvailable) {
     document.querySelector('#serverConnectionRest').classList.remove('error')

+ 50 - 0
frontend/js/websocket.js

@@ -0,0 +1,50 @@
+import {
+  refreshServerConnectionLabel
+} from './marshaller.js'
+
+export function checkWebsocketConnection () {
+  reconnectWebsocket()
+}
+
+window.websocketAvailable = false
+
+async function reconnectWebsocket () {
+  if (window.websocketAvailable) {
+    return
+  }
+
+  try {
+    window.websocketAvailable = true
+    for await (let e of window.client.eventStream()) {
+      handleEvent(e)
+    }
+  } catch (err) {
+    console.error('Websocket connection failed: ', err)
+  }
+
+  window.websocketAvailable = false
+  console.log('Reconnecting websocket...')
+}
+
+function handleEvent (msg) {
+  const typeName = msg.event.value.$typeName.replace('olivetin.api.v1.', '')
+
+  console.log("Websocket event receved: ", typeName)
+
+  const j = new Event(typeName)
+  j.payload = msg.event.value
+
+  switch (typeName) {
+    case 'EventOutputChunk':
+    case 'EventConfigChanged':
+    case 'EventEntityChanged':
+    case 'EventExecutionFinished':
+    case 'EventExecutionStarted':
+      window.dispatchEvent(j)
+      break
+    default:
+      console.warn('Unhandled websocket message type from server: ', typeName)
+
+      window.showBigError('ws-unhandled-message', 'handling websocket message', 'Unhandled websocket message type from server: ' + typeName, true)
+  }
+}

+ 0 - 0
webui.dev/lib/iconify-icon-2.0.0.min.js → frontend/lib/iconify-icon-2.0.0.min.js


+ 40 - 59
webui.dev/main.js → frontend/main.js

@@ -1,15 +1,22 @@
 'use strict'
 
+import { createClient } from '@connectrpc/connect'
+import { createConnectTransport } from '@connectrpc/connect-web'
+
+import { OliveTinApiService } from './resources/scripts/gen/olivetin/api/v1/olivetin_pb'
+
+import { createApp } from 'vue'
+import router from './resources/vue/router.js';
+import App from './resources/vue/App.vue';
+
 import {
   initMarshaller,
   setupSectionNavigation,
   marshalDashboardComponentsJsonToHtml,
-  marshalLogsJsonToHtml,
   refreshServerConnectionLabel
 } from './js/marshaller.js'
-import { checkWebsocketConnection } from './js/websocket.js'
 
-import { LoginForm } from './js/LoginForm.js'
+import { checkWebsocketConnection } from './js/websocket.js'
 
 function searchLogs (e) {
   document.getElementById('searchLogsClear').disabled = false
@@ -32,63 +39,24 @@ function searchLogsClear () {
   document.getElementById('logSearchBox').value = ''
 }
 
-function setupLogSearchBox () {
-  document.getElementById('logSearchBox').oninput = searchLogs
-  document.getElementById('searchLogsClear').onclick = searchLogsClear
-}
 
 function refreshLoop () {
-  if (window.websocketAvailable) {
-    // Websocket updates are streamed live, not updated on a loop.
-  } else if (window.restAvailable) {
-    // Fallback to rest, but try to reconnect the websocket anyway.
-
-    fetchGetDashboardComponents()
-    fetchGetLogs()
-
-    checkWebsocketConnection()
-  } else {
-    // Still try to fetch the dashboard, if successfull window.restAvailable = true
-    fetchGetDashboardComponents()
-  }
-
+  checkWebsocketConnection()
+//  fetchGetDashboardComponents()
+//  fetchGetLogs()
   refreshServerConnectionLabel()
 }
 
-function fetchGetDashboardComponents () {
-  window.fetch(window.restBaseUrl + 'GetDashboardComponents', {
-    cors: 'cors'
-  }).then(res => {
-    if (!res.ok && res.status === 403) {
-      return null
-    }
+async function fetchGetDashboardComponents () {
+  try {
+    const res = await window.client.getDashboardComponents()
 
-    return res.json()
-  }).then(res => {
-    if (!window.restAvailable) {
-      window.clearBigErrors()
-    }
-
-    window.restAvailable = true
     marshalDashboardComponentsJsonToHtml(res)
 
     refreshServerConnectionLabel() // in-case it changed, update the label quicker
-  }).catch((err) => { // err is 1st arg
-    window.restAvailable = false
-    window.showBigError('fetch-buttons', 'getting buttons', err, false)
-  })
-}
-
-function fetchGetLogs () {
-  window.fetch(window.restBaseUrl + 'GetLogs', {
-    cors: 'cors'
-  }).then(res => {
-    return res.json()
-  }).then(res => {
-    marshalLogsJsonToHtml(res)
-  }).catch(err => {
+  } catch(err) {
     window.showBigError('fetch-buttons', 'getting buttons', err, false)
-  })
+  }
 }
 
 function processWebuiSettingsJson (settings) {
@@ -130,13 +98,6 @@ function processWebuiSettingsJson (settings) {
 
   processAdditionalLinks(settings.AdditionalLinks)
 
-  const loginForm = new LoginForm()
-  loginForm.setup()
-  loginForm.processOAuth2Providers(settings.AuthOAuth2Providers)
-  loginForm.processLocalLogin(settings.AuthLocalLogin)
-
-  document.getElementsByTagName('main')[0].appendChild(loginForm)
-
   window.settings = settings
 }
 
@@ -165,10 +126,28 @@ function processAdditionalLinks (links) {
   }
 }
 
+function initClient () {
+  const transport = createConnectTransport({
+    baseUrl: window.location.protocol + '//' + window.location.host + '/api/',
+
+  })
+
+  window.client = createClient(OliveTinApiService, transport)
+}
+
+function setupVue () {
+  const app = createApp(App)
+
+  app.use(router);
+  app.mount('#app')
+}
+
 function main () {
-  initMarshaller()
+  setupVue();
+
+  initClient() 
 
-  setupLogSearchBox()
+  initMarshaller()
 
   window.addEventListener('EventConfigChanged', fetchGetDashboardComponents)
   window.addEventListener('EventEntityChanged', fetchGetDashboardComponents)
@@ -178,6 +157,8 @@ function main () {
   }).then(res => {
     processWebuiSettingsJson(res)
 
+    fetchGetDashboardComponents()
+
     window.restAvailable = true
     window.refreshLoop = refreshLoop
     window.refreshLoop()

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 588 - 1745
frontend/package-lock.json


+ 8 - 3
webui.dev/package.json → frontend/package.json

@@ -10,8 +10,6 @@
 		"eslint-plugin-import": "^2.22.1",
 		"eslint-plugin-node": "^11.1.0",
 		"eslint-plugin-promise": "^4.3.1",
-		"parcel": "^2.11.0",
-		"parcel-resolver-ignore": "^2.2.0",
 		"process": "^0.11.10",
 		"stylelint": "^15.6.0",
 		"stylelint-config-standard": "^33.0.0"
@@ -29,7 +27,14 @@
 	],
 	"license": "AGPL-3.0-only",
 	"dependencies": {
+		"@connectrpc/connect": "^2.0.3",
+		"@connectrpc/connect-web": "^2.0.3",
+		"@vitejs/plugin-vue": "^6.0.1",
+		"@xterm/addon-fit": "^0.10.0",
 		"@xterm/xterm": "^5.5.0",
-		"@xterm/addon-fit": "^0.10.0"
+		"femtocrank": "^1.2.2",
+		"unplugin-vue-components": "^28.8.0",
+		"vite": "^7.0.6",
+		"vue-router": "^4.5.1"
 	}
 }

+ 1347 - 0
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts

@@ -0,0 +1,1347 @@
+// @generated by protoc-gen-es v2.6.2
+// @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
+/* eslint-disable */
+
+import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
+import type { Message } from "@bufbuild/protobuf";
+
+/**
+ * Describes the file olivetin/api/v1/olivetin.proto.
+ */
+export declare const file_olivetin_api_v1_olivetin: GenFile;
+
+/**
+ * @generated from message olivetin.api.v1.Action
+ */
+export declare type Action = Message<"olivetin.api.v1.Action"> & {
+  /**
+   * @generated from field: string id = 1;
+   */
+  id: string;
+
+  /**
+   * @generated from field: string title = 2;
+   */
+  title: string;
+
+  /**
+   * @generated from field: string icon = 3;
+   */
+  icon: string;
+
+  /**
+   * @generated from field: bool can_exec = 4;
+   */
+  canExec: boolean;
+
+  /**
+   * @generated from field: repeated olivetin.api.v1.ActionArgument arguments = 5;
+   */
+  arguments: ActionArgument[];
+
+  /**
+   * @generated from field: string popup_on_start = 6;
+   */
+  popupOnStart: string;
+
+  /**
+   * @generated from field: int32 order = 7;
+   */
+  order: number;
+};
+
+/**
+ * Describes the message olivetin.api.v1.Action.
+ * Use `create(ActionSchema)` to create a new message.
+ */
+export declare const ActionSchema: GenMessage<Action>;
+
+/**
+ * @generated from message olivetin.api.v1.ActionArgument
+ */
+export declare type ActionArgument = Message<"olivetin.api.v1.ActionArgument"> & {
+  /**
+   * @generated from field: string name = 1;
+   */
+  name: string;
+
+  /**
+   * @generated from field: string title = 2;
+   */
+  title: string;
+
+  /**
+   * @generated from field: string type = 3;
+   */
+  type: string;
+
+  /**
+   * @generated from field: string default_value = 4;
+   */
+  defaultValue: string;
+
+  /**
+   * @generated from field: repeated olivetin.api.v1.ActionArgumentChoice choices = 5;
+   */
+  choices: ActionArgumentChoice[];
+
+  /**
+   * @generated from field: string description = 6;
+   */
+  description: string;
+
+  /**
+   * @generated from field: map<string, string> suggestions = 7;
+   */
+  suggestions: { [key: string]: string };
+};
+
+/**
+ * Describes the message olivetin.api.v1.ActionArgument.
+ * Use `create(ActionArgumentSchema)` to create a new message.
+ */
+export declare const ActionArgumentSchema: GenMessage<ActionArgument>;
+
+/**
+ * @generated from message olivetin.api.v1.ActionArgumentChoice
+ */
+export declare type ActionArgumentChoice = Message<"olivetin.api.v1.ActionArgumentChoice"> & {
+  /**
+   * @generated from field: string value = 1;
+   */
+  value: string;
+
+  /**
+   * @generated from field: string title = 2;
+   */
+  title: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.ActionArgumentChoice.
+ * Use `create(ActionArgumentChoiceSchema)` to create a new message.
+ */
+export declare const ActionArgumentChoiceSchema: GenMessage<ActionArgumentChoice>;
+
+/**
+ * @generated from message olivetin.api.v1.Entity
+ */
+export declare type Entity = Message<"olivetin.api.v1.Entity"> & {
+  /**
+   * @generated from field: string title = 1;
+   */
+  title: string;
+
+  /**
+   * @generated from field: string icon = 2;
+   */
+  icon: string;
+
+  /**
+   * @generated from field: repeated olivetin.api.v1.Action actions = 3;
+   */
+  actions: Action[];
+};
+
+/**
+ * Describes the message olivetin.api.v1.Entity.
+ * Use `create(EntitySchema)` to create a new message.
+ */
+export declare const EntitySchema: GenMessage<Entity>;
+
+/**
+ * @generated from message olivetin.api.v1.GetDashboardComponentsResponse
+ */
+export declare type GetDashboardComponentsResponse = Message<"olivetin.api.v1.GetDashboardComponentsResponse"> & {
+  /**
+   * @generated from field: string title = 1;
+   */
+  title: string;
+
+  /**
+   * @generated from field: repeated olivetin.api.v1.Dashboard dashboards = 4;
+   */
+  dashboards: Dashboard[];
+
+  /**
+   * @generated from field: string authenticated_user = 5;
+   */
+  authenticatedUser: string;
+
+  /**
+   * @generated from field: string authenticated_user_provider = 6;
+   */
+  authenticatedUserProvider: string;
+
+  /**
+   * @generated from field: olivetin.api.v1.EffectivePolicy effective_policy = 7;
+   */
+  effectivePolicy?: EffectivePolicy;
+};
+
+/**
+ * Describes the message olivetin.api.v1.GetDashboardComponentsResponse.
+ * Use `create(GetDashboardComponentsResponseSchema)` to create a new message.
+ */
+export declare const GetDashboardComponentsResponseSchema: GenMessage<GetDashboardComponentsResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.EffectivePolicy
+ */
+export declare type EffectivePolicy = Message<"olivetin.api.v1.EffectivePolicy"> & {
+  /**
+   * @generated from field: bool show_diagnostics = 1;
+   */
+  showDiagnostics: boolean;
+
+  /**
+   * @generated from field: bool show_log_list = 2;
+   */
+  showLogList: boolean;
+};
+
+/**
+ * Describes the message olivetin.api.v1.EffectivePolicy.
+ * Use `create(EffectivePolicySchema)` to create a new message.
+ */
+export declare const EffectivePolicySchema: GenMessage<EffectivePolicy>;
+
+/**
+ * @generated from message olivetin.api.v1.GetDashboardComponentsRequest
+ */
+export declare type GetDashboardComponentsRequest = Message<"olivetin.api.v1.GetDashboardComponentsRequest"> & {
+};
+
+/**
+ * Describes the message olivetin.api.v1.GetDashboardComponentsRequest.
+ * Use `create(GetDashboardComponentsRequestSchema)` to create a new message.
+ */
+export declare const GetDashboardComponentsRequestSchema: GenMessage<GetDashboardComponentsRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.Dashboard
+ */
+export declare type Dashboard = Message<"olivetin.api.v1.Dashboard"> & {
+  /**
+   * @generated from field: string title = 1;
+   */
+  title: string;
+
+  /**
+   * @generated from field: repeated olivetin.api.v1.DashboardComponent contents = 2;
+   */
+  contents: DashboardComponent[];
+};
+
+/**
+ * Describes the message olivetin.api.v1.Dashboard.
+ * Use `create(DashboardSchema)` to create a new message.
+ */
+export declare const DashboardSchema: GenMessage<Dashboard>;
+
+/**
+ * @generated from message olivetin.api.v1.DashboardComponent
+ */
+export declare type DashboardComponent = Message<"olivetin.api.v1.DashboardComponent"> & {
+  /**
+   * @generated from field: string title = 1;
+   */
+  title: string;
+
+  /**
+   * @generated from field: string type = 2;
+   */
+  type: string;
+
+  /**
+   * @generated from field: repeated olivetin.api.v1.DashboardComponent contents = 3;
+   */
+  contents: DashboardComponent[];
+
+  /**
+   * @generated from field: string icon = 4;
+   */
+  icon: string;
+
+  /**
+   * @generated from field: string css_class = 5;
+   */
+  cssClass: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.DashboardComponent.
+ * Use `create(DashboardComponentSchema)` to create a new message.
+ */
+export declare const DashboardComponentSchema: GenMessage<DashboardComponent>;
+
+/**
+ * @generated from message olivetin.api.v1.StartActionRequest
+ */
+export declare type StartActionRequest = Message<"olivetin.api.v1.StartActionRequest"> & {
+  /**
+   * @generated from field: string action_id = 1;
+   */
+  actionId: string;
+
+  /**
+   * @generated from field: repeated olivetin.api.v1.StartActionArgument arguments = 2;
+   */
+  arguments: StartActionArgument[];
+
+  /**
+   * @generated from field: string unique_tracking_id = 3;
+   */
+  uniqueTrackingId: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.StartActionRequest.
+ * Use `create(StartActionRequestSchema)` to create a new message.
+ */
+export declare const StartActionRequestSchema: GenMessage<StartActionRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.StartActionArgument
+ */
+export declare type StartActionArgument = Message<"olivetin.api.v1.StartActionArgument"> & {
+  /**
+   * @generated from field: string name = 1;
+   */
+  name: string;
+
+  /**
+   * @generated from field: string value = 2;
+   */
+  value: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.StartActionArgument.
+ * Use `create(StartActionArgumentSchema)` to create a new message.
+ */
+export declare const StartActionArgumentSchema: GenMessage<StartActionArgument>;
+
+/**
+ * @generated from message olivetin.api.v1.StartActionResponse
+ */
+export declare type StartActionResponse = Message<"olivetin.api.v1.StartActionResponse"> & {
+  /**
+   * @generated from field: string execution_tracking_id = 2;
+   */
+  executionTrackingId: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.StartActionResponse.
+ * Use `create(StartActionResponseSchema)` to create a new message.
+ */
+export declare const StartActionResponseSchema: GenMessage<StartActionResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.StartActionAndWaitRequest
+ */
+export declare type StartActionAndWaitRequest = Message<"olivetin.api.v1.StartActionAndWaitRequest"> & {
+  /**
+   * @generated from field: string action_id = 1;
+   */
+  actionId: string;
+
+  /**
+   * @generated from field: repeated olivetin.api.v1.StartActionArgument arguments = 2;
+   */
+  arguments: StartActionArgument[];
+};
+
+/**
+ * Describes the message olivetin.api.v1.StartActionAndWaitRequest.
+ * Use `create(StartActionAndWaitRequestSchema)` to create a new message.
+ */
+export declare const StartActionAndWaitRequestSchema: GenMessage<StartActionAndWaitRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.StartActionAndWaitResponse
+ */
+export declare type StartActionAndWaitResponse = Message<"olivetin.api.v1.StartActionAndWaitResponse"> & {
+  /**
+   * @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
+   */
+  logEntry?: LogEntry;
+};
+
+/**
+ * Describes the message olivetin.api.v1.StartActionAndWaitResponse.
+ * Use `create(StartActionAndWaitResponseSchema)` to create a new message.
+ */
+export declare const StartActionAndWaitResponseSchema: GenMessage<StartActionAndWaitResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.StartActionByGetRequest
+ */
+export declare type StartActionByGetRequest = Message<"olivetin.api.v1.StartActionByGetRequest"> & {
+  /**
+   * @generated from field: string action_id = 1;
+   */
+  actionId: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.StartActionByGetRequest.
+ * Use `create(StartActionByGetRequestSchema)` to create a new message.
+ */
+export declare const StartActionByGetRequestSchema: GenMessage<StartActionByGetRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.StartActionByGetResponse
+ */
+export declare type StartActionByGetResponse = Message<"olivetin.api.v1.StartActionByGetResponse"> & {
+  /**
+   * @generated from field: string execution_tracking_id = 2;
+   */
+  executionTrackingId: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.StartActionByGetResponse.
+ * Use `create(StartActionByGetResponseSchema)` to create a new message.
+ */
+export declare const StartActionByGetResponseSchema: GenMessage<StartActionByGetResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.StartActionByGetAndWaitRequest
+ */
+export declare type StartActionByGetAndWaitRequest = Message<"olivetin.api.v1.StartActionByGetAndWaitRequest"> & {
+  /**
+   * @generated from field: string action_id = 1;
+   */
+  actionId: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.StartActionByGetAndWaitRequest.
+ * Use `create(StartActionByGetAndWaitRequestSchema)` to create a new message.
+ */
+export declare const StartActionByGetAndWaitRequestSchema: GenMessage<StartActionByGetAndWaitRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.StartActionByGetAndWaitResponse
+ */
+export declare type StartActionByGetAndWaitResponse = Message<"olivetin.api.v1.StartActionByGetAndWaitResponse"> & {
+  /**
+   * @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
+   */
+  logEntry?: LogEntry;
+};
+
+/**
+ * Describes the message olivetin.api.v1.StartActionByGetAndWaitResponse.
+ * Use `create(StartActionByGetAndWaitResponseSchema)` to create a new message.
+ */
+export declare const StartActionByGetAndWaitResponseSchema: GenMessage<StartActionByGetAndWaitResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.GetLogsRequest
+ */
+export declare type GetLogsRequest = Message<"olivetin.api.v1.GetLogsRequest"> & {
+  /**
+   * @generated from field: int64 start_offset = 1;
+   */
+  startOffset: bigint;
+};
+
+/**
+ * Describes the message olivetin.api.v1.GetLogsRequest.
+ * Use `create(GetLogsRequestSchema)` to create a new message.
+ */
+export declare const GetLogsRequestSchema: GenMessage<GetLogsRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.LogEntry
+ */
+export declare type LogEntry = Message<"olivetin.api.v1.LogEntry"> & {
+  /**
+   * @generated from field: string datetime_started = 1;
+   */
+  datetimeStarted: string;
+
+  /**
+   * @generated from field: string action_title = 2;
+   */
+  actionTitle: string;
+
+  /**
+   * @generated from field: string output = 3;
+   */
+  output: string;
+
+  /**
+   * @generated from field: bool timed_out = 5;
+   */
+  timedOut: boolean;
+
+  /**
+   * @generated from field: int32 exit_code = 6;
+   */
+  exitCode: number;
+
+  /**
+   * @generated from field: string user = 7;
+   */
+  user: string;
+
+  /**
+   * @generated from field: string user_class = 8;
+   */
+  userClass: string;
+
+  /**
+   * @generated from field: string action_icon = 9;
+   */
+  actionIcon: string;
+
+  /**
+   * @generated from field: repeated string tags = 10;
+   */
+  tags: string[];
+
+  /**
+   * @generated from field: string execution_tracking_id = 11;
+   */
+  executionTrackingId: string;
+
+  /**
+   * @generated from field: string datetime_finished = 12;
+   */
+  datetimeFinished: string;
+
+  /**
+   * @generated from field: string action_id = 13;
+   */
+  actionId: string;
+
+  /**
+   * @generated from field: bool execution_started = 14;
+   */
+  executionStarted: boolean;
+
+  /**
+   * @generated from field: bool execution_finished = 15;
+   */
+  executionFinished: boolean;
+
+  /**
+   * @generated from field: bool blocked = 16;
+   */
+  blocked: boolean;
+
+  /**
+   * @generated from field: int64 datetime_index = 17;
+   */
+  datetimeIndex: bigint;
+
+  /**
+   * @generated from field: bool can_kill = 18;
+   */
+  canKill: boolean;
+};
+
+/**
+ * Describes the message olivetin.api.v1.LogEntry.
+ * Use `create(LogEntrySchema)` to create a new message.
+ */
+export declare const LogEntrySchema: GenMessage<LogEntry>;
+
+/**
+ * @generated from message olivetin.api.v1.GetLogsResponse
+ */
+export declare type GetLogsResponse = Message<"olivetin.api.v1.GetLogsResponse"> & {
+  /**
+   * @generated from field: repeated olivetin.api.v1.LogEntry logs = 1;
+   */
+  logs: LogEntry[];
+
+  /**
+   * @generated from field: int64 count_remaining = 2;
+   */
+  countRemaining: bigint;
+
+  /**
+   * @generated from field: int64 page_size = 3;
+   */
+  pageSize: bigint;
+};
+
+/**
+ * Describes the message olivetin.api.v1.GetLogsResponse.
+ * Use `create(GetLogsResponseSchema)` to create a new message.
+ */
+export declare const GetLogsResponseSchema: GenMessage<GetLogsResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.ValidateArgumentTypeRequest
+ */
+export declare type ValidateArgumentTypeRequest = Message<"olivetin.api.v1.ValidateArgumentTypeRequest"> & {
+  /**
+   * @generated from field: string value = 1;
+   */
+  value: string;
+
+  /**
+   * @generated from field: string type = 2;
+   */
+  type: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.ValidateArgumentTypeRequest.
+ * Use `create(ValidateArgumentTypeRequestSchema)` to create a new message.
+ */
+export declare const ValidateArgumentTypeRequestSchema: GenMessage<ValidateArgumentTypeRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.ValidateArgumentTypeResponse
+ */
+export declare type ValidateArgumentTypeResponse = Message<"olivetin.api.v1.ValidateArgumentTypeResponse"> & {
+  /**
+   * @generated from field: bool valid = 1;
+   */
+  valid: boolean;
+
+  /**
+   * @generated from field: string description = 2;
+   */
+  description: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.ValidateArgumentTypeResponse.
+ * Use `create(ValidateArgumentTypeResponseSchema)` to create a new message.
+ */
+export declare const ValidateArgumentTypeResponseSchema: GenMessage<ValidateArgumentTypeResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.WatchExecutionRequest
+ */
+export declare type WatchExecutionRequest = Message<"olivetin.api.v1.WatchExecutionRequest"> & {
+  /**
+   * @generated from field: string execution_tracking_id = 1;
+   */
+  executionTrackingId: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.WatchExecutionRequest.
+ * Use `create(WatchExecutionRequestSchema)` to create a new message.
+ */
+export declare const WatchExecutionRequestSchema: GenMessage<WatchExecutionRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.WatchExecutionUpdate
+ */
+export declare type WatchExecutionUpdate = Message<"olivetin.api.v1.WatchExecutionUpdate"> & {
+  /**
+   * @generated from field: string update = 1;
+   */
+  update: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.WatchExecutionUpdate.
+ * Use `create(WatchExecutionUpdateSchema)` to create a new message.
+ */
+export declare const WatchExecutionUpdateSchema: GenMessage<WatchExecutionUpdate>;
+
+/**
+ * @generated from message olivetin.api.v1.ExecutionStatusRequest
+ */
+export declare type ExecutionStatusRequest = Message<"olivetin.api.v1.ExecutionStatusRequest"> & {
+  /**
+   * @generated from field: string execution_tracking_id = 1;
+   */
+  executionTrackingId: string;
+
+  /**
+   * @generated from field: string action_id = 2;
+   */
+  actionId: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.ExecutionStatusRequest.
+ * Use `create(ExecutionStatusRequestSchema)` to create a new message.
+ */
+export declare const ExecutionStatusRequestSchema: GenMessage<ExecutionStatusRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.ExecutionStatusResponse
+ */
+export declare type ExecutionStatusResponse = Message<"olivetin.api.v1.ExecutionStatusResponse"> & {
+  /**
+   * @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
+   */
+  logEntry?: LogEntry;
+};
+
+/**
+ * Describes the message olivetin.api.v1.ExecutionStatusResponse.
+ * Use `create(ExecutionStatusResponseSchema)` to create a new message.
+ */
+export declare const ExecutionStatusResponseSchema: GenMessage<ExecutionStatusResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.WhoAmIRequest
+ */
+export declare type WhoAmIRequest = Message<"olivetin.api.v1.WhoAmIRequest"> & {
+};
+
+/**
+ * Describes the message olivetin.api.v1.WhoAmIRequest.
+ * Use `create(WhoAmIRequestSchema)` to create a new message.
+ */
+export declare const WhoAmIRequestSchema: GenMessage<WhoAmIRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.WhoAmIResponse
+ */
+export declare type WhoAmIResponse = Message<"olivetin.api.v1.WhoAmIResponse"> & {
+  /**
+   * @generated from field: string authenticated_user = 1;
+   */
+  authenticatedUser: string;
+
+  /**
+   * @generated from field: string usergroup = 2;
+   */
+  usergroup: string;
+
+  /**
+   * @generated from field: string provider = 3;
+   */
+  provider: string;
+
+  /**
+   * @generated from field: repeated string acls = 4;
+   */
+  acls: string[];
+
+  /**
+   * @generated from field: string sid = 5;
+   */
+  sid: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.WhoAmIResponse.
+ * Use `create(WhoAmIResponseSchema)` to create a new message.
+ */
+export declare const WhoAmIResponseSchema: GenMessage<WhoAmIResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.SosReportRequest
+ */
+export declare type SosReportRequest = Message<"olivetin.api.v1.SosReportRequest"> & {
+};
+
+/**
+ * Describes the message olivetin.api.v1.SosReportRequest.
+ * Use `create(SosReportRequestSchema)` to create a new message.
+ */
+export declare const SosReportRequestSchema: GenMessage<SosReportRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.SosReportResponse
+ */
+export declare type SosReportResponse = Message<"olivetin.api.v1.SosReportResponse"> & {
+  /**
+   * @generated from field: string alert = 1;
+   */
+  alert: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.SosReportResponse.
+ * Use `create(SosReportResponseSchema)` to create a new message.
+ */
+export declare const SosReportResponseSchema: GenMessage<SosReportResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.DumpVarsRequest
+ */
+export declare type DumpVarsRequest = Message<"olivetin.api.v1.DumpVarsRequest"> & {
+};
+
+/**
+ * Describes the message olivetin.api.v1.DumpVarsRequest.
+ * Use `create(DumpVarsRequestSchema)` to create a new message.
+ */
+export declare const DumpVarsRequestSchema: GenMessage<DumpVarsRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.DumpVarsResponse
+ */
+export declare type DumpVarsResponse = Message<"olivetin.api.v1.DumpVarsResponse"> & {
+  /**
+   * @generated from field: string alert = 1;
+   */
+  alert: string;
+
+  /**
+   * @generated from field: map<string, string> contents = 2;
+   */
+  contents: { [key: string]: string };
+};
+
+/**
+ * Describes the message olivetin.api.v1.DumpVarsResponse.
+ * Use `create(DumpVarsResponseSchema)` to create a new message.
+ */
+export declare const DumpVarsResponseSchema: GenMessage<DumpVarsResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.ActionEntityPair
+ */
+export declare type ActionEntityPair = Message<"olivetin.api.v1.ActionEntityPair"> & {
+  /**
+   * @generated from field: string action_title = 1;
+   */
+  actionTitle: string;
+
+  /**
+   * @generated from field: string entity_prefix = 2;
+   */
+  entityPrefix: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.ActionEntityPair.
+ * Use `create(ActionEntityPairSchema)` to create a new message.
+ */
+export declare const ActionEntityPairSchema: GenMessage<ActionEntityPair>;
+
+/**
+ * @generated from message olivetin.api.v1.DumpPublicIdActionMapRequest
+ */
+export declare type DumpPublicIdActionMapRequest = Message<"olivetin.api.v1.DumpPublicIdActionMapRequest"> & {
+};
+
+/**
+ * Describes the message olivetin.api.v1.DumpPublicIdActionMapRequest.
+ * Use `create(DumpPublicIdActionMapRequestSchema)` to create a new message.
+ */
+export declare const DumpPublicIdActionMapRequestSchema: GenMessage<DumpPublicIdActionMapRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.DumpPublicIdActionMapResponse
+ */
+export declare type DumpPublicIdActionMapResponse = Message<"olivetin.api.v1.DumpPublicIdActionMapResponse"> & {
+  /**
+   * @generated from field: string alert = 1;
+   */
+  alert: string;
+
+  /**
+   * @generated from field: map<string, olivetin.api.v1.ActionEntityPair> contents = 2;
+   */
+  contents: { [key: string]: ActionEntityPair };
+};
+
+/**
+ * Describes the message olivetin.api.v1.DumpPublicIdActionMapResponse.
+ * Use `create(DumpPublicIdActionMapResponseSchema)` to create a new message.
+ */
+export declare const DumpPublicIdActionMapResponseSchema: GenMessage<DumpPublicIdActionMapResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.GetReadyzRequest
+ */
+export declare type GetReadyzRequest = Message<"olivetin.api.v1.GetReadyzRequest"> & {
+};
+
+/**
+ * Describes the message olivetin.api.v1.GetReadyzRequest.
+ * Use `create(GetReadyzRequestSchema)` to create a new message.
+ */
+export declare const GetReadyzRequestSchema: GenMessage<GetReadyzRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.GetReadyzResponse
+ */
+export declare type GetReadyzResponse = Message<"olivetin.api.v1.GetReadyzResponse"> & {
+  /**
+   * @generated from field: string status = 1;
+   */
+  status: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.GetReadyzResponse.
+ * Use `create(GetReadyzResponseSchema)` to create a new message.
+ */
+export declare const GetReadyzResponseSchema: GenMessage<GetReadyzResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.EventStreamRequest
+ */
+export declare type EventStreamRequest = Message<"olivetin.api.v1.EventStreamRequest"> & {
+};
+
+/**
+ * Describes the message olivetin.api.v1.EventStreamRequest.
+ * Use `create(EventStreamRequestSchema)` to create a new message.
+ */
+export declare const EventStreamRequestSchema: GenMessage<EventStreamRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.EventStreamResponse
+ */
+export declare type EventStreamResponse = Message<"olivetin.api.v1.EventStreamResponse"> & {
+  /**
+   * @generated from oneof olivetin.api.v1.EventStreamResponse.event
+   */
+  event: {
+    /**
+     * @generated from field: olivetin.api.v1.EventEntityChanged entity_changed = 2;
+     */
+    value: EventEntityChanged;
+    case: "entityChanged";
+  } | {
+    /**
+     * @generated from field: olivetin.api.v1.EventConfigChanged config_changed = 3;
+     */
+    value: EventConfigChanged;
+    case: "configChanged";
+  } | {
+    /**
+     * @generated from field: olivetin.api.v1.EventExecutionFinished execution_finished = 4;
+     */
+    value: EventExecutionFinished;
+    case: "executionFinished";
+  } | {
+    /**
+     * @generated from field: olivetin.api.v1.EventExecutionStarted execution_started = 5;
+     */
+    value: EventExecutionStarted;
+    case: "executionStarted";
+  } | {
+    /**
+     * @generated from field: olivetin.api.v1.EventOutputChunk output_chunk = 6;
+     */
+    value: EventOutputChunk;
+    case: "outputChunk";
+  } | { case: undefined; value?: undefined };
+};
+
+/**
+ * Describes the message olivetin.api.v1.EventStreamResponse.
+ * Use `create(EventStreamResponseSchema)` to create a new message.
+ */
+export declare const EventStreamResponseSchema: GenMessage<EventStreamResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.EventOutputChunk
+ */
+export declare type EventOutputChunk = Message<"olivetin.api.v1.EventOutputChunk"> & {
+  /**
+   * @generated from field: string execution_tracking_id = 1;
+   */
+  executionTrackingId: string;
+
+  /**
+   * @generated from field: string output = 2;
+   */
+  output: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.EventOutputChunk.
+ * Use `create(EventOutputChunkSchema)` to create a new message.
+ */
+export declare const EventOutputChunkSchema: GenMessage<EventOutputChunk>;
+
+/**
+ * @generated from message olivetin.api.v1.EventEntityChanged
+ */
+export declare type EventEntityChanged = Message<"olivetin.api.v1.EventEntityChanged"> & {
+};
+
+/**
+ * Describes the message olivetin.api.v1.EventEntityChanged.
+ * Use `create(EventEntityChangedSchema)` to create a new message.
+ */
+export declare const EventEntityChangedSchema: GenMessage<EventEntityChanged>;
+
+/**
+ * @generated from message olivetin.api.v1.EventConfigChanged
+ */
+export declare type EventConfigChanged = Message<"olivetin.api.v1.EventConfigChanged"> & {
+};
+
+/**
+ * Describes the message olivetin.api.v1.EventConfigChanged.
+ * Use `create(EventConfigChangedSchema)` to create a new message.
+ */
+export declare const EventConfigChangedSchema: GenMessage<EventConfigChanged>;
+
+/**
+ * @generated from message olivetin.api.v1.EventExecutionFinished
+ */
+export declare type EventExecutionFinished = Message<"olivetin.api.v1.EventExecutionFinished"> & {
+  /**
+   * @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
+   */
+  logEntry?: LogEntry;
+};
+
+/**
+ * Describes the message olivetin.api.v1.EventExecutionFinished.
+ * Use `create(EventExecutionFinishedSchema)` to create a new message.
+ */
+export declare const EventExecutionFinishedSchema: GenMessage<EventExecutionFinished>;
+
+/**
+ * @generated from message olivetin.api.v1.EventExecutionStarted
+ */
+export declare type EventExecutionStarted = Message<"olivetin.api.v1.EventExecutionStarted"> & {
+  /**
+   * @generated from field: olivetin.api.v1.LogEntry log_entry = 1;
+   */
+  logEntry?: LogEntry;
+};
+
+/**
+ * Describes the message olivetin.api.v1.EventExecutionStarted.
+ * Use `create(EventExecutionStartedSchema)` to create a new message.
+ */
+export declare const EventExecutionStartedSchema: GenMessage<EventExecutionStarted>;
+
+/**
+ * @generated from message olivetin.api.v1.KillActionRequest
+ */
+export declare type KillActionRequest = Message<"olivetin.api.v1.KillActionRequest"> & {
+  /**
+   * @generated from field: string execution_tracking_id = 1;
+   */
+  executionTrackingId: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.KillActionRequest.
+ * Use `create(KillActionRequestSchema)` to create a new message.
+ */
+export declare const KillActionRequestSchema: GenMessage<KillActionRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.KillActionResponse
+ */
+export declare type KillActionResponse = Message<"olivetin.api.v1.KillActionResponse"> & {
+  /**
+   * @generated from field: string execution_tracking_id = 1;
+   */
+  executionTrackingId: string;
+
+  /**
+   * @generated from field: bool killed = 2;
+   */
+  killed: boolean;
+
+  /**
+   * @generated from field: bool already_completed = 3;
+   */
+  alreadyCompleted: boolean;
+
+  /**
+   * @generated from field: bool found = 4;
+   */
+  found: boolean;
+};
+
+/**
+ * Describes the message olivetin.api.v1.KillActionResponse.
+ * Use `create(KillActionResponseSchema)` to create a new message.
+ */
+export declare const KillActionResponseSchema: GenMessage<KillActionResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.LocalUserLoginRequest
+ */
+export declare type LocalUserLoginRequest = Message<"olivetin.api.v1.LocalUserLoginRequest"> & {
+  /**
+   * @generated from field: string username = 1;
+   */
+  username: string;
+
+  /**
+   * @generated from field: string password = 2;
+   */
+  password: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.LocalUserLoginRequest.
+ * Use `create(LocalUserLoginRequestSchema)` to create a new message.
+ */
+export declare const LocalUserLoginRequestSchema: GenMessage<LocalUserLoginRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.LocalUserLoginResponse
+ */
+export declare type LocalUserLoginResponse = Message<"olivetin.api.v1.LocalUserLoginResponse"> & {
+  /**
+   * @generated from field: bool success = 1;
+   */
+  success: boolean;
+};
+
+/**
+ * Describes the message olivetin.api.v1.LocalUserLoginResponse.
+ * Use `create(LocalUserLoginResponseSchema)` to create a new message.
+ */
+export declare const LocalUserLoginResponseSchema: GenMessage<LocalUserLoginResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.PasswordHashRequest
+ */
+export declare type PasswordHashRequest = Message<"olivetin.api.v1.PasswordHashRequest"> & {
+  /**
+   * @generated from field: string password = 1;
+   */
+  password: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.PasswordHashRequest.
+ * Use `create(PasswordHashRequestSchema)` to create a new message.
+ */
+export declare const PasswordHashRequestSchema: GenMessage<PasswordHashRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.PasswordHashResponse
+ */
+export declare type PasswordHashResponse = Message<"olivetin.api.v1.PasswordHashResponse"> & {
+  /**
+   * @generated from field: string hash = 1;
+   */
+  hash: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.PasswordHashResponse.
+ * Use `create(PasswordHashResponseSchema)` to create a new message.
+ */
+export declare const PasswordHashResponseSchema: GenMessage<PasswordHashResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.LogoutRequest
+ */
+export declare type LogoutRequest = Message<"olivetin.api.v1.LogoutRequest"> & {
+};
+
+/**
+ * Describes the message olivetin.api.v1.LogoutRequest.
+ * Use `create(LogoutRequestSchema)` to create a new message.
+ */
+export declare const LogoutRequestSchema: GenMessage<LogoutRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.LogoutResponse
+ */
+export declare type LogoutResponse = Message<"olivetin.api.v1.LogoutResponse"> & {
+};
+
+/**
+ * Describes the message olivetin.api.v1.LogoutResponse.
+ * Use `create(LogoutResponseSchema)` to create a new message.
+ */
+export declare const LogoutResponseSchema: GenMessage<LogoutResponse>;
+
+/**
+ * @generated from message olivetin.api.v1.GetDiagnosticsRequest
+ */
+export declare type GetDiagnosticsRequest = Message<"olivetin.api.v1.GetDiagnosticsRequest"> & {
+};
+
+/**
+ * Describes the message olivetin.api.v1.GetDiagnosticsRequest.
+ * Use `create(GetDiagnosticsRequestSchema)` to create a new message.
+ */
+export declare const GetDiagnosticsRequestSchema: GenMessage<GetDiagnosticsRequest>;
+
+/**
+ * @generated from message olivetin.api.v1.GetDiagnosticsResponse
+ */
+export declare type GetDiagnosticsResponse = Message<"olivetin.api.v1.GetDiagnosticsResponse"> & {
+  /**
+   * @generated from field: string SshFoundKey = 1;
+   */
+  SshFoundKey: string;
+
+  /**
+   * @generated from field: string SshFoundConfig = 2;
+   */
+  SshFoundConfig: string;
+};
+
+/**
+ * Describes the message olivetin.api.v1.GetDiagnosticsResponse.
+ * Use `create(GetDiagnosticsResponseSchema)` to create a new message.
+ */
+export declare const GetDiagnosticsResponseSchema: GenMessage<GetDiagnosticsResponse>;
+
+/**
+ * @generated from service olivetin.api.v1.OliveTinApiService
+ */
+export declare const OliveTinApiService: GenService<{
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.GetDashboardComponents
+   */
+  getDashboardComponents: {
+    methodKind: "unary";
+    input: typeof GetDashboardComponentsRequestSchema;
+    output: typeof GetDashboardComponentsResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.StartAction
+   */
+  startAction: {
+    methodKind: "unary";
+    input: typeof StartActionRequestSchema;
+    output: typeof StartActionResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.StartActionAndWait
+   */
+  startActionAndWait: {
+    methodKind: "unary";
+    input: typeof StartActionAndWaitRequestSchema;
+    output: typeof StartActionAndWaitResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.StartActionByGet
+   */
+  startActionByGet: {
+    methodKind: "unary";
+    input: typeof StartActionByGetRequestSchema;
+    output: typeof StartActionByGetResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait
+   */
+  startActionByGetAndWait: {
+    methodKind: "unary";
+    input: typeof StartActionByGetAndWaitRequestSchema;
+    output: typeof StartActionByGetAndWaitResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.KillAction
+   */
+  killAction: {
+    methodKind: "unary";
+    input: typeof KillActionRequestSchema;
+    output: typeof KillActionResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.ExecutionStatus
+   */
+  executionStatus: {
+    methodKind: "unary";
+    input: typeof ExecutionStatusRequestSchema;
+    output: typeof ExecutionStatusResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.GetLogs
+   */
+  getLogs: {
+    methodKind: "unary";
+    input: typeof GetLogsRequestSchema;
+    output: typeof GetLogsResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.ValidateArgumentType
+   */
+  validateArgumentType: {
+    methodKind: "unary";
+    input: typeof ValidateArgumentTypeRequestSchema;
+    output: typeof ValidateArgumentTypeResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.WhoAmI
+   */
+  whoAmI: {
+    methodKind: "unary";
+    input: typeof WhoAmIRequestSchema;
+    output: typeof WhoAmIResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.SosReport
+   */
+  sosReport: {
+    methodKind: "unary";
+    input: typeof SosReportRequestSchema;
+    output: typeof SosReportResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.DumpVars
+   */
+  dumpVars: {
+    methodKind: "unary";
+    input: typeof DumpVarsRequestSchema;
+    output: typeof DumpVarsResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap
+   */
+  dumpPublicIdActionMap: {
+    methodKind: "unary";
+    input: typeof DumpPublicIdActionMapRequestSchema;
+    output: typeof DumpPublicIdActionMapResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.GetReadyz
+   */
+  getReadyz: {
+    methodKind: "unary";
+    input: typeof GetReadyzRequestSchema;
+    output: typeof GetReadyzResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.LocalUserLogin
+   */
+  localUserLogin: {
+    methodKind: "unary";
+    input: typeof LocalUserLoginRequestSchema;
+    output: typeof LocalUserLoginResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.PasswordHash
+   */
+  passwordHash: {
+    methodKind: "unary";
+    input: typeof PasswordHashRequestSchema;
+    output: typeof PasswordHashResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.Logout
+   */
+  logout: {
+    methodKind: "unary";
+    input: typeof LogoutRequestSchema;
+    output: typeof LogoutResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.EventStream
+   */
+  eventStream: {
+    methodKind: "server_streaming";
+    input: typeof EventStreamRequestSchema;
+    output: typeof EventStreamResponseSchema;
+  },
+  /**
+   * @generated from rpc olivetin.api.v1.OliveTinApiService.GetDiagnostics
+   */
+  getDiagnostics: {
+    methodKind: "unary";
+    input: typeof GetDiagnosticsRequestSchema;
+    output: typeof GetDiagnosticsResponseSchema;
+  },
+}>;
+

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 10 - 0
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js


+ 287 - 0
frontend/resources/vue/ActionButton.vue

@@ -0,0 +1,287 @@
+<template>
+  <div :id="`actionButton-${actionId}`" role="none" class="action-button">
+    <button :id="`actionButtonInner-${actionId}`" :title="title" :disabled="!canExec || isDisabled"
+      :class="buttonClasses" @click="handleClick">
+      <span class="icon" v-html="unicodeIcon"></span>
+      <span class="title" aria-live="polite">{{ displayTitle }}</span>
+    </button>
+
+    <ArgumentForm v-if="showArgumentForm" :action-data="actionData" @submit="handleArgumentSubmit"
+      @cancel="handleArgumentCancel" @close="handleArgumentClose" />
+  </div>
+</template>
+
+<script setup>
+import ArgumentForm from './ArgumentForm.vue'
+
+import { ref, computed, watch, onMounted, inject } from 'vue'
+
+const executionDialog = inject('executionDialog');
+
+const props = defineProps({
+  actionData: {
+    type: Object,
+    required: true
+  }
+})
+
+const actionId = ref('')
+const title = ref('')
+const canExec = ref(true)
+const popupOnStart = ref('')
+
+// Display properties
+const unicodeIcon = ref('&#x1f4a9;')
+const displayTitle = ref('')
+
+// State
+const isDisabled = ref(false)
+const showArgumentForm = ref(false)
+
+// Animation classes
+const buttonClasses = ref([])
+
+// Timestamps
+const updateIterationTimestamp = ref(0)
+
+function getUnicodeIcon(icon) {
+  if (icon === '') {
+    return '&#x1f4a9;'
+  } else {
+    return unescape(icon)
+  }
+}
+
+function constructFromJson(json) {
+  updateIterationTimestamp.value = 0
+
+  // Class attributes
+  updateFromJson(json)
+
+  actionId.value = json.id
+  title.value = json.title
+  canExec.value = json.canExec
+  popupOnStart.value = json.popupOnStart
+
+  isDisabled.value = !json.canExec
+  displayTitle.value = title.value
+  unicodeIcon.value = getUnicodeIcon(json.icon)
+}
+
+function updateFromJson(json) {
+  // Fields that should not be updated
+  // title - as the callback URL relies on it
+
+  unicodeIcon.value = getUnicodeIcon(json.icon)
+}
+
+async function handleClick() {
+  if (props.actionData.arguments && props.actionData.arguments.length > 0) {
+    updateUrlWithAction()
+    showArgumentForm.value = true
+  } else {
+    await startAction()
+  }
+}
+
+function updateUrlWithAction() {
+  // Get the current URL and create a new URL object
+  const url = new URL(window.location.href)
+
+  // Set the action parameter
+  url.searchParams.set('action', title.value)
+
+  // Update the URL without reloading the page
+  window.history.replaceState({}, '', url.toString())
+}
+
+function getUniqueId() {
+  if (window.isSecureContext) {
+    return window.crypto.randomUUID()
+  } else {
+    return Date.now().toString()
+  }
+}
+
+async function startAction(actionArgs) {
+  buttonClasses.value = [] // Removes old animation classes
+
+  if (actionArgs === undefined) {
+    actionArgs = []
+  }
+
+  // UUIDs are create client side, so that we can setup a "execution-button"
+  // to track the execution before we send the request to the server.
+  const startActionArgs = {
+    actionId: actionId.value,
+    arguments: actionArgs,
+    uniqueTrackingId: getUniqueId()
+  }
+
+  onActionStarted(startActionArgs.uniqueTrackingId)
+
+  try {
+    await window.client.startAction(startActionArgs)
+  } catch (err) {
+    console.error('Failed to start action:', err)
+  }
+}
+
+function onActionStarted(execTrackingId) {
+  console.log('onActionStarted', execTrackingId)
+  console.log('executionDialog', executionDialog)
+
+  if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
+    if (executionDialog.value) {
+      executionDialog.value.reset();
+
+      if (popupOnStart.value === 'execution-dialog-stdout-only') {
+        executionDialog.value.hideEverythingApartFromOutput();
+      }
+    }
+
+    executionDialog.value.executionTrackingId = execTrackingId;
+    executionDialog.value.show()
+  }
+
+  isDisabled.value = true
+}
+
+function handleArgumentSubmit(args) {
+  startAction(args)
+  showArgumentForm.value = false
+}
+
+function handleArgumentCancel() {
+  showArgumentForm.value = false
+}
+
+function handleArgumentClose() {
+  showArgumentForm.value = false
+}
+
+// ExecutionFeedbackButton methods
+function onExecutionFinished(logEntry) {
+  if (logEntry.timedOut) {
+    renderExecutionResult('action-timeout', 'Timed out')
+  } else if (logEntry.blocked) {
+    renderExecutionResult('action-blocked', 'Blocked!')
+  } else if (logEntry.exitCode !== 0) {
+    renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
+  } else {
+    const ellapsed = Math.ceil(new Date(logEntry.datetimeFinished) - new Date(logEntry.datetimeStarted)) / 1000
+    renderExecutionResult('action-success', 'Success!')
+  }
+}
+
+function renderExecutionResult(resultCssClass, temporaryStatusMessage) {
+  updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
+  onExecStatusChanged()
+}
+
+function updateDom(resultCssClass, newTitle) {
+  if (resultCssClass == null) {
+    buttonClasses.value = []
+  } else {
+    buttonClasses.value = [resultCssClass]
+  }
+
+  displayTitle.value = newTitle
+}
+
+function onExecStatusChanged() {
+  isDisabled.value = false
+
+  setTimeout(() => {
+    updateDom(null, title.value)
+  }, 2000)
+}
+
+onMounted(() => {
+  constructFromJson(props.actionData)
+})
+
+watch(
+  () => props.actionData,
+  (newData) => {
+    updateFromJson(newData)
+  },
+  { deep: true }
+)
+
+defineExpose({
+  onExecutionFinished
+})
+</script>
+
+<style scoped>
+.action-button {
+  display: flex;
+  flex-direction: column;
+  flex-grow: 1;
+}
+
+.action-button button {
+  display: flex;
+  flex-direction: column;
+  flex-grow: 1;
+  justify-content: center;
+  gap: 0.5em;
+  padding: 0.5em 1em;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  background: #fff;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  box-shadow: 0 0 .6em #aaa;
+  font-size: .85em;
+  border-radius: .7em;
+}
+
+.action-button button:hover:not(:disabled) {
+  background: #f5f5f5;
+  border-color: #999;
+}
+
+.action-button button:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+}
+
+.action-button button .icon {
+  font-size: 3em;
+}
+
+.action-button button .title {
+  font-weight: 500;
+}
+
+/* Animation classes */
+.action-button button.action-timeout {
+  background: #fff3cd;
+  border-color: #ffeaa7;
+  color: #856404;
+}
+
+.action-button button.action-blocked {
+  background: #f8d7da;
+  border-color: #f5c6cb;
+  color: #721c24;
+}
+
+.action-button button.action-nonzero-exit {
+  background: #f8d7da;
+  border-color: #f5c6cb;
+  color: #721c24;
+}
+
+.action-button button.action-success {
+  background: #d4edda;
+  border-color: #c3e6cb;
+  color: #155724;
+}
+
+.action-button-footer {
+  margin-top: 0.5em;
+}
+</style>

+ 83 - 0
frontend/resources/vue/App.vue

@@ -0,0 +1,83 @@
+<template>
+    <header>
+        <img src="../../OliveTinLogo.png" alt="OliveTin logo" class="logo" />
+
+        <h1 id="page-title">
+            <router-link to="/">OliveTin</router-link>
+        </h1>
+
+        <button id="sidebar-toggler-button" aria-label="Open sidebar navigation" aria-pressed="false"
+            aria-haspopup="menu" @click="toggleSidebar">&#9776;</button>
+
+
+        <div class="fg1" />
+
+        <div class="userinfo">
+            <span id="link-login" hidden><router-link to="/login">Login</router-link> |</span>
+            <span id="link-logout" hidden><a href="/api/Logout">Logout</a> |</span>
+            <span id="username">&nbsp;</span>
+            <svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 24 24">
+                <g fill="none" fill-rule="evenodd">
+                    <path
+                        d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
+                    <path fill="currentColor"
+                        d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10s10-4.477 10-10S17.523 2 12 2M8.5 9.5a3.5 3.5 0 1 1 7 0a3.5 3.5 0 0 1-7 0m9.758 7.484A7.99 7.99 0 0 1 12 20a7.99 7.99 0 0 1-6.258-3.016C7.363 15.821 9.575 15 12 15s4.637.821 6.258 1.984" />
+                </g>
+            </svg>
+        </div>
+    </header>
+
+    <div id="layout">
+        <Sidebar ref="sidebar" />
+
+        <div id="content">
+            <main title="Main content">
+                <router-view />
+            </main>
+
+            <ExecutionDialog ref="executionDialog" />
+
+            <footer title="footer">
+                <p><img title="application icon" src="../../OliveTinLogo.png" alt="OliveTin logo" height="1em"
+                        class="logo" />
+                    OliveTin</p>
+                <p>
+                    <span>
+                        <a href="https://docs.olivetin.app" target="_new">Documentation</a>
+                    </span>
+
+                    <span>
+                        <a href="https://github.com/OliveTin/OliveTin/issues/new/choose" target="_new">Raise an issue on
+                            GitHub</a>
+                    </span>
+
+                    <span id="currentVersion">?</span>
+
+                    <span id="serverConnectionRest">REST</span>
+                    <span id="serverConnectionWebSocket">WebSocket</span>
+                </p>
+                <p>
+                    <a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
+                </p>
+            </footer>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import Sidebar from './components/Sidebar.vue';
+import ExecutionDialog from './ExecutionDialog.vue';
+
+import { provide } from 'vue';
+const sidebar = ref(null);
+const executionDialog = ref(null);
+
+provide('executionDialog', executionDialog.value);
+
+function toggleSidebar() {
+    if (sidebar.value && typeof sidebar.value.isOpen !== 'undefined') {
+        sidebar.value.isOpen = !sidebar.value.isOpen;
+    }
+}
+</script>

+ 432 - 0
frontend/resources/vue/ArgumentForm.vue

@@ -0,0 +1,432 @@
+<template>
+  <dialog 
+    ref="dialog" 
+    title="Arguments" 
+    class="action-arguments"
+    @close="handleClose"
+  >
+    <form class="padded-content" @submit.prevent="handleSubmit">
+      <div class="wrapper">
+        <div class="action-header">
+          <span class="icon" v-html="icon"></span>
+          <h2>{{ title }}</h2>
+        </div>
+
+        <div class="arguments">
+          <div 
+            v-for="arg in arguments" 
+            :key="arg.name"
+            class="argument-group"
+          >
+            <label :for="arg.name">
+              {{ formatLabel(arg.title) }}
+            </label>
+            
+            <datalist 
+              v-if="arg.suggestions && Object.keys(arg.suggestions).length > 0"
+              :id="`${arg.name}-choices`"
+            >
+              <option 
+                v-for="(suggestion, key) in arg.suggestions" 
+                :key="key"
+                :value="key"
+              >
+                {{ suggestion }}
+              </option>
+            </datalist>
+            
+            <component 
+              :is="getInputComponent(arg)"
+              :id="arg.name"
+              :name="arg.name"
+              :value="getArgumentValue(arg)"
+              :list="arg.suggestions ? `${arg.name}-choices` : undefined"
+              :type="getInputType(arg)"
+              :rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
+              :step="arg.type === 'datetime' ? 1 : undefined"
+              :pattern="getPattern(arg)"
+              :required="arg.required"
+              @input="handleInput(arg, $event)"
+              @change="handleChange(arg, $event)"
+            />
+            
+            <span 
+              v-if="arg.description"
+              class="argument-description"
+              v-html="arg.description"
+            ></span>
+          </div>
+        </div>
+
+        <div class="buttons">
+          <button 
+            name="start" 
+            type="submit"
+            :disabled="!isFormValid || (hasConfirmation && !confirmationChecked)"
+          >
+            Start
+          </button>
+          <button 
+            name="cancel" 
+            type="button"
+            @click="handleCancel"
+          >
+            Cancel
+          </button>
+        </div>
+      </div>
+    </form>
+  </dialog>
+</template>
+
+<script>
+export default {
+  name: 'ArgumentForm',
+  props: {
+    actionData: {
+      type: Object,
+      required: true
+    }
+  },
+  data() {
+    return {
+      title: '',
+      icon: '',
+      arguments: [],
+      argValues: {},
+      confirmationChecked: false,
+      hasConfirmation: false,
+      formErrors: {}
+    }
+  },
+  computed: {
+    isFormValid() {
+      return Object.keys(this.formErrors).length === 0
+    }
+  },
+  mounted() {
+    this.setup()
+  },
+  methods: {
+    setup() {
+      this.title = this.actionData.title
+      this.icon = this.actionData.icon
+      this.arguments = this.actionData.arguments || []
+      this.argValues = {}
+      this.formErrors = {}
+      this.confirmationChecked = false
+      this.hasConfirmation = false
+      
+      // Initialize values from query params or defaults
+      this.arguments.forEach(arg => {
+        const paramValue = this.getQueryParamValue(arg.name)
+        this.argValues[arg.name] = paramValue !== null ? paramValue : arg.defaultValue || ''
+        
+        if (arg.type === 'confirmation') {
+          this.hasConfirmation = true
+        }
+      })
+    },
+    
+    getQueryParamValue(paramName) {
+      const params = new URLSearchParams(window.location.search.substring(1))
+      return params.get(paramName)
+    },
+    
+    formatLabel(title) {
+      const lastChar = title.charAt(title.length - 1)
+      if (lastChar === '?' || lastChar === '.' || lastChar === ':') {
+        return title
+      }
+      return title + ':'
+    },
+    
+    getInputComponent(arg) {
+      if (arg.type === 'html') {
+        return 'div'
+      } else if (arg.type === 'raw_string_multiline') {
+        return 'textarea'
+      } else if (arg.choices && arg.choices.length > 0 && (arg.type === 'select' || arg.type === '')) {
+        return 'select'
+      } else {
+        return 'input'
+      }
+    },
+    
+    getInputType(arg) {
+      if (arg.type === 'html' || arg.type === 'raw_string_multiline' || arg.type === 'select') {
+        return undefined
+      }
+      return arg.type
+    },
+    
+    getPattern(arg) {
+      if (arg.type && arg.type.startsWith('regex:')) {
+        return arg.type.replace('regex:', '')
+      }
+      return undefined
+    },
+    
+    getArgumentValue(arg) {
+      if (arg.type === 'checkbox') {
+        return this.argValues[arg.name] === '1' || this.argValues[arg.name] === true
+      }
+      return this.argValues[arg.name] || ''
+    },
+    
+    handleInput(arg, event) {
+      const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
+      this.argValues[arg.name] = value
+      this.updateUrlWithArg(arg.name, value)
+    },
+    
+    handleChange(arg, event) {
+      if (arg.type === 'confirmation') {
+        this.confirmationChecked = event.target.checked
+        return
+      }
+      
+      // Validate the input
+      this.validateArgument(arg, event.target.value)
+    },
+    
+    async validateArgument(arg, value) {
+      if (!arg.type || arg.type.startsWith('regex:')) {
+        return
+      }
+      
+      try {
+        const validateArgumentTypeArgs = {
+          value: value,
+          type: arg.type
+        }
+        
+        const validation = await window.validateArgumentType(validateArgumentTypeArgs)
+        
+        if (validation.valid) {
+          this.$delete(this.formErrors, arg.name)
+        } else {
+          this.$set(this.formErrors, arg.name, validation.description)
+        }
+      } catch (err) {
+        console.warn('Validation failed:', err)
+      }
+    },
+    
+    updateUrlWithArg(name, value) {
+      if (name && value !== undefined) {
+        const url = new URL(window.location.href)
+        
+        // Don't add passwords to URL
+        const arg = this.arguments.find(a => a.name === name)
+        if (arg && arg.type === 'password') {
+          return
+        }
+        
+        url.searchParams.set(name, value)
+        window.history.replaceState({}, '', url.toString())
+      }
+    },
+    
+    getArgumentValues() {
+      const ret = []
+      
+      for (const arg of this.arguments) {
+        let value = this.argValues[arg.name] || ''
+        
+        if (arg.type === 'checkbox') {
+          value = value ? '1' : '0'
+        }
+        
+        ret.push({
+          name: arg.name,
+          value: value
+        })
+      }
+      
+      return ret
+    },
+    
+    handleSubmit() {
+      // Validate all inputs
+      let isValid = true
+      
+      for (const arg of this.arguments) {
+        const value = this.argValues[arg.name]
+        if (arg.required && (!value || value === '')) {
+          this.$set(this.formErrors, arg.name, 'This field is required')
+          isValid = false
+        }
+      }
+      
+      if (!isValid) {
+        return
+      }
+      
+      const argvs = this.getArgumentValues()
+      this.$emit('submit', argvs)
+      this.close()
+    },
+    
+    handleCancel() {
+      this.clearBookmark()
+      this.$emit('cancel')
+      this.close()
+    },
+    
+    handleClose() {
+      this.$emit('close')
+    },
+    
+    clearBookmark() {
+      // Remove the action from the URL
+      window.history.replaceState({
+        path: window.location.pathname
+      }, '', window.location.pathname)
+    },
+    
+    show() {
+      this.$refs.dialog.showModal()
+    },
+    
+    close() {
+      this.$refs.dialog.close()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.action-arguments {
+  border: none;
+  border-radius: 8px;
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+  max-width: 500px;
+  width: 90vw;
+}
+
+.wrapper {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+.action-header {
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
+  padding-bottom: 1rem;
+  border-bottom: 1px solid #eee;
+}
+
+.action-header .icon {
+  font-size: 1.5em;
+}
+
+.action-header h2 {
+  margin: 0;
+  font-size: 1.2em;
+}
+
+.arguments {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+.argument-group {
+  display: flex;
+  flex-direction: column;
+  gap: 0.25rem;
+}
+
+.argument-group label {
+  font-weight: 500;
+  color: #333;
+}
+
+.argument-group input,
+.argument-group select,
+.argument-group textarea {
+  padding: 0.5rem;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  font-size: 1rem;
+  transition: border-color 0.2s ease;
+}
+
+.argument-group input:focus,
+.argument-group select:focus,
+.argument-group textarea:focus {
+  outline: none;
+  border-color: #007bff;
+  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
+}
+
+.argument-group input:invalid,
+.argument-group select:invalid,
+.argument-group textarea:invalid {
+  border-color: #dc3545;
+}
+
+.argument-group textarea {
+  resize: vertical;
+  min-height: 100px;
+}
+
+.argument-description {
+  font-size: 0.875rem;
+  color: #666;
+  margin-top: 0.25rem;
+}
+
+.buttons {
+  display: flex;
+  gap: 0.5rem;
+  justify-content: flex-end;
+  padding-top: 1rem;
+  border-top: 1px solid #eee;
+}
+
+.buttons button {
+  padding: 0.5rem 1rem;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  background: #fff;
+  cursor: pointer;
+  font-size: 1rem;
+  transition: all 0.2s ease;
+}
+
+.buttons button:hover:not(:disabled) {
+  background: #f8f9fa;
+  border-color: #adb5bd;
+}
+
+.buttons button:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+}
+
+.buttons button[name="start"] {
+  background: #007bff;
+  color: white;
+  border-color: #007bff;
+}
+
+.buttons button[name="start"]:hover:not(:disabled) {
+  background: #0056b3;
+  border-color: #0056b3;
+}
+
+/* Checkbox specific styling */
+.argument-group input[type="checkbox"] {
+  width: auto;
+  margin-right: 0.5rem;
+}
+
+.argument-group input[type="checkbox"] + label {
+  display: inline;
+  font-weight: normal;
+}
+</style> 

+ 32 - 0
frontend/resources/vue/Dashboard.vue

@@ -0,0 +1,32 @@
+<template>
+    <section class = "transparent">
+        <fieldset>
+            <legend>{{ dashboard.title }}</legend>
+
+            <ActionButton :actionData = "action" v-for = "action in dashboard.contents" :key = "action.id" />
+        </fieldset>
+    </section>
+</template>
+
+<script setup>
+import ActionButton from './ActionButton.vue'
+
+defineProps({
+    dashboard: {
+        type: Object,
+        required: true
+    }
+})
+
+
+</script>
+
+<style>
+fieldset {
+	display: grid;
+	grid-template-columns: repeat(auto-fit, 180px);
+	grid-auto-rows: 1fr;
+	justify-content: center;
+	place-items: stretch;
+}   
+</style>

+ 141 - 0
frontend/resources/vue/ExecutionButton.vue

@@ -0,0 +1,141 @@
+<template>
+  <div 
+    :id="`execution-${executionTrackingId}`"
+    class="execution-button"
+  >
+    <button
+      :title="`${ellapsed}s`"
+      @click="show"
+    >
+      {{ buttonText }}
+    </button>
+  </div>
+</template>
+
+<script>
+//import { ExecutionFeedbackButton } from '../js/ExecutionFeedbackButton.js'
+
+export default {
+  name: 'ExecutionButton',
+//  mixins: [ExecutionFeedbackButton],
+  props: {
+    executionTrackingId: {
+      type: String,
+      required: true
+    }
+  },
+  data() {
+    return {
+      ellapsed: 0,
+      isWaiting: true
+    }
+  },
+  computed: {
+    buttonText() {
+      if (this.isWaiting) {
+        return 'Executing...'
+      } else {
+        return `${this.ellapsed}s`
+      }
+    }
+  },
+  mounted() {
+    this.constructFromJson(this.executionTrackingId)
+  },
+  methods: {
+    constructFromJson(json) {
+      this.executionTrackingId = json
+      this.ellapsed = 0
+      this.isWaiting = true
+    },
+    
+    show() {
+      this.$emit('show')
+      
+      if (window.executionDialog) {
+        window.executionDialog.reset()
+        window.executionDialog.show()
+        window.executionDialog.fetchExecutionResult(this.executionTrackingId)
+      }
+    },
+    
+    onExecStatusChanged() {
+      this.isWaiting = false
+      this.domTitle = this.ellapsed + 's'
+    },
+    
+    // Override from ExecutionFeedbackButton
+    onExecutionFinished(logEntry) {
+      if (logEntry.timedOut) {
+        this.renderExecutionResult('action-timeout', 'Timed out')
+      } else if (logEntry.blocked) {
+        this.renderExecutionResult('action-blocked', 'Blocked!')
+      } else if (logEntry.exitCode !== 0) {
+        this.renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
+      } else {
+        this.ellapsed = Math.ceil(new Date(logEntry.datetimeFinished) - new Date(logEntry.datetimeStarted)) / 1000
+        this.renderExecutionResult('action-success', 'Success!')
+      }
+    },
+    
+    renderExecutionResult(resultCssClass, temporaryStatusMessage) {
+      this.updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
+      this.onExecStatusChanged()
+    },
+    
+    updateDom(resultCssClass, title) {
+      // For execution button, we don't need to update classes as much
+      // since it's a simpler component
+      if (resultCssClass) {
+        this.$el.classList.add(resultCssClass)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.execution-button {
+  display: inline-block;
+}
+
+.execution-button button {
+  padding: 0.25em 0.5em;
+  border: 1px solid #ccc;
+  border-radius: 3px;
+  background: #fff;
+  cursor: pointer;
+  font-size: 0.9em;
+  transition: all 0.2s ease;
+}
+
+.execution-button button:hover {
+  background: #f5f5f5;
+  border-color: #999;
+}
+
+/* Animation classes */
+.execution-button button.action-timeout {
+  background: #fff3cd;
+  border-color: #ffeaa7;
+  color: #856404;
+}
+
+.execution-button button.action-blocked {
+  background: #f8d7da;
+  border-color: #f5c6cb;
+  color: #721c24;
+}
+
+.execution-button button.action-nonzero-exit {
+  background: #f8d7da;
+  border-color: #f5c6cb;
+  color: #721c24;
+}
+
+.execution-button button.action-success {
+  background: #d4edda;
+  border-color: #c3e6cb;
+  color: #155724;
+}
+</style> 

+ 421 - 0
frontend/resources/vue/ExecutionDialog.vue

@@ -0,0 +1,421 @@
+<template>
+  <dialog 
+    ref="dialog" 
+    title="Execution Results" 
+    :class="{ big: isBig }"
+    @close="handleClose"
+  >
+    <div class="action-header padded-content">
+      <span class="icon" role="img" v-html="icon"></span>
+
+      <h2>
+        <span :title="titleTooltip">{{ title }}</span>
+      </h2>
+
+      <button 
+        v-show="!hideToggleButton"
+        @click="toggleSize" 
+        title="Toggle dialog size"
+      >
+        <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
+          <path fill="currentColor" d="M3 3h6v2H6.462l4.843 4.843l-1.415 1.414L5 6.367V9H3zm0 18h6v-2H6.376l4.929-4.928l-1.415-1.414L5 17.548V15H3zm12 0h6v-6h-2v2.524l-4.867-4.866l-1.414 1.414L17.647 19H15zm6-18h-6v2h2.562l-4.843 4.843l1.414 1.414L19 6.39V9h2z"/>
+        </svg>
+      </button>
+    </div>
+    
+    <div v-show="!hideBasics" class="padded-content-sides">
+      <strong>Duration: </strong><span v-html="duration"></span>
+    </div>
+    
+    <div v-show="!hideDetails && logEntry" class="padded-content-sides">
+      <p>
+        <strong>Status: </strong>
+        <ActionStatusDisplay :log-entry="logEntry" v-if="logEntry"/>
+      </p>
+    </div>
+
+    <div ref="xtermOutput" v-show="!isHtmlOutput"></div>
+    <div 
+      v-show="isHtmlOutput" 
+      class="padded-content"
+      v-html="htmlOutput"
+    ></div>
+
+    <div class="buttons padded-content">
+      <button 
+        :disabled="!canRerun" 
+        @click="rerunAction" 
+        title="Rerun"
+      >
+        Rerun
+      </button>
+      <button 
+        :disabled="!canKill" 
+        @click="killAction" 
+        title="Kill"
+      >
+        Kill
+      </button>
+
+      <form method="dialog">
+        <button name="Cancel" title="Close">Close</button>
+      </form>
+    </div>
+  </dialog>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue'
+import ActionStatusDisplay from './components/ActionStatusDisplay.vue'
+import { OutputTerminal } from '../../js/OutputTerminal.js'
+
+// Refs for DOM elements
+const xtermOutput = ref(null)
+const dialog = ref(null)
+
+// State
+const state = reactive({
+  isBig: false,
+  hideToggleButton: false,
+  hideBasics: false,
+  hideDetails: false,
+  hideDetailsOnResult: false,
+
+  // Execution data
+  executionSeconds: 0,
+  executionTrackingId: 'notset',
+  executionTicker: null,
+
+  // Display data
+  icon: '',
+  title: 'Waiting for result...',
+  titleTooltip: '',
+  duration: '',
+  htmlOutput: '',
+  isHtmlOutput: false,
+
+  // Action data
+  logEntry: null,
+  canRerun: false,
+  canKill: false,
+
+  // Terminal
+  terminal: null
+})
+
+// Expose for template
+const isBig = ref(false)
+const hideToggleButton = ref(false)
+const hideBasics = ref(false)
+const hideDetails = ref(false)
+const hideDetailsOnResult = ref(false)
+const executionSeconds = ref(0)
+const executionTrackingId = ref('notset')
+const icon = ref('')
+const title = ref('Waiting for result...')
+const titleTooltip = ref('')
+const duration = ref('')
+const htmlOutput = ref('')
+const isHtmlOutput = ref(false)
+const logEntry = ref(null)
+const canRerun = ref(false)
+const canKill = ref(false)
+
+let executionTicker = null
+let terminal = null
+
+function syncStateToRefs() {
+  isBig.value = state.isBig
+  hideToggleButton.value = state.hideToggleButton
+  hideBasics.value = state.hideBasics
+  hideDetails.value = state.hideDetails
+  hideDetailsOnResult.value = state.hideDetailsOnResult
+  executionSeconds.value = state.executionSeconds
+  executionTrackingId.value = state.executionTrackingId
+  icon.value = state.icon
+  title.value = state.title
+  titleTooltip.value = state.titleTooltip
+  duration.value = state.duration
+  htmlOutput.value = state.htmlOutput
+  isHtmlOutput.value = state.isHtmlOutput
+  logEntry.value = state.logEntry
+  canRerun.value = state.canRerun
+  canKill.value = state.canKill
+}
+
+function syncRefsToState() {
+  state.isBig = isBig.value
+  state.hideToggleButton = hideToggleButton.value
+  state.hideBasics = hideBasics.value
+  state.hideDetails = hideDetails.value
+  state.hideDetailsOnResult = hideDetailsOnResult.value
+  state.executionSeconds = executionSeconds.value
+  state.executionTrackingId = executionTrackingId.value
+  state.icon = icon.value
+  state.title = title.value
+  state.titleTooltip = titleTooltip.value
+  state.duration = duration.value
+  state.htmlOutput = htmlOutput.value
+  state.isHtmlOutput = isHtmlOutput.value
+  state.logEntry = logEntry.value
+  state.canRerun = canRerun.value
+  state.canKill = canKill.value
+}
+
+function initializeTerminal() {
+  terminal = new OutputTerminal()
+  terminal.open(xtermOutput.value)
+  terminal.resize(80, 24)
+  window.terminal = terminal
+  state.terminal = terminal
+}
+
+function toggleSize() {
+  state.isBig = !state.isBig
+  isBig.value = state.isBig
+  if (state.isBig) {
+    terminal.fit()
+  } else {
+    terminal.resize(80, 24)
+  }
+}
+
+async function reset() {
+  state.executionSeconds = 0
+  state.executionTrackingId = 'notset'
+  state.isBig = false
+  state.hideToggleButton = false
+  state.hideBasics = false
+  state.hideDetails = false
+  state.hideDetailsOnResult = false
+
+  state.icon = ''
+  state.title = 'Waiting for result...'
+  state.titleTooltip = ''
+  state.duration = ''
+  state.htmlOutput = ''
+  state.isHtmlOutput = false
+
+  state.canRerun = false
+  state.canKill = false
+  state.logEntry = null
+
+  syncStateToRefs()
+
+  if (terminal) {
+    await terminal.reset()
+    terminal.fit()
+  }
+}
+
+function show(actionButton) {
+  if (actionButton) {
+    state.icon = actionButton.domIcon.innerText
+    icon.value = state.icon
+  }
+
+  state.canKill = true
+  canKill.value = true
+
+  // Clear existing ticker
+  if (executionTicker) {
+    clearInterval(executionTicker)
+  }
+
+  state.executionSeconds = 0
+  executionSeconds.value = 0
+  executionTick()
+  executionTicker = setInterval(() => {
+    executionTick()
+  }, 1000)
+  state.executionTicker = executionTicker
+
+  // Close if already open
+  if (dialog.value && dialog.value.open) {
+    dialog.value.close()
+  }
+
+  dialog.value && dialog.value.showModal()
+}
+
+function rerunAction() {
+  if (state.logEntry && state.logEntry.actionId) {
+    const actionButton = document.getElementById('actionButton-' + state.logEntry.actionId)
+    if (actionButton && actionButton.btn) {
+      actionButton.btn.click()
+    }
+  }
+  dialog.value && dialog.value.close()
+}
+
+async function killAction() {
+  if (!state.executionTrackingId || state.executionTrackingId === 'notset') {
+    return
+  }
+
+  const killActionArgs = {
+    executionTrackingId: state.executionTrackingId
+  }
+
+  try {
+    await window.client.killAction(killActionArgs)
+  } catch (err) {
+    console.error('Failed to kill action:', err)
+  }
+}
+
+function executionTick() {
+  state.executionSeconds++
+  executionSeconds.value = state.executionSeconds
+  updateDuration(null)
+}
+
+function hideEverythingApartFromOutput() {
+  state.hideDetailsOnResult = true
+  state.hideBasics = true
+  hideDetailsOnResult.value = true
+  hideBasics.value = true
+}
+
+async function fetchExecutionResult(executionTrackingIdParam) {
+  state.executionTrackingId = executionTrackingIdParam
+  executionTrackingId.value = executionTrackingIdParam
+
+  const executionStatusArgs = {
+    executionTrackingId: state.executionTrackingId
+  }
+
+  try {
+    const logEntryResult = await window.client.executionStatus(executionStatusArgs)
+    await renderExecutionResult(logEntryResult)
+  } catch (err) {
+    renderError(err)
+    throw err
+  }
+}
+
+function updateDuration(logEntryParam) {
+  let logEntry = logEntryParam
+  if (logEntry == null) {
+    duration.value = state.executionSeconds + ' seconds'
+    state.duration = duration.value
+  } else if (!logEntry.executionStarted) {
+    duration.value = logEntry.datetimeStarted + ' (request time). Not executed.'
+    state.duration = duration.value
+  } else if (logEntry.executionStarted && !logEntry.executionFinished) {
+    duration.value = logEntry.datetimeStarted
+    state.duration = duration.value
+  } else {
+    let delta = ''
+    try {
+      delta = (new Date(logEntry.datetimeStarted) - new Date(logEntry.datetimeStarted)) / 1000
+      delta = new Intl.RelativeTimeFormat().format(delta, 'seconds').replace('in ', '').replace('ago', '')
+    } catch (e) {
+      console.warn('Failed to calculate delta', e)
+    }
+    duration.value = logEntry.datetimeStarted + ' &rarr; ' + logEntry.datetimeFinished
+    if (delta !== '') {
+      duration.value += ' (' + delta + ')'
+    }
+    state.duration = duration.value
+  }
+}
+
+async function renderExecutionResult(res) {
+  // Clear ticker
+  if (executionTicker) {
+    clearInterval(executionTicker)
+  }
+  state.executionTicker = null
+
+  if (res.type === 'execution-dialog-output-html') {
+    state.isHtmlOutput = true
+    state.htmlOutput = res.logEntry.output
+    state.hideDetailsOnResult = true
+    isHtmlOutput.value = true
+    htmlOutput.value = res.logEntry.output
+    hideDetailsOnResult.value = true
+  } else {
+    state.isHtmlOutput = false
+    state.htmlOutput = ''
+    isHtmlOutput.value = false
+    htmlOutput.value = ''
+  }
+
+  if (state.hideDetailsOnResult) {
+    state.hideDetails = true
+    hideDetails.value = true
+  }
+
+  state.executionTrackingId = res.logEntry.executionTrackingId
+  executionTrackingId.value = res.logEntry.executionTrackingId
+  state.canRerun = res.logEntry.executionFinished
+  canRerun.value = res.logEntry.executionFinished
+  state.canKill = res.logEntry.canKill
+  canKill.value = res.logEntry.canKill
+  state.logEntry = res.logEntry
+  logEntry.value = res.logEntry
+
+  state.icon = res.logEntry.actionIcon
+  icon.value = res.logEntry.actionIcon
+  state.title = res.logEntry.actionTitle
+  title.value = res.logEntry.actionTitle
+  state.titleTooltip = 'Action ID: ' + res.logEntry.actionId + '\nExecution ID: ' + res.logEntry.executionTrackingId
+  titleTooltip.value = state.titleTooltip
+
+  updateDuration(res.logEntry)
+
+  if (terminal) {
+    await terminal.reset()
+    await terminal.write(res.logEntry.output, () => {
+      terminal.fit()
+    })
+  }
+}
+
+function renderError(err) {
+  window.showBigError('execution-dlg-err', 'in the execution dialog', 'Failed to fetch execution result. ' + err, false)
+}
+
+function handleClose() {
+  // Clean up when dialog is closed
+  if (executionTicker) {
+    clearInterval(executionTicker)
+  }
+  state.executionTicker = null
+}
+
+function cleanup() {
+  if (executionTicker) {
+    clearInterval(executionTicker)
+  }
+  state.executionTicker = null
+  if (terminal) {
+    terminal.close()
+  }
+  state.terminal = null
+}
+
+onMounted(() => {
+  nextTick(() => {
+    initializeTerminal()
+  })
+})
+
+onBeforeUnmount(() => {
+  cleanup()
+})
+
+// Expose methods for parent/imperative use
+defineExpose({
+  reset,
+  show,
+  rerunAction,
+  killAction,
+  fetchExecutionResult,
+  renderExecutionResult,
+  hideEverythingApartFromOutput,
+  handleClose
+})
+
+</script>

+ 15 - 33
webui.dev/js/LoginForm.js → frontend/resources/vue/LoginForm.vue

@@ -1,4 +1,4 @@
-export class LoginForm extends window.HTMLElement {
+<script setup>
   setup () {
     const tpl = document.getElementById('tplLoginForm')
     this.content = tpl.content.cloneNode(true)
@@ -11,40 +11,24 @@ export class LoginForm extends window.HTMLElement {
     })
   }
 
-  localLoginRequest () {
+  async localLoginRequest () {
     const username = this.querySelector('input.username').value
     const password = this.querySelector('input.password').value
 
     document.querySelector('.error').innerHTML = ''
 
-    window.fetch('/api/LocalUserLogin', {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json'
-      },
-      body: JSON.stringify({
-        username: username,
-        password: password
-      })
-    }).then((response) => {
-      if (response.ok) {
-        const res = response.json()
-
-        if (res !== undefined) {
-          return res
-        } else {
-          throw new Error('Failed to login - no res')
-        }
-      } else {
-        throw new Error('Failed to login')
-      }
-    }).then((res) => {
-      if (res.success) {
-        window.location.href = '/'
-      } else {
-        document.querySelector('.error').innerHTML = 'Login failed.'
-      }
-    })
+    const args = {
+      username: username,
+      password: password
+    }
+
+    const loginResult = await window.client.localUserLogin(args)
+
+    if (loginResult.success) {
+      window.location.href = '/'
+    } else {
+      document.querySelector('.error').innerHTML = 'Login failed.'
+    }
   }
 
   processOAuth2Providers (providers) {
@@ -85,6 +69,4 @@ export class LoginForm extends window.HTMLElement {
       this.querySelector('.login-disabled').hidden = true
     }
   }
-}
-
-window.customElements.define('login-form', LoginForm)
+</script>

+ 111 - 0
frontend/resources/vue/LogsList.vue

@@ -0,0 +1,111 @@
+<template>
+    <section title="Logs" class="">
+        <div class="toolbar">
+            <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"
+                        d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
+                </svg>
+                <input placeholder="Search for action name" id="logSearchBox" />
+                <button id="searchLogsClear" title="Clear search filter" disabled>
+                    <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>
+            </label>
+        </div>
+        <table id="logsTable" title="Logs" hidden>
+            <thead>
+                <tr title="untitled">
+                    <th>Timestamp</th>
+                    <th>Action</th>
+                    <th>Metadata</th>
+                    <th>Status</th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr v-for="logEntry in logEntries" :key="logEntry.executionTrackingId"></tr>
+                    <td>{{ logEntry.datetimeStarted }}</td>
+                    <td>{{ logEntry.actionTitle }}</td>
+                    <td>{{ logEntry.actionIcon }}</td>
+                    <td>{{ logEntry.tags }}</td>
+                    <td>{{ logEntry.user }}</td>
+                </tr>
+            </tbody>
+        </table>
+
+        <p id="logsTableEmpty">There are no logs to display. <a href="/">Return to index</a></p>
+
+        <p><strong>Note:</strong> The server is configured to only send <strong id="logs-server-page-size">?</strong>
+            log entries at a time. The search box at the top of this page only searches this current page of logs.</p>
+    </section>
+</template>
+
+<script setup>
+import { onMounted } from 'vue'
+
+function setupLogSearchBox () {
+  document.getElementById('logSearchBox').oninput = searchLogs
+  document.getElementById('searchLogsClear').onclick = searchLogsClear
+}
+
+function marshalLogsJsonToHtml (json) {
+  // This function is called internally with a "fake" server response, that does
+  // not have pageSize set. So we need to check if it's set before trying to use it.
+  if (json.pageSize !== undefined) {
+    document.getElementById('logs-server-page-size').innerText = json.pageSize
+  }
+
+  if (json.logs != null && json.logs.length > 0) {
+    document.getElementById('logsTable').hidden = false
+    document.getElementById('logsTableEmpty').hidden = true
+  } else {
+    return
+  }
+
+  for (const logEntry of json.logs) {
+    let row = document.getElementById('log-' + logEntry.executionTrackingId)
+
+    if (row == null) {
+      const tpl = document.getElementById('tplLogRow')
+      row = tpl.content.querySelector('tr').cloneNode(true)
+      row.id = 'log-' + logEntry.executionTrackingId
+
+      row.querySelector('.content').onclick = () => {
+        window.executionDialog.reset()
+        window.executionDialog.show()
+        window.executionDialog.renderExecutionResult({
+          logEntry: window.logEntries.get(logEntry.executionTrackingId)
+        })
+        pushNewNavigationPath('/logs/' + logEntry.executionTrackingId)
+      }
+
+      logEntry.dom = row
+
+      window.logEntries.set(logEntry.executionTrackingId, logEntry)
+
+      document.querySelector('#logTableBody').prepend(row)
+    }
+
+    row.querySelector('.timestamp').innerText = logEntry.datetimeStarted
+    row.querySelector('.content').innerText = logEntry.actionTitle
+    row.querySelector('.icon').innerHTML = logEntry.actionIcon
+    row.setAttribute('title', logEntry.actionTitle)
+
+    row.exitCodeDisplay.update(logEntry)
+
+    row.querySelector('.tags').innerHTML = ''
+
+    for (const tag of logEntry.tags) {
+      row.querySelector('.tags').append(createTag(tag))
+    }
+
+    row.querySelector('.tags').append(createAnnotation('user', logEntry.user))
+  }
+}
+
+onMounted(() => {
+    setupLogSearchBox()
+})
+</script>

+ 63 - 0
frontend/resources/vue/components/ActionStatusDisplay.vue

@@ -0,0 +1,63 @@
+<template>
+    <span>
+        <span :class="['action-status', statusClass]">{{ statusText }}</span><span>{{ exitCodeText }}</span>
+    </span>
+
+</template>
+
+<script setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+    logEntry: {
+        type: Object,
+        required: true
+    }
+})
+
+const statusText = computed(() => {
+    const logEntry = props.logEntry
+    if (!logEntry) return 'unknown'
+
+    if (logEntry.executionFinished) {
+        if (logEntry.blocked) {
+            return 'Blocked'
+        } else if (logEntry.timedOut) {
+            return 'Timed out'
+        } else {
+            return 'Completed'
+        }
+    } else {
+        return 'Still running...'
+    }
+})
+
+const exitCodeText = computed(() => {
+    const logEntry = props.logEntry
+    if (!logEntry) return ''
+    if (logEntry.executionFinished) {
+        if (logEntry.blocked || logEntry.timedOut) {
+            return ''
+        }
+        return ' Exit code: ' + logEntry.exitCode
+    }
+    return ''
+})
+
+const statusClass = computed(() => {
+    const logEntry = props.logEntry
+    if (!logEntry) return ''
+    if (logEntry.executionFinished) {
+        if (logEntry.blocked) {
+            return 'action-blocked'
+        } else if (logEntry.timedOut) {
+            return 'action-timeout'
+        } else if (logEntry.exitCode === 0) {
+            return 'action-success'
+        } else {
+            return 'action-nonzero-exit'
+        }
+    }
+    return ''
+})
+</script>

+ 254 - 0
frontend/resources/vue/components/Sidebar.vue

@@ -0,0 +1,254 @@
+<template>
+  <aside :class="{ 'shown': isOpen, 'stuck': isStuck }" class="sidebar">
+    <button
+      class="stick-toggle"
+      :aria-pressed="isStuck"
+      :title="isStuck ? 'Unstick sidebar' : 'Stick sidebar'"
+      @click="toggleStick"
+    >
+      <span v-if="isStuck">📌 Unstick</span>
+      <span v-else>📍 Stick</span>
+    </button>
+    <nav class="mainnav">
+      <ul class="navigation-links">
+        <li v-for="link in navigationLinks" :key="link.id" :title="link.title">
+          <router-link :to="link.path" :class="{ active: isActive(link.path) }">
+            <span v-if="link.icon" class="icon" v-html="link.icon"></span>
+            <span class="title">{{ link.title }}</span>
+          </router-link>
+        </li>
+      </ul>
+
+      <ul class="supplemental-links">
+        <li v-for="link in supplementalLinks" :key="link.id" :title="link.title">
+          <a :href="link.url" :target="link.target || '_self'">
+            <span v-if="link.icon" class="icon" v-html="link.icon"></span>
+            <span class="title">{{ link.title }}</span>
+          </a>
+        </li>
+      </ul>
+    </nav>
+  </aside>
+</template>
+
+<script setup>
+import { ref, onMounted, getCurrentInstance } from 'vue'
+import { useRoute } from 'vue-router'
+
+const isOpen = ref(false)
+const isStuck = ref(false)
+const navigationLinks = ref([
+  {
+    id: 'actions',
+    title: 'Actions',
+    path: '/',
+    icon: '⚡'
+  },
+  {
+    id: 'logs',
+    title: 'Logs',
+    path: '/logs',
+    icon: '📋'
+  },
+  {
+    id: 'diagnostics',
+    title: 'Diagnostics',
+    path: '/diagnostics',
+    icon: '🔧'
+  }
+])
+const supplementalLinks = ref([])
+
+const route = useRoute()
+const instance = getCurrentInstance()
+
+function toggleStick() {
+  isStuck.value = !isStuck.value
+}
+
+function toggle() {
+  isOpen.value = !isOpen.value
+}
+
+function open() {
+  isOpen.value = true
+}
+
+function close() {
+  isOpen.value = false
+}
+
+function isActive(path) {
+  return route.path === path
+}
+
+// Method to add navigation links from other components
+function addNavigationLink(link) {
+  const existingIndex = navigationLinks.value.findIndex(l => l.id === link.id)
+  if (existingIndex >= 0) {
+    navigationLinks.value[existingIndex] = { ...link }
+  } else {
+    navigationLinks.value.push({ ...link })
+  }
+}
+
+// Method to add supplemental links from other components
+function addSupplementalLink(link) {
+  const existingIndex = supplementalLinks.value.findIndex(l => l.id === link.id)
+  if (existingIndex >= 0) {
+    supplementalLinks.value[existingIndex] = { ...link }
+  } else {
+    supplementalLinks.value.push({ ...link })
+  }
+}
+
+// Method to remove links
+function removeNavigationLink(linkId) {
+  navigationLinks.value = navigationLinks.value.filter(link => link.id !== linkId)
+}
+
+function removeSupplementalLink(linkId) {
+  supplementalLinks.value = supplementalLinks.value.filter(link => link.id !== linkId)
+}
+
+// Method to clear all links
+function clearNavigationLinks() {
+  navigationLinks.value = []
+}
+
+function clearSupplementalLinks() {
+  supplementalLinks.value = []
+}
+
+// Method to get all links
+function getNavigationLinks() {
+  return [...navigationLinks.value]
+}
+
+function getSupplementalLinks() {
+  return [...supplementalLinks.value]
+}
+
+onMounted(() => {
+  // Make the sidebar globally accessible
+  window.sidebar = {
+    get isOpen() { return isOpen.value },
+    set isOpen(val) { isOpen.value = val },
+    toggle,
+    open,
+    close,
+    addNavigationLink,
+    addSupplementalLink,
+    removeNavigationLink,
+    removeSupplementalLink,
+    clearNavigationLinks,
+    clearSupplementalLinks,
+    getNavigationLinks,
+    getSupplementalLinks
+  }
+})
+
+defineExpose({
+  isOpen,
+  navigationLinks,
+  supplementalLinks,
+  toggle,
+  open,
+  close,
+  isActive,
+  addNavigationLink,
+  addSupplementalLink,
+  removeNavigationLink,
+  removeSupplementalLink,
+  clearNavigationLinks,
+  clearSupplementalLinks,
+  getNavigationLinks,
+  getSupplementalLinks
+})
+</script>
+
+<style scoped>
+.mainnav {
+  padding: 1rem 0;
+}
+
+.navigation-links,
+.supplemental-links {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+
+.navigation-links li,
+.supplemental-links li {
+  margin: 0;
+  padding: 0;
+}
+
+.navigation-links a,
+.supplemental-links a {
+  display: flex;
+  align-items: center;
+  gap: 0.75rem;
+  padding: 0.75rem 1rem;
+  color: #333;
+  text-decoration: none;
+  transition: background-color 0.2s ease;
+  border-left: 3px solid transparent;
+}
+
+.navigation-links a:hover,
+.supplemental-links a:hover {
+  background: #f8f9fa;
+  color: #007bff;
+}
+
+.navigation-links a.active {
+  background: #e3f2fd;
+  color: #007bff;
+  border-left-color: #007bff;
+}
+
+.navigation-links a.router-link-active {
+  background: #e3f2fd;
+  color: #007bff;
+  border-left-color: #007bff;
+}
+
+.icon {
+  font-size: 1.2em;
+  width: 1.5rem;
+  text-align: center;
+}
+
+.title {
+  font-weight: 500;
+}
+
+.supplemental-links {
+  border-top: 1px solid #eee;
+  margin-top: 1rem;
+  padding-top: 1rem;
+}
+
+.supplemental-links a {
+  font-size: 0.9rem;
+  color: #666;
+}
+
+.supplemental-links a:hover {
+  color: #007bff;
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+  .sidebar {
+    width: 100%;
+    left: -100%;
+  }
+  
+  .sidebar.shown {
+    left: 0;
+  }
+}
+</style> 

+ 179 - 0
frontend/resources/vue/components/SidebarExample.vue

@@ -0,0 +1,179 @@
+<template>
+  <div class="sidebar-example">
+    <h3>Sidebar Management Example</h3>
+    
+    <div class="controls">
+      <button @click="addCustomNavLink" class="btn btn-primary">
+        Add Custom Nav Link
+      </button>
+      
+      <button @click="addCustomSupplementalLink" class="btn btn-secondary">
+        Add Custom Supplemental Link
+      </button>
+      
+      <button @click="removeCustomLinks" class="btn btn-danger">
+        Remove Custom Links
+      </button>
+      
+      <button @click="toggleSidebar" class="btn btn-info">
+        Toggle Sidebar
+      </button>
+    </div>
+    
+    <div class="info">
+      <p><strong>Available Sidebar Methods:</strong></p>
+      <ul>
+        <li><code>window.sidebar.addNavigationLink(link)</code> - Add navigation link</li>
+        <li><code>window.sidebar.addSupplementalLink(link)</code> - Add supplemental link</li>
+        <li><code>window.sidebar.removeNavigationLink(id)</code> - Remove navigation link</li>
+        <li><code>window.sidebar.removeSupplementalLink(id)</code> - Remove supplemental link</li>
+        <li><code>window.sidebar.toggle()</code> - Toggle sidebar visibility</li>
+        <li><code>window.sidebar.open()</code> - Open sidebar</li>
+        <li><code>window.sidebar.close()</code> - Close sidebar</li>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SidebarExample',
+  data() {
+    return {
+      customLinkId: 1
+    }
+  },
+  methods: {
+    addCustomNavLink() {
+      if (window.sidebar) {
+        window.sidebar.addNavigationLink({
+          id: `custom-nav-${this.customLinkId}`,
+          title: `Custom Nav ${this.customLinkId}`,
+          path: `/custom-${this.customLinkId}`,
+          icon: '🔗'
+        })
+        this.customLinkId++
+      } else {
+        console.warn('Sidebar not available')
+      }
+    },
+    
+    addCustomSupplementalLink() {
+      if (window.sidebar) {
+        window.sidebar.addSupplementalLink({
+          id: `custom-supplemental-${this.customLinkId}`,
+          title: `Custom Link ${this.customLinkId}`,
+          url: `https://example.com/custom-${this.customLinkId}`,
+          target: '_blank',
+          icon: '🔗'
+        })
+        this.customLinkId++
+      } else {
+        console.warn('Sidebar not available')
+      }
+    },
+    
+    removeCustomLinks() {
+      if (window.sidebar) {
+        // Remove all custom links
+        for (let i = 1; i < this.customLinkId; i++) {
+          window.sidebar.removeNavigationLink(`custom-nav-${i}`)
+          window.sidebar.removeSupplementalLink(`custom-supplemental-${i}`)
+        }
+        this.customLinkId = 1
+      } else {
+        console.warn('Sidebar not available')
+      }
+    },
+    
+    toggleSidebar() {
+      if (window.sidebar) {
+        window.sidebar.toggle()
+      } else {
+        console.warn('Sidebar not available')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.sidebar-example {
+  padding: 1rem;
+  background: #f8f9fa;
+  border-radius: 8px;
+  margin: 1rem 0;
+}
+
+.controls {
+  display: flex;
+  gap: 0.5rem;
+  margin-bottom: 1rem;
+  flex-wrap: wrap;
+}
+
+.btn {
+  padding: 0.5rem 1rem;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 0.875rem;
+  transition: all 0.2s ease;
+}
+
+.btn-primary {
+  background: #007bff;
+  color: white;
+}
+
+.btn-primary:hover {
+  background: #0056b3;
+}
+
+.btn-secondary {
+  background: #6c757d;
+  color: white;
+}
+
+.btn-secondary:hover {
+  background: #545b62;
+}
+
+.btn-danger {
+  background: #dc3545;
+  color: white;
+}
+
+.btn-danger:hover {
+  background: #c82333;
+}
+
+.btn-info {
+  background: #17a2b8;
+  color: white;
+}
+
+.btn-info:hover {
+  background: #138496;
+}
+
+.info {
+  background: white;
+  padding: 1rem;
+  border-radius: 4px;
+  border-left: 4px solid #007bff;
+}
+
+.info ul {
+  margin: 0.5rem 0;
+  padding-left: 1.5rem;
+}
+
+.info code {
+  background: #f1f3f4;
+  padding: 0.125rem 0.25rem;
+  border-radius: 3px;
+  font-family: monospace;
+  font-size: 0.875rem;
+}
+</style> 

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

@@ -0,0 +1,76 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+// Import components
+import App from './App.vue'
+import ExecutionDialog from './ExecutionDialog.vue'
+import ActionButton from './ActionButton.vue'
+import ArgumentForm from './ArgumentForm.vue'
+
+// Define routes
+const routes = [
+  {
+    path: '/',
+    name: 'Home',
+    component: () => import('./views/DashboardRoot.vue'),
+    meta: { title: 'OliveTin - Dashboard' }
+  },
+  {
+    path: '/logs',
+    name: 'Logs',
+    component: () => import('./views/LogsView.vue'),
+    meta: { title: 'OliveTin - Logs' }
+  },
+  {
+    path: '/diagnostics',
+    name: 'Diagnostics',
+    component: () => import('./views/DiagnosticsView.vue'),
+    meta: { title: 'OliveTin - Diagnostics' }
+  },
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('./views/LoginView.vue'),
+    meta: { title: 'OliveTin - Login' }
+  },
+  {
+    path: '/:pathMatch(.*)*',
+    name: 'NotFound',
+    component: () => import('./views/NotFoundView.vue'),
+    meta: { title: 'OliveTin - Page Not Found' }
+  }
+]
+
+// Create router instance
+const router = createRouter({
+  history: createWebHistory(),
+  routes,
+  scrollBehavior(to, from, savedPosition) {
+    if (savedPosition) {
+      return savedPosition
+    } else {
+      return { top: 0 }
+    }
+  }
+})
+
+// Navigation guard to update page title
+router.beforeEach((to, from, next) => {
+  if (to.meta && to.meta.title) {
+    document.title = to.meta.title
+  }
+  next()
+})
+
+// Navigation guard for authentication (if needed)
+router.beforeEach((to, from, next) => {
+  // Check if user is authenticated for protected routes
+  const isAuthenticated = window.isAuthenticated || true // Default to true for now
+  
+  if (to.meta.requiresAuth && !isAuthenticated) {
+    next('/login')
+  } else {
+    next()
+  }
+})
+
+export default router 

+ 23 - 0
frontend/resources/vue/views/DashboardRoot.vue

@@ -0,0 +1,23 @@
+<template>
+    <div v-for="dashboard in dashboards" :key="dashboard.id">       
+        <Dashboard :dashboard="dashboard" />
+    </div>
+</template>
+
+<script setup>
+import { onMounted, ref } from 'vue'
+import Dashboard from '../Dashboard.vue'
+
+const dashboards = ref([])
+
+async function refreshActions() {
+    const ret = await window.client.getDashboardComponents();
+
+    console.log(ret.dashboards)
+    dashboards.value = ret.dashboards
+}
+
+onMounted(() => {
+    refreshActions()
+})
+</script>

+ 215 - 0
frontend/resources/vue/views/DiagnosticsView.vue

@@ -0,0 +1,215 @@
+<template>
+  <div class="diagnostics-view">
+    <div class="diagnostics-content">
+      <p class="note">
+        <strong>Note:</strong> Diagnostics are only generated on OliveTin startup - they are not updated in real-time or
+        when you refresh this page.
+        They are intended as a "quick reference" to help you.
+      </p>
+
+      <p class="note">
+        If you are having problems with OliveTin and want to raise a support request, please don't take a screenshot or
+        copy text from this page,
+        but instead it is highly recommended to include a
+        <a href="https://docs.olivetin.app/sosreport.html" target="_blank">sosreport</a>
+        which is more detailed, and makes it easier to help you.
+      </p>
+
+      <div class="diagnostics-section">
+        <h3>SSH</h3>
+        <table class="diagnostics-table">
+          <tbody>
+            <tr>
+              <td width="10%">Found Key</td>
+              <td>{{ diagnostics.sshFoundKey || '?' }}</td>
+            </tr>
+            <tr>
+              <td>Found Config</td>
+              <td>{{ diagnostics.sshFoundConfig || '?' }}</td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+
+      <div v-if="diagnostics.system" class="diagnostics-section">
+        <h3>System</h3>
+        <table class="diagnostics-table">
+          <tbody>
+            <tr v-for="(value, key) in diagnostics.system" :key="key">
+              <td width="10%">{{ formatKey(key) }}</td>
+              <td>{{ value }}</td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+
+      <div v-if="diagnostics.network" class="diagnostics-section">
+        <h3>Network</h3>
+        <table class="diagnostics-table">
+          <tbody>
+            <tr v-for="(value, key) in diagnostics.network" :key="key">
+              <td width="10%">{{ formatKey(key) }}</td>
+              <td>{{ value }}</td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+
+      <div v-if="diagnostics.storage" class="diagnostics-section">
+        <h3>Storage</h3>
+        <table class="diagnostics-table">
+          <tbody>
+            <tr v-for="(value, key) in diagnostics.storage" :key="key">
+              <td width="10%">{{ formatKey(key) }}</td>
+              <td>{{ value }}</td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+
+      <div v-if="diagnostics.services" class="diagnostics-section">
+        <h3>Services</h3>
+        <table class="diagnostics-table">
+          <tbody>
+            <tr v-for="(value, key) in diagnostics.services" :key="key">
+              <td width="10%">{{ formatKey(key) }}</td>
+              <td>{{ value }}</td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+
+      <div v-if="diagnostics.errors && diagnostics.errors.length > 0" class="diagnostics-section">
+        <h3>Errors</h3>
+        <div class="error-list">
+          <div v-for="(error, index) in diagnostics.errors" :key="index" class="error-item">
+            {{ error }}
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+
+const diagnostics = ref({})
+const loading = ref(false)
+
+async function fetchDiagnostics() {
+  loading.value = true
+
+  try {
+    const response = await window.client.getDiagnostics();
+    diagnostics.value = {
+      sshFoundKey: response.sshFoundKey,
+      sshFoundConfig: response.sshFoundConfig
+    };
+  } catch (err) {
+    console.error('Failed to fetch diagnostics:', err);
+    diagnostics.value = {
+      sshFoundKey: 'Unknown',
+      sshFoundConfig: 'Unknown'
+    }
+  }
+  loading.value = false
+}
+
+function formatKey(key) {
+  return key
+    .replace(/([A-Z])/g, ' $1')
+    .replace(/^./, str => str.toUpperCase())
+    .trim()
+}
+
+onMounted(() => {
+  fetchDiagnostics()
+})
+</script>
+
+<style scoped>
+.diagnostics-view {
+  padding: 1rem;
+}
+
+.diagnostics-content {
+  max-width: 800px;
+  margin: 0 auto;
+}
+
+.note {
+  background: #f8f9fa;
+  border-left: 4px solid #007bff;
+  padding: 1rem;
+  margin-bottom: 1rem;
+  border-radius: 0 4px 4px 0;
+  font-size: 0.875rem;
+  color: #495057;
+}
+
+.note a {
+  color: #007bff;
+  text-decoration: none;
+}
+
+.note a:hover {
+  text-decoration: underline;
+}
+
+.diagnostics-section {
+  margin-bottom: 2rem;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+}
+
+.diagnostics-section h3 {
+  margin: 0;
+  padding: 1rem;
+  background: #f8f9fa;
+  border-bottom: 1px solid #dee2e6;
+  font-size: 1.1rem;
+  font-weight: 600;
+}
+
+.diagnostics-table {
+  width: 100%;
+  border-collapse: collapse;
+}
+
+.diagnostics-table td {
+  padding: 0.75rem 1rem;
+  border-bottom: 1px solid #f1f3f4;
+}
+
+.diagnostics-table td:first-child {
+  font-weight: 500;
+  color: #495057;
+  background: #f8f9fa;
+}
+
+.diagnostics-table tr:last-child td {
+  border-bottom: none;
+}
+
+.error-list {
+  padding: 1rem;
+}
+
+.error-item {
+  background: #f8d7da;
+  color: #721c24;
+  padding: 0.75rem;
+  margin-bottom: 0.5rem;
+  border-radius: 4px;
+  border-left: 4px solid #dc3545;
+  font-family: monospace;
+  font-size: 0.875rem;
+}
+
+.error-item:last-child {
+  margin-bottom: 0;
+}
+</style>

+ 132 - 0
frontend/resources/vue/views/LoginView.vue

@@ -0,0 +1,132 @@
+<template>
+  <section class = "small">
+    <div class="login-container">
+      <div class="login-form" style="display: grid; grid-template-columns: max-content 1fr; gap: 1em;">
+        <h2>Login to OliveTin</h2>
+
+        <div v-if="!hasOAuth && !hasLocalLogin" class="login-disabled">
+          <p>This server is not configured with either OAuth, or local users, so you cannot login.</p>
+        </div>
+
+        <div v-if="hasOAuth" class="login-oauth2">
+          <h3>OAuth Login</h3>
+          <div class="oauth-providers">
+            <button v-for="provider in oauthProviders" :key="provider.name" class="oauth-button"
+              @click="loginWithOAuth(provider)">
+              <span v-if="provider.icon" class="provider-icon" v-html="provider.icon"></span>
+              <span class="provider-name">Login with {{ provider.name }}</span>
+            </button>
+          </div>
+        </div>
+
+        <div v-if="hasLocalLogin" class="login-local">
+          <h3>Local Login</h3>
+          <form @submit.prevent="handleLocalLogin" class="local-login-form">
+            <div v-if="loginError" class="error-message">
+              {{ loginError }}
+            </div>
+
+            <label for="username">Username:</label>
+            <input id="username" v-model="username" type="text" name="username" autocomplete="username" required />
+
+            <label for="password">Password:</label>
+            <input id="password" v-model="password" type="password" name="password" autocomplete="current-password"
+              required />
+
+            <button type="submit" :disabled="loading" class="login-button">
+              {{ loading ? 'Logging in...' : 'Login' }}
+            </button>
+          </form>
+        </div>
+      </div>
+    </div>
+  </section>
+</template>
+
+<script>
+export default {
+  name: 'LoginView',
+  data() {
+    return {
+      username: '',
+      password: '',
+      loading: false,
+      loginError: '',
+      hasOAuth: false,
+      hasLocalLogin: false,
+      oauthProviders: []
+    }
+  },
+  mounted() {
+    this.fetchLoginOptions()
+  },
+  methods: {
+    async fetchLoginOptions() {
+      try {
+        const response = await fetch('webUiSettings.json')
+        const settings = await response.json()
+
+        this.hasOAuth = settings.AuthOAuth2Providers && settings.AuthOAuth2Providers.length > 0
+        this.hasLocalLogin = settings.AuthLocalLogin
+
+        if (this.hasOAuth) {
+          this.oauthProviders = settings.AuthOAuth2Providers
+        }
+      } catch (err) {
+        console.error('Failed to fetch login options:', err)
+      }
+    },
+
+    async handleLocalLogin() {
+      this.loading = true
+      this.loginError = ''
+
+      try {
+        const response = await fetch('/api/login', {
+          method: 'POST',
+          headers: {
+            'Content-Type': 'application/json'
+          },
+          body: JSON.stringify({
+            username: this.username,
+            password: this.password
+          })
+        })
+
+        if (response.ok) {
+          // Redirect to home page on successful login
+          this.$router.push('/')
+        } else {
+          const error = await response.text()
+          this.loginError = error || 'Login failed. Please check your credentials.'
+        }
+      } catch (err) {
+        console.error('Login error:', err)
+        this.loginError = 'Network error. Please try again.'
+      } finally {
+        this.loading = false
+      }
+    },
+
+    loginWithOAuth(provider) {
+      // Redirect to OAuth provider
+      window.location.href = provider.authUrl
+    }
+  }
+}
+</script>
+
+<style scoped>
+.login-view {
+  min-height: 100vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 1rem;
+}
+
+form {
+  grid-template-columns: max-content 1fr;
+  gap: 1em;
+}
+</style>

+ 318 - 0
frontend/resources/vue/views/LogsView.vue

@@ -0,0 +1,318 @@
+<template>
+  <div class="logs-view">
+    <div class="toolbar">
+      <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" d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"/>
+        </svg>
+        <input 
+          placeholder="Search for action name" 
+          v-model="searchText"
+          @input="handleSearch"
+        />
+        <button 
+          title="Clear search filter" 
+          :disabled="!searchText"
+          @click="clearSearch"
+        >
+          <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>
+      </label>
+    </div>
+
+    <table v-show="filteredLogs.length > 0" class="logs-table">
+      <thead>
+        <tr>
+          <th>Timestamp</th>
+          <th>Action</th>
+          <th>Metadata</th>
+          <th>Status</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr 
+          v-for="log in filteredLogs" 
+          :key="log.executionTrackingId"
+          class="log-row"
+          :title="log.actionTitle"
+        >
+          <td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
+          <td>
+            <span class="icon" v-html="log.actionIcon"></span>
+            <a href="javascript:void(0)" class="content" @click="showLogDetails(log)">
+              {{ log.actionTitle }}
+            </a>
+          </td>
+          <td class="tags">
+            <span v-if="log.tags && log.tags.length > 0" class="tag-list">
+              <span v-for="tag in log.tags" :key="tag" class="tag">{{ tag }}</span>
+            </span>
+          </td>
+          <td class="exit-code">
+            <span :class="getStatusClass(log)">
+              {{ getStatusText(log) }}
+            </span>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+
+    <div v-show="filteredLogs.length === 0" class="empty-state">
+      <p>There are no logs to display.</p>
+      <router-link to="/">Return to index</router-link>
+    </div>
+
+    <p class="note">
+      <strong>Note:</strong> The server is configured to only send 
+      <strong>{{ pageSize }}</strong> log entries at a time. 
+      The search box at the top of this page only searches this current page of logs.
+    </p>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'LogsView',
+  data() {
+    return {
+      logs: [],
+      searchText: '',
+      pageSize: '?',
+      loading: false
+    }
+  },
+  computed: {
+    filteredLogs() {
+      if (!this.searchText) {
+        return this.logs
+      }
+      
+      const searchLower = this.searchText.toLowerCase()
+      return this.logs.filter(log => 
+        log.actionTitle.toLowerCase().includes(searchLower)
+      )
+    }
+  },
+  mounted() {
+    this.fetchLogs()
+    this.fetchPageSize()
+  },
+  methods: {
+    async fetchLogs() {
+      this.loading = true
+      try {
+        const response = await window.client.getLogs()
+        this.logs = response.logEntries || []
+      } catch (err) {
+        console.error('Failed to fetch logs:', err)
+        window.showBigError('fetch-logs', 'getting logs', err, false)
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    async fetchPageSize() {
+      try {
+        const response = await fetch('webUiSettings.json')
+        const settings = await response.json()
+        this.pageSize = settings.LogsPageSize || '?'
+      } catch (err) {
+        console.warn('Failed to fetch page size:', err)
+      }
+    },
+    
+    handleSearch() {
+      // Search is handled by computed property
+    },
+    
+    clearSearch() {
+      this.searchText = ''
+    },
+    
+    formatTimestamp(timestamp) {
+      if (!timestamp) return 'Unknown'
+      
+      try {
+        const date = new Date(timestamp)
+        return date.toLocaleString()
+      } catch (err) {
+        return timestamp
+      }
+    },
+    
+    getStatusClass(log) {
+      if (log.timedOut) return 'status-timeout'
+      if (log.blocked) return 'status-blocked'
+      if (log.exitCode !== 0) return 'status-error'
+      return 'status-success'
+    },
+    
+    getStatusText(log) {
+      if (log.timedOut) return 'Timed out'
+      if (log.blocked) return 'Blocked'
+      if (log.exitCode !== 0) return `Exit code ${log.exitCode}`
+      return 'Success'
+    },
+    
+    showLogDetails(log) {
+      // Emit event to parent or use global execution dialog
+      if (window.executionDialog) {
+        window.executionDialog.reset()
+        window.executionDialog.show()
+        window.executionDialog.fetchExecutionResult(log.executionTrackingId)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.logs-view {
+  padding: 1rem;
+}
+
+.toolbar {
+  margin-bottom: 1rem;
+}
+
+.input-with-icons {
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
+  background: #fff;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  padding: 0.5rem;
+}
+
+.input-with-icons input {
+  border: none;
+  outline: none;
+  flex: 1;
+  font-size: 1rem;
+}
+
+.input-with-icons button {
+  background: none;
+  border: none;
+  cursor: pointer;
+  padding: 0.25rem;
+  border-radius: 3px;
+}
+
+.input-with-icons button:hover:not(:disabled) {
+  background: #f5f5f5;
+}
+
+.input-with-icons button:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.logs-table {
+  width: 100%;
+  border-collapse: collapse;
+  background: #fff;
+  border-radius: 4px;
+  overflow: hidden;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.logs-table th {
+  background: #f8f9fa;
+  padding: 0.75rem;
+  text-align: left;
+  font-weight: 600;
+  border-bottom: 1px solid #dee2e6;
+}
+
+.logs-table td {
+  padding: 0.75rem;
+  border-bottom: 1px solid #f1f3f4;
+}
+
+.logs-table tr:hover {
+  background: #f8f9fa;
+}
+
+.timestamp {
+  font-family: monospace;
+  font-size: 0.875rem;
+  color: #666;
+}
+
+.icon {
+  margin-right: 0.5rem;
+  font-size: 1.2em;
+}
+
+.content {
+  color: #007bff;
+  text-decoration: none;
+  cursor: pointer;
+}
+
+.content:hover {
+  text-decoration: underline;
+}
+
+.tag-list {
+  display: flex;
+  gap: 0.25rem;
+  flex-wrap: wrap;
+}
+
+.tag {
+  background: #e9ecef;
+  color: #495057;
+  padding: 0.125rem 0.5rem;
+  border-radius: 12px;
+  font-size: 0.75rem;
+}
+
+.status-success {
+  color: #28a745;
+  font-weight: 500;
+}
+
+.status-error {
+  color: #dc3545;
+  font-weight: 500;
+}
+
+.status-timeout {
+  color: #ffc107;
+  font-weight: 500;
+}
+
+.status-blocked {
+  color: #6c757d;
+  font-weight: 500;
+}
+
+.empty-state {
+  text-align: center;
+  padding: 2rem;
+  color: #666;
+}
+
+.empty-state a {
+  color: #007bff;
+  text-decoration: none;
+}
+
+.empty-state a:hover {
+  text-decoration: underline;
+}
+
+.note {
+  margin-top: 1rem;
+  padding: 1rem;
+  background: #f8f9fa;
+  border-radius: 4px;
+  font-size: 0.875rem;
+  color: #666;
+}
+</style> 

+ 112 - 0
frontend/resources/vue/views/NotFoundView.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="not-found-view">
+    <div class="not-found-container">
+      <div class="not-found-content">
+        <h1>404</h1>
+        <h2>Page Not Found</h2>
+        <p>The page you're looking for doesn't exist.</p>
+        
+        <div class="actions">
+          <router-link to="/" class="btn btn-primary">
+            Go to Home
+          </router-link>
+          <button @click="goBack" class="btn btn-secondary">
+            Go Back
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'NotFoundView',
+  methods: {
+    goBack() {
+      this.$router.go(-1)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.not-found-view {
+  min-height: 100vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  padding: 1rem;
+}
+
+.not-found-container {
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+  max-width: 500px;
+  width: 100%;
+}
+
+.not-found-content {
+  padding: 3rem 2rem;
+  text-align: center;
+}
+
+.not-found-content h1 {
+  font-size: 6rem;
+  margin: 0;
+  color: #007bff;
+  font-weight: 700;
+  line-height: 1;
+}
+
+.not-found-content h2 {
+  font-size: 2rem;
+  margin: 0 0 1rem 0;
+  color: #333;
+}
+
+.not-found-content p {
+  font-size: 1.1rem;
+  color: #666;
+  margin-bottom: 2rem;
+}
+
+.actions {
+  display: flex;
+  gap: 1rem;
+  justify-content: center;
+  flex-wrap: wrap;
+}
+
+.btn {
+  padding: 0.75rem 1.5rem;
+  border: none;
+  border-radius: 6px;
+  font-size: 1rem;
+  cursor: pointer;
+  text-decoration: none;
+  display: inline-block;
+  transition: all 0.2s ease;
+}
+
+.btn-primary {
+  background: #007bff;
+  color: white;
+}
+
+.btn-primary:hover {
+  background: #0056b3;
+}
+
+.btn-secondary {
+  background: #6c757d;
+  color: white;
+}
+
+.btn-secondary:hover {
+  background: #545b62;
+}
+</style> 

+ 41 - 0
frontend/style.css

@@ -0,0 +1,41 @@
+@import 'femtocrank/style.css';
+
+section.transparent {
+	background-color: transparent;
+	box-shadow: none;
+}
+
+fieldset {
+	display: grid;
+	grid-template-columns: repeat(auto-fit, 180px);
+	grid-auto-rows: 1fr;
+	justify-content: center;
+	place-items: stretch;
+}
+
+action-button {
+	display: flex;
+	flex-direction: column;
+	flex-grow: 1;
+}
+
+action-button > button {
+	display: flex;
+	flex-direction: column;
+	flex-grow: 1;
+	justify-content: center;
+	font-weight: normal;
+	font-size: 0.85em;
+	box-shadow: 0 0 .6em #aaa;
+}
+
+action-button > button .icon {
+	font-size: 3em;
+}
+
+dialog {
+	border-radius: 1em;
+}
+footer span {
+	margin-right: 1em;
+}

+ 0 - 0
webui.dev/themes/waffles/theme.css → frontend/themes/waffles/theme.css


+ 29 - 0
frontend/vite.config.js

@@ -0,0 +1,29 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import Components from 'unplugin-vue-components/vite'
+
+export default defineConfig({
+  plugins: [
+    Components({
+      dirs: ['resources/vue/'],
+      extensions: ['vue'],
+      deep: true,
+      dts: false,
+    }),
+    vue(),
+  ],
+  server: {
+    proxy: {
+      '/webUiSettings.json': {
+        target: 'http://localhost:1337',
+        changeOrigin: true,
+        secure: false,
+      },
+      '/api': {
+        target: 'http://localhost:1337',
+        changeOrigin: true,
+        secure: false,
+      }
+    },
+  },
+})

+ 6 - 7
proto/buf.gen.yaml

@@ -1,17 +1,16 @@
 version: v2
 plugins:
   - remote: buf.build/protocolbuffers/go
-    out: ../service/gen/grpc/
+    out: ../service/gen/
     opt: paths=source_relative
 
-  - remote: buf.build/grpc/go
-    out: ../service/gen/grpc/
-    opt: paths=source_relative,require_unimplemented_servers=false
-
-  - remote: buf.build/grpc-ecosystem/gateway
-    out: ../service/gen/grpc/
+  - remote: buf.build/connectrpc/go
+    out: ../service/gen/
     opt: paths=source_relative
 
+  - remote: buf.build/bufbuild/es
+    out: ../frontend/resources/scripts/gen/
+
 #  - name: swagger
 #    out: reports/swagger
 

+ 69 - 121
proto/olivetin/api/v1/olivetin.proto

@@ -2,10 +2,7 @@ syntax = "proto3";
 
 package olivetin.api.v1;
 
-option go_package = "github.com/jamesread/OliveTin/gen/grpc/olivetin/api/v1;apiv1";
-
-import "google/api/annotations.proto";
-import "google/api/httpbody.proto";
+option go_package = "github.com/OliveTin/OliveTin/gen/olivetin/api/v1;apiv1";
 
 message Action {
 	string id = 1;
@@ -43,20 +40,12 @@ message Entity {
 message GetDashboardComponentsResponse {
 	string title = 1;
 
-	repeated Action actions = 2;
-	repeated Entity entities = 3;
-	repeated DashboardComponent dashboards = 4;
+	repeated Dashboard dashboards = 4;
 
 	string authenticated_user = 5;
     string authenticated_user_provider = 6;
 
 	EffectivePolicy effective_policy = 7;
-	Diagnostics diagnostics = 8;
-}
-
-message Diagnostics {
-	string SshFoundKey = 1;
-	string SshFoundConfig = 2;
 }
 
 message EffectivePolicy {
@@ -66,6 +55,11 @@ message EffectivePolicy {
 
 message GetDashboardComponentsRequest {}
 
+message Dashboard {
+	string title = 1;
+	repeated DashboardComponent contents = 2;
+}
+
 message DashboardComponent {
 	string title = 1;
 	string type = 2;
@@ -216,6 +210,19 @@ message GetReadyzResponse {
 	string status = 1;
 }
 
+message EventStreamRequest {
+}
+
+message EventStreamResponse {
+  oneof event {
+    EventEntityChanged entity_changed = 2;
+    EventConfigChanged config_changed = 3;
+    EventExecutionFinished execution_finished = 4;
+    EventExecutionStarted execution_started = 5;
+    EventOutputChunk output_chunk = 6;
+  }
+}
+
 message EventOutputChunk {
 	string execution_tracking_id = 1;
 
@@ -257,117 +264,58 @@ message PasswordHashRequest {
 }
 
 message PasswordHashResponse {
+    string hash = 1;
 }
 
 message LogoutRequest {}
 
+message LogoutResponse {
+}
+
+message GetDiagnosticsRequest {
+}
+
+message GetDiagnosticsResponse {
+	string SshFoundKey = 1;
+	string SshFoundConfig = 2;
+}
+
 service OliveTinApiService {
-	rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) {
-		option (google.api.http) = {
-			get: "/api/GetDashboardComponents"
-		};
-	}
-
-	rpc StartAction(StartActionRequest) returns (StartActionResponse) {
-		option (google.api.http) = {
-			post: "/api/StartAction"
-			body: "*"
-		};
-	}
-
-	rpc StartActionAndWait(StartActionAndWaitRequest) returns (StartActionAndWaitResponse) {
-		option (google.api.http) = {
-			post: "/api/StartActionAndWait"
-			body: "*"
-		};
-	}
-
-	rpc StartActionByGet(StartActionByGetRequest) returns (StartActionByGetResponse) {
-		option (google.api.http) = {
-			get: "/api/StartActionByGet/{action_id}"
-		};
-	}
-
-	rpc StartActionByGetAndWait(StartActionByGetAndWaitRequest) returns (StartActionByGetAndWaitResponse) {
-		option (google.api.http) = {
-			get: "/api/StartActionByGetAndWait/{action_id}"
-		};
-	}
-
-	rpc KillAction(KillActionRequest) returns (KillActionResponse) {
-		option (google.api.http) = {
-			post: "/api/KillAction"
-			body: "*"
-		};
-	}
-
-	rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) {
-		option (google.api.http) = {
-			post: "/api/ExecutionStatus"
-			body: "*"
-		};
-	}
-
-	rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {
-		option (google.api.http) = {
-			get: "/api/GetLogs"
-		};
-	}
-
-	rpc ValidateArgumentType(ValidateArgumentTypeRequest) returns (ValidateArgumentTypeResponse) {
-		option (google.api.http) = {
-			post: "/api/ValidateArgumentType"
-			body: "*"
-		};
-	}
-
-	rpc WhoAmI(WhoAmIRequest) returns (WhoAmIResponse) {
-		option (google.api.http) = {
-			get: "/api/WhoAmI"
-		};
-	}
-
-	rpc SosReport(SosReportRequest) returns (google.api.HttpBody) {
-		option (google.api.http) = {
-			get: "/api/sosreport"
-		};
-	}
-
-	rpc DumpVars(DumpVarsRequest) returns (DumpVarsResponse) {
-		option (google.api.http) = {
-			get: "/api/DumpVars"
-		};
-	}
-
-	rpc DumpPublicIdActionMap(DumpPublicIdActionMapRequest) returns (DumpPublicIdActionMapResponse) {
-		option (google.api.http) = {
-			get: "/api/DumpActionMap"
-		};
-	}
-
-	rpc GetReadyz(GetReadyzRequest) returns (GetReadyzResponse) {
-		option (google.api.http) = {
-			get: "/api/readyz"
-		};
-	}
-
-    rpc LocalUserLogin(LocalUserLoginRequest) returns (LocalUserLoginResponse) {
-        option (google.api.http) = {
-            post: "/api/LocalUserLogin"
-            body: "*"
-        };
-    }
-
-    rpc PasswordHash(PasswordHashRequest) returns (google.api.HttpBody) {
-        option (google.api.http) = {
-            post: "/api/PasswordHash"
-            body: "*"
-        };
-    }
-
-    rpc Logout(LogoutRequest) returns (google.api.HttpBody) {
-        option (google.api.http) = {
-            get: "/api/Logout"
-        };
-    }
+	rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) {}
+
+	rpc StartAction(StartActionRequest) returns (StartActionResponse) {}
+
+	rpc StartActionAndWait(StartActionAndWaitRequest) returns (StartActionAndWaitResponse) {}
+
+	rpc StartActionByGet(StartActionByGetRequest) returns (StartActionByGetResponse) {}
+
+	rpc StartActionByGetAndWait(StartActionByGetAndWaitRequest) returns (StartActionByGetAndWaitResponse) {}
+
+	rpc KillAction(KillActionRequest) returns (KillActionResponse) {}
+
+	rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) {}
+
+	rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {}
+
+	rpc ValidateArgumentType(ValidateArgumentTypeRequest) returns (ValidateArgumentTypeResponse) {}
+
+	rpc WhoAmI(WhoAmIRequest) returns (WhoAmIResponse) {}
+
+	rpc SosReport(SosReportRequest) returns (SosReportResponse) {}
+
+	rpc DumpVars(DumpVarsRequest) returns (DumpVarsResponse) {}
+
+	rpc DumpPublicIdActionMap(DumpPublicIdActionMapRequest) returns (DumpPublicIdActionMapResponse) {}
+
+	rpc GetReadyz(GetReadyzRequest) returns (GetReadyzResponse) {}
+
+    rpc LocalUserLogin(LocalUserLoginRequest) returns (LocalUserLoginResponse) {}
+
+    rpc PasswordHash(PasswordHashRequest) returns (PasswordHashResponse) {}
+
+    rpc Logout(LogoutRequest) returns (LogoutResponse) {}
+
+    rpc EventStream(EventStreamRequest) returns (stream EventStreamResponse) {}
+
+	rpc GetDiagnostics(GetDiagnosticsRequest) returns (GetDiagnosticsResponse) {}
 }

+ 0 - 1203
service/gen/grpc/olivetin/api/v1/olivetin.pb.gw.go

@@ -1,1203 +0,0 @@
-// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
-// source: olivetin/api/v1/olivetin.proto
-
-/*
-Package apiv1 is a reverse proxy.
-
-It translates gRPC into RESTful JSON APIs.
-*/
-package apiv1
-
-import (
-	"context"
-	"errors"
-	"io"
-	"net/http"
-
-	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
-	"github.com/grpc-ecosystem/grpc-gateway/v2/utilities"
-	"google.golang.org/grpc"
-	"google.golang.org/grpc/codes"
-	"google.golang.org/grpc/grpclog"
-	"google.golang.org/grpc/metadata"
-	"google.golang.org/grpc/status"
-	"google.golang.org/protobuf/proto"
-)
-
-// Suppress "imported and not used" errors
-var (
-	_ codes.Code
-	_ io.Reader
-	_ status.Status
-	_ = errors.New
-	_ = runtime.String
-	_ = utilities.NewDoubleArray
-	_ = metadata.Join
-)
-
-func request_OliveTinApiService_GetDashboardComponents_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq GetDashboardComponentsRequest
-		metadata runtime.ServerMetadata
-	)
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.GetDashboardComponents(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_GetDashboardComponents_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq GetDashboardComponentsRequest
-		metadata runtime.ServerMetadata
-	)
-	msg, err := server.GetDashboardComponents(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_StartAction_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq StartActionRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.StartAction(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_StartAction_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq StartActionRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	msg, err := server.StartAction(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_StartActionAndWait_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq StartActionAndWaitRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.StartActionAndWait(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_StartActionAndWait_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq StartActionAndWaitRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	msg, err := server.StartActionAndWait(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_StartActionByGet_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq StartActionByGetRequest
-		metadata runtime.ServerMetadata
-		err      error
-	)
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	val, ok := pathParams["action_id"]
-	if !ok {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "action_id")
-	}
-	protoReq.ActionId, err = runtime.String(val)
-	if err != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "action_id", err)
-	}
-	msg, err := client.StartActionByGet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_StartActionByGet_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq StartActionByGetRequest
-		metadata runtime.ServerMetadata
-		err      error
-	)
-	val, ok := pathParams["action_id"]
-	if !ok {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "action_id")
-	}
-	protoReq.ActionId, err = runtime.String(val)
-	if err != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "action_id", err)
-	}
-	msg, err := server.StartActionByGet(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_StartActionByGetAndWait_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq StartActionByGetAndWaitRequest
-		metadata runtime.ServerMetadata
-		err      error
-	)
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	val, ok := pathParams["action_id"]
-	if !ok {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "action_id")
-	}
-	protoReq.ActionId, err = runtime.String(val)
-	if err != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "action_id", err)
-	}
-	msg, err := client.StartActionByGetAndWait(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_StartActionByGetAndWait_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq StartActionByGetAndWaitRequest
-		metadata runtime.ServerMetadata
-		err      error
-	)
-	val, ok := pathParams["action_id"]
-	if !ok {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "action_id")
-	}
-	protoReq.ActionId, err = runtime.String(val)
-	if err != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "action_id", err)
-	}
-	msg, err := server.StartActionByGetAndWait(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_KillAction_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq KillActionRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.KillAction(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_KillAction_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq KillActionRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	msg, err := server.KillAction(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_ExecutionStatus_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq ExecutionStatusRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.ExecutionStatus(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_ExecutionStatus_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq ExecutionStatusRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	msg, err := server.ExecutionStatus(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-var filter_OliveTinApiService_GetLogs_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
-
-func request_OliveTinApiService_GetLogs_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq GetLogsRequest
-		metadata runtime.ServerMetadata
-	)
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	if err := req.ParseForm(); err != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_OliveTinApiService_GetLogs_0); err != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	msg, err := client.GetLogs(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_GetLogs_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq GetLogsRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := req.ParseForm(); err != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_OliveTinApiService_GetLogs_0); err != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	msg, err := server.GetLogs(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_ValidateArgumentType_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq ValidateArgumentTypeRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.ValidateArgumentType(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_ValidateArgumentType_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq ValidateArgumentTypeRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	msg, err := server.ValidateArgumentType(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_WhoAmI_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq WhoAmIRequest
-		metadata runtime.ServerMetadata
-	)
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.WhoAmI(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_WhoAmI_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq WhoAmIRequest
-		metadata runtime.ServerMetadata
-	)
-	msg, err := server.WhoAmI(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_SosReport_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq SosReportRequest
-		metadata runtime.ServerMetadata
-	)
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.SosReport(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_SosReport_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq SosReportRequest
-		metadata runtime.ServerMetadata
-	)
-	msg, err := server.SosReport(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_DumpVars_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq DumpVarsRequest
-		metadata runtime.ServerMetadata
-	)
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.DumpVars(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_DumpVars_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq DumpVarsRequest
-		metadata runtime.ServerMetadata
-	)
-	msg, err := server.DumpVars(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_DumpPublicIdActionMap_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq DumpPublicIdActionMapRequest
-		metadata runtime.ServerMetadata
-	)
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.DumpPublicIdActionMap(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_DumpPublicIdActionMap_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq DumpPublicIdActionMapRequest
-		metadata runtime.ServerMetadata
-	)
-	msg, err := server.DumpPublicIdActionMap(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_GetReadyz_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq GetReadyzRequest
-		metadata runtime.ServerMetadata
-	)
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.GetReadyz(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_GetReadyz_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq GetReadyzRequest
-		metadata runtime.ServerMetadata
-	)
-	msg, err := server.GetReadyz(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_LocalUserLogin_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq LocalUserLoginRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.LocalUserLogin(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_LocalUserLogin_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq LocalUserLoginRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	msg, err := server.LocalUserLogin(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_PasswordHash_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq PasswordHashRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.PasswordHash(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_PasswordHash_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq PasswordHashRequest
-		metadata runtime.ServerMetadata
-	)
-	if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	msg, err := server.PasswordHash(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-func request_OliveTinApiService_Logout_0(ctx context.Context, marshaler runtime.Marshaler, client OliveTinApiServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq LogoutRequest
-		metadata runtime.ServerMetadata
-	)
-	if req.Body != nil {
-		_, _ = io.Copy(io.Discard, req.Body)
-	}
-	msg, err := client.Logout(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-}
-
-func local_request_OliveTinApiService_Logout_0(ctx context.Context, marshaler runtime.Marshaler, server OliveTinApiServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var (
-		protoReq LogoutRequest
-		metadata runtime.ServerMetadata
-	)
-	msg, err := server.Logout(ctx, &protoReq)
-	return msg, metadata, err
-}
-
-// RegisterOliveTinApiServiceHandlerServer registers the http handlers for service OliveTinApiService to "mux".
-// UnaryRPC     :call OliveTinApiServiceServer directly.
-// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
-// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterOliveTinApiServiceHandlerFromEndpoint instead.
-// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
-func RegisterOliveTinApiServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server OliveTinApiServiceServer) error {
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_GetDashboardComponents_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/GetDashboardComponents", runtime.WithHTTPPathPattern("/api/GetDashboardComponents"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_GetDashboardComponents_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_GetDashboardComponents_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_StartAction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/StartAction", runtime.WithHTTPPathPattern("/api/StartAction"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_StartAction_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_StartAction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_StartActionAndWait_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/StartActionAndWait", runtime.WithHTTPPathPattern("/api/StartActionAndWait"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_StartActionAndWait_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_StartActionAndWait_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_StartActionByGet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/StartActionByGet", runtime.WithHTTPPathPattern("/api/StartActionByGet/{action_id}"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_StartActionByGet_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_StartActionByGet_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_StartActionByGetAndWait_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/StartActionByGetAndWait", runtime.WithHTTPPathPattern("/api/StartActionByGetAndWait/{action_id}"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_StartActionByGetAndWait_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_StartActionByGetAndWait_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_KillAction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/KillAction", runtime.WithHTTPPathPattern("/api/KillAction"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_KillAction_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_KillAction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_ExecutionStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/ExecutionStatus", runtime.WithHTTPPathPattern("/api/ExecutionStatus"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_ExecutionStatus_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_ExecutionStatus_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_GetLogs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/GetLogs", runtime.WithHTTPPathPattern("/api/GetLogs"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_GetLogs_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_GetLogs_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_ValidateArgumentType_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/ValidateArgumentType", runtime.WithHTTPPathPattern("/api/ValidateArgumentType"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_ValidateArgumentType_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_ValidateArgumentType_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_WhoAmI_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/WhoAmI", runtime.WithHTTPPathPattern("/api/WhoAmI"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_WhoAmI_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_WhoAmI_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_SosReport_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/SosReport", runtime.WithHTTPPathPattern("/api/sosreport"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_SosReport_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_SosReport_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_DumpVars_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/DumpVars", runtime.WithHTTPPathPattern("/api/DumpVars"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_DumpVars_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_DumpVars_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_DumpPublicIdActionMap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/DumpPublicIdActionMap", runtime.WithHTTPPathPattern("/api/DumpActionMap"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_DumpPublicIdActionMap_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_DumpPublicIdActionMap_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_GetReadyz_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/GetReadyz", runtime.WithHTTPPathPattern("/api/readyz"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_GetReadyz_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_GetReadyz_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_LocalUserLogin_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/LocalUserLogin", runtime.WithHTTPPathPattern("/api/LocalUserLogin"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_LocalUserLogin_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_LocalUserLogin_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_PasswordHash_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/PasswordHash", runtime.WithHTTPPathPattern("/api/PasswordHash"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_PasswordHash_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_PasswordHash_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_Logout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/Logout", runtime.WithHTTPPathPattern("/api/Logout"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_OliveTinApiService_Logout_0(annotatedContext, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_Logout_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-
-	return nil
-}
-
-// RegisterOliveTinApiServiceHandlerFromEndpoint is same as RegisterOliveTinApiServiceHandler but
-// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
-func RegisterOliveTinApiServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
-	conn, err := grpc.NewClient(endpoint, opts...)
-	if err != nil {
-		return err
-	}
-	defer func() {
-		if err != nil {
-			if cerr := conn.Close(); cerr != nil {
-				grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
-			}
-			return
-		}
-		go func() {
-			<-ctx.Done()
-			if cerr := conn.Close(); cerr != nil {
-				grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
-			}
-		}()
-	}()
-	return RegisterOliveTinApiServiceHandler(ctx, mux, conn)
-}
-
-// RegisterOliveTinApiServiceHandler registers the http handlers for service OliveTinApiService to "mux".
-// The handlers forward requests to the grpc endpoint over "conn".
-func RegisterOliveTinApiServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
-	return RegisterOliveTinApiServiceHandlerClient(ctx, mux, NewOliveTinApiServiceClient(conn))
-}
-
-// RegisterOliveTinApiServiceHandlerClient registers the http handlers for service OliveTinApiService
-// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "OliveTinApiServiceClient".
-// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "OliveTinApiServiceClient"
-// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
-// "OliveTinApiServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares.
-func RegisterOliveTinApiServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client OliveTinApiServiceClient) error {
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_GetDashboardComponents_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/GetDashboardComponents", runtime.WithHTTPPathPattern("/api/GetDashboardComponents"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_GetDashboardComponents_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_GetDashboardComponents_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_StartAction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/StartAction", runtime.WithHTTPPathPattern("/api/StartAction"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_StartAction_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_StartAction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_StartActionAndWait_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/StartActionAndWait", runtime.WithHTTPPathPattern("/api/StartActionAndWait"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_StartActionAndWait_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_StartActionAndWait_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_StartActionByGet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/StartActionByGet", runtime.WithHTTPPathPattern("/api/StartActionByGet/{action_id}"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_StartActionByGet_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_StartActionByGet_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_StartActionByGetAndWait_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/StartActionByGetAndWait", runtime.WithHTTPPathPattern("/api/StartActionByGetAndWait/{action_id}"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_StartActionByGetAndWait_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_StartActionByGetAndWait_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_KillAction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/KillAction", runtime.WithHTTPPathPattern("/api/KillAction"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_KillAction_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_KillAction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_ExecutionStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/ExecutionStatus", runtime.WithHTTPPathPattern("/api/ExecutionStatus"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_ExecutionStatus_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_ExecutionStatus_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_GetLogs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/GetLogs", runtime.WithHTTPPathPattern("/api/GetLogs"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_GetLogs_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_GetLogs_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_ValidateArgumentType_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/ValidateArgumentType", runtime.WithHTTPPathPattern("/api/ValidateArgumentType"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_ValidateArgumentType_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_ValidateArgumentType_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_WhoAmI_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/WhoAmI", runtime.WithHTTPPathPattern("/api/WhoAmI"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_WhoAmI_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_WhoAmI_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_SosReport_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/SosReport", runtime.WithHTTPPathPattern("/api/sosreport"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_SosReport_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_SosReport_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_DumpVars_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/DumpVars", runtime.WithHTTPPathPattern("/api/DumpVars"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_DumpVars_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_DumpVars_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_DumpPublicIdActionMap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/DumpPublicIdActionMap", runtime.WithHTTPPathPattern("/api/DumpActionMap"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_DumpPublicIdActionMap_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_DumpPublicIdActionMap_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_GetReadyz_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/GetReadyz", runtime.WithHTTPPathPattern("/api/readyz"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_GetReadyz_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_GetReadyz_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_LocalUserLogin_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/LocalUserLogin", runtime.WithHTTPPathPattern("/api/LocalUserLogin"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_LocalUserLogin_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_LocalUserLogin_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodPost, pattern_OliveTinApiService_PasswordHash_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/PasswordHash", runtime.WithHTTPPathPattern("/api/PasswordHash"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_PasswordHash_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_PasswordHash_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	mux.Handle(http.MethodGet, pattern_OliveTinApiService_Logout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/olivetin.api.v1.OliveTinApiService/Logout", runtime.WithHTTPPathPattern("/api/Logout"))
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_OliveTinApiService_Logout_0(annotatedContext, inboundMarshaler, client, req, pathParams)
-		annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
-		if err != nil {
-			runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		forward_OliveTinApiService_Logout_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-	})
-	return nil
-}
-
-var (
-	pattern_OliveTinApiService_GetDashboardComponents_0  = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "GetDashboardComponents"}, ""))
-	pattern_OliveTinApiService_StartAction_0             = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "StartAction"}, ""))
-	pattern_OliveTinApiService_StartActionAndWait_0      = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "StartActionAndWait"}, ""))
-	pattern_OliveTinApiService_StartActionByGet_0        = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "StartActionByGet", "action_id"}, ""))
-	pattern_OliveTinApiService_StartActionByGetAndWait_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "StartActionByGetAndWait", "action_id"}, ""))
-	pattern_OliveTinApiService_KillAction_0              = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "KillAction"}, ""))
-	pattern_OliveTinApiService_ExecutionStatus_0         = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "ExecutionStatus"}, ""))
-	pattern_OliveTinApiService_GetLogs_0                 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "GetLogs"}, ""))
-	pattern_OliveTinApiService_ValidateArgumentType_0    = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "ValidateArgumentType"}, ""))
-	pattern_OliveTinApiService_WhoAmI_0                  = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "WhoAmI"}, ""))
-	pattern_OliveTinApiService_SosReport_0               = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "sosreport"}, ""))
-	pattern_OliveTinApiService_DumpVars_0                = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "DumpVars"}, ""))
-	pattern_OliveTinApiService_DumpPublicIdActionMap_0   = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "DumpActionMap"}, ""))
-	pattern_OliveTinApiService_GetReadyz_0               = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "readyz"}, ""))
-	pattern_OliveTinApiService_LocalUserLogin_0          = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "LocalUserLogin"}, ""))
-	pattern_OliveTinApiService_PasswordHash_0            = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "PasswordHash"}, ""))
-	pattern_OliveTinApiService_Logout_0                  = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "Logout"}, ""))
-)
-
-var (
-	forward_OliveTinApiService_GetDashboardComponents_0  = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_StartAction_0             = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_StartActionAndWait_0      = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_StartActionByGet_0        = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_StartActionByGetAndWait_0 = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_KillAction_0              = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_ExecutionStatus_0         = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_GetLogs_0                 = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_ValidateArgumentType_0    = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_WhoAmI_0                  = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_SosReport_0               = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_DumpVars_0                = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_DumpPublicIdActionMap_0   = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_GetReadyz_0               = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_LocalUserLogin_0          = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_PasswordHash_0            = runtime.ForwardResponseMessage
-	forward_OliveTinApiService_Logout_0                  = runtime.ForwardResponseMessage
-)

+ 0 - 728
service/gen/grpc/olivetin/api/v1/olivetin_grpc.pb.go

@@ -1,728 +0,0 @@
-// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
-// versions:
-// - protoc-gen-go-grpc v1.5.1
-// - protoc             (unknown)
-// source: olivetin/api/v1/olivetin.proto
-
-package apiv1
-
-import (
-	context "context"
-	httpbody "google.golang.org/genproto/googleapis/api/httpbody"
-	grpc "google.golang.org/grpc"
-	codes "google.golang.org/grpc/codes"
-	status "google.golang.org/grpc/status"
-)
-
-// This is a compile-time assertion to ensure that this generated file
-// is compatible with the grpc package it is being compiled against.
-// Requires gRPC-Go v1.64.0 or later.
-const _ = grpc.SupportPackageIsVersion9
-
-const (
-	OliveTinApiService_GetDashboardComponents_FullMethodName  = "/olivetin.api.v1.OliveTinApiService/GetDashboardComponents"
-	OliveTinApiService_StartAction_FullMethodName             = "/olivetin.api.v1.OliveTinApiService/StartAction"
-	OliveTinApiService_StartActionAndWait_FullMethodName      = "/olivetin.api.v1.OliveTinApiService/StartActionAndWait"
-	OliveTinApiService_StartActionByGet_FullMethodName        = "/olivetin.api.v1.OliveTinApiService/StartActionByGet"
-	OliveTinApiService_StartActionByGetAndWait_FullMethodName = "/olivetin.api.v1.OliveTinApiService/StartActionByGetAndWait"
-	OliveTinApiService_KillAction_FullMethodName              = "/olivetin.api.v1.OliveTinApiService/KillAction"
-	OliveTinApiService_ExecutionStatus_FullMethodName         = "/olivetin.api.v1.OliveTinApiService/ExecutionStatus"
-	OliveTinApiService_GetLogs_FullMethodName                 = "/olivetin.api.v1.OliveTinApiService/GetLogs"
-	OliveTinApiService_ValidateArgumentType_FullMethodName    = "/olivetin.api.v1.OliveTinApiService/ValidateArgumentType"
-	OliveTinApiService_WhoAmI_FullMethodName                  = "/olivetin.api.v1.OliveTinApiService/WhoAmI"
-	OliveTinApiService_SosReport_FullMethodName               = "/olivetin.api.v1.OliveTinApiService/SosReport"
-	OliveTinApiService_DumpVars_FullMethodName                = "/olivetin.api.v1.OliveTinApiService/DumpVars"
-	OliveTinApiService_DumpPublicIdActionMap_FullMethodName   = "/olivetin.api.v1.OliveTinApiService/DumpPublicIdActionMap"
-	OliveTinApiService_GetReadyz_FullMethodName               = "/olivetin.api.v1.OliveTinApiService/GetReadyz"
-	OliveTinApiService_LocalUserLogin_FullMethodName          = "/olivetin.api.v1.OliveTinApiService/LocalUserLogin"
-	OliveTinApiService_PasswordHash_FullMethodName            = "/olivetin.api.v1.OliveTinApiService/PasswordHash"
-	OliveTinApiService_Logout_FullMethodName                  = "/olivetin.api.v1.OliveTinApiService/Logout"
-)
-
-// OliveTinApiServiceClient is the client API for OliveTinApiService service.
-//
-// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
-type OliveTinApiServiceClient interface {
-	GetDashboardComponents(ctx context.Context, in *GetDashboardComponentsRequest, opts ...grpc.CallOption) (*GetDashboardComponentsResponse, error)
-	StartAction(ctx context.Context, in *StartActionRequest, opts ...grpc.CallOption) (*StartActionResponse, error)
-	StartActionAndWait(ctx context.Context, in *StartActionAndWaitRequest, opts ...grpc.CallOption) (*StartActionAndWaitResponse, error)
-	StartActionByGet(ctx context.Context, in *StartActionByGetRequest, opts ...grpc.CallOption) (*StartActionByGetResponse, error)
-	StartActionByGetAndWait(ctx context.Context, in *StartActionByGetAndWaitRequest, opts ...grpc.CallOption) (*StartActionByGetAndWaitResponse, error)
-	KillAction(ctx context.Context, in *KillActionRequest, opts ...grpc.CallOption) (*KillActionResponse, error)
-	ExecutionStatus(ctx context.Context, in *ExecutionStatusRequest, opts ...grpc.CallOption) (*ExecutionStatusResponse, error)
-	GetLogs(ctx context.Context, in *GetLogsRequest, opts ...grpc.CallOption) (*GetLogsResponse, error)
-	ValidateArgumentType(ctx context.Context, in *ValidateArgumentTypeRequest, opts ...grpc.CallOption) (*ValidateArgumentTypeResponse, error)
-	WhoAmI(ctx context.Context, in *WhoAmIRequest, opts ...grpc.CallOption) (*WhoAmIResponse, error)
-	SosReport(ctx context.Context, in *SosReportRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
-	DumpVars(ctx context.Context, in *DumpVarsRequest, opts ...grpc.CallOption) (*DumpVarsResponse, error)
-	DumpPublicIdActionMap(ctx context.Context, in *DumpPublicIdActionMapRequest, opts ...grpc.CallOption) (*DumpPublicIdActionMapResponse, error)
-	GetReadyz(ctx context.Context, in *GetReadyzRequest, opts ...grpc.CallOption) (*GetReadyzResponse, error)
-	LocalUserLogin(ctx context.Context, in *LocalUserLoginRequest, opts ...grpc.CallOption) (*LocalUserLoginResponse, error)
-	PasswordHash(ctx context.Context, in *PasswordHashRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
-	Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
-}
-
-type oliveTinApiServiceClient struct {
-	cc grpc.ClientConnInterface
-}
-
-func NewOliveTinApiServiceClient(cc grpc.ClientConnInterface) OliveTinApiServiceClient {
-	return &oliveTinApiServiceClient{cc}
-}
-
-func (c *oliveTinApiServiceClient) GetDashboardComponents(ctx context.Context, in *GetDashboardComponentsRequest, opts ...grpc.CallOption) (*GetDashboardComponentsResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(GetDashboardComponentsResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_GetDashboardComponents_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) StartAction(ctx context.Context, in *StartActionRequest, opts ...grpc.CallOption) (*StartActionResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(StartActionResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_StartAction_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) StartActionAndWait(ctx context.Context, in *StartActionAndWaitRequest, opts ...grpc.CallOption) (*StartActionAndWaitResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(StartActionAndWaitResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_StartActionAndWait_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) StartActionByGet(ctx context.Context, in *StartActionByGetRequest, opts ...grpc.CallOption) (*StartActionByGetResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(StartActionByGetResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_StartActionByGet_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) StartActionByGetAndWait(ctx context.Context, in *StartActionByGetAndWaitRequest, opts ...grpc.CallOption) (*StartActionByGetAndWaitResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(StartActionByGetAndWaitResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_StartActionByGetAndWait_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) KillAction(ctx context.Context, in *KillActionRequest, opts ...grpc.CallOption) (*KillActionResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(KillActionResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_KillAction_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) ExecutionStatus(ctx context.Context, in *ExecutionStatusRequest, opts ...grpc.CallOption) (*ExecutionStatusResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(ExecutionStatusResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_ExecutionStatus_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) GetLogs(ctx context.Context, in *GetLogsRequest, opts ...grpc.CallOption) (*GetLogsResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(GetLogsResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_GetLogs_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) ValidateArgumentType(ctx context.Context, in *ValidateArgumentTypeRequest, opts ...grpc.CallOption) (*ValidateArgumentTypeResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(ValidateArgumentTypeResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_ValidateArgumentType_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) WhoAmI(ctx context.Context, in *WhoAmIRequest, opts ...grpc.CallOption) (*WhoAmIResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(WhoAmIResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_WhoAmI_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) SosReport(ctx context.Context, in *SosReportRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(httpbody.HttpBody)
-	err := c.cc.Invoke(ctx, OliveTinApiService_SosReport_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) DumpVars(ctx context.Context, in *DumpVarsRequest, opts ...grpc.CallOption) (*DumpVarsResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(DumpVarsResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_DumpVars_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) DumpPublicIdActionMap(ctx context.Context, in *DumpPublicIdActionMapRequest, opts ...grpc.CallOption) (*DumpPublicIdActionMapResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(DumpPublicIdActionMapResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_DumpPublicIdActionMap_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) GetReadyz(ctx context.Context, in *GetReadyzRequest, opts ...grpc.CallOption) (*GetReadyzResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(GetReadyzResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_GetReadyz_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) LocalUserLogin(ctx context.Context, in *LocalUserLoginRequest, opts ...grpc.CallOption) (*LocalUserLoginResponse, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(LocalUserLoginResponse)
-	err := c.cc.Invoke(ctx, OliveTinApiService_LocalUserLogin_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) PasswordHash(ctx context.Context, in *PasswordHashRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(httpbody.HttpBody)
-	err := c.cc.Invoke(ctx, OliveTinApiService_PasswordHash_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *oliveTinApiServiceClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) {
-	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
-	out := new(httpbody.HttpBody)
-	err := c.cc.Invoke(ctx, OliveTinApiService_Logout_FullMethodName, in, out, cOpts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-// OliveTinApiServiceServer is the server API for OliveTinApiService service.
-// All implementations should embed UnimplementedOliveTinApiServiceServer
-// for forward compatibility.
-type OliveTinApiServiceServer interface {
-	GetDashboardComponents(context.Context, *GetDashboardComponentsRequest) (*GetDashboardComponentsResponse, error)
-	StartAction(context.Context, *StartActionRequest) (*StartActionResponse, error)
-	StartActionAndWait(context.Context, *StartActionAndWaitRequest) (*StartActionAndWaitResponse, error)
-	StartActionByGet(context.Context, *StartActionByGetRequest) (*StartActionByGetResponse, error)
-	StartActionByGetAndWait(context.Context, *StartActionByGetAndWaitRequest) (*StartActionByGetAndWaitResponse, error)
-	KillAction(context.Context, *KillActionRequest) (*KillActionResponse, error)
-	ExecutionStatus(context.Context, *ExecutionStatusRequest) (*ExecutionStatusResponse, error)
-	GetLogs(context.Context, *GetLogsRequest) (*GetLogsResponse, error)
-	ValidateArgumentType(context.Context, *ValidateArgumentTypeRequest) (*ValidateArgumentTypeResponse, error)
-	WhoAmI(context.Context, *WhoAmIRequest) (*WhoAmIResponse, error)
-	SosReport(context.Context, *SosReportRequest) (*httpbody.HttpBody, error)
-	DumpVars(context.Context, *DumpVarsRequest) (*DumpVarsResponse, error)
-	DumpPublicIdActionMap(context.Context, *DumpPublicIdActionMapRequest) (*DumpPublicIdActionMapResponse, error)
-	GetReadyz(context.Context, *GetReadyzRequest) (*GetReadyzResponse, error)
-	LocalUserLogin(context.Context, *LocalUserLoginRequest) (*LocalUserLoginResponse, error)
-	PasswordHash(context.Context, *PasswordHashRequest) (*httpbody.HttpBody, error)
-	Logout(context.Context, *LogoutRequest) (*httpbody.HttpBody, error)
-}
-
-// UnimplementedOliveTinApiServiceServer should be embedded to have
-// forward compatible implementations.
-//
-// NOTE: this should be embedded by value instead of pointer to avoid a nil
-// pointer dereference when methods are called.
-type UnimplementedOliveTinApiServiceServer struct{}
-
-func (UnimplementedOliveTinApiServiceServer) GetDashboardComponents(context.Context, *GetDashboardComponentsRequest) (*GetDashboardComponentsResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method GetDashboardComponents not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) StartAction(context.Context, *StartActionRequest) (*StartActionResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method StartAction not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) StartActionAndWait(context.Context, *StartActionAndWaitRequest) (*StartActionAndWaitResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method StartActionAndWait not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) StartActionByGet(context.Context, *StartActionByGetRequest) (*StartActionByGetResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method StartActionByGet not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) StartActionByGetAndWait(context.Context, *StartActionByGetAndWaitRequest) (*StartActionByGetAndWaitResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method StartActionByGetAndWait not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) KillAction(context.Context, *KillActionRequest) (*KillActionResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method KillAction not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) ExecutionStatus(context.Context, *ExecutionStatusRequest) (*ExecutionStatusResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method ExecutionStatus not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) GetLogs(context.Context, *GetLogsRequest) (*GetLogsResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method GetLogs not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) ValidateArgumentType(context.Context, *ValidateArgumentTypeRequest) (*ValidateArgumentTypeResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method ValidateArgumentType not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) WhoAmI(context.Context, *WhoAmIRequest) (*WhoAmIResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method WhoAmI not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) SosReport(context.Context, *SosReportRequest) (*httpbody.HttpBody, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method SosReport not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) DumpVars(context.Context, *DumpVarsRequest) (*DumpVarsResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method DumpVars not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) DumpPublicIdActionMap(context.Context, *DumpPublicIdActionMapRequest) (*DumpPublicIdActionMapResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method DumpPublicIdActionMap not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) GetReadyz(context.Context, *GetReadyzRequest) (*GetReadyzResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method GetReadyz not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) LocalUserLogin(context.Context, *LocalUserLoginRequest) (*LocalUserLoginResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method LocalUserLogin not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) PasswordHash(context.Context, *PasswordHashRequest) (*httpbody.HttpBody, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method PasswordHash not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) Logout(context.Context, *LogoutRequest) (*httpbody.HttpBody, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented")
-}
-func (UnimplementedOliveTinApiServiceServer) testEmbeddedByValue() {}
-
-// UnsafeOliveTinApiServiceServer may be embedded to opt out of forward compatibility for this service.
-// Use of this interface is not recommended, as added methods to OliveTinApiServiceServer will
-// result in compilation errors.
-type UnsafeOliveTinApiServiceServer interface {
-	mustEmbedUnimplementedOliveTinApiServiceServer()
-}
-
-func RegisterOliveTinApiServiceServer(s grpc.ServiceRegistrar, srv OliveTinApiServiceServer) {
-	// If the following call pancis, it indicates UnimplementedOliveTinApiServiceServer was
-	// embedded by pointer and is nil.  This will cause panics if an
-	// unimplemented method is ever invoked, so we test this at initialization
-	// time to prevent it from happening at runtime later due to I/O.
-	if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
-		t.testEmbeddedByValue()
-	}
-	s.RegisterService(&OliveTinApiService_ServiceDesc, srv)
-}
-
-func _OliveTinApiService_GetDashboardComponents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(GetDashboardComponentsRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).GetDashboardComponents(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_GetDashboardComponents_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).GetDashboardComponents(ctx, req.(*GetDashboardComponentsRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_StartAction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(StartActionRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).StartAction(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_StartAction_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).StartAction(ctx, req.(*StartActionRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_StartActionAndWait_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(StartActionAndWaitRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).StartActionAndWait(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_StartActionAndWait_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).StartActionAndWait(ctx, req.(*StartActionAndWaitRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_StartActionByGet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(StartActionByGetRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).StartActionByGet(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_StartActionByGet_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).StartActionByGet(ctx, req.(*StartActionByGetRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_StartActionByGetAndWait_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(StartActionByGetAndWaitRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).StartActionByGetAndWait(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_StartActionByGetAndWait_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).StartActionByGetAndWait(ctx, req.(*StartActionByGetAndWaitRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_KillAction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(KillActionRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).KillAction(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_KillAction_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).KillAction(ctx, req.(*KillActionRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_ExecutionStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(ExecutionStatusRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).ExecutionStatus(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_ExecutionStatus_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).ExecutionStatus(ctx, req.(*ExecutionStatusRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_GetLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(GetLogsRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).GetLogs(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_GetLogs_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).GetLogs(ctx, req.(*GetLogsRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_ValidateArgumentType_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(ValidateArgumentTypeRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).ValidateArgumentType(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_ValidateArgumentType_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).ValidateArgumentType(ctx, req.(*ValidateArgumentTypeRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_WhoAmI_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(WhoAmIRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).WhoAmI(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_WhoAmI_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).WhoAmI(ctx, req.(*WhoAmIRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_SosReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(SosReportRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).SosReport(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_SosReport_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).SosReport(ctx, req.(*SosReportRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_DumpVars_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(DumpVarsRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).DumpVars(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_DumpVars_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).DumpVars(ctx, req.(*DumpVarsRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_DumpPublicIdActionMap_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(DumpPublicIdActionMapRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).DumpPublicIdActionMap(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_DumpPublicIdActionMap_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).DumpPublicIdActionMap(ctx, req.(*DumpPublicIdActionMapRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_GetReadyz_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(GetReadyzRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).GetReadyz(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_GetReadyz_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).GetReadyz(ctx, req.(*GetReadyzRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_LocalUserLogin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(LocalUserLoginRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).LocalUserLogin(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_LocalUserLogin_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).LocalUserLogin(ctx, req.(*LocalUserLoginRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_PasswordHash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(PasswordHashRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).PasswordHash(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_PasswordHash_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).PasswordHash(ctx, req.(*PasswordHashRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _OliveTinApiService_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(LogoutRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(OliveTinApiServiceServer).Logout(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: OliveTinApiService_Logout_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(OliveTinApiServiceServer).Logout(ctx, req.(*LogoutRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-// OliveTinApiService_ServiceDesc is the grpc.ServiceDesc for OliveTinApiService service.
-// It's only intended for direct use with grpc.RegisterService,
-// and not to be introspected or modified (even as a copy)
-var OliveTinApiService_ServiceDesc = grpc.ServiceDesc{
-	ServiceName: "olivetin.api.v1.OliveTinApiService",
-	HandlerType: (*OliveTinApiServiceServer)(nil),
-	Methods: []grpc.MethodDesc{
-		{
-			MethodName: "GetDashboardComponents",
-			Handler:    _OliveTinApiService_GetDashboardComponents_Handler,
-		},
-		{
-			MethodName: "StartAction",
-			Handler:    _OliveTinApiService_StartAction_Handler,
-		},
-		{
-			MethodName: "StartActionAndWait",
-			Handler:    _OliveTinApiService_StartActionAndWait_Handler,
-		},
-		{
-			MethodName: "StartActionByGet",
-			Handler:    _OliveTinApiService_StartActionByGet_Handler,
-		},
-		{
-			MethodName: "StartActionByGetAndWait",
-			Handler:    _OliveTinApiService_StartActionByGetAndWait_Handler,
-		},
-		{
-			MethodName: "KillAction",
-			Handler:    _OliveTinApiService_KillAction_Handler,
-		},
-		{
-			MethodName: "ExecutionStatus",
-			Handler:    _OliveTinApiService_ExecutionStatus_Handler,
-		},
-		{
-			MethodName: "GetLogs",
-			Handler:    _OliveTinApiService_GetLogs_Handler,
-		},
-		{
-			MethodName: "ValidateArgumentType",
-			Handler:    _OliveTinApiService_ValidateArgumentType_Handler,
-		},
-		{
-			MethodName: "WhoAmI",
-			Handler:    _OliveTinApiService_WhoAmI_Handler,
-		},
-		{
-			MethodName: "SosReport",
-			Handler:    _OliveTinApiService_SosReport_Handler,
-		},
-		{
-			MethodName: "DumpVars",
-			Handler:    _OliveTinApiService_DumpVars_Handler,
-		},
-		{
-			MethodName: "DumpPublicIdActionMap",
-			Handler:    _OliveTinApiService_DumpPublicIdActionMap_Handler,
-		},
-		{
-			MethodName: "GetReadyz",
-			Handler:    _OliveTinApiService_GetReadyz_Handler,
-		},
-		{
-			MethodName: "LocalUserLogin",
-			Handler:    _OliveTinApiService_LocalUserLogin_Handler,
-		},
-		{
-			MethodName: "PasswordHash",
-			Handler:    _OliveTinApiService_PasswordHash_Handler,
-		},
-		{
-			MethodName: "Logout",
-			Handler:    _OliveTinApiService_Logout_Handler,
-		},
-	},
-	Streams:  []grpc.StreamDesc{},
-	Metadata: "olivetin/api/v1/olivetin.proto",
-}

+ 631 - 0
service/gen/olivetin/api/v1/apiv1connect/olivetin.connect.go

@@ -0,0 +1,631 @@
+// Code generated by protoc-gen-connect-go. DO NOT EDIT.
+//
+// Source: olivetin/api/v1/olivetin.proto
+
+package apiv1connect
+
+import (
+	connect "connectrpc.com/connect"
+	context "context"
+	errors "errors"
+	v1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
+	http "net/http"
+	strings "strings"
+)
+
+// This is a compile-time assertion to ensure that this generated file and the connect package are
+// compatible. If you get a compiler error that this constant is not defined, this code was
+// generated with a version of connect newer than the one compiled into your binary. You can fix the
+// problem by either regenerating this code with an older version of connect or updating the connect
+// version compiled into your binary.
+const _ = connect.IsAtLeastVersion1_13_0
+
+const (
+	// OliveTinApiServiceName is the fully-qualified name of the OliveTinApiService service.
+	OliveTinApiServiceName = "olivetin.api.v1.OliveTinApiService"
+)
+
+// These constants are the fully-qualified names of the RPCs defined in this package. They're
+// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
+//
+// Note that these are different from the fully-qualified method names used by
+// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
+// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
+// period.
+const (
+	// OliveTinApiServiceGetDashboardComponentsProcedure is the fully-qualified name of the
+	// OliveTinApiService's GetDashboardComponents RPC.
+	OliveTinApiServiceGetDashboardComponentsProcedure = "/olivetin.api.v1.OliveTinApiService/GetDashboardComponents"
+	// OliveTinApiServiceStartActionProcedure is the fully-qualified name of the OliveTinApiService's
+	// StartAction RPC.
+	OliveTinApiServiceStartActionProcedure = "/olivetin.api.v1.OliveTinApiService/StartAction"
+	// OliveTinApiServiceStartActionAndWaitProcedure is the fully-qualified name of the
+	// OliveTinApiService's StartActionAndWait RPC.
+	OliveTinApiServiceStartActionAndWaitProcedure = "/olivetin.api.v1.OliveTinApiService/StartActionAndWait"
+	// OliveTinApiServiceStartActionByGetProcedure is the fully-qualified name of the
+	// OliveTinApiService's StartActionByGet RPC.
+	OliveTinApiServiceStartActionByGetProcedure = "/olivetin.api.v1.OliveTinApiService/StartActionByGet"
+	// OliveTinApiServiceStartActionByGetAndWaitProcedure is the fully-qualified name of the
+	// OliveTinApiService's StartActionByGetAndWait RPC.
+	OliveTinApiServiceStartActionByGetAndWaitProcedure = "/olivetin.api.v1.OliveTinApiService/StartActionByGetAndWait"
+	// OliveTinApiServiceKillActionProcedure is the fully-qualified name of the OliveTinApiService's
+	// KillAction RPC.
+	OliveTinApiServiceKillActionProcedure = "/olivetin.api.v1.OliveTinApiService/KillAction"
+	// OliveTinApiServiceExecutionStatusProcedure is the fully-qualified name of the
+	// OliveTinApiService's ExecutionStatus RPC.
+	OliveTinApiServiceExecutionStatusProcedure = "/olivetin.api.v1.OliveTinApiService/ExecutionStatus"
+	// OliveTinApiServiceGetLogsProcedure is the fully-qualified name of the OliveTinApiService's
+	// GetLogs RPC.
+	OliveTinApiServiceGetLogsProcedure = "/olivetin.api.v1.OliveTinApiService/GetLogs"
+	// OliveTinApiServiceValidateArgumentTypeProcedure is the fully-qualified name of the
+	// OliveTinApiService's ValidateArgumentType RPC.
+	OliveTinApiServiceValidateArgumentTypeProcedure = "/olivetin.api.v1.OliveTinApiService/ValidateArgumentType"
+	// OliveTinApiServiceWhoAmIProcedure is the fully-qualified name of the OliveTinApiService's WhoAmI
+	// RPC.
+	OliveTinApiServiceWhoAmIProcedure = "/olivetin.api.v1.OliveTinApiService/WhoAmI"
+	// OliveTinApiServiceSosReportProcedure is the fully-qualified name of the OliveTinApiService's
+	// SosReport RPC.
+	OliveTinApiServiceSosReportProcedure = "/olivetin.api.v1.OliveTinApiService/SosReport"
+	// OliveTinApiServiceDumpVarsProcedure is the fully-qualified name of the OliveTinApiService's
+	// DumpVars RPC.
+	OliveTinApiServiceDumpVarsProcedure = "/olivetin.api.v1.OliveTinApiService/DumpVars"
+	// OliveTinApiServiceDumpPublicIdActionMapProcedure is the fully-qualified name of the
+	// OliveTinApiService's DumpPublicIdActionMap RPC.
+	OliveTinApiServiceDumpPublicIdActionMapProcedure = "/olivetin.api.v1.OliveTinApiService/DumpPublicIdActionMap"
+	// OliveTinApiServiceGetReadyzProcedure is the fully-qualified name of the OliveTinApiService's
+	// GetReadyz RPC.
+	OliveTinApiServiceGetReadyzProcedure = "/olivetin.api.v1.OliveTinApiService/GetReadyz"
+	// OliveTinApiServiceLocalUserLoginProcedure is the fully-qualified name of the OliveTinApiService's
+	// LocalUserLogin RPC.
+	OliveTinApiServiceLocalUserLoginProcedure = "/olivetin.api.v1.OliveTinApiService/LocalUserLogin"
+	// OliveTinApiServicePasswordHashProcedure is the fully-qualified name of the OliveTinApiService's
+	// PasswordHash RPC.
+	OliveTinApiServicePasswordHashProcedure = "/olivetin.api.v1.OliveTinApiService/PasswordHash"
+	// OliveTinApiServiceLogoutProcedure is the fully-qualified name of the OliveTinApiService's Logout
+	// RPC.
+	OliveTinApiServiceLogoutProcedure = "/olivetin.api.v1.OliveTinApiService/Logout"
+	// OliveTinApiServiceEventStreamProcedure is the fully-qualified name of the OliveTinApiService's
+	// EventStream RPC.
+	OliveTinApiServiceEventStreamProcedure = "/olivetin.api.v1.OliveTinApiService/EventStream"
+	// OliveTinApiServiceGetDiagnosticsProcedure is the fully-qualified name of the OliveTinApiService's
+	// GetDiagnostics RPC.
+	OliveTinApiServiceGetDiagnosticsProcedure = "/olivetin.api.v1.OliveTinApiService/GetDiagnostics"
+)
+
+// OliveTinApiServiceClient is a client for the olivetin.api.v1.OliveTinApiService service.
+type OliveTinApiServiceClient interface {
+	GetDashboardComponents(context.Context, *connect.Request[v1.GetDashboardComponentsRequest]) (*connect.Response[v1.GetDashboardComponentsResponse], error)
+	StartAction(context.Context, *connect.Request[v1.StartActionRequest]) (*connect.Response[v1.StartActionResponse], error)
+	StartActionAndWait(context.Context, *connect.Request[v1.StartActionAndWaitRequest]) (*connect.Response[v1.StartActionAndWaitResponse], error)
+	StartActionByGet(context.Context, *connect.Request[v1.StartActionByGetRequest]) (*connect.Response[v1.StartActionByGetResponse], error)
+	StartActionByGetAndWait(context.Context, *connect.Request[v1.StartActionByGetAndWaitRequest]) (*connect.Response[v1.StartActionByGetAndWaitResponse], error)
+	KillAction(context.Context, *connect.Request[v1.KillActionRequest]) (*connect.Response[v1.KillActionResponse], error)
+	ExecutionStatus(context.Context, *connect.Request[v1.ExecutionStatusRequest]) (*connect.Response[v1.ExecutionStatusResponse], error)
+	GetLogs(context.Context, *connect.Request[v1.GetLogsRequest]) (*connect.Response[v1.GetLogsResponse], error)
+	ValidateArgumentType(context.Context, *connect.Request[v1.ValidateArgumentTypeRequest]) (*connect.Response[v1.ValidateArgumentTypeResponse], error)
+	WhoAmI(context.Context, *connect.Request[v1.WhoAmIRequest]) (*connect.Response[v1.WhoAmIResponse], error)
+	SosReport(context.Context, *connect.Request[v1.SosReportRequest]) (*connect.Response[v1.SosReportResponse], error)
+	DumpVars(context.Context, *connect.Request[v1.DumpVarsRequest]) (*connect.Response[v1.DumpVarsResponse], error)
+	DumpPublicIdActionMap(context.Context, *connect.Request[v1.DumpPublicIdActionMapRequest]) (*connect.Response[v1.DumpPublicIdActionMapResponse], error)
+	GetReadyz(context.Context, *connect.Request[v1.GetReadyzRequest]) (*connect.Response[v1.GetReadyzResponse], error)
+	LocalUserLogin(context.Context, *connect.Request[v1.LocalUserLoginRequest]) (*connect.Response[v1.LocalUserLoginResponse], error)
+	PasswordHash(context.Context, *connect.Request[v1.PasswordHashRequest]) (*connect.Response[v1.PasswordHashResponse], error)
+	Logout(context.Context, *connect.Request[v1.LogoutRequest]) (*connect.Response[v1.LogoutResponse], error)
+	EventStream(context.Context, *connect.Request[v1.EventStreamRequest]) (*connect.ServerStreamForClient[v1.EventStreamResponse], error)
+	GetDiagnostics(context.Context, *connect.Request[v1.GetDiagnosticsRequest]) (*connect.Response[v1.GetDiagnosticsResponse], error)
+}
+
+// NewOliveTinApiServiceClient constructs a client for the olivetin.api.v1.OliveTinApiService
+// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
+// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
+// the connect.WithGRPC() or connect.WithGRPCWeb() options.
+//
+// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
+// http://api.acme.com or https://acme.com/grpc).
+func NewOliveTinApiServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) OliveTinApiServiceClient {
+	baseURL = strings.TrimRight(baseURL, "/")
+	oliveTinApiServiceMethods := v1.File_olivetin_api_v1_olivetin_proto.Services().ByName("OliveTinApiService").Methods()
+	return &oliveTinApiServiceClient{
+		getDashboardComponents: connect.NewClient[v1.GetDashboardComponentsRequest, v1.GetDashboardComponentsResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceGetDashboardComponentsProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("GetDashboardComponents")),
+			connect.WithClientOptions(opts...),
+		),
+		startAction: connect.NewClient[v1.StartActionRequest, v1.StartActionResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceStartActionProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("StartAction")),
+			connect.WithClientOptions(opts...),
+		),
+		startActionAndWait: connect.NewClient[v1.StartActionAndWaitRequest, v1.StartActionAndWaitResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceStartActionAndWaitProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("StartActionAndWait")),
+			connect.WithClientOptions(opts...),
+		),
+		startActionByGet: connect.NewClient[v1.StartActionByGetRequest, v1.StartActionByGetResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceStartActionByGetProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("StartActionByGet")),
+			connect.WithClientOptions(opts...),
+		),
+		startActionByGetAndWait: connect.NewClient[v1.StartActionByGetAndWaitRequest, v1.StartActionByGetAndWaitResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceStartActionByGetAndWaitProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("StartActionByGetAndWait")),
+			connect.WithClientOptions(opts...),
+		),
+		killAction: connect.NewClient[v1.KillActionRequest, v1.KillActionResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceKillActionProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("KillAction")),
+			connect.WithClientOptions(opts...),
+		),
+		executionStatus: connect.NewClient[v1.ExecutionStatusRequest, v1.ExecutionStatusResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceExecutionStatusProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("ExecutionStatus")),
+			connect.WithClientOptions(opts...),
+		),
+		getLogs: connect.NewClient[v1.GetLogsRequest, v1.GetLogsResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceGetLogsProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("GetLogs")),
+			connect.WithClientOptions(opts...),
+		),
+		validateArgumentType: connect.NewClient[v1.ValidateArgumentTypeRequest, v1.ValidateArgumentTypeResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceValidateArgumentTypeProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("ValidateArgumentType")),
+			connect.WithClientOptions(opts...),
+		),
+		whoAmI: connect.NewClient[v1.WhoAmIRequest, v1.WhoAmIResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceWhoAmIProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("WhoAmI")),
+			connect.WithClientOptions(opts...),
+		),
+		sosReport: connect.NewClient[v1.SosReportRequest, v1.SosReportResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceSosReportProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("SosReport")),
+			connect.WithClientOptions(opts...),
+		),
+		dumpVars: connect.NewClient[v1.DumpVarsRequest, v1.DumpVarsResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceDumpVarsProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("DumpVars")),
+			connect.WithClientOptions(opts...),
+		),
+		dumpPublicIdActionMap: connect.NewClient[v1.DumpPublicIdActionMapRequest, v1.DumpPublicIdActionMapResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceDumpPublicIdActionMapProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("DumpPublicIdActionMap")),
+			connect.WithClientOptions(opts...),
+		),
+		getReadyz: connect.NewClient[v1.GetReadyzRequest, v1.GetReadyzResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceGetReadyzProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("GetReadyz")),
+			connect.WithClientOptions(opts...),
+		),
+		localUserLogin: connect.NewClient[v1.LocalUserLoginRequest, v1.LocalUserLoginResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceLocalUserLoginProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("LocalUserLogin")),
+			connect.WithClientOptions(opts...),
+		),
+		passwordHash: connect.NewClient[v1.PasswordHashRequest, v1.PasswordHashResponse](
+			httpClient,
+			baseURL+OliveTinApiServicePasswordHashProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("PasswordHash")),
+			connect.WithClientOptions(opts...),
+		),
+		logout: connect.NewClient[v1.LogoutRequest, v1.LogoutResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceLogoutProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("Logout")),
+			connect.WithClientOptions(opts...),
+		),
+		eventStream: connect.NewClient[v1.EventStreamRequest, v1.EventStreamResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceEventStreamProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("EventStream")),
+			connect.WithClientOptions(opts...),
+		),
+		getDiagnostics: connect.NewClient[v1.GetDiagnosticsRequest, v1.GetDiagnosticsResponse](
+			httpClient,
+			baseURL+OliveTinApiServiceGetDiagnosticsProcedure,
+			connect.WithSchema(oliveTinApiServiceMethods.ByName("GetDiagnostics")),
+			connect.WithClientOptions(opts...),
+		),
+	}
+}
+
+// oliveTinApiServiceClient implements OliveTinApiServiceClient.
+type oliveTinApiServiceClient struct {
+	getDashboardComponents  *connect.Client[v1.GetDashboardComponentsRequest, v1.GetDashboardComponentsResponse]
+	startAction             *connect.Client[v1.StartActionRequest, v1.StartActionResponse]
+	startActionAndWait      *connect.Client[v1.StartActionAndWaitRequest, v1.StartActionAndWaitResponse]
+	startActionByGet        *connect.Client[v1.StartActionByGetRequest, v1.StartActionByGetResponse]
+	startActionByGetAndWait *connect.Client[v1.StartActionByGetAndWaitRequest, v1.StartActionByGetAndWaitResponse]
+	killAction              *connect.Client[v1.KillActionRequest, v1.KillActionResponse]
+	executionStatus         *connect.Client[v1.ExecutionStatusRequest, v1.ExecutionStatusResponse]
+	getLogs                 *connect.Client[v1.GetLogsRequest, v1.GetLogsResponse]
+	validateArgumentType    *connect.Client[v1.ValidateArgumentTypeRequest, v1.ValidateArgumentTypeResponse]
+	whoAmI                  *connect.Client[v1.WhoAmIRequest, v1.WhoAmIResponse]
+	sosReport               *connect.Client[v1.SosReportRequest, v1.SosReportResponse]
+	dumpVars                *connect.Client[v1.DumpVarsRequest, v1.DumpVarsResponse]
+	dumpPublicIdActionMap   *connect.Client[v1.DumpPublicIdActionMapRequest, v1.DumpPublicIdActionMapResponse]
+	getReadyz               *connect.Client[v1.GetReadyzRequest, v1.GetReadyzResponse]
+	localUserLogin          *connect.Client[v1.LocalUserLoginRequest, v1.LocalUserLoginResponse]
+	passwordHash            *connect.Client[v1.PasswordHashRequest, v1.PasswordHashResponse]
+	logout                  *connect.Client[v1.LogoutRequest, v1.LogoutResponse]
+	eventStream             *connect.Client[v1.EventStreamRequest, v1.EventStreamResponse]
+	getDiagnostics          *connect.Client[v1.GetDiagnosticsRequest, v1.GetDiagnosticsResponse]
+}
+
+// GetDashboardComponents calls olivetin.api.v1.OliveTinApiService.GetDashboardComponents.
+func (c *oliveTinApiServiceClient) GetDashboardComponents(ctx context.Context, req *connect.Request[v1.GetDashboardComponentsRequest]) (*connect.Response[v1.GetDashboardComponentsResponse], error) {
+	return c.getDashboardComponents.CallUnary(ctx, req)
+}
+
+// StartAction calls olivetin.api.v1.OliveTinApiService.StartAction.
+func (c *oliveTinApiServiceClient) StartAction(ctx context.Context, req *connect.Request[v1.StartActionRequest]) (*connect.Response[v1.StartActionResponse], error) {
+	return c.startAction.CallUnary(ctx, req)
+}
+
+// StartActionAndWait calls olivetin.api.v1.OliveTinApiService.StartActionAndWait.
+func (c *oliveTinApiServiceClient) StartActionAndWait(ctx context.Context, req *connect.Request[v1.StartActionAndWaitRequest]) (*connect.Response[v1.StartActionAndWaitResponse], error) {
+	return c.startActionAndWait.CallUnary(ctx, req)
+}
+
+// StartActionByGet calls olivetin.api.v1.OliveTinApiService.StartActionByGet.
+func (c *oliveTinApiServiceClient) StartActionByGet(ctx context.Context, req *connect.Request[v1.StartActionByGetRequest]) (*connect.Response[v1.StartActionByGetResponse], error) {
+	return c.startActionByGet.CallUnary(ctx, req)
+}
+
+// StartActionByGetAndWait calls olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait.
+func (c *oliveTinApiServiceClient) StartActionByGetAndWait(ctx context.Context, req *connect.Request[v1.StartActionByGetAndWaitRequest]) (*connect.Response[v1.StartActionByGetAndWaitResponse], error) {
+	return c.startActionByGetAndWait.CallUnary(ctx, req)
+}
+
+// KillAction calls olivetin.api.v1.OliveTinApiService.KillAction.
+func (c *oliveTinApiServiceClient) KillAction(ctx context.Context, req *connect.Request[v1.KillActionRequest]) (*connect.Response[v1.KillActionResponse], error) {
+	return c.killAction.CallUnary(ctx, req)
+}
+
+// ExecutionStatus calls olivetin.api.v1.OliveTinApiService.ExecutionStatus.
+func (c *oliveTinApiServiceClient) ExecutionStatus(ctx context.Context, req *connect.Request[v1.ExecutionStatusRequest]) (*connect.Response[v1.ExecutionStatusResponse], error) {
+	return c.executionStatus.CallUnary(ctx, req)
+}
+
+// GetLogs calls olivetin.api.v1.OliveTinApiService.GetLogs.
+func (c *oliveTinApiServiceClient) GetLogs(ctx context.Context, req *connect.Request[v1.GetLogsRequest]) (*connect.Response[v1.GetLogsResponse], error) {
+	return c.getLogs.CallUnary(ctx, req)
+}
+
+// ValidateArgumentType calls olivetin.api.v1.OliveTinApiService.ValidateArgumentType.
+func (c *oliveTinApiServiceClient) ValidateArgumentType(ctx context.Context, req *connect.Request[v1.ValidateArgumentTypeRequest]) (*connect.Response[v1.ValidateArgumentTypeResponse], error) {
+	return c.validateArgumentType.CallUnary(ctx, req)
+}
+
+// WhoAmI calls olivetin.api.v1.OliveTinApiService.WhoAmI.
+func (c *oliveTinApiServiceClient) WhoAmI(ctx context.Context, req *connect.Request[v1.WhoAmIRequest]) (*connect.Response[v1.WhoAmIResponse], error) {
+	return c.whoAmI.CallUnary(ctx, req)
+}
+
+// SosReport calls olivetin.api.v1.OliveTinApiService.SosReport.
+func (c *oliveTinApiServiceClient) SosReport(ctx context.Context, req *connect.Request[v1.SosReportRequest]) (*connect.Response[v1.SosReportResponse], error) {
+	return c.sosReport.CallUnary(ctx, req)
+}
+
+// DumpVars calls olivetin.api.v1.OliveTinApiService.DumpVars.
+func (c *oliveTinApiServiceClient) DumpVars(ctx context.Context, req *connect.Request[v1.DumpVarsRequest]) (*connect.Response[v1.DumpVarsResponse], error) {
+	return c.dumpVars.CallUnary(ctx, req)
+}
+
+// DumpPublicIdActionMap calls olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap.
+func (c *oliveTinApiServiceClient) DumpPublicIdActionMap(ctx context.Context, req *connect.Request[v1.DumpPublicIdActionMapRequest]) (*connect.Response[v1.DumpPublicIdActionMapResponse], error) {
+	return c.dumpPublicIdActionMap.CallUnary(ctx, req)
+}
+
+// GetReadyz calls olivetin.api.v1.OliveTinApiService.GetReadyz.
+func (c *oliveTinApiServiceClient) GetReadyz(ctx context.Context, req *connect.Request[v1.GetReadyzRequest]) (*connect.Response[v1.GetReadyzResponse], error) {
+	return c.getReadyz.CallUnary(ctx, req)
+}
+
+// LocalUserLogin calls olivetin.api.v1.OliveTinApiService.LocalUserLogin.
+func (c *oliveTinApiServiceClient) LocalUserLogin(ctx context.Context, req *connect.Request[v1.LocalUserLoginRequest]) (*connect.Response[v1.LocalUserLoginResponse], error) {
+	return c.localUserLogin.CallUnary(ctx, req)
+}
+
+// PasswordHash calls olivetin.api.v1.OliveTinApiService.PasswordHash.
+func (c *oliveTinApiServiceClient) PasswordHash(ctx context.Context, req *connect.Request[v1.PasswordHashRequest]) (*connect.Response[v1.PasswordHashResponse], error) {
+	return c.passwordHash.CallUnary(ctx, req)
+}
+
+// Logout calls olivetin.api.v1.OliveTinApiService.Logout.
+func (c *oliveTinApiServiceClient) Logout(ctx context.Context, req *connect.Request[v1.LogoutRequest]) (*connect.Response[v1.LogoutResponse], error) {
+	return c.logout.CallUnary(ctx, req)
+}
+
+// EventStream calls olivetin.api.v1.OliveTinApiService.EventStream.
+func (c *oliveTinApiServiceClient) EventStream(ctx context.Context, req *connect.Request[v1.EventStreamRequest]) (*connect.ServerStreamForClient[v1.EventStreamResponse], error) {
+	return c.eventStream.CallServerStream(ctx, req)
+}
+
+// GetDiagnostics calls olivetin.api.v1.OliveTinApiService.GetDiagnostics.
+func (c *oliveTinApiServiceClient) GetDiagnostics(ctx context.Context, req *connect.Request[v1.GetDiagnosticsRequest]) (*connect.Response[v1.GetDiagnosticsResponse], error) {
+	return c.getDiagnostics.CallUnary(ctx, req)
+}
+
+// OliveTinApiServiceHandler is an implementation of the olivetin.api.v1.OliveTinApiService service.
+type OliveTinApiServiceHandler interface {
+	GetDashboardComponents(context.Context, *connect.Request[v1.GetDashboardComponentsRequest]) (*connect.Response[v1.GetDashboardComponentsResponse], error)
+	StartAction(context.Context, *connect.Request[v1.StartActionRequest]) (*connect.Response[v1.StartActionResponse], error)
+	StartActionAndWait(context.Context, *connect.Request[v1.StartActionAndWaitRequest]) (*connect.Response[v1.StartActionAndWaitResponse], error)
+	StartActionByGet(context.Context, *connect.Request[v1.StartActionByGetRequest]) (*connect.Response[v1.StartActionByGetResponse], error)
+	StartActionByGetAndWait(context.Context, *connect.Request[v1.StartActionByGetAndWaitRequest]) (*connect.Response[v1.StartActionByGetAndWaitResponse], error)
+	KillAction(context.Context, *connect.Request[v1.KillActionRequest]) (*connect.Response[v1.KillActionResponse], error)
+	ExecutionStatus(context.Context, *connect.Request[v1.ExecutionStatusRequest]) (*connect.Response[v1.ExecutionStatusResponse], error)
+	GetLogs(context.Context, *connect.Request[v1.GetLogsRequest]) (*connect.Response[v1.GetLogsResponse], error)
+	ValidateArgumentType(context.Context, *connect.Request[v1.ValidateArgumentTypeRequest]) (*connect.Response[v1.ValidateArgumentTypeResponse], error)
+	WhoAmI(context.Context, *connect.Request[v1.WhoAmIRequest]) (*connect.Response[v1.WhoAmIResponse], error)
+	SosReport(context.Context, *connect.Request[v1.SosReportRequest]) (*connect.Response[v1.SosReportResponse], error)
+	DumpVars(context.Context, *connect.Request[v1.DumpVarsRequest]) (*connect.Response[v1.DumpVarsResponse], error)
+	DumpPublicIdActionMap(context.Context, *connect.Request[v1.DumpPublicIdActionMapRequest]) (*connect.Response[v1.DumpPublicIdActionMapResponse], error)
+	GetReadyz(context.Context, *connect.Request[v1.GetReadyzRequest]) (*connect.Response[v1.GetReadyzResponse], error)
+	LocalUserLogin(context.Context, *connect.Request[v1.LocalUserLoginRequest]) (*connect.Response[v1.LocalUserLoginResponse], error)
+	PasswordHash(context.Context, *connect.Request[v1.PasswordHashRequest]) (*connect.Response[v1.PasswordHashResponse], error)
+	Logout(context.Context, *connect.Request[v1.LogoutRequest]) (*connect.Response[v1.LogoutResponse], error)
+	EventStream(context.Context, *connect.Request[v1.EventStreamRequest], *connect.ServerStream[v1.EventStreamResponse]) error
+	GetDiagnostics(context.Context, *connect.Request[v1.GetDiagnosticsRequest]) (*connect.Response[v1.GetDiagnosticsResponse], error)
+}
+
+// NewOliveTinApiServiceHandler builds an HTTP handler from the service implementation. It returns
+// the path on which to mount the handler and the handler itself.
+//
+// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
+// and JSON codecs. They also support gzip compression.
+func NewOliveTinApiServiceHandler(svc OliveTinApiServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
+	oliveTinApiServiceMethods := v1.File_olivetin_api_v1_olivetin_proto.Services().ByName("OliveTinApiService").Methods()
+	oliveTinApiServiceGetDashboardComponentsHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceGetDashboardComponentsProcedure,
+		svc.GetDashboardComponents,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("GetDashboardComponents")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceStartActionHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceStartActionProcedure,
+		svc.StartAction,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("StartAction")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceStartActionAndWaitHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceStartActionAndWaitProcedure,
+		svc.StartActionAndWait,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("StartActionAndWait")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceStartActionByGetHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceStartActionByGetProcedure,
+		svc.StartActionByGet,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("StartActionByGet")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceStartActionByGetAndWaitHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceStartActionByGetAndWaitProcedure,
+		svc.StartActionByGetAndWait,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("StartActionByGetAndWait")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceKillActionHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceKillActionProcedure,
+		svc.KillAction,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("KillAction")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceExecutionStatusHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceExecutionStatusProcedure,
+		svc.ExecutionStatus,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("ExecutionStatus")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceGetLogsHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceGetLogsProcedure,
+		svc.GetLogs,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("GetLogs")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceValidateArgumentTypeHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceValidateArgumentTypeProcedure,
+		svc.ValidateArgumentType,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("ValidateArgumentType")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceWhoAmIHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceWhoAmIProcedure,
+		svc.WhoAmI,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("WhoAmI")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceSosReportHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceSosReportProcedure,
+		svc.SosReport,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("SosReport")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceDumpVarsHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceDumpVarsProcedure,
+		svc.DumpVars,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("DumpVars")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceDumpPublicIdActionMapHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceDumpPublicIdActionMapProcedure,
+		svc.DumpPublicIdActionMap,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("DumpPublicIdActionMap")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceGetReadyzHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceGetReadyzProcedure,
+		svc.GetReadyz,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("GetReadyz")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceLocalUserLoginHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceLocalUserLoginProcedure,
+		svc.LocalUserLogin,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("LocalUserLogin")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServicePasswordHashHandler := connect.NewUnaryHandler(
+		OliveTinApiServicePasswordHashProcedure,
+		svc.PasswordHash,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("PasswordHash")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceLogoutHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceLogoutProcedure,
+		svc.Logout,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("Logout")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceEventStreamHandler := connect.NewServerStreamHandler(
+		OliveTinApiServiceEventStreamProcedure,
+		svc.EventStream,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("EventStream")),
+		connect.WithHandlerOptions(opts...),
+	)
+	oliveTinApiServiceGetDiagnosticsHandler := connect.NewUnaryHandler(
+		OliveTinApiServiceGetDiagnosticsProcedure,
+		svc.GetDiagnostics,
+		connect.WithSchema(oliveTinApiServiceMethods.ByName("GetDiagnostics")),
+		connect.WithHandlerOptions(opts...),
+	)
+	return "/olivetin.api.v1.OliveTinApiService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		switch r.URL.Path {
+		case OliveTinApiServiceGetDashboardComponentsProcedure:
+			oliveTinApiServiceGetDashboardComponentsHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceStartActionProcedure:
+			oliveTinApiServiceStartActionHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceStartActionAndWaitProcedure:
+			oliveTinApiServiceStartActionAndWaitHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceStartActionByGetProcedure:
+			oliveTinApiServiceStartActionByGetHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceStartActionByGetAndWaitProcedure:
+			oliveTinApiServiceStartActionByGetAndWaitHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceKillActionProcedure:
+			oliveTinApiServiceKillActionHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceExecutionStatusProcedure:
+			oliveTinApiServiceExecutionStatusHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceGetLogsProcedure:
+			oliveTinApiServiceGetLogsHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceValidateArgumentTypeProcedure:
+			oliveTinApiServiceValidateArgumentTypeHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceWhoAmIProcedure:
+			oliveTinApiServiceWhoAmIHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceSosReportProcedure:
+			oliveTinApiServiceSosReportHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceDumpVarsProcedure:
+			oliveTinApiServiceDumpVarsHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceDumpPublicIdActionMapProcedure:
+			oliveTinApiServiceDumpPublicIdActionMapHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceGetReadyzProcedure:
+			oliveTinApiServiceGetReadyzHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceLocalUserLoginProcedure:
+			oliveTinApiServiceLocalUserLoginHandler.ServeHTTP(w, r)
+		case OliveTinApiServicePasswordHashProcedure:
+			oliveTinApiServicePasswordHashHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceLogoutProcedure:
+			oliveTinApiServiceLogoutHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceEventStreamProcedure:
+			oliveTinApiServiceEventStreamHandler.ServeHTTP(w, r)
+		case OliveTinApiServiceGetDiagnosticsProcedure:
+			oliveTinApiServiceGetDiagnosticsHandler.ServeHTTP(w, r)
+		default:
+			http.NotFound(w, r)
+		}
+	})
+}
+
+// UnimplementedOliveTinApiServiceHandler returns CodeUnimplemented from all methods.
+type UnimplementedOliveTinApiServiceHandler struct{}
+
+func (UnimplementedOliveTinApiServiceHandler) GetDashboardComponents(context.Context, *connect.Request[v1.GetDashboardComponentsRequest]) (*connect.Response[v1.GetDashboardComponentsResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.GetDashboardComponents is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) StartAction(context.Context, *connect.Request[v1.StartActionRequest]) (*connect.Response[v1.StartActionResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.StartAction is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) StartActionAndWait(context.Context, *connect.Request[v1.StartActionAndWaitRequest]) (*connect.Response[v1.StartActionAndWaitResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.StartActionAndWait is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) StartActionByGet(context.Context, *connect.Request[v1.StartActionByGetRequest]) (*connect.Response[v1.StartActionByGetResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.StartActionByGet is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) StartActionByGetAndWait(context.Context, *connect.Request[v1.StartActionByGetAndWaitRequest]) (*connect.Response[v1.StartActionByGetAndWaitResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) KillAction(context.Context, *connect.Request[v1.KillActionRequest]) (*connect.Response[v1.KillActionResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.KillAction is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) ExecutionStatus(context.Context, *connect.Request[v1.ExecutionStatusRequest]) (*connect.Response[v1.ExecutionStatusResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.ExecutionStatus is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) GetLogs(context.Context, *connect.Request[v1.GetLogsRequest]) (*connect.Response[v1.GetLogsResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.GetLogs is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) ValidateArgumentType(context.Context, *connect.Request[v1.ValidateArgumentTypeRequest]) (*connect.Response[v1.ValidateArgumentTypeResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.ValidateArgumentType is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) WhoAmI(context.Context, *connect.Request[v1.WhoAmIRequest]) (*connect.Response[v1.WhoAmIResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.WhoAmI is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) SosReport(context.Context, *connect.Request[v1.SosReportRequest]) (*connect.Response[v1.SosReportResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.SosReport is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) DumpVars(context.Context, *connect.Request[v1.DumpVarsRequest]) (*connect.Response[v1.DumpVarsResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.DumpVars is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) DumpPublicIdActionMap(context.Context, *connect.Request[v1.DumpPublicIdActionMapRequest]) (*connect.Response[v1.DumpPublicIdActionMapResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) GetReadyz(context.Context, *connect.Request[v1.GetReadyzRequest]) (*connect.Response[v1.GetReadyzResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.GetReadyz is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) LocalUserLogin(context.Context, *connect.Request[v1.LocalUserLoginRequest]) (*connect.Response[v1.LocalUserLoginResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.LocalUserLogin is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) PasswordHash(context.Context, *connect.Request[v1.PasswordHashRequest]) (*connect.Response[v1.PasswordHashResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.PasswordHash is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) Logout(context.Context, *connect.Request[v1.LogoutRequest]) (*connect.Response[v1.LogoutResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.Logout is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) EventStream(context.Context, *connect.Request[v1.EventStreamRequest], *connect.ServerStream[v1.EventStreamResponse]) error {
+	return connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.EventStream is not implemented"))
+}
+
+func (UnimplementedOliveTinApiServiceHandler) GetDiagnostics(context.Context, *connect.Request[v1.GetDiagnosticsRequest]) (*connect.Response[v1.GetDiagnosticsResponse], error) {
+	return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.GetDiagnostics is not implemented"))
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 441 - 156
service/gen/olivetin/api/v1/olivetin.pb.go


+ 1 - 0
service/go.mod

@@ -93,6 +93,7 @@ require (
 	github.com/google/go-containerregistry v0.20.6 // indirect
 	github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/jamesread/golure v0.0.0-20250619190948-fa38cbd93cc4 // indirect
 	github.com/jdx/go-netrc v1.0.0 // indirect
 	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/klauspost/pgzip v1.2.6 // indirect

+ 2 - 0
service/go.sum

@@ -220,6 +220,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:
 github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jamesread/golure v0.0.0-20250619190948-fa38cbd93cc4 h1:MIZEqAaeMP1/saH0w6I5mzGKSv2lw8fAO7Hm2FgJb9k=
+github.com/jamesread/golure v0.0.0-20250619190948-fa38cbd93cc4/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=

+ 595 - 0
service/internal/api/api.go

@@ -0,0 +1,595 @@
+package api
+
+import (
+	ctx "context"
+
+	"connectrpc.com/connect"
+
+	apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
+	apiv1connect "github.com/OliveTin/OliveTin/gen/olivetin/api/v1/apiv1connect"
+	"github.com/google/uuid"
+	log "github.com/sirupsen/logrus"
+
+	"fmt"
+	"net/http"
+
+	acl "github.com/OliveTin/OliveTin/internal/acl"
+	config "github.com/OliveTin/OliveTin/internal/config"
+	executor "github.com/OliveTin/OliveTin/internal/executor"
+	installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
+	sv "github.com/OliveTin/OliveTin/internal/stringvariables"
+)
+
+type oliveTinAPI struct {
+	executor *executor.Executor
+	cfg      *config.Config
+
+	connectedClients []*connectedClients
+}
+
+type connectedClients struct {
+	channel           chan *apiv1.EventStreamResponse
+	AuthenticatedUser *acl.AuthenticatedUser
+}
+
+func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.KillActionRequest]) (*connect.Response[apiv1.KillActionResponse], error) {
+	ret := &apiv1.KillActionResponse{
+		ExecutionTrackingId: req.Msg.ExecutionTrackingId,
+	}
+
+	var execReqLogEntry *executor.InternalLogEntry
+
+	execReqLogEntry, ret.Found = api.executor.GetLog(req.Msg.ExecutionTrackingId)
+
+	if !ret.Found {
+		log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.Msg.ExecutionTrackingId)
+		return connect.NewResponse(ret), nil
+	}
+
+	log.Warnf("Killing execution request by tracking ID: %v", req.Msg.ExecutionTrackingId)
+
+	action := api.cfg.FindAction(execReqLogEntry.ActionTitle)
+
+	if action == nil {
+		log.Warnf("Killing execution request not possible - action not found: %v", execReqLogEntry.ActionTitle)
+		ret.Killed = false
+		return connect.NewResponse(ret), nil
+	}
+
+	user := acl.UserFromContext(ctx, api.cfg)
+
+	api.killActionByTrackingId(user, action, execReqLogEntry, ret)
+
+	return connect.NewResponse(ret), nil
+}
+
+func (api *oliveTinAPI) killActionByTrackingId(user *acl.AuthenticatedUser, action *config.Action, execReqLogEntry *executor.InternalLogEntry, ret *apiv1.KillActionResponse) {
+	if !acl.IsAllowedKill(api.cfg, user, action) {
+		log.Warnf("Killing execution request not possible - user not allowed to kill this action: %v", execReqLogEntry.ExecutionTrackingID)
+		ret.Killed = false
+	}
+
+	err := api.executor.Kill(execReqLogEntry)
+
+	if err != nil {
+		log.Warnf("Killing execution request err: %v", err)
+		ret.AlreadyCompleted = true
+		ret.Killed = false
+	} else {
+		ret.Killed = true
+	}
+}
+
+func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *connect.Request[apiv1.StartActionRequest]) (*connect.Response[apiv1.StartActionResponse], error) {
+	args := make(map[string]string)
+
+	for _, arg := range req.Msg.Arguments {
+		args[arg.Name] = arg.Value
+	}
+
+	api.executor.MapActionIdToBindingLock.RLock()
+	pair := api.executor.MapActionIdToBinding[req.Msg.ActionId]
+	api.executor.MapActionIdToBindingLock.RUnlock()
+
+	if pair == nil || pair.Action == nil {
+		return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.ActionId))
+	}
+
+	authenticatedUser := acl.UserFromContext(ctx, api.cfg)
+
+	execReq := executor.ExecutionRequest{
+		Action:            pair.Action,
+		EntityPrefix:      pair.EntityPrefix,
+		TrackingID:        req.Msg.UniqueTrackingId,
+		Arguments:         args,
+		AuthenticatedUser: authenticatedUser,
+		Cfg:               api.cfg,
+	}
+
+	api.executor.ExecRequest(&execReq)
+
+	ret := &apiv1.StartActionResponse{
+		ExecutionTrackingId: execReq.TrackingID,
+	}
+
+	return connect.NewResponse(ret), nil
+}
+
+func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1.PasswordHashRequest]) (*connect.Response[apiv1.PasswordHashResponse], error) {
+	hash, err := createHash(req.Msg.Password)
+
+	if err != nil {
+		return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("error creating hash: %w", err))
+	}
+
+	ret := &apiv1.PasswordHashResponse{
+		Hash: hash,
+	}
+
+	return connect.NewResponse(ret), nil
+}
+
+func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[apiv1.LocalUserLoginRequest]) (*connect.Response[apiv1.LocalUserLoginResponse], error) {
+	match := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password)
+
+	if match {
+		//grpc.SendHeader(ctx, metadata.Pairs("set-username", req.Username))
+
+		log.WithFields(log.Fields{
+			"username": req.Msg.Username,
+		}).Info("LocalUserLogin: User logged in successfully.")
+	} else {
+		log.WithFields(log.Fields{
+			"username": req.Msg.Username,
+		}).Warn("LocalUserLogin: User login failed.")
+	}
+
+	return connect.NewResponse(&apiv1.LocalUserLoginResponse{
+		Success: match,
+	}), nil
+}
+
+func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) {
+	args := make(map[string]string)
+
+	for _, arg := range req.Msg.Arguments {
+		args[arg.Name] = arg.Value
+	}
+
+	user := acl.UserFromContext(ctx, api.cfg)
+
+	execReq := executor.ExecutionRequest{
+		Action:            api.executor.FindActionBindingByID(req.Msg.ActionId),
+		TrackingID:        uuid.NewString(),
+		Arguments:         args,
+		AuthenticatedUser: user,
+		Cfg:               api.cfg,
+	}
+
+	wg, _ := api.executor.ExecRequest(&execReq)
+	wg.Wait()
+
+	internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
+
+	if ok {
+		return connect.NewResponse(&apiv1.StartActionAndWaitResponse{
+			LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
+		}), nil
+	} else {
+		return nil, fmt.Errorf("execution not found")
+	}
+}
+
+func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetRequest]) (*connect.Response[apiv1.StartActionByGetResponse], error) {
+	args := make(map[string]string)
+
+	execReq := executor.ExecutionRequest{
+		Action:            api.executor.FindActionBindingByID(req.Msg.ActionId),
+		TrackingID:        uuid.NewString(),
+		Arguments:         args,
+		AuthenticatedUser: acl.UserFromContext(ctx, api.cfg),
+		Cfg:               api.cfg,
+	}
+
+	_, uniqueTrackingId := api.executor.ExecRequest(&execReq)
+
+	return connect.NewResponse(&apiv1.StartActionByGetResponse{
+		ExecutionTrackingId: uniqueTrackingId,
+	}), nil
+}
+
+func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) {
+	args := make(map[string]string)
+
+	user := acl.UserFromContext(ctx, api.cfg)
+
+	execReq := executor.ExecutionRequest{
+		Action:            api.executor.FindActionBindingByID(req.Msg.ActionId),
+		TrackingID:        uuid.NewString(),
+		Arguments:         args,
+		AuthenticatedUser: user,
+		Cfg:               api.cfg,
+	}
+
+	wg, _ := api.executor.ExecRequest(&execReq)
+	wg.Wait()
+
+	internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
+
+	if ok {
+		return connect.NewResponse(&apiv1.StartActionByGetAndWaitResponse{
+			LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
+		}), nil
+	} else {
+		return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found"))
+	}
+}
+
+func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *acl.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,
+	}
+
+	if !pble.ExecutionFinished {
+		pble.CanKill = acl.IsAllowedKill(api.cfg, authenticatedUser, api.cfg.FindAction(logEntry.ActionConfigTitle))
+	}
+
+	return pble
+}
+
+func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string) *executor.InternalLogEntry {
+	logEntry, ok := api.executor.GetLog(executionTrackingId)
+
+	if !ok {
+		return nil
+	}
+
+	return logEntry
+}
+
+func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
+	var ile *executor.InternalLogEntry
+
+	logs := api.executor.GetLogsByActionId(actionId)
+
+	if len(logs) == 0 {
+		return nil
+	} else {
+		// Get last log entry
+		ile = logs[len(logs)-1]
+	}
+
+	return ile
+}
+
+func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[apiv1.ExecutionStatusRequest]) (*connect.Response[apiv1.ExecutionStatusResponse], error) {
+	res := &apiv1.ExecutionStatusResponse{}
+
+	user := acl.UserFromContext(ctx, api.cfg)
+
+	var ile *executor.InternalLogEntry
+
+	if req.Msg.ExecutionTrackingId != "" {
+		ile = getExecutionStatusByTrackingID(api, req.Msg.ExecutionTrackingId)
+
+	} else {
+		ile = getMostRecentExecutionStatusById(api, req.Msg.ActionId)
+	}
+
+	if ile == nil {
+		return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s or action ID %s", req.Msg.ExecutionTrackingId, req.Msg.ActionId))
+	} else {
+		res.LogEntry = api.internalLogEntryToPb(ile, user)
+	}
+
+	return connect.NewResponse(res), nil
+}
+
+/**
+func (api *oliveTinAPI) WatchExecution(req *apiv1.WatchExecutionRequest, srv apiv1.OliveTinApi_WatchExecutionServer) error {
+	log.Infof("Watch")
+
+	if logEntry, ok := api.executor.Logs[req.ExecutionUuid]; !ok {
+		log.Errorf("Execution not found: %v", req.ExecutionUuid)
+
+		return nil
+	} else {
+		if logEntry.ExecutionStarted {
+			for !logEntry.ExecutionCompleted {
+				tmp := make([]byte, 256)
+
+				red, err := io.ReadAtLeast(logEntry.StdoutBuffer, tmp, 1)
+
+				log.Infof("%v %v", red, err)
+
+				srv.Send(&apiv1.WatchExecutionUpdate{
+					Update: string(tmp),
+				})
+			}
+		}
+
+		return nil
+	}
+}
+*/
+
+func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) {
+	//user := acl.UserFromContext(ctx, cfg)
+
+	//grpc.SendHeader(ctx, metadata.Pairs("logout-provider", user.Provider))
+	//grpc.SendHeader(ctx, metadata.Pairs("logout-sid", user.SID))
+
+	return nil, nil
+}
+
+func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *connect.Request[apiv1.GetDashboardComponentsRequest]) (*connect.Response[apiv1.GetDashboardComponentsResponse], error) {
+	user := acl.UserFromContext(ctx, api.cfg)
+
+	if user.IsGuest() && api.cfg.AuthRequireGuestsToLogin {
+		return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("guests are not allowed to access the dashboard"))
+	}
+
+	res := buildDashboardResponse(api.executor, api.cfg, user)
+
+	/*
+		if len(res.Actions) == 0 {
+			log.WithFields(log.Fields{
+				"username":         user.Username,
+				"usergroupLine":    user.UsergroupLine,
+				"provider":         user.Provider,
+				"acls":             user.Acls,
+				"availableActions": len(api.cfg.Actions),
+			}).Warn("Zero actions found for user")
+		}
+	*/
+
+	log.Tracef("GetDashboardComponents: %v", res)
+
+	return connect.NewResponse(res), nil
+}
+
+func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetLogsRequest]) (*connect.Response[apiv1.GetLogsResponse], error) {
+	user := acl.UserFromContext(ctx, api.cfg)
+
+	ret := &apiv1.GetLogsResponse{}
+
+	logEntries, countRemaining := api.executor.GetLogTrackingIds(req.Msg.StartOffset, api.cfg.LogHistoryPageSize)
+
+	for _, logEntry := range logEntries {
+		action := api.cfg.FindAction(logEntry.ActionTitle)
+
+		if action == nil || acl.IsAllowedLogs(api.cfg, user, action) {
+			pbLogEntry := api.internalLogEntryToPb(logEntry, user)
+
+			ret.Logs = append(ret.Logs, pbLogEntry)
+		}
+	}
+
+	ret.CountRemaining = countRemaining
+	ret.PageSize = api.cfg.LogHistoryPageSize
+
+	return connect.NewResponse(ret), nil
+}
+
+/*
+This function is ONLY a helper for the UI - the arguments are validated properly
+on the StartAction -> Executor chain. This is here basically to provide helpful
+error messages more quickly before starting the action.
+*/
+func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Request[apiv1.ValidateArgumentTypeRequest]) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) {
+	err := executor.TypeSafetyCheck("", req.Msg.Value, req.Msg.Type)
+	desc := ""
+
+	if err != nil {
+		desc = err.Error()
+	}
+
+	return connect.NewResponse(&apiv1.ValidateArgumentTypeResponse{
+		Valid:       err == nil,
+		Description: desc,
+	}), nil
+}
+
+func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAmIRequest]) (*connect.Response[apiv1.WhoAmIResponse], error) {
+	user := acl.UserFromContext(ctx, api.cfg)
+
+	res := &apiv1.WhoAmIResponse{
+		AuthenticatedUser: user.Username,
+		Usergroup:         user.UsergroupLine,
+		Provider:          user.Provider,
+		Sid:               user.SID,
+		Acls:              user.Acls,
+	}
+
+	return connect.NewResponse(res), nil
+}
+
+func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *connect.Request[apiv1.SosReportRequest]) (*connect.Response[apiv1.SosReportResponse], error) {
+	sos := installationinfo.GetSosReport()
+
+	if !api.cfg.InsecureAllowDumpSos {
+		log.Info(sos)
+		sos = "Your SOS Report has been logged to OliveTin logs.\n\nIf you are in a safe network, you can temporarily set `insecureAllowDumpSos: true` in your config.yaml, restart OliveTin, and refresh this page - it will put the output directly in the browser."
+	}
+
+	ret := &apiv1.SosReportResponse{
+		Alert: sos,
+	}
+
+	return connect.NewResponse(ret), nil
+}
+
+func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.DumpVarsRequest]) (*connect.Response[apiv1.DumpVarsResponse], error) {
+	res := &apiv1.DumpVarsResponse{}
+
+	if !api.cfg.InsecureAllowDumpVars {
+		res.Alert = "Dumping variables is not allowed by default because it is insecure."
+
+		return connect.NewResponse(res), nil
+	}
+
+	res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore"
+	res.Contents = sv.GetAll()
+
+	return connect.NewResponse(res), nil
+}
+
+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)
+
+	if !api.cfg.InsecureAllowDumpActionMap {
+		res.Alert = "Dumping Public IDs is disallowed."
+
+		return connect.NewResponse(res), nil
+	}
+
+	api.executor.MapActionIdToBindingLock.RLock()
+
+	for k, v := range api.executor.MapActionIdToBinding {
+		res.Contents[k] = &apiv1.ActionEntityPair{
+			ActionTitle:  v.Action.Title,
+			EntityPrefix: v.EntityPrefix,
+		}
+	}
+
+	api.executor.MapActionIdToBindingLock.RUnlock()
+
+	res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpActionMap = false again after you don't need it anymore"
+
+	return connect.NewResponse(res), nil
+}
+
+func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *connect.Request[apiv1.GetReadyzRequest]) (*connect.Response[apiv1.GetReadyzResponse], error) {
+	res := &apiv1.GetReadyzResponse{
+		Status: "OK",
+	}
+
+	return connect.NewResponse(res), nil
+}
+
+func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.EventStreamRequest], srv *connect.ServerStream[apiv1.EventStreamResponse]) error {
+	log.Debugf("EventStream: %v", req.Msg)
+
+	client := &connectedClients{
+		channel:           make(chan *apiv1.EventStreamResponse, 10), // Buffered channel to hold Events
+		AuthenticatedUser: acl.UserFromContext(ctx, api.cfg),
+	}
+
+	log.Infof("EventStream: client connected: %v", client.AuthenticatedUser.Username)
+
+	api.connectedClients = append(api.connectedClients, client)
+
+	// loop over client channel and send events to connectedClient
+	for msg := range client.channel {
+		log.Debugf("Sending event to client: %v", msg)
+		if err := srv.Send(msg); err != nil {
+			log.Errorf("Error sending event to client: %v", err)
+		}
+	}
+
+	log.Infof("EventStream: client disconnected")
+
+	return nil
+}
+
+func (api *oliveTinAPI) OnActionMapRebuilt() {
+	for _, client := range api.connectedClients {
+		select {
+		case client.channel <- &apiv1.EventStreamResponse{
+			Event: &apiv1.EventStreamResponse_ConfigChanged{
+				ConfigChanged: &apiv1.EventConfigChanged{},
+			},
+		}:
+		default:
+			log.Warnf("EventStream: client channel is full, dropping message")
+		}
+	}
+}
+
+func (api *oliveTinAPI) OnExecutionStarted(ex *executor.InternalLogEntry) {
+	for _, client := range api.connectedClients {
+		select {
+		case client.channel <- &apiv1.EventStreamResponse{
+			Event: &apiv1.EventStreamResponse_ExecutionStarted{
+				ExecutionStarted: &apiv1.EventExecutionStarted{
+					LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
+				},
+			},
+		}:
+		default:
+			log.Warnf("EventStream: client channel is full, dropping message")
+		}
+	}
+}
+
+func (api *oliveTinAPI) OnExecutionFinished(ex *executor.InternalLogEntry) {
+	for _, client := range api.connectedClients {
+		select {
+		case client.channel <- &apiv1.EventStreamResponse{
+			Event: &apiv1.EventStreamResponse_ExecutionFinished{
+				ExecutionFinished: &apiv1.EventExecutionFinished{
+					LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
+				},
+			},
+		}:
+		default:
+			log.Warnf("EventStream: client channel is full, dropping message")
+		}
+	}
+}
+
+func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[apiv1.GetDiagnosticsRequest]) (*connect.Response[apiv1.GetDiagnosticsResponse], error) {
+	res := &apiv1.GetDiagnosticsResponse{
+		SshFoundKey:    installationinfo.Runtime.SshFoundKey,
+		SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
+	}
+
+	return connect.NewResponse(res), nil
+}
+
+func (api *oliveTinAPI) OnOutputChunk(content []byte, executionTrackingId string) {
+	for _, client := range api.connectedClients {
+		select {
+		case client.channel <- &apiv1.EventStreamResponse{
+			Event: &apiv1.EventStreamResponse_OutputChunk{
+				OutputChunk: &apiv1.EventOutputChunk{
+					Output:              string(content),
+					ExecutionTrackingId: executionTrackingId,
+				},
+			},
+		}:
+		default:
+			log.Warnf("EventStream: client channel is full, dropping message")
+		}
+	}
+}
+
+func newServer(ex *executor.Executor) *oliveTinAPI {
+	server := oliveTinAPI{}
+	server.cfg = ex.Cfg
+	server.executor = ex
+
+	ex.AddListener(&server)
+	return &server
+}
+
+func GetNewHandler(ex *executor.Executor) (string, http.Handler) {
+	server := newServer(ex)
+
+	return apiv1connect.NewOliveTinApiServiceHandler(server)
+}

+ 33 - 40
service/internal/grpcapi/grpcApiActions.go → service/internal/api/apiActions.go

@@ -1,19 +1,32 @@
-package grpcapi
+package api
 
 import (
-	apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
+	apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
 	acl "github.com/OliveTin/OliveTin/internal/acl"
 	config "github.com/OliveTin/OliveTin/internal/config"
 	executor "github.com/OliveTin/OliveTin/internal/executor"
-	installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
 	sv "github.com/OliveTin/OliveTin/internal/stringvariables"
-	"sort"
+	log "github.com/sirupsen/logrus"
 )
 
 type DashboardRenderRequest struct {
 	AuthenticatedUser   *acl.AuthenticatedUser
 	AllowedActionTitles []string `json:"allows_action_titles"`
 	cfg                 *config.Config
+	ex                  *executor.Executor
+
+	usedActions map[string]bool
+}
+
+func (rr *DashboardRenderRequest) findAction(title string) *apiv1.Action {
+	for _, action := range rr.cfg.Actions {
+		log.Infof("Checking action %s against %s", title, action.Title)
+		if action.Title == title {
+			return buildAction(action.ID, nil, rr)
+		}
+	}
+
+	return nil
 }
 
 func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl.AuthenticatedUser) *apiv1.GetDashboardComponentsResponse {
@@ -22,34 +35,25 @@ func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl
 		AuthenticatedUserProvider: user.Provider,
 	}
 
-	ex.MapActionIdToBindingLock.RLock()
-
-	for actionId, actionBinding := range ex.MapActionIdToBinding {
-		if !acl.IsAllowedView(cfg, user, actionBinding.Action) {
-			continue
-		}
-
-		res.Actions = append(res.Actions, buildAction(actionId, actionBinding, user))
-	}
-
-	ex.MapActionIdToBindingLock.RUnlock()
-
-	sort.Slice(res.Actions, func(i, j int) bool {
-		if res.Actions[i].Order == res.Actions[j].Order {
-			return res.Actions[i].Title < res.Actions[j].Title
-		} else {
-			return res.Actions[i].Order < res.Actions[j].Order
-		}
-	})
+	/*
+		sort.Slice(res.Actions, func(i, j int) bool {
+			if res.Actions[i].Order == res.Actions[j].Order {
+				return res.Actions[i].Title < res.Actions[j].Title
+			} else {
+				return res.Actions[i].Order < res.Actions[j].Order
+			}
+		})
+	*/
 
 	rr := &DashboardRenderRequest{
-		AuthenticatedUser:   user,
-		AllowedActionTitles: getActionTitles(res.Actions),
-		cfg:                 cfg,
+		AuthenticatedUser: user,
+		//		AllowedActionTitles: getActionTitles(res.Actions),
+		cfg:         cfg,
+		ex:          ex,
+		usedActions: make(map[string]bool),
 	}
 
 	res.EffectivePolicy = buildEffectivePolicy(user.EffectivePolicy)
-	res.Diagnostics = buildDiagnostics(res.EffectivePolicy.ShowDiagnostics)
 	res.Dashboards = dashboardCfgToPb(rr)
 
 	return res
@@ -74,25 +78,14 @@ func buildEffectivePolicy(policy *config.ConfigurationPolicy) *apiv1.EffectivePo
 	return ret
 }
 
-func buildDiagnostics(showDiagnostics bool) *apiv1.Diagnostics {
-	ret := &apiv1.Diagnostics{}
-
-	if showDiagnostics {
-		ret.SshFoundKey = installationinfo.Runtime.SshFoundKey
-		ret.SshFoundConfig = installationinfo.Runtime.SshFoundConfig
-	}
-
-	return ret
-}
-
-func buildAction(actionId string, actionBinding *executor.ActionBinding, user *acl.AuthenticatedUser) *apiv1.Action {
+func buildAction(actionId string, actionBinding *executor.ActionBinding, rr *DashboardRenderRequest) *apiv1.Action {
 	action := actionBinding.Action
 
 	btn := apiv1.Action{
 		Id:           actionId,
 		Title:        sv.ReplaceEntityVars(actionBinding.EntityPrefix, action.Title),
 		Icon:         sv.ReplaceEntityVars(actionBinding.EntityPrefix, action.Icon),
-		CanExec:      acl.IsAllowedExec(cfg, user, action),
+		CanExec:      acl.IsAllowedExec(rr.cfg, rr.AuthenticatedUser, action),
 		PopupOnStart: action.PopupOnStart,
 		Order:        int32(actionBinding.ConfigOrder),
 	}

+ 1 - 3
service/internal/grpcapi/grpcApi_test.go → service/internal/api/api_test.go

@@ -1,12 +1,10 @@
-package grpcapi
+package api
 
 // Thank you: https://stackoverflow.com/questions/42102496/testing-a-grpc-service
 
 import (
 	"context"
 	"github.com/stretchr/testify/assert"
-	"google.golang.org/grpc"
-	"google.golang.org/grpc/test/bufconn"
 	"net"
 	"testing"
 

+ 2 - 2
service/internal/grpcapi/grpcApiDashboardEntities.go → service/internal/api/dashboard_entities.go

@@ -1,7 +1,7 @@
-package grpcapi
+package api
 
 import (
-	apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
+	apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
 	config "github.com/OliveTin/OliveTin/internal/config"
 	sv "github.com/OliveTin/OliveTin/internal/stringvariables"
 	"golang.org/x/exp/slices"

+ 54 - 15
service/internal/grpcapi/grpcApiDashboard.go → service/internal/api/dashboards.go

@@ -1,36 +1,75 @@
-package grpcapi
+package api
 
 import (
-	apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
+	apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
 	config "github.com/OliveTin/OliveTin/internal/config"
-	log "github.com/sirupsen/logrus"
 	"golang.org/x/exp/slices"
 )
 
-func dashboardCfgToPb(rr *DashboardRenderRequest) []*apiv1.DashboardComponent {
-	ret := make([]*apiv1.DashboardComponent, 0)
+func dashboardCfgToPb(rr *DashboardRenderRequest) []*apiv1.Dashboard {
+	ret := make([]*apiv1.Dashboard, 0)
 
-	for _, dashboard := range cfg.Dashboards {
-		pbdb := &apiv1.DashboardComponent{
-			Type:     "dashboard",
+	for _, dashboard := range rr.cfg.Dashboards {
+		pbdb := &apiv1.Dashboard{
 			Title:    dashboard.Title,
 			Contents: removeNulls(getDashboardComponentContents(dashboard, rr)),
+			//			Contents: removeNulls(getDashboardComponentContents(dashboard, rr)),
 		}
 
-		if len(pbdb.Contents) == 0 {
-			log.WithFields(log.Fields{
-				"dashboard": dashboard.Title,
-				"username":  rr.AuthenticatedUser.Username,
-			}).Debugf("Dashboard has no readable contents, so it will not be visible in the web ui")
-			continue
-		}
+		/*
+			if len(pbdb.Contents) == 0 {
+				log.WithFields(log.Fields{
+					"dashboard": dashboard.Title,
+					"username":  rr.AuthenticatedUser.Username,
+				}).Debugf("Dashboard has no readable contents, so it will not be visible in the web ui")
+				continue
+			}
+		*/
 
 		ret = append(ret, pbdb)
 	}
 
+	ret = append(ret, buildDefaultDashboard(rr))
+
 	return ret
 }
 
+func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard {
+	fieldset := &apiv1.DashboardComponent{
+		Type:     "fieldset",
+		Contents: make([]*apiv1.DashboardComponent, 0),
+		Title:    "Default",
+	}
+
+	actions := make([]*apiv1.Action, 0)
+
+	for id, binding := range rr.ex.MapActionIdToBinding {
+		if binding.Action.Hidden {
+			continue
+		}
+
+		if rr.usedActions[id] {
+			continue
+		}
+
+		rr.usedActions[id] = true
+		actions = append(actions, buildAction(id, binding, rr))
+	}
+
+	for _, action := range actions {
+		fieldset.Contents = append(fieldset.Contents, &apiv1.DashboardComponent{
+			Type:  "link",
+			Title: action.Title,
+			Icon:  action.Icon,
+		})
+	}
+
+	return &apiv1.Dashboard{
+		Title:    "Default",
+		Contents: []*apiv1.DashboardComponent{fieldset},
+	}
+}
+
 func removeNulls(components []*apiv1.DashboardComponent) []*apiv1.DashboardComponent {
 	ret := make([]*apiv1.DashboardComponent, 0)
 

+ 1 - 1
service/internal/grpcapi/local_user_login.go → service/internal/api/local_user_login.go

@@ -1,4 +1,4 @@
-package grpcapi
+package api
 
 import (
 	config "github.com/OliveTin/OliveTin/internal/config"

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

@@ -21,6 +21,11 @@ import (
 	"time"
 )
 
+const (
+	DefaultExitCodeNotExecuted = -1337
+	MaxTriggerDepth = 10 
+)
+
 var (
 	metricActionsRequested = promauto.NewCounter(prometheus.CounterOpts{
 		Name: "olivetin_actions_requested_count",
@@ -64,6 +69,7 @@ type ExecutionRequest struct {
 	Cfg               *config.Config
 	AuthenticatedUser *acl.AuthenticatedUser
 	EntityPrefix      string
+	TriggerDepth      int
 
 	logEntry           *InternalLogEntry
 	finalParsedCommand string
@@ -88,6 +94,7 @@ type InternalLogEntry struct {
 	Username            string
 	Index               int64
 	EntityPrefix        string
+	ActionConfigTitle   string // This is the title of the action as defined in the config, not the final parsed title.
 
 	/*
 		The following 3 properties are obviously on Action normally, but it's useful
@@ -243,7 +250,7 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string)
 		DatetimeStarted:     time.Now(),
 		ExecutionTrackingID: req.TrackingID,
 		Output:              "",
-		ExitCode:            -1337, // If an Action is not actually executed, this is the default exit code.
+		ExitCode:            DefaultExitCodeNotExecuted, 
 		ExecutionStarted:    false,
 		ExecutionFinished:   false,
 		ActionId:            "",
@@ -440,6 +447,7 @@ func stepRequestAction(req *ExecutionRequest) bool {
 
 	metricActionsRequested.Inc()
 
+	req.logEntry.ActionConfigTitle = req.Action.Title
 	req.logEntry.ActionTitle = sv.ReplaceEntityVars(req.EntityPrefix, req.Action.Title)
 	req.logEntry.ActionIcon = req.Action.Icon
 	req.logEntry.ActionId = req.Action.ID
@@ -651,6 +659,15 @@ func stepTrigger(req *ExecutionRequest) bool {
 		return true
 	}
 
+	if req.TriggerDepth >= MaxTriggerDepth {
+		log.WithFields(log.Fields{
+			"actionTitle": req.logEntry.ActionTitle,
+			"depth":       req.TriggerDepth,
+		}).Warnf("Trigger action reached maximum depth of %v. Not triggering further actions.", MaxTriggerDepth)
+		req.logEntry.Output += fmt.Sprintf("OliveTin::trigger - this action reached maximum trigger depth of %v. Not triggering further actions.", MaxTriggerDepth)
+		return true
+	}
+
 	if len(req.Tags) > 0 && req.Tags[0] == "trigger" {
 		log.Warnf("Trigger action is triggering another trigger action. This is allowed, but be careful not to create trigger loops.")
 	}
@@ -669,6 +686,7 @@ func triggerLoop(req *ExecutionRequest) {
 			AuthenticatedUser: req.AuthenticatedUser,
 			Arguments:         req.Arguments,
 			Cfg:               req.Cfg,
+			TriggerDepth:      req.TriggerDepth + 1,
 		}
 
 		req.executor.ExecRequest(trigger)

+ 0 - 512
service/internal/grpcapi/grpcApi.go

@@ -1,512 +0,0 @@
-package grpcapi
-
-import (
-	ctx "context"
-
-	apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
-	"github.com/google/uuid"
-	log "github.com/sirupsen/logrus"
-	"google.golang.org/genproto/googleapis/api/httpbody"
-	"google.golang.org/grpc"
-	"google.golang.org/grpc/codes"
-	"google.golang.org/grpc/metadata"
-	"google.golang.org/grpc/status"
-
-	"fmt"
-	"net"
-
-	acl "github.com/OliveTin/OliveTin/internal/acl"
-	config "github.com/OliveTin/OliveTin/internal/config"
-	executor "github.com/OliveTin/OliveTin/internal/executor"
-	installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
-	sv "github.com/OliveTin/OliveTin/internal/stringvariables"
-)
-
-var (
-	cfg *config.Config
-)
-
-type oliveTinAPI struct {
-	// Uncomment this if you want to allow undefined methods during dev.
-	//	apiv1.UnimplementedOliveTinApiServiceServer
-
-	executor *executor.Executor
-}
-
-func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *apiv1.KillActionRequest) (*apiv1.KillActionResponse, error) {
-	ret := &apiv1.KillActionResponse{
-		ExecutionTrackingId: req.ExecutionTrackingId,
-	}
-
-	var execReqLogEntry *executor.InternalLogEntry
-
-	execReqLogEntry, ret.Found = api.executor.GetLog(req.ExecutionTrackingId)
-
-	if !ret.Found {
-		log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.ExecutionTrackingId)
-		return ret, nil
-	}
-
-	log.Warnf("Killing execution request by tracking ID: %v", req.ExecutionTrackingId)
-
-	action := cfg.FindAction(execReqLogEntry.ActionTitle)
-
-	if action == nil {
-		log.Warnf("Killing execution request not possible - action not found: %v", execReqLogEntry.ActionTitle)
-		ret.Killed = false
-		return ret, nil
-	}
-
-	user := acl.UserFromContext(ctx, cfg)
-
-	api.killActionByTrackingId(user, action, execReqLogEntry, ret)
-
-	return ret, nil
-}
-
-func (api *oliveTinAPI) killActionByTrackingId(user *acl.AuthenticatedUser, action *config.Action, execReqLogEntry *executor.InternalLogEntry, ret *apiv1.KillActionResponse) {
-	if !acl.IsAllowedKill(cfg, user, action) {
-		log.Warnf("Killing execution request not possible - user not allowed to kill this action: %v", execReqLogEntry.ExecutionTrackingID)
-		ret.Killed = false
-	}
-
-	err := api.executor.Kill(execReqLogEntry)
-
-	if err != nil {
-		log.Warnf("Killing execution request err: %v", err)
-		ret.AlreadyCompleted = true
-		ret.Killed = false
-	} else {
-		ret.Killed = true
-	}
-}
-
-func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *apiv1.StartActionRequest) (*apiv1.StartActionResponse, error) {
-	args := make(map[string]string)
-
-	for _, arg := range req.Arguments {
-		args[arg.Name] = arg.Value
-	}
-
-	api.executor.MapActionIdToBindingLock.RLock()
-	pair := api.executor.MapActionIdToBinding[req.ActionId]
-	api.executor.MapActionIdToBindingLock.RUnlock()
-
-	if pair == nil || pair.Action == nil {
-		return nil, status.Errorf(codes.NotFound, "Action not found.")
-	}
-
-	authenticatedUser := acl.UserFromContext(ctx, cfg)
-
-	execReq := executor.ExecutionRequest{
-		Action:            pair.Action,
-		EntityPrefix:      pair.EntityPrefix,
-		TrackingID:        req.UniqueTrackingId,
-		Arguments:         args,
-		AuthenticatedUser: authenticatedUser,
-		Cfg:               cfg,
-	}
-
-	api.executor.ExecRequest(&execReq)
-
-	return &apiv1.StartActionResponse{
-		ExecutionTrackingId: execReq.TrackingID,
-	}, nil
-}
-
-func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *apiv1.PasswordHashRequest) (*httpbody.HttpBody, error) {
-	hash, err := createHash(req.Password)
-
-	if err != nil {
-		return nil, status.Errorf(codes.Internal, "Error creating hash.")
-	}
-
-	ret := &httpbody.HttpBody{
-		ContentType: "text/plain",
-		Data:        []byte("Your password hash is: " + hash),
-	}
-
-	return ret, nil
-}
-
-func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *apiv1.LocalUserLoginRequest) (*apiv1.LocalUserLoginResponse, error) {
-	match := checkUserPassword(cfg, req.Username, req.Password)
-
-	if match {
-		grpc.SendHeader(ctx, metadata.Pairs("set-username", req.Username))
-
-		log.WithFields(log.Fields{
-			"username": req.Username,
-		}).Info("LocalUserLogin: User logged in successfully.")
-	} else {
-		log.WithFields(log.Fields{
-			"username": req.Username,
-		}).Warn("LocalUserLogin: User login failed.")
-	}
-
-	return &apiv1.LocalUserLoginResponse{
-		Success: match,
-	}, nil
-}
-
-func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *apiv1.StartActionAndWaitRequest) (*apiv1.StartActionAndWaitResponse, error) {
-	args := make(map[string]string)
-
-	for _, arg := range req.Arguments {
-		args[arg.Name] = arg.Value
-	}
-
-	user := acl.UserFromContext(ctx, cfg)
-
-	execReq := executor.ExecutionRequest{
-		Action:            api.executor.FindActionBindingByID(req.ActionId),
-		TrackingID:        uuid.NewString(),
-		Arguments:         args,
-		AuthenticatedUser: user,
-		Cfg:               cfg,
-	}
-
-	wg, _ := api.executor.ExecRequest(&execReq)
-	wg.Wait()
-
-	internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
-
-	if ok {
-		return &apiv1.StartActionAndWaitResponse{
-			LogEntry: internalLogEntryToPb(internalLogEntry, user),
-		}, nil
-	} else {
-		return nil, fmt.Errorf("execution not found")
-	}
-}
-
-func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *apiv1.StartActionByGetRequest) (*apiv1.StartActionByGetResponse, error) {
-	args := make(map[string]string)
-
-	execReq := executor.ExecutionRequest{
-		Action:            api.executor.FindActionBindingByID(req.ActionId),
-		TrackingID:        uuid.NewString(),
-		Arguments:         args,
-		AuthenticatedUser: acl.UserFromContext(ctx, cfg),
-		Cfg:               cfg,
-	}
-
-	_, uniqueTrackingId := api.executor.ExecRequest(&execReq)
-
-	return &apiv1.StartActionByGetResponse{
-		ExecutionTrackingId: uniqueTrackingId,
-	}, nil
-}
-
-func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *apiv1.StartActionByGetAndWaitRequest) (*apiv1.StartActionByGetAndWaitResponse, error) {
-	args := make(map[string]string)
-
-	user := acl.UserFromContext(ctx, cfg)
-
-	execReq := executor.ExecutionRequest{
-		Action:            api.executor.FindActionBindingByID(req.ActionId),
-		TrackingID:        uuid.NewString(),
-		Arguments:         args,
-		AuthenticatedUser: user,
-		Cfg:               cfg,
-	}
-
-	wg, _ := api.executor.ExecRequest(&execReq)
-	wg.Wait()
-
-	internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
-
-	if ok {
-		return &apiv1.StartActionByGetAndWaitResponse{
-			LogEntry: internalLogEntryToPb(internalLogEntry, user),
-		}, nil
-	} else {
-		return nil, status.Errorf(codes.NotFound, "Execution not found.")
-	}
-}
-
-func internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *acl.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,
-	}
-
-	if !pble.ExecutionFinished {
-		pble.CanKill = acl.IsAllowedKill(cfg, authenticatedUser, cfg.FindAction(logEntry.ActionTitle))
-	}
-
-	return pble
-}
-
-func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string) *executor.InternalLogEntry {
-	logEntry, ok := api.executor.GetLog(executionTrackingId)
-
-	if !ok {
-		return nil
-	}
-
-	return logEntry
-}
-
-func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
-	var ile *executor.InternalLogEntry
-
-	logs := api.executor.GetLogsByActionId(actionId)
-
-	if len(logs) == 0 {
-		return nil
-	} else {
-		// Get last log entry
-		ile = logs[len(logs)-1]
-	}
-
-	return ile
-}
-
-func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *apiv1.ExecutionStatusRequest) (*apiv1.ExecutionStatusResponse, error) {
-	res := &apiv1.ExecutionStatusResponse{}
-
-	user := acl.UserFromContext(ctx, cfg)
-
-	var ile *executor.InternalLogEntry
-
-	if req.ExecutionTrackingId != "" {
-		ile = getExecutionStatusByTrackingID(api, req.ExecutionTrackingId)
-
-	} else {
-		ile = getMostRecentExecutionStatusById(api, req.ActionId)
-	}
-
-	if ile == nil {
-		return nil, status.Error(codes.NotFound, "Execution not found")
-	} else {
-		res.LogEntry = internalLogEntryToPb(ile, user)
-	}
-
-	return res, nil
-}
-
-/**
-func (api *oliveTinAPI) WatchExecution(req *apiv1.WatchExecutionRequest, srv apiv1.OliveTinApi_WatchExecutionServer) error {
-	log.Infof("Watch")
-
-	if logEntry, ok := api.executor.Logs[req.ExecutionUuid]; !ok {
-		log.Errorf("Execution not found: %v", req.ExecutionUuid)
-
-		return nil
-	} else {
-		if logEntry.ExecutionStarted {
-			for !logEntry.ExecutionCompleted {
-				tmp := make([]byte, 256)
-
-				red, err := io.ReadAtLeast(logEntry.StdoutBuffer, tmp, 1)
-
-				log.Infof("%v %v", red, err)
-
-				srv.Send(&apiv1.WatchExecutionUpdate{
-					Update: string(tmp),
-				})
-			}
-		}
-
-		return nil
-	}
-}
-*/
-
-func (api *oliveTinAPI) Logout(ctx ctx.Context, req *apiv1.LogoutRequest) (*httpbody.HttpBody, error) {
-	user := acl.UserFromContext(ctx, cfg)
-
-	grpc.SendHeader(ctx, metadata.Pairs("logout-provider", user.Provider))
-	grpc.SendHeader(ctx, metadata.Pairs("logout-sid", user.SID))
-
-	return nil, nil
-}
-
-func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *apiv1.GetDashboardComponentsRequest) (*apiv1.GetDashboardComponentsResponse, error) {
-	user := acl.UserFromContext(ctx, cfg)
-
-	if user.IsGuest() && cfg.AuthRequireGuestsToLogin {
-		return nil, status.Errorf(codes.PermissionDenied, "Guests are not allowed to access the dashboard.")
-	}
-
-	res := buildDashboardResponse(api.executor, cfg, user)
-
-	if len(res.Actions) == 0 {
-		log.WithFields(log.Fields{
-			"username":         user.Username,
-			"usergroupLine":    user.UsergroupLine,
-			"provider":         user.Provider,
-			"acls":             user.Acls,
-			"availableActions": len(cfg.Actions),
-		}).Warn("Zero actions found for user")
-	}
-
-	log.Tracef("GetDashboardComponents: %v", res)
-
-	return res, nil
-}
-
-func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *apiv1.GetLogsRequest) (*apiv1.GetLogsResponse, error) {
-	user := acl.UserFromContext(ctx, cfg)
-
-	ret := &apiv1.GetLogsResponse{}
-
-	logEntries, countRemaining := api.executor.GetLogTrackingIds(req.StartOffset, cfg.LogHistoryPageSize)
-
-	for _, logEntry := range logEntries {
-		action := cfg.FindAction(logEntry.ActionTitle)
-
-		if action == nil || acl.IsAllowedLogs(cfg, user, action) {
-			pbLogEntry := internalLogEntryToPb(logEntry, user)
-
-			ret.Logs = append(ret.Logs, pbLogEntry)
-		}
-	}
-
-	ret.CountRemaining = countRemaining
-	ret.PageSize = cfg.LogHistoryPageSize
-
-	return ret, nil
-}
-
-/*
-This function is ONLY a helper for the UI - the arguments are validated properly
-on the StartAction -> Executor chain. This is here basically to provide helpful
-error messages more quickly before starting the action.
-*/
-func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *apiv1.ValidateArgumentTypeRequest) (*apiv1.ValidateArgumentTypeResponse, error) {
-	err := executor.TypeSafetyCheck("", req.Value, req.Type)
-	desc := ""
-
-	if err != nil {
-		desc = err.Error()
-	}
-
-	return &apiv1.ValidateArgumentTypeResponse{
-		Valid:       err == nil,
-		Description: desc,
-	}, nil
-}
-
-func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *apiv1.WhoAmIRequest) (*apiv1.WhoAmIResponse, error) {
-	user := acl.UserFromContext(ctx, cfg)
-
-	res := &apiv1.WhoAmIResponse{
-		AuthenticatedUser: user.Username,
-		Usergroup:         user.UsergroupLine,
-		Provider:          user.Provider,
-		Sid:               user.SID,
-		Acls:              user.Acls,
-	}
-
-	return res, nil
-}
-
-func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *apiv1.SosReportRequest) (*httpbody.HttpBody, error) {
-	sos := installationinfo.GetSosReport()
-
-	if !cfg.InsecureAllowDumpSos {
-		log.Info(sos)
-		sos = "Your SOS Report has been logged to OliveTin logs.\n\nIf you are in a safe network, you can temporarily set `insecureAllowDumpSos: true` in your config.yaml, restart OliveTin, and refresh this page - it will put the output directly in the browser."
-	}
-
-	ret := &httpbody.HttpBody{
-		ContentType: "text/plain",
-		Data:        []byte(sos),
-	}
-
-	return ret, nil
-}
-
-func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *apiv1.DumpVarsRequest) (*apiv1.DumpVarsResponse, error) {
-	res := &apiv1.DumpVarsResponse{}
-
-	if !cfg.InsecureAllowDumpVars {
-		res.Alert = "Dumping variables is not allowed by default because it is insecure."
-
-		return res, nil
-	}
-
-	res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore"
-	res.Contents = sv.GetAll()
-
-	return res, nil
-}
-
-func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *apiv1.DumpPublicIdActionMapRequest) (*apiv1.DumpPublicIdActionMapResponse, error) {
-	res := &apiv1.DumpPublicIdActionMapResponse{}
-	res.Contents = make(map[string]*apiv1.ActionEntityPair)
-
-	if !cfg.InsecureAllowDumpActionMap {
-		res.Alert = "Dumping Public IDs is disallowed."
-
-		return res, nil
-	}
-
-	api.executor.MapActionIdToBindingLock.RLock()
-
-	for k, v := range api.executor.MapActionIdToBinding {
-		res.Contents[k] = &apiv1.ActionEntityPair{
-			ActionTitle:  v.Action.Title,
-			EntityPrefix: v.EntityPrefix,
-		}
-	}
-
-	api.executor.MapActionIdToBindingLock.RUnlock()
-
-	res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpActionMap = false again after you don't need it anymore"
-
-	return res, nil
-}
-
-func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *apiv1.GetReadyzRequest) (*apiv1.GetReadyzResponse, error) {
-	res := &apiv1.GetReadyzResponse{
-		Status: "OK",
-	}
-
-	return res, nil
-}
-
-// Start will start the GRPC API.
-func Start(globalConfig *config.Config, ex *executor.Executor) {
-	cfg = globalConfig
-
-	log.WithFields(log.Fields{
-		"address": cfg.ListenAddressGrpcActions,
-	}).Info("Starting gRPC API")
-
-	lis, err := net.Listen("tcp", cfg.ListenAddressGrpcActions)
-
-	if err != nil {
-		log.Fatalf("Failed to listen - %v", err)
-	}
-
-	grpcServer := grpc.NewServer()
-	apiv1.RegisterOliveTinApiServiceServer(grpcServer, newServer(ex))
-
-	err = grpcServer.Serve(lis)
-
-	if err != nil {
-		log.Fatalf("Could not start gRPC Server - %v", err)
-	}
-}
-
-func newServer(ex *executor.Executor) *oliveTinAPI {
-	server := oliveTinAPI{}
-	server.executor = ex
-	return &server
-}

+ 3 - 8
service/internal/httpservers/httpServer.go

@@ -2,20 +2,15 @@ package httpservers
 
 import (
 	config "github.com/OliveTin/OliveTin/internal/config"
+	"github.com/OliveTin/OliveTin/internal/executor"
 )
 
 // StartServers will start 3 HTTP servers. The WebUI, the Rest API, and a proxy
 // for both of them.
-func StartServers(cfg *config.Config) {
-	go startWebUIServer(cfg)
-
-	if cfg.UseSingleHTTPFrontend {
-		go StartSingleHTTPFrontend(cfg)
-	}
-
+func StartServers(cfg *config.Config, ex *executor.Executor) {
 	if cfg.Prometheus.Enabled {
 		go StartPrometheus(cfg)
 	}
 
-	startRestAPIServer(cfg)
+	StartSingleHTTPFrontend(cfg, ex)
 }

+ 19 - 76
service/internal/httpservers/restapi.go

@@ -4,25 +4,17 @@ import (
 	"context"
 	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
 	log "github.com/sirupsen/logrus"
-	"google.golang.org/grpc"
-	"google.golang.org/grpc/credentials/insecure"
 	"google.golang.org/grpc/metadata"
-	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/reflect/protoreflect"
 	"net/http"
 	"strings"
 
-	apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
+//	apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
 
 	config "github.com/OliveTin/OliveTin/internal/config"
-	cors "github.com/OliveTin/OliveTin/internal/cors"
 )
 
-var (
-	cfg *config.Config
-)
-
-func parseHttpHeaderForAuth(req *http.Request) (string, string) {
+func parseHttpHeaderForAuth(cfg *config.Config, req *http.Request) (string, string) {
 	username, ok := req.Header[cfg.AuthHttpHeaderUsername]
 
 	if !ok {
@@ -49,34 +41,34 @@ func parseHttpHeaderForAuth(req *http.Request) (string, string) {
 }
 
 //gocyclo:ignore
-func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
+func parseRequestMetadata(cfg *config.Config, ctx context.Context, req *http.Request) metadata.MD {
 	username := ""
 	usergroup := ""
 	provider := "unknown"
 	sid := ""
 
 	if cfg.AuthJwtHeader != "" {
-		username, usergroup = parseJwtHeader(req)
+		username, usergroup = parseJwtHeader(cfg, req)
 		provider = "jwt-header"
 	}
 
 	if cfg.AuthJwtCookieName != "" {
-		username, usergroup = parseJwtCookie(req)
+		username, usergroup = parseJwtCookie(cfg, req)
 		provider = "jwt-cookie"
 	}
 
 	if cfg.AuthHttpHeaderUsername != "" && username == "" {
-		username, usergroup = parseHttpHeaderForAuth(req)
+		username, usergroup = parseHttpHeaderForAuth(cfg, req)
 		provider = "http-header"
 	}
 
-	if len(cfg.AuthOAuth2Providers) > 0 && username == "" {
-		username, usergroup, sid = parseOAuth2Cookie(req)
-		provider = "oauth2"
-	}
+//	if len(cfg.AuthOAuth2Providers) > 0 && username == "" {
+//		username, usergroup, sid = parseOAuth2Cookie(req)
+//		provider = "oauth2"
+//	}
 
 	if cfg.AuthLocalUsers.Enabled && username == "" {
-		username, usergroup, sid = parseLocalUserCookie(req)
+		username, usergroup, sid = parseLocalUserCookie(cfg, req)
 		provider = "local"
 	}
 
@@ -92,12 +84,12 @@ func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
 	return md
 }
 
-func parseJwtHeader(req *http.Request) (string, string) {
+func parseJwtHeader(cfg *config.Config, req *http.Request) (string, string) {
 	// JWTs in the Authorization header are usually prefixed with "Bearer " which is not part of the JWT token.
-	return parseJwt(strings.TrimPrefix(req.Header.Get(cfg.AuthJwtHeader), "Bearer "))
+	return parseJwt(cfg, strings.TrimPrefix(req.Header.Get(cfg.AuthJwtHeader), "Bearer "))
 }
 
-func forwardResponseHandler(ctx context.Context, w http.ResponseWriter, msg protoreflect.ProtoMessage) error {
+func (h *OAuth2Handler) forwardResponseHandler(cfg *config.Config, ctx context.Context, w http.ResponseWriter, msg protoreflect.ProtoMessage) error {
 	md, ok := runtime.ServerMetadataFromContext(ctx)
 
 	if !ok {
@@ -105,17 +97,17 @@ func forwardResponseHandler(ctx context.Context, w http.ResponseWriter, msg prot
 		return nil
 	}
 
-	forwardResponseHandlerLoginLocalUser(md.HeaderMD, w)
-	forwardResponseHandlerLogout(md.HeaderMD, w)
+	forwardResponseHandlerLoginLocalUser(cfg, md.HeaderMD, w)
+	h.forwardResponseHandlerLogout(cfg, md.HeaderMD, w)
 
 	return nil
 }
 
-func forwardResponseHandlerLogout(md metadata.MD, w http.ResponseWriter) {
+func (h *OAuth2Handler) forwardResponseHandlerLogout(cfg *config.Config, md metadata.MD, w http.ResponseWriter) {
 	if getMetadataKeyOrEmpty(md, "logout-provider") != "" {
 		sid := getMetadataKeyOrEmpty(md, "logout-sid")
 
-		delete(registeredStates, sid)
+		delete(h.registeredStates, sid)
 		http.SetCookie(
 			w,
 			&http.Cookie{
@@ -127,7 +119,7 @@ func forwardResponseHandlerLogout(md metadata.MD, w http.ResponseWriter) {
 			},
 		)
 
-		deleteLocalUserSession("local", sid)
+		deleteLocalUserSession(cfg, "local", sid)
 
 		http.SetCookie(
 			w,
@@ -156,52 +148,3 @@ func getMetadataKeyOrEmpty(md metadata.MD, key string) string {
 	return ""
 }
 
-func SetGlobalRestConfig(config *config.Config) {
-	cfg = config
-}
-
-func startRestAPIServer(globalConfig *config.Config) error {
-	cfg = globalConfig
-
-	loadUserSessions()
-
-	log.WithFields(log.Fields{
-		"address": cfg.ListenAddressRestActions,
-	}).Info("Starting REST API")
-
-	mux := newMux()
-
-	return http.ListenAndServe(cfg.ListenAddressRestActions, cors.AllowCors(mux))
-}
-
-func newMux() *runtime.ServeMux {
-	// The MarshalOptions set some important compatibility settings for the webui. See below.
-	mux := runtime.NewServeMux(
-		runtime.WithMetadata(parseRequestMetadata),
-		runtime.WithForwardResponseOption(forwardResponseHandler),
-		runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
-			Marshaler: &runtime.JSONPb{
-				MarshalOptions: protojson.MarshalOptions{
-					UseProtoNames:   false, // eg: canExec for js instead of can_exec from protobuf
-					EmitUnpopulated: true,  // Emit empty fields so that javascript does not get "undefined" when accessing fields with empty values.
-				},
-			},
-		}),
-	)
-
-	ctx := context.Background()
-
-	opts := []grpc.DialOption{
-		grpc.WithTransportCredentials(
-			insecure.NewCredentials(),
-		),
-	}
-
-	err := apiv1.RegisterOliveTinApiServiceHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
-
-	if err != nil {
-		log.Panicf("Could not register REST API Handler %v", err)
-	}
-
-	return mux
-}

+ 10 - 10
service/internal/httpservers/restapi_auth.go

@@ -45,14 +45,14 @@ func registerSessionProvider(provider string) {
 	}
 }
 
-func deleteLocalUserSession(provider string, sid string) {
+func deleteLocalUserSession(cfg *config.Config, provider string, sid string) {
 	sessionStorageMutex.Lock()
 
 	deleteLocalUserSessionBatch(provider, sid)
 
 	sessionStorageMutex.Unlock()
 
-	saveUserSessions()
+	saveUserSessions(cfg)
 }
 
 func deleteLocalUserSessionBatch(provider string, sid string) {
@@ -68,7 +68,7 @@ func deleteLocalUserSessionBatch(provider string, sid string) {
 	delete(sessionStorage.Providers[provider].Sessions, sid)
 }
 
-func registerUserSession(provider string, sid string, username string) {
+func registerUserSession(cfg *config.Config, provider string, sid string, username string) {
 	sessionStorageMutex.Lock()
 	sessionStorage.Providers[provider].Sessions[sid] = &UserSession{
 		Username: username,
@@ -76,10 +76,10 @@ func registerUserSession(provider string, sid string, username string) {
 	}
 	sessionStorageMutex.Unlock()
 
-	saveUserSessions()
+	saveUserSessions(cfg)
 }
 
-func saveUserSessions() {
+func saveUserSessions(cfg *config.Config) {
 	sessionStorageMutex.Lock()
 	defer sessionStorageMutex.Unlock()
 
@@ -97,7 +97,7 @@ func saveUserSessions() {
 	filehelper.WriteFile(filename, out)
 }
 
-func loadUserSessions() {
+func loadUserSessions(cfg *config.Config) {
 	registerSessionProviders()
 
 	filename := filepath.Join(cfg.GetDir(), "sessions.db.yaml")
@@ -124,10 +124,10 @@ func loadUserSessions() {
 		return
 	}
 
-	deleteExpiredSessions()
+	deleteExpiredSessions(cfg)
 }
 
-func deleteExpiredSessions() {
+func deleteExpiredSessions(cfg *config.Config) {
 	sessionStorageMutex.Lock()
 
 	for provider, sessions := range sessionStorage.Providers {
@@ -140,10 +140,10 @@ func deleteExpiredSessions() {
 
 	sessionStorageMutex.Unlock()
 
-	saveUserSessions()
+	saveUserSessions(cfg)
 }
 
-func getUserFromSession(providerName string, sid string) *config.LocalUser {
+func getUserFromSession(cfg *config.Config, providerName string, sid string) *config.LocalUser {
 	provider, ok := sessionStorage.Providers[providerName]
 
 	if !ok {

+ 19 - 17
service/internal/httpservers/restapi_auth_jwt.go

@@ -11,6 +11,8 @@ import (
 	"os"
 	"strings"
 
+	"github.com/OliveTin/OliveTin/internal/config"
+
 	//	"github.com/coreos/go-oidc/v3/oidc"
 	"github.com/MicahParks/keyfunc/v3"
 	"time"
@@ -23,7 +25,7 @@ var (
 	jwksVerifier keyfunc.Keyfunc
 )
 
-func initJwks() {
+func initJwks(cfg *config.Config) {
 	if jwksVerifier == nil {
 		var err error
 
@@ -43,7 +45,7 @@ func initJwks() {
 	}
 }
 
-func readLocalPublicKey() error {
+func readLocalPublicKey(cfg *config.Config) error {
 	if pubKeyBytes != nil {
 		return nil // Already read.
 	}
@@ -62,14 +64,14 @@ func readLocalPublicKey() error {
 	return nil
 }
 
-func parseJwtTokenWithRemoteKey(jwtToken string) (*jwt.Token, error) {
-	initJwks()
+func parseJwtTokenWithRemoteKey(cfg *config.Config, jwtToken string) (*jwt.Token, error) {
+	initJwks(cfg)
 
 	return jwt.Parse(jwtToken, jwksVerifier.Keyfunc, jwt.WithAudience(cfg.AuthJwtAud))
 }
 
-func parseJwtTokenWithLocalKey(jwtString string) (*jwt.Token, error) {
-	err := readLocalPublicKey()
+func parseJwtTokenWithLocalKey(cfg *config.Config, jwtString string) (*jwt.Token, error) {
+	err := readLocalPublicKey(cfg)
 
 	if err != nil {
 		return nil, err
@@ -85,7 +87,7 @@ func parseJwtTokenWithLocalKey(jwtString string) (*jwt.Token, error) {
 }
 
 // Hash-based Message Authentication Code
-func parseJwtTokenWithHMAC(jwtString string) (*jwt.Token, error) {
+func parseJwtTokenWithHMAC(cfg *config.Config, jwtString string) (*jwt.Token, error) {
 	return jwt.Parse(jwtString, func(token *jwt.Token) (interface{}, error) {
 		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
 			return nil, fmt.Errorf("parseJwt expected token algorithm HMAC but got: %v", token.Header["alg"])
@@ -95,20 +97,20 @@ func parseJwtTokenWithHMAC(jwtString string) (*jwt.Token, error) {
 	})
 }
 
-func parseJwtToken(jwtString string) (*jwt.Token, error) {
+func parseJwtToken(cfg *config.Config, jwtString string) (*jwt.Token, error) {
 	if cfg.AuthJwtCertsURL != "" {
-		return parseJwtTokenWithRemoteKey(jwtString)
+		return parseJwtTokenWithRemoteKey(cfg, jwtString)
 	}
 
 	if cfg.AuthJwtPubKeyPath != "" {
-		return parseJwtTokenWithLocalKey(jwtString)
+		return parseJwtTokenWithLocalKey(cfg, jwtString)
 	}
 
-	return parseJwtTokenWithHMAC(jwtString)
+	return parseJwtTokenWithHMAC(cfg, jwtString)
 }
 
-func getClaimsFromJwtToken(jwtString string) (jwt.MapClaims, error) {
-	token, err := parseJwtToken(jwtString)
+func getClaimsFromJwtToken(cfg *config.Config, jwtString string) (jwt.MapClaims, error) {
+	token, err := parseJwtToken(cfg, jwtString)
 
 	if err != nil {
 		log.Errorf("jwt parse failure: %v", err)
@@ -130,7 +132,7 @@ func lookupClaimValueOrDefault(claims jwt.MapClaims, key string, def string) str
 	}
 }
 
-func parseJwtCookie(request *http.Request) (string, string) {
+func parseJwtCookie(cfg *config.Config, request *http.Request) (string, string) {
 	cookie, err := request.Cookie(cfg.AuthJwtCookieName)
 
 	if err != nil {
@@ -138,11 +140,11 @@ func parseJwtCookie(request *http.Request) (string, string) {
 		return "", ""
 	}
 
-	return parseJwt(cookie.Value)
+	return parseJwt(cfg, cookie.Value)
 }
 
-func parseJwt(token string) (string, string) {
-	claims, err := getClaimsFromJwtToken(token)
+func parseJwt(cfg *config.Config, token string) (string, string) {
+	claims, err := getClaimsFromJwtToken(cfg, token)
 
 	if err != nil {
 		log.Warnf("jwt claim error: %+v", err)

+ 5 - 4
service/internal/httpservers/restapi_auth_local.go

@@ -2,12 +2,13 @@ package httpservers
 
 import (
 	"google.golang.org/grpc/metadata"
+	"github.com/OliveTin/OliveTin/internal/config"
 	"net/http"
 
 	"github.com/google/uuid"
 )
 
-func parseLocalUserCookie(req *http.Request) (string, string, string) {
+func parseLocalUserCookie(cfg *config.Config, req *http.Request) (string, string, string) {
 	cookie, err := req.Cookie("olivetin-sid-local")
 
 	if err != nil {
@@ -16,7 +17,7 @@ func parseLocalUserCookie(req *http.Request) (string, string, string) {
 
 	cookieValue := cookie.Value
 
-	user := getUserFromSession("local", cookieValue)
+	user := getUserFromSession(cfg, "local", cookieValue)
 
 	if user == nil {
 		return "", "", ""
@@ -25,7 +26,7 @@ func parseLocalUserCookie(req *http.Request) (string, string, string) {
 	return user.Username, user.Usergroup, cookie.Value
 }
 
-func forwardResponseHandlerLoginLocalUser(md metadata.MD, w http.ResponseWriter) error {
+func forwardResponseHandlerLoginLocalUser(cfg *config.Config, md metadata.MD, w http.ResponseWriter) error {
 	setUsername := getMetadataKeyOrEmpty(md, "set-username")
 
 	if setUsername != "" {
@@ -36,7 +37,7 @@ func forwardResponseHandlerLoginLocalUser(md metadata.MD, w http.ResponseWriter)
 		}
 
 		sid := uuid.NewString()
-		registerUserSession("local", sid, user.Username)
+		registerUserSession(cfg, "local", sid, user.Username)
 
 		http.SetCookie(
 			w,

+ 45 - 35
service/internal/httpservers/restapi_auth_oauth2.go

@@ -17,25 +17,20 @@ import (
 	"time"
 )
 
-var (
-	registeredStates    = make(map[string]*oauth2State)
-	registeredProviders = make(map[string]*oauth2.Config)
-)
-
-type oauth2State struct {
-	providerConfig *oauth2.Config
-	providerName   string
-	Username       string
-	Usergroup      string
+type OAuth2Handler struct {
+	cfg *config.Config
+	registeredStates    map[string]*oauth2State
+	registeredProviders map[string]*oauth2.Config
 }
 
-func assignIfEmpty(target *string, value string) {
-	if *target == "" {
-		*target = value
+func NewOAuth2Handler(cfg *config.Config) *OAuth2Handler {
+	h := &OAuth2Handler{
+		cfg: cfg,
 	}
-}
 
-func oauth2Init(cfg *config.Config) {
+	h.registeredStates    = make(map[string]*oauth2State)
+	h.registeredProviders = make(map[string]*oauth2.Config)
+
 	for providerName, providerConfig := range cfg.AuthOAuth2Providers {
 		completeProviderConfig(providerName, providerConfig)
 
@@ -50,10 +45,25 @@ func oauth2Init(cfg *config.Config) {
 			RedirectURL: cfg.AuthOAuth2RedirectURL,
 		}
 
-		registeredProviders[providerName] = newConfig
+		h.registeredProviders[providerName] = newConfig
 
 		log.Debugf("Dumping newly registered provider: %v = %+v", providerName, providerConfig)
 	}
+
+	return h
+}
+
+type oauth2State struct {
+	providerConfig *oauth2.Config
+	providerName   string
+	Username       string
+	Usergroup      string
+}
+
+func assignIfEmpty(target *string, value string) {
+	if *target == "" {
+		*target = value
+	}
 }
 
 func completeProviderConfig(providerName string, providerConfig *config.OAuth2Provider) {
@@ -76,8 +86,8 @@ func completeProviderConfig(providerName string, providerConfig *config.OAuth2Pr
 	}
 }
 
-func getOAuth2Config(providerName string) (*oauth2.Config, error) {
-	config, ok := registeredProviders[providerName]
+func (h *OAuth2Handler) getOAuth2Config(providerName string) (*oauth2.Config, error) {
+	config, ok := h.registeredProviders[providerName]
 
 	if !ok {
 		return nil, fmt.Errorf("provider not found in config: %v", providerName)
@@ -96,7 +106,7 @@ func randString(nByte int) (string, error) {
 	return base64.URLEncoding.EncodeToString(b), nil
 }
 
-func setOAuthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
+func (h *OAuth2Handler) setOAuthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
 	cookie := &http.Cookie{
 		Name:     name,
 		Value:    value,
@@ -109,7 +119,7 @@ func setOAuthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value
 	http.SetCookie(w, cookie)
 }
 
-func handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
+func (h *OAuth2Handler) handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
 	state, err := randString(16)
 
 	if err != nil {
@@ -118,7 +128,7 @@ func handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
 	}
 
 	providerName := r.URL.Query().Get("provider")
-	provider, err := getOAuth2Config(providerName)
+	provider, err := h.getOAuth2Config(providerName)
 
 	if err != nil {
 		log.Errorf("Failed to get provider config: %v %v", providerName, err)
@@ -126,20 +136,20 @@ func handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	registeredStates[state] = &oauth2State{
+	h.registeredStates[state] = &oauth2State{
 		providerConfig: provider,
 		providerName:   providerName,
 		Username:       "",
 	}
 
-	setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state)
+	h.setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state)
 
 	log.Infof("OAuth2 state: %v mapped to provider %v (found: %v), now redirecting", state, providerName, provider != nil)
 
 	http.Redirect(w, r, provider.AuthCodeURL(state), http.StatusFound)
 }
 
-func checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2State, string, bool) {
+func (h *OAuth2Handler) checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2State, string, bool) {
 	cookie, err := r.Cookie("olivetin-sid-oauth")
 	state := cookie.Value
 
@@ -157,7 +167,7 @@ func checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2St
 		return nil, state, false
 	}
 
-	registeredState, ok := registeredStates[state]
+	registeredState, ok := h.registeredStates[state]
 
 	if !ok {
 		log.Errorf("State not found in server: %v", state)
@@ -206,10 +216,10 @@ func getOAuthCertBundle(providerConfig *config.OAuth2Provider) *x509.CertPool {
 	return caCertPool
 }
 
-func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
+func (h *OAuth2Handler) handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
 	log.Infof("OAuth2 Callback received")
 
-	registeredState, state, ok := checkOAuthCallbackCookie(w, r)
+	registeredState, state, ok := h.checkOAuthCallbackCookie(w, r)
 
 	if !ok {
 		return
@@ -222,7 +232,7 @@ func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
 		"token-code": code,
 	}).Debug("OAuth2 Token Code")
 
-	providerConfig := cfg.AuthOAuth2Providers[registeredState.providerName]
+	providerConfig := h.cfg.AuthOAuth2Providers[registeredState.providerName]
 
 	clientSettings := getOAuth2HttpClient(providerConfig)
 
@@ -250,18 +260,18 @@ func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
 		Timeout: clientSettings.Timeout,
 	}
 
-	userinfo := getUserInfo(userInfoClient, cfg.AuthOAuth2Providers[registeredState.providerName])
+	userinfo := getUserInfo(userInfoClient, h.cfg.AuthOAuth2Providers[registeredState.providerName])
 
-	registeredStates[state].Username = userinfo.Username
-	registeredStates[state].Usergroup = userinfo.Usergroup
+	h.registeredStates[state].Username = userinfo.Username
+	h.registeredStates[state].Usergroup = userinfo.Usergroup
 
-	for k, v := range registeredStates {
+	for k, v := range h.registeredStates {
 		log.Debugf("states: %+v %+v", k, v)
 	}
 
 	log.WithFields(log.Fields{
 		"state":    state,
-		"username": registeredStates[state].Username,
+		"username": h.registeredStates[state].Username,
 	}).Info("OAuth2 login successful")
 
 	http.Redirect(w, r, "/", http.StatusFound)
@@ -330,7 +340,7 @@ func getDataField(data map[string]any, field string) string {
 	return val.(string)
 }
 
-func parseOAuth2Cookie(r *http.Request) (string, string, string) {
+func (h *OAuth2Handler) parseOAuth2Cookie(r *http.Request) (string, string, string) {
 	cookie, err := r.Cookie("olivetin-sid-oauth")
 
 	if err != nil {
@@ -342,7 +352,7 @@ func parseOAuth2Cookie(r *http.Request) (string, string, string) {
 		return "", "", ""
 	}
 
-	serverState, found := registeredStates[cookie.Value]
+	serverState, found := h.registeredStates[cookie.Value]
 
 	if !found {
 		log.WithFields(log.Fields{

+ 21 - 23
service/internal/httpservers/singleFrontend.go

@@ -10,11 +10,13 @@ away, and several other issues.
 
 import (
 	config "github.com/OliveTin/OliveTin/internal/config"
-	"github.com/OliveTin/OliveTin/internal/websocket"
+	"github.com/OliveTin/OliveTin/internal/api"
+	"github.com/OliveTin/OliveTin/internal/executor"
 	log "github.com/sirupsen/logrus"
 	"net/http"
 	"net/http/httputil"
 	"net/url"
+	"path"
 )
 
 func logDebugRequest(cfg *config.Config, source string, r *http.Request) {
@@ -31,42 +33,40 @@ func logDebugRequest(cfg *config.Config, source string, r *http.Request) {
 
 // StartSingleHTTPFrontend will create a reverse proxy that proxies the API
 // and webui internally.
-func StartSingleHTTPFrontend(cfg *config.Config) {
+func StartSingleHTTPFrontend(cfg *config.Config, ex *executor.Executor) {
 	log.WithFields(log.Fields{
 		"address": cfg.ListenAddressSingleHTTPFrontend,
 	}).Info("Starting single HTTP frontend")
 
-	apiURL, _ := url.Parse("http://" + cfg.ListenAddressRestActions)
-	apiProxy := httputil.NewSingleHostReverseProxy(apiURL)
+	mux := http.NewServeMux()
 
-	webuiURL, _ := url.Parse("http://" + cfg.ListenAddressWebUI)
-	webuiProxy := httputil.NewSingleHostReverseProxy(webuiURL)
+	apiPath, apiHandler := api.GetNewHandler(ex)
 
-	mux := http.NewServeMux()
+	log.Infof("API path is %s", apiPath)
 
-	mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
-		logDebugRequest(cfg, "api ", r)
+	mux.Handle("/api/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		fn := path.Base(r.URL.Path)
 
-		apiProxy.ServeHTTP(w, r)
-	})
+		r.URL.Path = apiPath + fn
 
-	mux.HandleFunc("/websocket", func(w http.ResponseWriter, r *http.Request) {
-		logDebugRequest(cfg, "ws  ", r)
+		log.Infof("SingleFrontend HTTP API Req URL after rewrite: %v", r.URL.Path)
 
-		websocket.HandleWebsocket(w, r)
-	})
+		apiHandler.ServeHTTP(w, r)
+	}))
 
-	mux.HandleFunc("/oauth/login", handleOAuthLogin)
+	oauth2handler := NewOAuth2Handler(cfg) 
 
-	mux.HandleFunc("/oauth/callback", handleOAuthCallback)
+	mux.HandleFunc("/oauth/login", oauth2handler.handleOAuthLogin)
+	mux.HandleFunc("/oauth/callback", oauth2handler.handleOAuthCallback)
 
 	mux.HandleFunc("/readyz", handleReadyz)
 
-	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		logDebugRequest(cfg, "ui  ", r)
+	webuiServer := NewWebUIServer(cfg)
 
-		webuiProxy.ServeHTTP(w, r)
-	})
+	mux.HandleFunc("/webUiSettings.json", webuiServer.generateWebUISettings)
+	mux.HandleFunc("/theme.css", webuiServer.generateThemeCss)	
+	mux.Handle("/custom-webui/", webuiServer.handleCustomWebui())
+	mux.HandleFunc("/", webuiServer.handleWebui)
 
 	if cfg.Prometheus.Enabled {
 		promURL, _ := url.Parse("http://" + cfg.ListenAddressPrometheus)
@@ -79,8 +79,6 @@ func StartSingleHTTPFrontend(cfg *config.Config) {
 		})
 	}
 
-	oauth2Init(cfg)
-
 	srv := &http.Server{
 		Addr:    cfg.ListenAddressSingleHTTPFrontend,
 		Handler: mux,

+ 71 - 73
service/internal/httpservers/webuiServer.go

@@ -7,13 +7,20 @@ import (
 	"net/http"
 	"os"
 	"path"
-	"path/filepath"
+
+	"github.com/jamesread/golure/pkg/dirs"
 
 	config "github.com/OliveTin/OliveTin/internal/config"
 	installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
 	sv "github.com/OliveTin/OliveTin/internal/stringvariables"
 )
 
+type webUIServer struct {
+	cfg *config.Config
+
+	webuiDir string
+}
+
 var (
 	customThemeCss     []byte
 	customThemeCssRead = false
@@ -36,46 +43,65 @@ type webUISettings struct {
 	AdditionalLinks        []*config.NavigationLink
 }
 
-func findWebuiDir() string {
+func NewWebUIServer(cfg *config.Config) *webUIServer {
+	s := &webUIServer{
+		cfg: cfg,
+	}
+
+	s.webuiDir = s.findWebuiDir()
+	s.setupCustomWebuiDir()
+
+	return s
+}
+
+func (s *webUIServer) handleWebui(w http.ResponseWriter, r *http.Request) {
+	//dirName := path.Dir(r.URL.Path)
+
+	// Mangle requests for any path like /logs or /config to load the webui index.html
+	if path.Ext(r.URL.Path) == "" && r.URL.Path != "/" {
+		log.Debugf("Mangling request for %s to /index.html", r.URL.Path)
+
+		http.ServeFile(w, r, path.Join(s.webuiDir, "index.html"))
+	} else {
+		log.Infof("Serving webui from %s for %s", s.webuiDir, r.URL.Path)
+		http.ServeFile(w, r, path.Join(s.webuiDir, r.URL.Path))
+//		http.StripPrefix(dirName, http.FileServer(http.Dir(s.webuiDir))).ServeHTTP(w, r)
+	}
+}
+
+
+func (s *webUIServer) findWebuiDir() string {
 	directoriesToSearch := []string{
-		cfg.WebUIDir,
-		"../webui/",
-		"/usr/share/OliveTin/webui/",
+		s.cfg.WebUIDir,
+		"../frontend/dist/",
+		"../frontend/",
+		"/usr/share/OliveTin/frontend/",
 		"/var/www/OliveTin/",
 		"/var/www/olivetin/",
-		"/etc/OliveTin/webui/",
+		"/etc/OliveTin/frontend/",
 	}
 
-	// Use a classic i := 0 style for loop here instead of range, as the
-	// search order must be deterministic - the order that the slice was defined in.
-	for i := 0; i < len(directoriesToSearch); i++ {
-		dir := directoriesToSearch[i]
-		absdir, _ := filepath.Abs(dir)
+	dir, err := dirs.GetFirstExistingDirectory("webui", directoriesToSearch)
 
-		if _, err := os.Stat(absdir); !os.IsNotExist(err) {
-			log.WithFields(log.Fields{
-				"dir": absdir,
-			}).Infof("Found the webui directory")
-
-			sv.Set("internal.webuidir", absdir+" ("+dir+")")
+	if err != nil {
+		log.Warnf("Did not find the webui directory, you will probably get 404 errors.")
 
-			return dir
-		}
+		return "./webui" // Should not exist
 	}
 
-	log.Warnf("Did not find the webui directory, you will probably get 404 errors.")
+	log.Infof("Using webui directory: %s", dir)
 
-	return "./webui" // Should not exist
+	return dir
 }
 
-func findCustomWebuiDir() string {
-	dir := path.Join(cfg.GetDir(), "custom-webui")
+func (s *webUIServer) findCustomWebuiDir() string {
+	dir := path.Join(s.cfg.GetDir(), "custom-webui")
 
 	return dir
 }
 
-func setupCustomWebuiDir() {
-	dir := findCustomWebuiDir()
+func (s *webUIServer) setupCustomWebuiDir() {
+	dir := s.findCustomWebuiDir()
 
 	err := os.MkdirAll(path.Join(dir, "themes/"), 0775)
 
@@ -87,10 +113,10 @@ func setupCustomWebuiDir() {
 	}
 }
 
-func generateThemeCss(w http.ResponseWriter, r *http.Request) {
-	themeCssFilename := path.Join(findCustomWebuiDir(), "themes", cfg.ThemeName, "theme.css")
+func (s *webUIServer) generateThemeCss(w http.ResponseWriter, r *http.Request) {
+	themeCssFilename := path.Join(s.findCustomWebuiDir(), "themes", s.cfg.ThemeName, "theme.css")
 
-	if !customThemeCssRead || cfg.ThemeCacheDisabled {
+	if !customThemeCssRead || s.cfg.ThemeCacheDisabled {
 		customThemeCssRead = true
 
 		if _, err := os.Stat(themeCssFilename); err == nil {
@@ -125,22 +151,24 @@ func buildPublicOAuth2ProvidersList(cfg *config.Config) []publicOAuth2Provider {
 	return publicProviders
 }
 
-func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
+func (s *webUIServer) generateWebUISettings(w http.ResponseWriter, r *http.Request) {
+	log.Infof("Generating webui settings for %s", r.RemoteAddr)
+
 	jsonRet, _ := json.Marshal(webUISettings{
-		Rest:                   cfg.ExternalRestAddress + "/api/",
-		ShowFooter:             cfg.ShowFooter,
-		ShowNavigation:         cfg.ShowNavigation,
-		ShowNewVersions:        cfg.ShowNewVersions,
+		Rest:                   s.cfg.ExternalRestAddress + "/api/",
+		ShowFooter:             s.cfg.ShowFooter,
+		ShowNavigation:         s.cfg.ShowNavigation,
+		ShowNewVersions:        s.cfg.ShowNewVersions,
 		AvailableVersion:       installationinfo.Runtime.AvailableVersion,
 		CurrentVersion:         installationinfo.Build.Version,
-		PageTitle:              cfg.PageTitle,
-		SectionNavigationStyle: cfg.SectionNavigationStyle,
-		DefaultIconForBack:     cfg.DefaultIconForBack,
-		EnableCustomJs:         cfg.EnableCustomJs,
-		AuthLoginUrl:           cfg.AuthLoginUrl,
-		AuthLocalLogin:         cfg.AuthLocalUsers.Enabled,
-		AuthOAuth2Providers:    buildPublicOAuth2ProvidersList(cfg),
-		AdditionalLinks:        cfg.AdditionalNavigationLinks,
+		PageTitle:              s.cfg.PageTitle,
+		SectionNavigationStyle: s.cfg.SectionNavigationStyle,
+		DefaultIconForBack:     s.cfg.DefaultIconForBack,
+		EnableCustomJs:         s.cfg.EnableCustomJs,
+		AuthLoginUrl:           s.cfg.AuthLoginUrl,
+		AuthLocalLogin:         s.cfg.AuthLocalUsers.Enabled,
+		AuthOAuth2Providers:    buildPublicOAuth2ProvidersList(s.cfg),
+		AdditionalLinks:        s.cfg.AdditionalNavigationLinks,
 	})
 
 	w.Header().Add("Content-Type", "application/json")
@@ -151,36 +179,6 @@ func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func startWebUIServer(cfg *config.Config) {
-	log.WithFields(log.Fields{
-		"address": cfg.ListenAddressWebUI,
-	}).Info("Starting WebUI server")
-
-	setupCustomWebuiDir()
-
-	mux := http.NewServeMux()
-	mux.Handle("/custom-webui/", http.StripPrefix("/custom-webui/", http.FileServer(http.Dir(findCustomWebuiDir()))))
-	mux.HandleFunc("/theme.css", generateThemeCss)
-	mux.HandleFunc("/webUiSettings.json", generateWebUISettings)
-
-	webuiDir := findWebuiDir()
-	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		dirName := path.Dir(r.URL.Path)
-
-		// Mangle requests for any path like /logs or /config to load the webui index.html
-		if path.Ext(r.URL.Path) == "" && r.URL.Path != "/" {
-			log.Debugf("Mangling request for %s to /index.html", r.URL.Path)
-
-			http.ServeFile(w, r, path.Join(webuiDir, "index.html"))
-		} else {
-			http.StripPrefix(dirName, http.FileServer(http.Dir(webuiDir))).ServeHTTP(w, r)
-		}
-	})
-
-	srv := &http.Server{
-		Addr:    cfg.ListenAddressWebUI,
-		Handler: mux,
-	}
-
-	log.Fatal(srv.ListenAndServe())
+func (s *webUIServer) handleCustomWebui() (http.Handler) {
+	return http.StripPrefix("/custom-webui/", http.FileServer(http.Dir(s.findCustomWebuiDir())))
 }

+ 1 - 1
service/internal/websocket/websocket.go

@@ -4,7 +4,7 @@ import (
 	"net/http"
 	"sync"
 
-	apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
+	apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
 	"github.com/OliveTin/OliveTin/internal/executor"
 	ws "github.com/gorilla/websocket"
 	log "github.com/sirupsen/logrus"

+ 1 - 4
service/main.go

@@ -7,7 +7,6 @@ import (
 
 	"github.com/OliveTin/OliveTin/internal/entityfiles"
 	"github.com/OliveTin/OliveTin/internal/executor"
-	grpcapi "github.com/OliveTin/OliveTin/internal/grpcapi"
 	"github.com/OliveTin/OliveTin/internal/httpservers"
 	"github.com/OliveTin/OliveTin/internal/installationinfo"
 	"github.com/OliveTin/OliveTin/internal/oncalendarfile"
@@ -179,7 +178,5 @@ func main() {
 
 	go updatecheck.StartUpdateChecker(cfg)
 
-	go grpcapi.Start(cfg, executor)
-
-	httpservers.StartServers(cfg)
+	httpservers.StartServers(cfg, executor)
 }

+ 0 - 2
service/tools.go

@@ -7,8 +7,6 @@ import (
 	_ "github.com/bufbuild/buf/cmd/buf"
 	_ "github.com/fzipp/gocyclo/cmd/gocyclo"
 	_ "github.com/go-critic/go-critic/cmd/gocritic"
-	_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
-	_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
 	_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
 	_ "google.golang.org/protobuf/cmd/protoc-gen-go"
 )

+ 6 - 0
var/entities/lights.yaml

@@ -0,0 +1,6 @@
+- id: kitchen
+  name: Kitchen
+  turned_on: false
+- id: living_room
+  name: Living Room
+  turned_on: false

+ 92 - 0
var/marketing/OliveTinLogoMonocrome.svg

@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="100mm"
+   height="100mm"
+   viewBox="0 0 100 100"
+   version="1.1"
+   id="svg8"
+   inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
+   sodipodi:docname="OliveTinLogoMonocrome.svg"
+   inkscape:export-filename="/home/xconspirisist/sandbox/OliveTin/webui/OliveTinLogo.png"
+   inkscape:export-xdpi="96"
+   inkscape:export-ydpi="96"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="214.5"
+     inkscape:cy="130.5"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     inkscape:document-rotation="0"
+     showgrid="false"
+     inkscape:pagecheckerboard="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1056"
+     inkscape:window-x="1280"
+     inkscape:window-y="1104"
+     inkscape:window-maximized="1"
+     inkscape:showpageshadow="0"
+     inkscape:deskcolor="#d1d1d1" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <path
+       id="rect1402"
+       style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:5.79097;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 78.150303,10.636649 0.06022,77.69053 c 0,0 -0.333558,7.417739 -26.071829,7.668294 -25.738273,0.250554 -27.812975,-7.668294 -27.812975,-7.668294 l 0.02958,-77.69053"
+       sodipodi:nodetypes="cczcc" />
+    <path
+       id="rect1469-7"
+       style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 26.974,41.794388 23.923879,2.635333 24.529783,-2.635333 v 21.176145 c 0,0 -0.404835,4.427058 -24.431009,4.612696 C 26.97048,67.768868 26.974,62.970533 26.974,62.970533 Z"
+       sodipodi:nodetypes="cccczcc" />
+    <ellipse
+       style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:4.00217;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path1436"
+       ry="19.291208"
+       rx="13.695867"
+       cy="28.679092"
+       cx="71.078308"
+       transform="rotate(25.917116)" />
+    <ellipse
+       style="display:inline;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:5.79097;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path1440"
+       cx="51.2528"
+       cy="10.636649"
+       rx="26.897503"
+       ry="6.525641" />
+    <path
+       id="path1436-3"
+       style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.574412;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       inkscape:transform-center-x="-1.1166908"
+       inkscape:transform-center-y="0.49206772"
+       d="m 36.494243,8.7784525 c 1.841786,-0.1158042 2.469235,0.5295096 2.656071,1.4191125 0.186835,0.889603 -0.0974,1.788639 -1.939188,1.904443 -0.848904,0.05338 -2.422996,-0.462298 -3.064668,-0.677573 -0.750505,-0.251787 -1.565122,-0.327933 -1.665841,-0.807506 -0.09913,-0.472029 0.614422,-0.7401021 1.22232,-1.0768269 0.537767,-0.2978804 1.926784,-0.7072919 2.791306,-0.7616496 z"
+       sodipodi:nodetypes="sssssss" />
+  </g>
+</svg>

BIN
var/marketing/mockup-laptop.png


BIN
var/marketing/mockup-laptop.xcf


+ 0 - 4
webui.dev/.parcelrc

@@ -1,4 +0,0 @@
-{
-  "extends": "@parcel/config-default",
-  "resolvers": ["parcel-resolver-ignore", "..."]
-}

+ 0 - 6
webui.dev/Makefile

@@ -1,6 +0,0 @@
-codestyle:
-	npm install
-	npx eslint --fix main.js js/*
-	npx stylelint style.css
-
-.PHONY: codestyle

+ 0 - 174
webui.dev/js/ActionButton.js

@@ -1,174 +0,0 @@
-import './ExecutionButton.js'
-import './ArgumentForm.js'
-import { ExecutionFeedbackButton } from './ExecutionFeedbackButton.js'
-
-class ActionButton extends ExecutionFeedbackButton {
-  constructDomFromTemplate () {
-    const tpl = document.getElementById('tplActionButton')
-    const content = tpl.content.cloneNode(true)
-
-    /*
-     * FIXME: Should probably be using a shadowdom here, but seem to
-     * get an error when combined with custom elements.
-     */
-
-    this.appendChild(content)
-
-    this.btn = this.querySelector('button')
-    this.domTitle = this.btn.querySelector('.title')
-    this.domIcon = this.btn.querySelector('.icon')
-  }
-
-  constructFromJson (json) {
-    this.updateIterationTimestamp = 0
-
-    this.constructDomFromTemplate()
-
-    // Class attributes
-    this.updateFromJson(json)
-
-    this.actionId = json.id
-
-    // DOM Attributes
-    this.setAttribute('role', 'none')
-    this.setAttribute('id', 'actionButton-' + this.actionId)
-
-    if (!json.canExec) {
-      this.btn.disabled = true
-    }
-
-    this.btn.setAttribute('id', 'actionButtonInner-' + this.actionId)
-    this.btn.title = json.title
-    this.btn.onclick = () => {
-      if (json.arguments.length > 0) {
-        for (const oldArgumentForm of document.querySelectorAll('argument-form')) {
-          oldArgumentForm.remove()
-        }
-
-        this.updateUrlWithAction()
-
-        const frm = document.createElement('argument-form')
-        frm.setup(json, (args) => {
-          this.startAction(args)
-        })
-
-        document.body.appendChild(frm)
-        frm.querySelector('dialog').showModal()
-      } else {
-        this.startAction()
-      }
-    }
-
-    this.popupOnStart = json.popupOnStart
-
-    this.updateFromJson(json)
-
-    this.domTitle.innerText = this.btn.title
-    this.domIcon.innerHTML = this.unicodeIcon
-  }
-
-  updateFromJson (json) {
-    // Fields that should not be updated
-    //
-    // title - as the callback URL relies on it
-
-    if (json.icon === '') {
-      this.unicodeIcon = '&#x1f4a9'
-    } else {
-      this.unicodeIcon = unescape(json.icon)
-    }
-
-    this.domIcon.innerHTML = this.unicodeIcon
-  }
-
-  onExecStatusChanged () {
-    this.btn.disabled = false
-
-    setTimeout(() => {
-      this.updateDom(null, this.btn.title)
-    }, 2000)
-  }
-
-  getUniqueId () {
-    if (window.isSecureContext) {
-      return window.crypto.randomUUID()
-    } else {
-      return Date.now().toString()
-    }
-  }
-
-  startAction (actionArgs) {
-    this.btn.classList = [] // Removes old animation classes
-
-    if (actionArgs === undefined) {
-      actionArgs = []
-    }
-
-    // UUIDs are create client side, so that we can setup a "execution-button"
-    // to track the execution before we send the request to the server.
-    const startActionArgs = {
-      actionId: this.actionId,
-      arguments: actionArgs,
-      uniqueTrackingId: this.getUniqueId()
-    }
-
-    this.onActionStarted(startActionArgs.uniqueTrackingId)
-
-    window.fetch(window.restBaseUrl + 'StartAction', {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json'
-      },
-      body: JSON.stringify(startActionArgs)
-    }).then((res) => {
-      if (res.ok) {
-        return res.json()
-      } else {
-        throw new Error(res.statusText)
-      }
-    }
-    ).then((json) => {
-      // The button used to wait for the action to finish, but now it is fire & forget
-    }).catch(err => {
-      throw err // We used to flash buttons red, but now hand to the global error handler
-    })
-  }
-
-  updateUrlWithAction () {
-    // Get the current URL and create a new URL object
-    const url = new URL(window.location.href)
-
-    // Set the action parameter
-    url.searchParams.set('action', this.btn.title)
-
-    // Update the URL without reloading the page
-    window.history.replaceState({}, '', url.toString())
-  }
-
-  onActionStarted (executionTrackingId) {
-    if (this.popupOnStart === 'execution-button') {
-      const btnExecution = document.createElement('execution-button')
-      btnExecution.constructFromJson(executionTrackingId)
-      this.querySelector('.action-button-footer').hidden = false
-      this.querySelector('.action-button-footer').style.display = 'flex'
-      this.querySelector('.action-button-footer').prepend(btnExecution)
-
-      return
-    }
-
-    if (this.popupOnStart.includes('execution-dialog')) {
-      window.executionDialog.reset()
-
-      if (this.popupOnStart === 'execution-dialog-stdout-only') {
-        window.executionDialog.hideEverythingApartFromOutput()
-      }
-
-      window.executionDialog.executionTrackingId = executionTrackingId
-      window.executionDialog.show(this)
-    }
-
-    this.btn.disabled = true
-  }
-}
-
-window.customElements.define('action-button', ActionButton)

+ 0 - 42
webui.dev/js/ActionStatusDisplay.js

@@ -1,42 +0,0 @@
-export class ActionStatusDisplay {
-  constructor (parentElement) {
-    this.exitCodeElement = document.createElement('span')
-    this.statusElement = document.createElement('span')
-    this.statusElement.innerText = 'unknown'
-
-    parentElement.innerText = ''
-    parentElement.appendChild(this.statusElement)
-    parentElement.appendChild(this.exitCodeElement)
-  }
-
-  getText () {
-    return this.statusElement.innerText
-  }
-
-  update (logEntry) {
-    this.statusElement.classList.remove(...this.statusElement.classList)
-    this.statusElement.classList.add('action-status')
-
-    if (logEntry.executionFinished) {
-      this.statusElement.innerText = 'Completed'
-      this.exitCodeElement.innerText = ' Exit code: ' + logEntry.exitCode
-
-      if (logEntry.exitCode === 0) {
-        this.statusElement.classList.add('action-success')
-      } else if (logEntry.blocked) {
-        this.statusElement.innerText = 'Blocked'
-        this.statusElement.classList.add('action-blocked')
-        this.exitCodeElement.innerText = ''
-      } else if (logEntry.timedOut) {
-        this.statusElement.innerText = 'Timed out'
-        this.statusElement.classList.add('action-timeout')
-        this.exitCodeElement.innerText = ''
-      } else {
-        this.statusElement.classList.add('action-nonzero-exit')
-      }
-    } else {
-      this.statusElement.innerText = 'Still running...'
-      this.exitCodeElement.innerText = ''
-    }
-  }
-}

+ 0 - 34
webui.dev/js/ExecutionButton.js

@@ -1,34 +0,0 @@
-import { ExecutionFeedbackButton } from './ExecutionFeedbackButton.js'
-
-class ExecutionButton extends ExecutionFeedbackButton {
-  constructFromJson (json) {
-    this.executionTrackingId = json
-    this.ellapsed = 0
-
-    this.appendChild(document.createElement('button'))
-    this.isWaiting = true
-
-    this.setAttribute('id', 'execution-' + json)
-
-    this.btn = this.querySelector('button')
-    this.btn.innerText = 'Executing...'
-    this.btn.onclick = () => {
-      this.show()
-    }
-
-    this.domTitle = this.btn
-  }
-
-  show () {
-    window.executionDialog.reset()
-    window.executionDialog.show()
-    window.executionDialog.fetchExecutionResult(this.executionTrackingId)
-  }
-
-  onExecStatusChanged () {
-    this.domTitle.innerText = this.ellapsed + 's'
-    this.btn.title = this.ellapsed + ' seconds'
-  }
-}
-
-window.customElements.define('execution-button', ExecutionButton)

+ 0 - 237
webui.dev/js/ExecutionDialog.js

@@ -1,237 +0,0 @@
-import { ActionStatusDisplay } from './ActionStatusDisplay.js'
-import { OutputTerminal } from './OutputTerminal.js'
-
-// This ExecutionDialog is NOT a custom HTML element, but rather just picks up
-// the <dialog /> element out of index.html and just re-uses that - as only
-// one dialog can be shown at a time.
-export class ExecutionDialog {
-  constructor () {
-    this.dlg = document.querySelector('dialog#execution-results-popup')
-
-    this.domIcon = document.getElementById('execution-dialog-icon')
-    this.domTitle = document.getElementById('execution-dialog-title')
-    this.domOutput = document.getElementById('execution-dialog-xterm')
-    this.domOutputHtml = document.getElementById('execution-dialog-output-html')
-    this.domOutputToggleBig = document.getElementById('execution-dialog-toggle-size')
-    this.domOutputToggleBig.onclick = () => {
-      this.toggleSize()
-    }
-
-    this.domBtnRerun = document.getElementById('execution-dialog-rerun-action')
-    this.domBtnKill = document.getElementById('execution-dialog-kill-action')
-
-    this.domDuration = document.getElementById('execution-dialog-duration')
-    this.domStatus = new ActionStatusDisplay(document.getElementById('execution-dialog-status'))
-
-    this.domExecutionBasics = document.getElementById('execution-dialog-basics')
-    this.domExecutionDetails = document.getElementById('execution-dialog-details')
-
-    window.terminal = new OutputTerminal()
-    window.terminal.open(this.domOutput)
-    window.terminal.resize(80, 24)
-  }
-
-  toggleSize () {
-    if (this.dlg.classList.contains('big')) {
-      this.dlg.classList.remove('big')
-      window.terminal.resize(80, 24)
-    } else {
-      this.dlg.classList.add('big')
-      window.terminal.fit()
-    }
-  }
-
-  async reset () {
-    this.executionSeconds = 0
-    this.executionTrackingId = 'notset'
-
-    this.dlg.classList.remove('big')
-
-    this.domOutputToggleBig.hidden = false
-
-    this.domIcon.innerText = ''
-    this.domTitle.innerText = 'Waiting for result... '
-    this.domDuration.innerText = ''
-
-    //    window.terminal.close()
-
-    this.domBtnRerun.disabled = true
-    this.domBtnRerun.onclick = () => {}
-    this.domBtnKill.disabled = true
-    this.domBtnKill.onclick = () => {}
-
-    this.hideDetailsOnResult = false
-    this.domExecutionBasics.hidden = false
-
-    this.domExecutionDetails.hidden = true
-
-    await window.terminal.reset()
-    window.terminal.fit()
-  }
-
-  show (actionButton) {
-    if (typeof actionButton !== 'undefined' && actionButton != null) {
-      this.domIcon.innerText = actionButton.domIcon.innerText
-    }
-
-    this.domBtnKill.disabled = false
-    this.domBtnKill.onclick = () => {
-      this.killAction()
-    }
-
-    clearInterval(window.executionDialogTicker)
-    this.executionSeconds = 0
-    this.executionTick()
-    window.executionDialogTicker = setInterval(() => {
-      this.executionTick()
-    }, 1000)
-
-    if (this.dlg.open) {
-      this.dlg.close()
-    }
-
-    this.dlg.showModal()
-  }
-
-  rerunAction (actionId) {
-    const actionButton = document.getElementById('actionButton-' + actionId)
-
-    if (actionButton !== undefined) {
-      actionButton.btn.click()
-    }
-
-    this.dlg.close()
-  }
-
-  killAction () {
-    const killActionArgs = {
-      executionTrackingId: this.executionTrackingId
-    }
-
-    window.fetch(window.restBaseUrl + 'KillAction', {
-      cors: 'cors',
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json'
-      },
-      body: JSON.stringify(killActionArgs)
-    }).then((res) => {
-      return res.json() // This isn't used by anything. UI is updated by OnExecutionFinished like normal.
-    }).catch(err => {
-      throw err
-    })
-  }
-
-  executionTick () {
-    this.executionSeconds++
-
-    this.updateDuration(null)
-  }
-
-  hideEverythingApartFromOutput () {
-    this.hideDetailsOnResult = true
-    this.domExecutionBasics.hidden = true
-  }
-
-  fetchExecutionResult (executionTrackingId) {
-    this.executionTrackingId = executionTrackingId
-
-    const executionStatusArgs = {
-      executionTrackingId: this.executionTrackingId
-    }
-
-    window.fetch(window.restBaseUrl + 'ExecutionStatus', {
-      cors: 'cors',
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json'
-      },
-      body: JSON.stringify(executionStatusArgs)
-    }).then((res) => {
-      if (res.ok) {
-        return res.json()
-      } else if (res.status === 404) {
-        throw new Error('Execution not found: ' + executionTrackingId)
-      } else {
-        throw new Error(res.statusText)
-      }
-    }
-    ).then((json) => {
-      this.renderExecutionResult(json)
-    }).catch(err => {
-      console.log(err)
-      this.renderError(err)
-    })
-  }
-
-  updateDuration (logEntry) {
-    if (logEntry == null) {
-      this.domDuration.innerHTML = this.executionSeconds + ' seconds'
-    } else if (!logEntry.executionStarted) {
-      this.domDuration.innerHTML = logEntry.datetimeStarted + ' (request time). Not executed.'
-    } else if (logEntry.executionStarted && !logEntry.executionFinished) {
-      this.domDuration.innerHTML = logEntry.datetimeStarted
-    } else {
-      let delta = ''
-
-      try {
-        delta = (new Date(logEntry.datetimeStarted) - new Date(logEntry.datetimeStarted)) / 1000
-        delta = new Intl.RelativeTimeFormat().format(delta, 'seconds').replace('in ', '').replace('ago', '')
-      } catch (e) {
-        console.warn('Failed to calculate delta', e)
-      }
-
-      this.domDuration.innerHTML = logEntry.datetimeStarted + ' &rarr; ' + logEntry.datetimeFinished
-
-      if (delta !== '') {
-        this.domDuration.innerHTML += ' (' + delta + ')'
-      }
-    }
-  }
-
-  async renderExecutionResult (res) {
-    this.res = res
-
-    clearInterval(window.executionDialogTicker)
-
-    if ('type' in res && res.type === 'execution-dialog-output-html') {
-      this.domOutputHtml.hidden = false
-      this.domOutput.hidden = true
-      this.domOutputHtml.innerHTML = res.logEntry.output
-      this.domOutputHtml.hidden = false
-      this.hideDetailsonResult = true
-    } else {
-      this.domOutput.hidden = false
-      this.domOutputHtml.innerHTML = ''
-      this.domOutputHtml.hidden = true
-    }
-
-    if (this.hideDetailsOnResult) {
-      this.domExecutionDetails.hidden = true
-    }
-
-    this.executionTrackingId = res.logEntry.executionTrackingId
-
-    this.domBtnRerun.disabled = !res.logEntry.executionFinished
-    this.domBtnRerun.onclick = () => { this.rerunAction(res.logEntry.actionId) }
-
-    this.domBtnKill.disabled = !res.logEntry.canKill
-
-    this.domStatus.update(res.logEntry)
-
-    this.domIcon.innerHTML = res.logEntry.actionIcon
-    this.domTitle.innerText = res.logEntry.actionTitle
-    this.domTitle.title = 'Action ID: ' + res.logEntry.actionId + '\nExecution ID: ' + res.logEntry.executionTrackingId
-
-    this.updateDuration(res.logEntry)
-
-    await window.terminal.reset()
-    await window.terminal.write(res.logEntry.output, () => {
-      window.terminal.fit()
-    })
-  }
-
-  renderError (err) {
-    window.showBigError('execution-dlg-err', 'in the execution dialog', 'Failed to fetch execution result. ' + err, false)
-  }
-}

+ 0 - 37
webui.dev/js/NavigationBar.js

@@ -1,37 +0,0 @@
-export class NavigationBar {
-  constructor () {
-    this.navbar = document.getElementsByTagName('nav')[0]
-    this.mainLinks = document.getElementById('navigation-links')
-    this.supplementalLinks = document.getElementById('supplemental-links')
-  }
-
-  createLink (title, url, isSupplemental) {
-    const parent = (isSupplemental) ? this.supplementalLinks : this.mainLinks
-
-    const existsAlready = Array.from(parent.querySelectorAll('li')).some(el => el.title === title)
-
-    if (existsAlready) {
-      return
-    }
-
-    const linkA = document.createElement('a')
-    linkA.href = url
-    linkA.innerText = title
-
-    const navigationLi = document.createElement('li')
-    navigationLi.appendChild(linkA)
-    navigationLi.title = title
-
-    parent.appendChild(navigationLi)
-  }
-
-  refreshSectionPolicyLinks (policy) {
-    if (policy.showDiagnostics) {
-      this.createLink('Diagnostics', '/diagnostics', true)
-    }
-
-    if (policy.showLogList) {
-      this.createLink('Logs', '/logs', true)
-    }
-  }
-}

+ 0 - 82
webui.dev/js/websocket.js

@@ -1,82 +0,0 @@
-import {
-  refreshServerConnectionLabel
-} from './marshaller.js'
-
-window.ws = null
-
-export function checkWebsocketConnection () {
-  if (window.ws === null || window.ws.readyState === 3) {
-    reconnectWebsocket()
-  }
-}
-
-function reconnectWebsocket () {
-  window.websocketAvailable = false
-
-  const websocketConnectionUrl = new URL(window.location.toString())
-  websocketConnectionUrl.hash = ''
-  websocketConnectionUrl.pathname = '/websocket'
-
-  if (window.location.protocol === 'https:') {
-    websocketConnectionUrl.protocol = 'wss'
-  } else {
-    websocketConnectionUrl.protocol = 'ws'
-  }
-
-  window.websocketConnectionUrl = websocketConnectionUrl
-
-  const ws = window.ws = new WebSocket(websocketConnectionUrl.toString())
-
-  ws.addEventListener('open', websocketOnOpen)
-  ws.addEventListener('message', websocketOnMessage)
-  ws.addEventListener('error', websocketOnError)
-  ws.addEventListener('close', websocketOnClose)
-}
-
-function websocketOnOpen (evt) {
-  window.websocketAvailable = true
-
-  window.ws.send('monitor')
-
-  refreshServerConnectionLabel()
-
-  window.refreshLoop()
-}
-
-function websocketOnMessage (msg) {
-  // FIXME check msg status is OK
-  const j = JSON.parse(msg.data)
-  j.type = j.type.replace('olivetin.api.v1.', '')
-
-  const e = new Event(j.type)
-  e.payload = j.payload
-
-  switch (j.type) {
-    case 'EventOutputChunk':
-    case 'EventConfigChanged':
-    case 'EventExecutionFinished':
-    case 'EventExecutionStarted':
-    case 'EventEntityChanged':
-      window.dispatchEvent(e)
-      break
-    default:
-      window.showBigError('ws-unhandled-message', 'handling websocket message', 'Unhandled websocket message type from server: ' + j.type, true)
-  }
-}
-
-function websocketOnError (err) {
-  window.websocketAvailable = false
-  window.refreshLoop()
-
-  console.log('Websocket error is: ', err)
-
-  window.showBigError('websocket-connection', 'connecting to the websocket', 'This often means the connection was closed, sometimes this can happen due to reverse proxy timeouts. Sometimes your web browser can provide helpful diagnostic information in the web developer console. The reason given by your browser is:' + err, true)
-
-  refreshServerConnectionLabel()
-}
-
-function websocketOnClose () {
-  window.websocketAvailable = false
-
-  refreshServerConnectionLabel()
-}

+ 0 - 782
webui.dev/style.css

@@ -1,782 +0,0 @@
-html, body {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-}
-
-body {
-  background-color: #dee3e7;
-  color: black;
-  font-family: sans-serif;
-  margin: 0;
-  padding: 0;
-  text-align: center;
-  display: flex;
-  flex-direction: column;
-}
-
-dialog {
-  box-shadow: 0 0 6px 0 #444;
-  max-width: 600px;
-  min-width: 60%;
-  text-align: left;
-  padding: 0;
-}
-
-dialog[open] {
-  display: flex;
-  flex-direction: column;
-}
-
-dialog.big {
-  max-width: 100vw;
-  width: 100vw;
-  height: 100vh;
-  max-height: 100dvh;
-  border: none;
-  margin: 0;
-}
-
-fieldset {
-  font-family: sans-serif;
-  display: grid;
-  grid-template-columns: repeat(auto-fit, 180px);
-  grid-auto-rows: 1fr;
-  grid-gap: 1em;
-  padding: 0;
-  text-align: center;
-  justify-content: center;
-  border: 0;
-}
-
-footer {
-  font-size: smaller;
-}
-
-footer span {
-  display: inline-block;
-}
-
-a {
-  color: black;
-}
-
-nav ul li a {
-  display: block;
-  padding: 0.5em 1em;
-  user-select: none;
-  text-decoration: none;
-  font-size: small;
-}
-
-a:focus {
-  background-color: black;
-  color: white;
-  outline: 1px solid black;
-}
-
-nav ul li a:hover {
-  background-color: #efefef;
-  color: black;
-  cursor: pointer;
-  border: 0;
-  outline: 0;
-}
-
-nav ul li a.selected {
-  background-color: #c4cdd4;
-  color: black;
-  outline: 0;
-}
-
-#sidebar-toggler-button {
-  height: 2em;
-  width: 2em;
-  text-align: center;
-  display: inline-grid;
-  place-items: center;
-  margin-left: 1em;
-  padding: 0;
-}
-
-.userinfo {
-  padding-right: 1em;
-  font-size: small;
-}
-
-.userinfo svg, .userinfo span {
-  vertical-align: middle;
-}
-
-nav {
-  left: -250px;
-}
-
-nav.topbar {
-  display: flex;
-  flex-direction: row;
-  place-content: end;
-  border-radius: .5em;
-  padding-right: 1em;
-  padding-left: 1em;
-  flex-grow: 1;
-}
-
-nav.sidebar {
-  background-color: white;
-  position: fixed;
-  width: 180px;
-  height: 100dvh;
-  top: 0;
-  transition: left 0.5s ease, visibility 0.5s ease;
-  box-shadow: 0 0 10px 0 #444;
-  z-index: -1;
-  flex-direction: column;
-  display: flex;
-  visibility: hidden;
-}
-
-nav.sidebar.shown {
-  visibility: visible;
-  left: 0;
-}
-
-.sidebar #navigation-links {
-  padding-top: 4em;
-  flex-grow: 1;
-}
-
-#supplemental-links {
-  flex-grow: 0;
-}
-
-h1 {
-  display: inline;
-  font-size: small;
-  padding-left: .5em;
-  flex-grow: 1;
-  margin: 0;
-}
-
-dialog h1 {
-  padding-left: 0;
-}
-
-nav ul {
-  margin: 0;
-  padding: 0;
-}
-
-nav.topbar ul {
-  display: inline-block;
-}
-
-nav.topbar ul li {
-  display: inline-block;
-  font-size: small;
-}
-
-nav.topbar ul li a {
-  border-radius: .5em;
-}
-
-nav.sidebar ul li {
-  list-style: none;
-  text-align: left;
-  border-bottom: 1px solid #ccc;
-}
-
-table {
-  background-color: white;
-  border-collapse: collapse;
-  width: 100%;
-}
-
-th,
-td {
-  border-top: 1px solid #efefef;
-  text-align: left;
-  padding: 0.6em;
-}
-
-th:first-child {
-  width: 5%;
-}
-
-tr:hover td {
-  background-color: beige;
-}
-
-legend {
-  font-family: sans-serif;
-  padding-top: 1em;
-  text-align: center;
-  width: 100%;
-  padding-bottom: 1em;
-  font-weight: bold;
-}
-
-span.icon {
-  display: block;
-  font-size: 3em;
-}
-
-span.icon img {
-  width: 1em;
-}
-
-.action-header {
-  display: flex;
-  align-items: center;
-}
-
-.action-header span.icon,
-tr.log-row span.icon {
-  display: inline-block;
-  padding-right: 0.2em;
-  vertical-align: middle;
-}
-
-.error {
-  background-color: salmon;
-  color: black;
-}
-
-.title.temporary-status-message {
-  color: gray;
-}
-
-h2 {
-  display: inline-block;
-  font-size: 1em;
-  margin-top: 0;
-  flex-grow: 1;
-}
-
-div.entity h2 {
-  grid-column: 1 / span all;
-}
-
-details {
-  display: inline-block;
-  flex-grow: 1;
-}
-
-/* General Buttons */
-
-button {
-  transition: none;
-  font-weight: bold;
-  border-radius: .7em;
-  box-shadow: none;
-  font-size: 1em;
-  padding: 0.6em 1em;
-  border: 1px solid #ccc;
-  font-family: sans-serif;
-  color: black;
-  text-align: center;
-  background-color: white;
-  user-select: none;
-}
-
-action-button {
-  display: flex;
-  flex-direction: column;
-}
-
-action-button button {
-  font-size: .85em;
-  font-weight: normal;
-  flex-grow: 1;
-  width: 100%;
-  z-index: 1;
-  padding: 1em;
-  box-shadow: 0 0 6px 0 #aaa;
-  transition: background-color 1s ease, color .3s ease;
-}
-
-execution-button button {
-  margin-top: 0.2em;
-  margin-bottom: 0.2em;
-}
-
-button:focus {
-  outline: 1px solid black;
-}
-
-button:hover {
-  background-color: #ececec;
-  color: black;
-  cursor: pointer;
-}
-
-button:disabled {
-  color: #3c3c3c;
-  background-color: #cecece;
-  cursor: not-allowed;
-  text-decoration: line-through;
-}
-
-button[name="cancel"]:hover {
-  background-color: salmon;
-  color: black;
-}
-
-button[name="start"]:hover {
-  background-color: #aceaac;
-  color: black;
-}
-
-action-button button:hover {
-  box-shadow: 0 0 10px 0 #666;
-}
-
-span.title {
-  display: inline;
-}
-
-action-button details {
-  flex-grow: 1;
-}
-
-action-button details[open] {
-  margin-top: 0;
-}
-
-action-button details summary div {
-  display: inline-flex;
-}
-
-action-button details summary div span:first-child {
-  flex-grow: 1;
-}
-
-.action-button-footer {
-  text-align: left;
-  font-size: smaller;
-  overflow: auto;
-}
-
-execution-button {
-  display: inline-block;
-  margin-right: .2em;
-  margin-left: .2em;
-  margin-top: .2em;
-}
-
-.action-status {
-  padding: .4em;
-  border-radius: .4em;
-}
-
-.action-failed {
-  background-color: #e78284;
-}
-
-.action-success {
-  background-color: #a6d189;
-  color: black;
-}
-
-.action-nonzero-exit {
-  background-color: #ef9f76;
-  color: black;
-}
-
-.action-timeout {
-  background-color: #99d1db;
-  color: black;
-}
-
-.action-blocked {
-  background-color: #ca9ee6;
-  color: black;
-}
-
-img.logo {
-  width: 1em;
-  height: 1em;
-  vertical-align: middle;
-}
-
-main {
-  padding: 1em;
-  padding-top: 3.5em;
-  flex-grow: 1;
-}
-
-summary {
-  cursor: pointer;
-}
-
-form div.wrapper {
-  background-color: white;
-  text-align: left;
-}
-
-label {
-  text-align: right;
-  display: inline-block;
-}
-
-header {
-  text-align: left;
-  display: flex;
-  flex-direction: row;
-  z-index: 3;
-  align-items: center;
-  padding-bottom: .2em;
-  padding-top: 1em;
-  position: fixed;
-  background-color: #dee3e7;
-  width: 100vw;
-}
-
-input {
-  font-family: sans-serif;
-  padding: 0.6em;
-  border: 1px solid #ccc;
-  border-radius: .4em;
-}
-
-input:invalid {
-  outline: 2px solid red;
-}
-
-input[type="checkbox"] {
-  justify-self: baseline;
-}
-
-form .wrapper span.icon {
-  display: inline-block;
-  vertical-align: middle;
-}
-
-div.arguments {
-  display: grid;
-  grid-template-columns: max-content auto auto; /* We don't want the label or the description to wrap, and the input to take up the rest of the space */
-  grid-template-rows: auto;
-  grid-gap: 1em;
-  align-items: center;
-
-}
-
-p.argument-wrapper {
-  display: flex;
-}
-
-div.buttons {
-  display: flex;
-  justify-content: end;
-  gap: 1em;
-}
-
-input.invalid {
-  background-color: salmon;
-}
-
-#available-version {
-  background-color: #aceaac;
-  padding: 0.2em;
-  border-radius: 1em;
-}
-
-span.tag {
-  border-radius: 0.4em;
-  margin-top: .2em;
-  margin-right: .2em;
-  display: inline-block;
-  background-color: lightgray;
-  padding: .4em;
-  color: black;
-}
-
-span.annotation {
-  display: inline-block;
-  margin-top: .2em;
-  margin-right: .2em;
-}
-
-span.annotation-key {
-  padding: 0.4em;
-  border-radius: 0.4em 0 0 .4em;
-  display: inline-block;
-  background-color: lightgray;
-  color: #666;
-}
-
-span.annotation-value {
-  padding: 0.4em;
-  border-radius: 0 .4em .4em 0;
-  display: inline-block;
-  background-color: lightgray;
-  color: black;
-}
-
-.box-shadow {
-  box-shadow: 0 0 5px 0 #444;
-  background-color: #fff;
-  text-align: left;
-}
-
-.border-radius {
-  border-radius: .7em;
-}
-
-.box-shadow p {
-  padding: .6em;
-  margin: 0;
-}
-
-div.toolbar {
-  padding: .4em;
-  text-align: left;
-  background-color: #fff;
-  display: flex;
-  flex-direction: row;
-}
-
-div.display {
-  border: 1px solid #666;
-  box-shadow: 0 0 6px 0 #aaa;
-  border-radius: .7em;
-  display: flex;
-  align-items: center;
-  place-content: center center;
-  flex-direction: column;
-  font-size: small;
-}
-
-#execution-dialog-xterm {
-  flex-grow: 1;
-}
-
-.padded-content {
-  padding: 1em;
-}
-
-.padded-content-sides {
-  padding-left: 1em;
-  padding-right: 1em;
-}
-
-.ta-left {
-  text-align: left;
-}
-
-.xterm {
-  padding: 1em;
-}
-
-.flex-col {
-  display: flex;
-  flex-direction: column;
-}
-
-label.input-with-icons {
-  border: 1px solid #ccc;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-}
-
-label.input-with-icons input {
-  border: 0;
-  padding: .6em;
-  flex-grow: 1;
-}
-
-label.input-with-icons svg {
-  font-size: 1.5em;
-}
-
-label.input-with-icons button {
-  border: 0;
-  background-color: transparent;
-  padding-top: 0;
-  padding-bottom: 0;
-  box-shadow: none;
-  transition: color .3s ease;
-}
-
-label.input-with-icons button:disabled {
-  display: none;
-}
-
-#content-login hr {
-  border: 0;
-  border-top: 1px dashed #ccc;
-}
-
-#content-login button {
-  width: 100%;
-  grid-column: 1 / span 3;
-  margin-bottom: 1em;
-}
-
-#content-login button:last-child {
-  margin-bottom: 0;
-}
-
-#content-login ul {
-  list-style: none;
-  padding: 0;
-}
-
-#content-login form {
-  width: auto;
-  place-self: center;
-}
-
-.oauth2-icon {
-  font-size: 1.8em;
-  vertical-align: middle;
-}
-
-@media screen and (width <= 600px) {
-  fieldset {
-    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
-  }
-
-  label {
-    text-align: left;
-    margin-bottom: .6em;
-    font-weight: bold;
-  }
-
-  p.argument-wrapper {
-    flex-direction: column;
-  }
-
-  div.arguments {
-    grid-template-columns: auto;
-  }
-
-  dialog {
-    border-left: 0;
-    border-right: 0;
-    margin-left: 0;
-    margin-right: 0;
-    width: 100vw;
-    max-width: 100vw;
-  }
-
-  .xterm {
-    margin-left: 0;
-    margin-right: 0;
-    width: fit-content;
-  }
-}
-
-@media (prefers-color-scheme: dark) {
-  body, header {
-    background-color: #333;
-    color: white;
-  }
-
-  .box-shadow {
-    box-shadow: 0 0 6px 0 #625c5c;
-    background-color: #222;
-  }
-
-  dialog {
-    background-color: #222;
-    color: white;
-  }
-
-  form div.wrapper {
-    background-color: #222;
-  }
-
-  label.input-with-icons {
-    border: 1px solid #666;
-    background-color: #383838;
-  }
-
-  input {
-    background-color: #383838;
-    color: white;
-	border: 1px solid #666;
-  }
-
-  button {
-    border: 1px solid #666;
-    background-color: #222;
-    color: white;
-  }
-
-  action-button button {
-    box-shadow: 0 0 6px 0 #444;
-  }
-
-  button:focus {
-    outline: 2px solid #72B7F4;
-  }
-
-  button:disabled {
-    background-color: black;
-  }
-
-  button:hover {
-	background-color: #444;
-	color: white;
-  }
-
-
-  footer {
-    color: #bbb;
-  }
-
-  a, a:visited {
-    color: #bbb;
-  }
-
-  a:focus {
-    background-color: #72B7F4;
-    color: black;
-    outline: 1px solid #72B7F4;
-  }
-
-  nav.sidebar {
-    background-color: #111;
-    color: white;
-  }
-
-  nav.sidebar ul li {
-    border-bottom: 1px solid #3e3e3e;
-  }
-
-  nav ul li a:hover {
-    background-color: #1b5988;
-    color: white;
-  }
-
-  nav ul li a.selected {
-    background-color: #0c3351;
-    color: white;
-  }
-
-  table,
-  td,
-  th {
-    border-top: 1px solid gray;
-  }
-
-  td,
-  tr {
-    background-color: #222;
-    color: white;
-  }
-
-  tr:hover td {
-    background-color: #1b5988;
-  }
-
-  div.toolbar {
-    background-color: black;
-  }
-
-  div.display {
-    box-shadow: 0 0 6px 0 #444;
-  }
-}

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä