diff --git a/.github/ISSUE_TEMPLATE/support_request.md b/.github/ISSUE_TEMPLATE/support_request.md
index 74accc8..87d75c9 100644
--- a/.github/ISSUE_TEMPLATE/support_request.md
+++ b/.github/ISSUE_TEMPLATE/support_request.md
@@ -1,48 +1,29 @@
---
name: Support request
-about: Need some help? Got an error message?
-title: ""
-type: support
-labels:
- - "waiting-on-developer"
+about: Ask for help with OliveTin
+title: ''
+labels: ''
assignees: ''
---
-**What seems to be the problem?!**
+**What is the problem you are having?**
-If you are getting an error message, then please copy/paste, or better, provide
-a screenshot to show us exactly what is wrong.
-**Can you provide a sosreport?**
+**What OliveTin version are you running?**
-A sosreport really helps us to help you, by providing critical information about your install. If you can generate a sosreport, please copy and paste the output here.
-How to generate a sosreport: https://docs.olivetin.app/sosreport.html
+**Can you provide Server Diagnostics?**
-**What package/file/container did you use to install OliveTin?**
+Server Diagnostics really helps us to help you, by providing critical information about your install. If you can generate Server Diagnostics, please copy and paste the output here.
-eg: OliveTin-1234-x86_64.rpm
-eg: Container from Dockerhub - please include the version number
+How to generate Server Diagnostics: https://docs.olivetin.app/troubleshooting/server-diagnostics.html
-**Your config.yaml**
-```
-Please copy-paste your config.yaml here
+**What is your config.yaml?**
-You don't need to include anything under "actions:" unless that is where your
-error probably is.
-```
-**OliveTin logs**
+**What are the OliveTin service logs showing?**
-If possible, please copy and paste your OliveTin logs from when the error happened.
-**Screenshot of WebDeveloper console logs**
-
-If you know how, and if you think it's relevant, a screenshot of the
-WebDeveloper console from when you clicked a button is often really helpful.
-
-**Anything else?**
-
-Add any other context about the problem here.
+**Screenshots (if appropriate)**
diff --git a/.gitignore b/.gitignore
index ad372af..2bfbf44 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,12 +18,12 @@ integration-tests/flakey-test-runs.jsonl
service/flakey-test-runs.log
service/flakey-test-runs.jsonl
.vscode/
-webui/
+/webui/
server.log
OliveTin
integration-tests/configs/authRequireGuestsToLogin/sessions.yaml
-webui
-webui.dev
+/webui
+/webui.dev
sessions.yaml
docs/build/
build/
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 46972c4..9ae58c8 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -163,9 +163,13 @@ nfpms:
- src: var/openrc/OliveTin
dst: /etc/init.d/OliveTin
- - src: webui/
- dst: /var/www/olivetin/
- type: tree
+ # Avoid type: tree for apk — nfpm writes GNU base-256 modes for walked
+ # dirs (ModeDir), which apk-tools rejects (#761 / goreleaser/nfpm#1112).
+ - src: webui/index.html
+ dst: /var/www/olivetin/index.html
+
+ - src: webui/assets/*
+ dst: /var/www/olivetin/assets/
- src: config.yaml
dst: /etc/OliveTin/config.yaml
diff --git a/docs/modules/ROOT/images/action-confirmation.png b/docs/modules/ROOT/images/action-confirmation.png
deleted file mode 100644
index d681c03..0000000
Binary files a/docs/modules/ROOT/images/action-confirmation.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/action_customization/max-rate/.gitignore b/docs/modules/ROOT/images/action_customization/max-rate/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/action_customization/max-rate/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/action_customization/max-rate/Makefile b/docs/modules/ROOT/images/action_customization/max-rate/Makefile
new file mode 100644
index 0000000..720e66a
--- /dev/null
+++ b/docs/modules/ROOT/images/action_customization/max-rate/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../screenshots.mk
diff --git a/docs/modules/ROOT/images/action_customization/max-rate/config.yaml b/docs/modules/ROOT/images/action_customization/max-rate/config.yaml
new file mode 100644
index 0000000..13963f1
--- /dev/null
+++ b/docs/modules/ROOT/images/action_customization/max-rate/config.yaml
@@ -0,0 +1,14 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+actions:
+ - title: date
+ shell: date
+ icon: clock
+ maxRate:
+ - limit: 3
+ duration: 5m
diff --git a/docs/modules/ROOT/images/action_customization/max-rate/maxRate.png b/docs/modules/ROOT/images/action_customization/max-rate/maxRate.png
new file mode 100644
index 0000000..08f763c
Binary files /dev/null and b/docs/modules/ROOT/images/action_customization/max-rate/maxRate.png differ
diff --git a/docs/modules/ROOT/images/action_customization/max-rate/screenshots.ini b/docs/modules/ROOT/images/action_customization/max-rate/screenshots.ini
new file mode 100644
index 0000000..38883c0
--- /dev/null
+++ b/docs/modules/ROOT/images/action_customization/max-rate/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 980
+height = 620
+post_script_sleep = 0.5
+
+[max-rate]
+url = .
+name = maxRate
+script = setup_max_rate.py
diff --git a/docs/modules/ROOT/images/action_customization/max-rate/setup_max_rate.py b/docs/modules/ROOT/images/action_customization/max-rate/setup_max_rate.py
new file mode 100644
index 0000000..cd3e81e
--- /dev/null
+++ b/docs/modules/ROOT/images/action_customization/max-rate/setup_max_rate.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+"""Show blocked rate-limit log entries for the date action."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+_START_ACTION_REPEATEDLY_JS = """
+const done = arguments[arguments.length - 1];
+const title = arguments[0];
+const count = arguments[1];
+
+function bindingIdForTitle(actionTitle) {
+ const button = document.querySelector('[title="' + actionTitle + '"]');
+ if (!button) {
+ throw new Error('Action button not found: ' + actionTitle);
+ }
+ return button.closest('.action-button').id.replace('actionButton-', '');
+}
+
+function uniqueTrackingId() {
+ if (window.isSecureContext && window.crypto?.randomUUID) {
+ return window.crypto.randomUUID();
+ }
+ return 'doc-screenshot-' + Date.now() + '-' + Math.random();
+}
+
+async function startAndWait(actionTitle) {
+ const bindingId = bindingIdForTitle(actionTitle);
+ const trackingId = uniqueTrackingId();
+ const response = await window.client.startAction({
+ bindingId,
+ arguments: [],
+ uniqueTrackingId: trackingId,
+ });
+ const executionTrackingId = response.executionTrackingId || trackingId;
+
+ while (true) {
+ const result = await window.client.executionStatus({
+ executionTrackingId,
+ });
+ if (result.logEntry?.executionFinished) {
+ return result.logEntry;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ }
+}
+
+async function startRepeatedly(actionTitle, runs) {
+ for (let i = 0; i < runs; i++) {
+ await startAndWait(actionTitle);
+ }
+}
+
+startRepeatedly(title, count).then(() => done(true)).catch((err) => done(String(err)));
+"""
+
+
+def _wait_for_dashboard(driver, timeout=15):
+ WebDriverWait(driver, timeout).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
+ )
+ WebDriverWait(driver, timeout).until(
+ lambda d: d.execute_script("return !!window.client")
+ )
+
+
+def _wait_for_rate_limited_logs(driver, timeout=20):
+ def ready(d):
+ try:
+ rows = d.find_elements(By.CSS_SELECTOR, ".logs-table tbody tr")
+ blocked = d.find_elements(By.CSS_SELECTOR, ".logs-table .status-blocked")
+ completed = d.find_elements(By.CSS_SELECTOR, ".logs-table .status-success")
+ except Exception:
+ return False
+ return len(rows) >= 5 and len(blocked) >= 2 and len(completed) >= 3
+
+ WebDriverWait(driver, timeout).until(ready)
+
+
+def run(driver):
+ _wait_for_dashboard(driver)
+
+ driver.execute_async_script(_START_ACTION_REPEATEDLY_JS, "date", 5)
+
+ time.sleep(1)
+
+ driver.execute_script("window.location.href = '/logs'")
+ _wait_for_rate_limited_logs(driver)
+
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/action_customization/popup-output-only/.gitignore b/docs/modules/ROOT/images/action_customization/popup-output-only/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/action_customization/popup-output-only/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/action_customization/popup-output-only/Makefile b/docs/modules/ROOT/images/action_customization/popup-output-only/Makefile
new file mode 100644
index 0000000..720e66a
--- /dev/null
+++ b/docs/modules/ROOT/images/action_customization/popup-output-only/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../screenshots.mk
diff --git a/docs/modules/ROOT/images/action_customization/popup-output-only/config.yaml b/docs/modules/ROOT/images/action_customization/popup-output-only/config.yaml
new file mode 100644
index 0000000..f5f96c6
--- /dev/null
+++ b/docs/modules/ROOT/images/action_customization/popup-output-only/config.yaml
@@ -0,0 +1,16 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+actions:
+ - title: Check disk space
+ icon: disk
+ onclick: execution-dialog
+ shell: |
+ echo "Filesystem Size Used Avail Use% Mounted on"
+ echo "/dev/mapper/fedora_mindstorm-root 99G 82G 12G 88% /"
+ echo "Filesystem Size Used Avail Use% Mounted on"
+ echo "/dev/mapper/fedora_mindstorm-root 99G 82G 12G 88% /"
diff --git a/docs/modules/ROOT/images/action_customization/popup-output-only/popupOutputOnly.png b/docs/modules/ROOT/images/action_customization/popup-output-only/popupOutputOnly.png
new file mode 100644
index 0000000..5898544
Binary files /dev/null and b/docs/modules/ROOT/images/action_customization/popup-output-only/popupOutputOnly.png differ
diff --git a/docs/modules/ROOT/images/action_customization/popup-output-only/screenshots.ini b/docs/modules/ROOT/images/action_customization/popup-output-only/screenshots.ini
new file mode 100644
index 0000000..362cfa9
--- /dev/null
+++ b/docs/modules/ROOT/images/action_customization/popup-output-only/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 900
+height = 620
+post_script_sleep = 0.5
+
+[popup-output-only]
+url = .
+name = popupOutputOnly
+script = setup_popup_output_only.py
diff --git a/docs/modules/ROOT/images/action_customization/popup-output-only/setup_popup_output_only.py b/docs/modules/ROOT/images/action_customization/popup-output-only/setup_popup_output_only.py
new file mode 100644
index 0000000..8e66353
--- /dev/null
+++ b/docs/modules/ROOT/images/action_customization/popup-output-only/setup_popup_output_only.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+"""Open the execution dialog for Check disk space."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_body_attr(driver, attr, timeout=15):
+ WebDriverWait(driver, timeout).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute(attr))
+ )
+
+
+def _wait_for_logs_page(driver, timeout=15):
+ WebDriverWait(driver, timeout).until(
+ lambda d: "/logs/" in d.current_url and not d.current_url.rstrip("/").endswith("/logs")
+ )
+
+
+def _wait_for_execution_complete(driver, timeout=15):
+ def finished(d):
+ try:
+ status = d.find_element(By.CSS_SELECTOR, ".execution-dialog-status").text
+ except Exception:
+ return False
+ return "Still running" not in status and "Queued" not in status
+
+ WebDriverWait(driver, timeout).until(finished)
+
+
+def run(driver):
+ _wait_for_body_attr(driver, "loaded-dashboard")
+
+ driver.find_element(By.CSS_SELECTOR, '[title="Check disk space"]').click()
+
+ _wait_for_logs_page(driver)
+ _wait_for_execution_complete(driver)
+
+ WebDriverWait(driver, 15).until(
+ lambda d: "fedora_mindstorm-root" in d.find_element(
+ By.CSS_SELECTOR, "#execution-results-popup .xterm-rows"
+ ).text
+ )
+
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/advanced_configuration/diagnostics/.gitignore b/docs/modules/ROOT/images/advanced_configuration/diagnostics/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/diagnostics/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/advanced_configuration/diagnostics/Makefile b/docs/modules/ROOT/images/advanced_configuration/diagnostics/Makefile
new file mode 100644
index 0000000..720e66a
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/diagnostics/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../screenshots.mk
diff --git a/docs/modules/ROOT/images/advanced_configuration/diagnostics/config.yaml b/docs/modules/ROOT/images/advanced_configuration/diagnostics/config.yaml
new file mode 100644
index 0000000..9de0c0d
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/diagnostics/config.yaml
@@ -0,0 +1,11 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+actions:
+ - title: date
+ icon: clock
+ shell: date
diff --git a/docs/modules/ROOT/images/advanced_configuration/diagnostics/diagnostics.png b/docs/modules/ROOT/images/advanced_configuration/diagnostics/diagnostics.png
new file mode 100644
index 0000000..e2088b4
Binary files /dev/null and b/docs/modules/ROOT/images/advanced_configuration/diagnostics/diagnostics.png differ
diff --git a/docs/modules/ROOT/images/advanced_configuration/diagnostics/screenshots.ini b/docs/modules/ROOT/images/advanced_configuration/diagnostics/screenshots.ini
new file mode 100644
index 0000000..d023631
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/diagnostics/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 980
+height = 640
+post_script_sleep = 0.5
+
+[diagnostics]
+url = /diagnostics
+name = diagnostics
+script = setup_diagnostics.py
diff --git a/docs/modules/ROOT/images/advanced_configuration/diagnostics/setup_diagnostics.py b/docs/modules/ROOT/images/advanced_configuration/diagnostics/setup_diagnostics.py
new file mode 100644
index 0000000..fb7ae37
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/diagnostics/setup_diagnostics.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+"""Open the diagnostics page from advanced_configuration/diagnostics.adoc."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_diagnostics_page(driver, timeout=30):
+ WebDriverWait(driver, timeout).until(
+ lambda d: d.execute_script("return !!window.client")
+ )
+
+ def ready(d):
+ try:
+ ssh_heading = d.find_element(
+ By.XPATH,
+ '//*[self::h2 or self::h3][contains(normalize-space(), "SSH")]',
+ )
+ server_diagnostics_heading = d.find_element(
+ By.XPATH,
+ '//*[self::h2 or self::h3][contains(normalize-space(), "Server Diagnostics")]',
+ )
+ key_value = d.find_element(
+ By.XPATH,
+ '//dt[contains(normalize-space(), "Found Key")]/following-sibling::dd[1]',
+ )
+ config_value = d.find_element(
+ By.XPATH,
+ '//dt[contains(normalize-space(), "Found Config")]/following-sibling::dd[1]',
+ )
+ except Exception:
+ return False
+
+ return all(
+ element.is_displayed()
+ for element in (
+ ssh_heading,
+ server_diagnostics_heading,
+ key_value,
+ config_value,
+ )
+ ) and all(
+ element.text.strip() not in ("", "?")
+ for element in (key_value, config_value)
+ )
+
+ WebDriverWait(driver, timeout).until(ready)
+
+
+def run(driver):
+ _wait_for_diagnostics_page(driver)
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/.gitignore b/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/Makefile b/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/Makefile
new file mode 100644
index 0000000..b474e2a
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../../screenshots.mk
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/config.yaml b/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/config.yaml
new file mode 100644
index 0000000..1f76cea
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/config.yaml
@@ -0,0 +1,48 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+showNavigation: false
+
+actions:
+ - title: Ping the Internet
+ icon: ping
+ shell: ping -c 1 127.0.0.1
+ - title: Check disk space
+ icon: disk
+ shell: df -h
+ - title: check dmesg logs
+ icon: logs
+ shell: dmesg | tail -n 1
+ - title: date
+ icon: clock
+ shell: date
+ - title: Run backup script
+ icon: backup
+ shell: echo backup
+ - title: Ping host
+ icon: ping
+ shell: ping -c 1 127.0.0.1
+ - title: Restart Docker Container
+ icon: restart
+ shell: echo restart
+ - title: Delete old backups
+ icon: ashtonished
+ shell: echo delete
+ - title: Get OliveTin Theme
+ icon: theme
+ shell: echo theme
+ - title: Setup easy SSH
+ icon: ssh
+ shell: echo ssh
+ - title: Restart httpd on server1
+ icon: restart
+ shell: echo restart httpd
+ - title: Toggle GPIO light
+ icon: light
+ shell: echo toggle
+ - title: Run Automation Playbook
+ icon: robot
+ shell: echo ansible
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/hide-navigation.png b/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/hide-navigation.png
new file mode 100644
index 0000000..40158eb
Binary files /dev/null and b/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/hide-navigation.png differ
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/screenshots.ini b/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/screenshots.ini
new file mode 100644
index 0000000..c2e4935
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 1180
+height = 760
+post_script_sleep = 0.5
+
+[hide-navigation]
+url = .
+name = hide-navigation
+script = setup_hide_navigation.py
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/setup_hide_navigation.py b/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/setup_hide_navigation.py
new file mode 100644
index 0000000..a5c4f0a
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/hide-navigation/setup_hide_navigation.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+"""Capture the Actions view with navigation hidden."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def run(driver):
+ WebDriverWait(driver, 15).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
+ )
+ WebDriverWait(driver, 15).until(
+ lambda d: len(d.find_elements(By.ID, "mainnav")) == 0
+ )
+ WebDriverWait(driver, 15).until(
+ lambda d: len(d.find_elements(By.CSS_SELECTOR, ".action-button button")) >= 8
+ )
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/page-title/.gitignore b/docs/modules/ROOT/images/advanced_configuration/webui/page-title/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/page-title/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/page-title/Makefile b/docs/modules/ROOT/images/advanced_configuration/webui/page-title/Makefile
new file mode 100644
index 0000000..b474e2a
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/page-title/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../../screenshots.mk
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/page-title/config.yaml b/docs/modules/ROOT/images/advanced_configuration/webui/page-title/config.yaml
new file mode 100644
index 0000000..e3ab997
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/page-title/config.yaml
@@ -0,0 +1,19 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+pageTitle: My OliveTin Instance
+
+actions:
+ - title: Ping the Internet
+ icon: ping
+ shell: ping -c 1 127.0.0.1
+ - title: Check disk space
+ icon: disk
+ shell: df -h /
+ - title: check dmesg logs
+ icon: logs
+ shell: dmesg | tail -n 1
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/page-title/page-title.png b/docs/modules/ROOT/images/advanced_configuration/webui/page-title/page-title.png
new file mode 100644
index 0000000..954b353
Binary files /dev/null and b/docs/modules/ROOT/images/advanced_configuration/webui/page-title/page-title.png differ
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/page-title/screenshots.ini b/docs/modules/ROOT/images/advanced_configuration/webui/page-title/screenshots.ini
new file mode 100644
index 0000000..84feadb
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/page-title/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 980
+height = 220
+post_script_sleep = 0.5
+
+[page-title]
+url = .
+name = page-title
+script = setup_page_title.py
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/page-title/setup_page_title.py b/docs/modules/ROOT/images/advanced_configuration/webui/page-title/setup_page_title.py
new file mode 100644
index 0000000..4359421
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/page-title/setup_page_title.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+"""Capture the custom page title from advanced_configuration/webui.adoc."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def run(driver):
+ WebDriverWait(driver, 15).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
+ )
+ WebDriverWait(driver, 15).until(
+ lambda d: "My OliveTin Instance" in d.find_element(By.CSS_SELECTOR, "header h1").text
+ )
+ WebDriverWait(driver, 15).until(
+ lambda d: d.find_element(By.CSS_SELECTOR, '[title="Ping the Internet"]').is_displayed()
+ )
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/.gitignore b/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/Makefile b/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/Makefile
new file mode 100644
index 0000000..b474e2a
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../../screenshots.mk
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/config.yaml b/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/config.yaml
new file mode 100644
index 0000000..f3404f6
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/config.yaml
@@ -0,0 +1,30 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+sectionNavigationStyle: sidebar
+
+actions:
+ - title: Check disk space
+ icon: disk
+ shell: df -h /
+ - title: Setup easy SSH
+ icon: ssh
+ shell: echo ssh
+ - title: My Servers ping
+ icon: ping
+ shell: echo ping
+ - title: My Containers list
+ icon: disk
+ shell: echo list
+
+dashboards:
+ - title: My Servers
+ contents:
+ - title: My Servers ping
+ - title: My Containers
+ contents:
+ - title: My Containers list
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/screenshots.ini b/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/screenshots.ini
new file mode 100644
index 0000000..5656dd0
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 980
+height = 520
+post_script_sleep = 0.5
+
+[sidebar]
+url = .
+name = sidebar
+script = setup_sidebar.py
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/setup_sidebar.py b/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/setup_sidebar.py
new file mode 100644
index 0000000..e2ed7c2
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/setup_sidebar.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+"""Capture the default sidebar navigation style."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_sidebar_navigation(driver, timeout=30):
+ WebDriverWait(driver, timeout).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
+ )
+
+ driver.find_element(By.ID, "sidebar-toggler-button").click()
+
+ WebDriverWait(driver, timeout).until(
+ lambda d: d.find_element(By.ID, "mainnav").is_displayed()
+ )
+
+ driver.find_element(By.CSS_SELECTOR, "#mainnav .stick-toggle").click()
+
+ def ready(d):
+ try:
+ my_servers = d.find_element(
+ By.XPATH,
+ '//*[@id="mainnav"]//a[contains(normalize-space(), "My Servers")]',
+ )
+ my_containers = d.find_element(
+ By.XPATH,
+ '//*[@id="mainnav"]//a[contains(normalize-space(), "My Containers")]',
+ )
+ disk_space = d.find_element(By.CSS_SELECTOR, '[title="Check disk space"]')
+ ssh = d.find_element(By.CSS_SELECTOR, '[title="Setup easy SSH"]')
+ except Exception:
+ return False
+ return all(element.is_displayed() for element in (my_servers, my_containers, disk_space, ssh))
+
+ WebDriverWait(driver, timeout).until(ready)
+
+
+def run(driver):
+ _wait_for_sidebar_navigation(driver)
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/sidebar.png b/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/sidebar.png
new file mode 100644
index 0000000..61fbac3
Binary files /dev/null and b/docs/modules/ROOT/images/advanced_configuration/webui/sidebar/sidebar.png differ
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/topbar/.gitignore b/docs/modules/ROOT/images/advanced_configuration/webui/topbar/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/topbar/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/topbar/Makefile b/docs/modules/ROOT/images/advanced_configuration/webui/topbar/Makefile
new file mode 100644
index 0000000..b474e2a
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/topbar/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../../screenshots.mk
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/topbar/config.yaml b/docs/modules/ROOT/images/advanced_configuration/webui/topbar/config.yaml
new file mode 100644
index 0000000..4f1ceb8
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/topbar/config.yaml
@@ -0,0 +1,33 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+sectionNavigationStyle: topbar
+
+actions:
+ - title: Ping host
+ icon: ping
+ shell: ping -c 1 127.0.0.1
+ - title: Restart Docker Container
+ icon: restart
+ shell: echo restart
+ - title: Delete old backups
+ icon: ashtonished
+ shell: echo delete
+ - title: Server ping
+ icon: ping
+ shell: echo ping
+ - title: Container list
+ icon: disk
+ shell: echo list
+
+dashboards:
+ - title: My Servers
+ contents:
+ - title: Server ping
+ - title: My Containers
+ contents:
+ - title: Container list
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/topbar/screenshots.ini b/docs/modules/ROOT/images/advanced_configuration/webui/topbar/screenshots.ini
new file mode 100644
index 0000000..aa51d4b
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/topbar/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 980
+height = 420
+post_script_sleep = 0.5
+
+[topbar]
+url = .
+name = topbar
+script = setup_topbar.py
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/topbar/setup_topbar.py b/docs/modules/ROOT/images/advanced_configuration/webui/topbar/setup_topbar.py
new file mode 100644
index 0000000..95a0c68
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/topbar/setup_topbar.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+"""Capture the topbar section navigation style."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_topbar_navigation(driver, timeout=30):
+ WebDriverWait(driver, timeout).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
+ )
+
+ def ready(d):
+ try:
+ topbar = d.find_element(By.CSS_SELECTOR, "nav.topbar")
+ my_servers = d.find_element(
+ By.XPATH,
+ '//nav[contains(@class, "topbar")]//a[contains(normalize-space(), "My Servers")]',
+ )
+ my_containers = d.find_element(
+ By.XPATH,
+ '//nav[contains(@class, "topbar")]//a[contains(normalize-space(), "My Containers")]',
+ )
+ diagnostics = d.find_element(
+ By.XPATH,
+ '//nav[contains(@class, "topbar")]//a[contains(normalize-space(), "Diagnostics")]',
+ )
+ logs = d.find_element(
+ By.XPATH,
+ '//nav[contains(@class, "topbar")]//a[contains(normalize-space(), "Logs")]',
+ )
+ ping_host = d.find_element(By.CSS_SELECTOR, '[title="Ping host"]')
+ restart = d.find_element(By.CSS_SELECTOR, '[title="Restart Docker Container"]')
+ delete_backups = d.find_element(By.CSS_SELECTOR, '[title="Delete old backups"]')
+ except Exception:
+ return False
+
+ return topbar.is_displayed() and all(
+ element.is_displayed()
+ for element in (
+ my_servers,
+ my_containers,
+ diagnostics,
+ logs,
+ ping_host,
+ restart,
+ delete_backups,
+ )
+ )
+
+ WebDriverWait(driver, timeout).until(ready)
+
+
+def run(driver):
+ _wait_for_topbar_navigation(driver)
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/topbar/topbar.png b/docs/modules/ROOT/images/advanced_configuration/webui/topbar/topbar.png
new file mode 100644
index 0000000..cf2edba
Binary files /dev/null and b/docs/modules/ROOT/images/advanced_configuration/webui/topbar/topbar.png differ
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/.gitignore b/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/Makefile b/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/Makefile
new file mode 100644
index 0000000..b474e2a
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../../screenshots.mk
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/config.yaml b/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/config.yaml
new file mode 100644
index 0000000..ba7a8d0
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/config.yaml
@@ -0,0 +1,47 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+actions:
+ - title: Ping the Internet
+ icon: ping
+ shell: ping -c 1 127.0.0.1
+ - title: Check disk space
+ icon: disk
+ shell: df -h
+ - title: check dmesg logs
+ icon: logs
+ shell: dmesg | tail -n 1
+ - title: date
+ icon: clock
+ shell: date
+ - title: Run backup script
+ icon: backup
+ shell: echo backup
+ - title: Ping host
+ icon: ping
+ shell: ping -c 1 127.0.0.1
+ - title: Restart Docker Container
+ icon: restart
+ shell: echo restart
+ - title: Delete old backups
+ icon: ashtonished
+ shell: echo delete
+ - title: Get OliveTin Theme
+ icon: theme
+ shell: echo theme
+ - title: Setup easy SSH
+ icon: ssh
+ shell: echo ssh
+ - title: Restart httpd on server1
+ icon: restart
+ shell: echo restart httpd
+ - title: Toggle GPIO light
+ icon: light
+ shell: echo toggle
+ - title: Run Automation Playbook
+ icon: robot
+ shell: echo ansible
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/screenshots.ini b/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/screenshots.ini
new file mode 100644
index 0000000..44b0063
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 1180
+height = 760
+post_script_sleep = 0.5
+
+[with-navigation]
+url = .
+name = with-navigation
+script = setup_with_navigation.py
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/setup_with_navigation.py b/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/setup_with_navigation.py
new file mode 100644
index 0000000..2daf9a5
--- /dev/null
+++ b/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/setup_with_navigation.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+"""Capture the default Actions view with sidebar navigation visible."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def run(driver):
+ WebDriverWait(driver, 15).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
+ )
+ WebDriverWait(driver, 15).until(
+ lambda d: len(d.find_elements(By.CSS_SELECTOR, ".action-button button")) >= 8
+ )
+
+ driver.find_element(By.ID, "sidebar-toggler-button").click()
+
+ WebDriverWait(driver, 15).until(
+ lambda d: d.find_element(By.ID, "mainnav").is_displayed()
+ )
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/with-navigation.png b/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/with-navigation.png
new file mode 100644
index 0000000..4e89e0c
Binary files /dev/null and b/docs/modules/ROOT/images/advanced_configuration/webui/with-navigation/with-navigation.png differ
diff --git a/docs/modules/ROOT/images/args-choices-entities.png b/docs/modules/ROOT/images/args-choices-entities.png
deleted file mode 100644
index 77a7ea4..0000000
Binary files a/docs/modules/ROOT/images/args-choices-entities.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/args-choices-exec.png b/docs/modules/ROOT/images/args-choices-exec.png
deleted file mode 100644
index aeca1d5..0000000
Binary files a/docs/modules/ROOT/images/args-choices-exec.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/args-multiline-text.png b/docs/modules/ROOT/images/args-multiline-text.png
deleted file mode 100644
index 02ac610..0000000
Binary files a/docs/modules/ROOT/images/args-multiline-text.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/args/checklist/.gitignore b/docs/modules/ROOT/images/args/checklist/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/args/checklist/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/args/checklist/Makefile b/docs/modules/ROOT/images/args/checklist/Makefile
new file mode 100644
index 0000000..720e66a
--- /dev/null
+++ b/docs/modules/ROOT/images/args/checklist/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../screenshots.mk
diff --git a/docs/modules/ROOT/images/args/checklist/checklist.png b/docs/modules/ROOT/images/args/checklist/checklist.png
new file mode 100644
index 0000000..495211c
Binary files /dev/null and b/docs/modules/ROOT/images/args/checklist/checklist.png differ
diff --git a/docs/modules/ROOT/images/args/checklist/config.yaml b/docs/modules/ROOT/images/args/checklist/config.yaml
new file mode 100644
index 0000000..35e537d
--- /dev/null
+++ b/docs/modules/ROOT/images/args/checklist/config.yaml
@@ -0,0 +1,22 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+actions:
+ - title: Backup selected directories
+ shell: echo "Backing up {{ directories }}"
+ arguments:
+ - name: directories
+ title: Directories to back up
+ type: checklist
+ choices:
+ - title: Documents
+ value: documents
+ - title: Photos
+ value: photos
+ - title: Music
+ value: music
+ default: '["documents","photos"]'
diff --git a/docs/modules/ROOT/images/args/checklist/screenshots.ini b/docs/modules/ROOT/images/args/checklist/screenshots.ini
new file mode 100644
index 0000000..d798e27
--- /dev/null
+++ b/docs/modules/ROOT/images/args/checklist/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 800
+height = 520
+post_script_sleep = 0.5
+
+[checklist]
+url = .
+name = checklist
+script = setup_checklist.py
diff --git a/docs/modules/ROOT/images/args/checklist/setup_checklist.py b/docs/modules/ROOT/images/args/checklist/setup_checklist.py
new file mode 100644
index 0000000..a7125cd
--- /dev/null
+++ b/docs/modules/ROOT/images/args/checklist/setup_checklist.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+"""Prepare the checklist argument form screenshot."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_body_attr(driver, attr, timeout=15):
+ WebDriverWait(driver, timeout).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute(attr))
+ )
+
+
+def run(driver):
+ _wait_for_body_attr(driver, "loaded-dashboard")
+
+ driver.find_element(
+ By.CSS_SELECTOR, '[title="Backup selected directories"]'
+ ).click()
+
+ _wait_for_body_attr(driver, "loaded-argument-form")
+
+ WebDriverWait(driver, 15).until(
+ lambda d: len(
+ d.find_elements(By.CSS_SELECTOR, ".choice-checklist-item input[type='checkbox']")
+ )
+ >= 3
+ )
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/args/confirmation/.gitignore b/docs/modules/ROOT/images/args/confirmation/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/args/confirmation/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/args/confirmation/Makefile b/docs/modules/ROOT/images/args/confirmation/Makefile
new file mode 100644
index 0000000..720e66a
--- /dev/null
+++ b/docs/modules/ROOT/images/args/confirmation/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../screenshots.mk
diff --git a/docs/modules/ROOT/images/args/confirmation/config.yaml b/docs/modules/ROOT/images/args/confirmation/config.yaml
new file mode 100644
index 0000000..85fa6a6
--- /dev/null
+++ b/docs/modules/ROOT/images/args/confirmation/config.yaml
@@ -0,0 +1,14 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+actions:
+ - title: Delete old backups
+ icon: ashtonished
+ shell: echo "Deleted old backups"
+ arguments:
+ - type: confirmation
+ title: Are you sure?!
diff --git a/docs/modules/ROOT/images/args/confirmation/confirmation.png b/docs/modules/ROOT/images/args/confirmation/confirmation.png
new file mode 100644
index 0000000..fbdd628
Binary files /dev/null and b/docs/modules/ROOT/images/args/confirmation/confirmation.png differ
diff --git a/docs/modules/ROOT/images/args/confirmation/screenshots.ini b/docs/modules/ROOT/images/args/confirmation/screenshots.ini
new file mode 100644
index 0000000..7c63be3
--- /dev/null
+++ b/docs/modules/ROOT/images/args/confirmation/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 800
+height = 420
+post_script_sleep = 0.5
+
+[confirmation]
+url = .
+name = confirmation
+script = setup_confirmation.py
diff --git a/docs/modules/ROOT/images/args/confirmation/setup_confirmation.py b/docs/modules/ROOT/images/args/confirmation/setup_confirmation.py
new file mode 100644
index 0000000..e7b5698
--- /dev/null
+++ b/docs/modules/ROOT/images/args/confirmation/setup_confirmation.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+"""Prepare the confirmation argument screenshot."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_body_attr(driver, attr, timeout=15):
+ WebDriverWait(driver, timeout).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute(attr))
+ )
+
+
+def run(driver):
+ _wait_for_body_attr(driver, "loaded-dashboard")
+
+ driver.find_element(By.CSS_SELECTOR, '[title="Delete old backups"]').click()
+ _wait_for_body_attr(driver, "loaded-argument-form")
+
+ WebDriverWait(driver, 15).until(
+ lambda d: not d.find_element(By.CSS_SELECTOR, 'button[name="start"]').is_enabled()
+ )
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/args/dropdown/.gitignore b/docs/modules/ROOT/images/args/dropdown/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/args/dropdown/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/args/dropdown/Makefile b/docs/modules/ROOT/images/args/dropdown/Makefile
new file mode 100644
index 0000000..720e66a
--- /dev/null
+++ b/docs/modules/ROOT/images/args/dropdown/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../screenshots.mk
diff --git a/docs/modules/ROOT/images/args/dropdown/config.yaml b/docs/modules/ROOT/images/args/dropdown/config.yaml
new file mode 100644
index 0000000..c1506c5
--- /dev/null
+++ b/docs/modules/ROOT/images/args/dropdown/config.yaml
@@ -0,0 +1,32 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+entities:
+ - file: entities/containers.json
+ name: container
+
+actions:
+ - title: Print a message
+ icon: smile
+ shell: echo "{{ message }}"
+ arguments:
+ - name: message
+ description: The message you want to print out.
+ choices:
+ - title: Hello
+ value: Hello there!
+ - title: Goodbye
+ value: Aww, goodbye. :-(
+ - title: restart container
+ shell: 'docker restart {{ containerToRestart }}'
+ arguments:
+ - name: containerToRestart
+ entity: container
+ title: Select Container
+ choices:
+ - value: '{{ container.Names }}'
+ title: '{{ container.Names }}'
diff --git a/docs/modules/ROOT/images/args/dropdown/dropdown-entities.png b/docs/modules/ROOT/images/args/dropdown/dropdown-entities.png
new file mode 100644
index 0000000..ae6874f
Binary files /dev/null and b/docs/modules/ROOT/images/args/dropdown/dropdown-entities.png differ
diff --git a/docs/modules/ROOT/images/args/dropdown/dropdown-logs-detail.png b/docs/modules/ROOT/images/args/dropdown/dropdown-logs-detail.png
new file mode 100644
index 0000000..0b7c869
Binary files /dev/null and b/docs/modules/ROOT/images/args/dropdown/dropdown-logs-detail.png differ
diff --git a/docs/modules/ROOT/images/args/dropdown/dropdown-logs-list.png b/docs/modules/ROOT/images/args/dropdown/dropdown-logs-list.png
new file mode 100644
index 0000000..9d511dd
Binary files /dev/null and b/docs/modules/ROOT/images/args/dropdown/dropdown-logs-list.png differ
diff --git a/docs/modules/ROOT/images/args/dropdown/dropdown.png b/docs/modules/ROOT/images/args/dropdown/dropdown.png
new file mode 100644
index 0000000..b715922
Binary files /dev/null and b/docs/modules/ROOT/images/args/dropdown/dropdown.png differ
diff --git a/docs/modules/ROOT/images/args/dropdown/entities/containers.json b/docs/modules/ROOT/images/args/dropdown/entities/containers.json
new file mode 100644
index 0000000..fe8df58
--- /dev/null
+++ b/docs/modules/ROOT/images/args/dropdown/entities/containers.json
@@ -0,0 +1,2 @@
+{"Names":"media-indexer"}
+{"Names":"game-server"}
diff --git a/docs/modules/ROOT/images/args/dropdown/screenshots.ini b/docs/modules/ROOT/images/args/dropdown/screenshots.ini
new file mode 100644
index 0000000..0826d9a
--- /dev/null
+++ b/docs/modules/ROOT/images/args/dropdown/screenshots.ini
@@ -0,0 +1,29 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 800
+height = 480
+post_script_sleep = 0.5
+
+[dropdown]
+url = .
+name = dropdown
+script = setup_dropdown.py
+
+[dropdown-logs-list]
+url = .
+name = dropdown-logs-list
+script = setup_dropdown_logs_list.py
+width = 960
+height = 640
+
+[dropdown-logs-detail]
+url = .
+name = dropdown-logs-detail
+script = setup_dropdown_logs_detail.py
+height = 560
+
+[dropdown-entities]
+url = .
+name = dropdown-entities
+script = setup_dropdown_entities.py
diff --git a/docs/modules/ROOT/images/args/dropdown/setup_dropdown.py b/docs/modules/ROOT/images/args/dropdown/setup_dropdown.py
new file mode 100644
index 0000000..866fce3
--- /dev/null
+++ b/docs/modules/ROOT/images/args/dropdown/setup_dropdown.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+"""Prepare the dropdown argument form screenshot."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_body_attr(driver, attr, timeout=15):
+ WebDriverWait(driver, timeout).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute(attr))
+ )
+
+
+def _open_choice_list(driver, input_id):
+ combobox_input = driver.find_element(By.ID, input_id)
+ combobox_input.click()
+ WebDriverWait(driver, 15).until(
+ lambda d: len(
+ d.find_elements(
+ By.CSS_SELECTOR,
+ f"#{input_id}-listbox li, #{input_id} + input + ul li",
+ )
+ )
+ >= 2
+ or len(d.find_elements(By.CSS_SELECTOR, ".choice-combobox-list li")) >= 2
+ )
+
+
+def run(driver):
+ _wait_for_body_attr(driver, "loaded-dashboard")
+
+ driver.find_element(By.CSS_SELECTOR, '[title="Print a message"]').click()
+ _wait_for_body_attr(driver, "loaded-argument-form")
+ _open_choice_list(driver, "message")
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/args/dropdown/setup_dropdown_entities.py b/docs/modules/ROOT/images/args/dropdown/setup_dropdown_entities.py
new file mode 100644
index 0000000..3200988
--- /dev/null
+++ b/docs/modules/ROOT/images/args/dropdown/setup_dropdown_entities.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python3
+"""Prepare the entity-backed dropdown screenshot."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_body_attr(driver, attr, timeout=15):
+ WebDriverWait(driver, timeout).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute(attr))
+ )
+
+
+def _open_choice_list(driver, input_id):
+ combobox_input = driver.find_element(By.ID, input_id)
+ combobox_input.click()
+ WebDriverWait(driver, 15).until(
+ lambda d: len(d.find_elements(By.CSS_SELECTOR, ".choice-combobox-list li")) >= 2
+ )
+
+
+def run(driver):
+ _wait_for_body_attr(driver, "loaded-dashboard")
+
+ driver.find_element(By.CSS_SELECTOR, '[title="restart container"]').click()
+ _wait_for_body_attr(driver, "loaded-argument-form")
+ _open_choice_list(driver, "containerToRestart")
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/args/dropdown/setup_dropdown_logs_detail.py b/docs/modules/ROOT/images/args/dropdown/setup_dropdown_logs_detail.py
new file mode 100644
index 0000000..55ace75
--- /dev/null
+++ b/docs/modules/ROOT/images/args/dropdown/setup_dropdown_logs_detail.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+"""Prepare the execution results screenshot for a dropdown action."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_dashboard(driver, timeout=15):
+ WebDriverWait(driver, timeout).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
+ )
+ WebDriverWait(driver, timeout).until(
+ lambda d: d.execute_script("return !!window.client")
+ )
+
+
+def _wait_for_logs_page(driver, timeout=15):
+ WebDriverWait(driver, timeout).until(
+ lambda d: "/logs/" in d.current_url and not d.current_url.rstrip("/").endswith("/logs")
+ )
+
+
+def _wait_for_execution_complete(driver, timeout=15):
+ def finished(d):
+ try:
+ status = d.find_element(By.CSS_SELECTOR, ".execution-dialog-status").text
+ except Exception:
+ return False
+ return "Still running" not in status and "Queued" not in status
+
+ WebDriverWait(driver, timeout).until(finished)
+
+
+def _wait_for_terminal_output(driver, expected, timeout=15):
+ WebDriverWait(driver, timeout).until(
+ lambda d: expected
+ in d.execute_script(
+ """
+ if (!window.terminal || !window.terminal.getBufferAsString) {
+ return '';
+ }
+ return window.terminal.getBufferAsString();
+ """
+ )
+ )
+
+
+def run(driver):
+ _wait_for_dashboard(driver)
+
+ driver.execute_async_script(
+ """
+ const done = arguments[arguments.length - 1];
+ const button = document.querySelector('[title="Print a message"]');
+ const bindingId = button.closest('.action-button').id.replace('actionButton-', '');
+
+ window.client.startAction({
+ bindingId: bindingId,
+ arguments: [{ name: 'message', value: 'Hello there!' }],
+ uniqueTrackingId: 'doc-screenshot-' + Date.now(),
+ }).then((response) => {
+ window.location.href = '/logs/' + response.executionTrackingId;
+ done(true);
+ }).catch((err) => done(String(err)));
+ """
+ )
+
+ _wait_for_logs_page(driver)
+ _wait_for_execution_complete(driver)
+ _wait_for_terminal_output(driver, "Hello there!")
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/args/dropdown/setup_dropdown_logs_list.py b/docs/modules/ROOT/images/args/dropdown/setup_dropdown_logs_list.py
new file mode 100644
index 0000000..ef78188
--- /dev/null
+++ b/docs/modules/ROOT/images/args/dropdown/setup_dropdown_logs_list.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+"""Prepare the logs list screenshot after running a dropdown action."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_dashboard(driver, timeout=15):
+ WebDriverWait(driver, timeout).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
+ )
+ WebDriverWait(driver, timeout).until(
+ lambda d: d.execute_script("return !!window.client")
+ )
+
+
+def _wait_for_logs_table(driver, timeout=15):
+ WebDriverWait(driver, timeout).until(
+ lambda d: len(d.find_elements(By.CSS_SELECTOR, ".logs-table tbody tr")) >= 1
+ )
+
+
+def run(driver):
+ _wait_for_dashboard(driver)
+
+ driver.execute_async_script(
+ """
+ const done = arguments[arguments.length - 1];
+ const button = document.querySelector('[title="Print a message"]');
+ const bindingId = button.closest('.action-button').id.replace('actionButton-', '');
+
+ window.client.startAction({
+ bindingId: bindingId,
+ arguments: [{ name: 'message', value: 'Hello there!' }],
+ uniqueTrackingId: 'doc-screenshot-' + Date.now(),
+ }).then(() => done(true)).catch((err) => done(String(err)));
+ """
+ )
+
+ time.sleep(0.5)
+ driver.execute_script("window.location.href = '/logs'")
+ _wait_for_logs_table(driver)
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/args/input/args1.png b/docs/modules/ROOT/images/args/input/args1.png
index 5400651..fb0412e 100644
Binary files a/docs/modules/ROOT/images/args/input/args1.png and b/docs/modules/ROOT/images/args/input/args1.png differ
diff --git a/docs/modules/ROOT/images/args/input/args2.png b/docs/modules/ROOT/images/args/input/args2.png
index e101daf..0114a07 100644
Binary files a/docs/modules/ROOT/images/args/input/args2.png and b/docs/modules/ROOT/images/args/input/args2.png differ
diff --git a/docs/modules/ROOT/images/args/input/args3.png b/docs/modules/ROOT/images/args/input/args3.png
index 203171a..d4ffd93 100644
Binary files a/docs/modules/ROOT/images/args/input/args3.png and b/docs/modules/ROOT/images/args/input/args3.png differ
diff --git a/docs/modules/ROOT/images/args/input/screenshots.ini b/docs/modules/ROOT/images/args/input/screenshots.ini
index 5006aaa..24898cf 100644
--- a/docs/modules/ROOT/images/args/input/screenshots.ini
+++ b/docs/modules/ROOT/images/args/input/screenshots.ini
@@ -19,3 +19,4 @@ script = setup_args2.py
url = .
name = args3
script = setup_args3.py
+height = 560
diff --git a/docs/modules/ROOT/images/args/input/setup_args3.py b/docs/modules/ROOT/images/args/input/setup_args3.py
index 9eda93e..bf358bb 100644
--- a/docs/modules/ROOT/images/args/input/setup_args3.py
+++ b/docs/modules/ROOT/images/args/input/setup_args3.py
@@ -65,5 +65,15 @@ def run(driver):
_wait_for_logs_page(driver)
_wait_for_execution_complete(driver)
-
+ WebDriverWait(driver, 15).until(
+ lambda d: "Hello World"
+ in d.execute_script(
+ """
+ if (!window.terminal || !window.terminal.getBufferAsString) {
+ return '';
+ }
+ return window.terminal.getBufferAsString();
+ """
+ )
+ )
time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/args/textarea/.gitignore b/docs/modules/ROOT/images/args/textarea/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/args/textarea/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/args/textarea/Makefile b/docs/modules/ROOT/images/args/textarea/Makefile
new file mode 100644
index 0000000..720e66a
--- /dev/null
+++ b/docs/modules/ROOT/images/args/textarea/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../screenshots.mk
diff --git a/docs/modules/ROOT/images/args/textarea/config.yaml b/docs/modules/ROOT/images/args/textarea/config.yaml
new file mode 100644
index 0000000..a9a28aa
--- /dev/null
+++ b/docs/modules/ROOT/images/args/textarea/config.yaml
@@ -0,0 +1,17 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+actions:
+ - title: Save text to file
+ shell: 'echo "{{ content }}" > file'
+ arguments:
+ - type: raw_string_multiline
+ name: content
+ title: Content
+ default: |
+ Line one of the message
+ Line two of the message
diff --git a/docs/modules/ROOT/images/args/textarea/multiline-text.png b/docs/modules/ROOT/images/args/textarea/multiline-text.png
new file mode 100644
index 0000000..ac26cff
Binary files /dev/null and b/docs/modules/ROOT/images/args/textarea/multiline-text.png differ
diff --git a/docs/modules/ROOT/images/args/textarea/screenshots.ini b/docs/modules/ROOT/images/args/textarea/screenshots.ini
new file mode 100644
index 0000000..853dad4
--- /dev/null
+++ b/docs/modules/ROOT/images/args/textarea/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 800
+height = 520
+post_script_sleep = 0.5
+
+[multiline-text]
+url = .
+name = multiline-text
+script = setup_textarea.py
diff --git a/docs/modules/ROOT/images/args/textarea/setup_textarea.py b/docs/modules/ROOT/images/args/textarea/setup_textarea.py
new file mode 100644
index 0000000..eee915a
--- /dev/null
+++ b/docs/modules/ROOT/images/args/textarea/setup_textarea.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+"""Prepare the multiline textarea argument screenshot."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_body_attr(driver, attr, timeout=15):
+ WebDriverWait(driver, timeout).until(
+ lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute(attr))
+ )
+
+
+def run(driver):
+ _wait_for_body_attr(driver, "loaded-dashboard")
+
+ driver.find_element(By.CSS_SELECTOR, '[title="Save text to file"]').click()
+
+ WebDriverWait(driver, 15).until(
+ lambda d: len(
+ d.find_elements(By.CSS_SELECTOR, "#argument-popup textarea#content")
+ )
+ >= 1
+ )
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/args4.png b/docs/modules/ROOT/images/args4.png
deleted file mode 100644
index 33b893f..0000000
Binary files a/docs/modules/ROOT/images/args4.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/dashboards/fieldsets/.gitignore b/docs/modules/ROOT/images/dashboards/fieldsets/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/dashboards/fieldsets/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/dashboards/fieldsets/Makefile b/docs/modules/ROOT/images/dashboards/fieldsets/Makefile
new file mode 100644
index 0000000..720e66a
--- /dev/null
+++ b/docs/modules/ROOT/images/dashboards/fieldsets/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../screenshots.mk
diff --git a/docs/modules/ROOT/images/dashboards/fieldsets/config.yaml b/docs/modules/ROOT/images/dashboards/fieldsets/config.yaml
new file mode 100644
index 0000000..6ae2be3
--- /dev/null
+++ b/docs/modules/ROOT/images/dashboards/fieldsets/config.yaml
@@ -0,0 +1,25 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+actions:
+ - title: Placeholder 1
+ shell: echo "placeholder 1"
+ - title: Placeholder 2
+ shell: echo "placeholder 2"
+
+dashboards:
+ - title: My First Dashboard
+ contents:
+ - title: Fieldset 1
+ type: fieldset
+ contents:
+ - title: Folder 1
+ contents:
+ - title: Placeholder 1
+ - title: Folder 2
+ contents:
+ - title: Placeholder 2
diff --git a/docs/modules/ROOT/images/dashboards/fieldsets/fieldset.png b/docs/modules/ROOT/images/dashboards/fieldsets/fieldset.png
new file mode 100644
index 0000000..4aa3b68
Binary files /dev/null and b/docs/modules/ROOT/images/dashboards/fieldsets/fieldset.png differ
diff --git a/docs/modules/ROOT/images/dashboards/fieldsets/screenshots.ini b/docs/modules/ROOT/images/dashboards/fieldsets/screenshots.ini
new file mode 100644
index 0000000..713e135
--- /dev/null
+++ b/docs/modules/ROOT/images/dashboards/fieldsets/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 980
+height = 400
+post_script_sleep = 0.5
+
+[fieldset]
+url = /dashboards/My%%20First%%20Dashboard
+name = fieldset
+script = setup_fieldset.py
diff --git a/docs/modules/ROOT/images/dashboards/fieldsets/setup_fieldset.py b/docs/modules/ROOT/images/dashboards/fieldsets/setup_fieldset.py
new file mode 100644
index 0000000..9feeec1
--- /dev/null
+++ b/docs/modules/ROOT/images/dashboards/fieldsets/setup_fieldset.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+"""Open My First Dashboard from dashboards/2-fieldsets.adoc."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_fieldset_dashboard(driver, timeout=30):
+ WebDriverWait(driver, timeout).until(
+ lambda d: d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard")
+ == "My First Dashboard"
+ )
+
+ def ready(d):
+ try:
+ folder1 = d.find_element(
+ By.XPATH,
+ '//button[contains(@class, "directory-button")]//span[contains(@class, "title") and text()="Folder 1"]',
+ )
+ folder2 = d.find_element(
+ By.XPATH,
+ '//button[contains(@class, "directory-button")]//span[contains(@class, "title") and text()="Folder 2"]',
+ )
+ except Exception:
+ return False
+ return all(element.is_displayed() for element in (folder1, folder2))
+
+ WebDriverWait(driver, timeout).until(ready)
+
+
+def run(driver):
+ _wait_for_fieldset_dashboard(driver)
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/dashboards/folders/.gitignore b/docs/modules/ROOT/images/dashboards/folders/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/dashboards/folders/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/dashboards/folders/Makefile b/docs/modules/ROOT/images/dashboards/folders/Makefile
new file mode 100644
index 0000000..720e66a
--- /dev/null
+++ b/docs/modules/ROOT/images/dashboards/folders/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../screenshots.mk
diff --git a/docs/modules/ROOT/images/dashboards/folders/config.yaml b/docs/modules/ROOT/images/dashboards/folders/config.yaml
new file mode 100644
index 0000000..945f57d
--- /dev/null
+++ b/docs/modules/ROOT/images/dashboards/folders/config.yaml
@@ -0,0 +1,33 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+actions:
+ - title: Action 1
+ shell: echo "action1"
+ - title: Action 2
+ shell: echo "action2"
+ - title: Action 3
+ shell: echo "action3"
+ - title: Action 4
+ shell: echo "action4"
+
+dashboards:
+ - title: My First Dashboard
+ contents:
+ - title: Fieldset 1
+ type: fieldset
+ contents:
+ - title: Folder 1
+ contents:
+ - title: Action 1
+ - title: Action 2
+ - title: Subfolder 2
+ contents:
+ - title: Action 3
+ - title: Folder 2
+ contents:
+ - title: Action 4
diff --git a/docs/modules/ROOT/images/dashboards/folders/folders.png b/docs/modules/ROOT/images/dashboards/folders/folders.png
new file mode 100644
index 0000000..d15c263
Binary files /dev/null and b/docs/modules/ROOT/images/dashboards/folders/folders.png differ
diff --git a/docs/modules/ROOT/images/dashboards/folders/screenshots.ini b/docs/modules/ROOT/images/dashboards/folders/screenshots.ini
new file mode 100644
index 0000000..ad856b4
--- /dev/null
+++ b/docs/modules/ROOT/images/dashboards/folders/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 980
+height = 640
+post_script_sleep = 0.5
+
+[folders]
+url = /dashboards/Folder%%201
+name = folders
+script = setup_folders.py
diff --git a/docs/modules/ROOT/images/dashboards/folders/setup_folders.py b/docs/modules/ROOT/images/dashboards/folders/setup_folders.py
new file mode 100644
index 0000000..c6d4875
--- /dev/null
+++ b/docs/modules/ROOT/images/dashboards/folders/setup_folders.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+"""Open Folder 1 from dashboards/3-folders.adoc."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_folder_dashboard(driver, timeout=30):
+ WebDriverWait(driver, timeout).until(
+ lambda d: d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard")
+ == "Folder 1"
+ )
+
+ def ready(d):
+ try:
+ action1 = d.find_element(By.CSS_SELECTOR, '[title="Action 1"]')
+ action2 = d.find_element(By.CSS_SELECTOR, '[title="Action 2"]')
+ subfolder = d.find_element(
+ By.XPATH,
+ '//button[contains(@class, "directory-button")]//span[contains(@class, "title") and text()="Subfolder 2"]',
+ )
+ except Exception:
+ return False
+ return all(
+ element.is_displayed()
+ for element in (action1, action2, subfolder)
+ )
+
+ WebDriverWait(driver, timeout).until(ready)
+
+
+def run(driver):
+ _wait_for_folder_dashboard(driver)
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/dashboards/intro/preview.png b/docs/modules/ROOT/images/dashboards/intro/preview.png
index 77e9158..7bb4e2f 100644
Binary files a/docs/modules/ROOT/images/dashboards/intro/preview.png and b/docs/modules/ROOT/images/dashboards/intro/preview.png differ
diff --git a/docs/modules/ROOT/images/dashboards/intro/screenshots.ini b/docs/modules/ROOT/images/dashboards/intro/screenshots.ini
index 048b585..254006a 100644
--- a/docs/modules/ROOT/images/dashboards/intro/screenshots.ini
+++ b/docs/modules/ROOT/images/dashboards/intro/screenshots.ini
@@ -6,6 +6,6 @@ height = 720
post_script_sleep = 0.5
[preview]
-url = /dashboards/My%20Servers
+url = /dashboards/My%%20Servers
name = preview
script = setup_preview.py
diff --git a/docs/modules/ROOT/images/defaultUiHideNav.png b/docs/modules/ROOT/images/defaultUiHideNav.png
deleted file mode 100644
index 07eaf35..0000000
Binary files a/docs/modules/ROOT/images/defaultUiHideNav.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/defaultUiWithNav.png b/docs/modules/ROOT/images/defaultUiWithNav.png
deleted file mode 100644
index 16d1a49..0000000
Binary files a/docs/modules/ROOT/images/defaultUiWithNav.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/diagnostics.png b/docs/modules/ROOT/images/diagnostics.png
deleted file mode 100644
index 4b216e7..0000000
Binary files a/docs/modules/ROOT/images/diagnostics.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/fieldset.png b/docs/modules/ROOT/images/fieldset.png
deleted file mode 100644
index cd9c0a5..0000000
Binary files a/docs/modules/ROOT/images/fieldset.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/folders.png b/docs/modules/ROOT/images/folders.png
deleted file mode 100644
index 7a36ff9..0000000
Binary files a/docs/modules/ROOT/images/folders.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/gitops.png b/docs/modules/ROOT/images/gitops.png
deleted file mode 100644
index 4288de6..0000000
Binary files a/docs/modules/ROOT/images/gitops.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/hacs-custom-repo.png b/docs/modules/ROOT/images/integrations/hass/hacs-custom-repo.png
similarity index 100%
rename from docs/modules/ROOT/images/hacs-custom-repo.png
rename to docs/modules/ROOT/images/integrations/hass/hacs-custom-repo.png
diff --git a/docs/modules/ROOT/images/hacs-download.png b/docs/modules/ROOT/images/integrations/hass/hacs-download.png
similarity index 100%
rename from docs/modules/ROOT/images/hacs-download.png
rename to docs/modules/ROOT/images/integrations/hass/hacs-download.png
diff --git a/docs/modules/ROOT/images/hacs-dropdown.png b/docs/modules/ROOT/images/integrations/hass/hacs-dropdown.png
similarity index 100%
rename from docs/modules/ROOT/images/hacs-dropdown.png
rename to docs/modules/ROOT/images/integrations/hass/hacs-dropdown.png
diff --git a/docs/modules/ROOT/images/hacs-search.png b/docs/modules/ROOT/images/integrations/hass/hacs-search.png
similarity index 100%
rename from docs/modules/ROOT/images/hacs-search.png
rename to docs/modules/ROOT/images/integrations/hass/hacs-search.png
diff --git a/docs/modules/ROOT/images/hass-add-integration.png b/docs/modules/ROOT/images/integrations/hass/hass-add-integration.png
similarity index 100%
rename from docs/modules/ROOT/images/hass-add-integration.png
rename to docs/modules/ROOT/images/integrations/hass/hass-add-integration.png
diff --git a/docs/modules/ROOT/images/hass-buttons.png b/docs/modules/ROOT/images/integrations/hass/hass-buttons.png
similarity index 100%
rename from docs/modules/ROOT/images/hass-buttons.png
rename to docs/modules/ROOT/images/integrations/hass/hass-buttons.png
diff --git a/docs/modules/ROOT/images/hass-configure-integration.png b/docs/modules/ROOT/images/integrations/hass/hass-configure-integration.png
similarity index 100%
rename from docs/modules/ROOT/images/hass-configure-integration.png
rename to docs/modules/ROOT/images/integrations/hass/hass-configure-integration.png
diff --git a/docs/modules/ROOT/images/hass-devices-and-services.png b/docs/modules/ROOT/images/integrations/hass/hass-devices-and-services.png
similarity index 100%
rename from docs/modules/ROOT/images/hass-devices-and-services.png
rename to docs/modules/ROOT/images/integrations/hass/hass-devices-and-services.png
diff --git a/docs/modules/ROOT/images/hassButtonSetup.png b/docs/modules/ROOT/images/integrations/hass/hassButtonSetup.png
similarity index 100%
rename from docs/modules/ROOT/images/hassButtonSetup.png
rename to docs/modules/ROOT/images/integrations/hass/hassButtonSetup.png
diff --git a/docs/modules/ROOT/images/hassConfigYaml.png b/docs/modules/ROOT/images/integrations/hass/hassConfigYaml.png
similarity index 100%
rename from docs/modules/ROOT/images/hassConfigYaml.png
rename to docs/modules/ROOT/images/integrations/hass/hassConfigYaml.png
diff --git a/docs/modules/ROOT/images/hassFileEditor.png b/docs/modules/ROOT/images/integrations/hass/hassFileEditor.png
similarity index 100%
rename from docs/modules/ROOT/images/hassFileEditor.png
rename to docs/modules/ROOT/images/integrations/hass/hassFileEditor.png
diff --git a/docs/modules/ROOT/images/hassFileEditorConfig.png b/docs/modules/ROOT/images/integrations/hass/hassFileEditorConfig.png
similarity index 100%
rename from docs/modules/ROOT/images/hassFileEditorConfig.png
rename to docs/modules/ROOT/images/integrations/hass/hassFileEditorConfig.png
diff --git a/docs/modules/ROOT/images/maxRate.png b/docs/modules/ROOT/images/maxRate.png
deleted file mode 100644
index 7721e2c..0000000
Binary files a/docs/modules/ROOT/images/maxRate.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/page-title.png b/docs/modules/ROOT/images/page-title.png
deleted file mode 100644
index 12af3be..0000000
Binary files a/docs/modules/ROOT/images/page-title.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/popupOutputOnly.png b/docs/modules/ROOT/images/popupOutputOnly.png
deleted file mode 100644
index 69feb03..0000000
Binary files a/docs/modules/ROOT/images/popupOutputOnly.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/sidebar.png b/docs/modules/ROOT/images/sidebar.png
deleted file mode 100644
index bcae4ac..0000000
Binary files a/docs/modules/ROOT/images/sidebar.png and /dev/null differ
diff --git a/docs/modules/ROOT/images/solutions/on-git-push/.gitignore b/docs/modules/ROOT/images/solutions/on-git-push/.gitignore
new file mode 100644
index 0000000..431dbf1
--- /dev/null
+++ b/docs/modules/ROOT/images/solutions/on-git-push/.gitignore
@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/
diff --git a/docs/modules/ROOT/images/solutions/on-git-push/Makefile b/docs/modules/ROOT/images/solutions/on-git-push/Makefile
new file mode 100644
index 0000000..720e66a
--- /dev/null
+++ b/docs/modules/ROOT/images/solutions/on-git-push/Makefile
@@ -0,0 +1,2 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+include ../../screenshots.mk
diff --git a/docs/modules/ROOT/images/solutions/on-git-push/config.yaml b/docs/modules/ROOT/images/solutions/on-git-push/config.yaml
new file mode 100644
index 0000000..97ed526
--- /dev/null
+++ b/docs/modules/ROOT/images/solutions/on-git-push/config.yaml
@@ -0,0 +1,42 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+actions:
+ - title: ServerConfiguration Ansible
+ id: ServerConfiguration_Ansible
+ icon:
+ shell: echo "ansible configuration"
+
+ - title: ServerConfiguration Flux
+ id: ServerConfiguration_Flux
+ icon:
+ shell: echo "flux configuration"
+
+ - title: ServerConfiguration Prom
+ id: ServerConfiguration_Prom
+ icon:
+ shell: echo "prometheus configuration"
+
+ - title: Run on Git Push
+ id: gitops
+ icon:
+ shell: |
+ echo "You just pushed commit $COMMIT to git, running action..."
+ date
+ arguments:
+ - name: commit
+ type: ascii
+
+dashboards:
+ - title: GitOps
+ contents:
+ - title: ServerConfiguration
+ type: fieldset
+ contents:
+ - title: ServerConfiguration Ansible
+ - title: ServerConfiguration Flux
+ - title: ServerConfiguration Prom
diff --git a/docs/modules/ROOT/images/solutions/on-git-push/gitops.png b/docs/modules/ROOT/images/solutions/on-git-push/gitops.png
new file mode 100644
index 0000000..a487323
Binary files /dev/null and b/docs/modules/ROOT/images/solutions/on-git-push/gitops.png differ
diff --git a/docs/modules/ROOT/images/solutions/on-git-push/screenshots.ini b/docs/modules/ROOT/images/solutions/on-git-push/screenshots.ini
new file mode 100644
index 0000000..c44bf0a
--- /dev/null
+++ b/docs/modules/ROOT/images/solutions/on-git-push/screenshots.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 980
+height = 520
+post_script_sleep = 0.5
+
+[gitops]
+url = /dashboards/GitOps
+name = gitops
+script = setup_gitops.py
diff --git a/docs/modules/ROOT/images/solutions/on-git-push/setup_gitops.py b/docs/modules/ROOT/images/solutions/on-git-push/setup_gitops.py
new file mode 100644
index 0000000..fc7f420
--- /dev/null
+++ b/docs/modules/ROOT/images/solutions/on-git-push/setup_gitops.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+"""Open the GitOps dashboard from solutions/on-git-push/index.adoc."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def _wait_for_gitops_dashboard(driver, timeout=30):
+ WebDriverWait(driver, timeout).until(
+ lambda d: d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard")
+ == "GitOps"
+ )
+
+ def ready(d):
+ required_titles = [
+ "ServerConfiguration Ansible",
+ "ServerConfiguration Flux",
+ "ServerConfiguration Prom",
+ ]
+ for title in required_titles:
+ try:
+ button = d.find_element(By.CSS_SELECTOR, f'[title="{title}"]')
+ except Exception:
+ return False
+ if not button.is_displayed():
+ return False
+ return True
+
+ WebDriverWait(driver, timeout).until(ready)
+
+
+def run(driver):
+ _wait_for_gitops_dashboard(driver)
+ time.sleep(0.2)
diff --git a/docs/modules/ROOT/images/topbar.png b/docs/modules/ROOT/images/topbar.png
deleted file mode 100644
index 347b465..0000000
Binary files a/docs/modules/ROOT/images/topbar.png and /dev/null differ
diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc
index ac12883..9e6d7da 100644
--- a/docs/modules/ROOT/nav.adoc
+++ b/docs/modules/ROOT/nav.adoc
@@ -98,6 +98,8 @@
** xref:logs/queue.adoc[Queue view]
** xref:logs/saving.adoc[Saving logs]
* xref:entities/intro.adoc[Entities]
+** xref:entities/properties.adoc[Entity Properties]
+** xref:entities/icons.adoc[Entity Icons]
** xref:entities/examples.adoc[Examples]
** xref:entities/yaml.adoc[YAML Entity Files]
** xref:entities/json.adoc[JSON Entity Files]
diff --git a/docs/modules/ROOT/pages/action_customization/ratelimiting.adoc b/docs/modules/ROOT/pages/action_customization/ratelimiting.adoc
index a229d0d..f3380cb 100644
--- a/docs/modules/ROOT/pages/action_customization/ratelimiting.adoc
+++ b/docs/modules/ROOT/pages/action_customization/ratelimiting.adoc
@@ -19,12 +19,10 @@ actions:
If you try to execute `date` more than 3 times in 5 minutes, you will get a log message in the UI that looks like this;
-image::maxRate.png[]
+image::action_customization/max-rate/maxRate.png[]
Additionally, OliveTin will also output this to it's process log;
----
INFO Blocked from executing. This action has run 3 out of 3 allowed times in the last 5m. actionTitle="date"
----
-
-
diff --git a/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc b/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc
index c12d09e..854ec96 100644
--- a/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc
+++ b/docs/modules/ROOT/pages/advanced_configuration/diagnostics.adoc
@@ -4,11 +4,11 @@ OliveTin provides a built-in diagnostics page that can be used to help check how
This is a screenshot of the diagnostics page, which can be accessed by clicking the "Diagnostics" link in the navigation bar:
-image::diagnostics.png[]
+image::advanced_configuration/diagnostics/diagnostics.png[]
== Disabling Diagnostics
-The diagnostics page is enabled by default, but you can disable it by using the OliveTin xref::security/acl.adoc#_acls_and_policies_global[security policy configuration], using the defaults, or via an ACL. Examples are shown below for each of these methods.
+The diagnostics page is enabled by default, but you can disable it by using the OliveTin xref::security/acl.adoc#_acls_and_policies_global[security policy configuration], using the defaults, or via an ACL. Examples are shown below for each of these methods.
=== Disable Diagnostics for all users;
diff --git a/docs/modules/ROOT/pages/advanced_configuration/webui.adoc b/docs/modules/ROOT/pages/advanced_configuration/webui.adoc
index 52215bf..504d349 100644
--- a/docs/modules/ROOT/pages/advanced_configuration/webui.adoc
+++ b/docs/modules/ROOT/pages/advanced_configuration/webui.adoc
@@ -7,7 +7,7 @@ The OliveTin web UI is reasonably customizable - parts of the page that you don'
You can customize the page title;
-image::page-title.png[]
+image::advanced_configuration/webui/page-title/page-title.png[]
.`config.yaml`
[source,yaml]
@@ -21,7 +21,7 @@ pageTitle: My OliveTin Instance
You can choose to hide the navigation elements in OliveTin, to present a simplified user interface.
.The default user interface with the sidebar shown
-image::defaultUiWithNav.png[]
+image::advanced_configuration/webui/with-navigation/with-navigation.png[]
To have OliveTin hide these buttons, add `showNavigation: false` to your config.yaml;
@@ -36,7 +36,7 @@ actions:
----
.The same user interface, but with the sidebar hidden (`showNavigation: false`)
-image::defaultUiHideNav.png[]
+image::advanced_configuration/webui/hide-navigation/hide-navigation.png[]
[#show-navigate-on-start-icons]
== Navigate-on-start icons on action buttons
@@ -65,13 +65,13 @@ showNavigateOnStartIcons: false
`sectionNavigationStyle: sidebar` looks like this;
-image::sidebar.png[]
+image::advanced_configuration/webui/sidebar/sidebar.png[]
=== Topbar navigation style
`sectionNavigationStyle: topbar` looks like this;
-image::topbar.png[]
+image::advanced_configuration/webui/topbar/topbar.png[]
[#show-version-number]
== Version number in the footer
diff --git a/docs/modules/ROOT/pages/args/input_checklist.adoc b/docs/modules/ROOT/pages/args/input_checklist.adoc
index 229814e..e831fd7 100644
--- a/docs/modules/ROOT/pages/args/input_checklist.adoc
+++ b/docs/modules/ROOT/pages/args/input_checklist.adoc
@@ -19,14 +19,17 @@ actions:
value: photos
- title: Music
value: music
- default: documents,photos
+ default: '["documents","photos"]'
----
+.Checklist argument form with Documents and Photos selected by default.
+image::args/checklist/checklist.png[]
+
When the example above runs with Documents and Photos selected, the shell command becomes:
[source,shell]
----
-echo "Backing up: documents,photos"
+echo "Backing up: [\"documents\",\"photos\"]"
----
== Select all / Select none
diff --git a/docs/modules/ROOT/pages/args/input_confirmation.adoc b/docs/modules/ROOT/pages/args/input_confirmation.adoc
index 6eb0f93..1209f22 100644
--- a/docs/modules/ROOT/pages/args/input_confirmation.adoc
+++ b/docs/modules/ROOT/pages/args/input_confirmation.adoc
@@ -14,8 +14,6 @@ actions:
title: Are you sure?!
----
-image::action-confirmation.png[]
+image::args/confirmation/confirmation.png[]
Notice in the webui the "start" button is disabled.
-
-
diff --git a/docs/modules/ROOT/pages/args/input_dropdown.adoc b/docs/modules/ROOT/pages/args/input_dropdown.adoc
index 2dffe73..6179694 100644
--- a/docs/modules/ROOT/pages/args/input_dropdown.adoc
+++ b/docs/modules/ROOT/pages/args/input_dropdown.adoc
@@ -24,15 +24,15 @@ Note that when predefined choices are used, the argument type is ignored.
This is what it looks like in the web interface;
-image::args4.png[]
+image::args/dropdown/dropdown.png[]
Then finally, when you execute this command, it would look something like this (remember that this is just a basic "echo" command).
-image::args-choices-exec.png[]
+image::args/dropdown/dropdown-logs-list.png[]
In the logs, you can then click on the log entry link to open the results;
-image::args/input/args3.png[]
+image::args/dropdown/dropdown-logs-detail.png[]
[#args-dropdown-entities]
== Using Entities in Dropdowns
@@ -60,7 +60,7 @@ entities:
This is what it looks like in the web interface;
-image::args-choices-entities.png[]
+image::args/dropdown/dropdown-entities.png[]
include::partial$args/reject-null.adoc[]
diff --git a/docs/modules/ROOT/pages/args/input_textarea.adoc b/docs/modules/ROOT/pages/args/input_textarea.adoc
index 2e06980..927144a 100644
--- a/docs/modules/ROOT/pages/args/input_textarea.adoc
+++ b/docs/modules/ROOT/pages/args/input_textarea.adoc
@@ -18,4 +18,4 @@ actions:
This renders like this;
-image::args-multiline-text.png[]
+image::args/textarea/multiline-text.png[]
diff --git a/docs/modules/ROOT/pages/dashboards/2-fieldsets.adoc b/docs/modules/ROOT/pages/dashboards/2-fieldsets.adoc
index 270aa78..78b43f9 100644
--- a/docs/modules/ROOT/pages/dashboards/2-fieldsets.adoc
+++ b/docs/modules/ROOT/pages/dashboards/2-fieldsets.adoc
@@ -4,7 +4,7 @@
It is possible to group actions together in a "group", which is not a directory, but is called
a "fieldset". This is an example of a fieldset that contains two xref:dashboards/3-folders.adoc[Folders].
-image::fieldset.png[]
+image::dashboards/fieldsets/fieldset.png[]
Fieldsets are defined under a xref:dashboards/intro.adoc[Dashboards] in your config.yaml.
@@ -38,5 +38,3 @@ dashboards:
- title: Start {{ server.Name }}
- title: Shutdown {{ server.Name }}
----
-
-
diff --git a/docs/modules/ROOT/pages/dashboards/3-folders.adoc b/docs/modules/ROOT/pages/dashboards/3-folders.adoc
index b4e964e..c6293ba 100644
--- a/docs/modules/ROOT/pages/dashboards/3-folders.adoc
+++ b/docs/modules/ROOT/pages/dashboards/3-folders.adoc
@@ -4,7 +4,7 @@
Folders (Directories) are a good way to group up actions in the same way that you would
organize files on your computer into directories.
-image::folders.png[]
+image::dashboards/folders/folders.png[]
You must first create a dashboard to use a directory, and then you "reference" actions that you
want in that folder based on the action name. Anything without a "contents" property is treated
@@ -48,5 +48,3 @@ dashboards:
- title: Action 4
----
-
-
diff --git a/docs/modules/ROOT/pages/entities/icons.adoc b/docs/modules/ROOT/pages/entities/icons.adoc
new file mode 100644
index 0000000..2a1635d
--- /dev/null
+++ b/docs/modules/ROOT/pages/entities/icons.adoc
@@ -0,0 +1,115 @@
+[#entity-icons]
+= Entity icons
+
+The `icon` field is configured on each entity definition in `config.yaml`. It sets an icon for the entity *type* (for example, all `server` instances share the same icon).
+
+Entity icons use the same icon system as actions. See xref:action_customization/icons.adoc[Icons] for the full range of supported formats — Unicode aliases, HTML entities, Iconify, image paths, and custom-webui assets.
+
+== Configuration
+
+Add `icon` under an entity definition alongside `file`, `name`, and `properties`:
+
+[source,yaml]
+----
+entities:
+ - file: /etc/OliveTin/servers.yaml
+ name: server
+ icon: ssh
+ properties:
+ - name: hostname
+ title: Hostname
+
+ - file: /etc/OliveTin/containers.json
+ name: container
+ icon:
+----
+
+If `icon` is omitted or empty, no icon is shown for that entity type. Unlike actions, entity types do not get a default icon.
+
+=== Short-name aliases
+
+OliveTin resolves a few common short names to Unicode HTML entities at config load time. These are convenient for entity types that match common action categories:
+
+[cols="1,1", options="header"]
+|===
+| Alias | Typical use
+
+| `ssh`
+| Servers accessed over SSH
+
+| `ping`
+| Network hosts
+
+| `box`
+| Containers or packages
+
+| `backup`
+| Backup targets
+
+| `restart` / `reboot`
+| Systems or services
+
+| `light`
+| Lights or switches
+
+| `robot`
+| Automation targets
+|===
+
+The full alias list is in the xref:action_customization/icons.adoc#icons[Icons] documentation and in the source file `service/internal/config/emoji.go`.
+
+=== Custom-webui image paths
+
+A path containing `/` (that does not start with `<`) is expanded to an `
` tag pointing at `/custom-webui/`. For example, `icons/server.png` becomes an image served from `/custom-webui/icons/server.png`.
+
+== Where icons appear
+
+Entity icons are shown in the web UI next to the entity type name, not next to individual instances.
+
+=== Entities page
+
+On the Entities page, each entity type section heading shows the configured icon beside the type name (for example, `Entity: server`).
+
+=== Entity details page
+
+When viewing a single entity instance, the icon appears in the page title beside the instance name. The icon comes from the entity type definition, not from fields in the entity data file.
+
+=== API
+
+The `icon` field is included on:
+
+* `EntityDefinition` — returned by `GetEntities` for each entity type
+* `Entity` — returned by `GetEntity` for a single instance (same type-level icon)
+
+== What entity icons do not affect
+
+* **Individual instances** — icons are per entity type in `config.yaml`, not per row in the entity data file. You cannot give `server1` a different icon from `server2` through entity configuration alone.
+* **Dashboards** — entity fieldsets and directories use their own `icon` fields on dashboard components. See xref:dashboards/intro.adoc[Dashboards].
+* **Action buttons** — actions have their own `icon` field. An entity-bound action does not inherit the entity type icon automatically.
+
+== Example
+
+[source,yaml]
+----
+entities:
+ - file: entities/servers.yaml
+ name: server
+ icon: ssh
+ properties:
+ - name: hostname
+ title: Hostname
+ - name: ip
+ title: IP
+
+ - file: entities/containers.json
+ name: container
+ icon: box
+----
+
+With this configuration, the Entities page shows a key icon beside the `server` section and a box icon beside the `container` section. Opening any server instance shows the same key icon in the details page title.
+
+== What's next?
+
+* xref:action_customization/icons.adoc[Icons] — full icon format reference (Iconify, Unicode, images, and offline hosting)
+* xref:entities/intro.adoc[Entities] — overview of entities in OliveTin
+* xref:entities/properties.adoc[Entity properties] — configure which fields appear in the entity list
diff --git a/docs/modules/ROOT/pages/entities/intro.adoc b/docs/modules/ROOT/pages/entities/intro.adoc
index 401a2a2..42cd355 100644
--- a/docs/modules/ROOT/pages/entities/intro.adoc
+++ b/docs/modules/ROOT/pages/entities/intro.adoc
@@ -9,7 +9,9 @@ A very popular use case that entities were designed for was for `container` enti
Entities are just loaded from files on disk, OliveTin will also watch these files for updates while OliveTin is running, and update entities.
-Entities can have properties defined in those files, and those can be used in your configuration as variables. For example; `container.status`, or `vm.hostname`.
+Entity data files can contain any fields you need. Those values are available in action templates as `{{ .CurrentEntity.field }}` — for example, `{{ .CurrentEntity.status }}` or `{{ .CurrentEntity.hostname }}`.
+
+To control which fields appear in the Entities page table and entity details view, configure `properties` on the entity definition in `config.yaml`. See xref:entities/properties.adoc[Entity properties] for details.
[source,yaml]
----
@@ -19,6 +21,12 @@ entities:
- file: /etc/OliveTin/servers.yaml
name: server
+ icon: ssh
+ properties:
+ - name: hostname
+ title: Hostname
+ - name: ip
+ title: IP
----
Entity Actions can only be used on xref:dashboards/intro.adoc[Dashboards].
@@ -27,6 +35,8 @@ Entity Actions can only be used on xref:dashboards/intro.adoc[Dashboards].
Now that you understand entities, here's how to use them effectively:
+* xref:entities/properties.adoc[Configure entity properties] - Choose which fields appear in the UI and API
+* xref:entities/icons.adoc[Configure entity icons] - Set an icon for each entity type
* xref:entities/yaml.adoc[Create YAML entity files] - Learn the YAML format for entity files
* xref:entities/json.adoc[Create JSON entity files] - Learn the JSON format for entity files
* xref:entities/examples.adoc[View entity examples] - See complete examples of entity configurations
diff --git a/docs/modules/ROOT/pages/entities/properties.adoc b/docs/modules/ROOT/pages/entities/properties.adoc
new file mode 100644
index 0000000..4f89c4d
--- /dev/null
+++ b/docs/modules/ROOT/pages/entities/properties.adoc
@@ -0,0 +1,120 @@
+[#entity-properties]
+= Entity properties
+
+The `properties` field is configured on each entity definition in `config.yaml`. It does not live in the entity data files themselves.
+
+Use it to choose which fields from your entity data files are shown in the web UI and API. Property *values* come from the entity file on disk; `properties` only controls which of those values OliveTin exposes when listing or viewing entities.
+
+== Configuration
+
+Add `properties` under an entity definition alongside `file`, `name`, and `icon`:
+
+[source,yaml]
+----
+entities:
+ - file: /etc/OliveTin/servers.yaml
+ name: server
+ icon: ssh
+ properties:
+ - name: hostname
+ title: Hostname
+ - name: ip
+ title: IP
+----
+
+Each entry has two fields:
+
+[cols="1,3", options="header"]
+|===
+| Field | Description
+
+| `name`
+| The key to read from each entity instance in the data file. Matching is case-insensitive.
+
+| `title`
+| The column heading shown in the entity list table. If omitted, OliveTin uses `name`.
+|===
+
+The entity data file supplies the values. For example, with the configuration above and this data file:
+
+[source,yaml]
+.`/etc/OliveTin/servers.yaml`
+----
+- name: server1
+ hostname: server1.example.com
+ ip: 192.168.0.1
+- name: server2
+ hostname: server2.example.com
+ ip: 192.168.0.2
+----
+
+OliveTin shows `server1` and `server2` as instance names (from the `name` field) and displays `hostname` and `ip` in the configured columns.
+
+== Effects
+
+=== Entity list in the web UI
+
+When `properties` is configured, the Entities page shows a searchable, paginated table for that entity type. Columns are:
+
+* **Name** — the instance title (derived from fields such as `name`, `title`, `hostname`, and so on)
+* One column per configured property, labelled with `title`
+
+When `properties` is omitted or empty, the Entities page shows a simple list of instance names and a total count instead of a table.
+
+=== Entity details page
+
+The entity details view shows only the fields listed in `properties`, plus type and title.
+
+If `properties` is not configured, all top-level fields from the entity data file are shown.
+
+=== API responses
+
+`properties` controls which fields appear in `fields` on entity instances returned by the API:
+
+* `GetEntity` — returns only configured property values in `fields` when `properties` is set; otherwise returns all top-level fields from the data file.
+* `GetEntities` — always includes the property definitions on `EntityDefinition`. When `properties` is set, instance rows in list responses include only those configured fields. The unfiltered list request omits instance rows (it returns `totalInstances` only); use a filtered request with `entityType` to fetch paginated instances for the table view.
+
+Search and pagination on the entity list operate over instance titles and the configured property values.
+
+=== What `properties` does not affect
+
+`properties` is a display and API filtering setting. It does **not** limit what you can use in action templates.
+
+Actions bound to an entity still have access to the full entity record through `{{ .CurrentEntity.field }}`, including fields you did not list under `properties`. See xref:args/templates.adoc[Templates in actions] and xref:action_customization/enabledExpression.adoc[Enabled Expression].
+
+Legacy template syntax such as `{{ server.hostname }}` is still migrated automatically to `{{ .CurrentEntity.hostname }}`.
+
+== Example
+
+This configuration pairs with the server data file shown above:
+
+[source,yaml]
+----
+entities:
+ - file: entities/servers.yaml
+ name: server
+ icon: ssh
+ properties:
+ - name: hostname
+ title: Hostname
+ - name: ip
+ title: IP
+----
+
+An action can still reference any field from the data file, even ones not listed in `properties`:
+
+[source,yaml]
+----
+actions:
+ - title: Wake server
+ shell: wakeonlan {{ .CurrentEntity.ip }}
+ entity: server
+----
+
+== What's next?
+
+* xref:entities/yaml.adoc[YAML entity files] — format for entity data files
+* xref:entities/json.adoc[JSON entity files] — line-delimited JSON entity files
+* xref:entities/icons.adoc[Entity icons] — configure an icon for each entity type
+* xref:entities/intro.adoc[Entities] — overview of entities in OliveTin
+* xref:dashboards/entity-directories.adoc[Entity directories] — generate per-entity dashboards
diff --git a/docs/modules/ROOT/pages/integrations/homeassistant-integration.adoc b/docs/modules/ROOT/pages/integrations/homeassistant-integration.adoc
index 816c924..814dc9e 100644
--- a/docs/modules/ROOT/pages/integrations/homeassistant-integration.adoc
+++ b/docs/modules/ROOT/pages/integrations/homeassistant-integration.adoc
@@ -2,38 +2,38 @@
Integrating OliveTin with Home Assistant allows you to control OliveTin from your Home Assistant dashboard. Using the HACS integration is the recommended way to integrate OliveTin with Home Assistant. It is easy to set up and provides a seamless experience.
-If you are not familiar with HACs, it is a custom component for Home Assistant that allows you to install and manage custom integrations easily. It is similar to the Home Assistant Add-ons store but for integrations.
+If you are not familiar with HACs, it is a custom component for Home Assistant that allows you to install and manage custom integrations easily. It is similar to the Home Assistant Add-ons store but for integrations.
== Setup guide
. link:https://hacs.xyz/docs/use/[Install HACS]
-. Go to HACS in your home assistant control panel. Click the "..." dropdown in the top right corner, and select "**Custom Repositories**".
+. Go to HACS in your home assistant control panel. Click the "..." dropdown in the top right corner, and select "**Custom Repositories**".
+
-image::hacs-dropdown.png[]
+image::integrations/hass/hacs-dropdown.png[]
. In the dialog that pops up, add the custom repo as follows;
+
-image::hacs-custom-repo.png[]
+image::integrations/hass/hacs-custom-repo.png[]
+
.. **Repository link**: https://github.com/OliveTin/OliveTin-HomeAssistant
.. **Type**: Integration
. Search for "OliveTin" in the HACS store, and download it. You will probably need to restart Home Assistant for it be registered correctly.
+
-image::hacs-search.png[]
+image::integrations/hass/hacs-search.png[]
+
-image::hacs-download.png[]
+image::integrations/hass/hacs-download.png[]
. Go to "Settings" and open "Devices & Services"
+
-image::hass-devices-and-services.png[]
+image::integrations/hass/hass-devices-and-services.png[]
. Click "Add integration" and search for "OliveTin". Note that if it does not show up, you may need to restart Home Assistant.
+
-image::hass-add-integration.png[]
+image::integrations/hass/hass-add-integration.png[]
.. Select "OliveTin"
.. Host: http://your-server:1337/api
.. Username:
.. Password:
-. Under "Configured", you should see OliveTin. Open it by clicking on the arrow.
+. Under "Configured", you should see OliveTin. Open it by clicking on the arrow.
+
-image:hass-configure-integration.png[]
+image::integrations/hass/hass-configure-integration.png[]
. After configuring it, you should see buttons appear in Home Assistant;
+
-image:hass-buttons.png[]
+image::integrations/hass/hass-buttons.png[]
diff --git a/docs/modules/ROOT/pages/integrations/homeassistant.adoc b/docs/modules/ROOT/pages/integrations/homeassistant.adoc
index 4a15eed..1d0dfec 100644
--- a/docs/modules/ROOT/pages/integrations/homeassistant.adoc
+++ b/docs/modules/ROOT/pages/integrations/homeassistant.adoc
@@ -31,11 +31,11 @@ Now that you have the URL to call to trigger your action, you can add this to yo
That page assumes you will use the Home Assistant File Editor addon to edit your configuration.yaml. Install it from the Home Assistant addon store if you have not done so already;
-image::hassFileEditor.png[]
+image::integrations/hass/hassFileEditor.png[]
The addon is started and added to the sidebar;
-image::hassFileEditorConfig.png[]
+image::integrations/hass/hassFileEditorConfig.png[]
Here is an example of a `rest_command` that calls the action above:
@@ -55,7 +55,7 @@ You save the file, and restart Home Assistant to pick up the changes.
Now that you have a `rest_command` set up to call your action, you can add a button to your Home Assistant dashboard to trigger the action. This is done by adding a `button` to your dashboard configuration.
-image::hassButtonSetup.png[]
+image::integrations/hass/hassButtonSetup.png[]
Set the "Tap Action" to "Call Service" and select the `rest_command` you created earlier. You can also set the icon and name of the button to whatever you like.
diff --git a/docs/modules/ROOT/pages/security/oauth2_pocketid.adoc b/docs/modules/ROOT/pages/security/oauth2_pocketid.adoc
index 26418d1..73de16a 100644
--- a/docs/modules/ROOT/pages/security/oauth2_pocketid.adoc
+++ b/docs/modules/ROOT/pages/security/oauth2_pocketid.adoc
@@ -39,6 +39,7 @@ authOAuth2Providers:
clientId: "[REDACTED]"
clientSecret: "[REDACTED]"
scopes:
+ - openid
- profile
- email
usernameField: preferred_username
@@ -52,4 +53,4 @@ actions:
== Pocket ID config
-image::pocketid.png[]
\ No newline at end of file
+image::pocketid.png[]
diff --git a/docs/modules/ROOT/pages/solutions/on-git-push/index.adoc b/docs/modules/ROOT/pages/solutions/on-git-push/index.adoc
index 361865b..455d4ac 100644
--- a/docs/modules/ROOT/pages/solutions/on-git-push/index.adoc
+++ b/docs/modules/ROOT/pages/solutions/on-git-push/index.adoc
@@ -1,7 +1,7 @@
[#solution-on-git-push]
= GitOps (run actions on Git Push)
-image::gitops.png[]
+image::solutions/on-git-push/gitops.png[]
A really helpful thing to do with OliveTin is to have it run actions when you push to a Git repository. This is a great way to automate things like running tests, building your project, or deploying your code - this turns OliveTin into a powerful GitOps tool, or even a Continuous Integration tool.
diff --git a/docs/modules/ROOT/pages/troubleshooting/advanced.adoc b/docs/modules/ROOT/pages/troubleshooting/advanced.adoc
index 149e5b7..245d3ab 100644
--- a/docs/modules/ROOT/pages/troubleshooting/advanced.adoc
+++ b/docs/modules/ROOT/pages/troubleshooting/advanced.adoc
@@ -9,7 +9,7 @@ All these configuration options are `false` by default, and should be deleted fr
[#dump-server-diagnostics]
== Dump server diagnostics
-`InsecureAllowDumpSos: true` - will allow dumping xref:troubleshooting/server-diagnostics.adoc[server diagnostics] as plain text when visiting `http://server:1337/api/sosreport`
+`insecureAllowDumpServerDiagnostics: true` - will allow dumping xref:troubleshooting/server-diagnostics.adoc[server diagnostics] as plain text when visiting `http://server:1337/api/ServerDiagnostics`
[#dump-action-map]
== Dump Action Map
diff --git a/docs/modules/ROOT/pages/troubleshooting/server-diagnostics.adoc b/docs/modules/ROOT/pages/troubleshooting/server-diagnostics.adoc
index 4ff61aa..a68e555 100644
--- a/docs/modules/ROOT/pages/troubleshooting/server-diagnostics.adoc
+++ b/docs/modules/ROOT/pages/troubleshooting/server-diagnostics.adoc
@@ -6,7 +6,7 @@ OliveTin has a useful feature to gather information about your installation when
[source,yaml]
.Example server diagnostics output
----
-### SOSREPORT START (copy all text to SOSREPORT END)
+### SERVER DIAGNOSTICS START (copy all text to SERVER DIAGNOSTICS END)
# Build:
commit: nocommit
version: dev
@@ -23,7 +23,7 @@ lastbrowseruseragent: ""
countofactions: 7
loglevel: INFO
-### SOSREPORT END (copy all text from SOSREPORT START)
+### SERVER DIAGNOSTICS END (copy all text from SERVER DIAGNOSTICS START)
----
The markers and field names above reflect what OliveTin prints; copy everything from the start marker through the end marker.
@@ -35,9 +35,9 @@ You can then copy and paste this text into a GitHub issue, discussion, Discord c
OliveTin needs to be able to start and its API needs to be functional. Once OliveTin is started, open this URL in a browser (replace the host and port with yours):
-`http://myserver:1337/api/sosreport`
+`http://myserver:1337/api/ServerDiagnostics`
-The `/api/sosreport` path is the HTTP endpoint OliveTin uses for server diagnostics.
+The `/api/ServerDiagnostics` path is the HTTP endpoint OliveTin uses for server diagnostics.
== Optional: Allow insecure (but easy) dumping to the browser
@@ -46,7 +46,7 @@ There is a configuration option you can set in your `config.yaml` that allows yo
[source,yaml]
.`config.yaml`
----
-InsecureAllowDumpSos: true
+insecureAllowDumpServerDiagnostics: true
----
== Default: Diagnostics dump to logs
@@ -54,7 +54,7 @@ InsecureAllowDumpSos: true
You should get a simple JSON message saying something like;
----
-alert: "Your SOS Report has been logged to OliveTin logs."
+alert: "Your Server Diagnostics have been logged to OliveTin logs."
----
The exact wording may match your OliveTin version.
@@ -66,4 +66,3 @@ To find the diagnostics text depends on how you are running OliveTin. If you are
== What if I cannot even get to the OliveTin API?
OliveTin's web interface doesn't need to be working to get server diagnostics (the web interface and the API are separate). However, if OliveTin won't even start, or you cannot reach the API, then server diagnostics cannot be generated. Please specify that when you ask for help.
-
diff --git a/frontend/package.json b/frontend/package.json
index 6865bb0..bd21edf 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,7 +10,7 @@
"stylelint-config-standard": "^40.0.0"
},
"scripts": {
- "test": "node --test resources/vue/components/*.test.mjs"
+ "test": "node --test resources/vue/components/*.test.mjs resources/vue/utils/*.test.mjs"
},
"author": "",
"parcelIgnore": [
diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts
index 4f0085f..6450b63 100644
--- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts
+++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts
@@ -1219,21 +1219,21 @@ export declare type WhoAmIResponse = Message<"olivetin.api.v1.WhoAmIResponse"> &
export declare const WhoAmIResponseSchema: GenMessage;
/**
- * @generated from message olivetin.api.v1.SosReportRequest
+ * @generated from message olivetin.api.v1.ServerDiagnosticsRequest
*/
-export declare type SosReportRequest = Message<"olivetin.api.v1.SosReportRequest"> & {
+export declare type ServerDiagnosticsRequest = Message<"olivetin.api.v1.ServerDiagnosticsRequest"> & {
};
/**
- * Describes the message olivetin.api.v1.SosReportRequest.
- * Use `create(SosReportRequestSchema)` to create a new message.
+ * Describes the message olivetin.api.v1.ServerDiagnosticsRequest.
+ * Use `create(ServerDiagnosticsRequestSchema)` to create a new message.
*/
-export declare const SosReportRequestSchema: GenMessage;
+export declare const ServerDiagnosticsRequestSchema: GenMessage;
/**
- * @generated from message olivetin.api.v1.SosReportResponse
+ * @generated from message olivetin.api.v1.ServerDiagnosticsResponse
*/
-export declare type SosReportResponse = Message<"olivetin.api.v1.SosReportResponse"> & {
+export declare type ServerDiagnosticsResponse = Message<"olivetin.api.v1.ServerDiagnosticsResponse"> & {
/**
* @generated from field: string alert = 1;
*/
@@ -1241,10 +1241,10 @@ export declare type SosReportResponse = Message<"olivetin.api.v1.SosReportRespon
};
/**
- * Describes the message olivetin.api.v1.SosReportResponse.
- * Use `create(SosReportResponseSchema)` to create a new message.
+ * Describes the message olivetin.api.v1.ServerDiagnosticsResponse.
+ * Use `create(ServerDiagnosticsResponseSchema)` to create a new message.
*/
-export declare const SosReportResponseSchema: GenMessage;
+export declare const ServerDiagnosticsResponseSchema: GenMessage;
/**
* @generated from message olivetin.api.v1.DumpVarsRequest
@@ -2176,12 +2176,12 @@ export declare const OliveTinApiService: GenService<{
output: typeof WhoAmIResponseSchema;
},
/**
- * @generated from rpc olivetin.api.v1.OliveTinApiService.SosReport
+ * @generated from rpc olivetin.api.v1.OliveTinApiService.ServerDiagnostics
*/
- sosReport: {
+ serverDiagnostics: {
methodKind: "unary";
- input: typeof SosReportRequestSchema;
- output: typeof SosReportResponseSchema;
+ input: typeof ServerDiagnosticsRequestSchema;
+ output: typeof ServerDiagnosticsResponseSchema;
},
/**
* @generated from rpc olivetin.api.v1.OliveTinApiService.DumpVars
@@ -2280,4 +2280,3 @@ export declare const OliveTinApiService: GenService<{
output: typeof EntitySchema;
},
}>;
-
diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js
index 0f5d513..825efe7 100644
--- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js
+++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js
@@ -8,7 +8,7 @@ import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2
* Describes the file olivetin/api/v1/olivetin.proto.
*/
export const file_olivetin_api_v1_olivetin = /*@__PURE__*/
- fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLGBAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBQgASgJEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcEoECBAQESJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAki1AEKE0VudGl0eVJlbGF0ZWRBY3Rpb24SJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhJZChNwcmVmaWxsZWRfYXJndW1lbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkVudGl0eVJlbGF0ZWRBY3Rpb24uUHJlZmlsbGVkQXJndW1lbnRzRW50cnkaOQoXUHJlZmlsbGVkQXJndW1lbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASL/AQoGRW50aXR5Eg0KBXRpdGxlGAEgASgJEhIKCnVuaXF1ZV9rZXkYAiABKAkSDAoEdHlwZRgDIAEoCRITCgtkaXJlY3RvcmllcxgEIAMoCRIzCgZmaWVsZHMYBSADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5LkZpZWxkc0VudHJ5Ej0KD3JlbGF0ZWRfYWN0aW9ucxgGIAMoCzIkLm9saXZldGluLmFwaS52MS5FbnRpdHlSZWxhdGVkQWN0aW9uEgwKBGljb24YByABKAkaLQoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJUChRHZXREYXNoYm9hcmRSZXNwb25zZRINCgV0aXRsZRgBIAEoCRItCglkYXNoYm9hcmQYBCABKAsyGi5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkIl8KD0VmZmVjdGl2ZVBvbGljeRIYChBzaG93X2RpYWdub3N0aWNzGAEgASgIEhUKDXNob3dfbG9nX2xpc3QYAiABKAgSGwoTc2hvd192ZXJzaW9uX251bWJlchgDIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSKUAQoSU3RhcnRBY3Rpb25SZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSGgoSdW5pcXVlX3RyYWNraW5nX2lkGAMgASgJEhUKDWp1c3RpZmljYXRpb24YBCABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIn4KGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50EhUKDWp1c3RpZmljYXRpb24YAyABKAkiSgoaU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IiwKF1N0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSI5ChhTdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIjMKHlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiTwofU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiXgoOR2V0TG9nc1JlcXVlc3QSFAoMc3RhcnRfb2Zmc2V0GAEgASgDEhMKC2RhdGVfZmlsdGVyGAIgASgJEhEKCXBhZ2Vfc2l6ZRgDIAEoAxIOCgZmaWx0ZXIYBCABKAkilAQKCExvZ0VudHJ5EhgKEGRhdGV0aW1lX3N0YXJ0ZWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEg4KBm91dHB1dBgDIAEoCRIRCgl0aW1lZF9vdXQYBSABKAgSEQoJZXhpdF9jb2RlGAYgASgFEgwKBHVzZXIYByABKAkSEgoKdXNlcl9jbGFzcxgIIAEoCRITCgthY3Rpb25faWNvbhgJIAEoCRIMCgR0YWdzGAogAygJEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgLIAEoCRIZChFkYXRldGltZV9maW5pc2hlZBgMIAEoCRIZChFleGVjdXRpb25fc3RhcnRlZBgOIAEoCBIaChJleGVjdXRpb25fZmluaXNoZWQYDyABKAgSDwoHYmxvY2tlZBgQIAEoCBIWCg5kYXRldGltZV9pbmRleBgRIAEoAxIQCghjYW5fa2lsbBgSIAEoCBIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYEyABKAkSEgoKYmluZGluZ19pZBgUIAEoCRIOCgZxdWV1ZWQYFSABKAgSGAoQcXVldWVkX2Zvcl9ncm91cBgWIAEoCRIVCg1qdXN0aWZpY2F0aW9uGBcgASgJEjcKCWFyZ3VtZW50cxgYIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IpEBCg9HZXRMb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyI/ChRHZXRBY3Rpb25Mb2dzUmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSFAoMc3RhcnRfb2Zmc2V0GAIgASgDIpcBChVHZXRBY3Rpb25Mb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyIaChhHZXRFeGVjdXRpb25RdWV1ZVJlcXVlc3QixgEKFEV4ZWN1dGlvblF1ZXVlQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEhMKC2FjdGlvbl9pY29uGAMgASgJEhYKDm1heF9jb25jdXJyZW50GAQgASgFEhQKDGFjdGl2ZV9jb3VudBgFIAEoBRIVCg1lbnRpdHlfcHJlZml4GAYgASgJEioKB2VudHJpZXMYByADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiwQEKE0V4ZWN1dGlvblF1ZXVlR3JvdXASDAoEbmFtZRgBIAEoCRIMCgRpY29uGAIgASgJEhYKDm1heF9jb25jdXJyZW50GAMgASgFEhQKDGFjdGl2ZV9jb3VudBgEIAEoBRI2CgdhY3Rpb25zGAUgAygLMiUub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlQWN0aW9uEhQKDHF1ZXVlZF9jb3VudBgGIAEoBRISCgpxdWV1ZV9zaXplGAcgASgFImcKGUdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2USNAoGZ3JvdXBzGAEgAygLMiQub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlR3JvdXASFAoMdG90YWxfYWN0aXZlGAIgASgFImUKG1ZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBINCgV2YWx1ZRgBIAEoCRIMCgR0eXBlGAIgASgJEhIKCmJpbmRpbmdfaWQYAyABKAkSFQoNYXJndW1lbnRfbmFtZRgEIAEoCSJCChxWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlEg0KBXZhbGlkGAEgASgIEhMKC2Rlc2NyaXB0aW9uGAIgASgJIjYKFVdhdGNoRXhlY3V0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkiJgoUV2F0Y2hFeGVjdXRpb25VcGRhdGUSDgoGdXBkYXRlGAEgASgJIkoKFkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEhEKCWFjdGlvbl9pZBgCIAEoCSJhChlEYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkSDAoEcGF0aBgEIAEoCSKPAQoXRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Ig8KDVdob0FtSVJlcXVlc3QibAoOV2hvQW1JUmVzcG9uc2USGgoSYXV0aGVudGljYXRlZF91c2VyGAEgASgJEhEKCXVzZXJncm91cBgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIMCgRhY2xzGAQgAygJEgsKA3NpZBgFIAEoCSISChBTb3NSZXBvcnRSZXF1ZXN0IiIKEVNvc1JlcG9ydFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJIhEKD0R1bXBWYXJzUmVxdWVzdCKVAQoQRHVtcFZhcnNSZXNwb25zZRINCgVhbGVydBgBIAEoCRJBCghjb250ZW50cxgCIAMoCzIvLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1Jlc3BvbnNlLkNvbnRlbnRzRW50cnkaLwoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIjsKDERlYnVnQmluZGluZxIUCgxhY3Rpb25fdGl0bGUYASABKAkSFQoNZW50aXR5X3ByZWZpeBgCIAEoCSIeChxEdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Is4BCh1EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZRINCgVhbGVydBgBIAEoCRJOCghjb250ZW50cxgCIAMoCzI8Lm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZS5Db250ZW50c0VudHJ5Gk4KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEiwKBXZhbHVlGAIgASgLMh0ub2xpdmV0aW4uYXBpLnYxLkRlYnVnQmluZGluZzoCOAEiEgoQR2V0UmVhZHl6UmVxdWVzdCIjChFHZXRSZWFkeXpSZXNwb25zZRIOCgZzdGF0dXMYASABKAkiFAoSRXZlbnRTdHJlYW1SZXF1ZXN0IpkDChNFdmVudFN0cmVhbVJlc3BvbnNlEj0KDmVudGl0eV9jaGFuZ2VkGAIgASgLMiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50RW50aXR5Q2hhbmdlZEgAEj0KDmNvbmZpZ19jaGFuZ2VkGAMgASgLMiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50Q29uZmlnQ2hhbmdlZEgAEkUKEmV4ZWN1dGlvbl9maW5pc2hlZBgEIAEoCzInLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvbkZpbmlzaGVkSAASQwoRZXhlY3V0aW9uX3N0YXJ0ZWQYBSABKAsyJi5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25TdGFydGVkSAASOQoMb3V0cHV0X2NodW5rGAYgASgLMiEub2xpdmV0aW4uYXBpLnYxLkV2ZW50T3V0cHV0Q2h1bmtIABI0CgloZWFydGJlYXQYByABKAsyHy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRIZWFydGJlYXRIAEIHCgVldmVudCJBChBFdmVudE91dHB1dENodW5rEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZvdXRwdXQYAiABKAkiFAoSRXZlbnRFbnRpdHlDaGFuZ2VkIhQKEkV2ZW50Q29uZmlnQ2hhbmdlZCIQCg5FdmVudEhlYXJ0YmVhdCJGChZFdmVudEV4ZWN1dGlvbkZpbmlzaGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJFChVFdmVudEV4ZWN1dGlvblN0YXJ0ZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IjIKEUtpbGxBY3Rpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSJtChJLaWxsQWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBmtpbGxlZBgCIAEoCBIZChFhbHJlYWR5X2NvbXBsZXRlZBgDIAEoCBINCgVmb3VuZBgEIAEoCCI7ChVMb2NhbFVzZXJMb2dpblJlcXVlc3QSEAoIdXNlcm5hbWUYASABKAkSEAoIcGFzc3dvcmQYAiABKAkiKQoWTG9jYWxVc2VyTG9naW5SZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIicKE1Bhc3N3b3JkSGFzaFJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkiJAoUUGFzc3dvcmRIYXNoUmVzcG9uc2USDAoEaGFzaBgBIAEoCSIPCg1Mb2dvdXRSZXF1ZXN0IhAKDkxvZ291dFJlc3BvbnNlIhcKFUdldERpYWdub3N0aWNzUmVxdWVzdCJFChZHZXREaWFnbm9zdGljc1Jlc3BvbnNlEhMKC1NzaEZvdW5kS2V5GAEgASgJEhYKDlNzaEZvdW5kQ29uZmlnGAIgASgJIg0KC0luaXRSZXF1ZXN0IusFCgxJbml0UmVzcG9uc2USEgoKc2hvd0Zvb3RlchgBIAEoCBIWCg5zaG93TmF2aWdhdGlvbhgCIAEoCBIXCg9zaG93TmV3VmVyc2lvbnMYAyABKAgSGAoQYXZhaWxhYmxlVmVyc2lvbhgEIAEoCRIWCg5jdXJyZW50VmVyc2lvbhgFIAEoCRIRCglwYWdlVGl0bGUYBiABKAkSHgoWc2VjdGlvbk5hdmlnYXRpb25TdHlsZRgHIAEoCRIaChJkZWZhdWx0SWNvbkZvckJhY2sYCCABKAkSFgoOZW5hYmxlQ3VzdG9tSnMYCSABKAgSFAoMYXV0aExvZ2luVXJsGAogASgJEhYKDmF1dGhMb2NhbExvZ2luGAsgASgIEhEKCXN0eWxlTW9kcxgMIAMoCRI4Cg9vQXV0aDJQcm92aWRlcnMYDSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuT0F1dGgyUHJvdmlkZXISOAoPYWRkaXRpb25hbExpbmtzGA4gAygLMh8ub2xpdmV0aW4uYXBpLnYxLkFkZGl0aW9uYWxMaW5rEhYKDnJvb3REYXNoYm9hcmRzGA8gAygJEhoKEmF1dGhlbnRpY2F0ZWRfdXNlchgQIAEoCRIjChthdXRoZW50aWNhdGVkX3VzZXJfcHJvdmlkZXIYESABKAkSOgoQZWZmZWN0aXZlX3BvbGljeRgSIAEoCzIgLm9saXZldGluLmFwaS52MS5FZmZlY3RpdmVQb2xpY3kSFgoOYmFubmVyX21lc3NhZ2UYEyABKAkSEgoKYmFubmVyX2NzcxgUIAEoCRIYChBzaG93X2RpYWdub3N0aWNzGBUgASgIEhUKDXNob3dfbG9nX2xpc3QYFiABKAgSFgoObG9naW5fcmVxdWlyZWQYFyABKAgSGAoQYXZhaWxhYmxlX3RoZW1lcxgYIAMoCRIkChxzaG93X25hdmlnYXRlX29uX3N0YXJ0X2ljb25zGBkgASgIIiwKDkFkZGl0aW9uYWxMaW5rEg0KBXRpdGxlGAEgASgJEgsKA3VybBgCIAEoCSI6Cg5PQXV0aDJQcm92aWRlchINCgV0aXRsZRgBIAEoCRIMCgRpY29uGAMgASgJEgsKA2tleRgEIAEoCSItChdHZXRBY3Rpb25CaW5kaW5nUmVxdWVzdBISCgpiaW5kaW5nX2lkGAEgASgJIosBChhHZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2USJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhJGChJiYWNrX3RvX2Rhc2hib2FyZHMYAiADKAsyKi5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkTmF2aWdhdGlvblRhcmdldCJaChJHZXRFbnRpdGllc1JlcXVlc3QSEwoLZW50aXR5X3R5cGUYASABKAkSDgoGZmlsdGVyGAIgASgJEgwKBHBhZ2UYAyABKAUSEQoJcGFnZV9zaXplGAQgASgFIlQKE0dldEVudGl0aWVzUmVzcG9uc2USPQoSZW50aXR5X2RlZmluaXRpb25zGAEgAygLMiEub2xpdmV0aW4uYXBpLnYxLkVudGl0eURlZmluaXRpb24ixQEKEEVudGl0eURlZmluaXRpb24SDQoFdGl0bGUYASABKAkSKgoJaW5zdGFuY2VzGAIgAygLMhcub2xpdmV0aW4uYXBpLnYxLkVudGl0eRIaChJ1c2VkX29uX2Rhc2hib2FyZHMYAyADKAkSDAoEaWNvbhgEIAEoCRIzCgpwcm9wZXJ0aWVzGAUgAygLMh8ub2xpdmV0aW4uYXBpLnYxLkVudGl0eVByb3BlcnR5EhcKD3RvdGFsX2luc3RhbmNlcxgGIAEoBSItCg5FbnRpdHlQcm9wZXJ0eRIMCgRuYW1lGAEgASgJEg0KBXRpdGxlGAIgASgJIjQKEEdldEVudGl0eVJlcXVlc3QSEgoKdW5pcXVlX2tleRgBIAEoCRIMCgR0eXBlGAIgASgJIjUKFFJlc3RhcnRBY3Rpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCTLWEwoST2xpdmVUaW5BcGlTZXJ2aWNlEl0KDEdldERhc2hib2FyZBIkLm9saXZldGluLmFwaS52MS5HZXREYXNoYm9hcmRSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlc3BvbnNlIgASWgoLU3RhcnRBY3Rpb24SIy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJvChJTdGFydEFjdGlvbkFuZFdhaXQSKi5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBorLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZSIAEmkKEFN0YXJ0QWN0aW9uQnlHZXQSKC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlIgASfgoXU3RhcnRBY3Rpb25CeUdldEFuZFdhaXQSLy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0GjAub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2UiABJeCg1SZXN0YXJ0QWN0aW9uEiUub2xpdmV0aW4uYXBpLnYxLlJlc3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJXCgpLaWxsQWN0aW9uEiIub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXF1ZXN0GiMub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXNwb25zZSIAEmYKD0V4ZWN1dGlvblN0YXR1cxInLm9saXZldGluLmFwaS52MS5FeGVjdXRpb25TdGF0dXNSZXF1ZXN0Gigub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1Jlc3BvbnNlIgASTgoHR2V0TG9ncxIfLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVxdWVzdBogLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVzcG9uc2UiABJgCg1HZXRBY3Rpb25Mb2dzEiUub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXF1ZXN0GiYub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXNwb25zZSIAEmwKEUdldEV4ZWN1dGlvblF1ZXVlEikub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVxdWVzdBoqLm9saXZldGluLmFwaS52MS5HZXRFeGVjdXRpb25RdWV1ZVJlc3BvbnNlIgASdQoUVmFsaWRhdGVBcmd1bWVudFR5cGUSLC5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Gi0ub2xpdmV0aW4uYXBpLnYxLlZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2UiABJLCgZXaG9BbUkSHi5vbGl2ZXRpbi5hcGkudjEuV2hvQW1JUmVxdWVzdBofLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXNwb25zZSIAElQKCVNvc1JlcG9ydBIhLm9saXZldGluLmFwaS52MS5Tb3NSZXBvcnRSZXF1ZXN0GiIub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlc3BvbnNlIgASUQoIRHVtcFZhcnMSIC5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXF1ZXN0GiEub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UiABJ4ChVEdW1wUHVibGljSWRBY3Rpb25NYXASLS5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdBouLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZSIAElQKCUdldFJlYWR5ehIhLm9saXZldGluLmFwaS52MS5HZXRSZWFkeXpSZXF1ZXN0GiIub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlc3BvbnNlIgASYwoOTG9jYWxVc2VyTG9naW4SJi5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkxvY2FsVXNlckxvZ2luUmVzcG9uc2UiABJdCgxQYXNzd29yZEhhc2gSJC5vbGl2ZXRpbi5hcGkudjEuUGFzc3dvcmRIYXNoUmVxdWVzdBolLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXNwb25zZSIAEksKBkxvZ291dBIeLm9saXZldGluLmFwaS52MS5Mb2dvdXRSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlc3BvbnNlIgASXAoLRXZlbnRTdHJlYW0SIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkV2ZW50U3RyZWFtUmVzcG9uc2UiADABEmMKDkdldERpYWdub3N0aWNzEiYub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVxdWVzdBonLm9saXZldGluLmFwaS52MS5HZXREaWFnbm9zdGljc1Jlc3BvbnNlIgASRQoESW5pdBIcLm9saXZldGluLmFwaS52MS5Jbml0UmVxdWVzdBodLm9saXZldGluLmFwaS52MS5Jbml0UmVzcG9uc2UiABJpChBHZXRBY3Rpb25CaW5kaW5nEigub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkJpbmRpbmdSZXF1ZXN0Gikub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkJpbmRpbmdSZXNwb25zZSIAEloKC0dldEVudGl0aWVzEiMub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5HZXRFbnRpdGllc1Jlc3BvbnNlIgASSQoJR2V0RW50aXR5EiEub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0eVJlcXVlc3QaFy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5IgBCOFo2Z2l0aHViLmNvbS9PbGl2ZVRpbi9PbGl2ZVRpbi9nZW4vb2xpdmV0aW4vYXBpL3YxO2FwaXYxYgZwcm90bzM");
+ fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLGBAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBQgASgJEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcEoECBAQESJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAki1AEKE0VudGl0eVJlbGF0ZWRBY3Rpb24SJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhJZChNwcmVmaWxsZWRfYXJndW1lbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkVudGl0eVJlbGF0ZWRBY3Rpb24uUHJlZmlsbGVkQXJndW1lbnRzRW50cnkaOQoXUHJlZmlsbGVkQXJndW1lbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASL/AQoGRW50aXR5Eg0KBXRpdGxlGAEgASgJEhIKCnVuaXF1ZV9rZXkYAiABKAkSDAoEdHlwZRgDIAEoCRITCgtkaXJlY3RvcmllcxgEIAMoCRIzCgZmaWVsZHMYBSADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5LkZpZWxkc0VudHJ5Ej0KD3JlbGF0ZWRfYWN0aW9ucxgGIAMoCzIkLm9saXZldGluLmFwaS52MS5FbnRpdHlSZWxhdGVkQWN0aW9uEgwKBGljb24YByABKAkaLQoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJUChRHZXREYXNoYm9hcmRSZXNwb25zZRINCgV0aXRsZRgBIAEoCRItCglkYXNoYm9hcmQYBCABKAsyGi5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkIl8KD0VmZmVjdGl2ZVBvbGljeRIYChBzaG93X2RpYWdub3N0aWNzGAEgASgIEhUKDXNob3dfbG9nX2xpc3QYAiABKAgSGwoTc2hvd192ZXJzaW9uX251bWJlchgDIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSKUAQoSU3RhcnRBY3Rpb25SZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSGgoSdW5pcXVlX3RyYWNraW5nX2lkGAMgASgJEhUKDWp1c3RpZmljYXRpb24YBCABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIn4KGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50EhUKDWp1c3RpZmljYXRpb24YAyABKAkiSgoaU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IiwKF1N0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSI5ChhTdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIjMKHlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiTwofU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiXgoOR2V0TG9nc1JlcXVlc3QSFAoMc3RhcnRfb2Zmc2V0GAEgASgDEhMKC2RhdGVfZmlsdGVyGAIgASgJEhEKCXBhZ2Vfc2l6ZRgDIAEoAxIOCgZmaWx0ZXIYBCABKAkilAQKCExvZ0VudHJ5EhgKEGRhdGV0aW1lX3N0YXJ0ZWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEg4KBm91dHB1dBgDIAEoCRIRCgl0aW1lZF9vdXQYBSABKAgSEQoJZXhpdF9jb2RlGAYgASgFEgwKBHVzZXIYByABKAkSEgoKdXNlcl9jbGFzcxgIIAEoCRITCgthY3Rpb25faWNvbhgJIAEoCRIMCgR0YWdzGAogAygJEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgLIAEoCRIZChFkYXRldGltZV9maW5pc2hlZBgMIAEoCRIZChFleGVjdXRpb25fc3RhcnRlZBgOIAEoCBIaChJleGVjdXRpb25fZmluaXNoZWQYDyABKAgSDwoHYmxvY2tlZBgQIAEoCBIWCg5kYXRldGltZV9pbmRleBgRIAEoAxIQCghjYW5fa2lsbBgSIAEoCBIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYEyABKAkSEgoKYmluZGluZ19pZBgUIAEoCRIOCgZxdWV1ZWQYFSABKAgSGAoQcXVldWVkX2Zvcl9ncm91cBgWIAEoCRIVCg1qdXN0aWZpY2F0aW9uGBcgASgJEjcKCWFyZ3VtZW50cxgYIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IpEBCg9HZXRMb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyI/ChRHZXRBY3Rpb25Mb2dzUmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSFAoMc3RhcnRfb2Zmc2V0GAIgASgDIpcBChVHZXRBY3Rpb25Mb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyIaChhHZXRFeGVjdXRpb25RdWV1ZVJlcXVlc3QixgEKFEV4ZWN1dGlvblF1ZXVlQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEhMKC2FjdGlvbl9pY29uGAMgASgJEhYKDm1heF9jb25jdXJyZW50GAQgASgFEhQKDGFjdGl2ZV9jb3VudBgFIAEoBRIVCg1lbnRpdHlfcHJlZml4GAYgASgJEioKB2VudHJpZXMYByADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiwQEKE0V4ZWN1dGlvblF1ZXVlR3JvdXASDAoEbmFtZRgBIAEoCRIMCgRpY29uGAIgASgJEhYKDm1heF9jb25jdXJyZW50GAMgASgFEhQKDGFjdGl2ZV9jb3VudBgEIAEoBRI2CgdhY3Rpb25zGAUgAygLMiUub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlQWN0aW9uEhQKDHF1ZXVlZF9jb3VudBgGIAEoBRISCgpxdWV1ZV9zaXplGAcgASgFImcKGUdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2USNAoGZ3JvdXBzGAEgAygLMiQub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlR3JvdXASFAoMdG90YWxfYWN0aXZlGAIgASgFImUKG1ZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBINCgV2YWx1ZRgBIAEoCRIMCgR0eXBlGAIgASgJEhIKCmJpbmRpbmdfaWQYAyABKAkSFQoNYXJndW1lbnRfbmFtZRgEIAEoCSJCChxWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlEg0KBXZhbGlkGAEgASgIEhMKC2Rlc2NyaXB0aW9uGAIgASgJIjYKFVdhdGNoRXhlY3V0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkiJgoUV2F0Y2hFeGVjdXRpb25VcGRhdGUSDgoGdXBkYXRlGAEgASgJIkoKFkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEhEKCWFjdGlvbl9pZBgCIAEoCSJhChlEYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkSDAoEcGF0aBgEIAEoCSKPAQoXRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Ig8KDVdob0FtSVJlcXVlc3QibAoOV2hvQW1JUmVzcG9uc2USGgoSYXV0aGVudGljYXRlZF91c2VyGAEgASgJEhEKCXVzZXJncm91cBgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIMCgRhY2xzGAQgAygJEgsKA3NpZBgFIAEoCSIaChhTZXJ2ZXJEaWFnbm9zdGljc1JlcXVlc3QiKgoZU2VydmVyRGlhZ25vc3RpY3NSZXNwb25zZRINCgVhbGVydBgBIAEoCSIRCg9EdW1wVmFyc1JlcXVlc3QilQEKEER1bXBWYXJzUmVzcG9uc2USDQoFYWxlcnQYASABKAkSQQoIY29udGVudHMYAiADKAsyLy5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZS5Db250ZW50c0VudHJ5Gi8KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI7CgxEZWJ1Z0JpbmRpbmcSFAoMYWN0aW9uX3RpdGxlGAEgASgJEhUKDWVudGl0eV9wcmVmaXgYAiABKAkiHgocRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdCLOAQodRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2USDQoFYWxlcnQYASABKAkSTgoIY29udGVudHMYAiADKAsyPC5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UuQ29udGVudHNFbnRyeRpOCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRIsCgV2YWx1ZRgCIAEoCzIdLm9saXZldGluLmFwaS52MS5EZWJ1Z0JpbmRpbmc6AjgBIhIKEEdldFJlYWR5elJlcXVlc3QiIwoRR2V0UmVhZHl6UmVzcG9uc2USDgoGc3RhdHVzGAEgASgJIhQKEkV2ZW50U3RyZWFtUmVxdWVzdCKZAwoTRXZlbnRTdHJlYW1SZXNwb25zZRI9Cg5lbnRpdHlfY2hhbmdlZBgCIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudEVudGl0eUNoYW5nZWRIABI9Cg5jb25maWdfY2hhbmdlZBgDIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudENvbmZpZ0NoYW5nZWRIABJFChJleGVjdXRpb25fZmluaXNoZWQYBCABKAsyJy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25GaW5pc2hlZEgAEkMKEWV4ZWN1dGlvbl9zdGFydGVkGAUgASgLMiYub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uU3RhcnRlZEgAEjkKDG91dHB1dF9jaHVuaxgGIAEoCzIhLm9saXZldGluLmFwaS52MS5FdmVudE91dHB1dENodW5rSAASNAoJaGVhcnRiZWF0GAcgASgLMh8ub2xpdmV0aW4uYXBpLnYxLkV2ZW50SGVhcnRiZWF0SABCBwoFZXZlbnQiQQoQRXZlbnRPdXRwdXRDaHVuaxIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGb3V0cHV0GAIgASgJIhQKEkV2ZW50RW50aXR5Q2hhbmdlZCIUChJFdmVudENvbmZpZ0NoYW5nZWQiEAoORXZlbnRIZWFydGJlYXQiRgoWRXZlbnRFeGVjdXRpb25GaW5pc2hlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiRQoVRXZlbnRFeGVjdXRpb25TdGFydGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIyChFLaWxsQWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkibQoSS2lsbEFjdGlvblJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZraWxsZWQYAiABKAgSGQoRYWxyZWFkeV9jb21wbGV0ZWQYAyABKAgSDQoFZm91bmQYBCABKAgiOwoVTG9jYWxVc2VyTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIikKFkxvY2FsVXNlckxvZ2luUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCInChNQYXNzd29yZEhhc2hSZXF1ZXN0EhAKCHBhc3N3b3JkGAEgASgJIiQKFFBhc3N3b3JkSGFzaFJlc3BvbnNlEgwKBGhhc2gYASABKAkiDwoNTG9nb3V0UmVxdWVzdCIQCg5Mb2dvdXRSZXNwb25zZSIXChVHZXREaWFnbm9zdGljc1JlcXVlc3QiRQoWR2V0RGlhZ25vc3RpY3NSZXNwb25zZRITCgtTc2hGb3VuZEtleRgBIAEoCRIWCg5Tc2hGb3VuZENvbmZpZxgCIAEoCSINCgtJbml0UmVxdWVzdCLrBQoMSW5pdFJlc3BvbnNlEhIKCnNob3dGb290ZXIYASABKAgSFgoOc2hvd05hdmlnYXRpb24YAiABKAgSFwoPc2hvd05ld1ZlcnNpb25zGAMgASgIEhgKEGF2YWlsYWJsZVZlcnNpb24YBCABKAkSFgoOY3VycmVudFZlcnNpb24YBSABKAkSEQoJcGFnZVRpdGxlGAYgASgJEh4KFnNlY3Rpb25OYXZpZ2F0aW9uU3R5bGUYByABKAkSGgoSZGVmYXVsdEljb25Gb3JCYWNrGAggASgJEhYKDmVuYWJsZUN1c3RvbUpzGAkgASgIEhQKDGF1dGhMb2dpblVybBgKIAEoCRIWCg5hdXRoTG9jYWxMb2dpbhgLIAEoCBIRCglzdHlsZU1vZHMYDCADKAkSOAoPb0F1dGgyUHJvdmlkZXJzGA0gAygLMh8ub2xpdmV0aW4uYXBpLnYxLk9BdXRoMlByb3ZpZGVyEjgKD2FkZGl0aW9uYWxMaW5rcxgOIAMoCzIfLm9saXZldGluLmFwaS52MS5BZGRpdGlvbmFsTGluaxIWCg5yb290RGFzaGJvYXJkcxgPIAMoCRIaChJhdXRoZW50aWNhdGVkX3VzZXIYECABKAkSIwobYXV0aGVudGljYXRlZF91c2VyX3Byb3ZpZGVyGBEgASgJEjoKEGVmZmVjdGl2ZV9wb2xpY3kYEiABKAsyIC5vbGl2ZXRpbi5hcGkudjEuRWZmZWN0aXZlUG9saWN5EhYKDmJhbm5lcl9tZXNzYWdlGBMgASgJEhIKCmJhbm5lcl9jc3MYFCABKAkSGAoQc2hvd19kaWFnbm9zdGljcxgVIAEoCBIVCg1zaG93X2xvZ19saXN0GBYgASgIEhYKDmxvZ2luX3JlcXVpcmVkGBcgASgIEhgKEGF2YWlsYWJsZV90aGVtZXMYGCADKAkSJAocc2hvd19uYXZpZ2F0ZV9vbl9zdGFydF9pY29ucxgZIAEoCCIsCg5BZGRpdGlvbmFsTGluaxINCgV0aXRsZRgBIAEoCRILCgN1cmwYAiABKAkiOgoOT0F1dGgyUHJvdmlkZXISDQoFdGl0bGUYASABKAkSDAoEaWNvbhgDIAEoCRILCgNrZXkYBCABKAkiLQoXR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCSKLAQoYR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlEicKBmFjdGlvbhgBIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SRgoSYmFja190b19kYXNoYm9hcmRzGAIgAygLMioub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQiWgoSR2V0RW50aXRpZXNSZXF1ZXN0EhMKC2VudGl0eV90eXBlGAEgASgJEg4KBmZpbHRlchgCIAEoCRIMCgRwYWdlGAMgASgFEhEKCXBhZ2Vfc2l6ZRgEIAEoBSJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uIsUBChBFbnRpdHlEZWZpbml0aW9uEg0KBXRpdGxlGAEgASgJEioKCWluc3RhbmNlcxgCIAMoCzIXLm9saXZldGluLmFwaS52MS5FbnRpdHkSGgoSdXNlZF9vbl9kYXNoYm9hcmRzGAMgAygJEgwKBGljb24YBCABKAkSMwoKcHJvcGVydGllcxgFIAMoCzIfLm9saXZldGluLmFwaS52MS5FbnRpdHlQcm9wZXJ0eRIXCg90b3RhbF9pbnN0YW5jZXMYBiABKAUiLQoORW50aXR5UHJvcGVydHkSDAoEbmFtZRgBIAEoCRINCgV0aXRsZRgCIAEoCSI0ChBHZXRFbnRpdHlSZXF1ZXN0EhIKCnVuaXF1ZV9rZXkYASABKAkSDAoEdHlwZRgCIAEoCSI1ChRSZXN0YXJ0QWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAky7hMKEk9saXZlVGluQXBpU2VydmljZRJdCgxHZXREYXNoYm9hcmQSJC5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVxdWVzdBolLm9saXZldGluLmFwaS52MS5HZXREYXNoYm9hcmRSZXNwb25zZSIAEloKC1N0YXJ0QWN0aW9uEiMub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlc3BvbnNlIgASbwoSU3RhcnRBY3Rpb25BbmRXYWl0Eioub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QaKy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2UiABJpChBTdGFydEFjdGlvbkJ5R2V0Eigub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0Gikub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRSZXNwb25zZSIAEn4KF1N0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0Ei8ub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBowLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlc3BvbnNlIgASXgoNUmVzdGFydEFjdGlvbhIlLm9saXZldGluLmFwaS52MS5SZXN0YXJ0QWN0aW9uUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlc3BvbnNlIgASVwoKS2lsbEFjdGlvbhIiLm9saXZldGluLmFwaS52MS5LaWxsQWN0aW9uUmVxdWVzdBojLm9saXZldGluLmFwaS52MS5LaWxsQWN0aW9uUmVzcG9uc2UiABJmCg9FeGVjdXRpb25TdGF0dXMSJy5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBooLm9saXZldGluLmFwaS52MS5FeGVjdXRpb25TdGF0dXNSZXNwb25zZSIAEk4KB0dldExvZ3MSHy5vbGl2ZXRpbi5hcGkudjEuR2V0TG9nc1JlcXVlc3QaIC5vbGl2ZXRpbi5hcGkudjEuR2V0TG9nc1Jlc3BvbnNlIgASYAoNR2V0QWN0aW9uTG9ncxIlLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25Mb2dzUmVxdWVzdBomLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25Mb2dzUmVzcG9uc2UiABJsChFHZXRFeGVjdXRpb25RdWV1ZRIpLm9saXZldGluLmFwaS52MS5HZXRFeGVjdXRpb25RdWV1ZVJlcXVlc3QaKi5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXNwb25zZSIAEnUKFFZhbGlkYXRlQXJndW1lbnRUeXBlEiwub2xpdmV0aW4uYXBpLnYxLlZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBotLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlIgASSwoGV2hvQW1JEh4ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuV2hvQW1JUmVzcG9uc2UiABJsChFTZXJ2ZXJEaWFnbm9zdGljcxIpLm9saXZldGluLmFwaS52MS5TZXJ2ZXJEaWFnbm9zdGljc1JlcXVlc3QaKi5vbGl2ZXRpbi5hcGkudjEuU2VydmVyRGlhZ25vc3RpY3NSZXNwb25zZSIAElEKCER1bXBWYXJzEiAub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVxdWVzdBohLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1Jlc3BvbnNlIgASeAoVRHVtcFB1YmxpY0lkQWN0aW9uTWFwEi0ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QaLi5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UiABJUCglHZXRSZWFkeXoSIS5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVxdWVzdBoiLm9saXZldGluLmFwaS52MS5HZXRSZWFkeXpSZXNwb25zZSIAEmMKDkxvY2FsVXNlckxvZ2luEiYub2xpdmV0aW4uYXBpLnYxLkxvY2FsVXNlckxvZ2luUmVxdWVzdBonLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlc3BvbnNlIgASXQoMUGFzc3dvcmRIYXNoEiQub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuUGFzc3dvcmRIYXNoUmVzcG9uc2UiABJLCgZMb2dvdXQSHi5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVxdWVzdBofLm9saXZldGluLmFwaS52MS5Mb2dvdXRSZXNwb25zZSIAElwKC0V2ZW50U3RyZWFtEiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50U3RyZWFtUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlc3BvbnNlIgAwARJjCg5HZXREaWFnbm9zdGljcxImLm9saXZldGluLmFwaS52MS5HZXREaWFnbm9zdGljc1JlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXNwb25zZSIAEkUKBEluaXQSHC5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlcXVlc3QaHS5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlc3BvbnNlIgASaQoQR2V0QWN0aW9uQmluZGluZxIoLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVxdWVzdBopLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2UiABJaCgtHZXRFbnRpdGllcxIjLm9saXZldGluLmFwaS52MS5HZXRFbnRpdGllc1JlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXNwb25zZSIAEkkKCUdldEVudGl0eRIhLm9saXZldGluLmFwaS52MS5HZXRFbnRpdHlSZXF1ZXN0Ghcub2xpdmV0aW4uYXBpLnYxLkVudGl0eSIAQjhaNmdpdGh1Yi5jb20vT2xpdmVUaW4vT2xpdmVUaW4vZ2VuL29saXZldGluL2FwaS92MTthcGl2MWIGcHJvdG8z");
/**
* Describes the message olivetin.api.v1.Action.
@@ -284,17 +284,17 @@ export const WhoAmIResponseSchema = /*@__PURE__*/
messageDesc(file_olivetin_api_v1_olivetin, 38);
/**
- * Describes the message olivetin.api.v1.SosReportRequest.
- * Use `create(SosReportRequestSchema)` to create a new message.
+ * Describes the message olivetin.api.v1.ServerDiagnosticsRequest.
+ * Use `create(ServerDiagnosticsRequestSchema)` to create a new message.
*/
-export const SosReportRequestSchema = /*@__PURE__*/
+export const ServerDiagnosticsRequestSchema = /*@__PURE__*/
messageDesc(file_olivetin_api_v1_olivetin, 39);
/**
- * Describes the message olivetin.api.v1.SosReportResponse.
- * Use `create(SosReportResponseSchema)` to create a new message.
+ * Describes the message olivetin.api.v1.ServerDiagnosticsResponse.
+ * Use `create(ServerDiagnosticsResponseSchema)` to create a new message.
*/
-export const SosReportResponseSchema = /*@__PURE__*/
+export const ServerDiagnosticsResponseSchema = /*@__PURE__*/
messageDesc(file_olivetin_api_v1_olivetin, 40);
/**
@@ -561,4 +561,3 @@ export const RestartActionRequestSchema = /*@__PURE__*/
*/
export const OliveTinApiService = /*@__PURE__*/
serviceDesc(file_olivetin_api_v1_olivetin, 0);
-
diff --git a/frontend/resources/vue/ActionButton.vue b/frontend/resources/vue/ActionButton.vue
index 649979c..3dd4931 100644
--- a/frontend/resources/vue/ActionButton.vue
+++ b/frontend/resources/vue/ActionButton.vue
@@ -36,7 +36,12 @@