diff --git a/config.yaml b/config.yaml index 54d6a05..b41ab82 100644 --- a/config.yaml +++ b/config.yaml @@ -9,18 +9,10 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337 # Docs: https://docs.olivetin.app/advanced_configuration/logs.html logLevel: "INFO" -# Action groups share a concurrency limit across multiple actions. When the -# limit is reached, additional requests are queued and run in order. -# Docs: https://docs.olivetin.app/action_customization/concurrency.html#action-groups -actionGroups: - backup-jobs: - maxConcurrent: 1 - icon: backup - # Actions are commands that are executed by OliveTin, and normally show up as # buttons on the WebUI. # -# Docs: https://docs.olivetin.app/action_execution/create_your_first.html +# Docs: https://docs.olivetin.app/action_buttons/create_your_first.html actions: # Lots of people use OliveTin to build web interfaces for their electronics # projects. It's best to install OliveTin as a native package (eg, .deb), and @@ -266,6 +258,7 @@ actions: timeout: 300 icon: logs onclick: execution-dialog + groups: [ con2queue10 ] execOnCron: - "@hourly" @@ -310,6 +303,17 @@ entities: - file: entities/containers.json name: container +# Action groups share a concurrency limit across multiple actions. When the +# limit is reached, additional requests are queued and run in order. +# Docs: https://docs.olivetin.app/action_customization/concurrency.html#action-groups +actionGroups: + backup-jobs: + maxConcurrent: 1 + icon: backup + con2queue10: + maxConcurrent: 2 + queueSize: 10 + # Dashboards are a way of taking actions from the default "actions" view, and # organizing them into groups - either into folders, or fieldsets. # diff --git a/docs/modules/ROOT/examples/solutions/human-in-the-control-loop/config.yaml b/docs/modules/ROOT/examples/solutions/human-in-the-control-loop/config.yaml new file mode 100644 index 0000000..66dda25 --- /dev/null +++ b/docs/modules/ROOT/examples/solutions/human-in-the-control-loop/config.yaml @@ -0,0 +1,31 @@ +--- +logLevel: "WARN" +checkForUpdates: false +showFooter: false + +actions: + - title: Pump ON - 5m + id: pump_on_5m + icon: restart + shell: | + echo "Pump started" + sleep 300 + triggers: + - Update Water Level + + - title: Update Water Level + id: update_water_level + shell: echo "Water level 47%" + hidden: true + execOnStartup: true + execOnCron: "*/1 * * * *" + +dashboards: + - title: Human in the Control Loop + contents: + - title: Water tank + type: fieldset + contents: + - type: stdout-most-recent-execution + title: update_water_level + - title: Pump ON - 5m diff --git a/docs/modules/ROOT/images/.gitignore b/docs/modules/ROOT/images/.gitignore new file mode 100644 index 0000000..63e998e --- /dev/null +++ b/docs/modules/ROOT/images/.gitignore @@ -0,0 +1,2 @@ +**/custom-webui +**/__pycache__ diff --git a/docs/modules/ROOT/images/SCREENSHOTS.md b/docs/modules/ROOT/images/SCREENSHOTS.md new file mode 100644 index 0000000..a9ff4ca --- /dev/null +++ b/docs/modules/ROOT/images/SCREENSHOTS.md @@ -0,0 +1,163 @@ +# Documentation screenshots + +Use [repo-helper](https://github.com/jamesread/repo-common) (`repo-helper screenshot`) to keep Antora doc images up to date. Each documented UI feature gets its own folder under `docs/modules/ROOT/images/`. + +Reference implementation: `args/suggestions/`. + +## Folder layout + +Create one folder per doc page (or logical screenshot group): + +``` +docs/modules/ROOT/images/// +├── screenshots.ini # batch capture config for repo-helper +├── config.yaml # minimal OliveTin config for this screenshot only +├── setup_.py # Selenium setup script(s); each defines run(driver) +├── Makefile # start OliveTin, capture, stop +├── .gitignore # runtime artifacts (see below) +└── *.png # output images (committed) +``` + +Wire images in the matching `.adoc` page: + +```asciidoc +image:://my-screenshot.png[] +``` + +Paths are relative to `docs/modules/ROOT/images/`. + +## Port and OliveTin instance + +- Doc screenshots use a **dedicated port** (not 1337) so they do not clash with a dev server. +- All screenshot folders share port **11337**. +- Set the same port in `config.yaml` (`listenAddressSingleHTTPFrontend`) and `screenshots.ini` (`base_url`). +- Start OliveTin from `service/` so the webui is found: + + ```bash + cd service && ./OliveTin -configdir /path/to/screenshot/folder/ + ``` + +The Makefile handles start/wait/capture/stop. + +## screenshots.ini + +Each `[section]` with a `url` is one PNG. Section `name` (or section title) becomes the filename (`name.png`). + +```ini +[DEFAULT] +base_url = http://localhost:11337/ +dir = . +width = 640 +height = 480 +post_script_sleep = 0.5 + +[my-screenshot] +url = . +name = my-screenshot +script = setup_my_screenshot.py +``` + +Notes: + +- `--config` must point at the real `screenshots.ini` in the folder; relative paths (`script`, `dir`) resolve from the INI directory. +- `url = .` loads the dashboard at `base_url`. +- Override `width`, `height`, `script`, etc. per section when needed. + +Capture: + +```bash +cd docs/modules/ROOT/images// +make update-screenshots +# or, if OliveTin is already running on that port: +repo-helper screenshot --config screenshots.ini +``` + +## config.yaml + +Keep configs **minimal**: only actions, dashboards, and settings required for the screenshot. + +- Match YAML examples shown in the doc page. +- Disable noise: `checkForUpdates: false`, `showFooter: false`, `logLevel: "WARN"`. +- Set argument `type` explicitly to avoid startup warnings. +- Omit `icon` on actions unless the screenshot needs a specific glyph. OliveTin 3k applies a default action icon (`defaultIconForActions`, currently the neutral CLI glyph) when `icon` is not set. + +Reuse integration-test patterns where possible (`integration-tests/tests/*/config.yaml`). + +## Setup scripts (Python) + +repo-helper loads each `--script` file and calls `run(driver)`. Scripts run **in isolation** (no imports from sibling modules unless you add `sys.path` yourself); prefer one self-contained file per variant. + +UI setup should mirror integration tests (`integration-tests/lib/elements.js`): + +1. Wait for `body[loaded-dashboard]` before clicking actions. +2. Click action buttons via `[title="Action Title"]` or `.action-button button`. +3. For argument forms, wait for `body[loaded-argument-form]`. +4. Use `#argument-popup`, input ids (`#container`), etc. + +Example skeleton: + +```python +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: d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard") + ) + driver.find_element(By.CSS_SELECTOR, '[title="My Action"]').click() + WebDriverWait(driver, 15).until( + lambda d: d.find_element(By.TAG_NAME, "body").get_attribute("loaded-argument-form") + ) + # optional: frame the form, open menus, inject overlays — see args/suggestions/ + time.sleep(0.2) +``` + +### Headless Chrome limitations + +repo-helper uses headless Chrome only. Native browser UI (e.g. `` dropdowns, date pickers) often **does not appear** in screenshots. When needed, use `driver.execute_script(...)` in the setup script to render a representative overlay after opening the real form. See `args/suggestions/setup_chrome.py` and `setup_firefox.py`. + +## Makefile template + +Each screenshot folder needs only a thin `Makefile` that sets `CONFIGDIR` and includes the shared rules: + +```makefile +CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include ../../screenshots.mk +``` + +Shared targets (defined in `docs/modules/ROOT/images/screenshots.mk`): + +- `make start` — background OliveTin with this folder's `config.yaml` +- `make` or `make update-screenshots` — stop any instance on 11337, start, run `repo-helper screenshot --config screenshots.ini`, stop +- `make stop` — kill whatever is listening on port 11337 + +## .gitignore (per folder) + +``` +custom-webui/ +__pycache__/ +``` + +## Checklist for a new doc screenshot + +1. Create `docs/modules/ROOT/images///` with the files above. +2. Add `config.yaml` that reproduces the doc example in the UI. +3. Write `setup_*.py` to reach the desired UI state; test selectors against the Vue UI. +4. Add sections to `screenshots.ini`; output PNG names match what the `.adoc` will reference. +5. Update the `.adoc` page: `image:://[]`. +6. Run `make update-screenshots` and commit PNGs plus config/scripts. +7. Remove obsolete PNGs from `images/` if paths moved. + +## Prompt template (for agents) + +Use or adapt this when asking to add or refresh doc screenshots: + +> Update the documentation screenshot(s) for ``. +> +> - Put everything in `docs/modules/ROOT/images///`: `screenshots.ini`, `config.yaml`, setup script(s), `Makefile`, `.gitignore`, and output PNGs. +> - Follow `docs/modules/ROOT/images/SCREENSHOTS.md` and copy structure from `docs/modules/ROOT/images/args/suggestions/`. +> - Use a dedicated OliveTin port (not 1337); start from `service/` with `-configdir` pointing at the screenshot folder. +> - Setup scripts should wait for `loaded-dashboard` / `loaded-argument-form` like integration tests; reuse selectors from `integration-tests/lib/elements.js` where applicable. +> - Update `image::` paths in the `.adoc` page to match the new folder. +> - Run `make update-screenshots` and verify the PNGs before finishing. diff --git a/docs/modules/ROOT/images/action_buttons/create_your_first/.gitignore b/docs/modules/ROOT/images/action_buttons/create_your_first/.gitignore new file mode 100644 index 0000000..431dbf1 --- /dev/null +++ b/docs/modules/ROOT/images/action_buttons/create_your_first/.gitignore @@ -0,0 +1,2 @@ +custom-webui/ +__pycache__/ diff --git a/docs/modules/ROOT/images/action_buttons/create_your_first/Makefile b/docs/modules/ROOT/images/action_buttons/create_your_first/Makefile new file mode 100644 index 0000000..720e66a --- /dev/null +++ b/docs/modules/ROOT/images/action_buttons/create_your_first/Makefile @@ -0,0 +1,2 @@ +CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include ../../screenshots.mk diff --git a/docs/modules/ROOT/images/action_buttons/create_your_first/config.yaml b/docs/modules/ROOT/images/action_buttons/create_your_first/config.yaml new file mode 100644 index 0000000..de35b8e --- /dev/null +++ b/docs/modules/ROOT/images/action_buttons/create_your_first/config.yaml @@ -0,0 +1,12 @@ +--- +listenAddressSingleHTTPFrontend: 0.0.0.0:11337 + +logLevel: "WARN" +checkForUpdates: false +showFooter: false + +actions: + - title: Say Hello + shell: echo "Hello World!" + icon: smile + onclick: execution-dialog diff --git a/docs/modules/ROOT/images/action_buttons/create_your_first/hello-world.png b/docs/modules/ROOT/images/action_buttons/create_your_first/hello-world.png new file mode 100644 index 0000000..792d61f Binary files /dev/null and b/docs/modules/ROOT/images/action_buttons/create_your_first/hello-world.png differ diff --git a/docs/modules/ROOT/images/action_buttons/create_your_first/screenshots.ini b/docs/modules/ROOT/images/action_buttons/create_your_first/screenshots.ini new file mode 100644 index 0000000..fc2651a --- /dev/null +++ b/docs/modules/ROOT/images/action_buttons/create_your_first/screenshots.ini @@ -0,0 +1,11 @@ +[DEFAULT] +base_url = http://localhost:11337/ +dir = . +width = 720 +height = 480 +post_script_sleep = 0.5 + +[hello-world] +url = . +name = hello-world +script = setup_hello.py diff --git a/docs/modules/ROOT/images/action_buttons/create_your_first/setup_hello.py b/docs/modules/ROOT/images/action_buttons/create_your_first/setup_hello.py new file mode 100644 index 0000000..0275a1f --- /dev/null +++ b/docs/modules/ROOT/images/action_buttons/create_your_first/setup_hello.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Show the Say Hello action on the default dashboard.""" + +import time + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait + + +def run(driver): + WebDriverWait(driver, 30).until( + lambda d: d.execute_script("return !!window.client") + ) + WebDriverWait(driver, 30).until( + lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard")) + ) + WebDriverWait(driver, 30).until( + lambda d: d.find_element(By.CSS_SELECTOR, '[title="Say Hello"]').is_displayed() + ) + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/action_buttons/layout/.gitignore b/docs/modules/ROOT/images/action_buttons/layout/.gitignore new file mode 100644 index 0000000..431dbf1 --- /dev/null +++ b/docs/modules/ROOT/images/action_buttons/layout/.gitignore @@ -0,0 +1,2 @@ +custom-webui/ +__pycache__/ diff --git a/docs/modules/ROOT/images/action_buttons/layout/Makefile b/docs/modules/ROOT/images/action_buttons/layout/Makefile new file mode 100644 index 0000000..720e66a --- /dev/null +++ b/docs/modules/ROOT/images/action_buttons/layout/Makefile @@ -0,0 +1,2 @@ +CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include ../../screenshots.mk diff --git a/docs/modules/ROOT/images/action_buttons/layout/config.yaml b/docs/modules/ROOT/images/action_buttons/layout/config.yaml new file mode 100644 index 0000000..0b08123 --- /dev/null +++ b/docs/modules/ROOT/images/action_buttons/layout/config.yaml @@ -0,0 +1,37 @@ +--- +listenAddressSingleHTTPFrontend: 0.0.0.0:11337 + +logLevel: "WARN" +checkForUpdates: false +showFooter: false + +actionGroups: + jobs: + maxConcurrent: 1 + queueSize: 5 + +actions: + - title: Restart service + icon: restart + onclick: execution-dialog + shell: echo "Service restarted" + + - title: Long task + shell: sleep 120 + timeout: 300 + groups: [jobs] + + - title: Backup job + shell: sleep 120 + timeout: 300 + groups: [jobs] + +dashboards: + - title: Action button layout + contents: + - title: Examples + type: fieldset + contents: + - title: Restart service + - title: Long task + - title: Backup job diff --git a/docs/modules/ROOT/images/action_buttons/layout/layout.png b/docs/modules/ROOT/images/action_buttons/layout/layout.png new file mode 100644 index 0000000..f6d7ef4 Binary files /dev/null and b/docs/modules/ROOT/images/action_buttons/layout/layout.png differ diff --git a/docs/modules/ROOT/images/action_buttons/layout/screenshots.ini b/docs/modules/ROOT/images/action_buttons/layout/screenshots.ini new file mode 100644 index 0000000..44c7f6b --- /dev/null +++ b/docs/modules/ROOT/images/action_buttons/layout/screenshots.ini @@ -0,0 +1,11 @@ +[DEFAULT] +base_url = http://localhost:11337/ +dir = . +width = 980 +height = 420 +post_script_sleep = 0.5 + +[layout] +url = /dashboards/Action%20button%20layout +name = layout +script = setup_layout.py diff --git a/docs/modules/ROOT/images/action_buttons/layout/setup_layout.py b/docs/modules/ROOT/images/action_buttons/layout/setup_layout.py new file mode 100644 index 0000000..ee3a9b2 --- /dev/null +++ b/docs/modules/ROOT/images/action_buttons/layout/setup_layout.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Show action buttons in idle, running, and queued states.""" + +import time + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait + +_START_ACTION_JS = """ +const done = arguments[arguments.length - 1]; +const title = arguments[0]; + +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(); +} + +window.client.startAction({ + bindingId: bindingIdForTitle(title), + arguments: [], + uniqueTrackingId: uniqueTrackingId(), +}).then(() => done(true)).catch((err) => done(String(err))); +""" + + +def _wait_for_dashboard(driver, timeout=30): + WebDriverWait(driver, timeout).until( + lambda d: d.execute_script("return !!window.client") + ) + WebDriverWait(driver, timeout).until( + lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard")) + ) + + +def _start_action(driver, title): + driver.execute_async_script(_START_ACTION_JS, title) + + +def _wait_for_layout_states(driver, timeout=20): + def ready(d): + try: + restart = d.find_element(By.CSS_SELECTOR, '[title="Restart service"]') + running = d.find_element( + By.CSS_SELECTOR, + '[title="Long task"]' + ).find_element(By.XPATH, './ancestor::div[contains(@class, "action-button")]//span[contains(@class, "execution-indicator-running")]') + queued = d.find_element( + By.CSS_SELECTOR, + '[title="Backup job"]' + ).find_element(By.XPATH, './ancestor::div[contains(@class, "action-button")]//span[contains(@class, "execution-indicator-queued")]') + onclick = d.find_element( + By.CSS_SELECTOR, + '[title="Restart service"] .navigate-on-start', + ) + except Exception: + return False + return all( + element.is_displayed() + for element in (restart, running, queued, onclick) + ) + + WebDriverWait(driver, timeout).until(ready) + + +def run(driver): + _wait_for_dashboard(driver) + + _start_action(driver, "Long task") + time.sleep(0.3) + _start_action(driver, "Backup job") + + _wait_for_layout_states(driver) + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/action_customization/execution-dialog/.gitignore b/docs/modules/ROOT/images/action_customization/execution-dialog/.gitignore new file mode 100644 index 0000000..431dbf1 --- /dev/null +++ b/docs/modules/ROOT/images/action_customization/execution-dialog/.gitignore @@ -0,0 +1,2 @@ +custom-webui/ +__pycache__/ diff --git a/docs/modules/ROOT/images/action_customization/execution-dialog/Makefile b/docs/modules/ROOT/images/action_customization/execution-dialog/Makefile new file mode 100644 index 0000000..720e66a --- /dev/null +++ b/docs/modules/ROOT/images/action_customization/execution-dialog/Makefile @@ -0,0 +1,2 @@ +CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include ../../screenshots.mk diff --git a/docs/modules/ROOT/images/action_customization/execution-dialog/config.yaml b/docs/modules/ROOT/images/action_customization/execution-dialog/config.yaml new file mode 100644 index 0000000..9861a44 --- /dev/null +++ b/docs/modules/ROOT/images/action_customization/execution-dialog/config.yaml @@ -0,0 +1,17 @@ +--- +listenAddressSingleHTTPFrontend: 0.0.0.0:11337 + +logLevel: "WARN" +checkForUpdates: false +showFooter: false + +actions: + - title: Check dmesg logs + icon: logs + onclick: execution-dialog + shell: | + echo "[ 0.000000] Linux version 6.8.7-100.fc38.x86_64 (mock build) #1 SMP PREEMPT_DYNAMIC" + echo "[ 0.123456] Command line: BOOT_IMAGE=/vmlinuz root=UUID=..." + echo "[ 1.234567] systemd[1]: Started OliveTin documentation screenshot service." + echo "[ 1.456789] eth0: renamed from enp0s3" + echo "[ 2.012345] IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready" diff --git a/docs/modules/ROOT/images/action_customization/execution-dialog/executionDialog.png b/docs/modules/ROOT/images/action_customization/execution-dialog/executionDialog.png new file mode 100644 index 0000000..f2a5a41 Binary files /dev/null and b/docs/modules/ROOT/images/action_customization/execution-dialog/executionDialog.png differ diff --git a/docs/modules/ROOT/images/action_customization/execution-dialog/screenshots.ini b/docs/modules/ROOT/images/action_customization/execution-dialog/screenshots.ini new file mode 100644 index 0000000..23c485a --- /dev/null +++ b/docs/modules/ROOT/images/action_customization/execution-dialog/screenshots.ini @@ -0,0 +1,11 @@ +[DEFAULT] +base_url = http://localhost:11337/ +dir = . +width = 900 +height = 620 +post_script_sleep = 0.5 + +[execution-dialog] +url = . +name = executionDialog +script = setup_execution_dialog.py diff --git a/docs/modules/ROOT/images/action_customization/execution-dialog/setup_execution_dialog.py b/docs/modules/ROOT/images/action_customization/execution-dialog/setup_execution_dialog.py new file mode 100644 index 0000000..bef9c99 --- /dev/null +++ b/docs/modules/ROOT/images/action_customization/execution-dialog/setup_execution_dialog.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Open the execution-dialog view for Check dmesg logs.""" + +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 dmesg logs"]').click() + + _wait_for_logs_page(driver) + _wait_for_execution_complete(driver) + + WebDriverWait(driver, 15).until( + lambda d: d.find_element(By.CSS_SELECTOR, "#execution-results-popup .xterm-rows").text.strip() != "" + ) + + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/action_customization/timeout-logs/.gitignore b/docs/modules/ROOT/images/action_customization/timeout-logs/.gitignore new file mode 100644 index 0000000..431dbf1 --- /dev/null +++ b/docs/modules/ROOT/images/action_customization/timeout-logs/.gitignore @@ -0,0 +1,2 @@ +custom-webui/ +__pycache__/ diff --git a/docs/modules/ROOT/images/action_customization/timeout-logs/Makefile b/docs/modules/ROOT/images/action_customization/timeout-logs/Makefile new file mode 100644 index 0000000..720e66a --- /dev/null +++ b/docs/modules/ROOT/images/action_customization/timeout-logs/Makefile @@ -0,0 +1,2 @@ +CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include ../../screenshots.mk diff --git a/docs/modules/ROOT/images/action_customization/timeout-logs/config.yaml b/docs/modules/ROOT/images/action_customization/timeout-logs/config.yaml new file mode 100644 index 0000000..99c6f02 --- /dev/null +++ b/docs/modules/ROOT/images/action_customization/timeout-logs/config.yaml @@ -0,0 +1,11 @@ +--- +listenAddressSingleHTTPFrontend: 0.0.0.0:11337 + +logLevel: "WARN" +checkForUpdates: false +showFooter: false + +actions: + - title: Slow action + icon: clock + shell: sleep 5 diff --git a/docs/modules/ROOT/images/action_customization/timeout-logs/screenshots.ini b/docs/modules/ROOT/images/action_customization/timeout-logs/screenshots.ini new file mode 100644 index 0000000..7334fc7 --- /dev/null +++ b/docs/modules/ROOT/images/action_customization/timeout-logs/screenshots.ini @@ -0,0 +1,11 @@ +[DEFAULT] +base_url = http://localhost:11337/ +dir = . +width = 900 +height = 420 +post_script_sleep = 0.5 + +[timeout-logs] +url = . +name = timeoutLogs +script = setup_timeout_logs.py diff --git a/docs/modules/ROOT/images/action_customization/timeout-logs/setup_timeout_logs.py b/docs/modules/ROOT/images/action_customization/timeout-logs/setup_timeout_logs.py new file mode 100644 index 0000000..93c79a6 --- /dev/null +++ b/docs/modules/ROOT/images/action_customization/timeout-logs/setup_timeout_logs.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Show a timed-out action on the logs page.""" + +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_timed_out_log(driver, timeout=20): + def has_timed_out_row(d): + try: + status = d.find_element(By.CSS_SELECTOR, ".logs-table .status-timeout").text + except Exception: + return False + return "Timed out" in status + + WebDriverWait(driver, timeout).until(has_timed_out_row) + + +def run(driver): + _wait_for_body_attr(driver, "loaded-dashboard") + + driver.find_element(By.CSS_SELECTOR, '[title="Slow action"]').click() + + # Default timeout is 3 seconds; the action sleeps for 5. + time.sleep(5) + + driver.execute_script("window.location.href = '/logs'") + + WebDriverWait(driver, 15).until( + lambda d: d.find_elements(By.CSS_SELECTOR, ".logs-table tbody tr") + ) + _wait_for_timed_out_log(driver) + + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/action_customization/timeout-logs/timeoutLogs.png b/docs/modules/ROOT/images/action_customization/timeout-logs/timeoutLogs.png new file mode 100644 index 0000000..c4c5977 Binary files /dev/null and b/docs/modules/ROOT/images/action_customization/timeout-logs/timeoutLogs.png differ diff --git a/docs/modules/ROOT/images/arg-suggestions-chrome.png b/docs/modules/ROOT/images/arg-suggestions-chrome.png deleted file mode 100644 index adb369b..0000000 Binary files a/docs/modules/ROOT/images/arg-suggestions-chrome.png and /dev/null differ diff --git a/docs/modules/ROOT/images/arg-suggestions-firefox.png b/docs/modules/ROOT/images/arg-suggestions-firefox.png deleted file mode 100644 index f044d47..0000000 Binary files a/docs/modules/ROOT/images/arg-suggestions-firefox.png and /dev/null differ diff --git a/docs/modules/ROOT/images/args/input/.gitignore b/docs/modules/ROOT/images/args/input/.gitignore new file mode 100644 index 0000000..431dbf1 --- /dev/null +++ b/docs/modules/ROOT/images/args/input/.gitignore @@ -0,0 +1,2 @@ +custom-webui/ +__pycache__/ diff --git a/docs/modules/ROOT/images/args/input/Makefile b/docs/modules/ROOT/images/args/input/Makefile new file mode 100644 index 0000000..720e66a --- /dev/null +++ b/docs/modules/ROOT/images/args/input/Makefile @@ -0,0 +1,2 @@ +CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include ../../screenshots.mk diff --git a/docs/modules/ROOT/images/args/input/args1.png b/docs/modules/ROOT/images/args/input/args1.png new file mode 100644 index 0000000..5400651 Binary files /dev/null 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 new file mode 100644 index 0000000..e101daf Binary files /dev/null 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 new file mode 100644 index 0000000..203171a Binary files /dev/null and b/docs/modules/ROOT/images/args/input/args3.png differ diff --git a/docs/modules/ROOT/images/args/input/config.yaml b/docs/modules/ROOT/images/args/input/config.yaml new file mode 100644 index 0000000..dca7e30 --- /dev/null +++ b/docs/modules/ROOT/images/args/input/config.yaml @@ -0,0 +1,16 @@ +--- +listenAddressSingleHTTPFrontend: 0.0.0.0:11337 + +logLevel: "WARN" +checkForUpdates: false +showFooter: false + +actions: + - title: Print a message + shell: echo {{ message }} + arguments: + - name: message + description: The message you want to print out on the shell. + title: Your Message + default: Hello World + type: ascii_sentence diff --git a/docs/modules/ROOT/images/args/input/screenshots.ini b/docs/modules/ROOT/images/args/input/screenshots.ini new file mode 100644 index 0000000..5006aaa --- /dev/null +++ b/docs/modules/ROOT/images/args/input/screenshots.ini @@ -0,0 +1,21 @@ +[DEFAULT] +base_url = http://localhost:11337/ +dir = . +width = 800 +height = 480 +post_script_sleep = 0.5 + +[args1] +url = . +name = args1 +script = setup_args1.py + +[args2] +url = . +name = args2 +script = setup_args2.py + +[args3] +url = . +name = args3 +script = setup_args3.py diff --git a/docs/modules/ROOT/images/args/input/setup_args1.py b/docs/modules/ROOT/images/args/input/setup_args1.py new file mode 100644 index 0000000..89ebaa2 --- /dev/null +++ b/docs/modules/ROOT/images/args/input/setup_args1.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""Prepare the dashboard action-button 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") + + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/args/input/setup_args2.py b/docs/modules/ROOT/images/args/input/setup_args2.py new file mode 100644 index 0000000..bae1017 --- /dev/null +++ b/docs/modules/ROOT/images/args/input/setup_args2.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Prepare the 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_form(driver): + _wait_for_body_attr(driver, "loaded-dashboard") + + action = driver.find_element(By.CSS_SELECTOR, '[title="Print a message"]') + action.click() + + _wait_for_body_attr(driver, "loaded-argument-form") + + +def run(driver): + _open_form(driver) + + driver.execute_script( + """ + const input = document.getElementById('message'); + if (input && !input.value) { + input.value = 'Hello World'; + } + """ + ) + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/args/input/setup_args3.py b/docs/modules/ROOT/images/args/input/setup_args3.py new file mode 100644 index 0000000..9eda93e --- /dev/null +++ b/docs/modules/ROOT/images/args/input/setup_args3.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Prepare the execution-results 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 _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 _start_action_and_open_logs(driver, timeout=15): + WebDriverWait(driver, timeout).until( + lambda d: d.execute_script("return !!window.client") + ) + WebDriverWait(driver, timeout).until( + lambda d: d.find_element(By.CSS_SELECTOR, 'button[name="start"]').is_enabled() + ) + + driver.execute_async_script( + """ + const done = arguments[arguments.length - 1]; + const bindingId = document.body.getAttribute('loaded-argument-form'); + window.client.startAction({ + bindingId: bindingId, + arguments: [{ name: 'message', value: 'Hello World' }], + uniqueTrackingId: 'doc-screenshot-' + Date.now(), + }).then((response) => { + window.location.href = '/logs/' + response.executionTrackingId; + done(true); + }).catch((err) => done('error: ' + err)); + """ + ) + + +def run(driver): + _wait_for_body_attr(driver, "loaded-dashboard") + + action = driver.find_element(By.CSS_SELECTOR, '[title="Print a message"]') + action.click() + + _wait_for_body_attr(driver, "loaded-argument-form") + _start_action_and_open_logs(driver) + + _wait_for_logs_page(driver) + _wait_for_execution_complete(driver) + + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/args/suggestions/.gitignore b/docs/modules/ROOT/images/args/suggestions/.gitignore new file mode 100644 index 0000000..431dbf1 --- /dev/null +++ b/docs/modules/ROOT/images/args/suggestions/.gitignore @@ -0,0 +1,2 @@ +custom-webui/ +__pycache__/ diff --git a/docs/modules/ROOT/images/args/suggestions/Makefile b/docs/modules/ROOT/images/args/suggestions/Makefile new file mode 100644 index 0000000..720e66a --- /dev/null +++ b/docs/modules/ROOT/images/args/suggestions/Makefile @@ -0,0 +1,2 @@ +CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include ../../screenshots.mk diff --git a/docs/modules/ROOT/images/args/suggestions/arg-suggestions-chrome.png b/docs/modules/ROOT/images/args/suggestions/arg-suggestions-chrome.png new file mode 100644 index 0000000..809f221 Binary files /dev/null and b/docs/modules/ROOT/images/args/suggestions/arg-suggestions-chrome.png differ diff --git a/docs/modules/ROOT/images/args/suggestions/arg-suggestions-firefox.png b/docs/modules/ROOT/images/args/suggestions/arg-suggestions-firefox.png new file mode 100644 index 0000000..b0f7573 Binary files /dev/null and b/docs/modules/ROOT/images/args/suggestions/arg-suggestions-firefox.png differ diff --git a/docs/modules/ROOT/images/args/suggestions/config.yaml b/docs/modules/ROOT/images/args/suggestions/config.yaml new file mode 100644 index 0000000..a69961b --- /dev/null +++ b/docs/modules/ROOT/images/args/suggestions/config.yaml @@ -0,0 +1,21 @@ +--- +listenAddressSingleHTTPFrontend: 0.0.0.0:11337 + +logLevel: "WARN" +checkForUpdates: false +showFooter: false + +actions: + - title: Restart Docker Container + icon: restart + shell: "echo 'Restarting container: {{ container }}'" + arguments: + - name: container + title: Container name + type: ascii_identifier + suggestions: + plex: + graefik: + grafana: + wifi-controller: WiFi Controller + firewall-controller: Firewall Controller diff --git a/docs/modules/ROOT/images/args/suggestions/screenshots.ini b/docs/modules/ROOT/images/args/suggestions/screenshots.ini new file mode 100644 index 0000000..8f97efc --- /dev/null +++ b/docs/modules/ROOT/images/args/suggestions/screenshots.ini @@ -0,0 +1,16 @@ +[DEFAULT] +base_url = http://localhost:11337/ +dir = . +width = 800 +height = 480 +post_script_sleep = 0.5 + +[arg-suggestions-chrome] +url = . +name = arg-suggestions-chrome +script = setup_chrome.py + +[arg-suggestions-firefox] +url = . +name = arg-suggestions-firefox +script = setup_firefox.py diff --git a/docs/modules/ROOT/images/args/suggestions/setup_chrome.py b/docs/modules/ROOT/images/args/suggestions/setup_chrome.py new file mode 100644 index 0000000..1dc1be6 --- /dev/null +++ b/docs/modules/ROOT/images/args/suggestions/setup_chrome.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Prepare the Chrome-style suggestions 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_form(driver): + _wait_for_body_attr(driver, "loaded-dashboard") + + action = driver.find_element( + By.CSS_SELECTOR, '[title="Restart Docker Container"]' + ) + action.click() + + _wait_for_body_attr(driver, "loaded-argument-form") + + driver.execute_script( + """ + const form = document.getElementById('argument-popup'); + if (form) { + form.style.margin = '2rem auto'; + form.style.maxWidth = '520px'; + } + """ + ) + + +def run(driver): + _open_form(driver) + + driver.execute_script( + """ + const input = document.getElementById('container'); + input.focus(); + input.value = ''; + + document.getElementById('doc-suggestions-overlay')?.remove(); + + const rect = input.getBoundingClientRect(); + const menu = document.createElement('div'); + menu.id = 'doc-suggestions-overlay'; + menu.style.position = 'fixed'; + menu.style.left = `${rect.left}px`; + menu.style.top = `${rect.bottom + 2}px`; + menu.style.width = `${rect.width}px`; + menu.style.background = '#fff'; + menu.style.border = '1px solid #888'; + menu.style.boxShadow = '0 2px 6px rgba(0, 0, 0, 0.2)'; + menu.style.font = '13px sans-serif'; + menu.style.zIndex = '9999'; + + const items = [ + ['firewall-controller', 'Firewall Controller'], + ['graefik', ''], + ['grafana', ''], + ['plex', ''], + ['wifi-controller', 'WiFi Controller'], + ]; + + for (const [value, label] of items) { + const row = document.createElement('div'); + row.style.padding = '4px 8px'; + row.style.lineHeight = '1.3'; + + const valueEl = document.createElement('div'); + valueEl.textContent = value; + valueEl.style.fontWeight = label ? '600' : '400'; + row.appendChild(valueEl); + + if (label) { + const labelEl = document.createElement('div'); + labelEl.textContent = label; + labelEl.style.color = '#666'; + labelEl.style.fontSize = '12px'; + row.appendChild(labelEl); + } + + menu.appendChild(row); + } + + document.body.appendChild(menu); + """ + ) + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/args/suggestions/setup_firefox.py b/docs/modules/ROOT/images/args/suggestions/setup_firefox.py new file mode 100644 index 0000000..0edaf73 --- /dev/null +++ b/docs/modules/ROOT/images/args/suggestions/setup_firefox.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Prepare the Firefox-style suggestions 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_form(driver): + _wait_for_body_attr(driver, "loaded-dashboard") + + action = driver.find_element( + By.CSS_SELECTOR, '[title="Restart Docker Container"]' + ) + action.click() + + _wait_for_body_attr(driver, "loaded-argument-form") + + driver.execute_script( + """ + const form = document.getElementById('argument-popup'); + if (form) { + form.style.margin = '2rem auto'; + form.style.maxWidth = '520px'; + } + """ + ) + + +def run(driver): + _open_form(driver) + + driver.execute_script( + """ + const input = document.getElementById('container'); + input.focus(); + input.value = ''; + + document.getElementById('doc-suggestions-overlay')?.remove(); + + const rect = input.getBoundingClientRect(); + const menu = document.createElement('div'); + menu.id = 'doc-suggestions-overlay'; + menu.style.position = 'fixed'; + menu.style.left = `${rect.left}px`; + menu.style.top = `${rect.bottom + 2}px`; + menu.style.width = `${rect.width}px`; + menu.style.background = '#fff'; + menu.style.border = '1px solid #ccc'; + menu.style.boxShadow = '0 1px 4px rgba(0, 0, 0, 0.15)'; + menu.style.font = '13px sans-serif'; + menu.style.zIndex = '9999'; + + for (const label of [ + 'Firewall Controller', + 'graefik', + 'grafana', + 'plex', + 'WiFi Controller', + ]) { + const row = document.createElement('div'); + row.textContent = label; + row.style.padding = '4px 8px'; + menu.appendChild(row); + } + + document.body.appendChild(menu); + """ + ) + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/args1.png b/docs/modules/ROOT/images/args1.png deleted file mode 100644 index 035595f..0000000 Binary files a/docs/modules/ROOT/images/args1.png and /dev/null differ diff --git a/docs/modules/ROOT/images/args2.png b/docs/modules/ROOT/images/args2.png deleted file mode 100644 index 0a5b8a4..0000000 Binary files a/docs/modules/ROOT/images/args2.png and /dev/null differ diff --git a/docs/modules/ROOT/images/args3.png b/docs/modules/ROOT/images/args3.png deleted file mode 100644 index 5425026..0000000 Binary files a/docs/modules/ROOT/images/args3.png and /dev/null differ diff --git a/docs/modules/ROOT/images/dashboard.png b/docs/modules/ROOT/images/dashboard.png deleted file mode 100644 index 1fe44d8..0000000 Binary files a/docs/modules/ROOT/images/dashboard.png and /dev/null differ diff --git a/docs/modules/ROOT/images/dashboards/intro/.gitignore b/docs/modules/ROOT/images/dashboards/intro/.gitignore new file mode 100644 index 0000000..431dbf1 --- /dev/null +++ b/docs/modules/ROOT/images/dashboards/intro/.gitignore @@ -0,0 +1,2 @@ +custom-webui/ +__pycache__/ diff --git a/docs/modules/ROOT/images/dashboards/intro/Makefile b/docs/modules/ROOT/images/dashboards/intro/Makefile new file mode 100644 index 0000000..720e66a --- /dev/null +++ b/docs/modules/ROOT/images/dashboards/intro/Makefile @@ -0,0 +1,2 @@ +CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include ../../screenshots.mk diff --git a/docs/modules/ROOT/images/dashboards/intro/config.yaml b/docs/modules/ROOT/images/dashboards/intro/config.yaml new file mode 100644 index 0000000..d2760e2 --- /dev/null +++ b/docs/modules/ROOT/images/dashboards/intro/config.yaml @@ -0,0 +1,53 @@ +--- +listenAddressSingleHTTPFrontend: 0.0.0.0:11337 + +logLevel: "WARN" +checkForUpdates: false +showFooter: false + +actions: + - title: Ping All Servers + icon: ping + shell: echo "ping all..." + + - title: Ping hypervisor1 + icon: ping + shell: echo "ping hypervisor1" + + - title: Ping hypervisor2 + icon: ping + shell: echo "ping hypervisor2" + + - title: '{{ server.name }} Wake on Lan' + shell: echo "wol {{ server.name }}" + entity: server + + - title: '{{ server.name }} Power Off' + shell: echo "poweroff {{ server.name }}" + entity: server + +entities: + - file: servers.yaml + name: server + +dashboards: + - title: My Servers + contents: + - title: All Servers + type: fieldset + contents: + - title: Ping All Servers + - title: Hypervisors + contents: + - title: Ping hypervisor1 + - title: Ping hypervisor2 + - type: fieldset + entity: server + title: 'Server: {{ server.hostname }}' + contents: + - type: display + title: | + Hostname: {{ server.name }} + IP Address: {{ server.ip }} + - title: '{{ server.name }} Wake on Lan' + - title: '{{ server.name }} Power Off' diff --git a/docs/modules/ROOT/images/dashboards/intro/preview.png b/docs/modules/ROOT/images/dashboards/intro/preview.png new file mode 100644 index 0000000..77e9158 Binary files /dev/null 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 new file mode 100644 index 0000000..048b585 --- /dev/null +++ b/docs/modules/ROOT/images/dashboards/intro/screenshots.ini @@ -0,0 +1,11 @@ +[DEFAULT] +base_url = http://localhost:11337/ +dir = . +width = 980 +height = 720 +post_script_sleep = 0.5 + +[preview] +url = /dashboards/My%20Servers +name = preview +script = setup_preview.py diff --git a/docs/modules/ROOT/images/dashboards/intro/servers.yaml b/docs/modules/ROOT/images/dashboards/intro/servers.yaml new file mode 100644 index 0000000..30c306d --- /dev/null +++ b/docs/modules/ROOT/images/dashboards/intro/servers.yaml @@ -0,0 +1,9 @@ +- name: server1 + hostname: server1.example.com + ip: 192.168.0.1 +- name: server2 + hostname: server2.example.com + ip: 192.168.0.2 +- name: server3 + hostname: server3.example.com + ip: 192.168.0.3 diff --git a/docs/modules/ROOT/images/dashboards/intro/setup_preview.py b/docs/modules/ROOT/images/dashboards/intro/setup_preview.py new file mode 100644 index 0000000..d8a9e41 --- /dev/null +++ b/docs/modules/ROOT/images/dashboards/intro/setup_preview.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Open the My Servers dashboard from dashboards/intro.adoc.""" + +import time + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait + + +def _wait_for_dashboard(driver, timeout=30): + WebDriverWait(driver, timeout).until( + lambda d: d.execute_script("return !!window.client") + ) + WebDriverWait(driver, timeout).until( + lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard")) + ) + + +def _wait_for_my_servers_dashboard(driver, timeout=30): + def ready(d): + try: + ping_all = d.find_element(By.CSS_SELECTOR, '[title="Ping All Servers"]') + hypervisors = d.find_element( + By.XPATH, + '//button[contains(@class, "directory-button")]//span[contains(@class, "title") and text()="Hypervisors"]', + ) + server1 = d.find_element(By.CSS_SELECTOR, '[title="server1 Wake on Lan"]') + server3 = d.find_element(By.CSS_SELECTOR, '[title="server3 Power Off"]') + except Exception: + return False + return all( + element.is_displayed() + for element in (ping_all, hypervisors, server1, server3) + ) + + WebDriverWait(driver, timeout).until(ready) + + +def run(driver): + _wait_for_dashboard(driver) + _wait_for_my_servers_dashboard(driver) + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/executionDialog.png b/docs/modules/ROOT/images/executionDialog.png deleted file mode 100644 index af7dfc3..0000000 Binary files a/docs/modules/ROOT/images/executionDialog.png and /dev/null differ diff --git a/docs/modules/ROOT/images/hello-world.png b/docs/modules/ROOT/images/hello-world.png deleted file mode 100644 index ab8268a..0000000 Binary files a/docs/modules/ROOT/images/hello-world.png and /dev/null differ diff --git a/docs/modules/ROOT/images/logs/views/.gitignore b/docs/modules/ROOT/images/logs/views/.gitignore new file mode 100644 index 0000000..431dbf1 --- /dev/null +++ b/docs/modules/ROOT/images/logs/views/.gitignore @@ -0,0 +1,2 @@ +custom-webui/ +__pycache__/ diff --git a/docs/modules/ROOT/images/logs/views/Makefile b/docs/modules/ROOT/images/logs/views/Makefile new file mode 100644 index 0000000..720e66a --- /dev/null +++ b/docs/modules/ROOT/images/logs/views/Makefile @@ -0,0 +1,2 @@ +CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include ../../screenshots.mk diff --git a/docs/modules/ROOT/images/logs/views/config.yaml b/docs/modules/ROOT/images/logs/views/config.yaml new file mode 100644 index 0000000..2a17b36 --- /dev/null +++ b/docs/modules/ROOT/images/logs/views/config.yaml @@ -0,0 +1,31 @@ +--- +listenAddressSingleHTTPFrontend: 0.0.0.0:11337 + +logLevel: "WARN" +checkForUpdates: false +showFooter: false + +actionGroups: + backup: + maxConcurrent: 1 + queueSize: 5 + +actions: + - title: Check disk space + icon: disk + shell: | + echo "Filesystem Size Used Avail Use% Mounted on" + echo "/dev/sda1 50G 12G 38G 24% /" + + - title: Restart service + icon: restart + shell: echo "Service restarted successfully" + + - title: Slow action + icon: clock + shell: sleep 5 + + - title: Slow backup + icon: backup + shell: sleep 30 + groups: [ backup ] diff --git a/docs/modules/ROOT/images/logs/views/logsCalendar.png b/docs/modules/ROOT/images/logs/views/logsCalendar.png new file mode 100644 index 0000000..df5d12c Binary files /dev/null and b/docs/modules/ROOT/images/logs/views/logsCalendar.png differ diff --git a/docs/modules/ROOT/images/logs/views/logsList.png b/docs/modules/ROOT/images/logs/views/logsList.png new file mode 100644 index 0000000..9b3717b Binary files /dev/null and b/docs/modules/ROOT/images/logs/views/logsList.png differ diff --git a/docs/modules/ROOT/images/logs/views/logsQueue.png b/docs/modules/ROOT/images/logs/views/logsQueue.png new file mode 100644 index 0000000..c0a26a3 Binary files /dev/null and b/docs/modules/ROOT/images/logs/views/logsQueue.png differ diff --git a/docs/modules/ROOT/images/logs/views/screenshots.ini b/docs/modules/ROOT/images/logs/views/screenshots.ini new file mode 100644 index 0000000..2ab0e5c --- /dev/null +++ b/docs/modules/ROOT/images/logs/views/screenshots.ini @@ -0,0 +1,24 @@ +[DEFAULT] +base_url = http://localhost:11337/ +dir = . +width = 900 +height = 480 +post_script_sleep = 0.5 + +[logs-list] +url = . +name = logsList +script = setup_logs_list.py +height = 460 + +[logs-calendar] +url = . +name = logsCalendar +script = setup_logs_calendar.py +height = 640 + +[logs-queue] +url = . +name = logsQueue +script = setup_logs_queue.py +height = 520 diff --git a/docs/modules/ROOT/images/logs/views/setup_logs_calendar.py b/docs/modules/ROOT/images/logs/views/setup_logs_calendar.py new file mode 100644 index 0000000..cdaa598 --- /dev/null +++ b/docs/modules/ROOT/images/logs/views/setup_logs_calendar.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Open the logs calendar view with executions on the current month.""" + +import time + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait + +_START_ACTIONS_JS = """ +const done = arguments[arguments.length - 1]; +const titles = arguments[0]; + +function bindingIdForTitle(title) { + const button = document.querySelector('[title="' + title + '"]'); + if (!button) { + throw new Error('Action button not found: ' + title); + } + 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(); +} + +function startByTitle(title) { + return window.client.startAction({ + bindingId: bindingIdForTitle(title), + arguments: [], + uniqueTrackingId: uniqueTrackingId(), + }); +} + +Promise.all(titles.map(startByTitle)).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 run(driver): + _wait_for_dashboard(driver) + + driver.execute_async_script( + _START_ACTIONS_JS, + ["Check disk space", "Restart service"], + ) + + time.sleep(2) + + driver.execute_script("window.location.href = '/logs/calendar'") + + WebDriverWait(driver, 15).until( + lambda d: len(d.find_elements(By.CSS_SELECTOR, ".calendar-event")) >= 2 + ) + + driver.execute_script( + """ + const today = new Date(); + const key = today.getFullYear() + '-' + + String(today.getMonth() + 1).padStart(2, '0') + '-' + + String(today.getDate()).padStart(2, '0'); + const cell = document.querySelector('[data-calendar-date="' + key + '"]'); + if (cell) { + cell.scrollIntoView({ block: 'center' }); + } + """ + ) + + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/logs/views/setup_logs_list.py b/docs/modules/ROOT/images/logs/views/setup_logs_list.py new file mode 100644 index 0000000..a0fd09b --- /dev/null +++ b/docs/modules/ROOT/images/logs/views/setup_logs_list.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Seed log entries and open the logs list view.""" + +import time + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait + +_START_ACTIONS_JS = """ +const done = arguments[arguments.length - 1]; +const titles = arguments[0]; + +function bindingIdForTitle(title) { + const button = document.querySelector('[title="' + title + '"]'); + if (!button) { + throw new Error('Action button not found: ' + title); + } + 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(); +} + +function startByTitle(title) { + return window.client.startAction({ + bindingId: bindingIdForTitle(title), + arguments: [], + uniqueTrackingId: uniqueTrackingId(), + }); +} + +Promise.all(titles.map(startByTitle)).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_logs_table(driver, timeout=15): + WebDriverWait(driver, timeout).until( + lambda d: len(d.find_elements(By.CSS_SELECTOR, ".logs-table tbody tr")) >= 3 + ) + + +def run(driver): + _wait_for_dashboard(driver) + + driver.execute_async_script( + _START_ACTIONS_JS, + ["Check disk space", "Restart service", "Slow action"], + ) + + # Slow action uses the default 3 second timeout while sleeping for 5. + time.sleep(5) + + driver.execute_script("window.location.href = '/logs'") + _wait_for_logs_table(driver) + + WebDriverWait(driver, 15).until( + lambda d: d.find_element(By.CSS_SELECTOR, ".logs-table .status-timeout").text.strip() != "" + ) + + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/logs/views/setup_logs_queue.py b/docs/modules/ROOT/images/logs/views/setup_logs_queue.py new file mode 100644 index 0000000..34de4b4 --- /dev/null +++ b/docs/modules/ROOT/images/logs/views/setup_logs_queue.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Show queued executions on the logs queue page.""" + +import time + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait + +_QUEUE_ACTIONS_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 queueActions() { + const bindingId = bindingIdForTitle(title); + for (let i = 0; i < count; i++) { + await window.client.startAction({ + bindingId: bindingId, + arguments: [], + uniqueTrackingId: uniqueTrackingId(), + }); + } +} + +queueActions().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 run(driver): + _wait_for_dashboard(driver) + + driver.execute_async_script(_QUEUE_ACTIONS_JS, "Slow backup", 3) + + time.sleep(1) + + driver.execute_script("window.location.href = '/logs/queue'") + + WebDriverWait(driver, 15).until( + lambda d: len(d.find_elements(By.CSS_SELECTOR, ".queue-action-group-section")) >= 1 + ) + WebDriverWait(driver, 15).until( + lambda d: len(d.find_elements(By.CSS_SELECTOR, ".queue-position")) >= 1 + ) + + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/screenshots.mk b/docs/modules/ROOT/images/screenshots.mk new file mode 100644 index 0000000..53945e0 --- /dev/null +++ b/docs/modules/ROOT/images/screenshots.mk @@ -0,0 +1,49 @@ +.PHONY: update-screenshots start stop + +ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../../../..) +OLIVETIN ?= $(ROOT)/service/OliveTin +PORT := 11337 + +ifndef CONFIGDIR +$(error CONFIGDIR must be set before including screenshots.mk) +endif + +.DEFAULT_GOAL := update-screenshots + +start: + @set -e; \ + if curl -sf "http://localhost:$(PORT)/" >/dev/null 2>&1; then \ + echo "Port $(PORT) is already in use; run 'make stop' first"; \ + exit 1; \ + fi; \ + cd "$(ROOT)/service" && "$(OLIVETIN)" -configdir "$(CONFIGDIR)" & \ + pid=$$!; \ + for i in 1 2 3 4 5 6 7 8 9 10; do \ + if curl -sf "http://localhost:$(PORT)/" >/dev/null; then \ + exit 0; \ + fi; \ + if ! kill -0 $$pid 2>/dev/null; then \ + echo "OliveTin exited before listening on port $(PORT)"; \ + exit 1; \ + fi; \ + sleep 1; \ + done; \ + echo "Timed out waiting for OliveTin on port $(PORT)"; \ + exit 1 + +stop: + @set +e; \ + if command -v fuser >/dev/null 2>&1; then \ + fuser -k $(PORT)/tcp 2>/dev/null; \ + else \ + for pid in $$(lsof -t -i :$(PORT) 2>/dev/null); do kill $$pid 2>/dev/null; done; \ + fi; \ + for i in 1 2 3 4 5; do \ + curl -sf "http://localhost:$(PORT)/" >/dev/null || exit 0; \ + sleep 1; \ + done; \ + exit 0 + +update-screenshots: stop start + cd "$(CONFIGDIR)" && repo-helper screenshot --config screenshots.ini + @$(MAKE) stop diff --git a/docs/modules/ROOT/images/solution-k8s-hosted.png b/docs/modules/ROOT/images/solution-k8s-hosted.png deleted file mode 100644 index cbbb92b..0000000 Binary files a/docs/modules/ROOT/images/solution-k8s-hosted.png and /dev/null differ diff --git a/docs/modules/ROOT/images/solution-systemd-control-panel.png b/docs/modules/ROOT/images/solution-systemd-control-panel.png deleted file mode 100644 index 24b7b90..0000000 Binary files a/docs/modules/ROOT/images/solution-systemd-control-panel.png and /dev/null differ diff --git a/docs/modules/ROOT/images/solutions/container-control-panel/.gitignore b/docs/modules/ROOT/images/solutions/container-control-panel/.gitignore new file mode 100644 index 0000000..431dbf1 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/container-control-panel/.gitignore @@ -0,0 +1,2 @@ +custom-webui/ +__pycache__/ diff --git a/docs/modules/ROOT/images/solutions/container-control-panel/Makefile b/docs/modules/ROOT/images/solutions/container-control-panel/Makefile new file mode 100644 index 0000000..720e66a --- /dev/null +++ b/docs/modules/ROOT/images/solutions/container-control-panel/Makefile @@ -0,0 +1,2 @@ +CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include ../../screenshots.mk diff --git a/docs/modules/ROOT/images/solutions/container-control-panel/config.yaml b/docs/modules/ROOT/images/solutions/container-control-panel/config.yaml new file mode 100644 index 0000000..bf83696 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/container-control-panel/config.yaml @@ -0,0 +1,34 @@ +--- +listenAddressSingleHTTPFrontend: 0.0.0.0:11337 + +logLevel: "WARN" +checkForUpdates: false +showFooter: false + +actions: + - title: Start {{ container.Names }} + icon: box + shell: echo "start {{ container.Names }}" + entity: container + + - title: Stop {{ container.Names }} + icon: box + shell: echo "stop {{ container.Names }}" + entity: container + +entities: + - file: containers.json + name: container + +dashboards: + - title: My Containers + contents: + - title: 'Container {{ container.Names }} ({{ container.Image }})' + entity: container + type: fieldset + contents: + - type: display + title: | + {{ container.RunningFor }}

{{ container.State }} + - title: 'Start {{ container.Names }}' + - title: 'Stop {{ container.Names }}' diff --git a/docs/modules/ROOT/images/solutions/container-control-panel/containers.json b/docs/modules/ROOT/images/solutions/container-control-panel/containers.json new file mode 100644 index 0000000..fc6f592 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/container-control-panel/containers.json @@ -0,0 +1,2 @@ +{"Command":"\"/bin/bash\"","CreatedAt":"2024-02-28 22:33:35 +0000 GMT","ID":"fcf468e18a0e","Image":"fedora","Labels":"maintainer=Clement Verna \u003ccverna@fedoraproject.org\u003e","LocalVolumes":"0","Mounts":"","Names":"minecraft","Networks":"bridge","Ports":"","RunningFor":"3 minutes ago","Size":"0B","State":"created","Status":"Created"} +{"Command":"\"/bin/bash\"","CreatedAt":"2024-02-23 23:18:57 +0000 GMT","ID":"442dd6fe316a","Image":"fedora","Labels":"maintainer=Clement Verna \u003ccverna@fedoraproject.org\u003e","LocalVolumes":"0","Mounts":"","Names":"brave_shirley","Networks":"bridge","Ports":"","RunningFor":"4 days ago","Size":"0B","State":"created","Status":"Created"} diff --git a/docs/modules/ROOT/images/solutions/container-control-panel/preview.png b/docs/modules/ROOT/images/solutions/container-control-panel/preview.png index 08c69d8..fc6f767 100644 Binary files a/docs/modules/ROOT/images/solutions/container-control-panel/preview.png and b/docs/modules/ROOT/images/solutions/container-control-panel/preview.png differ diff --git a/docs/modules/ROOT/images/solutions/container-control-panel/screenshots.ini b/docs/modules/ROOT/images/solutions/container-control-panel/screenshots.ini new file mode 100644 index 0000000..9f57458 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/container-control-panel/screenshots.ini @@ -0,0 +1,11 @@ +[DEFAULT] +base_url = http://localhost:11337/ +dir = . +width = 980 +height = 520 +post_script_sleep = 0.5 + +[preview] +url = /dashboards/My%20Containers +name = preview +script = setup_preview.py diff --git a/docs/modules/ROOT/images/solutions/container-control-panel/setup_preview.py b/docs/modules/ROOT/images/solutions/container-control-panel/setup_preview.py new file mode 100644 index 0000000..20fb416 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/container-control-panel/setup_preview.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Open the My Containers dashboard for the container control panel solution.""" + +import time + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait + + +def _wait_for_dashboard(driver, timeout=30): + WebDriverWait(driver, timeout).until( + lambda d: d.execute_script("return !!window.client") + ) + WebDriverWait(driver, timeout).until( + lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard")) + ) + + +def _wait_for_my_containers_dashboard(driver, timeout=30): + def ready(d): + required_titles = [ + "Start minecraft", + "Stop minecraft", + "Start brave_shirley", + "Stop brave_shirley", + ] + 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_dashboard(driver) + _wait_for_my_containers_dashboard(driver) + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/solutions/human-in-the-control-loop/.gitignore b/docs/modules/ROOT/images/solutions/human-in-the-control-loop/.gitignore new file mode 100644 index 0000000..431dbf1 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/human-in-the-control-loop/.gitignore @@ -0,0 +1,2 @@ +custom-webui/ +__pycache__/ diff --git a/docs/modules/ROOT/images/solutions/human-in-the-control-loop/Makefile b/docs/modules/ROOT/images/solutions/human-in-the-control-loop/Makefile new file mode 100644 index 0000000..720e66a --- /dev/null +++ b/docs/modules/ROOT/images/solutions/human-in-the-control-loop/Makefile @@ -0,0 +1,2 @@ +CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include ../../screenshots.mk diff --git a/docs/modules/ROOT/images/solutions/human-in-the-control-loop/config.yaml b/docs/modules/ROOT/images/solutions/human-in-the-control-loop/config.yaml new file mode 100644 index 0000000..b6e0191 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/human-in-the-control-loop/config.yaml @@ -0,0 +1,33 @@ +--- +listenAddressSingleHTTPFrontend: 0.0.0.0:11337 + +logLevel: "WARN" +checkForUpdates: false +showFooter: false + +actions: + - title: Pump ON - 5m + id: pump_on_5m + icon: restart + shell: | + echo "Pump started" + sleep 300 + triggers: + - Update Water Level + + - title: Update Water Level + id: update_water_level + shell: echo "Water level 47%" + hidden: true + execOnStartup: true + execOnCron: "*/1 * * * *" + +dashboards: + - title: Human in the Control Loop + contents: + - title: Water tank + type: fieldset + contents: + - type: stdout-most-recent-execution + title: update_water_level + - title: Pump ON - 5m diff --git a/docs/modules/ROOT/images/solutions/human-in-the-control-loop/preview.png b/docs/modules/ROOT/images/solutions/human-in-the-control-loop/preview.png new file mode 100644 index 0000000..8638739 Binary files /dev/null and b/docs/modules/ROOT/images/solutions/human-in-the-control-loop/preview.png differ diff --git a/docs/modules/ROOT/images/solutions/human-in-the-control-loop/screenshots.ini b/docs/modules/ROOT/images/solutions/human-in-the-control-loop/screenshots.ini new file mode 100644 index 0000000..5a0dd5e --- /dev/null +++ b/docs/modules/ROOT/images/solutions/human-in-the-control-loop/screenshots.ini @@ -0,0 +1,11 @@ +[DEFAULT] +base_url = http://localhost:11337/ +dir = . +width = 900 +height = 420 +post_script_sleep = 0.5 + +[preview] +url = /dashboards/Human%20in%20the%20Control%20Loop +name = preview +script = setup_preview.py diff --git a/docs/modules/ROOT/images/solutions/human-in-the-control-loop/setup_preview.py b/docs/modules/ROOT/images/solutions/human-in-the-control-loop/setup_preview.py new file mode 100644 index 0000000..565339c --- /dev/null +++ b/docs/modules/ROOT/images/solutions/human-in-the-control-loop/setup_preview.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Open the Human in the Control Loop dashboard with water level output.""" + +import time + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait + + +def _wait_for_dashboard(driver, timeout=30): + WebDriverWait(driver, timeout).until( + lambda d: d.execute_script("return !!window.client") + ) + WebDriverWait(driver, timeout).until( + lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard")) + ) + + +def _wait_for_water_level(driver, timeout=30): + def ready(d): + try: + output = d.find_element(By.CSS_SELECTOR, ".mre-output").text + pump = d.find_element(By.CSS_SELECTOR, '[title="Pump ON - 5m"]') + except Exception: + return False + return "Water level 47%" in output and pump.is_displayed() + + WebDriverWait(driver, timeout).until(ready) + + +def run(driver): + _wait_for_dashboard(driver) + _wait_for_water_level(driver) + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/.gitignore b/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/.gitignore new file mode 100644 index 0000000..431dbf1 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/.gitignore @@ -0,0 +1,2 @@ +custom-webui/ +__pycache__/ diff --git a/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/Makefile b/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/Makefile new file mode 100644 index 0000000..720e66a --- /dev/null +++ b/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/Makefile @@ -0,0 +1,2 @@ +CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include ../../screenshots.mk diff --git a/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/config.yaml b/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/config.yaml new file mode 100644 index 0000000..4d2db79 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/config.yaml @@ -0,0 +1,30 @@ +--- +listenAddressSingleHTTPFrontend: 0.0.0.0:11337 + +logLevel: "WARN" +checkForUpdates: false +showFooter: false + +actions: + - title: get pods + icon: + shell: | + echo "NAME READY STATUS RESTARTS AGE" + echo "olivetin-7f8b9c6d4-xk2mp 1/1 Running 0 3d" + echo "postgres-5d4f8b7c9-mn8pq 1/1 Running 0 12d" + echo "nginx-ingress-controller-2h9k 1/1 Running 0 45d" + + - title: restart postgres deployment + icon: + shell: echo "deployment.apps/postgres restarted" + + - title: evacuate node + icon: + shell: echo "node/{{ NodeName }} cordoned and drained" + arguments: + - name: NodeName + type: ascii_identifier + choices: + - value: node1 + - value: node2 + - value: node3 diff --git a/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/preview.png b/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/preview.png new file mode 100644 index 0000000..19d1bca Binary files /dev/null and b/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/preview.png differ diff --git a/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/screenshots.ini b/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/screenshots.ini new file mode 100644 index 0000000..15d777a --- /dev/null +++ b/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/screenshots.ini @@ -0,0 +1,11 @@ +[DEFAULT] +base_url = http://localhost:11337/ +dir = . +width = 980 +height = 380 +post_script_sleep = 0.5 + +[preview] +url = . +name = preview +script = setup_preview.py diff --git a/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/setup_preview.py b/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/setup_preview.py new file mode 100644 index 0000000..82f649b --- /dev/null +++ b/docs/modules/ROOT/images/solutions/k8s-control-panel-hosted/setup_preview.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Show the default Actions dashboard for the Kubernetes control panel.""" + +import time + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait + +ACTION_TITLES = [ + "get pods", + "restart postgres deployment", + "evacuate node", +] + + +def _wait_for_dashboard(driver, timeout=30): + WebDriverWait(driver, timeout).until( + lambda d: d.execute_script("return !!window.client") + ) + WebDriverWait(driver, timeout).until( + lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard")) + ) + + +def _wait_for_actions(driver, timeout=30): + def ready(d): + for title in ACTION_TITLES: + try: + button = d.find_element(By.CSS_SELECTOR, f'[title="{title}"]') + except Exception: + return False + if not button.is_displayed(): + return False + return len(d.find_elements(By.CSS_SELECTOR, ".action-button button")) >= 3 + + WebDriverWait(driver, timeout).until(ready) + + +def run(driver): + _wait_for_dashboard(driver) + _wait_for_actions(driver) + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/solutions/systemd-control-panel/.gitignore b/docs/modules/ROOT/images/solutions/systemd-control-panel/.gitignore new file mode 100644 index 0000000..431dbf1 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/systemd-control-panel/.gitignore @@ -0,0 +1,2 @@ +custom-webui/ +__pycache__/ diff --git a/docs/modules/ROOT/images/solutions/systemd-control-panel/Makefile b/docs/modules/ROOT/images/solutions/systemd-control-panel/Makefile new file mode 100644 index 0000000..720e66a --- /dev/null +++ b/docs/modules/ROOT/images/solutions/systemd-control-panel/Makefile @@ -0,0 +1,2 @@ +CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include ../../screenshots.mk diff --git a/docs/modules/ROOT/images/solutions/systemd-control-panel/config.yaml b/docs/modules/ROOT/images/solutions/systemd-control-panel/config.yaml new file mode 100644 index 0000000..2c6e612 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/systemd-control-panel/config.yaml @@ -0,0 +1,33 @@ +--- +listenAddressSingleHTTPFrontend: 0.0.0.0:11337 + +logLevel: "WARN" +checkForUpdates: false +showFooter: false + +actions: + - title: Stop {{ systemd_unit.unit }} + shell: echo "stop {{ systemd_unit.unit }}" + icon: + entity: systemd_unit + + - title: Start {{ systemd_unit.unit }} + shell: echo "start {{ systemd_unit.unit }}" + icon: + entity: systemd_unit + +entities: + - file: systemd_units.json + name: systemd_unit + +dashboards: + - title: My Services + contents: + - title: '{{ systemd_unit.description }}' + type: fieldset + entity: systemd_unit + contents: + - title: 'Status: {{ systemd_unit.sub }}' + type: display + - title: Start {{ systemd_unit.unit }} + - title: Stop {{ systemd_unit.unit }} diff --git a/docs/modules/ROOT/images/solutions/systemd-control-panel/preview.png b/docs/modules/ROOT/images/solutions/systemd-control-panel/preview.png new file mode 100644 index 0000000..09ee808 Binary files /dev/null and b/docs/modules/ROOT/images/solutions/systemd-control-panel/preview.png differ diff --git a/docs/modules/ROOT/images/solutions/systemd-control-panel/screenshots.ini b/docs/modules/ROOT/images/solutions/systemd-control-panel/screenshots.ini new file mode 100644 index 0000000..f7669a3 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/systemd-control-panel/screenshots.ini @@ -0,0 +1,11 @@ +[DEFAULT] +base_url = http://localhost:11337/ +dir = . +width = 980 +height = 620 +post_script_sleep = 0.5 + +[preview] +url = /dashboards/My%20Services +name = preview +script = setup_preview.py diff --git a/docs/modules/ROOT/images/solutions/systemd-control-panel/setup_preview.py b/docs/modules/ROOT/images/solutions/systemd-control-panel/setup_preview.py new file mode 100644 index 0000000..78af5a9 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/systemd-control-panel/setup_preview.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Open the My Services dashboard for the systemd control panel solution.""" + +import time + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait + + +def _wait_for_dashboard(driver, timeout=30): + WebDriverWait(driver, timeout).until( + lambda d: d.execute_script("return !!window.client") + ) + WebDriverWait(driver, timeout).until( + lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard")) + ) + + +def _wait_for_my_services_dashboard(driver, timeout=30): + def ready(d): + required_titles = [ + "Start boot.mount", + "Stop boot.mount", + "Start podman.service", + "Start upsilon-drone.service", + ] + 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_dashboard(driver) + _wait_for_my_services_dashboard(driver) + time.sleep(0.2) diff --git a/docs/modules/ROOT/images/solutions/systemd-control-panel/systemd_units.json b/docs/modules/ROOT/images/solutions/systemd-control-panel/systemd_units.json new file mode 100644 index 0000000..00a0022 --- /dev/null +++ b/docs/modules/ROOT/images/solutions/systemd-control-panel/systemd_units.json @@ -0,0 +1,4 @@ +{"unit":"boot.mount","load":"loaded","active":"active","sub":"mounted","description":"/boot"} +{"unit":"podman.service","load":"loaded","active":"inactive","sub":"dead","description":"Podman API Service"} +{"unit":"upsilon-drone.service","load":"loaded","active":"active","sub":"running","description":"upsilon-drone"} +{"unit":"podman.socket","load":"loaded","active":"active","sub":"listening","description":"Podman API Socket"} diff --git a/docs/modules/ROOT/images/timeoutLogs.png b/docs/modules/ROOT/images/timeoutLogs.png deleted file mode 100644 index 7a74fbe..0000000 Binary files a/docs/modules/ROOT/images/timeoutLogs.png and /dev/null differ diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 82ec8e1..29896a8 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -34,10 +34,12 @@ ** xref:reverse-proxies/nginx.adoc[Nginx] ** xref:reverse-proxies/nginx_proxy_manager.adoc[Nginx Proxy Manager] ** xref:reverse-proxies/traefik.adoc[Traefik] +* Action Buttons +** xref:action_buttons/layout.adoc[Layout] +** xref:action_buttons/create_your_first.adoc[Create your first action] * Action Execution -** xref:action_execution/create_your_first.adoc[Create your first action] ** xref:action_execution/shellvsexec.adoc[Shell vs Exec] -** xref:action_execution/ondemand.adoc[Execute on demand] +** xref:action_execution/ondemand.adoc[Execute on click] ** xref:action_execution/oncron.adoc[Execute on schedule (cron)] ** xref:action_execution/onstartup.adoc[Execute on startup] ** xref:action_execution/onwebhook.adoc[Execute on webhook] @@ -48,7 +50,6 @@ ** xref:action_execution/aftercompletion.adoc[Execute after completion] ** xref:action_execution/triggers.adoc[Triggers] * xref:action_customization/intro.adoc[Action Customization] -** xref:action_customization/popuponstart.adoc[On click] ** xref:action_customization/icons.adoc[Icons] ** xref:action_customization/timeouts.adoc[Timeouts] ** xref:action_customization/users.adoc[Users] @@ -56,7 +57,6 @@ ** xref:action_customization/ratelimiting.adoc[Rate Limiting] ** xref:action_customization/enabledExpression.adoc[Enabled Expression] ** xref:action_customization/ids.adoc[IDs] -** xref:action_customization/savelogs.adoc[Save Logs] * xref:action_examples/intro.adoc[Action Examples] ** xref:action_examples/containers.adoc[Containers - start/stop] *** xref:action_examples/docker-proxy.adoc[Docker control, via proxy] @@ -91,6 +91,11 @@ ** xref:dashboards/faq-display-hyperlinks.adoc[Hyperlinks (Displays)] ** xref:dashboards/5-output-views.adoc[Output Views] ** xref:dashboards/entity-directories.adoc[Entity Directories] +* xref:logs/intro.adoc[Logs] +** xref:logs/actions.adoc[Action logs] +** xref:logs/calendar.adoc[Calendar view] +** xref:logs/queue.adoc[Queue view] +** xref:logs/saving.adoc[Saving logs] * xref:entities/intro.adoc[Entities] ** xref:entities/examples.adoc[Examples] ** xref:entities/yaml.adoc[YAML Entity Files] @@ -127,6 +132,7 @@ ** xref:solutions/container-control-panel/index.adoc[Container Control Panel] ** xref:solutions/systemd-control-panel/index.adoc[Systemd Control Panel] ** xref:solutions/heating-control-panel/index.adoc[Heating Control Panel] +** xref:solutions/human-in-the-control-loop/index.adoc[Human in the Control Loop] ** xref:solutions/k8s-control-panel-hosted/index.adoc[Kubernetes Control Panel (hosted)] ** xref:solutions/primitive-password/index.adoc[Primitive Password Protection] ** xref:solutions/wol/index.adoc[Wake on LAN] @@ -134,8 +140,6 @@ ** xref:solutions/directory-actions/index.adoc[Directory Actions] * xref:advanced_configuration/intro.adoc[Advanced Configuration] ** xref:advanced_configuration/logs.adoc[Logging - Application] -** xref:advanced_configuration/logs-actions.adoc[Logging - Actions] -** xref:advanced_configuration/logs-calendar.adoc[Logs Calendar View] ** xref:advanced_configuration/diagnostics.adoc[Diagnostics] ** xref:advanced_configuration/config_envs.adoc[Config Envs] ** xref:advanced_configuration/ports.adoc[Ports] diff --git a/docs/modules/ROOT/pages/action_buttons/create_your_first.adoc b/docs/modules/ROOT/pages/action_buttons/create_your_first.adoc new file mode 100644 index 0000000..a2d3677 --- /dev/null +++ b/docs/modules/ROOT/pages/action_buttons/create_your_first.adoc @@ -0,0 +1,94 @@ +[#create-your-first-action] += Create your first action + +This page walks through adding your first action button — the step that turns a fresh OliveTin install into something you can actually click and use. + +When you are done, your dashboard will have a **Say Hello** button like this: + +image::action_buttons/create_your_first/hello-world.png[] + +== Before you start + +Make sure you have: + +* OliveTin **installed and running** — see the xref:install/intro.adoc[installation guide] if you have not done this yet +* Access to your OliveTin **`config.yaml`** file — see xref:config.adoc#config[Configuration] for where OliveTin looks for this file on your platform +* A text editor to change the file (any editor is fine) + +When OliveTin starts successfully, open the web interface in your browser (by default at `http://localhost:1337/`). You should see the OliveTin dashboard, even if it does not have any custom actions yet. + +== Step 1: Open `config.yaml` + +OliveTin is controlled entirely by `config.yaml`. On startup it looks for this file in several places — most commonly: + +* The directory you pass with `--configdir` (often the current working directory when you start OliveTin manually) +* `/config/` inside containers +* `/etc/OliveTin/` on Linux service installs + +If you are not sure which file your instance uses, check how you installed OliveTin (container, package, or manual) and open the `config.yaml` in that location. + +TIP: The xref:config.adoc[Configuration] page lists every search path and explains how live reload works when you save changes. + +== Step 2: Add an action + +Add an entry under `actions`. Each action needs at least a **title** (shown on the button) and a **shell** command to run: + +.`config.yaml` +[source,yaml] +---- +actions: + - title: Say Hello + shell: echo "Hello World!" + icon: smile + onclick: execution-dialog +---- + +* `title` — the label on the action button. It must be unique across all actions. +* `shell` — the command OliveTin runs when you click the button. Here it prints `Hello World!` to the output. +* `icon` — the glyph shown in the centre of the button. See xref:action_customization/icons.adoc[Icons] for other options. +* `onclick: execution-dialog` — opens a dialog with the command output when the action runs, so you can see straight away that it worked. See xref:action_execution/ondemand.adoc[Execute on click] for other options. + +If your `config.yaml` already has other settings or actions, add this block alongside them. Only the `actions:` list is required for this example. + +== Step 3: Save the file + +Save `config.yaml`. OliveTin watches the file and **reloads configuration automatically** when it changes — you do not need to restart the service in most setups. + +Refresh the web page in your browser so the dashboard picks up the new action. + +NOTE: If the button does not appear after saving, check the OliveTin application logs for YAML syntax errors, then refresh again. A missing quote or incorrect indentation in `config.yaml` is the most common cause. + +== Step 4: Find your new button + +After reload, a new **Say Hello** button appears on the dashboard: + +image::action_buttons/create_your_first/hello-world.png[] + +Each action button shows the title at the bottom and the icon in the centre. The small icon in the top-right corner indicates that clicking opens an execution dialog. See xref:action_buttons/layout.adoc[Layout] for a breakdown of every part of the button. + +== Step 5: Run the action + +Click **Say Hello**. OliveTin runs `echo "Hello World!"` and opens the execution dialog with the output, timing, and exit code. + +If the dialog shows `Hello World!` and a successful exit code, your first action is working. + +== Step 6: View the logs + +Every execution is also recorded in the xref:logs/intro.adoc[Logs] section of the web interface. Open **Logs** in the navigation to browse past runs, search for executions, and open full output again later. + +== Important considerations + +* The action **title must be unique**. If two actions share the same title, only one button is shown. +* The `shell` field runs your command through a shell. For more control (especially with arguments), use `exec` instead — see xref:action_execution/shellvsexec.adoc[Shell vs Exec]. + +== What's Next? + +Now that you have a working action, try: + +* xref:action_buttons/layout.adoc[Layout] — understand the parts of an action button +* xref:action_customization/intro.adoc[Customize your actions] — icons, timeouts, and other action properties +* xref:args/intro.adoc[Add arguments to actions] — make actions interactive with user input +* xref:action_examples/intro.adoc[Browse action examples] — real-world examples for common use cases +* xref:action_execution/oncron.adoc[Schedule actions] — run actions automatically on a schedule +* xref:action_execution/onwebhook.adoc[Trigger actions via webhooks] — integrate OliveTin with external systems +* xref:dashboards/intro.adoc[Organize actions with dashboards] — create custom views to organize your actions diff --git a/docs/modules/ROOT/pages/action_buttons/layout.adoc b/docs/modules/ROOT/pages/action_buttons/layout.adoc new file mode 100644 index 0000000..283bb73 --- /dev/null +++ b/docs/modules/ROOT/pages/action_buttons/layout.adoc @@ -0,0 +1,40 @@ +[#action-button-layout] += Layout + +Each action appears in the OliveTin web interface as a button card. The screenshot below shows three buttons in different states so you can see each part of the layout. + +image::action_buttons/layout/layout.png[] + +== Action title + +The **title** comes from the action's `title` field in `config.yaml`. It is shown at the bottom of the button and is also used as the tooltip when you hover over the button. + +== Icon + +The large glyph in the centre of the button is the action **icon**. You set this with the `icon` field on the action. + +If you do not set `icon`, OliveTin uses the default action icon. See xref:action_customization/icons.adoc[Icons] for Unicode, Iconify, and image options. + +== On-click indicator + +The small icon in the **top-right corner** of the button is the **on-click indicator**. It shows what happens when the action starts — for example, opening an execution dialog, an argument form, or action history. + +The indicator is controlled by the action's `onclick` setting (legacy name: `popupOnStart`). See xref:action_execution/ondemand.adoc[Execute on click] for the available options. + +You can hide these indicators globally with `showNavigateOnStartIcons: false` in `config.yaml`. + +== Running and queued indicators + +When an action is already running or waiting in a queue, OliveTin shows a small circle in the **top-left corner** of the button: + +* **Green** — the action has a running execution +* **Blue** — the action has an execution waiting in a queue + +These indicators appear when concurrency or xref:action_customization/concurrency.adoc[action groups] limit how many executions can run at once. They disappear when the execution finishes. + +In the screenshot above, **Long task** is running and **Backup job** is queued. + +== See also + +* xref:action_buttons/create_your_first.adoc[Create your first action] +* xref:action_customization/intro.adoc[Action customization] diff --git a/docs/modules/ROOT/pages/action_customization/concurrency.adoc b/docs/modules/ROOT/pages/action_customization/concurrency.adoc index f829299..ad3649d 100644 --- a/docs/modules/ROOT/pages/action_customization/concurrency.adoc +++ b/docs/modules/ROOT/pages/action_customization/concurrency.adoc @@ -40,6 +40,7 @@ Use `actionGroups` to define a shared limit, and assign actions to a group with actionGroups: unity: maxConcurrent: 1 + queueSize: 5 actions: - title: Unity Android Build @@ -51,8 +52,21 @@ actions: groups: [ unity ] ---- -When the group limit is reached, additional requests are queued automatically and run in order when a slot becomes free. Queued executions appear in the logs with a queued status. +=== maxConcurrent vs queueSize -Per-action `maxConcurrent` still applies separately. If the same action binding is started twice while one is already running, the second request is blocked immediately (not queued). +Action groups define two related limits: + +* `maxConcurrent` -- how many executions in the group may *run at the same time*. When every slot is in use, new requests wait for a free slot instead of running immediately. +* `queueSize` -- how many executions may *wait in the queue* for a slot. If the group is full and the queue already holds this many waiting executions, additional requests are blocked (the same "blocked" status as per-action concurrency). + +For example, with `maxConcurrent: 1` and `queueSize: 5`, OliveTin can have one execution running and up to five more waiting. A seventh request while those six are still outstanding is blocked. + +`queueSize` defaults to `5` when omitted. + +When the group limit is reached but the queue is not full, additional requests are queued automatically and run in order when a slot becomes free. Queued executions appear in the logs with a queued status. + +Per-action `maxConcurrent` still applies to actions that are not in a group. For actions in a group, concurrency is governed by the group `maxConcurrent` and `queueSize` settings instead. A second request for the same action may run concurrently when the group has spare capacity, or join the queue when the group is full. + +Actions that are not in a group are never queued. They only use their own `maxConcurrent` limit (default `1`). The queue is held in memory. If OliveTin restarts while actions are queued, those queued requests are not preserved. diff --git a/docs/modules/ROOT/pages/action_customization/intro.adoc b/docs/modules/ROOT/pages/action_customization/intro.adoc index dac1662..a3ce143 100644 --- a/docs/modules/ROOT/pages/action_customization/intro.adoc +++ b/docs/modules/ROOT/pages/action_customization/intro.adoc @@ -13,7 +13,6 @@ You can customize actions by: * Using enabled expressions to dynamically enable/disable actions based on entity state * Assigning unique IDs for API access * Configuring log saving for audit trails -* Setting popup behaviors when actions start See the links in this section for detailed information on each customization option. @@ -27,6 +26,5 @@ Explore specific customization options: * xref:action_customization/concurrency.adoc[Control concurrency] - Limit simultaneous executions * xref:action_customization/ratelimiting.adoc[Set rate limits] - Prevent actions from running too frequently * xref:action_customization/enabledExpression.adoc[Enabled expressions] - Dynamically enable/disable actions based on entity state -* xref:action_customization/popuponstart.adoc[Configure on-click behavior] - Control what happens when an action is started -* xref:action_customization/savelogs.adoc[Save action logs] - Configure log retention for actions +* xref:logs/saving.adoc[Save action logs] - Configure log retention for actions * xref:action_customization/ids.adoc[Set action IDs] - Assign IDs for API access diff --git a/docs/modules/ROOT/pages/action_customization/popuponstart.adoc b/docs/modules/ROOT/pages/action_customization/popuponstart.adoc deleted file mode 100644 index 69b1807..0000000 --- a/docs/modules/ROOT/pages/action_customization/popuponstart.adoc +++ /dev/null @@ -1,82 +0,0 @@ -[#onclick] -= On Click (Execution Feedback) - -OliveTin has several options to control what happens when an action button is clicked and the execution starts. This can be controlled on a per-action basis using the `onclick` configuration option. - -You can also set the default for OliveTin using the `defaultOnClick` configuration option. - -NOTE: The older names `popupOnStart` and `defaultPopupOnStart` do exactly the same thing. OliveTin copies legacy values into `onclick` / `defaultOnClick` during config load. New configurations should use `onclick` and `defaultOnClick`. - -== Big Flashy Buttons (default) - -[source,yaml] -.`config.yaml` ----- -actions: - - title: Ping the Internet - onclick: default ----- - -This will also be the option that is used if no other values match. - -image::../flashyButton.png[] - -== Execution Dialog (Output Only) - -This can be useful for just displaying the output of a command, without too many additional details like the start time, end time, etc. - -[source,yaml] -.`config.yaml` ----- -actions: - - title: Check disk space - onclick: execution-dialog-stdout-only ----- - -[NOTE] -OliveTin used to separate out the Standard Output (`stdout`) and Standard Error (`stderr`) into two separate output streams. This made no sense, as lines would effectively be separated. This behavior has change to now display `stdout` and `stderr` in the same output stream. However, the configuration option `execution-dialog-stdout-only` was not renamed - and now it includes `stderr` as well. - -image::../popupOutputOnly.png[] - -== Execution Dialog - -The `execution-dialog` option is simialr to the above `execution-dialog-stdout-only`, but it includes the start time, end time, exit code and the duration of time it took for the command to execute. - -[source,yaml] -.`config.yaml` ----- -actions: - - title: Check dmesg logs - onclick: execution-dialog ----- - -.Example of `onclick: execution-dialog` -image::../executionDialog.png[] - -== Execution Buttons - -This mode will create a new button for each individual execution. This can be useful for actions that are executed again and again. - -The text of the button (eg, "0s" in the screenshot below), is the time it took to execute the action in seconds. - -[source,yaml] -.`config.yaml` ----- -actions: - - title: date - onclick: execution-button ----- - -image::../executionButtons.png[] - -== Action execution history - -The `history` option opens the action details page for that binding when the execution starts, so you can see past runs and status for the same action. - -[source,yaml] -.`config.yaml` ----- -actions: - - title: Long-running job - onclick: history ----- diff --git a/docs/modules/ROOT/pages/action_customization/timeouts.adoc b/docs/modules/ROOT/pages/action_customization/timeouts.adoc index 98f14e9..d79a3de 100644 --- a/docs/modules/ROOT/pages/action_customization/timeouts.adoc +++ b/docs/modules/ROOT/pages/action_customization/timeouts.adoc @@ -25,6 +25,4 @@ makes sense, please open an issue and let's discuss. If a action really does "time out", it will show in the logs with "(timed out)" next to the exist code; -image:timeoutLogs.png[] - - +image::action_customization/timeout-logs/timeoutLogs.png[] diff --git a/docs/modules/ROOT/pages/action_examples/intro.adoc b/docs/modules/ROOT/pages/action_examples/intro.adoc index 10faae5..725a1fb 100644 --- a/docs/modules/ROOT/pages/action_examples/intro.adoc +++ b/docs/modules/ROOT/pages/action_examples/intro.adoc @@ -17,7 +17,7 @@ Each example includes a complete configuration snippet that you can adapt for yo After reviewing the examples, you can: -* xref:action_execution/create_your_first.adoc[Create your own action] - Build a custom action for your needs +* xref:action_buttons/create_your_first.adoc[Create your own action] - Build a custom action for your needs * xref:action_customization/intro.adoc[Customize your actions] - Learn how to configure action properties * xref:args/intro.adoc[Add arguments] - Make your actions interactive with user input * xref:solutions/intro.adoc[Explore complete solutions] - See full configurations for common use cases diff --git a/docs/modules/ROOT/pages/action_execution/create_your_first.adoc b/docs/modules/ROOT/pages/action_execution/create_your_first.adoc deleted file mode 100644 index 4394937..0000000 --- a/docs/modules/ROOT/pages/action_execution/create_your_first.adoc +++ /dev/null @@ -1,35 +0,0 @@ -[#create-your-first-action] -= Create your first action - -This is an example of your very first action. First of all, edit your config.yaml, and enter this YAML markup; - -.config.yaml -[source,yaml] ----- -actions: - - title: Say hello - shell: echo "Hello!" - icon: smile ----- - -If OliveTin is running, it should popup on your dashboard like this; - -image::hello-world.png[] - -Simply click on the button to execute the shell command. You can expand the executions to view the logs. - -== Important considerations - -* The action title must be unique. If you have multiple actions with the same title, only one will be shown. - -== What's Next? - -Now that you've created your first action, here are some logical next steps: - -* xref:action_customization/intro.adoc[Customize your actions] - Learn how to set icons, timeouts, and other action properties -* xref:args/intro.adoc[Add arguments to actions] - Make your actions interactive with user input -* xref:action_examples/intro.adoc[Browse action examples] - See real-world examples for common use cases -* xref:action_execution/oncron.adoc[Schedule actions] - Set up actions to run automatically on a schedule -* xref:action_execution/onwebhook.adoc[Trigger actions via webhooks] - Integrate OliveTin with external systems -* xref:dashboards/intro.adoc[Organize actions with dashboards] - Create custom views to organize your actions - diff --git a/docs/modules/ROOT/pages/action_execution/ondemand.adoc b/docs/modules/ROOT/pages/action_execution/ondemand.adoc index 4e38941..56b09fa 100644 --- a/docs/modules/ROOT/pages/action_execution/ondemand.adoc +++ b/docs/modules/ROOT/pages/action_execution/ondemand.adoc @@ -1,5 +1,69 @@ -[#exec-on-demand] -= Execute on demand +[#exec-on-click] +[#onclick] += Execute on click -This is the default, meaning that the action shows up on a dashboard, and at the moment cannot be changed. +OliveTin has several options to control what happens when an action button is clicked and the execution starts. This can be controlled on a per-action basis using the `onclick` configuration option. +You can also set the default for OliveTin using the `defaultOnClick` configuration option. + +NOTE: The older names `popupOnStart` and `defaultPopupOnStart` do exactly the same thing. OliveTin copies legacy values into `onclick` / `defaultOnClick` during config load. New configurations should use `onclick` and `defaultOnClick`. + +== Big Flashy Buttons (default) + +[source,yaml] +.`config.yaml` +---- +actions: + - title: Ping the Internet + onclick: default +---- + +This will also be the option that is used if no other values match. + +image::../flashyButton.png[] + +== Execution Dialog + +When an action uses `execution-dialog`, OliveTin opens the execution results view with the command output, plus the start time, end time, exit code, and duration. + +[NOTE] +The legacy option `execution-dialog-stdout-only` is deprecated in OliveTin 3k. It is still accepted in config files for compatibility, but is treated the same as `execution-dialog`. + +[source,yaml] +.`config.yaml` +---- +actions: + - title: Check dmesg logs + onclick: execution-dialog +---- + +.Example of `onclick: execution-dialog` +image::action_customization/execution-dialog/executionDialog.png[] + +== Execution Buttons + +This mode will create a new button for each individual execution. This can be useful for actions that are executed again and again. + +The text of the button (eg, "0s" in the screenshot below), is the time it took to execute the action in seconds. + +[source,yaml] +.`config.yaml` +---- +actions: + - title: date + onclick: execution-button +---- + +image::../executionButtons.png[] + +== Action execution history + +The `history` option opens the action details page for that binding when the execution starts, so you can see past runs and status for the same action. + +[source,yaml] +.`config.yaml` +---- +actions: + - title: Long-running job + onclick: history +---- diff --git a/docs/modules/ROOT/pages/action_execution/shellvsexec.adoc b/docs/modules/ROOT/pages/action_execution/shellvsexec.adoc index 5c40367..4abbc41 100644 --- a/docs/modules/ROOT/pages/action_execution/shellvsexec.adoc +++ b/docs/modules/ROOT/pages/action_execution/shellvsexec.adoc @@ -5,7 +5,7 @@ OliveTin supports two different methods to run commands: `shell` and `exec`. The * **Shell** is more flexible, because it allows you to chain commands (eg, using &&) and redirect or pipe output (eg: ">" or "|"). * **Exec** is more secure, because it does not invoke a shell, and thus avoids shell injection attacks. -Shell can be safe and secure with simple argument types (like ascii_identifier), but some argument types like URL can contain basically any character - /, :, ?, &, etc - which can lead to shell injection vulnerabilities while still being a valid URL. +Shell can be safe and secure with simple argument types (like ascii_identifier), but some argument types like URL can contain basically any character - /, :, ?, &, etc - which can lead to shell injection vulnerabilities while still being a valid URL. OliveTin will try and prevent you from using dangerous characters in shell commands (eg, URL is no longer permitted with Shell). @@ -36,7 +36,7 @@ When in doubt, prefer `exec` over `shell` for better security. Shell was added i Now that you understand execution methods, continue building your actions: -* xref:action_execution/create_your_first.adoc[Create your first action] - Build a simple action to get started +* xref:action_buttons/create_your_first.adoc[Create your first action] - Build a simple action to get started * xref:args/intro.adoc[Add arguments to actions] - Make actions interactive with user input * xref:action_execution/oncron.adoc[Schedule actions] - Set up automated execution * xref:action_execution/onwebhook.adoc[Trigger via webhooks] - Integrate with external systems diff --git a/docs/modules/ROOT/pages/advanced_configuration/intro.adoc b/docs/modules/ROOT/pages/advanced_configuration/intro.adoc index 1d0f6c8..157ea60 100644 --- a/docs/modules/ROOT/pages/advanced_configuration/intro.adoc +++ b/docs/modules/ROOT/pages/advanced_configuration/intro.adoc @@ -20,8 +20,8 @@ Most users will not need to modify these settings for basic OliveTin usage, but Explore the advanced configuration options: -* xref:advanced_configuration/logs.adoc[Configure logging] - Set up application and action logging -* xref:advanced_configuration/logs-calendar.adoc[Use the logs calendar] - Browse execution history visually +* xref:advanced_configuration/logs.adoc[Configure application logging] - Set up OliveTin service log levels +* xref:logs/intro.adoc[Browse action logs] - View execution history in the web interface * xref:advanced_configuration/diagnostics.adoc[Use diagnostics] - Access troubleshooting and diagnostic tools * xref:advanced_configuration/webui.adoc[Customize the web UI] - Modify the appearance and behavior of the interface * xref:advanced_configuration/stylemods.adoc[Apply style mods] - Use style modifications for UI customization diff --git a/docs/modules/ROOT/pages/advanced_configuration/logs.adoc b/docs/modules/ROOT/pages/advanced_configuration/logs.adoc index 3b0a829..90f3697 100644 --- a/docs/modules/ROOT/pages/advanced_configuration/logs.adoc +++ b/docs/modules/ROOT/pages/advanced_configuration/logs.adoc @@ -2,9 +2,9 @@ = Logging - Application [NOTE] -There are two different types of logs in OliveTin - xref:advanced_configuration/logs.adoc[application logs] and xref:advanced_configuration/logs-actions.adoc[action logs]. This page is about the __application logs__, which are the logs that OliveTin itself generates. The action logs are the logs that are generated by the actions that you run in OliveTin. +There are two different types of logs in OliveTin - xref:advanced_configuration/logs.adoc[application logs] and xref:logs/actions.adoc[action logs]. This page is about the __application logs__, which are the logs that OliveTin itself generates. The action logs are the logs that are generated by the actions that you run in OliveTin. -OliveTin supports a few different log levels. The default logLevel is `INFO`. +OliveTin supports a few different log levels. The default logLevel is `INFO`. You can set a `logLevel` in config.yaml like this; diff --git a/docs/modules/ROOT/pages/args/input.adoc b/docs/modules/ROOT/pages/args/input.adoc index 3cf3bf1..446ad72 100644 --- a/docs/modules/ROOT/pages/args/input.adoc +++ b/docs/modules/ROOT/pages/args/input.adoc @@ -34,17 +34,15 @@ actions: This will give you a normal button, like this; -image::args1.png[] +image::args/input/args1.png[] However, when you click on it, you'll get a prompt to enter arguments, like this; -image::args2.png[] +image::args/input/args2.png[] You'll see that the type is set to `ascii_sentence`. This applies fairly safe -input validation to arguments, so that only a-z, 0-9, spaces and .'s are allowed. +input validation to arguments, so that only a-z, 0-9, spaces and .'s are allowed. When you start the action, and it's finished, go to the "logs" view to view the output of the command we've just run. -image::args3.png[] - - +image::args/input/args3.png[] diff --git a/docs/modules/ROOT/pages/args/input_dropdown.adoc b/docs/modules/ROOT/pages/args/input_dropdown.adoc index fe0db77..2dffe73 100644 --- a/docs/modules/ROOT/pages/args/input_dropdown.adoc +++ b/docs/modules/ROOT/pages/args/input_dropdown.adoc @@ -1,7 +1,7 @@ [#arg-dropdowns] = Input: Dropdowns -Predefined choices are normally the safest type of arguments, because users are limited to only enter values that you specify. +Predefined choices are normally the safest type of arguments, because users are limited to only enter values that you specify. [source,yaml] ---- @@ -17,7 +17,7 @@ actions: value: Hello there! - title: Goodbye - value: Aww, goodbye. :-( + value: Aww, goodbye. :-( ---- Note that when predefined choices are used, the argument type is ignored. @@ -26,13 +26,13 @@ This is what it looks like in the web interface; image::args4.png[] -Then finally, when you execute this command, it would look something like this (remember that this is just a basic "echo" command). +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[] In the logs, you can then click on the log entry link to open the results; -image::args3.png[] +image::args/input/args3.png[] [#args-dropdown-entities] == Using Entities in Dropdowns diff --git a/docs/modules/ROOT/pages/args/suggestions.adoc b/docs/modules/ROOT/pages/args/suggestions.adoc index 71e47c5..9885d02 100644 --- a/docs/modules/ROOT/pages/args/suggestions.adoc +++ b/docs/modules/ROOT/pages/args/suggestions.adoc @@ -30,10 +30,10 @@ NOTE: `suggestions:` is a yaml **map**, not a **list**. If you leave the title e == Examples .Screenshot of input suggestions with Firefox on Linux. -image::arg-suggestions-firefox.png[] +image::args/suggestions/arg-suggestions-firefox.png[] .Screenshot of input suggestions with Chrome on Linux. -image::arg-suggestions-chrome.png[] +image::args/suggestions/arg-suggestions-chrome.png[] [#suggestions-browser-key] == Browser-Stored Suggestions @@ -125,5 +125,3 @@ This gives users quick access to the predefined environments while also remember == Browser Support `datalist` is widely supported now-a-days, but Firefox on Android notably lacks support; https://caniuse.com/datalist . See the upstream bug here; https://bugzilla.mozilla.org/show_bug.cgi?id=1535985 . - - diff --git a/docs/modules/ROOT/pages/args/templates.adoc b/docs/modules/ROOT/pages/args/templates.adoc index 6f99513..daeb7ee 100644 --- a/docs/modules/ROOT/pages/args/templates.adoc +++ b/docs/modules/ROOT/pages/args/templates.adoc @@ -21,7 +21,7 @@ The `Json` template function encodes a value as a JSON string. Pipe a template v ---- actions: - title: curl my knx thing - shell: curl --json https://knx.example.com/v1/group/global_on/write -d '{{ .Arguments | Json }}' + shell: curl --json '{{ .Arguments | Json }}' https://knx.example.com/v1/group/global_on/write entity: light arguments: - name: value diff --git a/docs/modules/ROOT/pages/config.adoc b/docs/modules/ROOT/pages/config.adoc index e13aff4..9003020 100644 --- a/docs/modules/ROOT/pages/config.adoc +++ b/docs/modules/ROOT/pages/config.adoc @@ -53,8 +53,8 @@ All configuration options are covered in the solution sections | `showNavigation` | Show (or hide) the sidebar/topbar section navigation. | `true` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI]. | `showNavigateOnStartIcons` | Show (or hide) the small icons on action buttons that indicate popup/argument/background behavior on start. | `true` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI]. | `sectionNavigationStyle` | The style of the section navigation. `sidebar`, `topbar` | `sidebar` | Live reloadable | xref:advanced_configuration/webui.adoc[Customize the web UI]. -| `defaultOnClick` | The default on-click behavior when an action starts. | `nothing` | Live reloadable | xref:action_customization/popuponstart.adoc[On Click]. -| `defaultPopupOnStart` | Legacy name for `defaultOnClick`. | - | Live reloadable | xref:action_customization/popuponstart.adoc[On Click]. +| `defaultOnClick` | The default on-click behavior when an action starts. | `nothing` | Live reloadable | xref:action_execution/ondemand.adoc[Execute on click]. +| `defaultPopupOnStart` | Legacy name for `defaultOnClick`. | - | Live reloadable | xref:action_execution/ondemand.adoc[Execute on click]. | `defaultIconForActions` | The default icon string for actions (Unicode aliases such as `smile`, `hugeicons:NeutralIcon`, HTML, Iconify snippets, images, etc.). See xref:action_customization/icons.adoc[Icons]. | `hugeicons:CommandLineIcon` | Requires Restart | - | `defaultIconForDirectories` | The default icon to use for directories. | `directory` | Requires Restart | - | `defaultIconForBack` | The default icon to use for back (from directories). | `«` | Requires Restart | - @@ -117,7 +117,7 @@ All configuration options are covered in the solution sections | `WebUIDir` | The directory to serve the web UI from. | Calculated at runtime. | Requires Restart | - | `CronSupportForSeconds` | Whether or not to support seconds in cron expressions. | `false` | Requires Restart | xref:action_execution/oncron.adoc[Cron] -| `SaveLogs` | Whether or not to save logs to disk. | `[]` | Requires Restart | xref:action_customization/savelogs.adoc[Save Logs] +| `SaveLogs` | Whether or not to save logs to disk. | `[]` | Requires Restart | xref:logs/saving.adoc[Save Logs] | `ServiceLogs` | Windows process log directory (`serviceLogs.directory`). | `%ProgramData%\OliveTin\logs\` on Windows | Requires Restart | xref:install/windows_service.adoc#windows-service-logs[Windows service logs] | `Prometheus` | Prometheus configuration. | `-` | Requires Restart | xref:advanced_configuration/prometheus.adoc[Prometheus] |=== @@ -126,7 +126,7 @@ All configuration options are covered in the solution sections Now that you understand the configuration structure, here are the next steps: -* xref:action_execution/create_your_first.adoc[Create your first action] - Start building actions for your use case +* xref:action_buttons/create_your_first.adoc[Create your first action] - Start building actions for your use case * xref:action_examples/intro.adoc[Browse action examples] - Get inspiration from real-world configurations * xref:args/intro.adoc[Add arguments to actions] - Make actions interactive with user input * xref:dashboards/intro.adoc[Organize with dashboards] - Create custom views to organize your actions diff --git a/docs/modules/ROOT/pages/dashboards/intro.adoc b/docs/modules/ROOT/pages/dashboards/intro.adoc index b64528a..0aca815 100644 --- a/docs/modules/ROOT/pages/dashboards/intro.adoc +++ b/docs/modules/ROOT/pages/dashboards/intro.adoc @@ -7,7 +7,7 @@ If you want to start organizing OliveTin actions more effectively, then **Dashbo One of the biggest reasons to use Dashboards as opposed to just the "Actions" section, is that dashboards allow you to use xref:dashboards/3-folders.adoc[Folders], and dashboards really start to get exciting when you start using xref:entities/intro.adoc[entities] and displays. -image::dashboard.png[] +image::dashboards/intro/preview.png[] == Dashboards pull actions from the default view @@ -44,7 +44,7 @@ dashboards: # Top level items are dashboards. - title: My Servers contents: - # On dashboards, all items need to be in a "fieldset". If you don't + # On dashboards, all items need to be in a "fieldset". If you don't # specify a fieldset, actions will be assigned to a fieldset with a title # called "default". - title: All Servers @@ -94,4 +94,3 @@ Now that you understand dashboards, explore these related features: * xref:entities/intro.adoc[Use entities with dashboards] - Dynamically generate actions from entity files * xref:dashboards/examples.adoc[View dashboard examples] - See complete dashboard configurations * xref:dashboards/css.adoc[Customize dashboard styling] - Change the appearance of dashboard components - diff --git a/docs/modules/ROOT/pages/install/intro.adoc b/docs/modules/ROOT/pages/install/intro.adoc index 862df3b..baddb14 100644 --- a/docs/modules/ROOT/pages/install/intro.adoc +++ b/docs/modules/ROOT/pages/install/intro.adoc @@ -6,7 +6,7 @@ You will need approximately **10 minutes** to install OliveTin on almost every p You probably will need about **10-20 minutes** to understand how OliveTin configuration works, and to be able to write your first action to make it do something useful. -== Where to find help +== Where to find help include::partial$support.adoc[] @@ -36,7 +36,7 @@ include::partial$support.adoc[] After installing OliveTin, follow these steps to get started: * xref:config.adoc[Learn about configuration] - Understand how OliveTin's configuration works -* xref:action_execution/create_your_first.adoc[Create your first action] - Build a simple action to test your installation +* xref:action_buttons/create_your_first.adoc[Create your first action] - Build a simple action to test your installation * xref:action_examples/intro.adoc[Explore action examples] - See what OliveTin can do with real-world examples * xref:solutions/intro.adoc[Check out solutions] - Find complete configurations for common use cases * xref:reverse-proxies/intro.adoc[Set up a reverse proxy] - Configure secure access through a reverse proxy diff --git a/docs/modules/ROOT/pages/advanced_configuration/logs-actions.adoc b/docs/modules/ROOT/pages/logs/actions.adoc similarity index 72% rename from docs/modules/ROOT/pages/advanced_configuration/logs-actions.adoc rename to docs/modules/ROOT/pages/logs/actions.adoc index baead5d..4bb8741 100644 --- a/docs/modules/ROOT/pages/advanced_configuration/logs-actions.adoc +++ b/docs/modules/ROOT/pages/logs/actions.adoc @@ -1,8 +1,8 @@ [#log-levels] -= Logging - Actions += Action logs [NOTE] -There are two different types of logs in OliveTin - xref:advanced_configuration/logs.adoc[application logs] and xref:advanced_configuration/logs-actions.adoc[action logs]. This page is about the __action logs__, which are the logs that are generated by the actions that you run in OliveTin. +There are two different types of logs in OliveTin - xref:advanced_configuration/logs.adoc[application logs] and xref:logs/actions.adoc[action logs]. This page is about the __action logs__, which are the logs that are generated by the actions that you run in OliveTin. == Controlling access to see action logs @@ -44,7 +44,7 @@ actions: == Disabling the log viewer -You can disable the log viewer in the interface with 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. +You can disable the log viewer in the interface with 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 log viewer for all users; @@ -74,6 +74,6 @@ accessControlLists: == See Also -* xref:action_customization/savelogs.adoc[Saving Action logs] -* xref:advanced_configuration/logs.adoc[Application Logs] -* xref:advanced_configuration/logs-calendar.adoc[Logs Calendar View] +* xref:logs/saving.adoc[Saving action logs] +* xref:advanced_configuration/logs.adoc[Application logs] +* xref:logs/calendar.adoc[Calendar view] diff --git a/docs/modules/ROOT/pages/advanced_configuration/logs-calendar.adoc b/docs/modules/ROOT/pages/logs/calendar.adoc similarity index 90% rename from docs/modules/ROOT/pages/advanced_configuration/logs-calendar.adoc rename to docs/modules/ROOT/pages/logs/calendar.adoc index 49e588c..89ca021 100644 --- a/docs/modules/ROOT/pages/advanced_configuration/logs-calendar.adoc +++ b/docs/modules/ROOT/pages/logs/calendar.adoc @@ -1,5 +1,5 @@ [#logs-calendar] -= Logs Calendar View += Calendar view The logs calendar view provides a visual way to browse your action execution history. Instead of scrolling through a list of logs, you can see executions displayed on a calendar, making it easy to identify patterns, busy periods, or specific dates when actions were run. @@ -9,6 +9,8 @@ To access the calendar view, navigate to the **Logs** page and click the **Calen You can return to the list view at any time by clicking the **Back to list** button. +image::logs/views/logsCalendar.png[] + == Features === View Executions on a Calendar @@ -50,6 +52,6 @@ The calendar view is particularly useful for: == See Also -* xref:advanced_configuration/logs-actions.adoc[Logging - Actions] -* xref:advanced_configuration/logs.adoc[Logging - Application] -* xref:action_customization/savelogs.adoc[Saving Action Logs] +* xref:logs/actions.adoc[Action logs] +* xref:advanced_configuration/logs.adoc[Application logs] +* xref:logs/saving.adoc[Saving logs] diff --git a/docs/modules/ROOT/pages/logs/intro.adoc b/docs/modules/ROOT/pages/logs/intro.adoc new file mode 100644 index 0000000..16180a6 --- /dev/null +++ b/docs/modules/ROOT/pages/logs/intro.adoc @@ -0,0 +1,38 @@ +[#logs] += Logs + +OliveTin records an entry every time an action runs. The **Logs** pages in the web interface let you browse that history, open individual executions, and filter or search past runs. + +Each log entry includes: + +* When the action started (and finished, if applicable) +* The action title and icon +* Who triggered it +* Status (for example, completed, timed out, or still running) +* A link to the full execution output + +== Logs in the web interface + +Use the **Logs** link in the navigation to open the list view. From there you can: + +* Search and filter executions +* Open the xref:logs/calendar.adoc[calendar view] to browse by date +* Open the xref:logs/queue.adoc[queue view] when executions are waiting for a concurrency slot +* Click an action title to see full output, timing, and exit status + +image::logs/views/logsList.png[] + +== Application logs vs action logs + +OliveTin has two different kinds of logging: + +* **Action logs** — history of commands you run through OliveTin (this section) +* **Application logs** — diagnostic output from the OliveTin service itself (see xref:advanced_configuration/logs.adoc[Logging - Application]) + +== What's next? + +* xref:logs/actions.adoc[Action logs] — permissions and visibility for the log viewer +* xref:logs/calendar.adoc[Calendar view] — browse executions on a calendar +* xref:logs/queue.adoc[Queue view] — see executions waiting for a concurrency slot +* xref:logs/saving.adoc[Saving logs] — persist logs to disk across restarts +* xref:action_customization/timeouts.adoc[Timeouts] — timed-out actions appear in the logs with a **Timed out** status diff --git a/docs/modules/ROOT/pages/logs/queue.adoc b/docs/modules/ROOT/pages/logs/queue.adoc new file mode 100644 index 0000000..f073692 --- /dev/null +++ b/docs/modules/ROOT/pages/logs/queue.adoc @@ -0,0 +1,19 @@ +[#logs-queue] += Queue view + +When actions belong to an xref:action_customization/concurrency.adoc[action group] and the group is at its concurrency limit, additional executions wait in a queue until a slot is free. + +The **Queue** page shows those waiting executions grouped by action group, including queue position and status. + +== Accessing the queue view + +From the **Logs** page, click the **Queue** button in the toolbar to open the queue view. + +You can return to the list view at any time using **Back to list**. + +image::logs/views/logsQueue.png[] + +== See also + +* xref:logs/intro.adoc[Logs overview] +* xref:action_customization/concurrency.adoc[Concurrency and action groups] diff --git a/docs/modules/ROOT/pages/action_customization/savelogs.adoc b/docs/modules/ROOT/pages/logs/saving.adoc similarity index 99% rename from docs/modules/ROOT/pages/action_customization/savelogs.adoc rename to docs/modules/ROOT/pages/logs/saving.adoc index c3e660b..d69d181 100644 --- a/docs/modules/ROOT/pages/action_customization/savelogs.adoc +++ b/docs/modules/ROOT/pages/logs/saving.adoc @@ -60,5 +60,3 @@ actionid: d3cf6e25-8bab-432d-b4f9-e6f531b2b67b ---- Sun 28 Apr 20:43:04 BST 2024 ---- - - diff --git a/docs/modules/ROOT/pages/solutions/human-in-the-control-loop/index.adoc b/docs/modules/ROOT/pages/solutions/human-in-the-control-loop/index.adoc new file mode 100644 index 0000000..2431c48 --- /dev/null +++ b/docs/modules/ROOT/pages/solutions/human-in-the-control-loop/index.adoc @@ -0,0 +1,37 @@ +[#human-in-the-control-loop] += Human in the Control Loop + +This solution shows a simple control-panel pattern where a human operator sees live status on a dashboard and decides when to run an action. + +A hidden background action keeps the displayed status up to date, while the dashboard only shows the operator control you want them to use. + +image::solutions/human-in-the-control-loop/preview.png[] + +== How it works + +* **Pump ON - 5m** is the only visible action on the dashboard. The operator starts the pump when they choose. +* **Update Water Level** is a xref:action_execution/triggers.adoc[hidden] action that runs on startup and on a schedule. It prints dummy output (`Water level 47%`) that OliveTin keeps in the action logs. +* A xref:dashboards/5-output-views.adoc[most recent execution] dashboard component shows the latest output from **Update Water Level**, so the operator always sees the current reading without clicking a refresh button. + +When the pump action finishes, it xref:action_execution/triggers.adoc[triggers] **Update Water Level** so the displayed value can be refreshed after manual control. + +== Configuration + +[source,yaml] +.`config.yaml` +---- +include::example$solutions/human-in-the-control-loop/config.yaml[] +---- + +== Customising this pattern + +* Replace the dummy `echo` commands with scripts that read real sensors or call your APIs. +* Change the cron schedule on **Update Water Level** to match how often the status should refresh. +* Add ACLs if only certain users should see the dashboard or run the pump action. + +== See also + +* xref:dashboards/5-output-views.adoc[Most recent action output] +* xref:action_execution/oncron.adoc[Execute on schedule (cron)] +* xref:action_execution/onstartup.adoc[Execute on startup] +* xref:action_execution/triggers.adoc[Triggers] diff --git a/docs/modules/ROOT/pages/solutions/k8s-control-panel-hosted/index.adoc b/docs/modules/ROOT/pages/solutions/k8s-control-panel-hosted/index.adoc index 0cfbc2a..310005e 100644 --- a/docs/modules/ROOT/pages/solutions/k8s-control-panel-hosted/index.adoc +++ b/docs/modules/ROOT/pages/solutions/k8s-control-panel-hosted/index.adoc @@ -4,7 +4,7 @@ This solution gives you quick and easy buttons to run kubectl commands, when Oli This use case is enabled by simply providing the OliveTin pod access to talk to the kubernetes API, and using `kubectl` which is preinstalled with modern versions of OliveTin. -image::solution-k8s-hosted.png[] +image::solutions/k8s-control-panel-hosted/preview.png[] == Requirements & Assumptions diff --git a/docs/modules/ROOT/pages/solutions/systemd-control-panel/index.adoc b/docs/modules/ROOT/pages/solutions/systemd-control-panel/index.adoc index 3c89135..5488391 100644 --- a/docs/modules/ROOT/pages/solutions/systemd-control-panel/index.adoc +++ b/docs/modules/ROOT/pages/solutions/systemd-control-panel/index.adoc @@ -3,7 +3,7 @@ OliveTin can be used to manage selected systemd units (services) as well. This is powered by OliveTin's powerful xref::entities/intro.adoc[entity support]. Here is a screenshot of what that can look like (dark theme preference enabled). -image::solution-systemd-control-panel.png[] +image::solutions/systemd-control-panel/preview.png[] NOTE: To control systemd, you will need the root user. If you are running OliveTin systemd service itself, then OliveTin should be configured to run as root. The alternative is to use OliveTin in a container, and use xref:action_examples/ssh-easy.adoc[SSH] to connect back to the host (as a user that can manage systemd -usually root). This is not an OliveTin limitation, it's just how systemd security works. @@ -40,5 +40,3 @@ Finally, here is the example configuration file to build the dashboard; ---- include::example$solutions/systemd-control-panel/config/config.yaml[] ---- - - diff --git a/docs/modules/ROOT/partials/install/to_config.adoc b/docs/modules/ROOT/partials/install/to_config.adoc index 1f48e49..f48e513 100644 --- a/docs/modules/ROOT/partials/install/to_config.adoc +++ b/docs/modules/ROOT/partials/install/to_config.adoc @@ -1,8 +1,7 @@ -You should be able to browse to http://yourserver:1337 (or similar) to get to +You should be able to browse to http://yourserver:1337 (or similar) to get to the web interface. If you see the OliveTin page popup in your browser, you are good to go! Here are some helpful next steps; -* xref:action_execution/create_your_first.adoc[Create your first action] +* xref:action_buttons/create_your_first.adoc[Create your first action] * xref:config.adoc[configuration section] for a list of all configuration options. - diff --git a/docs/modules/ROOT/partials/install/windows_service_logs.adoc b/docs/modules/ROOT/partials/install/windows_service_logs.adoc index 57b3e52..8bc14ee 100644 --- a/docs/modules/ROOT/partials/install/windows_service_logs.adoc +++ b/docs/modules/ROOT/partials/install/windows_service_logs.adoc @@ -3,7 +3,7 @@ On Windows, OliveTin writes its **process logs** (startup messages, configuration load, errors, and internal diagnostics) to a log file on disk. By default these files are stored under `%ProgramData%\OliveTin\logs\` as `OliveTin-service-.log`. -This is separate from xref:action_customization/savelogs.adoc[action execution logs] (`saveLogs`), which persist command output from individual actions. +This is separate from xref:logs/saving.adoc[action execution logs] (`saveLogs`), which persist command output from individual actions. Portable or self-contained installs often keep OliveTin, its configuration, and its logs together in one folder. Without a custom path, process logs always go to `%ProgramData%`, even when you run OliveTin from another location. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3ea33bf..c7ef7ca 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,19 +11,19 @@ "dependencies": { "@connectrpc/connect": "^2.1.2", "@connectrpc/connect-web": "^2.1.2", - "@hugeicons/core-free-icons": "^4.2.0", + "@hugeicons/core-free-icons": "^4.2.1", "@hugeicons/vue": "^1.0.6", "@vitejs/plugin-vue": "^6.0.7", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.16.0", + "picocrank": "^1.17.0", "standard": "^17.1.2", "unplugin-vue-components": "^32.1.0", "vite": "^8.0.16", "vue": "^3.5.38", - "vue-i18n": "^11.4.5", + "vue-i18n": "^11.4.6", "vue-router": "^5.1.0" }, "devDependencies": { @@ -941,9 +941,9 @@ } }, "node_modules/@hugeicons/core-free-icons": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.0.tgz", - "integrity": "sha512-V1G/Ph9TbmEow+pKnupZRWQjdORR/TGGr3JVRZOWkomdJ/5N6GgLuKPgBDs7G0kZ0//9LL34AGOUzWe3K+umNA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.1.tgz", + "integrity": "sha512-75jYZKYyA9VwS35YRmmGUGzFedbY+Fl0Vxx5FzXR2CGDlIhNRumFeVqaaKoClf2MeYEJwPAVMEL9RwCYtOgnSw==", "license": "MIT" }, "node_modules/@hugeicons/vue": { @@ -997,14 +997,14 @@ "license": "MIT" }, "node_modules/@intlify/core-base": { - "version": "11.4.5", - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.5.tgz", - "integrity": "sha512-lja3F/iKVIvTa48mIwmrIeDcQUFZ0F0drvFvT8AwINOvbwnAzl/S/p8p2DxILZpWEUHRi1qewfWNIkMvhD3kKA==", + "version": "11.4.6", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.6.tgz", + "integrity": "sha512-EOeHO95XESK9IFHgHeZXunsM/WBAoCA0DlaWODvx14vKmetAuS97t+l6Xe9hTUqntPpF93vtVSjjUDafw3wXMw==", "license": "MIT", "dependencies": { - "@intlify/devtools-types": "11.4.5", - "@intlify/message-compiler": "11.4.5", - "@intlify/shared": "11.4.5" + "@intlify/devtools-types": "11.4.6", + "@intlify/message-compiler": "11.4.6", + "@intlify/shared": "11.4.6" }, "engines": { "node": ">= 22" @@ -1014,13 +1014,13 @@ } }, "node_modules/@intlify/devtools-types": { - "version": "11.4.5", - "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.5.tgz", - "integrity": "sha512-W5vydP9Yq3t82IyWqCM6aR0BTWCZrN5RAwjZEPpH8I2OQWp2RLy03Evh2ANZlSMhcvGAoyDg25k0so85Kwncpw==", + "version": "11.4.6", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.6.tgz", + "integrity": "sha512-wowQPpNem56b2d43IJmqbrzG2FeBKe5f/kUGlpNuBmXs6OSqncF8m1+1lxHuW8ISZJF0ma2RkW3iLkw0g0G4VA==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.4.5", - "@intlify/shared": "11.4.5" + "@intlify/core-base": "11.4.6", + "@intlify/shared": "11.4.6" }, "engines": { "node": ">= 22" @@ -1030,12 +1030,12 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "11.4.5", - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.5.tgz", - "integrity": "sha512-IEOZiHtbQopyPc/Dz2M869lOlZYX1SdcniNJwphATDYHhovvIneEKf1EFF37DE7NAABZtza1FNtnwwqZWInfpw==", + "version": "11.4.6", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.6.tgz", + "integrity": "sha512-5nj3jULqeTAC1WovwMs1LQWgatTa2pM/rXN9T3XW8rdOtXW9ZF6/GLSNFTKDQmPLwclhPdgUWLJ/4w3fMeeC/Q==", "license": "MIT", "dependencies": { - "@intlify/shared": "11.4.5", + "@intlify/shared": "11.4.6", "source-map-js": "^1.0.2" }, "engines": { @@ -1046,9 +1046,9 @@ } }, "node_modules/@intlify/shared": { - "version": "11.4.5", - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.5.tgz", - "integrity": "sha512-g/i5mtdUa9ia/8BaJ4w6ZRHgAXYQd9XyCaQPRMvsd8d5qmZwkjoTmHrNsI28Q/7I8h+2ijUkI4uEnnMCziKupQ==", + "version": "11.4.6", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.6.tgz", + "integrity": "sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA==", "license": "MIT", "engines": { "node": ">= 22" @@ -5186,9 +5186,9 @@ "license": "ISC" }, "node_modules/picocrank": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.16.0.tgz", - "integrity": "sha512-FHZ11Y0Jwqw89LQ0wdmglzUIk5W9vhG7CyrbW86qTbnke6hTyO721aixu0QkZtsw8GYSKJqO6QPLoOOxi0ZUMQ==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.17.0.tgz", + "integrity": "sha512-EIUSI26elt6ulloN74CT2sX5tBxS2wjdV4A+eXHYNtHbH0hM2iKkYzpKKWOEr+yUTfrvjah6VuqieedFw/9R0w==", "license": "ISC", "dependencies": { "@hugeicons/core-free-icons": "^4.1.1", @@ -6900,14 +6900,14 @@ } }, "node_modules/vue-i18n": { - "version": "11.4.5", - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.5.tgz", - "integrity": "sha512-rm8YJ6RpjOrkcgS2GLrZwLvs/VbhxbTSuEspbyXDo233+fPK0OMFNLOj3fdQYVKdOgcpSfLW91JhbqgpkkcBWA==", + "version": "11.4.6", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.6.tgz", + "integrity": "sha512-l0gE7Rfy0phCa5ChKYkOq543Wgd39BCK6hkktfr1Ed4D99oRkgPK9ffShASZdeC8OJxGfdWmpYoAaAH6iLEuIg==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.4.5", - "@intlify/devtools-types": "11.4.5", - "@intlify/shared": "11.4.5", + "@intlify/core-base": "11.4.6", + "@intlify/devtools-types": "11.4.6", + "@intlify/shared": "11.4.6", "@vue/devtools-api": "^6.5.0" }, "engines": { diff --git a/frontend/package.json b/frontend/package.json index 720924e..6865bb0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,19 +24,19 @@ "dependencies": { "@connectrpc/connect": "^2.1.2", "@connectrpc/connect-web": "^2.1.2", - "@hugeicons/core-free-icons": "^4.2.0", + "@hugeicons/core-free-icons": "^4.2.1", "@hugeicons/vue": "^1.0.6", "@vitejs/plugin-vue": "^6.0.7", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.16.0", + "picocrank": "^1.17.0", "standard": "^17.1.2", "unplugin-vue-components": "^32.1.0", "vite": "^8.0.16", "vue": "^3.5.38", - "vue-i18n": "^11.4.5", + "vue-i18n": "^11.4.6", "vue-router": "^5.1.0" }, "engines": { 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 9893367..78e8d7a 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 @@ -105,6 +105,11 @@ export declare type Action = Message<"olivetin.api.v1.Action"> & { * @generated from field: bool has_queued_instance = 18; */ hasQueuedInstance: boolean; + + /** + * @generated from field: repeated olivetin.api.v1.ActionGroupMembership groups = 19; + */ + groups: ActionGroupMembership[]; }; /** @@ -113,6 +118,32 @@ export declare type Action = Message<"olivetin.api.v1.Action"> & { */ export declare const ActionSchema: GenMessage; +/** + * @generated from message olivetin.api.v1.ActionGroupMembership + */ +export declare type ActionGroupMembership = Message<"olivetin.api.v1.ActionGroupMembership"> & { + /** + * @generated from field: string name = 1; + */ + name: string; + + /** + * @generated from field: int32 max_concurrent = 2; + */ + maxConcurrent: number; + + /** + * @generated from field: int32 queue_size = 3; + */ + queueSize: number; +}; + +/** + * Describes the message olivetin.api.v1.ActionGroupMembership. + * Use `create(ActionGroupMembershipSchema)` to create a new message. + */ +export declare const ActionGroupMembershipSchema: GenMessage; + /** * @generated from message olivetin.api.v1.ActionWebhookExecHint */ @@ -912,6 +943,11 @@ export declare type ExecutionQueueGroup = Message<"olivetin.api.v1.ExecutionQueu * @generated from field: int32 queued_count = 6; */ queuedCount: number; + + /** + * @generated from field: int32 queue_size = 7; + */ + queueSize: number; }; /** @@ -1046,6 +1082,37 @@ export declare type ExecutionStatusRequest = Message<"olivetin.api.v1.ExecutionS */ export declare const ExecutionStatusRequestSchema: GenMessage; +/** + * @generated from message olivetin.api.v1.DashboardNavigationTarget + */ +export declare type DashboardNavigationTarget = Message<"olivetin.api.v1.DashboardNavigationTarget"> & { + /** + * @generated from field: string title = 1; + */ + title: string; + + /** + * @generated from field: string entity_type = 2; + */ + entityType: string; + + /** + * @generated from field: string entity_key = 3; + */ + entityKey: string; + + /** + * @generated from field: string path = 4; + */ + path: string; +}; + +/** + * Describes the message olivetin.api.v1.DashboardNavigationTarget. + * Use `create(DashboardNavigationTargetSchema)` to create a new message. + */ +export declare const DashboardNavigationTargetSchema: GenMessage; + /** * @generated from message olivetin.api.v1.ExecutionStatusResponse */ @@ -1054,6 +1121,11 @@ export declare type ExecutionStatusResponse = Message<"olivetin.api.v1.Execution * @generated from field: olivetin.api.v1.LogEntry log_entry = 1; */ logEntry?: LogEntry | undefined; + + /** + * @generated from field: repeated olivetin.api.v1.DashboardNavigationTarget back_to_dashboards = 2; + */ + backToDashboards: DashboardNavigationTarget[]; }; /** @@ -1800,6 +1872,11 @@ export declare type GetActionBindingResponse = Message<"olivetin.api.v1.GetActio * @generated from field: olivetin.api.v1.Action action = 1; */ action?: Action | undefined; + + /** + * @generated from field: repeated olivetin.api.v1.DashboardNavigationTarget back_to_dashboards = 2; + */ + backToDashboards: DashboardNavigationTarget[]; }; /** 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 665e344..0ca3819 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("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSKIBAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBAgASgIEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgiwwIKFUFjdGlvbldlYmhvb2tFeGVjSGludBIQCgh0ZW1wbGF0ZRgBIAEoCRISCgptYXRjaF9wYXRoGAIgASgJEk8KDW1hdGNoX2hlYWRlcnMYAyADKAsyOC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uV2ViaG9va0V4ZWNIaW50Lk1hdGNoSGVhZGVyc0VudHJ5EksKC21hdGNoX3F1ZXJ5GAQgAygLMjYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaFF1ZXJ5RW50cnkaMwoRTWF0Y2hIZWFkZXJzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ARoxCg9NYXRjaFF1ZXJ5RW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASK7AgoOQWN0aW9uQXJndW1lbnQSDAoEbmFtZRgBIAEoCRINCgV0aXRsZRgCIAEoCRIMCgR0eXBlGAMgASgJEhUKDWRlZmF1bHRfdmFsdWUYBCABKAkSNgoHY2hvaWNlcxgFIAMoCzIlLm9saXZldGluLmFwaS52MS5BY3Rpb25Bcmd1bWVudENob2ljZRITCgtkZXNjcmlwdGlvbhgGIAEoCRJFCgtzdWdnZXN0aW9ucxgHIAMoCzIwLm9saXZldGluLmFwaS52MS5BY3Rpb25Bcmd1bWVudC5TdWdnZXN0aW9uc0VudHJ5Eh8KF3N1Z2dlc3Rpb25zX2Jyb3dzZXJfa2V5GAggASgJGjIKEFN1Z2dlc3Rpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI0ChRBY3Rpb25Bcmd1bWVudENob2ljZRINCgV2YWx1ZRgBIAEoCRINCgV0aXRsZRgCIAEoCSKyAQoGRW50aXR5Eg0KBXRpdGxlGAEgASgJEhIKCnVuaXF1ZV9rZXkYAiABKAkSDAoEdHlwZRgDIAEoCRITCgtkaXJlY3RvcmllcxgEIAMoCRIzCgZmaWVsZHMYBSADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5LkZpZWxkc0VudHJ5Gi0KC0ZpZWxkc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiVAoUR2V0RGFzaGJvYXJkUmVzcG9uc2USDQoFdGl0bGUYASABKAkSLQoJZGFzaGJvYXJkGAQgASgLMhoub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZCJfCg9FZmZlY3RpdmVQb2xpY3kSGAoQc2hvd19kaWFnbm9zdGljcxgBIAEoCBIVCg1zaG93X2xvZ19saXN0GAIgASgIEhsKE3Nob3dfdmVyc2lvbl9udW1iZXIYAyABKAgiTQoTR2V0RGFzaGJvYXJkUmVxdWVzdBINCgV0aXRsZRgBIAEoCRITCgtlbnRpdHlfdHlwZRgCIAEoCRISCgplbnRpdHlfa2V5GAMgASgJIlEKCURhc2hib2FyZBINCgV0aXRsZRgBIAEoCRI1Cghjb250ZW50cxgCIAMoCzIjLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmRDb21wb25lbnQi2wEKEkRhc2hib2FyZENvbXBvbmVudBINCgV0aXRsZRgBIAEoCRIMCgR0eXBlGAIgASgJEjUKCGNvbnRlbnRzGAMgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudBIMCgRpY29uGAQgASgJEhEKCWNzc19jbGFzcxgFIAEoCRInCgZhY3Rpb24YBiABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEhMKC2VudGl0eV90eXBlGAcgASgJEhIKCmVudGl0eV9rZXkYCCABKAkilAEKElN0YXJ0QWN0aW9uUmVxdWVzdBISCgpiaW5kaW5nX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50EhoKEnVuaXF1ZV90cmFja2luZ19pZBgDIAEoCRIVCg1qdXN0aWZpY2F0aW9uGAQgASgJIjIKE1N0YXJ0QWN0aW9uQXJndW1lbnQSDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSI0ChNTdGFydEFjdGlvblJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgCIAEoCSJ+ChlTdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIVCg1qdXN0aWZpY2F0aW9uGAMgASgJIkoKGlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIsChdTdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiOQoYU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgCIAEoCSIzCh5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIk8KH1N0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5Il4KDkdldExvZ3NSZXF1ZXN0EhQKDHN0YXJ0X29mZnNldBgBIAEoAxITCgtkYXRlX2ZpbHRlchgCIAEoCRIRCglwYWdlX3NpemUYAyABKAMSDgoGZmlsdGVyGAQgASgJItsDCghMb2dFbnRyeRIYChBkYXRldGltZV9zdGFydGVkGAEgASgJEhQKDGFjdGlvbl90aXRsZRgCIAEoCRIOCgZvdXRwdXQYAyABKAkSEQoJdGltZWRfb3V0GAUgASgIEhEKCWV4aXRfY29kZRgGIAEoBRIMCgR1c2VyGAcgASgJEhIKCnVzZXJfY2xhc3MYCCABKAkSEwoLYWN0aW9uX2ljb24YCSABKAkSDAoEdGFncxgKIAMoCRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYCyABKAkSGQoRZGF0ZXRpbWVfZmluaXNoZWQYDCABKAkSGQoRZXhlY3V0aW9uX3N0YXJ0ZWQYDiABKAgSGgoSZXhlY3V0aW9uX2ZpbmlzaGVkGA8gASgIEg8KB2Jsb2NrZWQYECABKAgSFgoOZGF0ZXRpbWVfaW5kZXgYESABKAMSEAoIY2FuX2tpbGwYEiABKAgSIwobZGF0ZXRpbWVfcmF0ZV9saW1pdF9leHBpcmVzGBMgASgJEhIKCmJpbmRpbmdfaWQYFCABKAkSDgoGcXVldWVkGBUgASgIEhgKEHF1ZXVlZF9mb3JfZ3JvdXAYFiABKAkSFQoNanVzdGlmaWNhdGlvbhgXIAEoCSKRAQoPR2V0TG9nc1Jlc3BvbnNlEicKBGxvZ3MYASADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSFwoPY291bnRfcmVtYWluaW5nGAIgASgDEhEKCXBhZ2Vfc2l6ZRgDIAEoAxITCgt0b3RhbF9jb3VudBgEIAEoAxIUCgxzdGFydF9vZmZzZXQYBSABKAMiPwoUR2V0QWN0aW9uTG9nc1JlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEhQKDHN0YXJ0X29mZnNldBgCIAEoAyKXAQoVR2V0QWN0aW9uTG9nc1Jlc3BvbnNlEicKBGxvZ3MYASADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSFwoPY291bnRfcmVtYWluaW5nGAIgASgDEhEKCXBhZ2Vfc2l6ZRgDIAEoAxITCgt0b3RhbF9jb3VudBgEIAEoAxIUCgxzdGFydF9vZmZzZXQYBSABKAMiGgoYR2V0RXhlY3V0aW9uUXVldWVSZXF1ZXN0IsYBChRFeGVjdXRpb25RdWV1ZUFjdGlvbhISCgpiaW5kaW5nX2lkGAEgASgJEhQKDGFjdGlvbl90aXRsZRgCIAEoCRITCgthY3Rpb25faWNvbhgDIAEoCRIWCg5tYXhfY29uY3VycmVudBgEIAEoBRIUCgxhY3RpdmVfY291bnQYBSABKAUSFQoNZW50aXR5X3ByZWZpeBgGIAEoCRIqCgdlbnRyaWVzGAcgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5Iq0BChNFeGVjdXRpb25RdWV1ZUdyb3VwEgwKBG5hbWUYASABKAkSDAoEaWNvbhgCIAEoCRIWCg5tYXhfY29uY3VycmVudBgDIAEoBRIUCgxhY3RpdmVfY291bnQYBCABKAUSNgoHYWN0aW9ucxgFIAMoCzIlLm9saXZldGluLmFwaS52MS5FeGVjdXRpb25RdWV1ZUFjdGlvbhIUCgxxdWV1ZWRfY291bnQYBiABKAUiZwoZR2V0RXhlY3V0aW9uUXVldWVSZXNwb25zZRI0CgZncm91cHMYASADKAsyJC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVHcm91cBIUCgx0b3RhbF9hY3RpdmUYAiABKAUiZQobVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEgwKBHR5cGUYAiABKAkSEgoKYmluZGluZ19pZBgDIAEoCRIVCg1hcmd1bWVudF9uYW1lGAQgASgJIkIKHFZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2USDQoFdmFsaWQYASABKAgSEwoLZGVzY3JpcHRpb24YAiABKAkiNgoVV2F0Y2hFeGVjdXRpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSImChRXYXRjaEV4ZWN1dGlvblVwZGF0ZRIOCgZ1cGRhdGUYASABKAkiSgoWRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSEQoJYWN0aW9uX2lkGAIgASgJIkcKF0V4ZWN1dGlvblN0YXR1c1Jlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIPCg1XaG9BbUlSZXF1ZXN0ImwKDldob0FtSVJlc3BvbnNlEhoKEmF1dGhlbnRpY2F0ZWRfdXNlchgBIAEoCRIRCgl1c2VyZ3JvdXAYAiABKAkSEAoIcHJvdmlkZXIYAyABKAkSDAoEYWNscxgEIAMoCRILCgNzaWQYBSABKAkiEgoQU29zUmVwb3J0UmVxdWVzdCIiChFTb3NSZXBvcnRSZXNwb25zZRINCgVhbGVydBgBIAEoCSIRCg9EdW1wVmFyc1JlcXVlc3QilQEKEER1bXBWYXJzUmVzcG9uc2USDQoFYWxlcnQYASABKAkSQQoIY29udGVudHMYAiADKAsyLy5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZS5Db250ZW50c0VudHJ5Gi8KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI7CgxEZWJ1Z0JpbmRpbmcSFAoMYWN0aW9uX3RpdGxlGAEgASgJEhUKDWVudGl0eV9wcmVmaXgYAiABKAkiHgocRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdCLOAQodRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2USDQoFYWxlcnQYASABKAkSTgoIY29udGVudHMYAiADKAsyPC5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UuQ29udGVudHNFbnRyeRpOCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRIsCgV2YWx1ZRgCIAEoCzIdLm9saXZldGluLmFwaS52MS5EZWJ1Z0JpbmRpbmc6AjgBIhIKEEdldFJlYWR5elJlcXVlc3QiIwoRR2V0UmVhZHl6UmVzcG9uc2USDgoGc3RhdHVzGAEgASgJIhQKEkV2ZW50U3RyZWFtUmVxdWVzdCKZAwoTRXZlbnRTdHJlYW1SZXNwb25zZRI9Cg5lbnRpdHlfY2hhbmdlZBgCIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudEVudGl0eUNoYW5nZWRIABI9Cg5jb25maWdfY2hhbmdlZBgDIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudENvbmZpZ0NoYW5nZWRIABJFChJleGVjdXRpb25fZmluaXNoZWQYBCABKAsyJy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25GaW5pc2hlZEgAEkMKEWV4ZWN1dGlvbl9zdGFydGVkGAUgASgLMiYub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uU3RhcnRlZEgAEjkKDG91dHB1dF9jaHVuaxgGIAEoCzIhLm9saXZldGluLmFwaS52MS5FdmVudE91dHB1dENodW5rSAASNAoJaGVhcnRiZWF0GAcgASgLMh8ub2xpdmV0aW4uYXBpLnYxLkV2ZW50SGVhcnRiZWF0SABCBwoFZXZlbnQiQQoQRXZlbnRPdXRwdXRDaHVuaxIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGb3V0cHV0GAIgASgJIhQKEkV2ZW50RW50aXR5Q2hhbmdlZCIUChJFdmVudENvbmZpZ0NoYW5nZWQiEAoORXZlbnRIZWFydGJlYXQiRgoWRXZlbnRFeGVjdXRpb25GaW5pc2hlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiRQoVRXZlbnRFeGVjdXRpb25TdGFydGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIyChFLaWxsQWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkibQoSS2lsbEFjdGlvblJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZraWxsZWQYAiABKAgSGQoRYWxyZWFkeV9jb21wbGV0ZWQYAyABKAgSDQoFZm91bmQYBCABKAgiOwoVTG9jYWxVc2VyTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIikKFkxvY2FsVXNlckxvZ2luUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCInChNQYXNzd29yZEhhc2hSZXF1ZXN0EhAKCHBhc3N3b3JkGAEgASgJIiQKFFBhc3N3b3JkSGFzaFJlc3BvbnNlEgwKBGhhc2gYASABKAkiDwoNTG9nb3V0UmVxdWVzdCIQCg5Mb2dvdXRSZXNwb25zZSIXChVHZXREaWFnbm9zdGljc1JlcXVlc3QiRQoWR2V0RGlhZ25vc3RpY3NSZXNwb25zZRITCgtTc2hGb3VuZEtleRgBIAEoCRIWCg5Tc2hGb3VuZENvbmZpZxgCIAEoCSINCgtJbml0UmVxdWVzdCLrBQoMSW5pdFJlc3BvbnNlEhIKCnNob3dGb290ZXIYASABKAgSFgoOc2hvd05hdmlnYXRpb24YAiABKAgSFwoPc2hvd05ld1ZlcnNpb25zGAMgASgIEhgKEGF2YWlsYWJsZVZlcnNpb24YBCABKAkSFgoOY3VycmVudFZlcnNpb24YBSABKAkSEQoJcGFnZVRpdGxlGAYgASgJEh4KFnNlY3Rpb25OYXZpZ2F0aW9uU3R5bGUYByABKAkSGgoSZGVmYXVsdEljb25Gb3JCYWNrGAggASgJEhYKDmVuYWJsZUN1c3RvbUpzGAkgASgIEhQKDGF1dGhMb2dpblVybBgKIAEoCRIWCg5hdXRoTG9jYWxMb2dpbhgLIAEoCBIRCglzdHlsZU1vZHMYDCADKAkSOAoPb0F1dGgyUHJvdmlkZXJzGA0gAygLMh8ub2xpdmV0aW4uYXBpLnYxLk9BdXRoMlByb3ZpZGVyEjgKD2FkZGl0aW9uYWxMaW5rcxgOIAMoCzIfLm9saXZldGluLmFwaS52MS5BZGRpdGlvbmFsTGluaxIWCg5yb290RGFzaGJvYXJkcxgPIAMoCRIaChJhdXRoZW50aWNhdGVkX3VzZXIYECABKAkSIwobYXV0aGVudGljYXRlZF91c2VyX3Byb3ZpZGVyGBEgASgJEjoKEGVmZmVjdGl2ZV9wb2xpY3kYEiABKAsyIC5vbGl2ZXRpbi5hcGkudjEuRWZmZWN0aXZlUG9saWN5EhYKDmJhbm5lcl9tZXNzYWdlGBMgASgJEhIKCmJhbm5lcl9jc3MYFCABKAkSGAoQc2hvd19kaWFnbm9zdGljcxgVIAEoCBIVCg1zaG93X2xvZ19saXN0GBYgASgIEhYKDmxvZ2luX3JlcXVpcmVkGBcgASgIEhgKEGF2YWlsYWJsZV90aGVtZXMYGCADKAkSJAocc2hvd19uYXZpZ2F0ZV9vbl9zdGFydF9pY29ucxgZIAEoCCIsCg5BZGRpdGlvbmFsTGluaxINCgV0aXRsZRgBIAEoCRILCgN1cmwYAiABKAkiOgoOT0F1dGgyUHJvdmlkZXISDQoFdGl0bGUYASABKAkSDAoEaWNvbhgDIAEoCRILCgNrZXkYBCABKAkiLQoXR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCSJDChhHZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2USJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbiIUChJHZXRFbnRpdGllc1JlcXVlc3QiVAoTR2V0RW50aXRpZXNSZXNwb25zZRI9ChJlbnRpdHlfZGVmaW5pdGlvbnMYASADKAsyIS5vbGl2ZXRpbi5hcGkudjEuRW50aXR5RGVmaW5pdGlvbiJpChBFbnRpdHlEZWZpbml0aW9uEg0KBXRpdGxlGAEgASgJEioKCWluc3RhbmNlcxgCIAMoCzIXLm9saXZldGluLmFwaS52MS5FbnRpdHkSGgoSdXNlZF9vbl9kYXNoYm9hcmRzGAMgAygJIjQKEEdldEVudGl0eVJlcXVlc3QSEgoKdW5pcXVlX2tleRgBIAEoCRIMCgR0eXBlGAIgASgJIjUKFFJlc3RhcnRBY3Rpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCTLWEwoST2xpdmVUaW5BcGlTZXJ2aWNlEl0KDEdldERhc2hib2FyZBIkLm9saXZldGluLmFwaS52MS5HZXREYXNoYm9hcmRSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlc3BvbnNlIgASWgoLU3RhcnRBY3Rpb24SIy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJvChJTdGFydEFjdGlvbkFuZFdhaXQSKi5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBorLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZSIAEmkKEFN0YXJ0QWN0aW9uQnlHZXQSKC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlIgASfgoXU3RhcnRBY3Rpb25CeUdldEFuZFdhaXQSLy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0GjAub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2UiABJeCg1SZXN0YXJ0QWN0aW9uEiUub2xpdmV0aW4uYXBpLnYxLlJlc3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJXCgpLaWxsQWN0aW9uEiIub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXF1ZXN0GiMub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXNwb25zZSIAEmYKD0V4ZWN1dGlvblN0YXR1cxInLm9saXZldGluLmFwaS52MS5FeGVjdXRpb25TdGF0dXNSZXF1ZXN0Gigub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1Jlc3BvbnNlIgASTgoHR2V0TG9ncxIfLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVxdWVzdBogLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVzcG9uc2UiABJgCg1HZXRBY3Rpb25Mb2dzEiUub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXF1ZXN0GiYub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXNwb25zZSIAEmwKEUdldEV4ZWN1dGlvblF1ZXVlEikub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVxdWVzdBoqLm9saXZldGluLmFwaS52MS5HZXRFeGVjdXRpb25RdWV1ZVJlc3BvbnNlIgASdQoUVmFsaWRhdGVBcmd1bWVudFR5cGUSLC5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Gi0ub2xpdmV0aW4uYXBpLnYxLlZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2UiABJLCgZXaG9BbUkSHi5vbGl2ZXRpbi5hcGkudjEuV2hvQW1JUmVxdWVzdBofLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXNwb25zZSIAElQKCVNvc1JlcG9ydBIhLm9saXZldGluLmFwaS52MS5Tb3NSZXBvcnRSZXF1ZXN0GiIub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlc3BvbnNlIgASUQoIRHVtcFZhcnMSIC5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXF1ZXN0GiEub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UiABJ4ChVEdW1wUHVibGljSWRBY3Rpb25NYXASLS5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdBouLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZSIAElQKCUdldFJlYWR5ehIhLm9saXZldGluLmFwaS52MS5HZXRSZWFkeXpSZXF1ZXN0GiIub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlc3BvbnNlIgASYwoOTG9jYWxVc2VyTG9naW4SJi5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkxvY2FsVXNlckxvZ2luUmVzcG9uc2UiABJdCgxQYXNzd29yZEhhc2gSJC5vbGl2ZXRpbi5hcGkudjEuUGFzc3dvcmRIYXNoUmVxdWVzdBolLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXNwb25zZSIAEksKBkxvZ291dBIeLm9saXZldGluLmFwaS52MS5Mb2dvdXRSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlc3BvbnNlIgASXAoLRXZlbnRTdHJlYW0SIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkV2ZW50U3RyZWFtUmVzcG9uc2UiADABEmMKDkdldERpYWdub3N0aWNzEiYub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVxdWVzdBonLm9saXZldGluLmFwaS52MS5HZXREaWFnbm9zdGljc1Jlc3BvbnNlIgASRQoESW5pdBIcLm9saXZldGluLmFwaS52MS5Jbml0UmVxdWVzdBodLm9saXZldGluLmFwaS52MS5Jbml0UmVzcG9uc2UiABJpChBHZXRBY3Rpb25CaW5kaW5nEigub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkJpbmRpbmdSZXF1ZXN0Gikub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkJpbmRpbmdSZXNwb25zZSIAEloKC0dldEVudGl0aWVzEiMub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5HZXRFbnRpdGllc1Jlc3BvbnNlIgASSQoJR2V0RW50aXR5EiEub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0eVJlcXVlc3QaFy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5IgBCOFo2Z2l0aHViLmNvbS9PbGl2ZVRpbi9PbGl2ZVRpbi9nZW4vb2xpdmV0aW4vYXBpL3YxO2FwaXYxYgZwcm90bzM"); + fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLABAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBAgASgIEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcCJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiXwoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCBIbChNzaG93X3ZlcnNpb25fbnVtYmVyGAMgASgIIk0KE0dldERhc2hib2FyZFJlcXVlc3QSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCSJRCglEYXNoYm9hcmQSDQoFdGl0bGUYASABKAkSNQoIY29udGVudHMYAiADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50ItsBChJEYXNoYm9hcmRDb21wb25lbnQSDQoFdGl0bGUYASABKAkSDAoEdHlwZRgCIAEoCRI1Cghjb250ZW50cxgDIAMoCzIjLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmRDb21wb25lbnQSDAoEaWNvbhgEIAEoCRIRCgljc3NfY2xhc3MYBSABKAkSJwoGYWN0aW9uGAYgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhITCgtlbnRpdHlfdHlwZRgHIAEoCRISCgplbnRpdHlfa2V5GAggASgJIpQBChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkSFQoNanVzdGlmaWNhdGlvbhgEIAEoCSIyChNTdGFydEFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAkiNAoTU3RhcnRBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkifgoZU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSFQoNanVzdGlmaWNhdGlvbhgDIAEoCSJKChpTdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiLAoXU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIjkKGFN0YXJ0QWN0aW9uQnlHZXRSZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkiMwoeU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSJPCh9TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJeCg5HZXRMb2dzUmVxdWVzdBIUCgxzdGFydF9vZmZzZXQYASABKAMSEwoLZGF0ZV9maWx0ZXIYAiABKAkSEQoJcGFnZV9zaXplGAMgASgDEg4KBmZpbHRlchgEIAEoCSLbAwoITG9nRW50cnkSGAoQZGF0ZXRpbWVfc3RhcnRlZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSDgoGb3V0cHV0GAMgASgJEhEKCXRpbWVkX291dBgFIAEoCBIRCglleGl0X2NvZGUYBiABKAUSDAoEdXNlchgHIAEoCRISCgp1c2VyX2NsYXNzGAggASgJEhMKC2FjdGlvbl9pY29uGAkgASgJEgwKBHRhZ3MYCiADKAkSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAsgASgJEhkKEWRhdGV0aW1lX2ZpbmlzaGVkGAwgASgJEhkKEWV4ZWN1dGlvbl9zdGFydGVkGA4gASgIEhoKEmV4ZWN1dGlvbl9maW5pc2hlZBgPIAEoCBIPCgdibG9ja2VkGBAgASgIEhYKDmRhdGV0aW1lX2luZGV4GBEgASgDEhAKCGNhbl9raWxsGBIgASgIEiMKG2RhdGV0aW1lX3JhdGVfbGltaXRfZXhwaXJlcxgTIAEoCRISCgpiaW5kaW5nX2lkGBQgASgJEg4KBnF1ZXVlZBgVIAEoCBIYChBxdWV1ZWRfZm9yX2dyb3VwGBYgASgJEhUKDWp1c3RpZmljYXRpb24YFyABKAkikQEKD0dldExvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIj8KFEdldEFjdGlvbkxvZ3NSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCRIUCgxzdGFydF9vZmZzZXQYAiABKAMilwEKFUdldEFjdGlvbkxvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIhoKGEdldEV4ZWN1dGlvblF1ZXVlUmVxdWVzdCLGAQoURXhlY3V0aW9uUXVldWVBY3Rpb24SEgoKYmluZGluZ19pZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSEwoLYWN0aW9uX2ljb24YAyABKAkSFgoObWF4X2NvbmN1cnJlbnQYBCABKAUSFAoMYWN0aXZlX2NvdW50GAUgASgFEhUKDWVudGl0eV9wcmVmaXgYBiABKAkSKgoHZW50cmllcxgHIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSLBAQoTRXhlY3V0aW9uUXVldWVHcm91cBIMCgRuYW1lGAEgASgJEgwKBGljb24YAiABKAkSFgoObWF4X2NvbmN1cnJlbnQYAyABKAUSFAoMYWN0aXZlX2NvdW50GAQgASgFEjYKB2FjdGlvbnMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVBY3Rpb24SFAoMcXVldWVkX2NvdW50GAYgASgFEhIKCnF1ZXVlX3NpemUYByABKAUiZwoZR2V0RXhlY3V0aW9uUXVldWVSZXNwb25zZRI0CgZncm91cHMYASADKAsyJC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVHcm91cBIUCgx0b3RhbF9hY3RpdmUYAiABKAUiZQobVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEgwKBHR5cGUYAiABKAkSEgoKYmluZGluZ19pZBgDIAEoCRIVCg1hcmd1bWVudF9uYW1lGAQgASgJIkIKHFZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2USDQoFdmFsaWQYASABKAgSEwoLZGVzY3JpcHRpb24YAiABKAkiNgoVV2F0Y2hFeGVjdXRpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSImChRXYXRjaEV4ZWN1dGlvblVwZGF0ZRIOCgZ1cGRhdGUYASABKAkiSgoWRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSEQoJYWN0aW9uX2lkGAIgASgJImEKGURhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCRIMCgRwYXRoGAQgASgJIo8BChdFeGVjdXRpb25TdGF0dXNSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSRgoSYmFja190b19kYXNoYm9hcmRzGAIgAygLMioub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQiDwoNV2hvQW1JUmVxdWVzdCJsCg5XaG9BbUlSZXNwb25zZRIaChJhdXRoZW50aWNhdGVkX3VzZXIYASABKAkSEQoJdXNlcmdyb3VwGAIgASgJEhAKCHByb3ZpZGVyGAMgASgJEgwKBGFjbHMYBCADKAkSCwoDc2lkGAUgASgJIhIKEFNvc1JlcG9ydFJlcXVlc3QiIgoRU29zUmVwb3J0UmVzcG9uc2USDQoFYWxlcnQYASABKAkiEQoPRHVtcFZhcnNSZXF1ZXN0IpUBChBEdW1wVmFyc1Jlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEkEKCGNvbnRlbnRzGAIgAygLMi8ub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UuQ29udGVudHNFbnRyeRovCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiOwoMRGVidWdCaW5kaW5nEhQKDGFjdGlvbl90aXRsZRgBIAEoCRIVCg1lbnRpdHlfcHJlZml4GAIgASgJIh4KHER1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QizgEKHUR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEk4KCGNvbnRlbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlLkNvbnRlbnRzRW50cnkaTgoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSLAoFdmFsdWUYAiABKAsyHS5vbGl2ZXRpbi5hcGkudjEuRGVidWdCaW5kaW5nOgI4ASISChBHZXRSZWFkeXpSZXF1ZXN0IiMKEUdldFJlYWR5elJlc3BvbnNlEg4KBnN0YXR1cxgBIAEoCSIUChJFdmVudFN0cmVhbVJlcXVlc3QimQMKE0V2ZW50U3RyZWFtUmVzcG9uc2USPQoOZW50aXR5X2NoYW5nZWQYAiABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFbnRpdHlDaGFuZ2VkSAASPQoOY29uZmlnX2NoYW5nZWQYAyABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRDb25maWdDaGFuZ2VkSAASRQoSZXhlY3V0aW9uX2ZpbmlzaGVkGAQgASgLMicub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uRmluaXNoZWRIABJDChFleGVjdXRpb25fc3RhcnRlZBgFIAEoCzImLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvblN0YXJ0ZWRIABI5CgxvdXRwdXRfY2h1bmsYBiABKAsyIS5vbGl2ZXRpbi5hcGkudjEuRXZlbnRPdXRwdXRDaHVua0gAEjQKCWhlYXJ0YmVhdBgHIAEoCzIfLm9saXZldGluLmFwaS52MS5FdmVudEhlYXJ0YmVhdEgAQgcKBWV2ZW50IkEKEEV2ZW50T3V0cHV0Q2h1bmsSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBm91dHB1dBgCIAEoCSIUChJFdmVudEVudGl0eUNoYW5nZWQiFAoSRXZlbnRDb25maWdDaGFuZ2VkIhAKDkV2ZW50SGVhcnRiZWF0IkYKFkV2ZW50RXhlY3V0aW9uRmluaXNoZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IkUKFUV2ZW50RXhlY3V0aW9uU3RhcnRlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiMgoRS2lsbEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJIm0KEktpbGxBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGa2lsbGVkGAIgASgIEhkKEWFscmVhZHlfY29tcGxldGVkGAMgASgIEg0KBWZvdW5kGAQgASgIIjsKFUxvY2FsVXNlckxvZ2luUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCSIpChZMb2NhbFVzZXJMb2dpblJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgiJwoTUGFzc3dvcmRIYXNoUmVxdWVzdBIQCghwYXNzd29yZBgBIAEoCSIkChRQYXNzd29yZEhhc2hSZXNwb25zZRIMCgRoYXNoGAEgASgJIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UiFwoVR2V0RGlhZ25vc3RpY3NSZXF1ZXN0IkUKFkdldERpYWdub3N0aWNzUmVzcG9uc2USEwoLU3NoRm91bmRLZXkYASABKAkSFgoOU3NoRm91bmRDb25maWcYAiABKAkiDQoLSW5pdFJlcXVlc3Qi6wUKDEluaXRSZXNwb25zZRISCgpzaG93Rm9vdGVyGAEgASgIEhYKDnNob3dOYXZpZ2F0aW9uGAIgASgIEhcKD3Nob3dOZXdWZXJzaW9ucxgDIAEoCBIYChBhdmFpbGFibGVWZXJzaW9uGAQgASgJEhYKDmN1cnJlbnRWZXJzaW9uGAUgASgJEhEKCXBhZ2VUaXRsZRgGIAEoCRIeChZzZWN0aW9uTmF2aWdhdGlvblN0eWxlGAcgASgJEhoKEmRlZmF1bHRJY29uRm9yQmFjaxgIIAEoCRIWCg5lbmFibGVDdXN0b21KcxgJIAEoCBIUCgxhdXRoTG9naW5VcmwYCiABKAkSFgoOYXV0aExvY2FsTG9naW4YCyABKAgSEQoJc3R5bGVNb2RzGAwgAygJEjgKD29BdXRoMlByb3ZpZGVycxgNIAMoCzIfLm9saXZldGluLmFwaS52MS5PQXV0aDJQcm92aWRlchI4Cg9hZGRpdGlvbmFsTGlua3MYDiADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWRkaXRpb25hbExpbmsSFgoOcm9vdERhc2hib2FyZHMYDyADKAkSGgoSYXV0aGVudGljYXRlZF91c2VyGBAgASgJEiMKG2F1dGhlbnRpY2F0ZWRfdXNlcl9wcm92aWRlchgRIAEoCRI6ChBlZmZlY3RpdmVfcG9saWN5GBIgASgLMiAub2xpdmV0aW4uYXBpLnYxLkVmZmVjdGl2ZVBvbGljeRIWCg5iYW5uZXJfbWVzc2FnZRgTIAEoCRISCgpiYW5uZXJfY3NzGBQgASgJEhgKEHNob3dfZGlhZ25vc3RpY3MYFSABKAgSFQoNc2hvd19sb2dfbGlzdBgWIAEoCBIWCg5sb2dpbl9yZXF1aXJlZBgXIAEoCBIYChBhdmFpbGFibGVfdGhlbWVzGBggAygJEiQKHHNob3dfbmF2aWdhdGVfb25fc3RhcnRfaWNvbnMYGSABKAgiLAoOQWRkaXRpb25hbExpbmsSDQoFdGl0bGUYASABKAkSCwoDdXJsGAIgASgJIjoKDk9BdXRoMlByb3ZpZGVyEg0KBXRpdGxlGAEgASgJEgwKBGljb24YAyABKAkSCwoDa2V5GAQgASgJIi0KF0dldEFjdGlvbkJpbmRpbmdSZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkiiwEKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0IhQKEkdldEVudGl0aWVzUmVxdWVzdCJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uImkKEEVudGl0eURlZmluaXRpb24SDQoFdGl0bGUYASABKAkSKgoJaW5zdGFuY2VzGAIgAygLMhcub2xpdmV0aW4uYXBpLnYxLkVudGl0eRIaChJ1c2VkX29uX2Rhc2hib2FyZHMYAyADKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMtYTChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASbAoRR2V0RXhlY3V0aW9uUXVldWUSKS5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2UiABJ1ChRWYWxpZGF0ZUFyZ3VtZW50VHlwZRIsLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QaLS5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZSIAEksKBldob0FtSRIeLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlc3BvbnNlIgASVAoJU29zUmVwb3J0EiEub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuU29zUmVwb3J0UmVzcG9uc2UiABJRCghEdW1wVmFycxIgLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1JlcXVlc3QaIS5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZSIAEngKFUR1bXBQdWJsaWNJZEFjdGlvbk1hcBItLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Gi4ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlIgASVAoJR2V0UmVhZHl6EiEub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVzcG9uc2UiABJjCg5Mb2NhbFVzZXJMb2dpbhImLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXNwb25zZSIAEl0KDFBhc3N3b3JkSGFzaBIkLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlc3BvbnNlIgASSwoGTG9nb3V0Eh4ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVzcG9uc2UiABJcCgtFdmVudFN0cmVhbRIjLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXNwb25zZSIAMAESYwoOR2V0RGlhZ25vc3RpY3MSJi5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVzcG9uc2UiABJFCgRJbml0Ehwub2xpdmV0aW4uYXBpLnYxLkluaXRSZXF1ZXN0Gh0ub2xpdmV0aW4uYXBpLnYxLkluaXRSZXNwb25zZSIAEmkKEEdldEFjdGlvbkJpbmRpbmcSKC5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlIgASWgoLR2V0RW50aXRpZXMSIy5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVzcG9uc2UiABJJCglHZXRFbnRpdHkSIS5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXR5UmVxdWVzdBoXLm9saXZldGluLmFwaS52MS5FbnRpdHkiAEI4WjZnaXRodWIuY29tL09saXZlVGluL09saXZlVGluL2dlbi9vbGl2ZXRpbi9hcGkvdjE7YXBpdjFiBnByb3RvMw"); /** * Describes the message olivetin.api.v1.Action. @@ -17,516 +17,530 @@ export const file_olivetin_api_v1_olivetin = /*@__PURE__*/ export const ActionSchema = /*@__PURE__*/ messageDesc(file_olivetin_api_v1_olivetin, 0); +/** + * Describes the message olivetin.api.v1.ActionGroupMembership. + * Use `create(ActionGroupMembershipSchema)` to create a new message. + */ +export const ActionGroupMembershipSchema = /*@__PURE__*/ + messageDesc(file_olivetin_api_v1_olivetin, 1); + /** * Describes the message olivetin.api.v1.ActionWebhookExecHint. * Use `create(ActionWebhookExecHintSchema)` to create a new message. */ export const ActionWebhookExecHintSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 1); + messageDesc(file_olivetin_api_v1_olivetin, 2); /** * Describes the message olivetin.api.v1.ActionArgument. * Use `create(ActionArgumentSchema)` to create a new message. */ export const ActionArgumentSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 2); + messageDesc(file_olivetin_api_v1_olivetin, 3); /** * Describes the message olivetin.api.v1.ActionArgumentChoice. * Use `create(ActionArgumentChoiceSchema)` to create a new message. */ export const ActionArgumentChoiceSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 3); + messageDesc(file_olivetin_api_v1_olivetin, 4); /** * Describes the message olivetin.api.v1.Entity. * Use `create(EntitySchema)` to create a new message. */ export const EntitySchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 4); + messageDesc(file_olivetin_api_v1_olivetin, 5); /** * Describes the message olivetin.api.v1.GetDashboardResponse. * Use `create(GetDashboardResponseSchema)` to create a new message. */ export const GetDashboardResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 5); + messageDesc(file_olivetin_api_v1_olivetin, 6); /** * Describes the message olivetin.api.v1.EffectivePolicy. * Use `create(EffectivePolicySchema)` to create a new message. */ export const EffectivePolicySchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 6); + messageDesc(file_olivetin_api_v1_olivetin, 7); /** * Describes the message olivetin.api.v1.GetDashboardRequest. * Use `create(GetDashboardRequestSchema)` to create a new message. */ export const GetDashboardRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 7); + messageDesc(file_olivetin_api_v1_olivetin, 8); /** * Describes the message olivetin.api.v1.Dashboard. * Use `create(DashboardSchema)` to create a new message. */ export const DashboardSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 8); + messageDesc(file_olivetin_api_v1_olivetin, 9); /** * Describes the message olivetin.api.v1.DashboardComponent. * Use `create(DashboardComponentSchema)` to create a new message. */ export const DashboardComponentSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 9); + messageDesc(file_olivetin_api_v1_olivetin, 10); /** * Describes the message olivetin.api.v1.StartActionRequest. * Use `create(StartActionRequestSchema)` to create a new message. */ export const StartActionRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 10); + messageDesc(file_olivetin_api_v1_olivetin, 11); /** * Describes the message olivetin.api.v1.StartActionArgument. * Use `create(StartActionArgumentSchema)` to create a new message. */ export const StartActionArgumentSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 11); + messageDesc(file_olivetin_api_v1_olivetin, 12); /** * Describes the message olivetin.api.v1.StartActionResponse. * Use `create(StartActionResponseSchema)` to create a new message. */ export const StartActionResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 12); + messageDesc(file_olivetin_api_v1_olivetin, 13); /** * Describes the message olivetin.api.v1.StartActionAndWaitRequest. * Use `create(StartActionAndWaitRequestSchema)` to create a new message. */ export const StartActionAndWaitRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 13); + messageDesc(file_olivetin_api_v1_olivetin, 14); /** * Describes the message olivetin.api.v1.StartActionAndWaitResponse. * Use `create(StartActionAndWaitResponseSchema)` to create a new message. */ export const StartActionAndWaitResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 14); + messageDesc(file_olivetin_api_v1_olivetin, 15); /** * Describes the message olivetin.api.v1.StartActionByGetRequest. * Use `create(StartActionByGetRequestSchema)` to create a new message. */ export const StartActionByGetRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 15); + messageDesc(file_olivetin_api_v1_olivetin, 16); /** * Describes the message olivetin.api.v1.StartActionByGetResponse. * Use `create(StartActionByGetResponseSchema)` to create a new message. */ export const StartActionByGetResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 16); + messageDesc(file_olivetin_api_v1_olivetin, 17); /** * Describes the message olivetin.api.v1.StartActionByGetAndWaitRequest. * Use `create(StartActionByGetAndWaitRequestSchema)` to create a new message. */ export const StartActionByGetAndWaitRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 17); + messageDesc(file_olivetin_api_v1_olivetin, 18); /** * Describes the message olivetin.api.v1.StartActionByGetAndWaitResponse. * Use `create(StartActionByGetAndWaitResponseSchema)` to create a new message. */ export const StartActionByGetAndWaitResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 18); + messageDesc(file_olivetin_api_v1_olivetin, 19); /** * Describes the message olivetin.api.v1.GetLogsRequest. * Use `create(GetLogsRequestSchema)` to create a new message. */ export const GetLogsRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 19); + messageDesc(file_olivetin_api_v1_olivetin, 20); /** * Describes the message olivetin.api.v1.LogEntry. * Use `create(LogEntrySchema)` to create a new message. */ export const LogEntrySchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 20); + messageDesc(file_olivetin_api_v1_olivetin, 21); /** * Describes the message olivetin.api.v1.GetLogsResponse. * Use `create(GetLogsResponseSchema)` to create a new message. */ export const GetLogsResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 21); + messageDesc(file_olivetin_api_v1_olivetin, 22); /** * Describes the message olivetin.api.v1.GetActionLogsRequest. * Use `create(GetActionLogsRequestSchema)` to create a new message. */ export const GetActionLogsRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 22); + messageDesc(file_olivetin_api_v1_olivetin, 23); /** * Describes the message olivetin.api.v1.GetActionLogsResponse. * Use `create(GetActionLogsResponseSchema)` to create a new message. */ export const GetActionLogsResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 23); + messageDesc(file_olivetin_api_v1_olivetin, 24); /** * Describes the message olivetin.api.v1.GetExecutionQueueRequest. * Use `create(GetExecutionQueueRequestSchema)` to create a new message. */ export const GetExecutionQueueRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 24); + messageDesc(file_olivetin_api_v1_olivetin, 25); /** * Describes the message olivetin.api.v1.ExecutionQueueAction. * Use `create(ExecutionQueueActionSchema)` to create a new message. */ export const ExecutionQueueActionSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 25); + messageDesc(file_olivetin_api_v1_olivetin, 26); /** * Describes the message olivetin.api.v1.ExecutionQueueGroup. * Use `create(ExecutionQueueGroupSchema)` to create a new message. */ export const ExecutionQueueGroupSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 26); + messageDesc(file_olivetin_api_v1_olivetin, 27); /** * Describes the message olivetin.api.v1.GetExecutionQueueResponse. * Use `create(GetExecutionQueueResponseSchema)` to create a new message. */ export const GetExecutionQueueResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 27); + messageDesc(file_olivetin_api_v1_olivetin, 28); /** * Describes the message olivetin.api.v1.ValidateArgumentTypeRequest. * Use `create(ValidateArgumentTypeRequestSchema)` to create a new message. */ export const ValidateArgumentTypeRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 28); + messageDesc(file_olivetin_api_v1_olivetin, 29); /** * Describes the message olivetin.api.v1.ValidateArgumentTypeResponse. * Use `create(ValidateArgumentTypeResponseSchema)` to create a new message. */ export const ValidateArgumentTypeResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 29); + messageDesc(file_olivetin_api_v1_olivetin, 30); /** * Describes the message olivetin.api.v1.WatchExecutionRequest. * Use `create(WatchExecutionRequestSchema)` to create a new message. */ export const WatchExecutionRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 30); + messageDesc(file_olivetin_api_v1_olivetin, 31); /** * Describes the message olivetin.api.v1.WatchExecutionUpdate. * Use `create(WatchExecutionUpdateSchema)` to create a new message. */ export const WatchExecutionUpdateSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 31); + messageDesc(file_olivetin_api_v1_olivetin, 32); /** * Describes the message olivetin.api.v1.ExecutionStatusRequest. * Use `create(ExecutionStatusRequestSchema)` to create a new message. */ export const ExecutionStatusRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 32); + messageDesc(file_olivetin_api_v1_olivetin, 33); + +/** + * Describes the message olivetin.api.v1.DashboardNavigationTarget. + * Use `create(DashboardNavigationTargetSchema)` to create a new message. + */ +export const DashboardNavigationTargetSchema = /*@__PURE__*/ + messageDesc(file_olivetin_api_v1_olivetin, 34); /** * Describes the message olivetin.api.v1.ExecutionStatusResponse. * Use `create(ExecutionStatusResponseSchema)` to create a new message. */ export const ExecutionStatusResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 33); + messageDesc(file_olivetin_api_v1_olivetin, 35); /** * Describes the message olivetin.api.v1.WhoAmIRequest. * Use `create(WhoAmIRequestSchema)` to create a new message. */ export const WhoAmIRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 34); + messageDesc(file_olivetin_api_v1_olivetin, 36); /** * Describes the message olivetin.api.v1.WhoAmIResponse. * Use `create(WhoAmIResponseSchema)` to create a new message. */ export const WhoAmIResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 35); + messageDesc(file_olivetin_api_v1_olivetin, 37); /** * Describes the message olivetin.api.v1.SosReportRequest. * Use `create(SosReportRequestSchema)` to create a new message. */ export const SosReportRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 36); + messageDesc(file_olivetin_api_v1_olivetin, 38); /** * Describes the message olivetin.api.v1.SosReportResponse. * Use `create(SosReportResponseSchema)` to create a new message. */ export const SosReportResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 37); + messageDesc(file_olivetin_api_v1_olivetin, 39); /** * Describes the message olivetin.api.v1.DumpVarsRequest. * Use `create(DumpVarsRequestSchema)` to create a new message. */ export const DumpVarsRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 38); + messageDesc(file_olivetin_api_v1_olivetin, 40); /** * Describes the message olivetin.api.v1.DumpVarsResponse. * Use `create(DumpVarsResponseSchema)` to create a new message. */ export const DumpVarsResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 39); + messageDesc(file_olivetin_api_v1_olivetin, 41); /** * Describes the message olivetin.api.v1.DebugBinding. * Use `create(DebugBindingSchema)` to create a new message. */ export const DebugBindingSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 40); + messageDesc(file_olivetin_api_v1_olivetin, 42); /** * Describes the message olivetin.api.v1.DumpPublicIdActionMapRequest. * Use `create(DumpPublicIdActionMapRequestSchema)` to create a new message. */ export const DumpPublicIdActionMapRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 41); + messageDesc(file_olivetin_api_v1_olivetin, 43); /** * Describes the message olivetin.api.v1.DumpPublicIdActionMapResponse. * Use `create(DumpPublicIdActionMapResponseSchema)` to create a new message. */ export const DumpPublicIdActionMapResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 42); + messageDesc(file_olivetin_api_v1_olivetin, 44); /** * Describes the message olivetin.api.v1.GetReadyzRequest. * Use `create(GetReadyzRequestSchema)` to create a new message. */ export const GetReadyzRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 43); + messageDesc(file_olivetin_api_v1_olivetin, 45); /** * Describes the message olivetin.api.v1.GetReadyzResponse. * Use `create(GetReadyzResponseSchema)` to create a new message. */ export const GetReadyzResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 44); + messageDesc(file_olivetin_api_v1_olivetin, 46); /** * Describes the message olivetin.api.v1.EventStreamRequest. * Use `create(EventStreamRequestSchema)` to create a new message. */ export const EventStreamRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 45); + messageDesc(file_olivetin_api_v1_olivetin, 47); /** * Describes the message olivetin.api.v1.EventStreamResponse. * Use `create(EventStreamResponseSchema)` to create a new message. */ export const EventStreamResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 46); + messageDesc(file_olivetin_api_v1_olivetin, 48); /** * Describes the message olivetin.api.v1.EventOutputChunk. * Use `create(EventOutputChunkSchema)` to create a new message. */ export const EventOutputChunkSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 47); + messageDesc(file_olivetin_api_v1_olivetin, 49); /** * Describes the message olivetin.api.v1.EventEntityChanged. * Use `create(EventEntityChangedSchema)` to create a new message. */ export const EventEntityChangedSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 48); + messageDesc(file_olivetin_api_v1_olivetin, 50); /** * Describes the message olivetin.api.v1.EventConfigChanged. * Use `create(EventConfigChangedSchema)` to create a new message. */ export const EventConfigChangedSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 49); + messageDesc(file_olivetin_api_v1_olivetin, 51); /** * Describes the message olivetin.api.v1.EventHeartbeat. * Use `create(EventHeartbeatSchema)` to create a new message. */ export const EventHeartbeatSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 50); + messageDesc(file_olivetin_api_v1_olivetin, 52); /** * Describes the message olivetin.api.v1.EventExecutionFinished. * Use `create(EventExecutionFinishedSchema)` to create a new message. */ export const EventExecutionFinishedSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 51); + messageDesc(file_olivetin_api_v1_olivetin, 53); /** * Describes the message olivetin.api.v1.EventExecutionStarted. * Use `create(EventExecutionStartedSchema)` to create a new message. */ export const EventExecutionStartedSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 52); + messageDesc(file_olivetin_api_v1_olivetin, 54); /** * Describes the message olivetin.api.v1.KillActionRequest. * Use `create(KillActionRequestSchema)` to create a new message. */ export const KillActionRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 53); + messageDesc(file_olivetin_api_v1_olivetin, 55); /** * Describes the message olivetin.api.v1.KillActionResponse. * Use `create(KillActionResponseSchema)` to create a new message. */ export const KillActionResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 54); + messageDesc(file_olivetin_api_v1_olivetin, 56); /** * Describes the message olivetin.api.v1.LocalUserLoginRequest. * Use `create(LocalUserLoginRequestSchema)` to create a new message. */ export const LocalUserLoginRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 55); + messageDesc(file_olivetin_api_v1_olivetin, 57); /** * Describes the message olivetin.api.v1.LocalUserLoginResponse. * Use `create(LocalUserLoginResponseSchema)` to create a new message. */ export const LocalUserLoginResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 56); + messageDesc(file_olivetin_api_v1_olivetin, 58); /** * Describes the message olivetin.api.v1.PasswordHashRequest. * Use `create(PasswordHashRequestSchema)` to create a new message. */ export const PasswordHashRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 57); + messageDesc(file_olivetin_api_v1_olivetin, 59); /** * Describes the message olivetin.api.v1.PasswordHashResponse. * Use `create(PasswordHashResponseSchema)` to create a new message. */ export const PasswordHashResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 58); + messageDesc(file_olivetin_api_v1_olivetin, 60); /** * Describes the message olivetin.api.v1.LogoutRequest. * Use `create(LogoutRequestSchema)` to create a new message. */ export const LogoutRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 59); + messageDesc(file_olivetin_api_v1_olivetin, 61); /** * Describes the message olivetin.api.v1.LogoutResponse. * Use `create(LogoutResponseSchema)` to create a new message. */ export const LogoutResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 60); + messageDesc(file_olivetin_api_v1_olivetin, 62); /** * Describes the message olivetin.api.v1.GetDiagnosticsRequest. * Use `create(GetDiagnosticsRequestSchema)` to create a new message. */ export const GetDiagnosticsRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 61); + messageDesc(file_olivetin_api_v1_olivetin, 63); /** * Describes the message olivetin.api.v1.GetDiagnosticsResponse. * Use `create(GetDiagnosticsResponseSchema)` to create a new message. */ export const GetDiagnosticsResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 62); + messageDesc(file_olivetin_api_v1_olivetin, 64); /** * Describes the message olivetin.api.v1.InitRequest. * Use `create(InitRequestSchema)` to create a new message. */ export const InitRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 63); + messageDesc(file_olivetin_api_v1_olivetin, 65); /** * Describes the message olivetin.api.v1.InitResponse. * Use `create(InitResponseSchema)` to create a new message. */ export const InitResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 64); + messageDesc(file_olivetin_api_v1_olivetin, 66); /** * Describes the message olivetin.api.v1.AdditionalLink. * Use `create(AdditionalLinkSchema)` to create a new message. */ export const AdditionalLinkSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 65); + messageDesc(file_olivetin_api_v1_olivetin, 67); /** * Describes the message olivetin.api.v1.OAuth2Provider. * Use `create(OAuth2ProviderSchema)` to create a new message. */ export const OAuth2ProviderSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 66); + messageDesc(file_olivetin_api_v1_olivetin, 68); /** * Describes the message olivetin.api.v1.GetActionBindingRequest. * Use `create(GetActionBindingRequestSchema)` to create a new message. */ export const GetActionBindingRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 67); + messageDesc(file_olivetin_api_v1_olivetin, 69); /** * Describes the message olivetin.api.v1.GetActionBindingResponse. * Use `create(GetActionBindingResponseSchema)` to create a new message. */ export const GetActionBindingResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 68); + messageDesc(file_olivetin_api_v1_olivetin, 70); /** * Describes the message olivetin.api.v1.GetEntitiesRequest. * Use `create(GetEntitiesRequestSchema)` to create a new message. */ export const GetEntitiesRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 69); + messageDesc(file_olivetin_api_v1_olivetin, 71); /** * Describes the message olivetin.api.v1.GetEntitiesResponse. * Use `create(GetEntitiesResponseSchema)` to create a new message. */ export const GetEntitiesResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 70); + messageDesc(file_olivetin_api_v1_olivetin, 72); /** * Describes the message olivetin.api.v1.EntityDefinition. * Use `create(EntityDefinitionSchema)` to create a new message. */ export const EntityDefinitionSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 71); + messageDesc(file_olivetin_api_v1_olivetin, 73); /** * Describes the message olivetin.api.v1.GetEntityRequest. * Use `create(GetEntityRequestSchema)` to create a new message. */ export const GetEntityRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 72); + messageDesc(file_olivetin_api_v1_olivetin, 74); /** * Describes the message olivetin.api.v1.RestartActionRequest. * Use `create(RestartActionRequestSchema)` to create a new message. */ export const RestartActionRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 73); + messageDesc(file_olivetin_api_v1_olivetin, 75); /** * @generated from service olivetin.api.v1.OliveTinApiService diff --git a/frontend/resources/vue/components/ActionGroupLimitsLabel.vue b/frontend/resources/vue/components/ActionGroupLimitsLabel.vue new file mode 100644 index 0000000..da15c19 --- /dev/null +++ b/frontend/resources/vue/components/ActionGroupLimitsLabel.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/frontend/resources/vue/router.js b/frontend/resources/vue/router.js index a5d20f5..7710716 100644 --- a/frontend/resources/vue/router.js +++ b/frontend/resources/vue/router.js @@ -165,23 +165,19 @@ const router = createRouter({ }) // Navigation guard to update page title -router.beforeEach((to, from, next) => { +router.beforeEach((to) => { if (to.meta && to.meta.title) { const pageTitle = window.initResponse?.pageTitle || 'OliveTin' document.title = to.meta.title + " - " + pageTitle } - next() }) // Navigation guard for authentication (if needed) -router.beforeEach((to, from, next) => { - // Check if user is authenticated for protected routes - const isAuthenticated = window.isAuthenticated || true // Default to true for now +router.beforeEach((to) => { + const isAuthenticated = window.isAuthenticated ?? false if (to.meta.requiresAuth && !isAuthenticated) { - next('/login') - } else { - next() + return '/login' } }) diff --git a/frontend/resources/vue/utils/executionConditionCount.js b/frontend/resources/vue/utils/executionConditionCount.js new file mode 100644 index 0000000..c0d1b6d --- /dev/null +++ b/frontend/resources/vue/utils/executionConditionCount.js @@ -0,0 +1,32 @@ +function nonEmptyList (list) { + return Array.isArray(list) && list.length > 0 +} + +export function countExecutionConditions (action) { + if (!action) { + return 0 + } + + let count = 1 + + if (action.execOnStartup) { + count++ + } + if (nonEmptyList(action.execOnCron)) { + count++ + } + if (nonEmptyList(action.execOnFileCreatedInDir)) { + count++ + } + if (nonEmptyList(action.execOnFileChangedInDir)) { + count++ + } + if (action.execOnCalendarFile) { + count++ + } + if (nonEmptyList(action.execOnWebhooks)) { + count++ + } + + return count +} diff --git a/frontend/resources/vue/views/ActionDetailsView.vue b/frontend/resources/vue/views/ActionDetailsView.vue index fec73bb..4e8738e 100644 --- a/frontend/resources/vue/views/ActionDetailsView.vue +++ b/frontend/resources/vue/views/ActionDetailsView.vue @@ -1,12 +1,27 @@