Merge branch 'next' into wip

This commit is contained in:
James Read 2026-06-20 01:27:37 +01:00 committed by GitHub
commit d23fb2fd77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
536 changed files with 26555 additions and 7481 deletions

View File

@ -1,82 +0,0 @@
version: 2
updates:
# npm updates for frontend - targeting "next" branch
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
# npm updates for frontend - targeting "release/2k" branch
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 10
# npm updates for integration-tests - targeting "next" branch
- package-ecosystem: "npm"
directory: "/integration-tests"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
# npm updates for integration-tests - targeting "release/2k" branch
- package-ecosystem: "npm"
directory: "/integration-tests"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 10
# Go modules updates for service - targeting "next" branch
- package-ecosystem: "gomod"
directory: "/service"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
# Go modules updates for service - targeting "release/2k" branch
- package-ecosystem: "gomod"
directory: "/service"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 10
# Go modules updates for lang - targeting "next" branch
- package-ecosystem: "gomod"
directory: "/lang"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
# Go modules updates for lang - targeting "release/2k" branch
- package-ecosystem: "gomod"
directory: "/lang"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 10
# Docker updates - targeting "next" branch
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
# Docker updates - targeting "release/2k" branch
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 10

View File

@ -3,6 +3,16 @@ name: "Build & Release pipeline"
on:
pull_request:
paths:
- '.github/workflows/build-and-release.yml'
- '.goreleaser.yml'
- 'Dockerfile.multiarches'
- 'Dockerfile.singlearch'
- 'Makefile'
- 'frontend/**'
- 'integration-tests/**'
- 'proto/**'
- 'service/**'
workflow_dispatch:
push:
tags:
@ -11,31 +21,49 @@ on:
- main
- next
- beta
paths:
- '.github/workflows/build-and-release.yml'
- '.goreleaser.yml'
- 'Dockerfile.multiarches'
- 'Dockerfile.singlearch'
- 'Makefile'
- 'frontend/**'
- 'integration-tests/**'
- 'proto/**'
- 'service/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
image: tonistiigi/binfmt:latest
platforms: arm64,arm
- name: Setup node
uses: actions/setup-node@v4
- name: Setup node (npm cache)
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@v5
uses: actions/setup-go@v6
with:
go-version-file: 'service/go.mod'
cache: true
@ -45,13 +73,15 @@ jobs:
run: go version
- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_KEY }}
- name: Login to ghcr
uses: docker/login-action@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -74,7 +104,7 @@ jobs:
run: cd integration-tests && make -w
- name: Archive integration tests
uses: actions/upload-artifact@v4.3.1
uses: actions/upload-artifact@v7
if: always()
with:
name: "OliveTin-integration-tests-${{ env.DATE }}-${{ github.sha }}"
@ -83,12 +113,17 @@ jobs:
!integration-tests/node_modules
- name: Install goreleaser
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
uses: goreleaser/goreleaser-action@v6
with:
install-only: true
- name: Set up Docker Buildx
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
uses: docker/setup-buildx-action@v3
- name: release
if: github.ref_type != 'tag'
if: github.ref_type != 'tag' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
uses: cycjimmy/semantic-release-action@v4
with:
extra_plugins: |
@ -100,8 +135,8 @@ jobs:
GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
- name: Archive binaries
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
uses: actions/upload-artifact@v4.3.1
with:
name: "OliveTin-snapshot-${{ env.DATE }}-${{ github.sha }}"
path: dist/OliveTin*.*

View File

@ -15,13 +15,19 @@ name: "CodeQL"
on:
push:
paths:
- 'cmd/**'
- 'internal/**'
- 'webui.dev/**'
- '.github/workflows/codeql-analysis.yml'
- 'frontend/**'
- 'integration-tests/**'
- 'OliveTin.proto'
- 'proto/**'
- 'service/**'
branches: [main]
pull_request:
paths:
- '.github/workflows/codeql-analysis.yml'
- 'frontend/**'
- 'integration-tests/**'
- 'proto/**'
- 'service/**'
branches: [main]
schedule:
- cron: '25 10 * * 5'
@ -51,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

@ -4,11 +4,11 @@ name: "Codestyle checks"
on:
push:
paths:
- 'cmd/**'
- 'internal/**'
- 'webui.dev/**'
- '.github/workflows/codestyle.yml'
- 'frontend/**'
- 'integration-tests/**'
- 'OliveTin.proto'
- 'proto/**'
- 'service/**'
jobs:
@ -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

@ -7,8 +7,28 @@ name: DevSkim
on:
push:
paths:
- '.github/workflows/devskim.yml'
- '.goreleaser.yml'
- 'Dockerfile.multiarches'
- 'Dockerfile.singlearch'
- 'Makefile'
- 'frontend/**'
- 'integration-tests/**'
- 'proto/**'
- 'service/**'
branches: [ "main" ]
pull_request:
paths:
- '.github/workflows/devskim.yml'
- '.goreleaser.yml'
- 'Dockerfile.multiarches'
- 'Dockerfile.singlearch'
- 'Makefile'
- 'frontend/**'
- 'integration-tests/**'
- 'proto/**'
- 'service/**'
branches: [ "main" ]
schedule:
- cron: '34 21 * * 2'

44
.github/workflows/docs-antora.yml vendored Normal file
View File

@ -0,0 +1,44 @@
name: Antora docs
on:
push:
paths:
- 'docs/**'
- 'local-antora-playbook.yml'
- 'local-antora-playbook-ci.yml'
- '.github/workflows/docs-antora.yml'
pull_request:
paths:
- 'docs/**'
- 'local-antora-playbook.yml'
- 'local-antora-playbook-ci.yml'
- '.github/workflows/docs-antora.yml'
jobs:
antora:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v4
with:
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

7
.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
@ -19,4 +21,7 @@ OliveTin
integration-tests/configs/authRequireGuestsToLogin/sessions.yaml
webui
webui.dev
sessions.yaml
sessions.yaml
docs/build/
build/
.cursor

View File

@ -76,8 +76,9 @@ archives:
- README.md
- src: Dockerfile.singlearch
dst: Dockerfile
- examples/backupScript.sh
- webui
- ./var/
- var
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}"
wrap_in_directory: true
format_overrides:
@ -103,6 +104,7 @@ dockers_v2:
- var/entities/
- config.yaml
- var/helper-actions/
- examples/backupScript.sh
labels:
org.opencontainers.image.revision: "{{ .FullCommit }}"
org.opencontainers.image.version: "{{ .Tag }}"

View File

@ -9,6 +9,19 @@ repos:
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-merge-conflict
- id: detect-private-key
- id: mixed-line-ending
args: ['--fix', 'lf']
- id: check-json
exclude: |
(?x)^(
service/internal/entities/testdata/.*\.json|
integration-tests/tests/.*/entities/.*\.json|
var/entities/.*\.json
)$
- id: check-case-conflict
- id: detect-aws-credentials
# Alternative semantic commit checker
- repo: https://github.com/compilerla/conventional-pre-commit
@ -34,9 +47,23 @@ repos:
pass_filenames: false
always_run: true
- id: it
name: it
entry: make service-codestyle frontend-codestyle
- id: service-unittests
name: service-unittests
entry: make service-unittests
language: system
pass_filenames: false
always_run: true
- id: service-build
name: service-build
entry: make service
language: system
pass_filenames: false
always_run: true
- id: it
name: integration-tests
entry: make it
language: system
pass_filenames: false
always_run: true

View File

@ -13,12 +13,15 @@ If you are looking for OliveTin's AI policy, you can find it in `AI.md`.
- **Frontend (Vue 3)**: `frontend/` (served by the service)
- **Integration tests**: `integration-tests/`
- **Protos/Generated**: `proto/`, `service/gen/...`
- **Specs**: `specs/` — Markdown specs that define how code should behave in human-readable form. When changing behavior in a spec-covered area, keep implementation and tests aligned with the spec; do not reference code or symbols in specs (English only).
### How to Run
- Run the server (dev):
- From repo root: `go run ./service`
- Unit tests (Go):
- From repo root: `cd service && make unittests`
- Code style (after editing code in `service/`):
- From repo root: `cd service && make codestyle`
- Integration tests (Mocha + Selenium):
- Single test: `cd integration-tests && npx --yes mocha test/general.mjs`
- All tests: `cd integration-tests && npx --yes mocha`
@ -41,6 +44,7 @@ If you are looking for OliveTin's AI policy, you can find it in `AI.md`.
- Do not swallow errors; propagate or log meaningfully.
- Match existing formatting; avoid unrelated reformatting.
- Be safe around nils in executor steps (e.g., guard `req.Binding` and `req.Binding.Action`).
- Cyclomatic complexity over 4 is not permitted.
### API and Execution Flow (High-level)
1. Client calls Connect RPC (e.g., `Init`, `GetDashboard`, `StartAction`).
@ -59,11 +63,18 @@ If you are looking for OliveTin's AI policy, you can find it in `AI.md`.
### Contributing Checklist
- Review the contributing guidelines at `CONTRIBUTING.adoc`.
- Review the AI guidance in `AI.md`.
- Review the pull request template at `.github/PULL_REQUEST_TEMPLATE.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.
- Integration timeouts: wait for `loaded-dashboard` and use selectors matching the Vue UI.

View File

@ -58,6 +58,7 @@ make
The project layout is reasonably straightforward;
* See the `Makefile` for common targets. This project was originally created on top of Fedora, but it should be usable on Debian/your faveourite distro with minor changes (if any).
* End-user documentation (AsciiDoc for link:https://docs.olivetin.app[docs.olivetin.app]) lives in `docs/` as an Antora component; the published site is built from the separate link:https://github.com/OliveTin/docs.olivetin.app[docs.olivetin.app] repository.
* The API is defined in protobuf+Connect RPC - you will need to `make proto`.
* The Go daemon is built from the `cmd` and `internal` directories mostly.
* The webui is just a single page application with a bit of Javascript in the `webui` directory. This can happily be hosted on another webserver.

View File

@ -1,17 +1,17 @@
# Multi-arch Dockerfile for GoReleaser (dockers_v2).
# Base image :43 is used without arch suffix so the registry can supply the right
# Base image :44 is used without arch suffix so the registry can supply the right
# platform (manifest list). TARGETPLATFORM is set by BuildKit for COPY.
# For custom/local single-arch builds, use Dockerfile.singlearch instead.
ARG TARGETPLATFORM
FROM registry.fedoraproject.org/fedora-minimal:43 AS olivetin-tmputils
FROM registry.fedoraproject.org/fedora-minimal:44 AS olivetin-tmputils
RUN microdnf -y install dnf-plugins-core && \
dnf-3 config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && \
microdnf install -y docker-ce-cli docker-compose-plugin && microdnf clean all
FROM registry.fedoraproject.org/fedora-minimal:43
FROM registry.fedoraproject.org/fedora-minimal:44
LABEL org.opencontainers.image.source https://github.com/OliveTin/OliveTin
LABEL org.opencontainers.image.title OliveTin
@ -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

@ -1,10 +1,10 @@
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:43-x86_64 AS olivetin-tmputils
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:44-x86_64 AS olivetin-tmputils
RUN microdnf -y install dnf-plugins-core && \
dnf-3 config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && \
microdnf install -y docker-ce-cli docker-compose-plugin && microdnf clean all
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:43-x86_64
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:44-x86_64
LABEL org.opencontainers.image.source https://github.com/OliveTin/OliveTin
LABEL org.opencontainers.image.title OliveTin
@ -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

@ -10,6 +10,7 @@
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5050/badge)](https://bestpractices.coreinfrastructure.org/projects/5050)
[![Go Report Card](https://goreportcard.com/badge/github.com/Olivetin/OliveTin)](https://goreportcard.com/report/github.com/OliveTin/OliveTin)
[![AI Autonomy Level](https://img.shields.io/badge/AI%20Autonomy-Level%201%20of%205%20(assistance--only)-blue)](https://blog.jread.com/posts/ai-levels-of-autonomy-in-software-engineering/)
[OliveTin 2k to 3k upgrade guide](https://docs.olivetin.app/upgrade/2k3k.html)
</div>
@ -19,6 +20,8 @@
All documentation can be found at [docs.olivetin.app](https://docs.olivetin.app). This includes installation and usage guide, etc.
The AsciiDoc sources for that site live in this repository under [`docs/`](docs/) (Antora component). The [docs.olivetin.app](https://github.com/OliveTin/docs.olivetin.app) repository contains the Antora playbook, theme supplemental files, and the workflow that publishes GitHub Pages.
## Use cases
**Safely** give access to commands, for less technical people;

View File

@ -2,12 +2,59 @@
## Supported Versions
Currently, only the `main` branch is "supported".
The following branches are currently being supported with security updates:
| Version | Supported |
| ------- | ------------------ |
| `main` | :white_check_mark: |
| `main` (3k release branch) | :white_check_mark: - advisories will be published when patched in this branch |
| `release/2k` (2k release branch) | :white_check_mark: - receives security updates, but much slower |
To understand more about 2k vs 3k, see the following docs; https://docs.olivetin.app/upgrade/2k3k.html
## OliveTin *is* a remote code execution (RCE) "tool"
The very purpose of OliveTin is to allow users to execute commands remotely on a machine.
This means that, by design, OliveTin has much higher potential to be used for remote code execution (RCE), and any security vulnerabilities that do occur have the potential to be much more severe than in other types of software.
We hope that you understand that while the project goes to great aims to be safe, and mitigate, that security vulnerabilities are inevitable, as they are with all software of all sizes - like Kubernetes, the Kernel, etc - and OliveTin has substantially less resources than those projects.
With that being said, OliveTin tries to follow examples of best practice, so judge the project not on if/when it has security issues, but how security issues are responded to as the measure of quality.
This is why we take security very seriously, and why we encourage responsible disclosure practices when reporting vulnerabilities.
## Reporting a Vulnerability
Please email `contact@jread.com` for responsible disclosure. Accepted issues will be made public once patched, and you will be given credit.
Please use responsible disclosure practices when reporting a vulnerability. **You will receive full credit for your discovery**, and we will work with you to ensure that the issue is resolved as quickly as **possible**. Please note that only James Read has access to security issues at the moment, so please be patient and understanding if you do not receive an immediate response.
* **Option A (preferred)**: GitHub Security Advisories, which allows you to report a vulnerability privately and securely. Use this direct link to report privately: `https://github.com/OliveTin/OliveTin/security/advisories/new`. This allows you to provide details without making them public.
* **Option B**: Please email `contact@jread.com` for responsible disclosure.
The following notes might be helpful when reporting a vulnerability:
* OliveTin does not offer a bug bounty program.
* GitHub usernames are how we you will be credited for discoveries reported via GitHub, if using emails we'll ask for your preferred name/handle to credit you with.
* CVEs will be requested via GitHub Security Advisories when appropriate, but we do not guarantee that all vulnerabilities will receive CVEs, as this is determined on a case-by-case basis.
## Disclosure of how vulnerabilities were found
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

@ -6,14 +6,39 @@
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
# Choose from INFO (default), WARN and DEBUG
# Docs: https://docs.olivetin.app/advanced_configuration/logs.html
# Docs: https://docs.olivetin.app/advanced_configuration/logs.html
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/ ).
#
# This is the most simple action, it just runs the command and flashes the
# button to indicate status.
#
@ -22,47 +47,40 @@ 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
# 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
# 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
# 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.
@ -73,7 +91,12 @@ 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:
- matchHeaders:
X-OliveTin-Demo: ping-host
arguments:
- name: host
title: Host
@ -95,7 +118,7 @@ actions:
# Docs: https://docs.olivetin.app/solutions/container-control-panel/index.html
- title: Restart Docker Container
icon: restart
shell: docker restart {{ .CurrentEntity }}
shell: docker restart {{ container }}
arguments:
- name: container
title: Container name
@ -110,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
@ -124,7 +148,7 @@ actions:
#
# Docs: https://docs.olivetin.app/reference/reference_themes_for_users.html
- title: Get OliveTin Theme
exec:
exec:
- "olivetin-get-theme"
- "{{ themeGitRepo }}"
- "{{ themeFolderName }}"
@ -148,7 +172,11 @@ 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:
demo: setup-ssh
# Here's how to use SSH with the "easy" config, to restart a service on
# another server.
@ -161,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.
@ -215,6 +236,10 @@ actions:
- title: Ping All Servers
shell: "echo 'Ping all servers'"
icon: ping
# https://docs.olivetin.app/action_execution/onfilecreated.html
# mkdir -p /tmp/olivetin-demo-file-created
execOnFileCreatedInDir:
- /tmp/olivetin-demo-file-created
- title: Start {{ .CurrentEntity.Names }}
icon: box
@ -228,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.
@ -269,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.
#
@ -341,7 +386,7 @@ dashboards:
# Security - Authentication
# This setting effectively enables or disables guests.
# This setting effectively enables or disables guests.
# If set to "true", then users will have to login to do anything.
authRequireGuestsToLogin: false
@ -350,7 +395,7 @@ authRequireGuestsToLogin: false
# and JWT authentication which are documented separately.
#
# Docs: https://docs.olivetin.app/security/local.html
#
#
# How to get a hashed password:
# Docs: https://docs.olivetin.app/security/local.html#_get_a_argon2id_hashed_password
authLocalUsers:

15
docs/antora.yml Normal file
View File

@ -0,0 +1,15 @@
---
name: ROOT
title: OliveTin
version: ''
display_version: 'Version 3k'
start_page: index.adoc
asciidoc:
attributes:
source-language: asciidoc@
table-caption: false
toclevels: 2
nav:
- modules/ROOT/nav.adoc

View File

@ -0,0 +1,40 @@
#!/usr/bin/env python3
import glob
import re
nav_file = open('nav.adoc', 'r')
nav_string = nav_file.read()
adoc_files = glob.glob('pages/**/*.adoc', recursive=True)
filelist = dict()
for file in adoc_files:
with open(file, 'r') as handle:
content = handle.read()
matches = re.findall(r'<<(.*?),?([\w\- ]+)>>', content)
for match in matches:
m = match
if match[0] == "":
m = match[1]
else:
m = match[0]
if content.count("#" + m) != 1:
if file not in filelist:
filelist[file] = list()
filelist[file].append(m)
print("Files:", len(filelist))
for file in filelist.keys():
print(file)
for match in filelist[file]:
print("\t", match)

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
import glob
import re
adoc_files = glob.glob('pages/**/*.adoc', recursive=True)
filelist = list()
for file in adoc_files:
with open(file, 'r') as handle:
content = handle.read()
matches = re.findall('^= ', content, re.MULTILINE)
if len(matches) == 0:
filelist.append(file)
print("Files:", len(filelist))
for file in filelist:
print(file)

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
# find .adoc files that are not navigable from the nav.adoc file
import glob
nav_file = open('nav.adoc', 'r')
nav_string = nav_file.read()
adoc_files = glob.glob('pages/**/*.adoc', recursive=True)
unnavigable_files = []
for file in adoc_files:
filename = file.replace("pages/", "")
if filename not in nav_string:
unnavigable_files.append(filename)
unnavigable_files = sorted(unnavigable_files)
print("Unnavigable files:", len(unnavigable_files))
for file in unnavigable_files:
print(file)

View File

@ -0,0 +1,15 @@
actions:
- title: Unicode (emoji) alias icon
shell: echo "Hello!"
icon: smile
- title: Unicode (emoji) icon
shell: echo "Hello!"
icon: "&#128526;"
- title: Iconify Icon
icon: <iconify-icon icon="ant-design:bug-filled"></iconify-icon>
- title: HTML Image (jpg/png/gif/etc) icon
shell: echo "Hello!"
icon: '<img src = "custom-webui/icons/mrgreen.gif" style = "width: 1em;" />'

View File

@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: olivetin-config
data:
config.yaml: |
actions:
- title: "Hello world!"
shell: echo 'Hello World!'

View File

@ -0,0 +1,37 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: olivetin
spec:
replicas: 1
selector:
matchLabels:
app: olivetin
template:
metadata:
labels:
app: olivetin
spec:
containers:
- name: olivetin
image: docker.io/jamesread/olivetin:latest
ports:
- containerPort: 1337
volumeMounts:
- name: olivetin-config
mountPath: "/config"
readOnly: true
livenessProbe:
exec:
command:
- curl
- localhost:1337
initialDelaySeconds: 5
periodSeconds: 30
volumes:
- name: olivetin-config
configMap:
name: olivetin-config

View File

@ -0,0 +1,21 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: olivetin-ingress
spec:
defaultBackend:
service:
name: olivetin
port:
number: 1337
rules:
- host: olivetin.apps.ocp.teratan.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: olivetin
port:
number: 1337

View File

@ -0,0 +1,19 @@
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
olivetin:
container_name: olivetin
image: jamesread/olivetin
volumes:
- ./OliveTin:/config # replace host path or volume as needed
ports:
- "1337:1337"
restart: unless-stopped

View File

@ -0,0 +1,25 @@
server {
listen 443 ssl;
ssl_certificate "/etc/nginx/conf.d/server.crt";
ssl_certificate_key "/etc/nginx/conf.d/server.key";
access_log /var/log/nginx/ot.access.log main;
error_log /var/log/nginx/ot.error.log notice;
server_name olivetin.example.com;
location / {
proxy_pass http://localhost:1337/;
proxy_redirect http://localhost:1337/ http://localhost/OliveTin/;
}
location /websocket {
proxy_set_header Upgrade "websocket";
proxy_set_header Connection "upgrade";
proxy_pass http://localhost:1337/websocket;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
}

View File

@ -0,0 +1,48 @@
# This config has two actions which are applied to all "container" entities
# found in the entity file.
#
# Docs: http://localhost/docs.olivetin.app/docs/entities.html
actions:
- title: Start {{ container.Names }}
icon: box
shell: docker start {{ container.Names }}
entity: container
triggers:
- Update container entity file
- title: Stop {{ container.Names }}
icon: box
shell: docker stop {{ container.Names }}
entity: container
triggers:
- Update container entity file
# This is a hidden action, that is run on startup, and every 5 minutes, and
# when the above start/stop commands are run (see the `triggers` property).
- title: Update container entity file
shell: 'docker ps -a --format json > /etc/OliveTin/entities/containers.json'
hidden: true
execOnStartup: true
execOnCron: '*/5 * * * *'
# Docs: http://docs.olivetin.app/entities.html
entities:
- file: /etc/OliveTin/entities/containers.json
name: container
# The only way to properly use entities, are to use them with a `fieldset` on
# a dashboard.
dashboards:
# This is the second dashboard.
- 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"}

View File

@ -0,0 +1,32 @@
actions:
- title: check log directory
hidden: true
shell: |
function addDirectory {
COUNT=$(ls -l $1 | wc -l)
echo "- directory: $1" >> /etc/OliveTin/entities/directories.yaml
echo " count: $COUNT" >> /etc/OliveTin/entities/directories.yaml
}
truncate -s 0 /etc/OliveTin/entities/directories.yaml
addDirectory /var/log/
addDirectory /home/xconspirisist/logs
execOnStartup: true
execOnCron: "* * * * *"
- title: clean {{ log_directory.directory }} ({{log_directory.count }} files)
shell: |
echo "Removing all files in {{ log_directory.directory }}"
entity: log_directory
entities:
- name: log_directory
file: /etc/OliveTin/entities/directories.yaml
dashboards:
- title: Log Actions
contents:
- entity: log_directory
type: fieldset
contents:
- title: clean {{ log_directory.directory }} ({{log_directory.count }} files)

View File

@ -0,0 +1,28 @@
logLevel: "INFO"
actions:
- title: Turn heating up
icon: '&#128316;'
shell: /opt/heating.sh up
- title: Turn heating down
icon: '&#128317;'
shell: /opt/heating.sh down
entities:
- file: /etc/OliveTin/entities/heating.yaml
name: heating
dashboards:
- title: Heating Control Panel
contents:
- title: "{{ heater.title }}"
entity: heating
type: fieldset
contents:
- type: display
title: |
<span class = "icon">&#127777;</span> <br />{{ heating.temperature }}
- title: Turn heating up
- title: Turn heating down

View File

@ -0,0 +1,2 @@
- title: Main heater
temperature: 20 degrees

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

View File

@ -0,0 +1,35 @@
const myPassword = 'sekrit'
const domMain = document.getElementsByTagName('main')[0]
domMain.style.display = 'none'
const domPassword = document.createElement('input')
const domLogin = document.createElement('button')
function checkPassword () {
if (domPassword.value === myPassword) {
domMain.style.display = 'block'
domPassword.remove()
domLogin.remove()
} else {
window.alert('Incorrect password. Please try again.')
}
}
function setupPasswordForm () {
domPassword.setAttribute('type', 'password')
domPassword.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
checkPassword()
}
})
domLogin.innerText = 'Login'
domLogin.onclick = checkPassword
const domHeader = document.querySelector('header')
domHeader.appendChild(domPassword)
domHeader.appendChild(domLogin)
}
document.addEventListener('DOMContentLoaded', setupPasswordForm)

View File

@ -0,0 +1,36 @@
actions:
- title: Stop {{ systemd_unit.unit }}
shell: systemctl stop {{ systemd_unit.unit }}
icon: <iconify-icon icon="zondicons:hand-stop"></iconify-icon>
entity: systemd_unit
triggers:
- Update services file
- title: Start {{ systemd_unit.unit }}
shell: systemctl start {{ systemd_unit.unit }}
icon: <iconify-icon icon="ic:round-directions-run"></iconify-icon>
entity: systemd_unit
triggers:
- Update services file
- title: Update services file
shell: systemctl list-units -a -o json --no-pager | jq -c 'map(select (.unit | contains ("upsilon", "podman", "boot.mount"))) | .[]' > /etc/OliveTin/entities/systemd_units.json
hidden: true
execOnStartup: true
entities:
- file: /etc/OliveTin/entities/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 }}

View File

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

View File

@ -0,0 +1,10 @@
actions:
- title: WakeOnLan Server1
shell: ether-wake A8:5E:45:E4:FF:2A
icon: ping
- title: Install ether-wake on startup
shell: microdnf install -y net-tools
hidden: true
execOnStartup: true
timeout: 120

View File

@ -0,0 +1,6 @@
- title: WakeOnLan Server1
# The r0gger/docker-wake-on-lan is a minimal container for WOL
# that can be run on the host network.
# It is not required to run the OliveTin container on the host network.
shell: |
docker run --rm --name wake-on-lan --net=host -e MAC='A8:5E:45:E4:FF:2A' r0gger/docker-wake-on-lan

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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 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,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.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 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.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

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

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