This commit is contained in:
James Read 2026-06-19 16:11:28 +00:00 committed by GitHub
commit c0fb2e010e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
262 changed files with 10761 additions and 6390 deletions

View File

@ -52,12 +52,15 @@ jobs:
if: github.event_name != 'pull_request'
uses: actions/setup-node@v6.4.0
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Setup node
if: github.event_name == 'pull_request'
uses: actions/setup-node@v6.4.0
with:
node-version: '22'
- name: Setup Go
uses: actions/setup-go@v6

View File

@ -57,6 +57,12 @@ jobs:
cache: true
cache-dependency-path: 'service/go.mod'
- name: Setup Node
if: matrix.language == 'javascript'
uses: actions/setup-node@v4
with:
node-version: '22'
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3

View File

@ -31,5 +31,10 @@ jobs:
- name: service
run: make -wC service codestyle
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22'
- name: frontend
run: make -wC frontend codestyle

View File

@ -25,10 +25,20 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
- name: Install Antora toolchain
run: npm i antora@3.1.14 asciidoctor-kroki@0.18.1 @asciidoctor/tabs@1.0.0-beta.6
- name: Generate docs site (smoke)
run: npx antora local-antora-playbook-ci.yml --log-level info
trigger-docs-publish:
needs: antora
if: github.event_name == 'push' && github.ref == 'refs/heads/next'
runs-on: ubuntu-latest
steps:
- name: Trigger docs.olivetin.app publish
env:
GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
run: gh workflow run asciidoc.yml --repo OliveTin/docs.olivetin.app --ref main

3
.gitignore vendored
View File

@ -12,6 +12,8 @@ frontend/dist/
frontend/node_modules
custom-frontend
integration-tests/screenshots/
integration-tests/flakey-test-runs.log
integration-tests/flakey-test-runs.jsonl
.vscode/
webui/
server.log
@ -22,3 +24,4 @@ webui.dev
sessions.yaml
docs/build/
build/
.cursor

View File

@ -66,6 +66,14 @@ If you are looking for OliveTin's AI policy, you can find it in `AI.md`.
- Review the pull request template at `.github/PULL_REQUEST_TEMPLATE.md`.
- When changing behaviour covered by a spec in `specs/`, ensure implementation and tests match the spec.
### Branch Naming
Use conventional-commit-style branch names with a type prefix, optional issue reference, and a short kebab-case description:
- `feat/[#123]-add-justification-prompt`
- `fix/[#456]-websocket-reconnect`
Do **not** use `feat-...`, `feature/...`, or other variants. Omit the `[#<issue>]` segment only when there is no linked issue.
### Troubleshooting
- API tests failing with content-type errors: ensure Connect handler is served under `/api/` and the client targets that base URL.
- Executor panics: check for nil `Binding/Action` and add guards in step functions.

View File

@ -42,6 +42,8 @@ EXPOSE 1337/tcp
COPY config.yaml /config
COPY var/entities/* /config/entities/
COPY examples/backupScript.sh /opt/backupScript.sh
RUN chmod 755 /opt/backupScript.sh
VOLUME /config
ARG TARGETPLATFORM

View File

@ -35,6 +35,8 @@ EXPOSE 1337/tcp
COPY config.yaml /config
COPY var/entities/* /config/entities/
COPY examples/backupScript.sh /opt/backupScript.sh
RUN chmod 755 /opt/backupScript.sh
VOLUME /config
COPY OliveTin /usr/bin/OliveTin

View File

@ -41,15 +41,20 @@ The following notes might be helpful when reporting a vulnerability:
It is incredibly useful to not just patch security vulnerabilities, but also to understand how they were found. If you are able to share this information, it can help us and the community to better understand potential attack vectors and improve the overall security of the project.
## Duplicate reports
If you are reporting via GitHub Security Advisories, search existing [repository advisories](https://github.com/OliveTin/OliveTin/security/advisories) for the same component and attack path before filing. Maintainers may close duplicate submissions and continue work on a single canonical advisory; duplicate reporters are still credited.
Maintainers: see [.github/SECURITY_ADVISORY_DUPLICATES.md](.github/SECURITY_ADVISORY_DUPLICATES.md) for known duplicate pairs, triage steps, and OAuth2 issues that are easy to confuse with each other.
## Process
Once a vulnerability is reported, the process is;
* Check [.github/SECURITY_ADVISORY_DUPLICATES.md](.github/SECURITY_ADVISORY_DUPLICATES.md) and open advisories for duplicates before accepting.
* Accept or reject the report, and communicate with the reporter about next steps.
* If accepted, patch using a temporary branch, and code review will be requested from the original reporter if they are interested.
* The severity of the vulnerability will be assessed using CVSS, and the patch will be prioritised accordingly.
* Once the patch is ready, it will be queued for a release onto the `next` branch (3k) or `release/2k` branch (2k)
* The reporter will be credited in the advisory and the release notes, but not the commit message.
* The commit message will contain a reference to the CVSS score (eg: MED) and the advisory ID.

View File

@ -12,8 +12,29 @@ logLevel: "INFO"
# 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
# then you can use either a python script or the `gpio` command.
- title: Toggle GPIO light
shell: gpioset gpiochip1 9=1 || true # The "|| true" is to ignore errors the demo environment doesn't have GPIO access.
icon: light
# Lots of people also use OliveTin to monitor their servers, like checking
# disk space, or checking logs. `onclick: execution-dialog` shows output.
- title: Check disk space
icon: disk
shell: df -h /
onclick: execution-dialog
# This uses `onclick: execution-dialog` to show a dialog with more
# information about the command that was run.
- title: Check shell history
shell: cat ~/.bash_history
icon: logs
onclick: execution-dialog
# Every action can still be run on demand from the web UI or API. The keys
# below are optional *additional* triggers (see each action and
# https://docs.olivetin.app/action_execution/ ).
@ -26,62 +47,41 @@ actions:
- title: Ping the Internet
shell: ping -c 3 1.1.1.1
icon: ping
popupOnStart: execution-dialog-stdout-only
onclick: execution-dialog
# https://docs.olivetin.app/action_execution/onstartup.html
execOnStartup: true
# This uses `popupOnStart: execution-dialog-stdout-only` to simply show just
# the command output.
- title: Check disk space
icon: disk
shell: df -h /media
popupOnStart: execution-dialog-stdout-only
# https://docs.olivetin.app/action_execution/onfilechanged.html
# Create the directory first, e.g. mkdir -p /tmp/olivetin-demo-file-changed
execOnFileChangedInDir:
- /tmp/olivetin-demo-file-changed
# This uses `popupOnStart: execution-dialog` to show a dialog with more
# information about the command that was run.
- title: check dmesg logs
shell: dmesg | tail
icon: logs
popupOnStart: execution-dialog
# https://docs.olivetin.app/action_execution/oncron.html — second example;
# the "date" action uses @hourly elsewhere in this file.
execOnCron:
- "0 3 * * 0"
# This uses `popupOnStart: execution-button` to display a mini button that
# links to the logs.
#
# You can also rate-limit actions too.
- title: date
shell: date
id: date
timeout: 6
icon: clock
popupOnStart: execution-button
- title: Sync Disks
shell: sync
id: syncdisks
icon: disk
onclick: execution-button
maxRate:
- limit: 3
duration: 1m
execOnCron:
- "@hourly"
# You are not limited to operating system commands, and of course you can run
# your own scripts. Here `maxConcurrent` stops the script running multiple
# times in parallel. There is also a timeout that will kill the command if it
# runs for too long.
# your own scripts. The backup-jobs action group limits how many backup-related
# actions can run at once; extra requests are queued instead of blocked.
# There is also a timeout that will kill the command if it runs for too long.
- title: Run backup script
shell: /opt/backupScript.sh
shellAfterCompleted: "apprise -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ output }} '"
maxConcurrent: 1
groups: [ backup-jobs ]
timeout: 10
icon: backup
popupOnStart: execution-dialog
onclick: execution-dialog
# https://docs.olivetin.app/action_execution/oncalendar.html
execOnCalendarFile: examples/demo-olivetin-calendar.yaml
- title: Verify backup archive
shell: sleep 3 && echo "Backup archive verified"
groups: [ backup-jobs ]
timeout: 30
icon: backup
onclick: execution-dialog
# When you want to prompt users for input, that is when you should use
# `arguments` - this presents a popup dialog and asks for argument values.
#
@ -91,7 +91,7 @@ actions:
shell: ping {{ host }} -c {{ count }}
icon: ping
timeout: 100
popupOnStart: execution-dialog-stdout-only
onclick: history
# https://docs.olivetin.app/action_execution/onwebhook.html — POST to /webhooks
# with header X-OliveTin-Demo: ping-host (path and payload rules are documented).
execOnWebhook:
@ -133,7 +133,8 @@ actions:
# Docs: https://docs.olivetin.app/args/input_confirmation.html
- title: Delete old backups
icon: ashtonished
shell: rm -rf /opt/oldBackups/
justification: true
shell: rm -rf /opt/oliveTinOldBackups/ && sleep 5
arguments:
- type: html
title: Description
@ -171,7 +172,7 @@ actions:
- title: "Setup easy SSH"
icon: ssh
shell: olivetin-setup-easy-ssh
popupOnStart: execution-dialog
onclick: execution-dialog
# Second webhook example: POST /webhooks?demo=setup-ssh
execOnWebhook:
- matchQuery:
@ -188,13 +189,6 @@ actions:
timeout: 1
shell: ssh -F /config/ssh/easy.cfg root@server1 'service httpd restart'
# 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
# then you can use either a python script or the `gpio` command.
- title: Toggle GPIO light
shell: gpioset gpiochip1 9=1
icon: light
# There are several built-in shortcuts for the `icon` option, but you
# can also just specify any HTML, this includes any unicode character,
# or a <img = "..." /> link to a custom icon.
@ -259,6 +253,15 @@ actions:
entity: container
triggers: ["Update container entity file"]
- title: Long running action
shell: sleep 300
timeout: 300
icon: logs
onclick: execution-dialog
groups: [ con2queue10 ]
execOnCron:
- "@hourly"
# Lastly, you can hide actions from the web UI, this is useful for creating
# background helpers that execute only on startup or a cron, for updating
# entity files.
@ -300,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.
#

View File

@ -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

2
docs/modules/ROOT/images/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
**/custom-webui
**/__pycache__

View File

@ -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/<topic>/<name>/
├── screenshots.ini # batch capture config for repo-helper
├── config.yaml # minimal OliveTin config for this screenshot only
├── setup_<variant>.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::<topic>/<name>/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/<topic>/<name>
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. `<datalist>` 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/<topic>/<name>/` 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::<topic>/<name>/<png>[]`.
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 `<page.adoc>`.
>
> - Put everything in `docs/modules/ROOT/images/<topic>/<name>/`: `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.

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -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

View File

@ -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

View File

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -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: <strong>{{ server.name }}</strong>
IP Address: <strong>{{ server.ip }}</strong>
- title: '{{ server.name }} Wake on Lan'
- title: '{{ server.name }} Power Off'

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -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

View File

@ -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

View File

@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -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 ]

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -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 }} <br /><br /><strong>{{ container.State }}</strong>
- title: 'Start {{ container.Names }}'
- title: 'Stop {{ container.Names }}'

View File

@ -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"}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,2 @@
custom-webui/
__pycache__/

View File

@ -0,0 +1,2 @@
CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
include ../../screenshots.mk

View File

@ -0,0 +1,30 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:11337
logLevel: "WARN"
checkForUpdates: false
showFooter: false
actions:
- title: get pods
icon: <iconify-icon icon="pajamas:pod"></iconify-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: <iconify-icon icon="pajamas:clear-all"></iconify-icon>
shell: echo "deployment.apps/postgres restarted"
- title: evacuate node
icon: <iconify-icon icon="pajamas:rocket-launch"></iconify-icon>
shell: echo "node/{{ NodeName }} cordoned and drained"
arguments:
- name: NodeName
type: ascii_identifier
choices:
- value: node1
- value: node2
- value: node3

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Some files were not shown because too many files have changed in this diff Show More